İçeriğe geç
academia.sh

Ders 16 / 18

Giden Kutusu Kalıbı

Veritabanı yazması ile ileti yayınının aynı işleme alınmasının ölçülen bedeli: yayıncı çöktüğünde giden kutusuz kaybolan ileti, giden kutusuyla ikinci kez teslim edilen ileti, yoklama aralığının belirlediği aktarıcı gecikmesi ve aktarıcı durduğunda tabloda biriken satır.

İçindekiler

Önceki derste her adım bir sonrakini doğrudan çağırdı: katalog kendi deposuna yazdı ve hemen ardından ödünç bilgisini üyeliğe bildirdi. Kod tek satırdı, ama iki iş vardı — biri yerel işlemin içinde, öteki dışında. Aralarına düşen süreç, işi yapmış ama kimseye söylememiş olur.

M16/K04’ün dağıtık işlem sorunu dersi giden kutusu düzenini kurmuş ve tek bir iletinin iki kez teslim edildiğini göstermişti. O ölçüm burada tekrarlanmıyor. Buradaki ölçü bir yük üzerindedir: yayıncı çöktüğünde kaç ileti kayboluyor, giden kutusuyla aynı çökme dizisinde kaç ileti ikinci kez teslim ediliyor, aktarıcının gecikmesi ne oluyor ve tabloda kaç satır birikiyor.

Düzenek

DT5. Yayıncının çökmesi süreç gerçekten öldürülerek değil, kesinleştirme ile yayın arasındaki noktada tohumlu bir üreteçten çekilen kararla modellenir. Tohum 20260731, çökme oranı 0,10. Karar dizisi teslim denemesi numarasına göre okunduğu için, k’inci teslim denemesi iki gerçekleştirmede de aynı kaderi paylaşır.

DT6. Aktarıcı tetiklenmez, kutuyu yoklar. Ölçülen gecikme bu tercihin doğrudan sonucudur; tetiklemeli bir aktarıcıda gecikmenin alt sınırı yoklama aralığına bağlı olmaz.

# kur.sh — katalog kendi durumunu ve giden kutusunu tutar, uyelik teslimleri kaydeder
rm -f katalog.db* uyelik.db*
sqlite3 katalog.db "PRAGMA journal_mode=WAL;
  CREATE TABLE hareket (id INTEGER PRIMARY KEY, kitap_id INTEGER, uye_id INTEGER);
  CREATE TABLE giden_kutusu (ileti_id INTEGER PRIMARY KEY, kitap_id INTEGER, uye_id INTEGER,
                             yazilma INTEGER NOT NULL, durum TEXT NOT NULL DEFAULT 'bekliyor');" >/dev/null
sqlite3 uyelik.db "PRAGMA journal_mode=WAL;
  CREATE TABLE teslim (sira INTEGER PRIMARY KEY, ileti_id INTEGER, varis INTEGER);" >/dev/null
// uretec.mjs — kendi yazdigimiz uretec; tohum gorunur, dizi her kosumda ayni
export function uretec(tohum) {
  let d = tohum >>> 0;
  return () => { d = (Math.imul(d, 1103515245) + 12345) >>> 0; return (d >>> 8) / 16777216; };
}

Üyelik tarafındaki teslim tablosu her teslimi ayrı satır olarak biriktirir; yinelenen bir teslim aynı ileti_id ile ikinci kez görünür. Kaybolan ileti ile yinelenen ileti bu tek tablodan sayılır.

Aynı Yük, İki Gerçekleştirme

İki yüz ödünç isteği. Birinci gerçekleştirme durumu kesinleştirir, sonra yayımlar. İkincisi durumu ve iletiyi tek işlemde yazar, yayını ayrı bir geçişe bırakır.

// yayinci.mjs — ayni yuk, iki gerceklestirme: --dogrudan ve --giden-kutusu
import { DatabaseSync } from "node:sqlite";
import { uretec } from "./uretec.mjs";

const kip = process.argv.includes("--dogrudan") ? "dogrudan" : "giden-kutusu";
const N = 200, TOHUM = 20260731, ORAN = 0.10;
const zar = uretec(TOHUM);
// deneme numarasina gore sabit cokme dizisi: k'inci teslim denemesi iki kipte de ayni kaderi paylasir
const coker = Array.from({ length: N * 4 }, () => zar() < ORAN);
let deneme = 0, cokme = 0;

const katalog = new DatabaseSync("katalog.db");
const uyelik = new DatabaseSync("uyelik.db");
const teslimEt = (iletiId) =>
  uyelik.prepare("INSERT INTO teslim (ileti_id, varis) VALUES (?,?)").run(iletiId, Date.now());

if (kip === "dogrudan") {
  for (let n = 1; n <= N; n++) {
    katalog.exec("BEGIN IMMEDIATE");
    const r = katalog.prepare("INSERT INTO hareket (kitap_id, uye_id) VALUES (?,4)").run(n);
    katalog.exec("COMMIT");
    if (coker[deneme++]) { cokme += 1; continue; }   // surec burada duser: ileti hic uretilmedi
    teslimEt(Number(r.lastInsertRowid));
  }
} else {
  for (let n = 1; n <= N; n++) {
    katalog.exec("BEGIN IMMEDIATE");                 // durum ve ileti tek islemde
    katalog.prepare("INSERT INTO hareket (kitap_id, uye_id) VALUES (?,4)").run(n);
    katalog.prepare("INSERT INTO giden_kutusu (kitap_id, uye_id, yazilma) VALUES (?,4,?)")
      .run(n, Date.now());
    katalog.exec("COMMIT");
  }
  for (let gecis = 0; gecis < 500; gecis++) {        // aktarici gecisleri
    const bekleyen = katalog.prepare(
      "SELECT ileti_id FROM giden_kutusu WHERE durum='bekliyor' ORDER BY ileti_id").all();
    if (bekleyen.length === 0) break;
    for (const i of bekleyen) {
      teslimEt(i.ileti_id);
      if (coker[deneme++]) { cokme += 1; break; }    // isaretleme oncesi duser: yeniden teslim edilecek
      katalog.prepare("UPDATE giden_kutusu SET durum='gonderildi' WHERE ileti_id=?").run(i.ileti_id);
    }
  }
}

const satir = uyelik.prepare("SELECT COUNT(*) c FROM teslim").get().c;
const benzersiz = uyelik.prepare("SELECT COUNT(DISTINCT ileti_id) c FROM teslim").get().c;
const kutu = katalog.prepare("SELECT COUNT(*) c FROM giden_kutusu").get().c;
console.log(`${kip.padEnd(13)}| uretilen=${N} cokme=${cokme} | teslim satiri=${satir}` +
  ` benzersiz=${benzersiz} kaybolan=${N - benzersiz} yinelenen=${satir - benzersiz}` +
  ` | kutuda satir=${kutu}`);
sh kur.sh
node yayinci.mjs --dogrudan
sh kur.sh
node yayinci.mjs --giden-kutusu
dogrudan     | uretilen=200 cokme=22 | teslim satiri=178 benzersiz=178 kaybolan=22 yinelenen=0 | kutuda satir=0
giden-kutusu | uretilen=200 cokme=24 | teslim satiri=224 benzersiz=200 kaybolan=0 yinelenen=24 | kutuda satir=200

Doğrudan yayında 200 durum değişikliğinin 22’si için hiçbir ileti üretilmedi. Bu 22 kitap katalogda ödünç göründü, üyelik tarafında hiç görünmedi ve bu durumdan hiçbir kayıt kalmadı: kutuda 0 satır var, yani sistemde “gönderilmesi gereken ama gönderilmemiş” diye bakılabilecek bir yer yok. Kayıp sessizdir.

Giden kutusunda kaybolan sıfır. Çökme sayısı 22 değil 24; çünkü bu kip daha çok teslim denemesi yaptı (224’e karşı 200) ve ilk 200 denemenin kaderi doğrudan kiple aynı olsa da sonraki 24 denemede 2 çökme daha çıktı. Her çökme tam olarak bir ikinci teslim doğurdu: 224 − 200 = 24 yinelenen. Kayıp, yinelenmeye dönüştü.

Üçüncü sayı gözden kaçmamalı: kutuda 200 satır var. Bunlar teslim edilmiş, işi bitmiş satırlardır ve tablo kendiliğinden küçülmez.

Aktarıcı Gecikmesi

Yukarıdaki geçişler ardışıktı. Gerçekte aktarıcı ayrı koşar ve kutuyu belirli aralıklarla yoklar; aralık, iletinin yazılmasıyla teslimi arasındaki süreyi doğrudan belirler.

// yazici.mjs — durum degisikligini ve iletiyi tek islemde, duzenli araliklarla kutuya yazar
import { DatabaseSync } from "node:sqlite";
const [adet, aralik] = process.argv.slice(2).map(Number);
const db = new DatabaseSync("katalog.db");
db.exec("PRAGMA busy_timeout = 500");
for (let n = 1; n <= adet; n++) {
  db.exec("BEGIN IMMEDIATE");
  db.prepare("INSERT INTO hareket (kitap_id, uye_id) VALUES (?,4)").run(n);
  db.prepare("INSERT INTO giden_kutusu (kitap_id, uye_id, yazilma) VALUES (?,4,?)").run(n, Date.now());
  db.exec("COMMIT");
  await new Promise((r) => setTimeout(r, aralik));
}
console.log(`yazici: ${adet} ileti, ${aralik} ms arayla yazildi`);
// aktarici.mjs — kutuyu belirli araliklarla yoklar; gecikmeyi ve bos yoklamayi olcer
import { DatabaseSync } from "node:sqlite";
const [aralik, sure] = process.argv.slice(2).map(Number);
const katalog = new DatabaseSync("katalog.db"); katalog.exec("PRAGMA busy_timeout = 500");
const uyelik = new DatabaseSync("uyelik.db");
const gecikmeler = [];
let yoklama = 0, bosYoklama = 0, enCokBekleyen = 0;
const bitis = Date.now() + sure;
while (Date.now() < bitis) {
  const bekleyen = katalog.prepare(
    "SELECT ileti_id, yazilma FROM giden_kutusu WHERE durum='bekliyor' ORDER BY ileti_id").all();
  yoklama += 1;
  if (bekleyen.length === 0) bosYoklama += 1;
  enCokBekleyen = Math.max(enCokBekleyen, bekleyen.length);
  for (const i of bekleyen) {
    const simdi = Date.now();
    uyelik.prepare("INSERT INTO teslim (ileti_id, varis) VALUES (?,?)").run(i.ileti_id, simdi);
    katalog.exec("BEGIN IMMEDIATE");
    katalog.prepare("UPDATE giden_kutusu SET durum='gonderildi' WHERE ileti_id=?").run(i.ileti_id);
    katalog.exec("COMMIT");
    gecikmeler.push(simdi - i.yazilma);
  }
  await new Promise((r) => setTimeout(r, aralik));
}
gecikmeler.sort((a, b) => a - b);
const ortanca = gecikmeler[Math.floor(gecikmeler.length / 2)] ?? 0;
console.log(`aktarici(aralik=${aralik} ms): yoklama=${yoklama} bos yoklama=${bosYoklama}` +
  ` teslim=${gecikmeler.length} en cok bekleyen satir=${enCokBekleyen}` +
  ` | gecikme ortanca=${ortanca} ms en buyuk=${gecikmeler.at(-1) ?? 0} ms (bu kosumda)`);
# ayni yazma hizi (60 ileti, 50 ms arayla), iki farkli yoklama araligi
for ARALIK in 20 250; do
  sh kur.sh
  node yazici.mjs 60 50 &
  node aktarici.mjs $ARALIK 4500
  wait
  sqlite3 katalog.db "SELECT durum, COUNT(*) FROM giden_kutusu GROUP BY durum;"
done
yazici: 60 ileti, 50 ms arayla yazildi
aktarici(aralik=20 ms): yoklama=204 bos yoklama=144 teslim=60 en cok bekleyen satir=1 | gecikme ortanca=13 ms en buyuk=23 ms (bu kosumda)
gonderildi|60
yazici: 60 ileti, 50 ms arayla yazildi
aktarici(aralik=250 ms): yoklama=18 bos yoklama=4 teslim=60 en cok bekleyen satir=5 | gecikme ortanca=131 ms en buyuk=252 ms (bu kosumda)
gonderildi|60

Koşumdan bağımsız olan taraf nettir: iki koşumda da 60 ileti yazıldı ve 60’ı teslim edildi. Değişen, aynı işin kaç sorguyla ve ne kadar bekleyerek yapıldığıdır. Yirmi milisaniyelik aralıkta 204 yoklamanın 144’ü boştur (bu koşumda) — teslim başına yaklaşık 3,4 sorgu, üçte ikisi karşılıksız. İki yüz elli milisaniyelik aralıkta yoklama 18’e iner ve boş yoklama neredeyse kalmaz, ama ortanca gecikme 13 ms’ten 131 ms’e çıkar ve bir geçişte kutuda en çok 1 yerine 5 satır birikir. Aralık, boş sorgu ile gecikme arasında doğrudan bir takastır.

Aktarıcı Durduğunda

Aktarıcı yayıncıdan bağımsız bir süreç olduğu için ayrı düşebilir. Bu, giden kutusunun kazancının en görünür olduğu ve bedelinin en somutlaştığı durumdur.

sh kur.sh
node yazici.mjs 60 20
sqlite3 katalog.db "SELECT durum, COUNT(*) FROM giden_kutusu GROUP BY durum;"
node aktarici.mjs 20 1500
sqlite3 katalog.db "SELECT durum, COUNT(*) FROM giden_kutusu GROUP BY durum;"
sqlite3 katalog.db "DELETE FROM giden_kutusu WHERE durum='gonderildi';
                    SELECT 'budamadan sonra satir: ' || COUNT(*) FROM giden_kutusu;"
yazici: 60 ileti, 20 ms arayla yazildi
bekliyor|60
aktarici(aralik=20 ms): yoklama=68 bos yoklama=67 teslim=60 en cok bekleyen satir=60 | gecikme ortanca=743 ms en buyuk=1382 ms (bu kosumda)
gonderildi|60
budamadan sonra satir: 0

Aktarıcı yokken 60 satır kutuda birikti ve hiçbiri kaybolmadı. Aktarıcı geri geldiğinde hepsini tek geçişte teslim etti; bedeli, ortanca gecikmenin 13 ms’ten 743 ms’e, en büyük gecikmenin 1382 ms’e çıkmasıdır (bu koşumda). Aktarıcı ne kadar uzun durursa biriken satır ve yakalama gecikmesi o kadar büyür — bu, izlenmesi gereken bir niceliktir, kendiliğinden sınırlı değildir.

Son satır tablonun büyümesini gösterir: teslim edilen 60 satır tabloda kalır ve ancak açık bir budama ile silinir. Giden kutusu, iş kuralıyla ilgisi olmayan bir tabloyu yazma yoluna ekler; o tablonun temizlenmesi de bir iştir.

Üç Sütun

Neyi ucuzlattı: durum değişikliği ile iletinin birlikte yazılması, “yapıldı ama duyurulmadı” durumunu ortadan kaldırdı. Aynı çökme dizisinde doğrudan yayın 22 ileti kaybederken giden kutusu 0 kaybetti; üstelik kayıp sessiz değil, kutuda bekleyen bir satır olarak görünür.

Neyi pahalılaştırdı: her ödünç isteği artık iki tabloya yazıyor; ayrıca yeni bir süreç (aktarıcı) ve yeni bir ayar (yoklama aralığı) var. Teslim başına 3,4 sorguya kadar çıkan boş yoklama, gecikmeyi kısaltmanın bedelidir. Kutu kendiliğinden küçülmez; budama ayrı bir iştir.

Hangi yeni arıza kipi doğdu: iki tane. Birincisi ikinci teslim: aktarıcının teslim ile işaretleme arasında düşmesi 24 iletinin iki kez ulaşmasına yol açtı. İkincisi aktarıcı gerilemesi: aktarıcı durduğunda sistem hata vermez, yalnız geç kalır; ölçüm yapılmazsa biriken satır ve büyüyen gecikme fark edilmez.

Özet

  • Aynı çökme dizisi altında doğrudan yayın 200 durum değişikliğinin 22’si için hiç ileti üretmedi; giden kutusu hiç ileti kaybetmedi.
  • Kaybın yerine yinelenme geçti: giden kutusu 224 teslim yaptı, bunların 24’ü ikinci teslimdir; her çökme tam olarak bir yinelenme doğurdu.
  • Yoklama aralığı gecikme ile boş sorgu arasında takastır: bu koşumda 20 ms’de ortanca gecikme 13 ms ve 204 yoklamanın 144’ü boş; 250 ms’de gecikme 131 ms’e çıkar, yoklama 18’e iner.
  • Aktarıcı durduğunda 60 satır kutuda birikti, hiçbiri kaybolmadı; yakalama sırasında ortanca gecikme 743 ms, en büyük 1382 ms oldu (bu koşumda).
  • Teslim edilen satırlar tabloda kalır (200 ve 60 satır); giden kutusu açık bir budama işi gerektirir.

Sonraki Adım

Giden kutusunun bıraktığı 24 ikinci teslim, yayıncı tarafında çözülemez: aktarıcı ile hedef ayrı depolarda olduğu sürece “gönderdim” bilgisi ile “işledim” bilgisi aynı işleme giremez. Geriye tek yol kalıyor — bedeli tüketiciye taşımak. Sonraki ders iletileri hem yinelenmiş hem de sırası bozulmuş olarak tüketiciye verir, iki tüketici gerçekleştirmesinin ürettiği yan etkiyi sayar ve sürüm damgasının eski iletiyi nasıl attığını ö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