Ders 25 / 25
Sürüm Yükseltme
Çalışan bir sistemin altındaki katmanın değiştirilmesi: yükseltme yollarının kesinti penceresi, şemanın katalogdan çıkarılıp karşılaştırılmasıyla uyumluluk denetimi, genişlet–daralt göçünün her adımında hangi uygulama sürümünün çalıştığı ve geri dönüş planı.
İçindekiler
Bu konudaki dersler bir veritabanının yaşamını sürdürmesi için gereken işleri kurdu: yetkilendirme, yedekleme, kurtarma, çoğaltma, erişilebilirlik, bağlantı yönetimi, izleme ve toplu aktarım. Geriye bir tanesi kaldı ve hepsini birden ilgilendirir.
Motorun sürümü zamanla değişir. Güvenlik düzeltmeleri, giderilen hatalar ve desteğin sona ermesi yükseltmeyi eninde sonunda zorunlu kılar. Yükseltme, çalışan bir sistemin altındaki katmanı değiştirmektir; üstteki uygulama çalışmaya devam ederken yapılır ve yanlış giderse geri dönülebilmesi gerekir.
Yükseltme Yolları ve Kesinti Penceresi
Üç yol vardır ve aralarındaki fark kesintinin ne kadar sürdüğüdür.
Yerinde yükseltme (in-place upgrade), motor yazılımının değiştirilip aynı veri dosyalarıyla başlatılmasıdır. Kesinti, sunucunun durup yeniden başlaması kadardır. Koşulu, yeni sürümün eski dosya biçimini okuyabilmesidir; okuyamıyorsa dosyaların dönüştürülmesi gerekir ve kesinti veri büyüklüğüne bağlanır.
Dök ve yükle, verinin mantıksal yedek olarak dışarı alınıp yeni sürüme yüklenmesidir. Her sürüm arasında çalışır, çünkü taşınan şey dosya değil deyimlerdir. Kesintisi, yedeklemenin ve geri yüklemenin toplam süresidir.
Mantıksal çoğaltmayla geçiş, yeni sürümde kurulan bir veritabanının eskisine abone edilmesidir. Veri akarken eski sürüm çalışmayı sürdürür; kesinti yalnız uygulamaların yönlendirilmesi kadardır. Bu yol Mantıksal Çoğaltma dersinde kurulmuştu.
Aşağıdaki blok ikinci yolun kesintisini ölçüyor: aynı veritabanı iki büyüklükte dökülüp geri yükleniyor. Süreler makineye ve diske bağlıdır; anlamlı olan iki satır arasındaki orandır.
rm -f eski.db yeni.db dokum.sql node - <<'EOF' const { DatabaseSync } = require("node:sqlite"); const { spawnSync } = require("node:child_process"); const fs = require("node:fs"); const kur = (dosya, n) => { fs.rmSync(dosya, { force: true }); const db = new DatabaseSync(dosya); db.exec(`CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT NOT NULL, uye_id INT NOT NULL, sube_id INT NOT NULL, alis TEXT NOT NULL, iade TEXT)`); const e = db.prepare("INSERT INTO odunc(kitap_id,uye_id,sube_id,alis) VALUES(?,?,?,?)"); db.exec("BEGIN"); for (let i = 0; i < n; i++) e.run(i % 400 + 1, i % 900 + 1, i % 3 + 1, "2025-06-01"); db.exec("COMMIT"); db.exec("CREATE INDEX odunc_uye ON odunc(uye_id)"); db.exec("CREATE INDEX odunc_sube_alis ON odunc(sube_id, alis)"); db.close(); }; const sure = (is) => { const t = process.hrtime.bigint(); is(); return Number(process.hrtime.bigint() - t) / 1e6; }; console.log("satir | dosya | dokum | geri yukleme | kesinti"); console.log("----------|----------|---------|--------------|--------"); for (const n of [100000, 400000]) { kur("eski.db", n); const boyut = fs.statSync("eski.db").size; const dokum = sure(() => { const c = spawnSync("sqlite3", ["eski.db", ".dump"], { maxBuffer: 1 << 30 }); fs.writeFileSync("dokum.sql", c.stdout); }); fs.rmSync("yeni.db", { force: true }); const yukleme = sure(() => { spawnSync("sqlite3", ["yeni.db"], { input: fs.readFileSync("dokum.sql"), maxBuffer: 1 << 30 }); }); const dogru = new DatabaseSync("yeni.db").prepare("SELECT COUNT(*) c FROM odunc").get().c; console.log(n.toLocaleString("tr-TR").padStart(9) + " | " + (Math.round(boyut / 1024) + " KB").padStart(8) + " | " + (dokum.toFixed(0) + " ms").padStart(7) + " | " + (yukleme.toFixed(0) + " ms").padStart(12) + " | " + ((dokum + yukleme).toFixed(0) + " ms").padStart(7) + " (" + dogru + " satir)"); } EOF
satir | dosya | dokum | geri yukleme | kesinti ----------|----------|---------|--------------|-------- 100.000 | 5712 KB | 60 ms | 160 ms | 220 ms (100000 satir) 400.000 | 23040 KB | 206 ms | 663 ms | 869 ms (400000 satir)
Dört kat veri, yaklaşık dört kat süre. Dök ve yükle yolunun kesintisi veri büyüklüğüyle doğru orantılıdır ve bu, planlamanın dayanağıdır: küçük bir veritabanında ölçülen süre, üretim büyüklüğüne oranlanarak kesinti penceresi tahmin edilir. Oranın büyük veride de doğrusal kalacağı bir varsayımdır; gerçek pencere, üretim büyüklüğündeki bir kopyayla sınanarak bulunur.
Geri yükleme, dökümden üç kat uzun sürüyor. Neden, önceki derste ölçülen şeydir: döküm yalnız okur, geri yükleme ise satırları yeniden yazar ve dizinleri yeniden kurar. Kesinti penceresi hesaplanırken bakılacak sayı geri yükleme tarafıdır.
Uyumluluk Denetimi
Yükseltmenin kırdığı şey genellikle motor değil, uygulamayla veritabanı arasındaki sözleşmedir. Yeni sürümle birlikte gelen şema, uygulamanın beklediği şemadan farklıysa uygulama çalışmaz. Bu farkın yükseltmeden önce, katalogdan okunarak bulunması gerekir.
Sistem kataloğu bu kursun Motor Mimarisi konusunda ele alınmıştı. Aşağıdaki blok onu bir denetim aracı olarak kullanıyor: üretimdeki şema ile adaydaki şema aynı sorguyla karşılaştırılıyor.
rm -f uretim.db aday.db # Uretimdeki sema. sqlite3 uretim.db <<'SQL' CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT NOT NULL, uye_id INT NOT NULL, sube_id INT, alis TEXT NOT NULL, iade TEXT); CREATE INDEX odunc_uye ON odunc(uye_id); CREATE INDEX odunc_alis ON odunc(alis); SQL # Yeni surumle birlikte gelen aday sema. sqlite3 aday.db <<'SQL' CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT NOT NULL, uye_id INT NOT NULL, sube_id INT NOT NULL, alis_tarihi TEXT NOT NULL, iade TEXT, gecikme_gunu INT DEFAULT 0); CREATE INDEX odunc_uye ON odunc(uye_id); CREATE INDEX odunc_sube_alis ON odunc(sube_id, alis_tarihi); SQL sqlite3 -box -header uretim.db <<'SQL' ATTACH 'aday.db' AS aday; SELECT IFNULL(u.name, a.name) AS alan, IFNULL(u.type || CASE WHEN u."notnull" THEN ' NOT NULL' ELSE '' END, '-') AS uretim, IFNULL(a.type || CASE WHEN a."notnull" THEN ' NOT NULL' ELSE '' END, '-') AS aday, CASE WHEN u.name IS NULL THEN 'eklendi' WHEN a.name IS NULL THEN 'kaldirildi' WHEN u.type <> a.type OR u."notnull" <> a."notnull" THEN 'degisti' ELSE 'ayni' END AS durum FROM pragma_table_info('odunc','main') u FULL OUTER JOIN pragma_table_info('odunc','aday') a USING(name) ORDER BY IFNULL(u.cid, 100 + a.cid); SELECT IFNULL(u.name, a.name) AS dizin, CASE WHEN u.name IS NULL THEN 'eklendi' WHEN a.name IS NULL THEN 'kaldirildi' ELSE 'ayni' END AS durum FROM (SELECT name FROM main.sqlite_schema WHERE type='index' AND sql IS NOT NULL) u FULL OUTER JOIN (SELECT name FROM aday.sqlite_schema WHERE type='index' AND sql IS NOT NULL) a USING(name) ORDER BY 1; SQL
┌──────────────┬───────────────┬───────────────┬────────────┐ │ alan │ uretim │ aday │ durum │ ├──────────────┼───────────────┼───────────────┼────────────┤ │ id │ INTEGER │ INTEGER │ ayni │ │ kitap_id │ INT NOT NULL │ INT NOT NULL │ ayni │ │ uye_id │ INT NOT NULL │ INT NOT NULL │ ayni │ │ sube_id │ INT │ INT NOT NULL │ degisti │ │ alis │ TEXT NOT NULL │ - │ kaldirildi │ │ iade │ TEXT │ TEXT │ ayni │ │ alis_tarihi │ - │ TEXT NOT NULL │ eklendi │ │ gecikme_gunu │ - │ INT │ eklendi │ └──────────────┴───────────────┴───────────────┴────────────┘ ┌─────────────────┬────────────┐ │ dizin │ durum │ ├─────────────────┼────────────┤ │ odunc_alis │ kaldirildi │ │ odunc_sube_alis │ eklendi │ │ odunc_uye │ ayni │ └─────────────────┴────────────┘
Farklar üç sınıfa ayrılır ve üçü ayrı risk taşır.
Eklenenler zararsızdır. Varsayılan değeri olan yeni bir sütun, eski uygulamayı etkilemez; eski sürüm o sütunu hiç bilmez.
Kaldırılanlar kırıcıdır. Kaldırılan sütun, o sütunu okuyan ya da yazan her deyimi bozar. Kaldırılan dizin ise sözdizimsel bir hata üretmez; bunun yerine o dizine dayanan sorgular sessizce yavaşlar. İkinci durum daha tehlikelidir, çünkü yükseltme “başarılı” görünür ve sorun ancak yük geldiğinde ortaya çıkar.
Değişenler koşulludur. Zorunlu olmayan bir sütunun zorunlu hâle gelmesi, o sütunu boş bırakan her ekleme deyimini bozar. Tip daralması aynı biçimde davranır.
Bu denetimin çıktısı bir listedir; kararı veren şey listenin uygulama koduyla karşılaştırılmasıdır. Kaldırılan sütuna hiçbir sorgu dokunmuyorsa risk yoktur; dokunuyorsa yükseltme, uygulamanın o sorgusu değişmeden yapılamaz.
Genişlet ve Daralt
Şema değişikliğini yükseltmeyle aynı ana sıkıştırmak, kaçınılmaz olarak kesinti üretir: uygulamanın eski sürümü ile yeni şema bir arada çalışmaz. Genişlet–daralt göçü (expand–contract migration) bu anı ikiye böler ve arada iki tarafın da çalıştığı bir pencere açar.
Aşağıdaki blok bunu bir sütunun yeniden adlandırılması üzerinde gösteriyor. Üç uygulama sürümü tanımlı: eski sürüm eski sütunu yazıp okur, geçiş sürümü iki sütuna da yazıp yeniyi okur, son sürüm yalnız yeni sütunu kullanır. Her aşamada üçü de denenip hangisinin çalıştığı yazılıyor.
rm -f goc.db node - <<'EOF' const { DatabaseSync } = require("node:sqlite"); const db = new DatabaseSync("goc.db"); db.exec(`CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT NOT NULL, uye_id INT NOT NULL, alis TEXT NOT NULL)`); db.exec("INSERT INTO odunc(kitap_id,uye_id,alis) VALUES(1,1,'2025-06-01')"); let sira = 100; // Uc uygulama surumu: her biri bir yazma ve bir okuma yapar. const eskiSurum = () => { db.prepare("INSERT INTO odunc(id,kitap_id,uye_id,alis) VALUES(?,2,2,'2025-06-02')").run(sira++); db.prepare("SELECT alis FROM odunc WHERE id=1").get().alis; }; const gecisSurumu = () => { // cift yazma: iki sutuna da yazar, yeniden okur db.prepare("INSERT INTO odunc(id,kitap_id,uye_id,alis,alis_tarihi)" + " VALUES(?,3,3,'2025-06-03','2025-06-03')").run(sira++); db.prepare("SELECT alis_tarihi FROM odunc WHERE id=1").get().alis_tarihi; }; const sonSurum = () => { db.prepare("INSERT INTO odunc(id,kitap_id,uye_id,alis_tarihi)" + " VALUES(?,4,4,'2025-06-04')").run(sira++); db.prepare("SELECT alis_tarihi FROM odunc WHERE id=1").get().alis_tarihi; }; const dene = (is) => { try { is(); return "calisir"; } catch { return "HATA"; } }; const durum = (asama) => console.log(asama.padEnd(33) + " | " + dene(eskiSurum).padEnd(10) + " | " + dene(gecisSurumu).padEnd(12) + " | " + dene(sonSurum)); console.log("asama | eski surum | gecis surumu | son surum"); console.log("----------------------------------|------------|--------------|----------"); durum("0. baslangic"); // Dogrudan yol: sutun yeniden adlandirilir. db.exec("ALTER TABLE odunc RENAME COLUMN alis TO alis_tarihi"); durum("dogrudan yeniden adlandirma"); // Basa donulur ve genislet-daralt yolu izlenir. db.exec("ALTER TABLE odunc RENAME COLUMN alis_tarihi TO alis"); db.exec("ALTER TABLE odunc ADD COLUMN alis_tarihi TEXT"); db.exec("UPDATE odunc SET alis_tarihi = alis WHERE alis_tarihi IS NULL"); db.exec(`CREATE TRIGGER goc_esitle AFTER INSERT ON odunc WHEN NEW.alis_tarihi IS NULL BEGIN UPDATE odunc SET alis_tarihi = NEW.alis WHERE id = NEW.id; END`); durum("1. genislet (sutun + esitleme)"); // Butun ornekler gecis surumune gectikten sonra eski sutun kaldirilir. db.exec("DROP TRIGGER goc_esitle"); db.exec("ALTER TABLE odunc DROP COLUMN alis"); durum("2. daralt (eski sutun kaldirildi)"); console.log(); console.log("eski surumun yazdigi satirlarda bos kalan yeni sutun: " + db.prepare("SELECT COUNT(*) c FROM odunc WHERE alis_tarihi IS NULL").get().c); EOF
asama | eski surum | gecis surumu | son surum ----------------------------------|------------|--------------|---------- 0. baslangic | calisir | HATA | HATA dogrudan yeniden adlandirma | HATA | HATA | calisir 1. genislet (sutun + esitleme) | calisir | calisir | HATA 2. daralt (eski sutun kaldirildi) | HATA | HATA | calisir eski surumun yazdigi satirlarda bos kalan yeni sutun: 0
Tabloda aranacak şey, her satırda kaç sürümün çalıştığıdır.
Doğrudan yeniden adlandırma satırında yalnız son sürüm çalışıyor. Şema değişikliği ile uygulamanın yeni sürümünün yayılması arasında geçen her saniye kesintidir; üstelik geri dönüş de yoktur, çünkü eski sürüme dönmek şemanın da geri alınmasını gerektirir.
Genişlet satırında iki sürüm birden çalışıyor. Bu, yayılmanın kademeli yapılabileceği anlamına gelir: örnekler teker teker geçiş sürümüne alınır, bir sorun görülürse eski sürüme dönülür. Şema değişikliği geri alınmaz, çünkü eklenen sütun eski sürümü rahatsız etmez.
Daralt satırı, bütün örnekler geçiş sürümüne alındıktan sonra uygulanır. Bu adımdan sonra geri dönüş, kaldırılan sütunu geri getirmeyi gerektirir; bu yüzden daraltma yükseltmenin en son ve en dikkatli adımıdır. Aradaki bekleme süresi, geri dönüş olasılığının kabul edilebilir biçimde azaldığı süredir.
Son satır eşitlemenin işlediğini gösteriyor: eski sürümün yalnız eski sütuna yazdığı satırlarda yeni sütun boş kalmadı. Bu eşitleme olmadan, geçiş penceresinde eski sürümün yazdığı her satır yeni sütunda eksik kalır ve daraltma anında sessizce veri kaybedilir.
Yükseltmenin Çevresindeki İşler
Yükseltme kararı, bu konudaki bütün derslerin toplandığı yerdir.
Sınama. Yükseltme önce üretim kopyası üzerinde yapılır. Kopyanın kaynağı, doğrulanmış bir yedektir; yedek doğrulama tatbikatı bu yüzden yükseltmenin ön koşuludur. Sınamada ölçülecek şey yalnız “çalışıyor mu” değil, sorgu planlarının değişip değişmediğidir. Planlayıcı sürümle birlikte değişir; iyi çalışan bir sorgu yeni sürümde farklı bir plan seçebilir. Bu risk, üretim iş yükünün kopya üzerinde tekrar oynatılmasıyla ölçülür.
Geri dönüş planı. Yükseltmenin bir geri dönüşü olmalıdır ve bu, “yedekten döneriz” cümlesinden ibaret olamaz — yedekten dönmek, yükseltmeden sonra yapılan işi silmektir. Yerinde yükseltmede geri dönüş, yükseltme öncesi alınmış bir dosya kopyasıdır; mantıksal çoğaltmayla geçişte ise ters yönde bir çoğaltma kurularak eski sürümün güncel tutulmasıdır.
Sıra. Çoğaltmalı bir kurulumda yükseltme sırası önemlidir. Yedek sunucular önce yükseltilir, birincil sonra; böylece devralma gerekirse hedef hazırdır. Fiziksel çoğaltma iki tarafın aynı sürümü kullanmasını gerektirdiği için bu sıra ancak mantıksal çoğaltmayla ya da kısa bir kesintiyle uygulanabilir.
Yükseltme sonrası. İstatistikler yenilenir, izleme değerleri yükseltme öncesiyle karşılaştırılır ve ilk yoğun saat gözlenir. Yükseltmenin başarılı sayılması, sürümün değişmesi değil, izlenen büyüklüklerin taban çizgisine dönmesidir.
Özet
- Yükseltme yolları kesinti penceresine göre ayrışır: yerinde yükseltme yeniden başlama kadar, dök ve yükle veri büyüklüğüyle orantılı, mantıksal çoğaltmayla geçiş yalnız yönlendirme kadar sürer.
- Dök ve yükle yolunda geri yükleme dökümden belirgin biçimde uzun sürer; kesinti hesaplanırken bakılacak sayı odur.
- Uyumluluk denetimi katalogdan yapılır: eklenen sütunlar zararsız, kaldırılanlar kırıcı, zorunluluğu değişenler koşulludur; kaldırılan dizin hata değil sessiz yavaşlama üretir.
- Genişlet–daralt göçü, şema değişikliğini iki adıma bölerek eski ve yeni uygulama sürümünün birlikte çalıştığı bir pencere açar; kademeli yayılım ve geri dönüş bu pencerede olanaklıdır.
- Daraltma adımı, bütün örnekler yeni sürüme geçtikten sonra uygulanır ve geri dönüşü en pahalı adımdır.
- Sürümle birlikte sorgu planları da değişebilir; yükseltme, üretim iş yükünün bir kopya üzerinde tekrar oynatılmasıyla sınanır.
Kurs Kapanışı
Bu kurs bir veritabanı motoruna üç ayrı yerden baktı.
Motor Mimarisi konusu, verinin nerede durduğunu ve kalıcılığın nasıl sağlandığını kurdu: süreç ve bellek düzeni, sayfa yapısı, yazma öncesi günlük, denetim noktaları, çok sürümlü eşzamanlılık denetimi, ölü satır temizliği ve sistem kataloğu. Bu konudaki tek kavram — günlüğün her değişikliği sırayla taşıması — sonraki iki konunun yarısını taşıdı: zaman noktasına kurtarma da, fiziksel çoğaltma da o günlüğün başka amaçlarla kullanılmasıdır.
Dizinler ve Bölümleme konusu, verinin nasıl bulunduğunu kurdu: dizin türleri, bileşik ve kısmi dizinler, kapsayan dizinler, dizin bakımı, tablo bölümleme ve parçalama kalıpları. Buradaki ödünleşim işletimde tekrar tekrar karşımıza çıktı — her dizin okumayı hızlandırır ve yazmayı yavaşlatır; toplu yükleme ölçümü bunun en açık kanıtıydı.
İşletim konusu, çalışan bir sistemin zaman içinde güvenli, kurtarılabilir ve erişilebilir kalmasını kurdu: roller ve satır düzeyi güvenlik, yedekleme türleri, zaman noktasına kurtarma, yedek doğrulama, fiziksel ve mantıksal çoğaltma, yüksek erişilebilirlik, bağlantı havuzlayıcıları, izleme, toplu yükleme ve sürüm yükseltme.
Bu on iki dersin ortak bir savı vardı: işletimde güvence, ölçülmemiş hiçbir şeyden gelmez. Doğrulanmamış yedek yedek değildir, izlenmeyen çoğaltma gecikmesi kurtarma noktası hedefi değildir, ölçülmemiş havuz boyutu bir varsayımdır ve sınanmamış bir yükseltme bir umuttur. Kütüphane ödünç kayıtları örneği boyunca her bölümde aynı soru soruldu: bunu nereden biliyoruz? Yanıt her seferinde bir ölçüm ya da bir tatbikat oldu.
Kurs boyunca ilişkisel modelin kendisi hiç sorgulanmadı: tablolar, satırlar, şema, işlemler ve SQL verili kabul edildi. Bu modelin karşılayamadığı gereksinimler de vardır. Şeması sürekli değişen belgeler, tek bir anahtarla erişilen devasa anahtar–değer kümeleri, ilişkilerin kendisinin sorgu konusu olduğu çizgeler ve saniyede milyonlarca ölçüm yazan zaman serileri, ilişkisel modelin güçlü yanlarını kullanmadan bedelini ödetir.
Bir sonraki kurs — İlişkisel Olmayan Veri Modelleri — bu ailelere bakıyor: anahtar–değer depoları, belge veritabanları, geniş sütun depoları, çizge ve zaman serisi sistemleri; belge modelinde gömme ile referans arasındaki karar; ve dağıtık davranışın kaçınılmaz soruları — çoğaltma kümeleri, parçalama, okuma ve yazma ilgileri, tutarlılık modelleri. Bu kursta öğrenilen çoğaltma, devralma, çoğunluk ve tutarlılık kavramları orada aynı adlarla geri gelecek; değişen şey, hangi güvenceden hangi başarım için vazgeçildiğidir.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.