Ders 04 / 16
Uygulama Çalışma Zamanı
Uygulamanın altındaki süreç modeli: olay döngüsünü bloklayan işin diğer istekleri ne kadar beklettiği ölçülür, iş parçacığı havuzunun sınırı gösterilir ve ölçekleme biriminin neden süreç olduğu aynı bağlantı noktasını paylaşan işçilerle sayılır.
İçindekiler
Önceki ders vekilin arkasındaki uygulama sunucusunu tek bir süreç olarak çalıştırdı ve sorusunu açık bıraktı: bir istek uzun sürdüğünde diğerlerine ne olur, aynı uygulamanın iki kopyası aynı bağlantı noktasını nasıl paylaşır, ölçekleme kararının birimi süreç müdür yoksa iş parçacığı mıdır? Bu ders o üç soruyu ölçerek yanıtlar.
Ölçüm için kütüphane ödünç servisinin iki ucu kullanılacak: üye parolasını doğrulayan pahalı bir uç ve katalogdan tek satır döndüren ucuz bir uç. İkisi aynı süreçte yaşadığında pahalı işin ucuz isteklere ne yaptığı, aradaki gecikmeyle görünür hâle gelir.
Çalışma Zamanı Neyi Sağlar
Çalışma zamanı (runtime), uygulama kodunun üstünde durduğu katmandır: kodu yürüten motoru, işletim sisteminin soket ve dosya arayüzlerine açılan kapıları ve bu ikisinin arasında işleri sıraya koyan olay döngüsünü (event loop) barındırır. Node.js Çalışma Zamanı kursunda kurulan model burada olduğu gibi geçerlidir; bu derste yeni olan, aynı modelin bir sunucu uygulamasının ölçekleme kararlarını nasıl belirlediğidir.
Model tek cümleyle şudur: uygulama kodu tek bir iş parçacığında (thread) çalışır. Bir istek işleyicisi çalışırken başka hiçbir işleyici çalışmaz. Olay döngüsü, bir işleyici bittikten sonra kuyruktaki sıradaki işi alır. Girdi-çıktı beklemeleri — disk okuma, ağ çağrısı, veritabanı sorgusu — bu kuyruğu meşgul etmez; çalışma zamanı beklemeyi işletim sistemine devreder ve sonuç geldiğinde geri çağrıyı kuyruğa koyar.
Bu modelin sonucu iki tanedir. Bekleyen iş ucuzdur: binlerce açık bağlantı tek süreçte tutulabilir, çünkü her biri yalnız bir soket ve bir geri çağrıdır. Buna karşılık hesaplayan iş pahalıdır: işlemciyi meşgul eden bir hesap, süresi boyunca kuyruğu kilitler.
Bloklayan İş: Bir İstek Diğerlerini Ne Kadar Bekletir
Aşağıdaki sunucu üye parolası doğrulamasını iki ayrı yoldan yapar. dogrula-bloklayan
anahtar türetmeyi doğrudan çağırır ve sonucu beklerken olay döngüsünü elinde tutar.
dogrula-havuzda aynı hesabı çalışma zamanının iş parçacığı havuzuna (thread pool)
verir ve döngüyü serbest bırakır. Hesabın kendisi ikisinde de aynıdır.
// tek-surec.mjs — odunc servisi tek surecte; ayni is hem bloklayan hem bloklamayan bicimde import { createServer } from "node:http"; import { scrypt, scryptSync } from "node:crypto"; const RAFLAR = [{ isbn: "978-0262033848", ad: "Algoritmalara Giris", raf: "R-12" }]; // Anahtar turetme maliyeti: yarim saniyenin uzerinde islemci isi uretecek kadar yuksek. const MALIYET = { N: 1 << 16, r: 8, p: 8, maxmem: 512 * 1024 * 1024 }; createServer((istek, yanit) => { yanit.sendDate = false; yanit.setHeader("content-type", "application/json; charset=utf-8"); const yol = new URL(istek.url, "http://yerel").pathname; if (yol === "/uye/dogrula-bloklayan") { const ozet = scryptSync("parola", "tuz", 32, MALIYET).toString("hex").slice(0, 16); return yanit.writeHead(200).end(JSON.stringify({ ozet, yol })); } if (yol === "/uye/dogrula-havuzda") { return scrypt("parola", "tuz", 32, MALIYET, (hata, anahtar) => { if (hata) return yanit.writeHead(500).end(JSON.stringify({ hata: "turetme" })); yanit.writeHead(200).end(JSON.stringify({ ozet: anahtar.toString("hex").slice(0, 16), yol })); }); } if (yol === "/kitaplar") { return yanit.writeHead(200).end(JSON.stringify({ kitaplar: RAFLAR })); } yanit.writeHead(404).end(JSON.stringify({ hata: "yol_yok" })); }).listen(8430, "127.0.0.1", () => console.log("tek surec 127.0.0.1:8430 pid=" + process.pid));
Ölçüm şu düzendedir: önce ağır istek gönderilir, elli milisaniye sonra dört hafif istek arkasından gelir. Hafif isteklerin kendi işi neredeyse yoktur; süreleri, yalnızca sıralarını beklemekten gelir.
#!/usr/bin/env bash # tek-surec.mjs'yi baslatir; agir istegin yaninda gelen dort hafif istegin suresini olcer. node tek-surec.mjs & sunucu=$! sleep 0.6 curl -sS -o /dev/null http://127.0.0.1:8430/kitaplar # isinma olc() { # $1 = agir ucun yolu pidler=() curl -sS -o /dev/null -w "agir %{time_total} s\n" "http://127.0.0.1:8430$1" > agir.txt & pidler+=($!) sleep 0.05 for i in 1 2 3 4; do curl -sS -o /dev/null -w "hafif-$i %{time_total} s\n" http://127.0.0.1:8430/kitaplar > "hafif-$i.txt" & pidler+=($!) done wait "${pidler[@]}" cat agir.txt hafif-1.txt hafif-2.txt hafif-3.txt hafif-4.txt } echo "--- agir is olay dongusunde: /uye/dogrula-bloklayan ---" olc /uye/dogrula-bloklayan echo "--- agir is is parcacigi havuzunda: /uye/dogrula-havuzda ---" olc /uye/dogrula-havuzda kill "$sunucu" rm -f agir.txt hafif-*.txt
tek surec 127.0.0.1:8430 pid=14674 --- agir is olay dongusunde: /uye/dogrula-bloklayan --- agir 0.635000 s hafif-1 0.581384 s hafif-2 0.581601 s hafif-3 0.580992 s hafif-4 0.581166 s --- agir is is parcacigi havuzunda: /uye/dogrula-havuzda --- agir 0.618827 s hafif-1 0.001071 s hafif-2 0.000954 s hafif-3 0.000756 s hafif-4 0.001232 s
Süreç kimliği ve süreler her çalıştırmada değişir; ağır işin mutlak süresi de makinenin işlemcisine bağlıdır. Değişmeyen, iki bloktaki hafif isteklerin süreleri arasındaki büyüklük farkıdır.
Birinci ölçümde hafif istekler yaklaşık 580 milisaniye beklemiştir. Kendi işleri bir milisaniyeden azdır; bekledikleri şey, ağır isteğin kalan süresidir. Dördü de neredeyse aynı anda tamamlanır, çünkü hepsi aynı engelin kalkmasını beklemiş ve engel kalkınca art arda işlenmiştir.
İkinci ölçümde ağır isteğin süresi değişmemiştir — hesap aynı hesaptır — ama hafif istekler bir milisaniyenin altında yanıtlanmıştır. Aradaki tek fark, hesabın nerede yapıldığıdır. Havuz sürümünde olay döngüsü hesabı bir iş parçacığına verip serbest kalmış, arada gelen istekleri normal hızında karşılamış, sonuç hazır olduğunda geri çağrıyı çalıştırmıştır.
Havuz da Sınırlıdır
İş parçacığı havuzu bloklayan işin çözümü değil, taşındığı yerdir. Havuzun iş parçacığı sayısı sabittir ve varsayılan değeri küçüktür. Havuza aynı anda kapasitesinden fazla iş verilirse fazlası kuyruğa girer; bu kez olay döngüsü değil, havuz doyar. Ölçümdeki tek ağır istek havuzu doldurmadığı için gecikme görünmemiştir.
Bu, ölçeklemenin ilk kuralını verir: bir sürecin işlemci kapasitesi sabittir. Hesabı döngüden havuza taşımak isteklerin birbirini beklemesini azaltır, ama sürecin toplam işlemci işini artırmaz. Saniyede yapılabilecek anahtar türetme sayısı, işi hangi iş parçacığının yaptığından bağımsız olarak makinenin çekirdek sayısıyla sınırlıdır.
Ölçekleme Birimi: Süreç
Tek bir süreç tek bir çekirdeği doyurabilir. Çok çekirdekli bir makinede kalan çekirdekleri kullanmanın yolu, uygulamanın birden çok kopyasını çalıştırmaktır. Küme (cluster) düzeninde bir birincil süreç (primary process) dinleyen soketi açar ve birkaç işçi süreç (worker process) çatallar; gelen bağlantılar işçiler arasında dağıtılır. İşçiler aynı bağlantı noktasını paylaşır, çünkü soketi açan taraf birincil süreçtir.
// kume.mjs — ayni baglanti noktasini paylasan uc isci surec; her surecin kendi sayaci var import cluster from "node:cluster"; import { createServer } from "node:http"; const ISCI_SAYISI = 3; if (cluster.isPrimary) { let kapaniyor = false; for (let i = 0; i < ISCI_SAYISI; i++) cluster.fork(); cluster.on("exit", (isci) => { // olen iscinin yerine yenisi if (kapaniyor) return; console.log(`isci oldu pid=${isci.process.pid}, yerine yenisi catallaniyor`); cluster.fork(); }); process.on("SIGTERM", () => { // isciler birincille birlikte iner kapaniyor = true; for (const isci of Object.values(cluster.workers)) isci.kill(); process.exit(0); }); console.log(`birincil pid=${process.pid}, isci sayisi=${ISCI_SAYISI}`); } else { let oduncSayaci = 0; // SURECE OZEL bellek: her iscide ayri createServer((istek, yanit) => { yanit.sendDate = false; yanit.setHeader("content-type", "application/json; charset=utf-8"); if (new URL(istek.url, "http://yerel").pathname === "/odunc") oduncSayaci++; yanit.writeHead(200).end(JSON.stringify({ pid: process.pid, oduncSayaci })); }).listen(8431, "127.0.0.1"); }
Sunucu her yola yanıt verir ve yanıtta kendi süreç kimliğini bildirir; yalnız /odunc yolu
sayacı artırır. Bu ayrım, dağılımı sayaç değerini değiştirmeden yoklamayı sağlar.
#!/usr/bin/env bash # kume.mjs'yi baslatir; 12 istegin isciler arasindaki dagilimini, sayaclarin ayrismasini # ve olen iscinin yerine yenisinin gelmesini gosterir. node kume.mjs & kume=$! sleep 0.8 echo "--- 12 odunc istegi: hangi isci kac kez yanitladi ---" for i in $(seq 12); do curl -sS http://127.0.0.1:8431/odunc; echo; done | grep -o '"pid":[0-9]*' | sort | uniq -c echo "--- ayni uce dort istek: her iscinin sayaci kendi belleginde ---" for i in 1 2 3 4; do curl -sS http://127.0.0.1:8431/odunc; echo; done echo "--- bir isci oldurulur, yerine yenisi gelir ---" kurban=$(curl -sS http://127.0.0.1:8431/durum | sed -n 's/.*"pid":\([0-9]*\).*/\1/p') echo "oldurulen isci pid=$kurban" kill -9 "$kurban" sleep 0.8 for i in $(seq 6); do curl -sS http://127.0.0.1:8431/durum; echo; done | grep -o '"pid":[0-9]*' | sort -u kill "$kume"
birincil pid=3068, isci sayisi=3
--- 12 odunc istegi: hangi isci kac kez yanitladi ---
4 "pid":3070
4 "pid":3071
4 "pid":3072
--- ayni uce dort istek: her iscinin sayaci kendi belleginde ---
{"pid":3072,"oduncSayaci":5}
{"pid":3070,"oduncSayaci":5}
{"pid":3071,"oduncSayaci":5}
{"pid":3072,"oduncSayaci":6}
--- bir isci oldurulur, yerine yenisi gelir ---
oldurulen isci pid=3070
isci oldu pid=3070, yerine yenisi catallaniyor
"pid":3071
"pid":3072
"pid":3098
Süreç kimlikleri her çalıştırmada başkadır. Sabit olan üç gözlemdir.
Dağılım eşittir. On iki istek üç işçiye dörder düşmüştür; birincil süreç bağlantıları sırayla dağıtmıştır. İstemci tarafında hiçbir şey değişmemiştir: aynı adrese, aynı bağlantı noktasına istek yapılmıştır. Kaç sürecin çalıştığı istemciye görünmez.
Ölen işçinin yerine yenisi gelir. Bir işçi zorla sonlandırıldığında birincil süreç bunu görür ve yeni bir işçi çatallar; son listede eski kimliklerden biri yoktur, yerine yeni bir kimlik vardır. Bu, sürecin ölçekleme biriminin yanı sıra yalıtım birimi olduğunu da gösterir: bir işçinin çökmesi diğerlerini etkilemez ve hizmet kesilmez.
Süreç Belleği Ölçekleme Sınırıdır
Çıktıdaki en öğretici satırlar sayaç satırlarıdır. On iki isteğin ardından her işçinin
sayacı dörttür; sonraki dört istek dağıtıldığında değerler 5, 5, 5 ve 6 olarak
okunur. Hiçbir işçi toplam sayıyı bilmez, çünkü sayaç süreç belleğinde yaşar ve süreçler
belleklerini paylaşmaz.
Kütüphane uygulamasında bunun karşılığı doğrudandır. Ödünç sayısı, oturum bilgisi, geçici rezervasyon listesi, hız sınırı sayacı — hepsi süreç belleğinde tutulduğunda ölçekleme anında bozulur. Üyenin oturum açtığı istek bir işçiye, sonraki isteği başka bir işçiye düşer ve ikinci işçi oturumu tanımaz.
Bunun sonucu, çalışma zamanı modelinin uygulama tasarımına koyduğu bağlayıcı kuraldır: süreç durumsuz olmalıdır. İstekler arasında yaşaması gereken her değer süreç dışına, paylaşılan bir depoya taşınır. Süreç yalnız o isteğin süresince yaşayan değerleri kendi belleğinde tutar. Bu kural sağlandığında süreç sayısını artırmak bir yapılandırma kararına iner; sağlanmadığında ikinci süreci başlatmak hatalara yol açar.
Aynı kural sürecin ölümlü olmasından da gelir. Ölen işçinin belleği onunla birlikte gider; yerine gelen işçi boş bellekle başlar. Süreç belleğinde tutulan bir değer, yalnız ölçekleme sırasında değil, her yeniden başlatmada kaybolur.
Özet
- Uygulama kodu tek bir iş parçacığında çalışır; bekleyen girdi-çıktı ucuzdur, işlemciyi meşgul eden hesap ise süresi boyunca kuyruğu kilitler.
- Ölçümde olay döngüsünü bloklayan yarım saniyelik bir hesap, yanında gelen dört hafif isteği yaklaşık 580 milisaniye bekletmiştir; aynı hesap iş parçacığı havuzuna verildiğinde hafif istekler bir milisaniyenin altında yanıtlanmıştır.
- İş parçacığı havuzu işi taşır, kapasiteyi artırmaz; bir sürecin işleyebileceği toplam işlemci işi sabittir.
- Ölçekleme birimi süreçtir: birincil süreç dinleyen soketi açar, işçi süreçler aynı bağlantı noktasını paylaşır, on iki istek üç işçiye dörder dağılmıştır ve ölen işçinin yerine yenisi çatallanmıştır.
- Süreçler bellek paylaşmadığı için süreç belleğindeki sayaç işçiler arasında ayrışır; istekler arasında yaşaması gereken durum süreç dışına, paylaşılan bir depoya taşınmalıdır.
Sonraki Adım
Bu ders sürecin dışını ölçtü: kaç kopya çalışır, istek hangi kopyaya düşer, kopyalar neyi paylaşmaz. Sürecin içinde ise tek bir isteğin geçtiği bir yol vardır ve bu yol henüz tek bir kutu gibi ele alındı. Bağlantı kabul edildiği andan yanıtın son baytı yazıldığı ana kadar kaç ayrı aşama vardır, her aşama ne kadar sürer ve bir aşamada verilen karar sonrakini nasıl kısıtlar? Sonraki ders isteğin yaşam döngüsünü aşamalara böler; her aşamaya zaman damgası koyup sırayı ve süreleri yazdırır.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.