Ders 09 / 16
Eşzamanlı Girdi/Çıktı
Engelleyen çağrıların kapasiteye etkisinin ölçülmesi: aynı verim tavanını veren iki nedenin bağımlılık doluluğuyla ayrılması (0,9999 ve 0,1819), engelleyen tasarımın verimi eşzamanlılık sınırının bekleme artı iş süresine bölümüyle sınırlaması, aynı verimi taşımak için işçi sayısının 21 kat büyümesi ve düzeltmenin karşılığında uçuştaki isteğin 8'den 46'ya çıkması.
İçindekiler
Bir önceki dersin bütün ölçümleri bir sayıyı sabit tuttu: havuzun turda 540 birim işlediği. O sayının nereden geldiği hiç sorulmadı. Bir sürecin taşıyabileceği iş, işlemcisinin hızıyla değil, aynı anda kaç isteği ilerletebildiğiyle sınırlıdır.
Belirti şudur: verim turda 0,36 istekte tavan yaptı ve işçi eklemek bu sayıyı büyütmüyor. İşçilerin doluluğu 0,95 — havuz doymuş görünüyor. Bu ders o görüntünün yanıltıcı olabildiğini gösterir.
Aynı Belirti, İki Neden
Birinci neden: aşağı akış doymuş. Bağımlılık turda ne kadar çağrı karşılayabiliyorsa verim odur; işçi eklemek hiçbir şeyi değiştirmez, çünkü darboğaz çağıranda değildir.
İkinci neden: işçiler engelleyen çağrıda bekliyor. Bağımlılık boşta duruyor, ama her işçi bir engelleyen çağrının (blocking call) yanıtını beklerken başka hiçbir isteği ilerletemez. Buna eşzamanlı girdi/çıktı (synchronous I/O) denir.
İki durumda da işçi doluluğu 1’e yakın çıkar ve iki durumda da işçi eklemek çare gibi görünür. Ayıran ölçüm işçilerde değil, bağımlılıktadır: bağımlılığın kendi doluluğu 1’e yakınsa neden birincidir, düşükse ikincidir. Bu ölçüm alınmadan yapılan her açıklama tahmindir.
Engelleyen Çağrı Gerçekte Ne Yapar
Aşağıdaki blok bir model değil, bu makinede koşan gerçek bir ölçümdür. İki yüz dosya okunur; süre ölçülmez, çünkü süre bu makineye bağlıdır. Ölçülen nicelikler ikisi de belirlenimlidir: okuma sürerken ilerleyen başka iş sayısı ve eş zamanlı uçuştaki işlem sayısı.
// eszamanli/gercek.mjs — gercek node kosumu: engelleyen cagri sirasinda baska hicbir is // ilerleyemez. Olculen nicelik sure degil, ilerleyen is sayisi ve es zamanli ucustaki islem. import { mkdtempSync, writeFileSync, readFileSync, rmSync } from "node:fs"; import { readFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; const N = 200; const dizin = mkdtempSync(join(tmpdir(), "kk09-")); for (let i = 0; i < N; i += 1) writeFileSync(join(dizin, `k${i}`), "x".repeat(1024)); const yol = (i) => join(dizin, `k${i}`); // A: engelleyen cagri. Donguye girmeden once siraya konan is, dongu bitene kadar ilerleyemez. let ilerleyen = 0, ucus = 0, enBuyukUcusA = 0; const bekleyenIs = Promise.resolve().then(() => { ilerleyen += 1; }); for (let i = 0; i < N; i += 1) { ucus += 1; enBuyukUcusA = Math.max(enBuyukUcusA, ucus); readFileSync(yol(i)); ucus -= 1; } console.log(`engelleyen: dongu boyunca ilerleyen baska is = ${ilerleyen},` + ` en buyuk es zamanli ucus = ${enBuyukUcusA}, okunan dosya = ${N}`); await bekleyenIs; // B: engellemeyen cagri. Ayni is, cagrilar surerken ilerler. let ilerleyenB = 0, ucusB = 0, enBuyukUcusB = 0; const bekleyenIsB = Promise.resolve().then(() => { ilerleyenB += 1; }); const sonuc = await Promise.all(Array.from({ length: N }, async (_, i) => { ucusB += 1; enBuyukUcusB = Math.max(enBuyukUcusB, ucusB); const b = await readFile(yol(i)); ucusB -= 1; return b.length; })); await bekleyenIsB; console.log(`engellemeyen: cagrilar surerken ilerleyen baska is = ${ilerleyenB},` + ` en buyuk es zamanli ucus = ${enBuyukUcusB}, okunan dosya = ${sonuc.length}`); console.log(`es zamanli ucustaki islem orani = ${enBuyukUcusB / enBuyukUcusA}`); rmSync(dizin, { recursive: true, force: true });
engelleyen: dongu boyunca ilerleyen baska is = 0, en buyuk es zamanli ucus = 1, okunan dosya = 200 engellemeyen: cagrilar surerken ilerleyen baska is = 1, en buyuk es zamanli ucus = 200, okunan dosya = 200 es zamanli ucustaki islem orani = 200
İki sayı bütün mekaniği veriyor. Engelleyen düzende, döngüye girmeden önce sıraya konmuş bir iş iki yüz okuma bitene kadar hiç ilerlemiyor ve aynı anda uçuşta olan işlem sayısı 1’de kalıyor. Engellemeyen düzende aynı iş çağrılar sürerken ilerliyor ve uçuştaki işlem 200’e çıkıyor: 200 kat. Burada ölçülen şey koşutluk değildir — çalışma zamanının kaç işi gerçekten koşut yürüttüğü ayrı bir sorudur. Ölçülen şey, işçinin bloke olup olmadığıdır.
Dil düzeyinde eşzamansız model Eşzamansız JavaScript ve Çalışma Zamanı kursunda kuruldu; burada yeniden anlatılmaz. Buradaki soru, bu farkın kapasiteye ne yaptığıdır.
İki Nedeni Ayıran İki Ölçüm
Aşağıdaki koşum bir modeldir: tur soyut bir adımdır ve gerçek süreç ya da iş parçacığı kurulmaz. KK13 — çağrı profili. Eşzamanlılık sınırı 8 işçi, yerel iş 1 tur, engelleyen çağrının beklemesi 20 tur. Gerekçe: uzak bir bağımlılığın yanıtı yerel işten iki mertebe uzundur; duyarlılığı son tabloda bekleme süresi değiştirilerek verilir. KK14 — çevrim katsayısı. Bir tur 1 ms sayılır; bu yalnız verimi K01’in istek/s ölçüsüne çevirmek içindir. Bu varsayımlar K01’in tablosuna eklenmez.
// eszamanli/kapasite.mjs — isci havuzunun MODELI. Tur soyut bir adimdir; KK14 ile bir tur // 1 ms sayilir (cevrim katsayisi varsayim). Gercek surec ya da is parcacigi kurulmaz. export const K01 = { tepeUc: 513.89 }; export function kosum({ tur = 20_000, sinir = 8, is = 1, bekleme = 20, bagimlilik = 2.0, engelleyen = true, teklif = 3 }) { let bosIsci = sinir, kuyruk = 0, tBirikim = 0, jBirikim = 0; const aktif = []; // { asama: "is" | "jeton" | "bekle", kalan } const s = { tamam: 0, calisan: 0, bekleyen: 0, jeton: 0, enBuyukUcus: 0 }; for (let t = 1; t <= tur; t += 1) { tBirikim += teklif; while (tBirikim >= 1) { tBirikim -= 1; kuyruk += 1; } jBirikim += bagimlilik; let jeton = Math.floor(jBirikim); jBirikim -= jeton; while (bosIsci > 0 && kuyruk > 0) { kuyruk -= 1; bosIsci -= 1; aktif.push({ asama: "is", kalan: is }); } for (let i = aktif.length - 1; i >= 0; i -= 1) { const r = aktif[i]; if (r.asama === "is") { r.kalan -= 1; s.calisan += 1; if (r.kalan === 0) r.asama = "jeton"; } else if (r.asama === "jeton") { if (jeton > 0) { jeton -= 1; s.jeton += 1; r.asama = "bekle"; r.kalan = bekleme; if (!engelleyen) bosIsci += 1; // isci cagriyi baslatip serbest kalir } else if (engelleyen) s.bekleyen += 1; // jeton yokken isci elde tutuluyor } else { r.kalan -= 1; if (engelleyen) s.bekleyen += 1; if (r.kalan === 0) { s.tamam += 1; if (engelleyen) bosIsci += 1; aktif.splice(i, 1); } } } s.enBuyukUcus = Math.max(s.enBuyukUcus, aktif.length); } const isciTur = s.calisan + s.bekleyen; return { verim: s.tamam / tur, isciDoluluk: isciTur / (sinir * tur), beklemePayi: isciTur === 0 ? 0 : s.bekleyen / isciTur, bagimlilikDoluluk: s.jeton / (bagimlilik * tur), ucus: s.enBuyukUcus, }; }
// eszamanli/olcum.mjs — ayni belirtiyi veren iki neden, ayirt eden iki olcum ve karsiligi import { K01, kosum } from "./kapasite.mjs"; const EN = [34, 8, 12, 13, 13, 8]; const yaz = (h) => console.log(h.map((x, i) => (i ? String(x).padStart(EN[i]) : String(x).padEnd(EN[i]))).join(" ")); const cizgi = () => console.log(EN.map((n) => "-".repeat(n)).join(" ")); const satir = (ad, r) => yaz([ad, r.verim.toFixed(4), r.isciDoluluk.toFixed(4), r.beklemePayi.toFixed(4), r.bagimlilikDoluluk.toFixed(4), r.ucus]); const [SINIR, IS, BEKLEME] = [8, 1, 20]; console.log(`eszamanlilik siniri ${SINIR} isci, yerel is ${IS} tur, engelleyen cagrinin beklemesi ${BEKLEME} tur`); console.log(`aritmetik ust sinir: engelleyen ${SINIR}/(${BEKLEME}+${IS}) = ${(SINIR / (BEKLEME + IS)).toFixed(4)} istek/tur,` + ` engellemeyen min(${SINIR}/${IS}, bagimlilik) istek/tur\n`); yaz(["durum", "verim", "isci dol.", "bekleme payi", "bagim. dol.", "ucus"]); cizgi(); satir("neden 1: bagimlilik doymus (D=0,35)", kosum({ bagimlilik: 0.35 })); satir("neden 2: engelleyen cagri (D=2,0)", kosum({ bagimlilik: 2.0 })); console.log(); satir("neden 1, engellemeyen", kosum({ bagimlilik: 0.35, engelleyen: false })); satir("neden 2, engellemeyen", kosum({ bagimlilik: 2.0, engelleyen: false })); const e = kosum({ bagimlilik: 2.0 }); const n = kosum({ bagimlilik: 2.0, engelleyen: false }); console.log(`\nneden 2'de duzeltmenin kazanci: ${e.verim.toFixed(4)} -> ${n.verim.toFixed(4)} istek/tur` + ` = ${(n.verim / e.verim).toFixed(2)} kat`); console.log(`ayni verimi engelleyen tasarimla tasimak icin gereken isci = ${n.verim} x (${BEKLEME}+${IS})` + ` = ${(n.verim * (BEKLEME + IS)).toFixed(0)}, yani ${((BEKLEME + IS) / IS).toFixed(0)} kat`); console.log(`karsiliginda buyuyen ucustaki istek: ${e.ucus} -> ${n.ucus} (${(n.ucus / e.ucus).toFixed(2)} kat)` + ` ve bagimlilik dolulugu ${e.bagimlilikDoluluk.toFixed(4)} -> ${n.bagimlilikDoluluk.toFixed(4)}`); // KK14: bir tur 1 ms sayilir; verim istek/s'ye cevrilir ve K01'in tepe hiziyla karsilastirilir. const TUR_MS = 1; const saniye = (r) => (r.verim * 1000) / TUR_MS; console.log(`\nbir tur ${TUR_MS} ms (KK14): tek surecin tasidigi istek engelleyen ${saniye(e).toFixed(2)} istek/s,` + ` engellemeyen ${saniye(n).toFixed(2)} istek/s`); console.log(`K01 tepe ucta ${K01.tepeUc} istek/s -> gereken surec sayisi ` + `${Math.ceil(K01.tepeUc / saniye(e))} ve ${Math.ceil(K01.tepeUc / saniye(n))}`); console.log(`\nisci eklemenin engelleyen tasarimdaki etkisi (D=2,0):`); yaz(["durum", "verim", "isci dol.", "bekleme payi", "bagim. dol.", "ucus"]); cizgi(); for (const sinir of [8, 16, 42, 84]) satir(`sinir ${sinir} isci`, kosum({ sinir, bagimlilik: 2.0 })); console.log(`\nKK13'un duyarliligi: bekleme kisaldikca engelleyen tasarimin bedeli (D=2,0)`); for (const bekleme of [20, 5, 2, 1]) { const a = kosum({ bekleme, bagimlilik: 2.0 }), b = kosum({ bekleme, bagimlilik: 2.0, engelleyen: false }); console.log(` bekleme ${String(bekleme).padStart(2)} tur -> engelleyen ${a.verim.toFixed(4)},` + ` engellemeyen ${b.verim.toFixed(4)}, oran ${(b.verim / a.verim).toFixed(2)}`); }
eszamanlilik siniri 8 isci, yerel is 1 tur, engelleyen cagrinin beklemesi 20 tur aritmetik ust sinir: engelleyen 8/(20+1) = 0.3810 istek/tur, engellemeyen min(8/1, bagimlilik) istek/tur durum verim isci dol. bekleme payi bagim. dol. ucus ---------------------------------- -------- ------------ ------------- ------------- -------- neden 1: bagimlilik doymus (D=0,35) 0.3496 0.9562 0.9542 0.9999 8 neden 2: engelleyen cagri (D=2,0) 0.3635 0.9545 0.9524 0.1819 8 neden 1, engellemeyen 0.3496 0.0438 0.0000 0.9999 15 neden 2, engellemeyen 1.9979 0.2500 0.0000 1.0000 46 neden 2'de duzeltmenin kazanci: 0.3635 -> 1.9979 istek/tur = 5.50 kat ayni verimi engelleyen tasarimla tasimak icin gereken isci = 1.9979 x (20+1) = 42, yani 21 kat karsiliginda buyuyen ucustaki istek: 8 -> 46 (5.75 kat) ve bagimlilik dolulugu 0.1819 -> 1.0000 bir tur 1 ms (KK14): tek surecin tasidigi istek engelleyen 363.50 istek/s, engellemeyen 1997.90 istek/s K01 tepe ucta 513.89 istek/s -> gereken surec sayisi 2 ve 1 isci eklemenin engelleyen tasarimdaki etkisi (D=2,0): durum verim isci dol. bekleme payi bagim. dol. ucus ---------------------------------- -------- ------------ ------------- ------------- -------- sinir 8 isci 0.3635 0.9545 0.9524 0.1819 8 sinir 16 isci 0.7267 0.9544 0.9524 0.3636 16 sinir 42 isci 1.9071 0.9542 0.9524 0.9545 42 sinir 84 isci 1.9979 0.9755 0.9756 1.0000 82 KK13'un duyarliligi: bekleme kisaldikca engelleyen tasarimin bedeli (D=2,0) bekleme 20 tur -> engelleyen 0.3635, engellemeyen 1.9979, oran 5.50 bekleme 5 tur -> engelleyen 1.1426, engellemeyen 1.9994, oran 1.75 bekleme 2 tur -> engelleyen 1.9997, engellemeyen 1.9997, oran 1.00 bekleme 1 tur -> engelleyen 1.9998, engellemeyen 1.9998, oran 1.00
Uçuş sayıları ve ilerleyen iş sayısı ölçüm, model çıktısındaki verim ve doluluklar belirlenimli koşumdan çıkan hesap, çağrı profili ve çevrim katsayısı varsayım sınıfındadır.
Ölçüm Nedeni Söylüyor
İlk tablonun iki satırı aynı belirtiyi veriyor: verim 0,3496 ile 0,3635, işçi doluluğu 0,9562 ile 0,9545, bekleme payı 0,9542 ile 0,9524. İşçiler doymuş görünüyor ve iki durumda da vakitlerinin yüzde doksan beşi bekleyerek geçiyor.
Ayrım son sütundan bir öncekinde. Birinci nedende bağımlılık doluluğu 0,9999 — bağımlılık gerçekten doymuş. İkinci nedende 0,1819 — bağımlılık kapasitesinin beşte dördü boşta dururken verim tavana çarpıyor. Tek başına işçi doluluğuna bakan bir tanı iki durumu ayıramaz; bağımlılığın kendi doluluğu ayırır.
Doğrulaması ikinci tablodadır. Engelleyen çağrı kaldırıldığında birinci nedende hiçbir şey değişmiyor (0,3496 → 0,3496), ikinci nedende verim 0,3635’ten 1,9979’a, yani 5,50 kata çıkıyor. Aynı düzeltme bir nedende işe yarar, ötekinde yaramaz — bu yüzden ölçüm düzeltmeden önce gelir.
Tavanın kaynağı aritmetiktir ve ilk satırda yazılı: engelleyen tasarımda verim, eşzamanlılık sınırının bekleme artı iş süresine bölümünü aşamaz — 8 / (20 + 1) = 0,3810. Model 0,3635 ölçüyor; aradaki fark bağımlılığın jeton dağıtımının turlara yayılmasından gelir.
Üçüncü tablo işçi eklemenin ne yaptığını gösteriyor. Sınır 8’den 16’ya çıkınca verim 0,7267’ye, 42’ye çıkınca 1,9071’e ulaşıyor; yani engelleyen tasarım aynı verimi ancak 21 kat işçiyle taşıyor. İşçi doluluğu bu satırların hepsinde 0,95 civarında kalıyor — doluluk hiçbir zaman sorunu göstermiyor, yalnız işçilerin meşgul olduğunu söylüyor.
Düzeltmenin Karşılığında Büyüyen
Engellemeyen tasarım bedava değildir ve bedeli sayılabilir.
Uçuştaki istek. Aynı anda sistemde duran istek sayısı 8’den 46’ya çıkıyor: 5,75 kat. Her uçuştaki istek bellek tutar — tampon, bağlam, kısmi yanıt. Engelleyen tasarımda bu sayıyı eşzamanlılık sınırı kendiliğinden bağlıyordu; engellemeyen tasarımda böyle bir sınır yoktur ve kabul denetimi ayrıca konmalıdır. Önbellekleme, Kuyruklar ve Eşzamansız İşleme kursunun kabul denetimi ve eşzamanlılık sınırı dersleri tam olarak bu boşluğu doldurur.
Darboğazın yer değiştirmesi. Bağımlılık doluluğu 0,1819’dan 1,0000’e çıkıyor. Çağıran düzeldiğinde yük bağımlılığa iniyor ve bir sonraki tanı orada yapılacaktır.
Taşınan istek. KK14 ile bir tur 1 ms sayıldığında tek süreç engelleyen tasarımda 363,50 istek/s, engellemeyen tasarımda 1997,90 istek/s taşıyor. K01’in 513,89 istek/s tepe hızı için gereken süreç sayısı 2 ile 1 arasında değişiyor: aynı donanım, iki kat fark.
Engelleyen Çağrının Doğru Olduğu Koşul
Son tablo sınırı veriyor. Bedel, beklemenin yerel işe oranından doğar. Bekleme 20 turken oran 5,50; 5 turken 1,75; 2 turken 1,00. Bekleme yerel işle aynı mertebeye indiğinde engelleyen tasarım hiçbir şey kaybettirmez, çünkü işçinin tuttuğu süre zaten çalışarak geçiyordur.
İkinci koşul birinci nedenden çıkar: bağımlılık doymuşsa engellememenin kazancı sıfırdır (0,3496 → 0,3496). Darboğaz aşağıdaysa çağıranı karmaşıklaştırmak yalnız uçuştaki isteği büyütür.
Özet
- Belirti verimin tavana çarpması ve işçi eklemenin işe yaramamasıdır; iki nedeni vardır — aşağı akış doymuştur ya da işçiler engelleyen çağrıda beklemektedir.
- İşçi doluluğu iki nedeni ayıramaz: 0,9562 ile 0,9545, bekleme payı 0,9542 ile 0,9524. Ayıran ölçüm bağımlılığın kendi doluluğudur — 0,9999 ile 0,1819.
- Engelleyen tasarımda verim, eşzamanlılık sınırının bekleme artı iş süresine bölümünü aşamaz: 8 / (20 + 1) = 0,3810 istek/tur; aynı verimi taşımak için işçi sayısı 21 kat büyümelidir.
- Düzeltme ikinci nedende verimi 0,3635’ten 1,9979’a (5,50 kat) çıkarıyor, birinci nedende hiçbir şeyi değiştirmiyor; bir tur 1 ms sayıldığında tek süreç 363,50 yerine 1997,90 istek/s taşıyor.
- Karşılığında uçuştaki istek 8’den 46’ya (5,75 kat) çıkıyor ve bağımlılık doluluğu 0,1819’dan 1,0000’e yükseliyor: sınır kalktığı için kabul denetimi ayrıca konmalıdır.
- Engelleyen çağrı, bekleme yerel işle aynı mertebedeyken doğrudur: bekleme 2 turda oran 1,00’e iniyor.
Sonraki Adım
Dokuz kalıbın hepsi bir varsayımın üstünde duruyor: bir isteğin bir kez yapıldığı. Uçtan gelen yük dışarıdan belirlenir, sistem onu yalnız taşır. Bu varsayım bir şey bozulduğu anda düşer. Çağrılar başarısız olmaya başladığında çağıran yeniden dener, denemeler zincirin her katmanında çarpılır ve bağımlılığa varan yük artık dışarıdan değil, sistemin kendisinden gelir. Sonraki ders konunun son tanısıdır ve sorusu şudur: böyle bir döngü dışarıdan nasıl görünür. Bağımlılığa varan çağrı hızının iki katına çıkması hem gerçek bir talep artışının hem de bir yeniden deneme fırtınasının belirtisi olabilir; ikisini ayıran ölçüt çağrı sayısı değildir.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.