Ders 18 / 19
Aşamalı Yayın ve Geri Alma
Yayın riskinin trafiğin payıyla sınırlanması; yapışkan atama, izleme penceresi ve gürültüye dayanıklı eşikle otomatik geri alma, işaretçi değişimiyle atomik sürüm geçişi ve geri alınamayan değişikliklerin genişlet–daralt sırasıyla ele alınması.
İçindekiler
Önizlemede sınanmış bir değişiklik yayına girdiğinde bütün kullanıcılara aynı anda ulaşır. Önizlemenin yakalayamadığı her kusur da öyle: yalnız gerçek trafikte ortaya çıkan yük davranışları, yalnız gerçek veride bulunan uç durumlar, yalnız belirli aygıtlarda görülen hatalar.
Bu ders yayını bir anahtar çevirmesi olmaktan çıkarır. Trafiğin küçük bir payı yeni sürüme yönlendirilir, hata oranı bir eşiğe karşı izlenir, eşik aşıldığında önceki dağıtıma dönülür. Sorusu şudur: pay, eşik ve geri alma süresi hangi ölçülere göre seçilir?
Aşamalı Yayının Dört Parçası
Pay planı. Yeni sürüme yönlendirilen trafik oranı, aralarında gözlem süresi bırakılan adımlarla artırılır. Her adım bir karar noktasıdır: devam mı, geri mi.
Yapışkan atama. Bir kullanıcının hangi sürümü göreceği, kimliğinden türetilen bir kovayla belirlenir ve yayın boyunca değişmez. Atama isteğe göre yapılırsa aynı kullanıcı iki sürüm arasında gidip gelir; bunun üç somut sonucu vardır — belgenin taşıdığı karmalı adlar öteki sürümün parçalarını istemeye başlar, oturum içinde yarım kalan akışlar bozulur ve ölçüm kirlenir. Kova sayısı payın çözünürlüğünü verir: yüz kova, yüzde bir adım demektir.
İzleme penceresi ve eşik. Karar, son bir pencerede yeni sürüme düşen isteklerin hata oranına bakılarak verilir. Pencerenin en az bir örneklem büyüklüğüne ulaşmış olması gerekir; ulaşmadan verilen karar gürültüdür.
Geri alma. Eşik aşıldığında payın sıfırlanması. Bunun ön koşulu, dönülecek dağıtımın hâlâ yayında duruyor olmasıdır — üçüncü dersteki korunan dağıtımlar kuralı burada işe yarar.
Aynı Kusurun Dört Yayın Biçimindeki Bedeli
// asamali.mjs — yuzde payli yayin, yapiskan atama ve esige gore geri alma. // Sanal saat kullanilir; Date.now() cagrilmaz, sonuc her kosuda aynidir. import { createHash } from "node:crypto"; // Belirlenimci uretec (splitmix32). Tohum sabit oldugu icin dizi her kosuda aynidir. function uretec(tohum) { let s = tohum >>> 0; return () => { s = (s + 0x9e3779b9) >>> 0; let z = s; z = Math.imul(z ^ (z >>> 16), 0x21f0aaad) >>> 0; z = Math.imul(z ^ (z >>> 15), 0x735a2d97) >>> 0; return ((z ^ (z >>> 15)) >>> 0) / 4294967296; }; } const SURE = 3600; // s const HIZ = 40; // istek/s const KULLANICI = 5000; const ESKI_HATA = 0.004; // eski surumun taban hata orani const PLAN = [[0, 1], [300, 5], [600, 25], [1200, 50], [1800, 100]]; const PENCERE = 120; // s, izleme penceresi const EN_AZ_ORNEK = 200; // pencerede gereken en az yeni surum istegi const ESIK = 0.02; // pencere hata orani esigi const ADIM = 30; // s, degerlendirme araligi // Esik, ornekleme gurultusunu asacak kadar gecilmelidir: oranin alt siniri bakilir. const altSinir = (hatali, n) => { const p = hatali / n; return p - 2 * Math.sqrt((p * (1 - p)) / n); }; // Yapiskan atama: kullanici kimligi 0-99 arasi bir kovaya sabitlenir. // Ayni kullanici yayin boyunca ayni tarafta kalir. const kova = (kullanici) => createHash("sha256").update("kullanici-" + kullanici).digest().readUInt32BE(0) % 100; const KOVA = Array.from({ length: KULLANICI }, (_, i) => kova(i)); const payi = (t) => PLAN.filter(([an]) => an <= t).at(-1)[1]; function yayinla({ yeniHata, tekSeferde, elle }) { // Iki ayri akis: kullanici secimi ile hata cekilisi birbirini etkilemesin. const rKullanici = uretec(20_260_412); const rHata = uretec(78_345_921); const olay = []; // { t, yeni, hata, kullanici } let geriAlma = null, sonrakiDegerlendirme = ADIM; const gunluk = []; let ilkPay = null; for (let n = 0; n < SURE * HIZ; n++) { const t = n / HIZ; if (elle !== undefined && geriAlma === null && t >= elle) { geriAlma = elle; gunluk.push([geriAlma, "elle saptandi ve geri alindi"]); } while (t >= sonrakiDegerlendirme) { if (geriAlma === null && elle === undefined) { const pencere = olay.filter((o) => o.yeni && o.t > sonrakiDegerlendirme - PENCERE); const hatali = pencere.filter((o) => o.hata).length; const n = pencere.length; if (n >= EN_AZ_ORNEK && altSinir(hatali, n) > ESIK) { geriAlma = sonrakiDegerlendirme; gunluk.push([geriAlma, "geri alindi, pencere hata orani %" + ((100 * hatali) / n).toFixed(1) + " (" + hatali + "/" + n + "), alt sinir %" + (100 * altSinir(hatali, n)).toFixed(1)]); } } sonrakiDegerlendirme += ADIM; } const pay = geriAlma !== null ? 0 : tekSeferde ? 100 : payi(t); if (pay !== ilkPay && geriAlma === null) { gunluk.push([t, "pay %" + pay]); ilkPay = pay; } const kullanici = Math.floor(rKullanici() * KULLANICI); const yeni = KOVA[kullanici] < pay; const hata = rHata() < (yeni ? yeniHata : ESKI_HATA); olay.push({ t, yeni, hata, kullanici }); } const yeniOlay = olay.filter((o) => o.yeni); const hatali = yeniOlay.filter((o) => o.hata); return { geriAlma, yeniIstek: yeniOlay.length, hata: hatali.length, kullanici: new Set(hatali.map((o) => o.kullanici)).size, gunluk, }; } console.log("yuk: " + (SURE * HIZ).toLocaleString("tr-TR") + " istek / " + SURE + " s, " + KULLANICI.toLocaleString("tr-TR") + " kullanici, yapiskan atama"); console.log("plan: " + PLAN.map(([t, p]) => t + " s %" + p).join(", ")); console.log("izleme: " + PENCERE + " s pencere, en az " + EN_AZ_ORNEK + " ornek, esik %" + (100 * ESIK).toFixed(1) + ", degerlendirme her " + ADIM + " s"); console.log("eski surumun taban hata orani: %" + (100 * ESKI_HATA).toFixed(1)); for (const [ad, ayar] of [ ["saglikli yeni surum (hata orani %0.5)", { yeniHata: 0.005 }], ["kusurlu yeni surum (hata orani %6.0)", { yeniHata: 0.06 }], ]) { const s = yayinla(ayar); console.log("\nsenaryo: " + ad); for (const [t, ileti] of s.gunluk) console.log(" " + (t.toFixed(0) + " s").padStart(8) + " " + ileti); console.log(" sonuc: " + (s.geriAlma === null ? "yayin tamamlandi" : "geri alindi") + ", yeni surume dusen " + s.yeniIstek.toLocaleString("tr-TR") + " istek, " + s.hata + " hata, " + s.kullanici + " ayri kullanici etkilendi"); } console.log("\nayni kusur (%6.0), dort yayin ve saptama bicimi:"); console.log(" " + "bicim".padEnd(34) + "saptama".padStart(9) + "hata".padStart(8) + "etkilenen kullanici".padStart(21)); for (const [ad, ayar] of [ ["asamali + otomatik izleme", { yeniHata: 0.06 }], ["tek seferde + otomatik izleme", { yeniHata: 0.06, tekSeferde: true }], ["asamali + elle saptama (600 s)", { yeniHata: 0.06, elle: 600 }], ["tek seferde + elle saptama (600 s)", { yeniHata: 0.06, tekSeferde: true, elle: 600 }], ]) { const s = yayinla(ayar); console.log(" " + ad.padEnd(34) + (s.geriAlma + " s").padStart(9) + String(s.hata).padStart(8) + (s.kullanici + " (%" + ((100 * s.kullanici) / KULLANICI).toFixed(1) + ")").padStart(21)); }
$ node asamali.mjs
yuk: 144.000 istek / 3600 s, 5.000 kullanici, yapiskan atama
plan: 0 s %1, 300 s %5, 600 s %25, 1200 s %50, 1800 s %100
izleme: 120 s pencere, en az 200 ornek, esik %2.0, degerlendirme her 30 s
eski surumun taban hata orani: %0.4
senaryo: saglikli yeni surum (hata orani %0.5)
0 s pay %1
300 s pay %5
600 s pay %25
1200 s pay %50
1800 s pay %100
sonuc: yayin tamamlandi, yeni surume dusen 90.818 istek, 479 hata, 451 ayri kullanici etkilendi
senaryo: kusurlu yeni surum (hata orani %6.0)
0 s pay %1
300 s pay %5
420 s geri alindi, pencere hata orani %9.4 (20/212), alt sinir %5.4
sonuc: geri alindi, yeni surume dusen 318 istek, 30 hata, 30 ayri kullanici etkilendi
ayni kusur (%6.0), dort yayin ve saptama bicimi:
bicim saptama hata etkilenen kullanici
asamali + otomatik izleme 420 s 30 30 (%0.6)
tek seferde + otomatik izleme 30 s 67 67 (%1.3)
asamali + elle saptama (600 s) 600 s 61 53 (%1.1)
tek seferde + elle saptama (600 s) 600 s 1490 1275 (%25.5)
Çıktı dört sonuç verir.
Küçük pay karar veremez. Kusurlu sürüm yüzde bir payındayken saptanmadı. Neden aritmetiktir: saniyede kırk isteğin yüzde biri saniyede 0,4 istektir; yüz yirmi saniyelik pencerede bu elli isteğe bile ulaşmaz ve iki yüzlük en az örneklem koşulunu sağlamaz. Saptama, pay yüzde beşe çıktıktan sonra, pencere dolar dolmaz gerçekleşti. İlk adımın payı, risk iştahından değil, karar için gereken örneklem büyüklüğünden hesaplanır.
Eşik gürültüyle birlikte tanımlanır. Karar, hata oranının nokta değerine değil alt sınırına bakar: kusurlu sürümde oran yüzde 9,4, alt sınırı yüzde 5,4’tür ve yüzde 2’lik eşiği bu alt sınır aşar. Nokta değere bakan bir kural, sağlıklı sürümde de zaman zaman eşiği aşan pencereler bulur — iki yüz örnekte beş hata görmek, gerçek oranı yüzde 0,5 olan bir sürüm için sıra dışı değildir. Alt sınır koşulu bu yanlış geri almaları eler.
Aşamalı yayın saptamayı yavaşlatır, etkiyi küçültür. Tek seferde yayında kusur otuz saniyede saptanır, aşamalıda dört yüz yirmi saniyede. Buna karşılık etkilenen kullanıcı sayısı otomatik izlemede iki kat, elle saptamada yirmi dört kat farklıdır. Aşamalı yayının sattığı şey hız değil, yanılma payıdır.
Otomatik izleme olmadan aşamalı yayın da yetmez. Son iki satır aynı on dakikalık saptama gecikmesini iki yayın biçimine uygular: aşamalıda elli üç kullanıcı, tek seferde bin iki yüz yetmiş beş kullanıcı — kullanıcıların dörtte biri — etkilenir. Pay planı ile izleme birlikte çalışır; biri olmadan öteki eksik kalır.
Geri Alınabilir Olmanın Koşulu
Geri alma, eski kaynağı yeniden derleyip yayınlamak değildir; o bir yeni yayındır ve derleme süresi kadar sürer. Geri almanın kısa olması için iki koşul gerekir: dönülecek dağıtımın hâlâ yayında duruyor olması ve hangisinin canlı olduğunun tek bir işaretçiyle belirlenmesi.
İşaretçinin değişimi atomik olmalıdır: geçici bir dosyaya yazılır, sonra rename(2) ile
eskisinin üzerine taşınır. Bu çağrı ya tümüyle olur ya hiç olmaz; okuyan taraf ya eski ya
yeni değeri görür, yarısını görmez.
// degistir.mjs — surum etiketli dagitimlar arasinda atomik gecis. // Once gecici bir isaretci dosyasi yazilir, sonra rename(2) ile yerine tasinir. import { writeFileSync, renameSync, readFileSync } from "node:fs"; const hedef = process.argv[2]; const t0 = process.hrtime.bigint(); writeFileSync("yayin/guncel.yeni", hedef + "\n"); renameSync("yayin/guncel.yeni", "yayin/guncel"); const t1 = process.hrtime.bigint(); console.log("gecis -> " + hedef + " yazma + rename " + (Number(t1 - t0) / 1e6).toFixed(3) + " ms isaretci " + readFileSync("yayin/guncel", "utf8").trim());
// okuyucu.mjs — gecis surerken surekli okur; kirik ya da karisik okuma sayilir. import { readFileSync } from "node:fs"; const sayim = new Map(); let kirik = 0, tutarsiz = 0; for (let i = 0; i < 120_000; i++) { try { const surum = readFileSync("yayin/guncel", "utf8").trim(); const govde = readFileSync("yayin/surum/" + surum + "/belge.txt", "utf8").trim(); if (govde !== surum) tutarsiz += 1; sayim.set(surum, (sayim.get(surum) ?? 0) + 1); } catch { kirik += 1; } } console.log("okuma : " + [...sayim].sort().map(([k, v]) => k + "=" + v).join(" ") + " kirik=" + kirik + " tutarsiz=" + tutarsiz);
#!/usr/bin/env bash # Surum etiketli iki dagitim, atomik gecis ve geri alma. rm -rf yayin mkdir -p yayin/surum/2026.03 yayin/surum/2026.04 echo "2026.03" > yayin/surum/2026.03/belge.txt echo "2026.04" > yayin/surum/2026.04/belge.txt echo "2026.03" > yayin/guncel echo "baslangic: $(cat yayin/guncel)" node okuyucu.mjs & okuyucu=$! sleep 0.3 node degistir.mjs 2026.04 wait "$okuyucu" node okuyucu.mjs & okuyucu=$! sleep 0.3 node degistir.mjs 2026.03 wait "$okuyucu"
baslangic: 2026.03 gecis -> 2026.04 yazma + rename 0.250 ms isaretci 2026.04 okuma : 2026.03=18088 2026.04=101912 kirik=0 tutarsiz=0 gecis -> 2026.03 yazma + rename 0.292 ms isaretci 2026.03 okuma : 2026.03=101743 2026.04=18257 kirik=0 tutarsiz=0
Okuma sayıları makineye ve yüke bağlıdır; her koşuda değişir. Değişmeyen iki alan sonuncu ikisidir: hiçbir okuma kırılmaz ve hiçbir okuma işaretçiyle içeriğin uyuşmadığı bir sonuç vermez. Yüz yirmi bin okumanın her biri ya eski ya yeni sürümü görür.
Geçişin kendisi bir milisaniyenin altındadır. Buradan geri alma süresinin nereden geldiği de çıkar: süre, işaretçinin değişmesinden değil, değişimin yayılmasından gelir. Birinci dersteki geçersiz kılma ne anında yayılır ne de kullanıcının kendi önbelleğine ulaşır; belgeye kısa tazelik penceresi vermek bu yüzden geri alma süresinin üst sınırını da belirler. Karmalı adlar sayesinde eski sürümün varlıkları hâlâ yayındadır, dolayısıyla geri alma yeni bir varlık indirmesi gerektirmez.
Geri Alınamayan Değişiklikler
Kodun geri alınması, kodun yaptığı işin geri alınması değildir. Üç sınıf değişiklik bu kuralın dışında kalır ve aşamalı yayının koruması onlarda çalışmaz.
Şema değişikliği. Yeni sürüm bir alanı kaldırdıysa, geri alınan eski sürüm o alanı arar ve bulamaz. Çözüm, değişikliği geri alınabilir adımlara bölmektir: genişlet–daralt (expand–contract). Önce şema, iki sürümün de çalışabileceği biçime genişletilir — yeni alan eklenir, eskisi durur. Sonra kod yayınlanır ve kararlılığı beklenir. Ancak bundan sonra, ayrı bir yayında, eski alan daraltılır. Her adımın kendi geri alınabilirliği vardır.
Yeni biçimde yazılmış veri. Yeni sürümün ürettiği kayıtları eski sürüm okuyamıyorsa geri alma veriyi okunamaz bırakır. Kural, okuma desteğinin yazma değişikliğinden bir yayın önce girmesidir: önce her iki biçimi de okuyabilen sürüm yayınlanır, sonra yeni biçimde yazan sürüm.
Dışa çıkmış yan etkiler. Gönderilmiş bildirim, yapılmış ödeme çağrısı ya da üçüncü tarafta oluşmuş kayıt geri alınamaz. Bunlar için tek yol ileri düzeltmedir: kusuru gideren yeni bir sürümü yayınlamak. Aşamalı yayının burada sağladığı tek şey, yan etkinin ulaştığı kullanıcı sayısını küçük tutmuş olmasıdır.
Üçü birlikte bir tasarım kuralı verir: geri alınabilirlik bir özelliktir, kendiliğinden gelmez. Bir değişiklik geri alınamaz hâle geldiği anda, aşamalı yayın yalnız etkiyi küçültür; düzeltmeyi kolaylaştırmaz.
Özet
- Aşamalı yayın dört parçadan oluşur: pay planı, yapışkan atama, izleme penceresi ve eşik, geri alma. Kova sayısı payın çözünürlüğünü belirler.
- İlk adımın payı risk iştahından değil, karar için gereken örneklem büyüklüğünden hesaplanır; örnekte yüzde birlik pay pencereyi dolduramadığı için karar veremedi.
- Eşik, hata oranının nokta değerine değil alt sınırına uygulanır; nokta değere bakan bir kural sağlıklı sürümleri de geri alır.
- Aşamalı yayın saptamayı yavaşlatır ama etkiyi küçültür; saptama gecikmesi büyüdükçe fark büyür — örnekte elli üç kullanıcıya karşılık bin iki yüz yetmiş beş kullanıcı.
- Geri alma, korunan bir dağıtıma tek bir işaretçiyle dönmektir;
rename(2)ile yapılan değişim bir milisaniyenin altındadır ve süreyi belirleyen şey değişimin yayılmasıdır. - Şema değişiklikleri genişlet–daralt sırasıyla, yazma biçimi değişiklikleri okuma desteği bir yayın önde olacak biçimde yapılır; dışa çıkmış yan etkiler yalnız ileri düzeltmeyle karşılanır.
Sonraki Adım
Buraya kadarki her dağıtım biçimi aynı varsayımı paylaşıyordu: çıktı bir adrese konur, kullanıcı o adresi bir tarayıcıyla açar ve yeni sürüm bir sonraki istekte geçerli olur. Bu varsayımın geçerli olmadığı hedefler vardır. Aynı derleme çıktısı bir masaüstü kabuğunun içine, bir yerel gövdenin içindeki web görünümüne ya da bir uygulama mağazasından dağıtılan pakete konabilir. O zaman adresler göreli olmak zorunda kalır, güncelleme kanalı bir inceleme sürecinden geçer ve geri alma bu derste ölçülen milisaniyelerle değil günlerle anılır. Son ders bu hedefleri ve getirdikleri kısıtları ele alır.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.