Ders 21 / 22
Ölçüm ve Yavaş Komut Analizi
Tek iş parçacıklı bellek içi deponun başarım tanısı: uzun süren tek bir komutun arkasında bekleyen istek sayısının ve toplam gecikmenin sayılması, taramanın imleçle parçalanmasının kazandırdığı ile kaçırdığı giriş, yavaş komut günlüğü eşiğinin taranması.
İçindekiler
Önceki dersin dört bellek hesabı da sessiz bir varsayıma yaslanıyordu: her erişim sırası geldiğinde yapılıyor, hiçbir istek bir başkasını beklemiyor. Bellek içi depoların çoğunda bu varsayım özel bir biçimde kırılır, çünkü komutlar tek bir iş parçacığında sırayla işlenir. Sıra, atomikliği bedava getiren şeydir: iki komut hiçbir zaman iç içe geçmez, kilit gerekmez.
Aynı sıra bir tuzağı da getirir. Bir komut ne kadar sürerse arkasındaki bütün istekler o kadar bekler. Diskteki bir veritabanında yavaş bir sorgu kendi istemcisini geciktirir; burada bütün istemcileri geciktirir. Bu ders o gecikmeyi sayar.
Uzun Komutun Arkasında Bekleyenler
Düzenek tek kuyruklu ve tek iş parçacıklıdır. İstekler her iki iş biriminde bir gelir, sıradan bir komut bir birim sürer; yük yarı doludur (BK15). Bir noktada bütün katalog önbelleğini tarayan bir bakım komutu araya girer ve toplam 4.000 birimlik iş getirir. Aynı iş bir kez tek komut olarak, sonra parçalara bölünmüş komutlar olarak verilir. Toplam iş her satırda aynıdır.
// tek-parcacik.mjs — tek is parcacikli depo: uzun komut arkasinda bekleyen istekler sayilir const GELIS = 2, ISTEK = 3000, TARAMA_ANI = 2000; // her 2 birimde bir istek, komut maliyeti 1 birim function kosum(toplamIs, parca) { // tarama isi `parca` komuta bolunur const parcaMaliyet = parca ? toplamIs / parca : 0; const sira = []; // tek kuyruk, tek is parcacigi, sirayla let gelen = 0, kalanParca = parca, taramaGelisi = parca ? TARAMA_ANI : -1; let calisan = null, bitisAni = 0; let bekleyen = 0, enUzun = 0, toplamBekleme = 0, taramaBitis = 0, islenen = 0; for (let t = 0; islenen < ISTEK + parca && t < 200000; t += 1) { if (calisan && bitisAni === t) { // 1) calisan komut bitti if (calisan.tarama) { taramaBitis = t; if (kalanParca > 0) taramaGelisi = t; } calisan = null; islenen += 1; } while (gelen < ISTEK && (gelen + 1) * GELIS === t) { // 2) normal istek gelisi sira.push({ gelis: t, maliyet: 1, tarama: false }); gelen += 1; } if (kalanParca > 0 && taramaGelisi === t) { // tarama parcasi: ilki takvimle, sonrakiler sira.push({ gelis: t, maliyet: parcaMaliyet, tarama: true }); // oncekinin bitisinde kalanParca -= 1; taramaGelisi = -1; } if (calisan === null && sira.length > 0) { // 3) sirada bekleyen ilk komut alinir calisan = sira.shift(); const b = t - calisan.gelis; if (b > 0) { bekleyen += 1; toplamBekleme += b; enUzun = Math.max(enUzun, b); } bitisAni = t + calisan.maliyet; } } return { parcaMaliyet, bekleyen, enUzun, toplamBekleme, taramaBitis }; } console.log(`is yuku: ${ISTEK} istek, ${GELIS} birimde bir gelis, komut maliyeti 1 birim`); console.log(["tarama", "parca maliyeti", "bekleyen istek", "en uzun bekleme", "toplam bekleme", "tarama bitisi"].map((b, i) => (i === 0 ? b.padEnd(18) : b.padStart(16))).join("")); for (const [ad, toplam, parca] of [ ["tarama yok", 0, 0], ["tek komut (4000)", 4000, 1], ["10 parca", 4000, 10], ["40 parca", 4000, 40], ["200 parca", 4000, 200], ]) { const r = kosum(toplam, parca); console.log(ad.padEnd(18) + [r.parcaMaliyet, r.bekleyen, r.enUzun, r.toplamBekleme, r.taramaBitis] .map((n) => String(n).padStart(16)).join("")); }
is yuku: 3000 istek, 2 birimde bir gelis, komut maliyeti 1 birim tarama parca maliyeti bekleyen istek en uzun bekleme toplam bekleme tarama bitisi tarama yok 0 0 0 0 0 tek komut (4000) 4000 2001 3999 5999001 6001 10 parca 400 2007 786 1045801 8001 40 parca 100 2022 198 289501 8001 200 parca 20 2102 38 58681 8001
İlk satır ölçünün sıfır noktasıdır: tarama olmadığında hiçbir istek beklemez, çünkü gelme hızı hizmet hızının yarısıdır. İkinci satırda tek bir komut 4.000 birim çalışır ve arkasında 2.001 istek birikir. En uzun bekleme 3.999 birimdir; yani sıraya taramanın hemen ardından giren istek, kendi işi bir birim sürdüğü hâlde neredeyse dört bin birim bekler. Toplam bekleme 5.999.001 birimdir.
Aşağıdaki satırlarda aynı 4.000 birimlik iş parçalara bölünüyor. Parça maliyeti 4.000’den 400’e indiğinde en uzun bekleme 3.999’dan 786’ya, toplam bekleme 5.999.001’den 1.045.801’e düşüyor. 200 parçada en uzun bekleme 38 birim, toplam bekleme 58.681 birimdir: yüzde birinden azı. Yapılan iş bir birim bile azalmadı; yalnız bölünebilir hâle geldi.
Son sütun bedeli gösteriyor. Tek komutta tarama 6.001. birimde bitiyor, parçalıda 8.001. birimde. Parçalı tarama kendi bitiş süresinden vazgeçerek başkalarının beklemesini satın alır. Tek iş parçacıklı bir depoda bu takas her uzun iş için yeniden yapılır: taramanın ne zaman biteceği genellikle kimseyi ilgilendirmez, arada bekleyen üç bin istek ilgilendirir.
İmleçli Taramanın Kaçırdığı
Parçalama bedava değildir ve bedeli yalnız gecikmiş bitiş değildir. Tek komut bütün taramayı iş parçacığını elinde tutarak yapar, dolayısıyla anahtar alanının değişmeyen bir hâlini görür. İmleçle yürüyen tarama parçalar arasında iş parçacığını bırakır ve o aralıklarda anahtar alanı değişir. Aşağıdaki koşum bunu 200.000 girişlik bir anahtar alanında ölçer; her parçadan sonra iki kitap ödünç verilir ve önbellek girdisi silinir.
// imlecli-tarama.mjs — ayni tarama: tek blokta mi, imlecle parca parca mi function anahtarAlani(adet) { // ekleme sirasini koruyan anahtar listesi const depo = new Map(); const anahtarlar = []; for (let i = 1; i <= adet; i += 1) { const a = `kitap:${i}`; depo.set(a, { rafta: i % 5, sube: (i % 3) + 1 }); anahtarlar.push(a); } return { depo, anahtarlar }; } const ADET = 200000, PARCA = 1000, SILME = 2; // parca basina 2 silme (odunc verilen kitaplar) function tekBlok() { // komut is parcacigini bastan sona tutar const { depo, anahtarlar } = anahtarAlani(ADET); let gorulen = 0, eslesen = 0; for (const a of anahtarlar) { gorulen += 1; if (depo.get(a).rafta === 0) eslesen += 1; } return { ad: "tek blok", enUzunBlok: gorulen, gorulen, eslesen, komut: 1, kacirilan: 0 }; } function imlecli() { // her parca ayri bir komut; arada baskalari calisir const { depo, anahtarlar } = anahtarAlani(ADET); const ziyaret = new Set(); let p = 0, gorulen = 0, eslesen = 0, komut = 0, silinen = 0; while (p < anahtarlar.length) { const son = Math.min(p + PARCA, anahtarlar.length); for (let i = p; i < son; i += 1) { // bu parcanin isi const a = anahtarlar[i]; ziyaret.add(a); gorulen += 1; if (depo.get(a).rafta === 0) eslesen += 1; } p = son; komut += 1; for (let s = 0; s < SILME && p < anahtarlar.length; s += 1) { // parcalar arasinda silme olur const yer = (silinen * 977) % p; // imlecten once bir anahtar silinir depo.delete(anahtarlar[yer]); anahtarlar.splice(yer, 1); // liste kayar: imlecteki giris atlanir silinen += 1; } } const kacirilan = anahtarlar.filter((a) => !ziyaret.has(a)).length; // sonuna kadar duran ama gorulmeyen return { ad: `imlecli (${PARCA})`, enUzunBlok: PARCA, gorulen, eslesen, komut, kacirilan }; } console.log(`anahtar alani=${ADET} parca=${PARCA} parca basina silme=${SILME}`); console.log(["yol", "komut", "en uzun blok", "gorulen giris", "eslesen", "kacirilan"] .map((b, i) => (i === 0 ? b.padEnd(16) : b.padStart(15))).join("")); for (const r of [tekBlok(), imlecli()]) { console.log(r.ad.padEnd(16) + [r.komut, r.enUzunBlok, r.gorulen, r.eslesen, r.kacirilan] .map((n) => String(n).padStart(15)).join("")); }
anahtar alani=200000 parca=1000 parca basina silme=2 yol komut en uzun blok gorulen giris eslesen kacirilan tek blok 1 200000 200000 40000 0 imlecli (1000) 200 1000 199602 39921 398
Tek blok 200.000 girişi kesintisiz gezer ve tam sayıyı verir: 40.000 eşleşme, sıfır kaçırılan giriş. İmleçli tarama en uzun bloğu 1.000 girişe indirir — az önceki ölçümde beklemeyi iki mertebe düşüren şey buydu — ama 398 giriş hiç görülmez. Bu girişler tarama boyunca varlığını sürdürmüştü; imlecin önünden bir anahtar silindiğinde liste kaydığı için atlandılar.
Bu, tek iş parçacıklı deponun tanı tablosundaki asıl ödünleşimdir. Kesintisiz tarama tam yanıt verir ve herkesi bekletir; imleçli tarama kimseyi bekletmez ve yaklaşık yanıt verir. Bir bakım işi için yaklaşık yanıt genellikle yeterlidir, bir sayım raporu için değildir.
Yavaş Komut Günlüğünün Eşiği
Yukarıdaki ölçümler düzeneğin içini görebildiği için yapılabildi. Çalışan bir depoda hangi komutun uzun sürdüğü doğrudan görünmez; bunun için yavaş komut günlüğü tutulur: süresi bir eşiğin üstünde kalan komutlar son N kaydı tutan bir halka tampona yazılır. Günlüğün kendisi de bellekten yenir, dolayısıyla eşik iki şeyi birden belirler: ne görüleceğini ve kaç bayt tutulacağını. Aşağıdaki koşum 128 kayıtlık bir halkayla beş eşiği tarar (BK16).
// yavas-komut-gunlugu.mjs — esik taramasi: hangi esik sucluyu gosterir, gunluk kac bayt tutar const HALKA = 128, EK_GIRIS = 16; // gunluk halka tampondur: son 128 kayit function komutlar(adet) { // belirlenimli karisim, tohum gorunur let tohum = 20250731; const rast = () => ((tohum = (tohum * 1103515245 + 12345) % 2147483648) / 2147483648); return Array.from({ length: adet }, (_, i) => { if (i === 1500) return { ad: "tarama", maliyet: 4000 }; // sucluyu ariyoruz const r = rast(); if (r < 0.95) return { ad: "oku", maliyet: 1 }; if (r < 0.99) return { ad: "kume-uye", maliyet: 5 + Math.floor(rast() * 36) }; return { ad: "sirali-kesit", maliyet: 200 }; }); } const akis = komutlar(3000); const kayitBayt = (k, sira) => // gunluk kaydi: komut adi, maliyet, sira Buffer.byteLength(JSON.stringify({ ad: k.ad, maliyet: k.maliyet, sira })) + EK_GIRIS; console.log(`komut=${akis.length} halka=${HALKA} kayit toplam is=` + `${akis.reduce((t, k) => t + k.maliyet, 0)} birim`); console.log(["esik", "kaydedilen", "halkada kalan", "tutulan bayt", "tarama halkada mi"] .map((b, i) => (i === 0 ? b.padEnd(8) : b.padStart(18))).join("")); for (const esik of [1, 5, 20, 100, 1000]) { const halka = []; let kaydedilen = 0; akis.forEach((k, i) => { if (k.maliyet < esik) return; // esigin altindaki komut gunluge girmez kaydedilen += 1; halka.push({ ...k, sira: i, bayt: kayitBayt(k, i) }); if (halka.length > HALKA) halka.shift(); // en eski kayit dusurulur }); const bayt = halka.reduce((t, k) => t + k.bayt, 0); const var_ = halka.some((k) => k.maliyet === 4000) ? "evet" : "hayir"; console.log(String(esik).padEnd(8) + [kaydedilen, halka.length, bayt, var_].map((n) => String(n).padStart(18)).join("")); }
komut=3000 halka=128 kayit toplam is=17267 birim esik kaydedilen halkada kalan tutulan bayt tarama halkada mi 1 3000 128 6719 hayir 5 159 128 7546 evet 20 108 108 6422 evet 100 40 40 2502 evet 1000 1 1 58 evet
Son sütun eşik seçiminin neden bir ölçüm kararı olduğunu gösteriyor. Eşik 1 iken günlük her komutu kaydediyor: 3.000 kayıt üretiliyor, ama halka yalnız son 128’ini tutabildiği için 4.000 birimlik tarama günlükten düşmüş. Bütün komutları kaydeden bir günlük, aranan komutu kaydetmeyen bir günlüktür.
Eşik 5’te kayıt sayısı 159’a iniyor ve tarama halkada kalıyor. Eşik 20’de kayıt sayısı halkanın altına düşüyor (108) ve artık hiçbir kayıt kaybolmuyor; günlük 6.422 bayt tutuyor. Eşik 1.000’de tek kayıt kalıyor: suçlu görünür ama 200 birimlik sıralı kesit komutlarının varlığı görünmez — oysa toplam 17.267 birimlik işin dörtte biri onlardan geliyor.
Kullanılabilir eşik, kayıt sayısını halka boyunun altında tutan ve normal komutları dışarıda bırakan aralıktır. Bu aralık iş yükünden okunur, önceden bilinemez: eşik önce geniş tutulup kayıt sayısı halka boyunu aşıyorsa daraltılır. Günlüğün bütçesi de aynı tablodadır — 128 kayıt en fazla 7.546 bayt tuttu — ve bu bütçe, sorunun görünür olmasını satın alır.
Özet
- Tek iş parçacıklı depoda uzun komut yalnız kendi istemcisini geciktirmez: 4.000 birimlik tek komut arkasında 2.001 istek bekletti, en uzun bekleme 3.999, toplam bekleme 5.999.001 birim oldu.
- Aynı iş 200 parçaya bölündüğünde en uzun bekleme 38, toplam bekleme 58.681 birime indi; yapılan iş değişmedi, yalnız bölünebilir hâle geldi.
- Parçalamanın bedeli iki kalemdir: tarama 6.001 yerine 8.001. birimde bitti ve imleç önünden silinen anahtarlar yüzünden 398 giriş hiç görülmedi (tek blokta 0).
- Yavaş komut günlüğünde eşik hem ne görüleceğini hem kaç bayt tutulacağını belirler: eşik 1’de 3.000 kayıt üretildi ve aranan komut 128 kayıtlık halkadan düştü.
- Kullanılabilir eşik kayıt sayısını halka boyunun altında tutandır; eşik 20’de 108 kayıt, 6.422 bayt ve suçlu komut günlükte kaldı.
Sonraki Adım
Bu kursun bütün ölçümleri deponun yalnız kendi istemcileriyle konuştuğunu varsaydı: yayımlayan taraf tanınan bir uygulama, tarayan komut bir bakım işi, ölçülen gecikme kendi iş yükünün gecikmesiydi. Oysa şimdiye kadar sayılan her maliyet, komutu kimin gönderdiği sorulmadan ödendi. Bütün anahtar alanını silen bir komutun, bütün belleği dolduran bir yazma dizisinin ya da yavaş abone olup tamponu şişiren bir bağlantının önünde hiçbir engel kurulmadı. Sonraki ders bu boşluğu ele alır: varsayılan olarak açık bir kurulumun ne kadar sürede bulunduğunu, komut kısıtlamasının neyi kapattığını ve ağ yalıtımının hangi maliyetle geldiğini ölçer.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.