İçeriğe geç
academia.sh

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 λ\lambda iş girip saniyede μ\mu iş çıkıyorsa, kuyruk uzunluğu λμ\lambda - \mu hızıyla değişir. λ<μ\lambda < \mu olduğu sürece kuyruk boş kalır ve bekleme süresi işin kendi süresine yaklaşır. λ>μ\lambda > \mu olduğu anda kuyruk her saniye λμ\lambda - \mu 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 LL, varış hızı λ\lambda ve ortalama bekleme süresi WW arasında şunu söyler:

L=λWL = \lambda W

Bağıntı tersten okunduğunda asıl uyarıyı verir: W=L/λW = L / \lambda. 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 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.
  • L=λWL = \lambda W 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:stream bunu 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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat