Ders 16 / 25
İşçi Süreçleri
İşin mesajdan ayrı bir ölçekleme birimi olarak ele alınması: iş havuzunun kurulması, işçi sayısının işlem hacmine etkisinin ölçülmesi, düşen işçinin gözetimle yerine konması ve zarif kapanma.
İçindekiler
Bir önceki ders mesajın kendisini dayanıklı hale getirmeyi tamamladı: deneme sayacı, üstel geri çekilme, sarsıntı ve ölü mektup kuyruğu. Artık bir mesaj kaybolmuyor, başarısız olduğunda yeniden deneniyor, düzelmiyorsa yalıtılıyor.
Bu güvencelerin tamamı mesajın kendisiyle ilgiliydi. Mesajı işleyen taraf ise hâlâ adsız bir “tüketici” olarak duruyor. Oysa o taraf bir süreçtir: belleği vardır, çöker, yeniden başlar, kapatılır. Kaç tane olacağı bir karardır ve bu karar servisin saatte kaç iş bitireceğini belirler. Bu ders tüketiciyi bir ölçekleme birimi (unit of scaling) olarak ele alır ve o birimin kaç tane olması gerektiğini ölçmeyi öğretir.
İşçi Nedir
İşçi (worker), kuyruktan iş alıp işleyen bağımsız bir çalıştırma birimidir. Ayrı bir süreç, ayrı bir iş parçacığı ya da ayrı bir kap olabilir; ortak yanı, kendi başına düşebilmesi ve kendi başına çoğaltılabilmesidir.
İşçiyi istek işleyen sunucudan ayıran üç özellik vardır. İşçinin bir istemcisi yoktur; kimse yanıtı beklemez, dolayısıyla iş uzun sürebilir. İşçinin iş kaynağı ağ dinleyicisi değil kuyruktur; yükü kabul etmeyi reddedemez, yalnız yavaş tüketebilir. Ve işçi yatay çoğaltılır: aynı kuyruktan besleneceği için ikinci bir işçi eklemek herhangi bir eşgüdüm gerektirmez.
Kütüphane ödünç servisinde arka plana taşınan işler bellidir: gecikme bildirimlerinin gönderilmesi, aylık ödünç raporunun üretilmesi, yüklenen kapak görselinin işlenmesi, şube stok özetinin yeniden hesaplanması. Bu derste ilkini örnek alıyoruz.
İşin Kendisi
İşçi süreci tek bir sorumluluk taşır: gelen bir iş tanımını çalıştırmak ve sonucu bildirmek. Kuyruğun nerede durduğunu, işin kaç kez denendiğini, kaç işçi olduğunu bilmez.
// isci.mjs — bir isi alir, yapar, sonucu bildirir; havuz.mjs onu fork eder if (!process.send) { console.log("bu dosya tek basina calismaz; havuz.mjs onu fork eder"); process.exit(0); } const SIRA = Number(process.argv[2]); // havuzun isciye verdigi sira numarasi const bekle = (ms) => new Promise((c) => setTimeout(c, ms)); // Gecikme bildirimi gonderimi: disariya bir cagri yapan, 100 ms suren is. async function bildirimGonder(is) { if (is.bozuk) process.exit(1); // is surecin kendisini dusuruyor await bekle(100); return `uye ${is.uyeId} bilgilendirildi`; } process.on("message", async (is) => { const sonuc = await bildirimGonder(is); process.send({ tur: "bitti", isId: is.isId, sira: SIRA, sonuc }); }); process.on("SIGTERM", () => process.exit(0)); // havuz kapatirken is yarida kalmaz
bu dosya tek basina calismaz; havuz.mjs onu fork eder
Dosya doğrudan çalıştırıldığında process.send tanımsızdır ve işçi bunu söyleyip çıkar.
Anlamlı çalıştığı tek durum, bir üst sürecin onu çatallamasıdır.
bildirimGonder içindeki 100 milisaniyelik bekleme, dışarıya yapılan bir çağrının
yerine geçer. İşin girdi–çıktı bağımlı (I/O-bound) olduğunu belirtir: süre işlemci
kullanılarak değil, cevabı beklenerek geçer. Bu ayrım birazdan işçi sayısı kararında
belirleyici olacak.
Havuz ve Çekme Temelli Dağıtım
İşleri işçilere bölüştürmenin iki yolu vardır. İtme temelli dağıtımda üst süreç işleri baştan paylaştırır: on iki iş, dört işçi, her birine üç iş. Bu yol yalnız işlerin süreleri eşitse verimlidir; biri uzun sürerse o işçi çalışırken diğerleri boş bekler. Çekme temelli (pull-based) dağıtımda ise işçi bir işi bitirdiğinde sıradakini ister. Yük kendiliğinden dengelenir.
Aşağıdaki havuz çekme temellidir: dagit her çağrıldığında kuyruğun başındaki işi
gönderir, kuyruk boşaldığında işçiyi kapatır.
// havuz.mjs — sabit sayida isci fork eder, is kuyrugunu onlara dagitir, sureyi olcer import { fork } from "node:child_process"; const ISLER = Array.from({ length: 12 }, (_, i) => ({ isId: i + 1, oduncId: 500 + i, uyeId: 100 + i, })); function havuzCalistir(isciSayisi, isler) { return new Promise((coz) => { const kuyruk = [...isler]; const bitenler = []; const isciler = []; const basla = performance.now(); const dagit = (isci) => { const is = kuyruk.shift(); if (is) isci.send(is); else isci.kill("SIGTERM"); }; for (let s = 1; s <= isciSayisi; s++) { const isci = fork(new URL("isci.mjs", import.meta.url), [String(s)]); isci.on("message", (ileti) => { bitenler.push(ileti); if (bitenler.length === isler.length) { const sure = performance.now() - basla; for (const i of isciler) i.kill("SIGTERM"); coz({ sure, bitenler }); } else dagit(isci); }); isciler.push(isci); dagit(isci); } }); } for (const n of [1, 2, 4]) { const { sure, bitenler } = await havuzCalistir(n, ISLER); const dilim = Math.floor(sure / 100); // her is 100 ms surer; dilim = 100 ms console.log(`isci=${n} is=${bitenler.length} sure=${dilim} dilim`); }
isci=1 is=12 sure=12 dilim isci=2 is=12 sure=6 dilim isci=4 is=12 sure=3 dilim
Süre, tek bir işin süresi olan 100 milisaniyelik dilimler cinsinden yazdırılıyor; böylece sonuç makinenin hızından bağımsız okunabilir hale gelir. Dilim sayısı aşağı yuvarlanır, dolayısıyla süreç başlatma gideri sayıya yansımaz. Yavaş bir makinede dilim sayıları bir üste kayabilir.
Okunacak şey oran: on iki iş bir işçiyle on iki dilim, iki işçiyle altı, dört işçiyle üç dilim sürüyor. İşçi sayısı iki katına çıktığında süre yarıya iniyor. Bu, işin bölünebilir olmasının doğrudan sonucudur: bir üyeye bildirim göndermek başka bir üyeye bildirim göndermeyi beklemez.
Ölçeklemenin Sınırı
Oranın sonsuza kadar sürmediği iki nokta vardır.
Birincisi işin türüdür. Yukarıdaki iş girdi–çıktı bağımlıydı: işçi çoğu zaman cevabı bekliyordu, o yüzden işçi sayısı çekirdek sayısını aşsa da kazanç sürdü. İş işlemci bağımlı (CPU-bound) olsaydı — örneğin kapak görselinin yeniden boyutlandırılması — kazanç çekirdek sayısında durur, o noktadan sonra işçi eklemek yalnız bağlam değiştirme gideri üretirdi.
İkincisi paylaşılan kaynaktır. On iki işçi aynı veritabanı bağlantı havuzundan besleniyorsa darboğaz işçi sayısı değil bağlantı sayısıdır. Bildirim gönderimi tek bir dış servise gidiyorsa darboğaz o servisin kabul ettiği eşzamanlılıktır. İşçi sayısını artırmak bu iki durumda toplam süreyi kısaltmaz, yalnız beklemeyi işçilere dağıtır.
Bu yüzden işçi sayısı bir tahmin değil, ölçüm sonucudur: sayı artırılır, toplam süre ölçülür, süre düşmeyi bıraktığı yerde durulur.
Düşen İşçi ve Gözetim
İşçi bağımsız bir süreç olduğu için düşebilir: bellek biter, işletim sistemi onu sonlandırır, işlediği iş sürecin kendisini çökertir. Havuzun buna karşı iki görevi vardır. Düşen işçinin yerine yenisini koymak ve o işçinin üzerindeki işi kaybetmemek.
İkincisi kritik olandır. İşçi çökerken hiçbir şey bildirmez; havuz yalnız exit
olayını görür. O anda işçinin hangi işi taşıdığı bilinmiyorsa iş sessizce kaybolur. Bu
yüzden havuz her işçinin üzerindeki işi ayrı bir eşlemede tutar.
// dayanikli.mjs — isci dustugunde yerine yenisi konur, uzerindeki is geri kuyruga alinir import { fork } from "node:child_process"; const ISLER = [ { isId: 1, oduncId: 500, uyeId: 100 }, { isId: 2, oduncId: 501, uyeId: 101 }, { isId: 3, oduncId: 502, uyeId: 102, bozuk: true }, { isId: 4, oduncId: 503, uyeId: 103 }, { isId: 5, oduncId: 504, uyeId: 104 }, ]; const EN_COK_DENEME = 2; const kuyruk = [...ISLER]; const deneme = new Map(); // isId -> kac kez denendi const bitenler = [], oluMektup = []; const uzerindeki = new Map(); // isci -> o an isledigi is let acik = 0, sonrakiSira = 1; function isciYarat() { const isci = fork(new URL("isci.mjs", import.meta.url), [String(sonrakiSira++)]); acik++; isci.on("message", (m) => { uzerindeki.delete(isci); bitenler.push(m.isId); dagit(isci); }); isci.on("exit", () => { acik--; const is = uzerindeki.get(isci); // dolu ise: isci is uzerindeyken coktu if (is) { uzerindeki.delete(isci); if (deneme.get(is.isId) < EN_COK_DENEME) kuyruk.push(is); else oluMektup.push(is.isId); isciYarat(); // coken iscinin yerine yenisi } if (acik === 0) rapor(); }); dagit(isci); } function dagit(isci) { const is = kuyruk.shift(); if (!is) return isci.kill("SIGTERM"); deneme.set(is.isId, (deneme.get(is.isId) ?? 0) + 1); uzerindeki.set(isci, is); isci.send(is); } function rapor() { console.log("biten isler :", bitenler.sort((a, b) => a - b).join(", ")); console.log("olu mektup kuyrugu:", oluMektup.join(", ") || "-"); console.log("yaratilan isci :", sonrakiSira - 1); console.log("3 numarali deneme :", deneme.get(3)); } isciYarat(); isciYarat();
biten isler : 1, 2, 4, 5 olu mektup kuyrugu: 3 yaratilan isci : 4 3 numarali deneme : 2
Üç numaralı iş işçiyi çökertiyor. Havuz exit olayında işi geri kuyruğa alıyor ve
düşen işçinin yerine yenisini yaratıyor. İkinci deneme de aynı sonucu verdiği için iş
deneme sınırını aşıyor ve önceki derste tanıtılan ölü mektup kuyruğuna gidiyor. Diğer
dört iş etkilenmiyor; toplamda dört işçi yaratılıyor, ikisi başlangıçtaki, ikisi
çökenlerin yerine gelen.
Buradaki kalıbın adı gözetimdir (supervision): işi yapan birim ile onun yaşamını yöneten birim ayrılır. İşçi kendi hatasını onarmaya çalışmaz, çöker; havuz onarır. Bu ayrım işçinin kodunu belirgin ölçüde yalınlaştırır.
Gözetimin gizli bir maliyeti vardır. Geri kuyruğa alınan iş, çökme anında kısmen yapılmış olabilir: bildirim gönderilmiş, ancak gönderildiği kaydedilmemiş olabilir. Bu yüzden yeniden denenen her iş, Web API Tasarımı kursunda tanıtılan tekillik anahtarıyla korunmalıdır; aksi hâlde gözetim, üyeye iki bildirim gönderme pahasına dayanıklılık satın alır.
Düzgün Kapanma
Havuz işçiyi SIGTERM ile kapatıyor. İşçi de bu sinyali karşılayıp çıkıyor. Sinyalin
karşılanmaması durumunda süreç varsayılan davranışla anında sonlanır ve üzerindeki iş
yarıda kalır.
Zarif kapanma (graceful shutdown) üç adımdır: yeni iş almayı bırak, elindeki işi
bitir, sonra çık. Uygulamada üst süreç önce SIGTERM gönderir, işçinin bitirmesi için
sınırlı bir süre tanır, süre dolarsa SIGKILL ile zorlar. Bu ikinci sinyal işçi
tarafından karşılanamaz; bu yüzden tanınan sürenin en uzun işten büyük olması gerekir.
Dağıtım araçlarının işçi kaplarını durdururken izlediği sıra da budur. İşçinin sinyali karşılamaması, her yeniden dağıtımda yarıda kalmış iş üretir.
Özet
- İşçi, kuyruktan iş alıp işleyen bağımsız bir çalıştırma birimidir; kendi başına düşebilir ve kendi başına çoğaltılabilir olması onu bir ölçekleme birimi yapar.
- Çekme temelli dağıtımda işçi bir işi bitirdiğinde sıradakini ister; yük, işlerin süreleri eşit olmasa da dengelenir.
- İşçi sayısının kazancı ölçülerek bulunur: girdi–çıktı bağımlı işte çekirdek sayısının ötesine geçebilir, işlemci bağımlı işte orada durur, paylaşılan kaynak varsa çok daha erken durur.
- Gözetim, düşen işçinin yerine yenisini koyar ve üzerindeki işi geri kuyruğa alır; bunun bedeli işin kısmen yapılmış olma ihtimalidir, bu yüzden yeniden denenen iş tekillik anahtarıyla korunur.
- Zarif kapanma sırası yeni iş almayı bırakmak, elindekini bitirmek ve çıkmaktır; tanınan süre en uzun işten kısa olursa her kapanış yarım iş üretir.
Sonraki Adım
Bu derste işler dışarıdan geldi: havuz elindeki listeyi dağıttı. Arka plan işlerinin büyük bölümü ise dışarıdan gelmez, takvimden gelir. Gecikme bildirimi her sabah altıda, aylık ödünç raporu her ayın birinde, şube stok özeti her on beş dakikada bir üretilir. Bu işleri bir zaman ölçütüne bağlamak tek satırlık bir tanım gibi görünür, ama altında iki soru vardır: yineleme nasıl tanımlanır ve aynı görevin iki koşumu üst üste bindiğinde ne olur. Sonraki ders yineleyen görev tanımını kurar ve çakışmayı ölçerek önler.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.