Ders 15 / 16
Veritabanı Göçü ile Yayın Sırası
Aynı şema değişikliği üç ayrı sırada koşturulur ve her sırada kaç isteğin hata aldığı sayılır: göç önce, kod önce ve genişlet–daralt. İki sürümün aynı şemayla çalıştığı pencere, çift yazmanın işlevi ve sütun silmenin geri alınamazlığı ölçülür.
İçindekiler
Önceki dersin üç yayın düzeni de aynı varsayıma yaslandı: iki sürüm bir süre birlikte ayakta durabilir. Süreçler için sağlanan bu varsayım, altlarındaki şema için kendiliğinden sağlanmaz. Mavi–yeşil geçişte iki sürüm aynı veritabanını okur; aşamalı yayında bu birlikte yaşama daha uzun sürer. Şema iki sürümden yalnız birini destekliyorsa yayın düzeninin kesintisizliği bir şey ifade etmez.
Bu dersin konusu göçün kendisi değil, sırasıdır: göç ile kod yayınının hangisi önce
gider. Göç adımlarının teste alınması tumlesim/04‘te, sözleşmenin sürümler arası
evrimi M16/K06’da kuruldu; burada sorulan tek soru, iki işlemin arasındaki pencerede
sistemin ne yaptığıdır. Ölçü, o pencerede hata alan istek sayısıdır.
- DP16. Göç adımları uygulamadan ayrı koşar ve kod yayınıyla aynı işlemin parçası değildir. Sıra sorusu bu ayrılıktan doğar.
- DP17. Şema tek bir veritabanında durur ve bütün sürümler aynı veritabanını okur.
- DP18. Üye kaydının iletişim alanı serbest metindir: kimi üyede e-posta, kimisinde telefon numarası. Değişiklik bu alanı yalnız e-posta tutan bir alana daraltır.
İki Sürüm, Tek Şema
Üye ucunun üç sürümü var. v1 eski sütunu okur ve yazar, v2 yeni sütunu okur ama iki
sütuna birden yazar, v3 yalnız yeni sütunu tanır. Üçü de aynı veritabanı dosyasını açar.
// servis.mjs — uye ucunun bir surumu; SURUM hangi sutunu okuyup yazacagini belirler import { createServer } from 'node:http'; import { DatabaseSync } from 'node:sqlite'; const SURUM = process.env.SURUM; const db = new DatabaseSync(process.env.DB_YOLU); const OKU = { v1: 'SELECT uye_no, ad, iletisim AS deger FROM uye WHERE uye_no = ?', v2: 'SELECT uye_no, ad, eposta AS deger FROM uye WHERE uye_no = ?', v3: 'SELECT uye_no, ad, eposta AS deger FROM uye WHERE uye_no = ?', }; const YAZ = { v1: 'INSERT INTO uye (uye_no, ad, iletisim) VALUES (?, ?, ?)', v2: 'INSERT INTO uye (uye_no, ad, iletisim, eposta) VALUES (?, ?, ?, ?)', // cift yazma v3: 'INSERT INTO uye (uye_no, ad, eposta) VALUES (?, ?, ?)', }; createServer((istek, yanit) => { const yol = new URL(istek.url, 'http://yerel'); const no = yol.searchParams.get('no'); const bas = { 'content-type': 'application/json' }; try { if (istek.method === 'POST') { const ad = yol.searchParams.get('ad'); const deger = yol.searchParams.get('deger'); db.prepare(YAZ[SURUM]).run(...(SURUM === 'v2' ? [no, ad, deger, deger] : [no, ad, deger])); return yanit.writeHead(201, bas).end(JSON.stringify({ surum: SURUM, yazildi: no })); } const satir = db.prepare(OKU[SURUM]).get(no); yanit.writeHead(200, bas).end(JSON.stringify({ surum: SURUM, ...satir })); } catch (e) { yanit.writeHead(500, bas).end(JSON.stringify({ surum: SURUM, hata: e.message })); } }).listen(Number(process.env.PORT), () => console.log(`hazir ${SURUM}`));
Göç yapılmamış bir şema üzerinde ikisi birden ayağa kaldırılıp aynı üye sorulduğunda fark görünür.
node -e "const {DatabaseSync} = require('node:sqlite'); const db = new DatabaseSync('katalog.db'); db.exec('CREATE TABLE uye (uye_no TEXT PRIMARY KEY, ad TEXT NOT NULL, iletisim TEXT)'); db.prepare('INSERT INTO uye VALUES (?, ?, ?)').run('U-0001', 'Ayse Demir', 'ayse@kutuphane')" SURUM=v1 PORT=8741 DB_YOLU=katalog.db node servis.mjs > /dev/null & ESKI=$! SURUM=v2 PORT=8742 DB_YOLU=katalog.db node servis.mjs > /dev/null & YENI=$! sleep 0.6 curl -s -w '\n' 'http://127.0.0.1:8741/uye?no=U-0001' curl -s -w '\n' 'http://127.0.0.1:8742/uye?no=U-0001' kill $ESKI $YENI
{"surum":"v1","uye_no":"U-0001","ad":"Ayse Demir","deger":"ayse@kutuphane"}
{"surum":"v2","hata":"no such column: eposta"}
Şema bir sürümü destekliyor, diğerini desteklemiyor. Yayın sırasının bütün sorunu, bu durumun ne kadar sürdüğü ve o sürede hangi sürümün yükü taşıdığıdır.
Aynı Değişiklik, Üç Sıra
Üç sıra da aynı sonuca varır: iletişim alanı e-posta alanına dönüşür. Birincisi göçü öne alır, ikincisi kod yayınını öne alır, üçüncüsü değişikliği geriye uyumlu adımlara böler. Her sırada her aşamada yirmi istek akıtılır ve kaçının hata aldığı sayılır.
// sira.mjs — ayni sema degisikligi uc ayri sirada: yikici goc once, kod once, genislet-daralt import { spawn } from 'node:child_process'; import { DatabaseSync } from 'node:sqlite'; import { mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; const kok = mkdtempSync(join(tmpdir(), 'goc-sira-')); const YUK = 20; const UYELER = [ ['U-0001', 'Ayse Demir', 'ayse@kutuphane'], ['U-0002', 'Emre Koc', '0500 000 0002'], ['U-0003', 'Elif Sahin', 'elif@kutuphane'], ['U-0004', 'Mert Yilmaz', '0500 000 0004'], ['U-0005', 'Zeynep Ak', 'zeynep@kutuphane'], ['U-0006', 'Can Ozturk', '0500 000 0006'], ['U-0007', 'Deniz Kaya', 'deniz@kutuphane'], ['U-0008', 'Bora Tas', '0500 000 0008'], ['U-0009', 'Sema Gul', 'sema@kutuphane'], ['U-0010', 'Kerem Yurt', '0500 000 0010'], ['U-0011', 'Nil Aydin', 'nil@kutuphane'], ['U-0012', 'Efe Duran', 'efe@kutuphane'], ]; function veritabaniKur(yol) { const db = new DatabaseSync(yol); db.exec('CREATE TABLE uye (uye_no TEXT PRIMARY KEY, ad TEXT NOT NULL, iletisim TEXT)'); const ekle = db.prepare('INSERT INTO uye VALUES (?, ?, ?)'); for (const u of UYELER) ekle.run(...u); return db; } const GOC = { yikici: (db) => db.exec('ALTER TABLE uye RENAME COLUMN iletisim TO eposta'), genislet: (db) => { db.exec('ALTER TABLE uye ADD COLUMN eposta TEXT'); db.exec("UPDATE uye SET eposta = iletisim WHERE instr(iletisim, '@') > 0"); }, daralt: (db) => db.exec('ALTER TABLE uye DROP COLUMN iletisim'), }; const cocuklar = []; async function baslat(surum, port, yol) { const cocuk = spawn(process.execPath, ['servis.mjs'], { stdio: ['ignore', 'pipe', 'inherit'], env: { ...process.env, SURUM: surum, PORT: String(port), DB_YOLU: yol } }); await new Promise((c) => cocuk.stdout.once('data', c)); cocuklar.push(cocuk); return port; } const kapat = () => { cocuklar.forEach((c) => c.kill('SIGKILL')); cocuklar.length = 0; }; async function akit(portlar, n) { const say = { tamam: 0, hata: 0 }; for (let i = 0; i < n; i += 1) { const y = await fetch(`http://127.0.0.1:${portlar[i % portlar.length]}/uye?no=${UYELER[i % UYELER.length][0]}`); say[y.ok ? 'tamam' : 'hata'] += 1; await y.text(); } return say; } const yaz = (port, no, ad, deger) => fetch(`http://127.0.0.1:${port}/uye?no=${no}&ad=${ad}°er=${deger}`, { method: 'POST' }).then((y) => y.json()); const oku = (port, no) => fetch(`http://127.0.0.1:${port}/uye?no=${no}`).then((y) => y.json()); async function kur(ad) { const yol = join(kok, `${ad}.db`); const db = veritabaniKur(yol); const [v1, v2, v3] = [await baslat('v1', 8741, yol), await baslat('v2', 8742, yol), await baslat('v3', 8743, yol)]; return { db, v1, v2, v3, faz: [], adim: [] }; } async function gocOnce() { const o = await kur('goc-once'); o.faz.push(['eski surum, goc yapilmadi', await akit([o.v1], YUK)]); o.adim.push('yikici goc'); GOC.yikici(o.db); o.faz.push(['goc gecti, kod hala eski', await akit([o.v1], YUK)]); o.adim.push('yeni surumun yayini'); o.faz.push(['yeni surum yayinlandi', await akit([o.v2], YUK)]); kapat(); return { ad: 'once goc sonra kod', ...o, birlikte: 0 }; } async function kodOnce() { const o = await kur('kod-once'); o.faz.push(['eski surum, goc yapilmadi', await akit([o.v1], YUK)]); o.adim.push('yeni surumun yayini'); o.faz.push(['kod yeni, sutun yok', await akit([o.v2], YUK)]); o.adim.push('yikici goc'); GOC.yikici(o.db); o.faz.push(['goc gecti', await akit([o.v2], YUK)]); kapat(); return { ad: 'once kod sonra goc', ...o, birlikte: 0 }; } async function genisletDaralt() { const o = await kur('genislet-daralt'); o.faz.push(['eski surum, goc yapilmadi', await akit([o.v1], YUK)]); o.adim.push('genislet: sutun eklenir ve doldurulur'); GOC.genislet(o.db); o.faz.push(['sema genis, kod hala eski', await akit([o.v1], YUK)]); o.adim.push('cift yazan surumun yayini'); o.faz.push(['iki surum birlikte', await akit([o.v1, o.v2], YUK)]); await yaz(o.v2, 'U-0013', 'Cift Yazan', 'onuc@kutuphane'); await yaz(o.v3, 'U-0014', 'Tek Yazan', 'ondort@kutuphane'); const pencere = [await oku(o.v1, 'U-0013'), await oku(o.v1, 'U-0014')]; o.adim.push('eski surum cekilir'); o.faz.push(['yalniz cift yazan surum', await akit([o.v2], YUK)]); o.adim.push('cift yazma kaldirilir'); o.faz.push(['cift yazma kaldirildi', await akit([o.v3], YUK)]); o.adim.push('daralt: eski sutun silinir'); GOC.daralt(o.db); o.faz.push(['sema dar, kod yeni', await akit([o.v3], YUK)]); const erken = await yaz(o.v2, 'U-0015', 'Erken Daralt', 'onbes@kutuphane'); kapat(); return { ad: 'genislet-daralt', ...o, birlikte: 1, pencere, erken }; } const sonuc = [await gocOnce(), await kodOnce(), await genisletDaralt()]; for (const s of sonuc) { console.log(`${s.ad} — ${s.adim.length} adim`); for (const [etiket, say] of s.faz) { console.log(` ${etiket.padEnd(28)} tamam=${String(say.tamam).padEnd(3)} hata=${say.hata}`); } } console.log(''); console.log('sira adim gonderilen hata alan iki surumun birlikte kostugu faz'); for (const s of sonuc) { const g = s.faz.reduce((t, [, y]) => t + y.tamam + y.hata, 0); const h = s.faz.reduce((t, [, y]) => t + y.hata, 0); console.log(`${s.ad.padEnd(21)} ${String(s.adim.length).padEnd(5)} ${String(g).padEnd(11)} ` + `${String(h).padEnd(10)} ${s.birlikte}`); } console.log(''); const gd = sonuc.at(-1); console.log('pencerede yeni surum yazdi, eski surum okudu'); for (const p of gd.pencere) console.log(` ${p.uye_no} ${p.ad.padEnd(12)} eski surumun gordugu deger: ${p.deger}`); console.log(''); console.log('daralt tamamlandiktan sonra cift yazan surum yazmayi denerse'); console.log(` ${gd.erken.hata}`); console.log(''); const db = gd.db; const say = (kosul) => db.prepare(`SELECT COUNT(*) AS n FROM uye WHERE ${kosul}`).get().n; db.exec('ALTER TABLE uye ADD COLUMN iletisim TEXT'); db.exec('UPDATE uye SET iletisim = eposta WHERE eposta IS NOT NULL'); console.log('daralt sonrasi geri alma: sutun yeniden eklenip epostadan dolduruldu'); console.log(` toplam uye : ${say('1 = 1')}`); console.log(` iletisimi kurtarilan : ${say('iletisim IS NOT NULL')}`); console.log(` iletisimi kurtarilamayan : ${say('iletisim IS NULL')}`); rmSync(kok, { recursive: true, force: true });
once goc sonra kod — 2 adim eski surum, goc yapilmadi tamam=20 hata=0 goc gecti, kod hala eski tamam=0 hata=20 yeni surum yayinlandi tamam=20 hata=0 once kod sonra goc — 2 adim eski surum, goc yapilmadi tamam=20 hata=0 kod yeni, sutun yok tamam=0 hata=20 goc gecti tamam=20 hata=0 genislet-daralt — 5 adim eski surum, goc yapilmadi tamam=20 hata=0 sema genis, kod hala eski tamam=20 hata=0 iki surum birlikte tamam=20 hata=0 yalniz cift yazan surum tamam=20 hata=0 cift yazma kaldirildi tamam=20 hata=0 sema dar, kod yeni tamam=20 hata=0 sira adim gonderilen hata alan iki surumun birlikte kostugu faz once goc sonra kod 2 60 20 0 once kod sonra goc 2 60 20 0 genislet-daralt 5 120 0 1 pencerede yeni surum yazdi, eski surum okudu U-0013 Cift Yazan eski surumun gordugu deger: onuc@kutuphane U-0014 Tek Yazan eski surumun gordugu deger: null daralt tamamlandiktan sonra cift yazan surum yazmayi denerse table uye has no column named iletisim daralt sonrasi geri alma: sutun yeniden eklenip epostadan dolduruldu toplam uye : 14 iletisimi kurtarilan : 9 iletisimi kurtarilamayan : 5
Yanlış Sıranın Ölçüsü
İlk iki sıra aynı göçü çalıştırıyor ve aynı sayıyı üretiyor: yirmi hata alan istek. Yönleri farklı, sonuçları değil. Göç önce giderse eski sürüm kendi sütununu bulamaz; kod önce giderse yeni sürüm henüz var olmayan sütunu arar. İkisinde de hatanın süresi, iki işlem arasındaki boşluktur — göç akşam koşup yayın sabah yapılıyorsa bu boşluk bir gecedir.
Bu sayının anlamı, önceki dersin sayısıyla yan yana konunca ortaya çıkıyor. Orada mavi–yeşil düzen sıfır düşen istekle ölçülmüştü. Aynı yayın, altındaki şema tek sürümü destekliyorsa yirmi hata üretir. Yayın düzeninin kesintisizliği şemanın uyumluluğuna bağlıdır; ikisi ayrı ayrı ölçülmediğinde birinin sıfırı diğerinin yirmisini gizler.
Genişlet–daralt sırası yüz yirmi isteğin hepsini hatasız karşıladı. Bedeli adım sayısında görünüyor: iki adım yerine beş. Beş adımın her biri ayrı bir yayın ya da ayrı bir göç koşumudur ve aralarında bir bekleme vardır — o beklemeler olmadan sıra bir anlam taşımaz. Değişikliğin takvimi bu yüzden uzar: iki adımlık bir yeniden adlandırma, beş adımlık ve birkaç yayına yayılan bir işe dönüşür.
Pencere ve Çift Yazma
Üçüncü faz, iki sürümün aynı şemayla birlikte koştuğu tek fazdır. Yirmi istek dönüşümlü olarak eski ve yeni sürüme gitti, hepsi tamamlandı. Bu pencerenin ayakta durmasının koşulu, yeni sürümün eski sütuna da yazmaya devam etmesidir.
Pencere içinde iki üye yazıldı. U-0013 çift yazan sürümle geldi ve eski sürüm onu
okuyabildi. U-0014 yalnız yeni sütunu yazan sürümle geldi ve eski sürümün gördüğü değer
null oldu. İkinci satırda hiçbir hata yok: istek 200 döndü, kayıt var, alan boş. Çift
yazmanın atlanması bir hata üretmez, eksik veri üretir; ve bu eksiklik yalnız eski
sürümün baktığı yerde görünür.
Çift yazmanın ne zaman kaldırılacağı da sıranın parçasıdır. Daralt adımı tamamlandıktan
sonra çift yazan sürüm yazmayı denediğinde table uye has no column named iletisim hatası
alıyor. Bu, dördüncü adımın — çift yazmanın kaldırılması — beşinci adımdan önce gelmesinin
nedenidir. Sıra iki uçtan da kısıtlıdır: genişletme kod yayınından önce, daraltma kod
yayınından sonra gelir.
Geri Alınamayan Adım
Beş adımın dördü geri alınabilir. Genişletme sütunu düşürerek, kod yayınları önceki sürüme dönerek geri alınır ve önceki dersin ölçtüğü gibi bu tek adımlık bir iştir. Daraltma böyle değildir.
Sütun geri eklenebilir; çıktı bunu yapıyor ve eposta alanından dolduruyor. On dört üyeden
dokuzunun iletişim bilgisi geri geldi, beşininki gelmedi. Kayıp olan beş üye, iletişim alanı
telefon numarası tutan üyelerdir: genişletme adımı yalnız e-posta görünenleri kopyaladığı
için o beş değer hiçbir zaman yeni sütuna geçmedi ve daraltma adımı onları taşıyan tek yeri
sildi. Geri alma komutu başarıyla çalıştı; sonuç yine de eksik.
Buradan iki kural çıkar. Birincisi, daraltma adımı geri alma penceresinin dışında durur: ondan sonrası için geri alma yolu yayın düzeni değil, yedekten dönmedir. İkincisi, genişletme adımının geri doldurması kaçırdığı satırları saymalıdır; bu ölçümde o sayı beştir ve daraltmadan önce bilinebilecek tek uyarı odur.
Sıranın Durduğu Yer
Göç adımları veri katmanında, yayın adımları dağıtım düzeninde durur; ikisini birbirine bağlayan bir ayar yoktur. Sırayı tutan tek şey yazılı bir adım listesidir ve o liste hiçbir yerde doğrulanmaz. Çift yazma ayarı ise uygulama kodundadır ve her düğümde bulunur: bir düğümde eski, diğerinde yeni sürüm koşarken çift yazmanın açık olup olmadığı düğüm başına değişebilir.
Yanlış ayarın sessizliği burada iki kademelidir. Yanlış sıra gürültülüdür: yirmi istek 500 alır, ilk dakikada görünür. Yanlış çift yazma sessizdir: istekler 200 döner, kayıtlar oluşur, yalnız bir alan boş kalır. Daraltmadan sonra fark edilen bu boşluk artık düzeltilemez, çünkü değerin durduğu sütun silinmiştir. Ölçülmesi gereken sayı bu yüzden hata sayısı değil, geri doldurmanın kaçırdığı satır sayısıdır.
Özet
- Aynı şema değişikliği göç önce ve kod önce sıralarında yirmişer hata alan istek üretti; genişlet–daralt sırasında yüz yirmi isteğin hepsi tamamlandı.
- Genişlet–daralt iki adım yerine beş adım ister ve adımlar arasındaki beklemelerle birlikte değişikliğin takvimini birkaç yayına yayar.
- İki sürümün aynı şemayla koşabildiği pencerenin koşulu çift yazmadır: çift yazan sürümün yazdığı kaydı eski sürüm okuyabildi, yalnız yeni sütunu yazanınkinde alan boş göründü ve hiçbir istek hata almadı.
- Çift yazma daraltmadan önce kaldırılır; daralttan sonra eski yazma yolu sütunu bulamaz.
- Sütun silme geri alınamaz: sütun yeniden eklenip yeni alandan dolduruldu, on dört üyenin dokuzu kurtarıldı, beşi kurtarılamadı — geri alma komutu hatasız çalıştığı hâlde.
Sonraki Adım
Bu konu boyunca ölçülen her karar, kaynağın var olduğunu varsaydı: ters vekilin arkasında duracak düğümler, yayın penceresinde ikiye katlanan süreçler, oturumun taşındığı depo ve göç adımlarının koştuğu veritabanı. Hiçbir ölçüm bu kaynağın ne kadar olması gerektiğini sormadı. Kurs bu soruyla kapanır: bir düğüm hangi yükü taşır, kaç düğüm gerekir, yayın ve arıza payı bu sayının üstüne ne ekler ve tahmin ile gerçekleşen arasındaki fark nasıl ölçülür.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.