JAVASCRIPT’IN TARIHÇESI

Promise -1 (Non-Blocking I/O)

Rest Supported Mobile/Web Frameworks, Process, Thread, Concurrency, Async, Sync

--

Bu yazıyı daha önceden yazmış olduğum Javascript’in Tarihçesi yazısının bir devamı olarak yazıyorum. Bir çok kavramı tek bir yazıda ele almanın yaratacağı karmaşıklıktan kaçmak için bu yönteme başvurdum. Bu yazılardaki amacım önceden Javascript’in varolan hangi özelliklerinin yetmediğini ve bu geliştirmeyle neyi hedeflediklerini anlatacağım.

Promise konusuna başlamadan önce biraz teknik konulardan bahsedeyim istedim. İlk önce temel kavramlardan bahsedip bir sonraki yazımda JS Promise, await, async kelimelerinin ne için kullanıldığını anlatmaya çalışacağım.

Bu yazıda Mobil ve Tarayıcıdan sunucuya giden HTTP istek ve cevaplarının nasıl sunucu tarafında asenkron hale getirildiğini anlatmaya çalışacağım ve bunlar için geliştirilen kütüphanelerden kısaca bahsedeceğim. Aşağıda anlattığım kısım işin sunucu kısmıdır.

Öncelikle günümüz Web Uygulama Mimarisi senkron istek-cevap üzerine kurgulanmıştır. Yani istemci’ den gelen bir istek sunucu tarafından işlenmesi tamamlandıktan daha sonra diğer isteğe geçilir.

Dilden dile farklılık göstersede Örneğin JAVA ortamında istekleri karşılamak için geliştirilmiş Servlet teknolojisinde her istek için bir Thread üretilir, bu isteğe cevap verilinceye kadar Thread başka bir iş yapmaz.

Tomcat, Jetty, Glassfish, Weblogic gibi web sunucularında istekleri karşılamak için açılabilecek Thread sayıları belli sayıları geçemez. Örneğin tek core bir işlemcide çalıştırılan bir tomcat makinesi aynı anda 256 tane istek için Thread açabilir.

Daha doğrusu Thread sayısını config dosyalarında sınırlandırılır. Bunun nedenide Thread sayısının fazlalaşması hem Belleği şişirir hem de Thread’ ler arası geçişlerin giderek maliyetli hale gelmesine sebep olur. Thread Dosya sistemine erişmek, DB Transaction yapmak veya Network’ten başka bir servisin cevabını bekliyorsa bu Thread işini bırakıp başka bir işe dönememesi Thread sayısının ve yükünün giderek artmasına sebep olur.

I/O işlemlerinin ne kadar maliyetli olduğunu aşağıdaki resimden görebiliriz. L1 Cache < L2 Cache < RAM < Disk < Network

I/O Maliyet Tablosu

İstemci ve istek sayısının çok olduğu Facebook, Twitter, LinkedIn, Paypal vb uygulamarda esas problemin istek-cevap(request-response) mantığındaki I/O bloklanmasından kaynaklandığını farkettmişler ve mevcut teknolojideki Thread problemini çözebilmek için Non Blocking I/O Framework ve kütüphaneler geliştirme başlamışlardır. Bu teknolojiye paralel olarak kodlama yöntemi sunucu tarafında Javascript(Node), Scala gibi daha scripting dillere dönüşmüş . Uygulama geliştirme şeklinin senkrondan → asenkron’a arka arkaya sıralı fonksiyon çağırımından → event-driven yapıya dönüşmüştür.

1. Non Blocking I/O

I/O nasıl çalışıyor ? Aşağıdaki resimde ortada Process/Thread yazan kısımda bir kişinin oturduğunu önünde ve arkasında bir kova olduğunu düşünelim.

Process/Thread Input → Output üretmesi

Öncelikle önündeki kovanın dolmasını bekliyor. Önündeki kova dolduktan sonra bu kovayı arkasındaki kovaya boşaltarak önündeki kovanın boşalmasını sağlıyor. Bu sırayı uzatarak arka arkaya başka kişiler ve kovalar ekleyerek bu süreci uzatabiliriz. Sonuçta kişi önündeki kovanın hızlı dolması durumunda az bekleyecek, uzun dolması durumunda çok bekleyecek.

Multiple Process

Sunucuların şekilde görüldüğü gibi çalıştığını düşünürsek. 20 istemci için 20 kişi kovaların dolmasını beklemesi gerekmektedir. Bunun yerine öndeki bir kişinin kovaların önünde gezmesi ve dolan kovaları arkadaki kovaları boşaltması 20 kişinin boşta çalışmasını önleyecektir.

JVM üzerinde Non-Blocking I/O destekleyen MINA ve Netty gibi network kütüphaneleri bulunmaktadır . Bu network kütüphanelerini kullanan daha üst seviye Play(Akka), Vertx, Reactor, Finangle gibi framework’ler bulunmaktadır.

Servlet’ de bu yapıyı desteklemesi için Servlet 3’de NIO Blocking desteği eklenmiştir. İstekleri saklayabileceğiniz bir AsyncContext mevcuttur. Gelen istekler işleninceye kadar bellekteki kuyruğa atılır. Ve tanımladığınız Thread’ ler bu istekleri kuyruktan çekerek işler. Bu sayede istek ne kadar çok olursa olsun Thread sayınızı sabit tutmanız mümkündür.

Bu konuda son dönemde popüler olan ortamlardan biriside Node.js dir. Chrome Browser Google tarafından Safari Webkit Engine forklanarak oluşturulmuş bir tarayıcıdır. Webkit Rendering engine iken, JS kodlarını işlemek için V8Engine kullanılmaktadır. V8 Engine JS kodlarını Event-Driven yönteminde, çok hızlı bir şekilde C++ kodlarına dönüştürerek farklı işletim ve donanımlar üzerinde çalışmasını sağlamaktadır.

Joyent firması’ da V8Engine JS motorunu tarayıcılarda değilde sunucularda kullanılabileceklerini düşünmüşler ve bunun üzerine sunucu kütüphaneleri Node.js ortamına taşıyarak Asenkron, Event-Driven ve Javascript ile kod yazılabilen bir ortam oluşturmuşlardır.

Node.js tüm yapı event ve callback ler üzerine kurgulanmıştır. Bir fonksiyon oluşturulup EventLoop atılır. Callback fonksiyonu çağrılması beklenir. Bundan dolayı normal senkron programlama yönteminden farklı bir şekilde düşünmeniz gerekmektedir. Çünkü bir alt satır çalıştığında üstteki satırın çalışıp çalışmadığı belli değildir.

Aşağıda görüldüğü gibi EventLoop yani işi yöneten kodunuzun ilerlediği kısım single thread çalışmakta ama diğer Worker iş atayıp, asenkron olarak işine devam edebiliyor. Bu sayede I/O bloklaması yaşamadan programı işletmeye devam ediyor.

Node.JS Processing Model

2. Concurrency/Paralel Arasında Nasıl Bir Fark Bulunur ?

Eşzamanlı çalışma ve parallel çalışma arasındaki temel fark aslında Parallel sizin birden fazla task aynı anda çalıştırabilmenizdir. Bunun için multi-core işlemcilere ihtiyacınız vardır. Eğer tek bir işlemciniz varsa birden fazla uygulamayı nasıl aynı anda çalıştırıyoruz. Multi-tasking yani bu süre içerisinde işlemci processor iş bölümlemesi yaparak birden fazla uygulamayı örneğin Spotify, Tarayıcı, Word vb parça parça zaman vererek çalıştırır. Bu işlemi çok hızlı yaptığı için son kullanıcılar bu durumu hissetmez. Hatta alt tarafta işletim sistemi seviyesinde bir çok task bu scheduler tarafından çalıştırılır. Özetle CPU süre paylaşımını gerçekleştirir.

Concurrency , Parallel

İşlemci(CPU) hangi işlemi önce yapacağına nasıl karar verir ?

CPU

CPU yani fiziksel işlemciler kendilerine verilen işlemleri gerçekleştirmekten sorumludur. Birden fazla işlem var ise bunlardan hangisinin önce çalışacağına, sırasının ne olacağına karar verme işlemi İşletim Sistemleri tarafından gerçekleştirilir (Windows, MacOS, Linux, Unix, Android iOS vb)

İşletim sisteminin yaptığı bu işleme CPU Scheduling veya Processor Scheduling işlemi verilir. Bu işlemi gerçekleştirebilmek için Scheduling Algoritmalarını kullanır ve bir takım hedefler gözetilerek bu algoritmalar çalıştırılır.

  • Fairness : İşlemler arası adaletli bir dağıtım
  • Policy Enforcement: Sistem politika ve kurallarının doğru şekilde çalışmasının sağlanması
  • Efficiency: Sistem bileşenlerinin ve işlemcinin en etkin şekilde kullanılması
  • Response Time: Kullanıcı etkileşimde çok hızlı cevap dönebilmesi
  • Turnaround: İşlem üzerinde uzun zaman beklemesini minimize etmesi.
  • Throughput: İşlemin bitmeden diğer işleme geçişi, yani bir iş bitmeden işlemden işleme atlama olayını minimize etmesi

Scheduling algoritmaları temel’de ikiye ayrılır. Preemptive ve NonPreemtive Yani işlemin CPU tarafından alınıp bitirilinceye kadar bırakılmadığı NonPreemtive, bir diğeride işlemin bitmeden diğer işlemlere geçebildiği Preemtive algoritmalardır.

Scheduling Algoritmaları

A. Batch Sistemler

  • FCFS (First Come First Server)
  • Shortest Job First

B. Interaktif Sistemler

  • Round Robin
  • Priority Scheduling
  • Multi Level Queue Scheduling
  • Multi Level Feedback Queue Scheduling

3. Process/Thread

Process işletim sistemi seviyesinde bir uygulama başlattığınızda gördüşünüz işlemciler gibi düşünebilirsiniz. komut satırısın ps -a yazdığınızda hangi processlerin hangi id ile çalıştığını görebilirsiniz.

Yukarıda bahsettiğimiz scheduler bu process zaman vererek tüm uygulamaların birlikte çalışmasına olanak sağlar. O process içerisinde tek bir thread çalıştırılabileceği gibi SingleThread EventLoop’daki gibi. Java’daki Servlet içerisindeki gibi multi-thread yaklaşımlarda uygulanabilir. Multi-Thread yapının avantajı JVM tarafından optimize bir şekilde CPU nun kullanılması ve bir process içerisinde bir çok request karşılayabilmesi açısından avantajlıdır. Ama yukarıda bahsettiğim gibi istek sayısı çok attığında Thread Memory Space ve Stack Trace saklayıp kaldığı yerden context-switch yapma maliyeti çok artar bu gibi durumlarda Single Thread Event-Driven yaklaşımların daha performanslı çalıştığı görülmektedir.

Multi-Thread vs Single Thread

3. Senkron/Asenkron (Sync/Async)

Senkron yapıyı incelediğimizde process içerisindeki kod işletilirken hep sonraki satırın işletilmesini bekler. Örneğin dosyaya bir şey yazdırıyorsunuz, dosyaya yazma işlemini bekliyorsanız ProcessB işlemin bitmesini bekliyorsanız bu yapılara Senkron, eğerki beklemeden ProcessB görevi verip diğer işlemleri yapmaya devam ediyorsanız. ProcessB cevap geldiğinde o işe o zaman bakıyorsanız bu tip çalışma mantıklarınada async denir.

Sync vs Async

Tabiki sync kodun yazılması , çalıştırılması , async göre hem daha kolay hemde daha beklendiği gibi çalışır. Ama Asenkron kodun okunabilirliği , debug etmesi ve testi de zordur.

Not: Burda bahsettiğimiz durum bir bilgisayarın içerinde gerçekleşiyor. Bunu daha büyük mimarilerde de görebiliriz. Önceden Tek bir sunucu ve tek bir veri tabanı varken, bunun yerine artık daha dağıtık sistemler, arada kuyruk yapıları, kuyruktaki işleri eriten worker’lar, birden fazla veritabanı , object storage vb dağıtık sistemler mevcut ve tüm sistemler asenkron çalışma üzerine kurulu, transactional yapılar yavaş yavaş asenkron ve yukarıdaki gibi dağıtık yapılara geçiyor.

Okumaya Devam Et 😃

Bu yazının devamı veya yazı grubundaki diğer yazılara erişmek için bu linke tıklayabilirsiniz.

--

--