Ders 19 / 25
Geri Basınç
Üretim hızının tüketim hızını aştığı durumun sonuçları: sınırsız kuyrukta bekleme süresinin sınırsız büyümesi, sınırlı kuyruğun üç politikasının benzetimle sayısal karşılaştırılması ve geri basıncın akış üzerinden ölçülerek üreticiye yayılması.
İçindekiler
Bir önceki ders tek bir uzun işi ele aldı: parçalara bölmek, ilerlemeyi kaydetmek ve iptal edilebilir kılmak yeterliydi. Gerçek bir arka planda ise işler tek tek gelmez, akar: her ödünç işlemi bir bildirim işi, her kapak yüklemesi bir görsel işleme işi doğurur.
İşlerin geliş hızı ile işçilerin bitirme hızı birbirinden bağımsız iki büyüklüktür. Eşit olmalarını sağlayan hiçbir mekanizma yoktur; geliş hızını istek trafiği, bitirme hızını işçi sayısı ve işin süresi belirler. Geliş hızı bitirme hızını aştığında kuyruk büyür. Bu ders o büyümenin ne ürettiğini ölçer ve sistemin bunu üreticiye nasıl bildirdiğini kurar.
Hız Uyuşmazlığı
Bir kuyruğa saniyede iş girip saniyede iş çıkıyorsa, kuyruk uzunluğu hızıyla değişir. olduğu sürece kuyruk boş kalır ve bekleme süresi işin kendi süresine yaklaşır. olduğu anda kuyruk her saniye kadar uzar ve bu büyüme kendiliğinden durmaz.
Büyümenin doğrudan sonucu bekleme süresidir. Kuyruk kuramının temel bağıntısı, kararlı bir kuyrukta ortalama uzunluk , varış hızı ve ortalama bekleme süresi arasında şunu söyler:
Bağıntı tersten okunduğunda asıl uyarıyı verir: . Kuyruk uzunluğu sınırsızsa bekleme süresi de sınırsızdır. Bellek dolmadan çok önce, kuyruktaki iş hâlâ işlense bile anlamını yitirmiş olur — dört saat geciken bir gecikme bildirimi, gönderilmemiş bildirimden farklı değildir.
Bu yüzden kuyruk uzunluğu bir kaynak sorunu değil, bir gecikme bütçesi sorunudur.
Sınırlı Kuyruk ve Üç Politika
Çözüm kuyruğu sınırlamaktır. Sınırlama tek başına bir soru doğurur: kuyruk doluyken gelen iş ne olacak. Üç yanıt vardır ve üçü de bir şeyden vazgeçer.
Engelleme: üretici kuyrukta yer açılana kadar bekletilir. Hiçbir iş kaybolmaz, ama yavaşlama üreticiye geçer — bu, dar anlamıyla geri basınçtır (backpressure). Yeniyi atma (tail drop): kuyruk doluyken gelen iş reddedilir. Eskiyi atma (head drop): kuyruğun başındaki en eski iş atılır, yeni iş sona eklenir.
Aşağıdaki benzetim dördünü de aynı yük altında koşturur: saniyede yüz iş üretiliyor, altmış iş tüketiliyor, pencere on saniye, sınırlı kuyruğun kapasitesi yüz.
// benzetim.mjs — uretim hizi tuketim hizini astiginda kuyrugun dort politikadaki davranisi const PENCERE = 10_000; // benzetim penceresi (ms) const URETIM = 100; // saniyede uretilen is const TUKETIM = 60; // saniyede tuketilen is const KAPASITE = 100; // sinirli kuyrugun kapasitesi function kosum(politika) { const kuyruk = []; // her oge: uretildigi an (ms) let uretilen = 0, teslim = 0, atilan = 0, bekleme = 0; let zirve = 0, enUzunBekleme = 0; let ub = 0, tb = 0; // birikimler tam sayi: her ms'de hiz kadar artar for (let t = 1; t <= PENCERE; t++) { ub += URETIM; while (ub >= 1000) { ub -= 1000; uretilen++; if (politika === "sinirsiz" || kuyruk.length < KAPASITE) kuyruk.push(t); else if (politika === "yeniyi-at") atilan++; else if (politika === "eskiyi-at") { kuyruk.shift(); atilan++; kuyruk.push(t); } else { uretilen--; ub += 1000; bekleme++; break; } // engelle: uretici bekler } tb += TUKETIM; while (tb >= 1000 && kuyruk.length) { tb -= 1000; enUzunBekleme = Math.max(enUzunBekleme, t - kuyruk.shift()); teslim++; } zirve = Math.max(zirve, kuyruk.length); } return { politika, uretilen, teslim, atilan, bekleme, zirve, kalan: kuyruk.length, enUzunBekleme }; } const s3 = (n) => String(n).padStart(6); console.log("politika uretilen teslim atilan kalan zirve en-uzun-bekleme uretici-beklemesi"); for (const p of ["sinirsiz", "yeniyi-at", "eskiyi-at", "engelle"]) { const s = kosum(p); console.log(`${s.politika.padEnd(11)}${s3(s.uretilen)}${s3(s.teslim)}${s3(s.atilan)}` + `${s3(s.kalan)}${s3(s.zirve)}${String(s.enUzunBekleme).padStart(13)} ms` + `${String(s.bekleme).padStart(14)} ms`); }
politika uretilen teslim atilan kalan zirve en-uzun-bekleme uretici-beklemesi sinirsiz 1000 600 0 400 400 4000 ms 0 ms yeniyi-at 1000 600 301 99 100 1664 ms 0 ms eskiyi-at 1000 600 301 99 100 997 ms 0 ms engelle 699 600 0 99 100 1666 ms 7490 ms
Benzetim gerçek zaman kullanmaz; sanal bir saati milisaniye milisaniye ilerletir ve hızları tam sayı birikimleriyle uygular. Bu yüzden sayılar makineden bağımsız olarak aynıdır.
Sayıların Okunması
Dört satır dört ayrı ödünleşimi gösteriyor.
Sınırsız kuyrukta hiçbir iş kaybolmadı, ama kuyruk on saniyede dört yüze çıktı ve en uzun bekleme dört saniyeye ulaştı. Önemli olan sayının kendisi değil yönü: pencere yirmi saniye olsaydı kuyruk sekiz yüz, bekleme sekiz saniye olurdu. Bu politika, işi kaybetmeme karşılığında gecikmeyi sınırsız bırakır.
Yeniyi atma ve eskiyi atma aynı sayıda işi teslim etti (600) ve aynı sayıda işi attı (301). Aralarındaki tek fark bekleme süresinde: 1664 ms’ye karşı 997 ms. Fark teslim edilen işlerin yaşından gelir. Yeniyi atarken kuyrukta hep en eski yüz iş durur; eskiyi atarken kuyruk sürekli tazelenir ve tüketiciye daha genç işler ulaşır. Kayıp oranı aynı olduğunda bu, eskiyi atmayı bildirim gibi tazeliği önemli işler için üstün kılar; sıraya girmiş bir ödünç isteği gibi sırası korunması gereken işlerde ise en eskiyi atmak kabul edilemez.
Engelleme hiçbir işi atmadı ama yalnız 699 iş üretebildi: üretici on saniyenin 7490 milisaniyesinde bekledi. Kaybolan iş yok, çünkü hiç üretilmedi. Geri basıncın tanımı budur — darboğaz, sonucu kendisine gelen tarafa değil, kendisini besleyen tarafa yansıtır.
Dört satırın ortak dersi şudur: hız uyuşmazlığı yok edilemez, yalnız nereye yazılacağı seçilebilir. Belleğe (sınırsız kuyruk), kayba (atma) ya da üreticinin hızına (engelleme).
Geri Basıncın Yayılması
Engelleme kulağa soyut gelir; oysa akış soyutlamasının içine yerleşmiş durumdadır.
node:stream üzerinde yazılabilir bir akışın write çağrısı bir mantıksal değer
döndürür: false, tamponun eşiği aştığını ve üretimin durdurulması gerektiğini bildirir.
Yer açıldığında akış drain olayını yayar.
Aşağıdaki ölçüm aynı altmış dört parçayı iki kez yazar. İlkinde dönüş değeri dinlenir, ikincisinde yok sayılır.
// akis.mjs — node:stream geri basinci: write() donusunu dinlemek ile yok saymak import { Writable } from "node:stream"; const PARCA = Buffer.alloc(1024, 0x61); // 1 KiB'lik is govdesi const ADET = 64; const yavasAlici = () => new Writable({ highWaterMark: 16 * 1024, // tampon sinirimiz: 16 KiB write(_p, _k, bitti) { setTimeout(bitti, 5); },// her parcanin yazimi 5 ms surer }); async function dinleyerek() { const alici = yavasAlici(); let doluDonus = 0, drain = 0, zirve = 0; for (let i = 0; i < ADET; i++) { const yerVar = alici.write(PARCA); zirve = Math.max(zirve, alici.writableLength); if (!yerVar) { // tampon doldu: uretimi durdur doluDonus++; await new Promise((c) => alici.once("drain", () => { drain++; c(); })); } } await new Promise((c) => alici.end(c)); return { doluDonus, drain, zirve }; } async function yokSayarak() { const alici = yavasAlici(); let doluDonus = 0, zirve = 0; for (let i = 0; i < ADET; i++) { if (!alici.write(PARCA)) doluDonus++; // donus okunuyor ama uyulmuyor zirve = Math.max(zirve, alici.writableLength); } await new Promise((c) => alici.end(c)); return { doluDonus, drain: 0, zirve }; } for (const [ad, calis] of [["dinleyerek", dinleyerek], ["yok sayarak", yokSayarak]]) { const s = await calis(); console.log(`${ad.padEnd(12)} dolu-donus=${String(s.doluDonus).padStart(2)}` + ` drain=${String(s.drain).padStart(2)} tampon-zirvesi=${s.zirve} B`); }
dinleyerek dolu-donus= 4 drain= 4 tampon-zirvesi=16384 B yok sayarak dolu-donus=49 drain= 0 tampon-zirvesi=65536 B
Dönüş değeri dinlendiğinde tampon zirvesi tam olarak eşiğin kendisidir: 16384 bayt. Yok
sayıldığında altmış dört parçanın tamamı tamponda birikir: 65536 bayt. Altmış dört
parçada oran dört kat; üretici sınırsız yazmaya devam ederse oran da sınırsızdır, çünkü
false dönüşünü yok saymak tamponu bir kuyruğa, üstelik sınırsız bir kuyruğa
çevirir.
Buradaki ders akışa özgü değildir. Kuyruğa iş yazan her katman aynı soruyla karşı karşıyadır: yazma reddedildiğinde durur mu, yoksa arkasında kendi kuyruğunu mu biriktirir. Durmayan her katman, önündeki sınırı işe yaramaz hale getirir.
Geri Basınç Nereye Kadar Yayılır
Zincirin son halkası kullanıcıdır ve geri basınç oraya kadar taşınamaz. İstek işleyen bir sunucu, arka plan kuyruğu dolduğu için HTTP isteğini süresiz bekletemez; bekletirse bağlantıları tüketir ve sorun kuyruktan ağa taşınır.
Bu yüzden geri basınç sistemin iç halkalarında uygulanır: işçi ile veritabanı arasında, kuyruk yazıcısı ile kuyruk arasında, akış üreticisi ile alıcısı arasında. Dış sınırda ise başka bir mekanizma gerekir — gelen isteği bekletmek yerine açıkça reddetmek.
Özet
- Üretim hızı tüketim hızını aştığında kuyruk her saniye aradaki fark kadar uzar; büyüme kendiliğinden durmaz.
- bağıntısı gereği sınırsız kuyruk uzunluğu sınırsız bekleme süresi demektir; kuyruk sınırı bir bellek kararı değil gecikme bütçesi kararıdır.
- Sınırlı kuyrukta üç politika vardır: engelleme işi kaybetmez ama üreticiyi yavaşlatır, yeniyi atma ile eskiyi atma aynı kaybı üretir ama teslim edilen işlerin yaşı farklıdır.
- Geri basınç, darboğazın sonucunu kendisini besleyen tarafa yansıtmasıdır;
node:streambunu yazma dönüşü ve boşalma olayıyla uygular ve tampon zirvesini eşikte tutar. - Yazma dönüşünü yok sayan katman, önündeki her sınırı geçersiz kılar; geri basınç zincirin tamamında uygulanmadıkça hiçbir yerinde uygulanmış sayılmaz.
Sonraki Adım
Geri basınç iç halkalarda çalışır, ama zincirin dış ucunda duran istemci bekletilemez. Kuyruk dolduğunda, işçiler yetişemediğinde ve gecikme bütçesi tükendiğinde sunucunun elinde tek seçenek kalır: gelen işi kabul etmemek. Bunu rastgele yapmak sistemi kararsızlaştırır; hangi isteğin reddedileceği, reddin istemciye nasıl bildirileceği ve reddedilen ile kabul edilenin gecikme farkının ne olduğu ayrı kararlardır. Sonraki ders aşırı yükte denetimli reddetmeyi kurar ve reddin gecikmeye etkisini ölçer.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.