İçeriğe geç
academia.sh

Ders 08 / 25

Eşzamansız İşlemenin Gerekçesi

İstek yolunun işi çıkararak kısaltılması: adım maliyetlerinin ve başarı olasılığının ölçülmesi, isteğin içinde kalması zorunlu iş ile ertelenebilir işin ayrılması, yanıtın anlamının değişmesi ve devrin bellekte yapılmasıyla giden kutusuna yazılması arasındaki dayanıklılık farkı.

İçindekiler

Önceki ders önbellek isabet oranını ölçtü ve okuma yolunun ne kadar kısaldığını sayıyla gösterdi. Aynı ders bir sınırı da işaretledi: önbellek yalnız okumaya yarar. Ödünç verme isteği bir yazmadır, üstelik yazmanın yanında sürüklediği başka işler vardır — üyeye gecikme bildirimi, şube stok özetinin yeniden hesaplanması, aylık rapora bir satır. Bunların hiçbiri önbelleklenemez ve hepsi isteğin süresine eklenir.

İstek yolunu kısaltmanın ikinci yolu işi hızlandırmak değil, işi istekten çıkarmaktır. Bu ders üç soruyu sırayla yanıtlar: isteğin içinde ne kalmak zorundadır, ne çıkarılabilir, çıkarmanın bedeli nedir.

İsteğin İçindeki İş

Ödünç verme isteğinin beş adımı var. Her adımın maliyeti aşağıdaki modelde bildirilmiştir; bu değerler ölçülmüş süreler değil, adımların birbirine göre büyüklüğünü temsil eden model değerleridir. Asıl ilgilendiğimiz, hangi adımın toplam süreye ne kattığıdır.

// istek-yolu.mjs — odunc verme isteginin adimlari ve her adimin modellenmis maliyeti
const ADIMLAR = [
  { ad: "yetki denetimi",           ms: 3,   yaniti_belirler: true  },
  { ad: "odunc kaydinin yazilmasi", ms: 9,   yaniti_belirler: true  },
  { ad: "gecikme bildirimi",        ms: 140, yaniti_belirler: false },
  { ad: "sube stok ozetinin yeniden hesabi", ms: 260, yaniti_belirler: false },
  { ad: "aylik rapor satiri",       ms: 55,  yaniti_belirler: false },
];

let birikim = 0;
for (const a of ADIMLAR) {
  birikim += a.ms;
  const isaret = a.yaniti_belirler ? "zorunlu " : "ertelenebilir";
  console.log(`${a.ad.padEnd(40)} ${String(a.ms).padStart(4)} ms  ${isaret}  birikim=${birikim} ms`);
}

const toplam = ADIMLAR.reduce((t, a) => t + a.ms, 0);
const zorunlu = ADIMLAR.filter((a) => a.yaniti_belirler).reduce((t, a) => t + a.ms, 0);
console.log(`\nistek suresi (hepsi istekte)      = ${toplam} ms`);
console.log(`istek suresi (yalniz zorunlu is)  = ${zorunlu} ms`);
console.log(`kisalma orani                     = ${(1 - zorunlu / toplam).toFixed(3)}`);

// Tek is parcasi saniyede kac istek bitirebilir?
console.log(`tek is parcasinin verimi: ${(1000 / toplam).toFixed(1)} -> ${(1000 / zorunlu).toFixed(1)} istek/s`);

// Her adim bir dis bagimliliktir; istegin basarisi hepsinin ayni anda calismasina baglidir.
const p = 0.999;
const hepsi = p ** ADIMLAR.length, azi = p ** ADIMLAR.filter((a) => a.yaniti_belirler).length;
console.log(`istek basari olasiligi: ${hepsi.toFixed(5)} -> ${azi.toFixed(5)}`);
node istek-yolu.mjs
yetki denetimi                              3 ms  zorunlu   birikim=3 ms
odunc kaydinin yazilmasi                    9 ms  zorunlu   birikim=12 ms
gecikme bildirimi                         140 ms  ertelenebilir  birikim=152 ms
sube stok ozetinin yeniden hesabi         260 ms  ertelenebilir  birikim=412 ms
aylik rapor satiri                         55 ms  ertelenebilir  birikim=467 ms

istek suresi (hepsi istekte)      = 467 ms
istek suresi (yalniz zorunlu is)  = 12 ms
kisalma orani                     = 0.974
tek is parcasinin verimi: 2.1 -> 83.3 istek/s
istek basari olasiligi: 0.99501 -> 0.99800

Üç sayı okunmaya değer. Birincisi süre: isteğin işini yapan iki adım 12 milisaniye tutuyor, geri kalan 455 milisaniye üyenin yanıtı beklediği ölü zaman. İkincisi verim: aynı iş parçası bu ölü zamanı taşımadığında birim zamanda kırk kat fazla istek bitiriyor. Üçüncüsü başarı olasılığı: her adım ayrı bir dış bağımlılıktır ve isteğin başarılı olması hepsinin aynı anda çalışmasına bağlıdır. Beş bağımlılık iki bağımlılığa indiğinde başarısızlık oranı binde beşten binde ikiye düşüyor. Rapor sunucusunun erişilemez olması artık ödünç vermeyi engellemiyor.

Zorunlu İş ile Ertelenebilir İş

Ayrımı yapan ölçüt hız değildir. Gecikme bildirimi hızlı olsaydı da isteğin içinde kalması gerekmezdi; asıl soru şudur: bu adım yapılmazsa yanıt yanlış olur mu?

Üç durumda iş isteğin içinde kalır. Adım yanıtın içeriğini belirliyorsa — ödünç kimliği yanıtta dönüyorsa kaydın yazılması ertelenemez. Adım isteğin reddine yol açabiliyorsa — yetki denetimi ya da stok kısıtı sonradan çalıştırılamaz, çünkü üyeye “verildi” denmiş olurdu. Adım bir değişmezi koruyorsa — kitabın aynı anda iki üyeye verilmemesi kuralı ödünç kaydıyla aynı işlem sınırının içinde kalmak zorundadır; İşlem Sınırları dersinde kurulan kural budur.

Kalan iş ertelenebilir. Ertelenebilir işin ortak özelliği, sonunda yapılmasının yetmesidir: bildirim iki saniye sonra gitse de doğrudur, stok özeti bir dakika geriden gelse de rapor doğruyu gösterir. İş Mantığı Yerleşimi konusunda alan olaylarının dinleyicisi için konan ölçüt aynıdır: dinleyiciye verilen iş, yapılmasa da ödünç kaydının geçerli kaldığı iştir.

Devrin Bedeli: Yanıtın Anlamı Değişir

İş çıkarıldığında yanıt artık “yapıldı” demez, “kabul edildi” der. API Tasarımı kursunun Eşzamanlı ve Eşzamansız API’ler dersinde kurulan kalıp burada karşılığını bulur: sunucu 202 döndürür ve işin izleneceği kaynağı bildirir. Bu değişimin üç sonucu vardır.

İstemci işin bittiğini yanıttan öğrenemez; sormak ya da bildirilmek zorundadır. Hata artık istemciye dönmez; ertelenmiş iş başarısız olduğunda ortada yanıtını bekleyen kimse yoktur, dolayısıyla hatanın görülmesi ayrı bir düzenek ister. Ve iki uç arasında bir mesaj doğar: bir tarafın yazdığı, başka bir tarafın sonra okuyacağı kayıt.

Mesajın nereye yazıldığı, bu dersin son ölçümünün konusudur.

Devir Dayanıklı Olmalı

İşi istekten çıkarmanın en kısa yolu, onu aynı süreçte sonraya bırakmaktır: bir diziye koy, yanıtı döndür, sonra işle. Ölçüm bunun neyi kaybettiğini gösterir. Aşağıdaki kurulum Veri Erişim Katmanı ve İş Mantığı kursunda kullanılan tabloların aynısını kurar.

# kur.sh — kutuphane veritabanini sifirdan kurar
rm -f kutuphane.db
sqlite3 kutuphane.db <<'SQL'
CREATE TABLE sube (sube_id INTEGER PRIMARY KEY, ad TEXT NOT NULL);
CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL);
CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL, sube_id INTEGER NOT NULL);
CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL,
                    uye_id INTEGER NOT NULL, alis_tarihi TEXT NOT NULL, iade_tarihi TEXT);
CREATE TABLE giden_kutusu (ileti_id INTEGER PRIMARY KEY, tur TEXT NOT NULL,
                           govde TEXT NOT NULL, durum TEXT NOT NULL DEFAULT 'bekliyor');
INSERT INTO sube VALUES (1,'Merkez'),(2,'Bahcelievler');
INSERT INTO uye VALUES (1,'Ayse Demir'),(2,'Bora Kaya'),(3,'Ceren Ak');
INSERT INTO kitap VALUES (1,'Korluk',1),(2,'Kum Kitabi',1),(3,'Puslu Kitalar',2);
SQL

İlk sürüm yan işleri bellekteki bir diziye bırakır. Sürecin dizi boşaltılmadan sonlanması process.exit ile temsil edilmiştir; gerçek karşılığı dağıtımın yeni sürümü yerleştirmesi ya da sürecin çökmesidir.

// bellekte.mjs — yan isler bellekteki bir diziye birakilir; surec bosaltmadan sonlanir
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("kutuphane.db");
const yanIsler = [];

function oduncVer(uyeId, kitapId) {
  db.exec("BEGIN IMMEDIATE");
  const oduncId = Number(db.prepare(
    "INSERT INTO odunc (kitap_id, uye_id, alis_tarihi) VALUES (?,?,'2025-07-20')")
    .run(kitapId, uyeId).lastInsertRowid);
  db.exec("COMMIT");
  for (const tur of ["gecikme_bildirimi", "stok_ozeti", "rapor_satiri"])
    yanIsler.push({ tur, oduncId });
  return oduncId;
}

for (const [uyeId, kitapId] of [[1, 1], [2, 2], [3, 3]]) oduncVer(uyeId, kitapId);
console.log(`istek bitti; bellekte bekleyen yan is = ${yanIsler.length}`);
process.exit(0);   // surec yeniden baslatildi: dizi bosaldi

İkinci sürüm aynı yan işleri ödünç kaydıyla aynı işlemde giden kutusuna yazar. Giden kutusu düzeni Alan Olayları dersinde tanıtılmıştı; buradaki kullanımı aynıdır, yalnız yazılan şey bir alan olayı değil, yapılacak bir iştir.

// giden-kutusuyla.mjs — yan isler odunc kaydiyla ayni islemde giden kutusuna yazilir
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("kutuphane.db");

function oduncVer(uyeId, kitapId) {
  db.exec("BEGIN IMMEDIATE");
  const oduncId = Number(db.prepare(
    "INSERT INTO odunc (kitap_id, uye_id, alis_tarihi) VALUES (?,?,'2025-07-20')")
    .run(kitapId, uyeId).lastInsertRowid);
  for (const tur of ["gecikme_bildirimi", "stok_ozeti", "rapor_satiri"])
    db.prepare("INSERT INTO giden_kutusu (tur, govde) VALUES (?,?)")
      .run(tur, JSON.stringify({ oduncId, uyeId, kitapId }));
  db.exec("COMMIT");
  return oduncId;
}

for (const [uyeId, kitapId] of [[1, 1], [2, 2], [3, 3]]) oduncVer(uyeId, kitapId);
console.log("istek bitti; yan isler giden kutusunda");
process.exit(0);   // surec yeniden baslatildi

Yeniden başlatılmış süreç geriye ne kaldığını sayar.

// sayim.mjs — yeniden baslatilmis surec: geriye ne kaldi
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("kutuphane.db");
const say = (s) => db.prepare(s).get().n;
console.log(`odunc satiri = ${say("SELECT count(*) AS n FROM odunc")}, ` +
            `kurtarilabilir yan is = ${say("SELECT count(*) AS n FROM giden_kutusu WHERE durum = 'bekliyor'")}`);
sh kur.sh
node bellekte.mjs
node sayim.mjs
sh kur.sh
node giden-kutusuyla.mjs
node sayim.mjs
istek bitti; bellekte bekleyen yan is = 9
odunc satiri = 3, kurtarilabilir yan is = 0
istek bitti; yan isler giden kutusunda
odunc satiri = 3, kurtarilabilir yan is = 9

Her iki sürümde de üç ödünç kaydı yazıldı ve üç üye yanıtını hızlı aldı. Fark kurtarılabilir yan iş sayısındadır: bellekte biriken dokuz iş süreçle birlikte yok oldu, giden kutusuna yazılan dokuz iş yeniden başlatılan süreç tarafından bulundu. Üç üye bildirim almadı ve kimse bunu fark etmedi — çünkü isteğe dönen yanıt zaten başarılıydı.

Buradan çıkan kural, eşzamansız işlemenin temel koşuludur: iş istekten çıkarılıyorsa, çıkarıldığı yerin kalıcı olması gerekir. İşin devri, yapıldığı işlemle aynı atomikliği paylaşmalıdır; yoksa kayıp sessiz olur.

Ne Zaman Çıkarılmaz

Karşı yönde de bir maliyet vardır. Çıkarılan iş, isteği yapan için görünmez olur: üye kitabı aldığını görür, bildirimin gelip gelmediğini bilmez. İşin durumu ayrı bir kaynakla izlenmiyorsa hata kimsenin görmediği bir yerde birikir.

İkinci maliyet gecikmedir. Yan iş “sonunda” yapılır, ama o “sonunda” ölçülmüyorsa sınırsızdır. Üç ölçüt işi isteğin içinde tutmayı haklı kılar: yanıtın içeriği o işe bağlıysa, iş bir değişmezi koruyorsa, ya da işin gecikmesi kullanıcı için hatadan ayırt edilemiyorsa.

Üçüncüsü karmaşıklıktır. Bellekte iki satırla yapılan devir, kalıcı hâle geldiğinde bir tablo, bir okuyucu döngü, bir yeniden deneme kuralı ve bir hata yalıtımı gerektirir. Bu dersten sonrası, o dört parçanın nasıl kurulduğudur.

Özet

  • İstek yolundaki beş adımdan ikisi yanıtı belirliyordu; kalan üçü çıkarıldığında modelde istek süresi 467 milisaniyeden 12 milisaniyeye, dış bağımlılık sayısı beşten ikiye indi.
  • Aynı iş parçasının verimi 2,1 istek/s değerinden 83,3 istek/s değerine, istek başarı olasılığı 0,99501 değerinden 0,99800 değerine çıktı.
  • Bir adım yanıtın içeriğini belirliyor, isteğin reddine yol açabiliyor ya da bir değişmezi koruyorsa isteğin içinde kalır; kalan iş ertelenebilir.
  • İş çıkarıldığında yanıtın anlamı “yapıldı” olmaktan çıkıp “kabul edildi” olur; hata artık istemciye dönmediği için görünürlüğü ayrı kurulur.
  • Yan işler bellekte biriktirildiğinde süreç sonlanınca dokuzu da kayboldu; aynı işlemde giden kutusuna yazıldığında dokuzu da yeniden başlatılan süreçte bulundu.

Sonraki Adım

Giden kutusu işi kalıcı kıldı, ama tek başına bir taşıma düzeneği değil. Satırları kim okuyacak, iki okuyucu aynı satırı aynı anda alırsa ne olacak, bir okuyucu satırı alıp işlemeden çökerse o iş nasıl geri dönecek? Bunlar tek bir yapının soruları: iki uç arasında mesaj taşıyan kuyruk. Sonraki ders kuyruğu en küçük hâliyle kurar — üretici, tüketici, onay ve görünmezlik süresi — ve her mesajın kaç kez teslim edildiğini sayarak teslim garantisinin ne anlama geldiğini ö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