Ders 11 / 16
Yatay ve Dikey Ölçekleme
İki ölçekleme ekseninin backend tarafındaki faturası: aynı yükün tek büyük süreçle ve dört küçük süreçle karşılanmasında süreç tabanının kaç kez ödendiği, hazır olma süresinin sıralı yayında nasıl çarpıldığı, her sürecin kendi bağlantı havuzunu açmasından doğan çarpan ve süreç sayısı artarken değişmeyen havuz ayarının depoya düşen toplam bağlantıyı sınırın üstüne taşıması.
İçindekiler
Önceki iki ders tek bir süreçle çalıştı. Vekil tek bir hedefe iletti, süreç yöneticisi tek bir işçiyi başlattı ve bellek sınırı o tek sürecin sınırıydı. Yük büyüdüğünde iki yol vardır: aynı sürece daha çok kaynak vermek — dikey ölçekleme — ya da daha çok süreç açmak — yatay ölçekleme.
İki eksenin hızlanma tarafı, seri payın koyduğu tavan ve kopya eklemenin azalan getirisi Sistem Tasarımına Giriş kursunda kurulmuştu; burada girdi olarak alınır ve tekrarlanmaz. Bu dersin ölçtüğü şey backend tarafındadır: iki yerleşim aynı yükü karşıladığında arka planda ne kadar fazla kaynak tutulduğu ve süreç sayısı değiştiğinde hangi ayarın sessizce yanlışa döndüğü.
Üç Yerleşim, Aynı Yük
Düzenek üç parçadan oluşuyor: bir kayıt deposu, ödünç uygulamasının bir ya da dört kopyası ve kopyalar arasında sırayla dağıtan bir süreç.
- DP1. Depo eş zamanlı 16 bağlantı kabul eder; on yedincisi kurulur kurulmaz kapatılır. Bir sorgu 20 milisaniye sürer.
- DP2. Her uygulama kopyası başlarken havuzu kadar kalıcı bağlantı açar (ısınma). Hiçbiri kurulamazsa havuz boştur ve süreç isteği depoya hiç gitmeden reddeder.
- DP3. Kopyalar sırayla kaldırılır; havuzunu önce açan kopya bağlantıyı önce alır. Bellek süreç başına ortalanır, hazır olma süresi kopyaların en uzunudur; ikisi de 10’a yuvarlanır ve koşuma bağlıdır. Çarpanlar koşuma bağlı değildir.
// topoloji/depo.mjs — kayit deposu. Es zamanli baglanti sayisinin bir ustu vardir; asan baglanti // kurulur kurulmaz kapatilir. Sayaclar ayri bir noktadan okunur ki olcum baglantisi sayima girmesin. import { createServer } from "node:http"; const [nokta, olcumNoktasi, sinir, sorguMs] = process.argv.slice(2, 6).map(Number); let acik = 0, tepe = 0, reddedilen = 0, sorgu = 0; if (Number.isInteger(nokta) === false) console.log("kullanim: node topoloji/depo.mjs <nokta> <olcum-noktasi> <baglanti-siniri> <sorgu-ms>"); else { const s = createServer((istek, cevap) => { sorgu += 1; cevap.sendDate = false; setTimeout(() => cevap.writeHead(200).end("kayit"), sorguMs); }); s.on("connection", (soket) => { acik += 1; tepe = Math.max(tepe, acik); soket.on("close", () => { acik -= 1; }); if (acik > sinir) { reddedilen += 1; soket.destroy(); } // sinir asildi: baglanti kapatiliyor }); s.listen(nokta, "127.0.0.1"); createServer((istek, cevap) => { cevap.sendDate = false; cevap.writeHead(200).end(JSON.stringify({ tepe, reddedilen, sorgu })); }).listen(olcumNoktasi, "127.0.0.1"); }
Uygulama kopyası havuz boyunu bir argüman olarak alır. Bu sayı yerleşimin tek ayarıdır ve tek süreçliyken doğru seçilmiştir.
// topoloji/uygulama.mjs — odunc surecinin bir kopyasi. Baslarken depoya <havuz> kadar kalici // baglanti acar (isinma). Hicbiri kurulamazsa havuz bostur ve surec istegi depoya hic gitmeden // reddeder; kurulanlar varsa her istek havuzdaki bir baglantiyla karsilanir. import { createServer, request, Agent } from "node:http"; const baslangic = Date.now(); const [nokta, depo, havuzBoyu] = process.argv.slice(2, 5).map(Number); const havuz = new Agent({ keepAlive: true, maxSockets: havuzBoyu }); let hazirMs = -1, isinmaHatasi = 0, yanit = 0, hata = 0; const depoya = (yol) => new Promise((coz) => { const r = request({ port: depo, path: yol, agent: havuz }, (y) => { y.resume(); y.on("end", () => coz(true)); }); r.on("error", () => coz(false)); r.end(); }); if (Number.isInteger(nokta) === false) console.log("kullanim: node topoloji/uygulama.mjs <nokta> <depo> <havuz>"); else { const sonuc = await Promise.all(Array.from({ length: havuzBoyu }, () => depoya("/isinma"))); isinmaHatasi = sonuc.filter((b) => b === false).length; hazirMs = Date.now() - baslangic; createServer(async (istek, cevap) => { cevap.sendDate = false; if (istek.url === "/olcum") { const rss = Math.round(process.memoryUsage().rss / (1024 * 1024)); cevap.writeHead(200).end(JSON.stringify({ rss, hazirMs, isinmaHatasi, yanit, hata })); return; } if (isinmaHatasi === havuzBoyu) { hata += 1; cevap.writeHead(503).end("havuz bos"); } else if (await depoya("/kayit")) { yanit += 1; cevap.writeHead(200).end("odunc"); } else { hata += 1; cevap.writeHead(503).end("depo yok"); } }).listen(nokta, "127.0.0.1"); }
// topoloji/dagitici.mjs — istekleri kopyalar arasinda sirayla dagitir. Kopya listesi virgulle verilir. import { createServer, request } from "node:http"; const [nokta, listeMetni] = [Number(process.argv[2]), process.argv[3] ?? ""]; const HEDEF = listeMetni.split(",").filter(Boolean).map(Number); let sira = 0; if (Number.isInteger(nokta) === false || HEDEF.length === 0) console.log("kullanim: node topoloji/dagitici.mjs <nokta> <hedef,hedef,...>"); else createServer((istekDisi, yanitDisi) => { yanitDisi.sendDate = false; const hedef = HEDEF[sira++ % HEDEF.length]; const istekIc = request({ port: hedef, path: istekDisi.url, method: istekDisi.method }, (yanitIc) => { yanitDisi.writeHead(yanitIc.statusCode); yanitIc.pipe(yanitDisi); }); istekIc.on("error", () => yanitDisi.writeHead(502).end("kopya yok")); istekDisi.pipe(istekIc); }).listen(nokta, "127.0.0.1");
// topoloji/yuk.mjs — <adet> istegi <esz> es zamanlilikla gonderir, sonra kopyalarin ve deponun // sayaclarini toplar. Bellek ve hazir olma sureleri kopya basina okunup toplanir. import { request } from "node:http"; const [nokta, adet, esz, depoOlcum, kopyaMetni, etiket] = [Number(process.argv[2]), Number(process.argv[3]), Number(process.argv[4]), Number(process.argv[5]), process.argv[6] ?? "", process.argv[7] ?? "-"]; const KOPYA = kopyaMetni.split(",").filter(Boolean).map(Number); let yanit = 0, reddedilen = 0; const bir = () => new Promise((coz) => { const r = request({ port: nokta, path: "/odunc" }, (y) => { y.resume(); y.on("end", () => { if (y.statusCode === 200) yanit += 1; else reddedilen += 1; coz(); }); }); r.on("error", () => { reddedilen += 1; coz(); }); r.end(); }); const oku = (p, yol) => new Promise((coz) => { request({ port: p, path: yol, agent: false }, (y) => { let b = ""; y.on("data", (d) => { b += d; }); y.on("end", () => coz(JSON.parse(b))); }).end(); }); if (Number.isInteger(nokta) === false) console.log("kullanim: node topoloji/yuk.mjs <nokta> <adet> <esz> <depo-olcum> <kopyalar> <etiket>"); else { let gonderilen = 0; await Promise.all(Array.from({ length: esz }, async () => { while (gonderilen++ < adet) await bir(); })); const k = await Promise.all(KOPYA.map((p) => oku(p, "/olcum"))); const d = await oku(depoOlcum, "/olcum"); const toplam = (alan) => k.reduce((t, x) => t + x[alan], 0); const P = KOPYA.length; // Surec basina bellek ortalamasi ve en uzun hazir olma suresi 10'a yuvarlanir, carpimla yazilir. const bellek = Math.round(toplam("rss") / P / 10) * 10; const hazir = Math.round(Math.max(...k.map((x) => x.hazirMs)) / 10) * 10; console.log(`${etiket} yanit ${String(yanit).padStart(2)}/${adet} ` + `depo tepe ${String(d.tepe).padStart(2)} baglanti, ${String(d.reddedilen).padStart(2)} reddedilen ` + `isinma hatasi ${String(toplam("isinmaHatasi")).padStart(2)} ` + `bellek ${P}x${bellek}=${String(P * bellek).padStart(3)} MB hazir ${P}x${hazir}=${String(P * hazir).padStart(3)} ms`); }
Üç yerleşim aynı altmış isteği aynı eş zamanlılıkla karşılıyor. Birincisi tek süreç, havuz sekiz. İkincisi dört süreç ve aynı havuz ayarı — süreç sayısı büyütülürken havuza dokunulmamış. Üçüncüsü dört süreç, havuz iki.
# olc.sh — ayni yuk uc yerlesimle. Depo es zamanli 16 baglantiyi kabul ediyor, sorgu 20 ms. # Kopyalar sirayla kaldiriliyor: havuzunu once acan kopya baglantiyi once aliyor. kur() { # kur <havuz> <kopya-sayisi> node topoloji/depo.mjs 8950 8959 16 20 & D=$! P=""; N="" for i in $(seq 1 "$2"); do node topoloji/uygulama.mjs $((8950 + i)) 8950 "$1" & P="$P $!" N="$N,$((8950 + i))" sleep 0.25 done node topoloji/dagitici.mjs 8960 "${N#,}" & G=$! sleep 1 } kapat() { kill $D $G $P 2>/dev/null; wait $D $G $P 2>/dev/null; sleep 0.3; } kur 8 1; node topoloji/yuk.mjs 8960 60 32 8959 "8951" "tek surec, havuz 8 "; kapat kur 8 4; node topoloji/yuk.mjs 8960 60 32 8959 "8951,8952,8953,8954" "dort surec, havuz 8 "; kapat kur 2 4; node topoloji/yuk.mjs 8960 60 32 8959 "8951,8952,8953,8954" "dort surec, havuz 2 "; kapat
tek surec, havuz 8 yanit 60/60 depo tepe 8 baglanti, 0 reddedilen isinma hatasi 0 bellek 1x60= 60 MB hazir 1x30= 30 ms dort surec, havuz 8 yanit 30/60 depo tepe 17 baglanti, 16 reddedilen isinma hatasi 16 bellek 4x60=240 MB hazir 4x30=120 ms dort surec, havuz 2 yanit 60/60 depo tepe 8 baglanti, 0 reddedilen isinma hatasi 0 bellek 4x60=240 MB hazir 4x30=120 ms
Bellek ve süre sütunları koşuma bağlıdır; sayılar bu koşumdandır. Kopya sayısı, havuz boyu, depo sınırı, istek sayısı ve reddedilen bağlantı sayısı koşumdan bağımsızdır.
Taban Kaç Kez Ödenir
Birinci ve üçüncü satır aynı işi yapıyor: altmış isteğin altmışı yanıtlandı. Bellek toplamı 60 MB’den 240 MB’ye çıktı.
Fark isteğin kendisinden gelmiyor. Aynı altmış istek, aynı yirmi milisaniyelik sorgu, aynı yanıt. Değişen şey tabanın kaç kez ödendiğidir: yorumlayıcı, yüklenen kod, olay döngüsü ve süreç başına ayrılan yığın, süreç başına 60 MB tutuyor ve dört süreç bunu dört kez ödüyor. Çıktı bu yüzden toplamı değil çarpımı yazıyor — okunması gereken sayı çarpandır.
İki eksenin faturası burada ayrışır. Dikey ölçekleme tabanı bir kez öder: süreç büyür, taban sabit kalır, artan bellek işe gider. Yatay ölçekleme tabanı süreç sayısı kadar öder: dört kopyaya geçmek, hiç istek gelmese bile 180 MB’yi işten önce harcar. Kopya sayısı arttıkça bu kalem doğrusal büyür ve iş yapmaz.
Bu, yatay ölçeklemenin yanlış olduğunu söylemez; K01’in kopya tablosu yatay eksenin niçin seçildiğini zaten kurmuştu. Söylediği şey, karşılaştırmanın kaynak toplamı üzerinden yapılması gerektiğidir: dört küçük süreç, aynı toplam belleği kullanan tek bir büyük süreçten daha az iş belleğine sahiptir.
Hazır Olma Süresi
Bir kopya, havuzunu açıp isteğe hazır hâle gelene kadar 30 milisaniye harcıyor. Bu sayı üç yerleşimde de aynı; kopya sayısı onu değiştirmiyor.
Değişen, o sürenin kaç kez ödendiğidir. Dört kopya koşut başlatıldığında duvar saati tek kopyanınkine yakın kalır. Kopyalar sırayla kaldırıldığında — bir yayında kopyaların teker teker değiştirilmesi tam olarak budur — süreler toplanır ve duvar saatine 120 milisaniye yazılır. Aynı 30 milisaniye, dört kopyada dört kez ödenir ve kopya sayısıyla doğrusal büyür.
Sayının kendisi küçüktür, çarpanı değildir. Isınmanın bir depo bağlantısı yerine yüklenecek bir dizin, kurulacak bir bağlantı kümesi ya da ısıtılacak bir önbellek olduğu bir uygulamada aynı çarpan saniyelere uygulanır; sıralı bir yayının süresi kopya sayısına bağlanır.
Havuz Çarpanı
İkinci satır bu dersin asıl ölçüsüdür. Havuz ayarı değişmedi — tek süreçte doğru olan sekiz, dört süreçte de sekiz. Değişen tek şey süreç sayısı.
Her süreç kendi havuzunu açar. Depoya düşen bağlantı sayısı süreç başına havuz değil, süreç sayısı çarpı havuzdur:
| Yerleşim | Süreç | Süreç başına havuz | Depoya düşen | Depo sınırı | Sonuç |
|---|---|---|---|---|---|
| tek süreç | 1 | 8 | 8 | 16 | 60/60 yanıt |
| dört süreç, havuz 8 | 4 | 8 | 32 | 16 | 30/60 yanıt, 16 reddedilen bağlantı |
| dört süreç, havuz 2 | 4 | 2 | 8 | 16 | 60/60 yanıt |
İkinci satırda depo tepe olarak 17 bağlantı gördü — sınırın bir üstü, çünkü sınırı aşan bağlantı sayılıp sonra kapatılıyor. Otuz iki bağlantı isteğinin on altısı kapatıldı; kapatılanların hepsi sonradan kalkan iki kopyanın ısınma bağlantılarıydı.
Sonuç bu iki kopyanın boş havuzla ayakta kalmasıdır. Süreçleri çalışıyor, bağlantı noktalarını dinliyor, süreç yöneticisi dört canlı süreç sayıyor ve dağıtıcı dört hedef görüyor. Altmış isteğin otuzu bu iki kopyaya gidiyor ve depoya hiç ulaşmadan 503 alıyor. Ayakta olmakla iş yapabilmek arasındaki fark, yerleşim ölçeklendiği anda açılıyor.
Bu ölçünün sessiz tarafı yapılandırmanın nerede olmadığındadır. Havuz boyu uygulama kodunda tek bir sayıdır ve doğrudur. Süreç sayısı süreç yöneticisinde başka bir sayıdır ve o da doğrudur. Depoya düşen toplam bağlantı hiçbir yerde yazmaz; iki ayarın çarpımıdır ve o çarpımı tutan bir dosya yoktur. Kopya sayısını ikiden dörde çıkaran değişiklik havuz satırına dokunmaz, gözden geçirmede havuz satırı görünmez ve sonuç bir hata iletisi olarak değil, depo tarafında kapanan bağlantılar olarak ortaya çıkar.
Üçüncü satır düzeltmeyi gösteriyor: çarpım korunuyor. Dört süreç, havuz iki, depoya düşen sekiz. Aynı altmış istek yeniden yanıtlanıyor. Düzeltme havuzu küçültmektir ve bedeli vardır — süreç başına eş zamanlılık dörtte bire iner. Yatay ölçeklemenin depo tarafındaki gerçek kısıtı budur: kopya sayısı bağlantı bütçesini böler.
Ayarın Yeri
| Ayar | Yeri | Kaç yerde | Yanlış değerin sessiz sonucu |
|---|---|---|---|
| Kopya sayısı | süreç yöneticisi | 1 | bellek tabanı kopya sayısı kadar ödenir |
| Süreç başına havuz | uygulama kodu | 1 (her kopyada aynı) | çarpım depo sınırını aşar, bağlantılar sessizce kapanır |
| Depo bağlantı sınırı | depo | 1 | düşük sınır iyi istekleri, yüksek sınır depoyu düşürür |
| Isınma bağlantısı | uygulama kodu | 1 | havuzu kurulamayan kopya ayakta kalır ve payına düşen isteği reddeder |
Dört satırın ikisi tek başına doğru, birlikte yanlış olabilir. Kopya sayısı ile havuz boyu ayrı dosyalarda durur, ayrı zamanlarda değişir ve aralarındaki çarpımı denetleyen bir yer yoktur.
Özet
- Aynı altmış istek tek süreçte 60 MB, dört süreçte 240 MB tuttu; artan şey iş belleği değil, süreç tabanının dört kez ödenmesidir.
- Kopya başına 30 milisaniyelik hazır olma süresi kopya sayısıyla çarpılır; dört kopya sırayla kaldırıldığında duvar saatine 120 milisaniye yazılır.
- Süreç sayısı birden dörde çıkarılıp havuz ayarına dokunulmadığında depoya düşen bağlantı 8’den 32’ye çıktı, depo 16 bağlantıyı kapattı, 16 ısınma isteği başarısız oldu ve altmış isteğin yalnız otuzu yanıtlandı.
- Havuzu kurulamayan iki kopya ayakta kaldı: süreç yöneticisi dört canlı süreç, dağıtıcı dört hedef sayarken bu iki kopya payına düşen otuz isteği depoya hiç gitmeden reddetti.
- Havuzu ikiye indirmek çarpımı sekizde tuttu ve altmış isteğin altmışı yeniden yanıtlandı; bedeli süreç başına eş zamanlılığın dörtte bire inmesidir.
- Depoya düşen toplam bağlantı iki ayarın çarpımıdır ve hiçbir dosyada yazmaz; iki ayar ayrı yerlerde durduğu için değişiklik gözden geçirmede görünmez.
Sonraki Adım
Bu ders kopya sayısını bir ayar gibi ele aldı: süreç yöneticisine bir sayı yazıldı, dört süreç kalktı, dağıtıcı istekleri sırayla paylaştırdı. Üç 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. Uygulama kodunda bellekte bir oturum, yerel diskte bir dosya, süreç içinde bir önbellek ya da kurulmuş bir zamanlayıcı varsa kopyalar birbirinin eşi olmaktan çıkar. Sonraki ders bu bağların kodda nerede durduğunu sayar: kaç çağrı yeri düğüme bağlıdır ve ikinci süreç eklendiğinde kaç istek yanlış sonuç alır.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.