Ders 14 / 16
Sıfır Kesintili Yayın
Yayın sırasında düşen istek üç düzende sayılır: doğrudan değiştirme, mavi–yeşil ve aşamalı yayın. Düşen istek eşdeğer kesinti saniyesine çevrilir, bağlantı boşaltmanın uçuştaki isteklere etkisi ölçülür ve geri almanın adım sayısı karşılaştırılır.
İçindekiler
Önceki dersin son ölçümü düğüm sayısını ikiden üçe çıkardı ve bunun oturumlara ne yaptığını saydı. Düğüm kümesini değiştiren ikinci olay yayındır: bir sürüm durur, başkası açılır ve bir süre ikisi birlikte hizmet verir. Ölçekleme bu değişikliği seyrek yapar, yayın sık.
Bu ders bölünmüş ödünç sisteminin ödünç servisini üç ayrı düzende yayınlar ve tek bir şeyi sayar: yayın sırasında kaç istek düştü. Düşen istek sayısı, akan yükün hızına bölündüğünde kesinti saniyesine çevrilir; kesinti “sistem kapalıydı” demenin sayılabilir biçimidir.
- DP13. Yeni sürecin açılıp istek almaya hazır olması, düzenekte 20 isteklik bir pencereye denk gelir. Gerçek bir kurulumda bu pencere sürecin açılış süresine ve o anki istek hızına bağlıdır; burada sabit tutulur ki düzenler karşılaştırılabilsin.
- DP14. Ödünç servisinin iki sürümü aynı sözleşmeyi karşılar; istemci hangi sürüme düştüğünü ayırt etmez. Sözleşmenin kendisi değişiyorsa bu ders geçerli değildir.
- DP15. Yük sıralı akar: her istek bir öncekinin yanıtından sonra gönderilir. Eşdeğer kesinti saniyesi, saniyede 50 istek varsayımıyla hesaplanır.
Ölçüm Düzeneği
İki süreç türü var. Birincisi ödünç servisinin bir sürümü; hangi sürüm olduğunu ve kapanırken ne yapacağını dışarıdan alır.
// surum.mjs — odunc servisinin bir surumu; SURUM, GECIKME ve kapanma turu disaridan gelir import { createServer } from 'node:http'; const SURUM = process.env.SURUM; const GECIKME = Number(process.env.GECIKME ?? 0); const bekle = (ms) => new Promise((c) => setTimeout(c, ms)); const sunucu = createServer(async (istek, yanit) => { const yol = new URL(istek.url, 'http://yerel'); if (yol.pathname === '/kapan') { yanit.writeHead(200).end('kapaniyor'); if (yol.searchParams.get('tur') === 'zarif') { // Yeni baglanti alinmaz, ucustaki istekler bitirilir, sonra surec cikar. sunucu.close(() => process.exit(0)); sunucu.closeIdleConnections(); setTimeout(() => process.exit(0), 5000).unref(); } else { process.exit(0); // ucustaki istekler yarida kalir } return; } if (GECIKME) await bekle(GECIKME); yanit.writeHead(200, { 'content-type': 'application/json' }) .end(JSON.stringify({ surum: SURUM, port: Number(process.env.PORT) })); }); sunucu.listen(Number(process.env.PORT), () => console.log(`hazir ${SURUM}`));
İkincisi ön yüzdür: istemcinin bildiği tek adres. Arkasındaki havuzu bir denetim ucundan değiştirir; havuz boşken istek gidecek yer bulamaz ve 503 döner. Yayın düzenleri arasındaki bütün fark, bu havuzun hangi sırayla değiştirildiğidir.
// onyuz.mjs — istegi havuzdaki sureclere sirayla dagitir; havuz bosken 503 doner import { createServer } from 'node:http'; let havuz = []; let sira = 0; createServer(async (istek, yanit) => { const yol = new URL(istek.url, 'http://yerel'); if (yol.pathname === '/havuz') { havuz = (yol.searchParams.get('hedef') ?? '').split(',').filter(Boolean); sira = 0; return yanit.writeHead(200).end(havuz.join(',')); } if (havuz.length === 0) return yanit.writeHead(503).end('havuz bos'); const hedef = havuz[sira++ % havuz.length]; try { const ust = await fetch(`http://127.0.0.1:${hedef}${istek.url}`); yanit.writeHead(ust.status, { 'content-type': 'application/json' }).end(await ust.text()); } catch { yanit.writeHead(502).end('ust akis yanit vermedi'); } }).listen(Number(process.env.PORT), () => console.log('onyuz hazir'));
SURUM=v1 PORT=8731 node surum.mjs > /dev/null & ESKI=$! PORT=8730 node onyuz.mjs > /dev/null & ON=$! sleep 0.6 curl -s -o /dev/null -w 'havuz bosken durum kodu: %{http_code}\n' 'http://127.0.0.1:8730/odunc' curl -s -o /dev/null 'http://127.0.0.1:8730/havuz?hedef=8731' curl -s -w '\n' 'http://127.0.0.1:8730/odunc' kill $ESKI $ON
havuz bosken durum kodu: 503
{"surum":"v1","port":8731}
Düşen isteğin tanımı bu 503’tür: istemci istedi, sistem veremedi.
Üç Düzen
Üç düzen de aynı işi yapar — v1 gider, v2 gelir — ve aynı yük altında koşar. Ayrıldıkları
yer, eski sürümün ne zaman durdurulduğudur. Doğrudan değiştirmede eskiyi durdurmak yeniyi
açmanın önkoşuludur; mavi–yeşilde yeni sürüm eski hâlâ hizmet verirken ayağa kalkar;
aşamalıda havuz karışık kalır ve yeni sürümün payı adım adım büyür.
// yayin.mjs — uc yayin duzeninde akan yuk: dogrudan degistirme, mavi-yesil, asamali import { spawn } from 'node:child_process'; const ONYUZ = 8730, HIZ = 50, YUK = 20, PENCERE = 20; const surecler = new Map(); async function baslat(surum, port) { const cocuk = spawn(process.execPath, ['surum.mjs'], { stdio: ['ignore', 'pipe', 'inherit'], env: { ...process.env, SURUM: surum, PORT: String(port) } }); await new Promise((c) => cocuk.stdout.once('data', c)); surecler.set(port, cocuk); } const durdur = (portlar) => portlar.forEach((p) => { surecler.get(p)?.kill('SIGKILL'); surecler.delete(p); }); const havuza = (portlar) => fetch(`http://127.0.0.1:${ONYUZ}/havuz?hedef=${portlar}`).then((y) => y.text()); async function akit(n) { const say = { v1: 0, v2: 0, dusen: 0 }; for (let i = 0; i < n; i += 1) { try { const y = await fetch(`http://127.0.0.1:${ONYUZ}/odunc`); if (y.ok) say[(await y.json()).surum] += 1; else { await y.text(); say.dusen += 1; } } catch { say.dusen += 1; } } return say; } async function dogrudan() { const adim = [], asama = []; await baslat('v1', 8731); await baslat('v1', 8732); await havuza('8731,8732'); asama.push(['eski surum hizmet veriyor', await akit(YUK)]); adim.push('eski surecleri durdur ve havuzu bosalt'); durdur([8731, 8732]); await havuza(''); asama.push(['eski durdu, yeni acilmadi', await akit(PENCERE)]); adim.push('yeni surumu ayaga kaldir'); await baslat('v2', 8733); await baslat('v2', 8734); adim.push('havuza yeni surumu koy'); await havuza('8733,8734'); asama.push(['yeni surum hizmet veriyor', await akit(YUK)]); const geri = ['yeni surecleri durdur ve havuzu bosalt', 'eski surumu ayaga kaldir', 'havuza eski surumu koy']; durdur([8733, 8734]); await havuza(''); const geriSay = await akit(PENCERE); await baslat('v1', 8731); await baslat('v1', 8732); await havuza('8731,8732'); durdur([8731, 8732]); return { ad: 'dogrudan degistirme', adim, asama, geri, geriSay }; } async function maviYesil() { const adim = [], asama = []; await baslat('v1', 8731); await baslat('v1', 8732); await havuza('8731,8732'); asama.push(['mavi hizmet veriyor', await akit(YUK)]); adim.push('yesili ayaga kaldir'); await baslat('v2', 8733); await baslat('v2', 8734); asama.push(['yesil ayakta, havuzda degil', await akit(YUK)]); adim.push('havuzu yesile cevir'); await havuza('8733,8734'); asama.push(['havuz yesile cevrildi', await akit(YUK)]); const geriSay = await (async () => { await havuza('8731,8732'); return akit(YUK); })(); adim.push('maviyi bosalt ve kapat'); durdur([8731, 8732, 8733, 8734]); return { ad: 'mavi-yesil', adim, asama, geri: ['havuzu maviye geri cevir'], geriSay }; } async function asamali() { const adim = [], asama = []; await baslat('v1', 8731); await baslat('v1', 8732); await havuza('8731,8732'); asama.push(['yeni surumun payi yok', await akit(YUK)]); adim.push('yeni surumun ilk dugumunu ayaga kaldir'); await baslat('v2', 8733); adim.push('havuzda uc dugumun biri yeni'); await havuza('8731,8732,8733'); asama.push(['uc dugumun biri yeni', await akit(YUK)]); adim.push('ikinci yeni dugum, bir eski dugum havuzdan cikar'); await baslat('v2', 8734); await havuza('8731,8733,8734'); asama.push(['uc dugumun ikisi yeni', await akit(YUK)]); adim.push('havuz butunuyle yeni surume gecer'); await havuza('8733,8734'); asama.push(['butun havuz yeni', await akit(YUK)]); const geriSay = await (async () => { await havuza('8731,8732'); return akit(YUK); })(); adim.push('eski dugumleri bosalt ve kapat'); durdur([8731, 8732, 8733, 8734]); return { ad: 'asamali', adim, asama, geri: ['havuzu eski surume geri cevir'], geriSay }; } const onyuz = spawn(process.execPath, ['onyuz.mjs'], { stdio: ['ignore', 'pipe', 'inherit'], env: { ...process.env, PORT: String(ONYUZ) } }); await new Promise((c) => onyuz.stdout.once('data', c)); const sonuc = [await dogrudan(), await maviYesil(), await asamali()]; for (const s of sonuc) { console.log(`${s.ad} — yayin adimi ${s.adim.length}`); for (const [etiket, say] of s.asama) { console.log(` ${etiket.padEnd(32)} v1=${String(say.v1).padEnd(3)} v2=${String(say.v2).padEnd(3)} dusen=${say.dusen}`); } console.log(` ${`geri alma, ${s.geri.length} adim`.padEnd(32)} ${' '.repeat(14)} dusen=${s.geriSay.dusen}`); } console.log(''); console.log(`akan yuk saniyede ${HIZ} istek varsayimiyla`); console.log('duzen yayin adimi dusen istek esdeger kesinti (sn) geri alma adimi geri almada dusen'); for (const s of sonuc) { const dusen = s.asama.reduce((t, [, y]) => t + y.dusen, 0); console.log(`${s.ad.padEnd(20)} ${String(s.adim.length).padEnd(12)} ${String(dusen).padEnd(12)} ` + `${(dusen / HIZ).toFixed(2).padEnd(21)} ${String(s.geri.length).padEnd(16)} ${s.geriSay.dusen}`); } onyuz.kill('SIGKILL');
dogrudan degistirme — yayin adimi 3 eski surum hizmet veriyor v1=20 v2=0 dusen=0 eski durdu, yeni acilmadi v1=0 v2=0 dusen=20 yeni surum hizmet veriyor v1=0 v2=20 dusen=0 geri alma, 3 adim dusen=20 mavi-yesil — yayin adimi 3 mavi hizmet veriyor v1=20 v2=0 dusen=0 yesil ayakta, havuzda degil v1=20 v2=0 dusen=0 havuz yesile cevrildi v1=0 v2=20 dusen=0 geri alma, 1 adim dusen=0 asamali — yayin adimi 5 yeni surumun payi yok v1=20 v2=0 dusen=0 uc dugumun biri yeni v1=14 v2=6 dusen=0 uc dugumun ikisi yeni v1=7 v2=13 dusen=0 butun havuz yeni v1=0 v2=20 dusen=0 geri alma, 1 adim dusen=0 akan yuk saniyede 50 istek varsayimiyla duzen yayin adimi dusen istek esdeger kesinti (sn) geri alma adimi geri almada dusen dogrudan degistirme 3 20 0.40 3 20 mavi-yesil 3 0 0.00 1 0 asamali 5 0 0.00 1 0
Doğrudan değiştirmenin kesintisi bir yanlışlıktan doğmuyor; düzenin kendisinden doğuyor. Eskiyi durdurmadan yeniyi aynı yere koyamayan bir kurulumda, aradaki pencere boyunca gidecek yer yoktur. Yirmi istek düştü ve saniyede elli istekle bu 0,40 saniyelik kesintiye denk geliyor. Sayı küçük görünür; günde on yayın yapan bir ekipte dört saniyelik bir kesintidir ve tamamı kullanıcıya hata olarak gider.
Mavi–yeşil aynı işi sıfır düşen istekle bitirdi. Farkı ikinci satırda görünüyor: yeşil ayağa kalkarken mavi hizmet vermeye devam etti, yani açılış penceresi yükün dışında kaldı. Bunun bedeli, o pencere boyunca iki sürümün birden ayakta olmasıdır — yayın anında kaynak gereksinimi iki katına çıkar.
Aşamalı yayının tablosu ikinci ve üçüncü satırda karışık pay gösteriyor: üç düğümün biri yeniyken yirmi isteğin altısı, ikisi yeniyken on üçü yeni sürüme gitti. Ön yüz havuzu her değiştiğinde sırayı sıfırladığı için bu paylar koşumdan bağımsızdır. Aşamalı yayının kazandırdığı şey kesinti değil — mavi–yeşilde de sıfırdı — hatalı sürümün kaç isteğe dokunacağıdır: yeni sürüm ikinci aşamada bozuk çıksaydı yirmi isteğin altısı etkilenirdi, mavi–yeşilde yirmisi de.
Geri alma sütunu bu dersin asıl kararını taşıyor. Mavi–yeşilde ve aşamalıda geri alma tek adımdır — havuzu eski sürüme çevirmek — ve sıfır istek düşürür, çünkü eski süreçler hâlâ ayaktadır. Doğrudan değiştirmede geri alma yeni bir yayındır: üç adım, yirmi düşen istek. Buradan okunacak kural, eski süreçlerin ayakta tutulduğu sürenin adı olan geri alma penceresidir; o pencere kapandığı anda geri alma tek adım olmaktan çıkar.
Bağlantı Boşaltma
Havuz değiştiğinde eski düğüme yeni istek gitmez, ama o anda uçuşta olan istekler vardır. Onların ne olduğu, eski sürecin nasıl kapatıldığına bağlıdır.
// bosaltma.mjs — eski dugum kapatilirken ucusta kalan istekler: sert ve zarif kapanma import { spawn } from 'node:child_process'; const PORT = 8735, UCUSTA = 8, GECIKME = 500; const bekle = (ms) => new Promise((c) => setTimeout(c, ms)); async function olc(tur) { const cocuk = spawn(process.execPath, ['surum.mjs'], { stdio: ['ignore', 'pipe', 'inherit'], env: { ...process.env, SURUM: 'v1', PORT: String(PORT), GECIKME: String(GECIKME) } }); await new Promise((c) => cocuk.stdout.once('data', c)); const ucus = Array.from({ length: UCUSTA }, () => fetch(`http://127.0.0.1:${PORT}/odunc`).then((y) => y.ok).catch(() => false)); await bekle(150); // sekiz istek ucusta, hicbiri bitmedi await fetch(`http://127.0.0.1:${PORT}/kapan?tur=${tur}`).catch(() => {}); const sonuc = await Promise.all(ucus); cocuk.kill('SIGKILL'); return { tur, tamamlanan: sonuc.filter(Boolean).length }; } console.log(`kapanma aninda ucusta ${UCUSTA} istek var, her biri ${GECIKME} ms suruyor`); console.log('kapanma tamamlanan dusen'); for (const tur of ['sert', 'zarif']) { const s = await olc(tur); console.log(`${s.tur.padEnd(8)} ${String(s.tamamlanan).padEnd(11)} ${UCUSTA - s.tamamlanan}`); }
kapanma aninda ucusta 8 istek var, her biri 500 ms suruyor kapanma tamamlanan dusen sert 0 8 zarif 8 0
Doğru ayarın ölçüsü budur: zarif kapanmada uçuştaki sekiz isteğin sekizi de tamamlandı, sert kapanmada sekizi de düştü. Aradaki tek fark, kapanma işaretini alan sürecin hemen çıkması ile açık isteklerin bitmesini beklemesidir.
Sessiz Olan Ne
Bu iki ölçüm aynı anın iki tarafını gösteriyor ve birbirini yalanlıyor gibi görünüyor. Yayın tablosunda mavi–yeşilin düşen istek sayısı sıfırdı; boşaltma tablosunda aynı geçişte sekiz istek düştü. İkisi de doğru. Yayın tablosunu üreten yük, ön yüzden geçen yeni isteklerdir ve havuz çevrildikten sonra hepsi yeşile gitti, hiçbiri düşmedi. Sekiz düşen istek, havuz çevrilmeden önce maviye yönlendirilmiş ve hâlâ yanıt bekleyen isteklerdir. Ön yüzün sayaçlarında görünmezler.
Yanlış ayarın sessiz sonucu tam olarak budur: kapanma türü sert bırakıldığında yayın “sıfır kesintili” raporlanmaya devam eder, çünkü ön yüz hiç boş havuz görmemiştir ve düğümler açılışta hazır bildirmiştir. Uygulama hata da vermez — süreç düzgün bir çıkış koduyla kapanır. Fark yalnız uçuştaki isteklerin sayısında görünür ve o sayı yükle birlikte büyür: saniyede elli istek alan, ortalama yarım saniye süren bir uçta her kapanmada yaklaşık yirmi beş istek uçuştadır.
Ayarların durduğu yer de düzenler arasında değişiyor. Havuz üyeliği tek bir yerde, ön yüzdedir; bir yayın sırasında birkaç kez değişir ama hep aynı yerden. Kapanma türü ise her düğümde durur: sürece hangi işaretin gönderileceği süreç yöneticisinin ayarıdır, o işaretin nasıl karşılandığı uygulama kodundadır. İkisinin uyuşmaması — süreç yöneticisi zarif kapanma işareti gönderirken uygulamanın onu dinlememesi — tabloda sert kapanma satırını üretir, hiçbir hata kaydı bırakmadan.
Özet
- Düşen istek, ön yüzün gidecek yer bulamadığı istektir; düşen istek sayısı yük hızına bölününce eşdeğer kesinti saniyesini verir.
- Doğrudan değiştirmede kesinti düzenin kendisinden gelir: eskiyi durdurmak yeniyi açmanın önkoşulu olduğu için açılış penceresi boyunca yirmi istek düştü, 0,40 saniye.
- Mavi–yeşil ve aşamalı yayın sıfır istek düşürdü; bedeli, yayın penceresinde iki sürümün birden ayakta durmasıdır.
- Aşamalı yayının kazancı kesinti değil etki alanıdır: üç düğümün biri yeniyken yirmi isteğin altısı yeni sürüme gitti.
- Geri alma mavi–yeşilde ve aşamalıda tek adım ve sıfır düşen istek; doğrudan değiştirmede üç adım ve yirmi düşen istektir. Eski süreçlerin ayakta kaldığı süre geri alma penceresidir.
- Sert kapanmada uçuştaki sekiz isteğin sekizi de düştü, zarif kapanmada sıfırı; bu fark ön yüzün sayaçlarında görünmez.
Sonraki Adım
Bu dersin üç düzeni de bir varsayıma yaslandı: iki sürüm aynı anda ayakta durabilir ve aynı isteği aynı biçimde karşılayabilir. Süreçler için doğru olan bu varsayım, altlarındaki şema için kendiliğinden doğru değildir. Mavi–yeşil geçişte iki sürüm aynı veritabanını okur; aşamalı yayında bu birlikte yaşama daha da uzun sürer. Sonraki ders bu pencereyi ölçer: göç ile kod yayınının hangi sırada gittiği, yanlış sırada kaç isteğin hata aldığı, iki sürümün aynı şemayla çalışabildiği aralık ve geri alınamayan adımın nerede durduğu.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.