Ders 12 / 16
Durumsuz Uygulama Tasarımı
Ölçeklemenin önkoşulunun kodda nerede kırıldığı: bellekteki oturum, kopyaya özel yerel dosya, süreç içi önbellek ve süreç başına kurulan zamanlayıcı için düğüme bağlı çağrı yerlerinin taranıp sayılması, aynı uygulamanın tek kopya ve iki kopyayla koşulup kaç isteğin yanlış sonuç aldığının ölçülmesi ve dört sızıntının görünürlük sırasına dizilmesi.
İçindekiler
Önceki ders kopya sayısını bir ayar gibi ele aldı: süreç yöneticisine bir sayı yazıldı, dört süreç kalktı, istekler kopyalar arasında paylaştırıldı. Üç yerleşimin hepsinde altmış isteğin her biri herhangi bir kopyaya gidebildi ve sonuç değişmedi.
Bu, ölçeklemenin görünmeyen önkoşuludur ve kendiliğinden sağlanmaz. Kopyalar ancak isteğin hangisine gittiği fark etmiyorsa çoğaltılabilir. Bu ders o önkoşulun kodda nerede kırıldığını arar ve iki soruyu sayıyla yanıtlar: kaç çağrı yeri düğüme bağlıdır ve ikinci süreç eklendiğinde kaç istek yanlış sonuç alır.
Dört Bağ
Ödünç uygulamasında dört yer sürecin kendisine bağlıdır. Bellekteki oturum: giriş bir belirteç üretir ve onu bir tabloya yazar. Yerel dosya: ödünç makbuzu sürecin kendi dizinine yazılır. Süreç içi önbellek: katalog değeri ilk okuyuşta belleğe alınır. Zamanlayıcı: gecikme cezası süreç içinde kurulan bir sayaçla işlenir.
- DP1. Paylaşılan tek kaynak
katalog.jsonve ceza defteridir; makbuz dizini kopyaya özeldir ve kopyanın yerel diskini temsil eder. - DP2. İstekler kopyalar arasında sırayla paylaştırılır. Dağıtım kuralı önceki derste ölçüldü; buranın ölçüsü dağıtım değil, kopyaların ayrışmasıdır.
- DP3. Zamanlayıcı süreç başına tam üç tur işler ve durur. Ceza defterine düşen satır sayısı bu yüzden koşumdan bağımsızdır.
// topoloji/uygulama.mjs — odunc uygulamasinin bir kopyasi. Dort durum bu surecin icinde durur: // oturum tablosu, yerel makbuz dizini, katalog onbellegi ve gecikme cezasi zamanlayicisi. // Paylasilan tek yer katalog.json ile defter.txt; makbuz dizini kopyaya ozeldir. import { createServer } from "node:http"; import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; const nokta = Number(process.argv[2]); const oturum = new Map(); // bellekteki oturum const onbellek = new Map(); // surec ici katalog onbellegi let belirtecSayaci = 0; const katalogOku = () => JSON.parse(readFileSync("katalog.json", "utf8")); const govde = (istek) => new Promise((coz) => { let b = ""; istek.on("data", (p) => { b += p; }); istek.on("end", () => coz(b)); }); if (Number.isInteger(nokta) === false) console.log("kullanim: node topoloji/uygulama.mjs <nokta>"); else { mkdirSync(`makbuz-${nokta}`, { recursive: true }); // kopyanin kendi yerel diski let tur = 0; const zamanlayici = setInterval(() => { // her kopya kendi zamanlayicisini kurar if (++tur > 3) { clearInterval(zamanlayici); return; } appendFileSync("defter.txt", `uye-7 gecikme cezasi 2 (kopya ${nokta}, tur ${tur})\n`); }, 150); createServer(async (istek, cevap) => { cevap.sendDate = false; const [, kok, alt] = istek.url.split("/"); if (kok === "giris") { const belirtec = `B${nokta}-${++belirtecSayaci}`; oturum.set(belirtec, "uye-7"); cevap.writeHead(200, { "x-kopya": String(nokta) }).end(belirtec); } else if (kok === "odunclerim") { const uye = oturum.get(istek.headers["x-belirtec"]); if (uye === undefined) cevap.writeHead(401, { "x-kopya": String(nokta) }).end("oturum yok"); else cevap.writeHead(200, { "x-kopya": String(nokta) }).end(uye); } else if (kok === "makbuz" && istek.method === "POST") { const kimlik = `M${nokta}-${Date.now() % 100000}`; writeFileSync(`makbuz-${nokta}/${kimlik}.txt`, "odunc alindi"); cevap.writeHead(200, { "x-kopya": String(nokta) }).end(kimlik); } else if (kok === "makbuz") { let icerik = null; try { icerik = readFileSync(`makbuz-${nokta}/${alt}.txt`, "utf8"); } catch { icerik = null; } if (icerik === null) cevap.writeHead(404, { "x-kopya": String(nokta) }).end("makbuz yok"); else cevap.writeHead(200, { "x-kopya": String(nokta) }).end(icerik); } else if (kok === "kitap" && istek.method === "POST") { const k = katalogOku(); k[alt] = Number(await govde(istek)); writeFileSync("katalog.json", JSON.stringify(k)); // paylasilan kaynak guncellendi onbellek.delete(alt); // yalniz bu surecin onbellegi tazelendi cevap.writeHead(200, { "x-kopya": String(nokta) }).end(String(k[alt])); } else if (kok === "kitap") { if (onbellek.has(alt) === false) onbellek.set(alt, katalogOku()[alt]); cevap.writeHead(200, { "x-kopya": String(nokta) }).end(String(onbellek.get(alt))); } else cevap.writeHead(404, { "x-kopya": String(nokta) }).end("yok"); }).listen(nokta, "127.0.0.1"); }
Birinci ölçü koda bakar. Tarayıcı dört kalıbı kaynakta arar ve düğüme bağlı satırları listeler.
// topoloji/tarayici.mjs — kaynakta dugume bagli cagri yerlerini sayar. Her kalip bir durum turudur; // bulunan satirlar o surece bagli olan ve ikinci kopya eklendiginde ayrisan yerlerdir. // Ice aktarma satirlari sayilmaz; sayilan sey durumun bildirildigi ve kullanildigi yerlerdir. import { readFileSync } from "node:fs"; const KALIP = [ ["bellekteki oturum", /oturum\.(set|get|has|delete)|new Map\(\).*oturum|const oturum/], ["yerel dosya", /makbuz-\$\{nokta\}|mkdirSync/], ["surec ici onbellek", /onbellek\.(set|get|has|delete)|const onbellek/], ["zamanlayici", /setInterval|appendFileSync/], ]; const dosya = process.argv[2]; if (dosya === undefined) console.log("kullanim: node topoloji/tarayici.mjs <kaynak-dosya>"); else { const satir = readFileSync(dosya, "utf8").split("\n"); let toplam = 0; console.log("durum turu cagri yeri satir"); for (const [ad, kalip] of KALIP) { const yer = satir.map((s, i) => (kalip.test(s) && s.startsWith("import") === false ? i + 1 : 0)).filter(Boolean); toplam += yer.length; console.log(`${ad.padEnd(21)}${String(yer.length).padStart(10)} ${yer.join(", ")}`); } console.log(`${"toplam".padEnd(21)}${String(toplam).padStart(10)}`); }
İkinci ölçü çalışmaya bakar. İstemci dört senaryoyu sırayla koşar ve her birinde kaç isteğin yanlış sonuç aldığını sayar.
// topoloji/istemci.mjs — istekleri kopyalar arasinda sirayla paylastirir (onceki dersin dagiticisi // da bunu yapiyordu; burada olculen sey dagitim degil, kopyalarin ayrismasidir) ve dort durum // turunun her birinde kac istegin yanlis sonuc aldigini sayar. import { request } from "node:http"; import { readFileSync } from "node:fs"; const [kopyaMetni, etiket] = [process.argv[2] ?? "", process.argv[3] ?? "-"]; const KOPYA = kopyaMetni.split(",").filter(Boolean).map(Number); const ADET = 6; let sira = 0; const bir = (yol, { yontem = "GET", basliklar = {}, govde } = {}) => new Promise((coz) => { const hedef = KOPYA[sira++ % KOPYA.length]; const r = request({ port: hedef, path: yol, method: yontem, headers: basliklar }, (y) => { let b = ""; y.on("data", (p) => { b += p; }); y.on("end", () => coz({ kod: y.statusCode, govde: b, kopya: y.headers["x-kopya"] })); }); r.on("error", () => coz({ kod: 0, govde: "", kopya: "-" })); r.end(govde); }); const yaz = (ad, yanlis, ek) => { console.log(`${etiket} ${ad.padEnd(20)} ${ADET - yanlis}/${ADET} dogru ${String(yanlis).padStart(2)} yanlis ${ek}`); return yanlis; }; if (KOPYA.length === 0) console.log("kullanim: node topoloji/istemci.mjs <kopya,kopya> <etiket>"); else { const giris = await bir("/giris", { yontem: "POST" }); const o = await Promise.all(Array.from({ length: ADET }, () => bir("/odunclerim", { basliklar: { "x-belirtec": giris.govde } }))); const a = yaz("bellekteki oturum", o.filter((y) => y.kod !== 200).length, `oturum ${giris.kopya} kopyasinda`); const makbuz = await bir("/makbuz", { yontem: "POST" }); const m = await Promise.all(Array.from({ length: ADET }, () => bir(`/makbuz/${makbuz.govde}`))); const b = yaz("yerel dosya", m.filter((y) => y.kod !== 200).length, `makbuz ${makbuz.kopya} kopyasinda`); await Promise.all([bir("/kitap/K1"), bir("/kitap/K1")]); // iki kopyanin onbellegi isiniyor await bir("/kitap/K1", { yontem: "POST", govde: "2" }); // paylasilan katalog 1'den 2'ye const k = await Promise.all(Array.from({ length: ADET }, () => bir("/kitap/K1"))); const c = yaz("surec ici onbellek", k.filter((y) => y.govde !== "2").length, "guncel deger 2"); const satir = readFileSync("defter.txt", "utf8").trim().split("\n").filter((s) => s.includes("uye-7")); const d = Math.max(0, satir.length - 3); console.log(`${etiket} ${"zamanlayici".padEnd(20)} ${3}/${satir.length} dogru ${String(d).padStart(2)} yanlis ` + `uye-7 cezasi ${satir.length} kez islendi`); console.log(`${etiket} ${"toplam".padEnd(20)} yanlis sonuc alan istek ${a + b + c + d}/${3 * ADET + 3}`); }
Aynı uygulama iki yerleşimle koşuyor. Kodda tek bir satır değişmiyor; değişen tek şey kaç süreç kaldırıldığı.
# olc.sh — ayni uygulama once tek kopyayla, sonra iki kopyayla. Paylasilan katalog her kosumdan # once sifirlaniyor; defter ve kopyalara ozel makbuz dizinleri de siliniyor. kur() { # kur <kopya-sayisi> rm -rf makbuz-897* defter.txt echo '{"K1":1}' > katalog.json : > defter.txt P=""; N="" for i in $(seq 1 "$1"); do node topoloji/uygulama.mjs $((8970 + i)) & P="$P $!"; N="$N,$((8970 + i))"; done sleep 0.9 } kapat() { kill $P 2>/dev/null; wait $P 2>/dev/null; sleep 0.2; } kur 1; node topoloji/istemci.mjs "${N#,}" "tek kopya"; kapat kur 2; node topoloji/istemci.mjs "${N#,}" "iki kopya"; kapat echo node topoloji/tarayici.mjs topoloji/uygulama.mjs
tek kopya bellekteki oturum 6/6 dogru 0 yanlis oturum 8971 kopyasinda tek kopya yerel dosya 6/6 dogru 0 yanlis makbuz 8971 kopyasinda tek kopya surec ici onbellek 6/6 dogru 0 yanlis guncel deger 2 tek kopya zamanlayici 3/3 dogru 0 yanlis uye-7 cezasi 3 kez islendi tek kopya toplam yanlis sonuc alan istek 0/21 iki kopya bellekteki oturum 3/6 dogru 3 yanlis oturum 8971 kopyasinda iki kopya yerel dosya 3/6 dogru 3 yanlis makbuz 8972 kopyasinda iki kopya surec ici onbellek 3/6 dogru 3 yanlis guncel deger 2 iki kopya zamanlayici 3/6 dogru 3 yanlis uye-7 cezasi 6 kez islendi iki kopya toplam yanlis sonuc alan istek 12/21 durum turu cagri yeri satir bellekteki oturum 3 8, 29, 32 yerel dosya 3 17, 37, 41 surec ici onbellek 4 9, 48, 51, 52 zamanlayici 2 19, 21 toplam 12
Bütün sayılar belirlenimlidir: istek sayısı sabit, paylaştırma sırayla, zamanlayıcı tur sayısı üç.
Kodun Söylediği
Tarama elli beş satırlık bir kaynakta on iki çağrı yeri buluyor. Sayının kendisinden çok dağılımı önemlidir. Oturum üç yerde geçiyor: bildirildiği satır, yazıldığı satır, okunduğu satır. Önbellek dört yerde. Zamanlayıcı yalnız iki yerde ve o iki yer bir uç noktanın içinde bile değil — sunucu kurulmadan önce, modülün gövdesinde duruyor.
Bu dağılım bir düzeltme planının maliyetidir. Oturumu paylaşılan bir depoya taşımak üç satıra dokunur, önbelleği dörde. Zamanlayıcı iki satırdır ama ona dokunmak kodu değil yerleşimi değiştirir: işin bir kopyada çalışması, ötekilerde çalışmaması gerekir; bu bir kod satırı değil, bir dağıtım kararıdır.
Bir sayının okunmasında dikkat gerekir. Tarama on iki çağrı yeri, koşum on iki yanlış sonuç veriyor; ikisi aynı şey değildir ve eşitlikleri bu düzeneğe özgüdür. Biri kaynağın statik ölçüsüdür ve kopya sayısından bağımsızdır; öteki isteklerin çalışma anındaki ölçüsüdür ve kopya sayısıyla büyür.
Yirmi Bir İsteğin On İkisi
Tek kopyalı yerleşimde yirmi bir isteğin hepsi doğru sonuç aldı. Uygulama doğru çalışıyor, sınamalardan geçiyor ve hiçbir sızıntı görünmüyor. Sızıntıların hepsi tek kopyada doğru davranıştır: oturumu bellekte tutmak hızlıdır, makbuzu yerel diske yazmak ucuzdur, katalog önbelleği bir okuma tasarrufudur, zamanlayıcı işini yapar.
İkinci süreç eklendiğinde aynı kod yirmi bir isteğin on ikisinde yanlış sonuç veriyor. Dört türün her birinde tam üçer istek, çünkü paylaştırma sırayla yapılıyor ve durumun tutulduğu kopyaya isteklerin yarısı gidiyor.
- Oturum 8971 kopyasında açıldı; 8972’ye düşen üç istek 401 aldı. Üye giriş yaptığını biliyor ve sistem her ikinci isteğinde onu tanımıyor.
- Makbuz 8972 kopyasının yerel dizinine yazıldı; 8971’e düşen üç istek 404 aldı. Makbuz kaybolmadı, yanlış düğümde duruyor.
- Önbellek iki kopyada da ısıtıldı, güncelleme yalnız birinde geçersizleştirildi. Paylaşılan katalog 2 değerini tutuyor, üç istek 1 değerini alıyor — kod 200 ile.
- Zamanlayıcı iki kopyada da kuruldu ve gecikme cezası üç yerine altı kez işlendi. Bu üç fazla işlemin karşılığında hiçbir istek yoktur; kimse bir şey sormadı, bir durum kodu üretilmedi.
Dört Sızıntı, Dört Görünürlük
Dördü aynı ağırlıkta değildir ve sıralamayı belirleyen şey ne sıklıkları ne de sayılarıdır — dışarıdan ne olarak göründükleridir.
| Sızıntı | İstemcinin gördüğü | Kayıtta görünür mü | Veriye dokunur mu |
|---|---|---|---|
| Bellekteki oturum | 401 | evet, hata olarak | hayır |
| Yerel dosya | 404 | evet, hata olarak | hayır |
| Süreç içi önbellek | 200, eski değer | hayır | hayır |
| Zamanlayıcı | hiçbir şey | hayır | evet |
İlk iki satır gürültülüdür: yanlış sonuç bir hata kodu olarak döner, kayda düşer, sayaçta görünür. Rahatsız edicidir ve tam bu yüzden erken bulunur.
Üçüncü satır sessizdir. İstek başarılıdır, kayıt temizdir, gecikme düşüktür. Ödünç sisteminde bu, elde olmayan bir kitabın rafta görünmesidir. Üçüncü satırın ölçülebilmesinin tek nedeni istemcinin doğru cevabı önceden bilmesidir; bir kullanıcı bilmez.
Dördüncü satır hem sessizdir hem de tek başına paylaşılan veriyi bozar. Üye yediye gecikme cezası iki kez işlendi. İstek yok, hata yok, kayıtta bir sapma yok — yalnız defterde iki katı satır. Kopya sayısı üçe çıkarılsa ceza üç kat işlenirdi; sızıntının şiddeti doğrudan kopya sayısına bağlıdır ve kopya sayısı ölçekleme kararının kendisidir.
Ayarın Yeri
| Ayar | Yeri | Kaç yerde | Yanlış değerin sessiz sonucu |
|---|---|---|---|
| Kopya sayısı | süreç yöneticisi | 1 satır | uygulamada 12 çağrı yeri ayrışır, 21 isteğin 12’si yanlışlanır |
| Oturumun tutulduğu yer | uygulama kodu | 3 | her ikinci istekte oturum kaybı |
| Makbuz dizini | uygulama kodu | 3 | yazan kopya dışında makbuz yok |
| Önbellek geçersizleştirme | uygulama kodu | 4 | eski değer 200 ile döner |
| Zamanlayıcının kurulduğu yer | uygulama kodu, modül gövdesi | 2 | iş kopya sayısı kadar tekrar edilir |
Tablodaki asimetri bu dersin kuralıdır. Değişen ayar bir satırdır ve süreç yöneticisindedir; sonucunu belirleyen satırlar on iki tanedir ve uygulama kodundadır. Kopya sayısını artıran değişiklik uygulama kodunda hiçbir satıra dokunmaz, bu yüzden gözden geçirmede uygulama kodu hiç açılmaz.
Özet
- Aynı uygulama tek kopyayla yirmi bir isteğin hepsinde doğru sonuç verdi, iki kopyayla on ikisinde yanlış sonuç verdi; kodda tek satır değişmedi.
- Dört sızıntı türünün her biri tam üç isteği yanlışladı: oturum 401, makbuz 404, önbellek eski değeri 200 ile, zamanlayıcı üye yedinin gecikme cezasını üç yerine altı kez işleyerek.
- Kaynak taraması elli beş satırda on iki düğüme bağlı çağrı yeri buldu: oturum 3, yerel dosya 3, önbellek 4, zamanlayıcı 2; bu dağılım düzeltmenin dokunacağı satır sayısıdır.
- Statik on iki ile çalışma anındaki on iki farklı ölçülerdir; ilki kopya sayısından bağımsızdır, ikincisi kopya sayısıyla büyür.
- En sessiz iki sızıntı önbellek ile zamanlayıcıdır: biri yanlış değeri 200 ile döndürür, öteki hiçbir istek üretmeden paylaşılan veriyi bozar ve şiddeti kopya sayısıyla çarpılır.
Sonraki Adım
Dört sızıntının üçü için yön bellidir: durum sürecin dışına, kopyaların paylaştığı bir yere taşınır. Oturum için bu, akla gelen tek çözüm değildir ve çoğu zaman ilk denenen de o olmaz. Daha ucuz görünen bir yol vardır: durumu taşımak yerine isteği taşımak, yani aynı üyenin bütün isteklerini oturumunun açıldığı kopyaya göndermek. Bu, ölçümdeki üç yanlış isteği sıfıra indirir ve uygulama kodunda hiçbir satıra dokunmaz. Bedelini başka bir yerde ödetir. Sonraki ders bu yolu adlandırır ve faturasını çıkarır: dağılımın ne kadar bozulduğunu, bir kopya düştüğünde kaç oturumun kaybolduğunu ve oturumu paylaşılan bir depoya taşımanın aynı ölçümde ne değiştirdiğini sayar.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.