Ders 03 / 25
Yazma Öncesi Günlük
Kesinleştirmenin veri sayfalarını diske yazmaya bağlanmasının maliyeti, yazma öncesi kuralın tanımı, günlük dosyasının kesinleştirmede büyüyüp veri dosyasının değişmemesi, kesinleştirilmiş verinin yalnız günlükte bulunduğunun gösterilmesi, çökme sonrası kurtarma ve günlük kipleriyle eşitleme ayarının ölçülen etkisi.
İçindekiler
Önceki ders verinin diskte sayfalar hâlinde durduğunu gösterdi. Bir satır güncellendiğinde önce bellekteki sayfa değişir. Peki o sayfa diske yazılmadan makine kapanırsa? İşlemler konusunda kesinleştirilen bir değişikliğin kalıcı olduğu söylenmişti; bu söz, sayfa henüz diskte değilken nasıl tutulur?
Akla gelen ilk cevap — kesinleştirme anında değişmiş bütün sayfaları diske yazmak — hem pahalıdır hem de yetmez. Bu ders neden yetmediğini ve gerçekte ne yapıldığını ele alır. Ölçülecek şey nettir: kesinleştirilmiş bir veri, kesinleştirmenin hemen ardından hangi dosyanın içindedir?
Sayfaları Yazmak Neden Çözüm Değil
Kesinleştirmede değişmiş sayfaları diske yazmanın üç ayrı sorunu vardır.
Yazmalar dağınıktır. Tek bir işlem birbirinden uzak sayfalara dokunabilir: bir ödünç satırı, üyenin bulunduğu sayfa, üç ayrı dizinin farklı yaprakları. Bunları yazmak dosyanın dört bir yanına küçük yazmalar yapmak demektir. Sıralı bir yazmanın maliyeti ise erişim başına değil, bayt başınadır.
Yazma bölünebilir. Dört kilobaytlık bir sayfanın diske yazılması, donanım düzeyinde tek bir bölünmez işlem olmayabilir. Yazmanın ortasında elektrik kesilirse sayfanın bir kısmı yeni, bir kısmı eski olur. Bu, kaybolmuş bir güncellemeden kötüdür: bozulmuş bir sayfadır.
Aynı sayfa tekrar tekrar yazılır. Sık güncellenen bir sayfa her kesinleştirmede yeniden yazılır. Oysa o sayfanın son hâlinin bir kez yazılması yeterlidir.
Yazma Öncesi Kural
Çözüm, kalıcılığı veri sayfalarından ayırmaktır. Motor, değişikliği önce yazma öncesi günlüğe (write-ahead log) yazar: dosyanın sonuna eklenen, ne değiştiğini anlatan sıralı kayıtlar. Kural tek cümledir:
Bir veri sayfası diske yazılmadan önce, o sayfadaki değişikliği anlatan günlük kaydı diske yazılmış olmalıdır.
Kesinleştirme, günlük kayıtlarının kalıcı olmasıyla tamamlanır. Veri sayfaları bellekte kalabilir; onları yazmak sonraya bırakılır.
Bu düzenlemenin üç sorunu da çözmesi doğrudandır. Yazma sıralıdır, çünkü günlük yalnız sonuna eklenir. Bölünmüş yazma tehlikesi ortadan kalkar, çünkü eksik yazılmış bir günlük kaydı kurtarma sırasında tanınıp yok sayılır — veri sayfası ise henüz hiç dokunulmamıştır. Sık güncellenen sayfa da tek seferde yazılır, çünkü sayfanın diske inmesi kesinleştirmeye bağlı değildir.
Kurtarma buradan çıkar. Çökmeden sonra açılışta motor günlüğü okur ve veri dosyasına yansımamış kesinleştirilmiş değişiklikleri uygular. Kesinleştirilmemiş olanlar uygulanmaz. Sonuç, çökme anındaki kesinleştirilmiş duruma tam olarak dönmektir.
İlişkisel Kuram kursunda ACID’in kalıcılık maddesi anlatılırken bu yapıdan önden yazma kütüğü adıyla söz edilmişti; aynı yapıdır, bu kurs boyunca katalogdaki adlandırma kullanılacaktır.
Günlük Dosyasının Ortaya Çıkışı
İddia ölçülebilir. Aşağıdaki koşum bir veritabanı kurar, günlük kipini açar, sonra beş bin satır ekleyip iki dosyanın boyutuna bakar. Denetim noktası devre dışı bırakılmıştır; nedeni sonraki dersin konusudur.
rm -f g.db g.db-wal g.db-shm sqlite3 g.db <<'SQL' CREATE TABLE odunc(odunc_id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, alis_tarihi TEXT, iade_tarihi TEXT); INSERT INTO odunc SELECT n, 1+(n*7)%200000, 1+(n*13)%120000, '2024-01-01', NULL FROM (WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n<50000) SELECT n FROM s); PRAGMA journal_mode = WAL; PRAGMA wal_autocheckpoint = 0; UPDATE odunc SET iade_tarihi = '2024-02-01' WHERE odunc_id = 1; .shell echo "--- baslangic ---" .shell echo "veri dosyasi: $(wc -c < g.db) gunluk: $(wc -c < g.db-wal)" INSERT INTO odunc SELECT 100000+n, 1+(n*7)%200000, 1+(n*13)%120000, '2024-03-01', NULL FROM (WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n<5000) SELECT n FROM s); .shell echo "--- 5000 satirlik islem kesinlestikten sonra ---" .shell echo "veri dosyasi: $(wc -c < g.db) gunluk: $(wc -c < g.db-wal)" SELECT count(*) AS toplam_satir FROM odunc; SQL
wal 0 --- baslangic --- veri dosyasi: 1380352 gunluk: 24752 --- 5000 satirlik islem kesinlestikten sonra --- veri dosyasi: 1380352 gunluk: 173072 55000
İlk iki satır iki ayarın döndürdüğü değerlerdir: günlük kipi wal oldu, kendiliğinden
denetim noktası sıfırlandı. Asıl sonuç alttadır. Beş bin satırlık ekleme kesinleştikten
sonra veri dosyasının boyutu bir bayt bile değişmedi — 1.380.352 idi, 1.380.352 kaldı.
Günlük dosyası ise 24.752 bayttan 173.072 bayta çıktı. Sorgu ise yeni satırları görüyor:
55.000.
Dosya boyutları ortama ve sayfa boyutuna bağlıdır; değişmeyecek olan, hangi dosyanın büyüdüğüdür.
Kesinleştirilmiş Veri Nerede
Veri dosyasının değişmemesi, kesinleştirilmiş satırların orada olmadığı anlamına gelir. Bunu doğrudan sınamanın yolu, dosyaları ayırıp ayrı ayrı açmaktır.
rm -f g.db g.db-wal g.db-shm yalniz_veri.db veri_ve_gunluk.db veri_ve_gunluk.db-wal sqlite3 g.db <<'SQL' CREATE TABLE odunc(odunc_id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, alis_tarihi TEXT, iade_tarihi TEXT); INSERT INTO odunc SELECT n, 1+(n*7)%200000, 1+(n*13)%120000, '2024-01-01', NULL FROM (WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n<50000) SELECT n FROM s); PRAGMA journal_mode = WAL; PRAGMA wal_autocheckpoint = 0; INSERT INTO odunc SELECT 100000+n, 1+(n*7)%200000, 1+(n*13)%120000, '2024-03-01', NULL FROM (WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n<5000) SELECT n FROM s); .shell cp g.db yalniz_veri.db .shell cp g.db veri_ve_gunluk.db .shell cp g.db-wal veri_ve_gunluk.db-wal SQL echo "--- yalniz veri dosyasi kopyalandi ---" sqlite3 yalniz_veri.db "SELECT count(*) FROM odunc;" echo "--- veri dosyasi + gunluk kopyalandi ---" sqlite3 veri_ve_gunluk.db "SELECT count(*) FROM odunc;"
wal 0 --- yalniz veri dosyasi kopyalandi --- 50000 --- veri dosyasi + gunluk kopyalandi --- 55000
Sonuç tartışmasızdır. Yalnız veri dosyası kopyalandığında elli bin satır görünüyor: kesinleştirilmiş beş bin satır orada yok. Günlük de kopyalandığında elli beş bin satır görünüyor. Kesinleştirilmiş veri, kesinleştirmenin hemen ardından günlüğün içindedir.
Bu gözlemin yedekleme tarafında doğrudan bir sonucu vardır ve bu kursun İşletim konusunda yeniden karşılaşılacaktır: çalışan bir veritabanının dosya düzeyinde yedeği, günlüğü de kapsamak zorundadır. Yalnız veri dosyasını kopyalayan bir yedek, en son kesinleştirilmiş işlemleri içermez.
Çökmeden Sonra Kurtarma
Kurtarmanın gerçekten çalıştığını görmek için gerçek bir çökme gerekir. Aşağıdaki betik iki bin satırı tek bir işlemde kesinleştirir, sonra süreci hiçbir kapanış işlemi yapmadan öldürür: dosyalar kapatılmaz, denetim noktası alınmaz, tampon boşaltılmaz.
rm -f c.db c.db-wal c.db-shm cat > cokme.mjs <<'JS' import { DatabaseSync } from 'node:sqlite'; const db = new DatabaseSync('c.db'); db.exec('PRAGMA journal_mode = WAL'); db.exec('PRAGMA wal_autocheckpoint = 0'); db.exec(`CREATE TABLE odunc(odunc_id INTEGER PRIMARY KEY, uye_id INT, alis_tarihi TEXT)`); db.exec('BEGIN'); for (let i = 1; i <= 2000; i++) db.exec(`INSERT INTO odunc VALUES (${i}, ${1 + (i * 13) % 500}, '2024-03-01')`); db.exec('COMMIT'); console.log('kesinleştirildi; işlem içindeki satır sayısı:', db.prepare('SELECT count(*) AS c FROM odunc').get().c); process.kill(process.pid, 'SIGKILL'); // temizlik yapmadan, kapanış olmadan sonlan JS node cokme.mjs echo "süreç çıkış kodu: $?" echo "--- çökme anındaki dosyalar ---" echo "veri dosyası: $(wc -c < c.db) günlük: $(wc -c < c.db-wal)" echo "--- veritabanı yeniden açılıyor ---" sqlite3 c.db "SELECT count(*) FROM odunc;"
kesinleştirildi; işlem içindeki satır sayısı: 2000 süreç çıkış kodu: 137 --- çökme anındaki dosyalar --- veri dosyası: 4096 günlük: 61832 --- veritabanı yeniden açılıyor --- 2000
Çıkış kodu 137, sürecin öldürülerek sonlandığını gösterir. Çökme anında veri dosyası tek bir sayfa büyüklüğünde: ne tablo tanımı ne de satırlar oraya yazılmış. Her şey 61.832 baytlık günlüktedir. Veritabanı yeniden açıldığında iki bin satırın tamamı yerindedir.
Kalıcılığın nasıl sağlandığı budur. Kesinleştirme, veriyi son duracağı yere yazmak değil, neyin değiştiğini geri döndürülemez biçimde kaydetmektir. Verinin yerine yerleşmesi sonraki dersin konusudur.
Günlük Kipi ve Eşitleme Ayarı
Günlük tutmanın tek bir biçimi yoktur ve seçim ölçülebilir bir fark yaratır. İki eksen vardır.
Birinci eksen günlük kipidir. Yazma öncesi günlükte değişiklikler ayrı bir dosyaya eklenir, veri dosyası olduğu gibi kalır. Bunun alternatifi geri alma günlüğüdür (rollback journal): değişiklikten önceki sayfanın bir kopyası ayrı dosyaya yazılır, sayfanın yeni hâli doğrudan veri dosyasına işlenir; işlem geri alınırsa eski sayfalar geri yazılır. Kipin adı ve seçenekleri motora özgüdür.
İkinci eksen eşitleme düzeyidir: kesinleştirmede motorun işletim sisteminden yazmanın gerçekten diske indiğini beklemesi mi, yoksa yazmayı teslim edip devam etmesi mi.
cat > kip.mjs <<'JS' import { DatabaseSync } from 'node:sqlite'; import { rmSync } from 'node:fs'; function olc(kip, esitleme) { for (const ek of ['', '-wal', '-shm', '-journal']) rmSync('k.db' + ek, { force: true }); const db = new DatabaseSync('k.db'); db.exec(`PRAGMA journal_mode = ${kip}`); db.exec(`PRAGMA synchronous = ${esitleme}`); db.exec('CREATE TABLE odunc(odunc_id INTEGER PRIMARY KEY, uye_id INT, alis_tarihi TEXT)'); const ekle = db.prepare('INSERT INTO odunc VALUES (?, ?, ?)'); const bas = process.hrtime.bigint(); for (let i = 1; i <= 2000; i++) { // her satır ayrı bir işlem db.exec('BEGIN'); ekle.run(i, 1 + (i * 13) % 500, '2024-03-01'); db.exec('COMMIT'); } const sure = Number(process.hrtime.bigint() - bas) / 1e6; db.close(); return sure; } const olculer = [['delete', 'FULL'], ['wal', 'FULL'], ['wal', 'NORMAL']]; const sonuc = olculer.map(([k, e]) => [k, e, olc(k, e)]); const taban = sonuc[0][2]; console.log('günlük kipi eşitleme 2000 işlem (ms) orana göre'); for (const [k, e, s] of sonuc) console.log(k.padEnd(13), e.padEnd(10), s.toFixed(0).padStart(14), (taban / s).toFixed(1).padStart(12) + '×'); JS node kip.mjs
günlük kipi eşitleme 2000 işlem (ms) orana göre delete FULL 335 1.0× wal FULL 60 5.6× wal NORMAL 12 26.8×
Süreler ortama bağlıdır; okunacak olan oranlardır. Yazma öncesi günlük, geri alma günlüğüne göre bu ortamda beş buçuk kat hızlı kesinleştirdi. Nedeni yazma biçimidir: geri alma günlüğünde her işlem hem günlük dosyasını hem veri dosyasını değiştirir ve sonunda günlük dosyasını siler; yazma öncesi günlükte tek bir dosyanın sonuna eklenir.
Üçüncü satır ise ödünleşimin kendisidir. Eşitleme gevşetildiğinde ölçüm yirmi yedi kat hızlanır, çünkü kesinleştirme artık yazmanın diske indiğini beklemez. Bunun bedeli kalıcılıktır: ani bir güç kesintisinde en son kesinleştirilmiş birkaç işlem kaybolabilir. Veritabanı bozulmaz — günlük yapısı bunu engeller — ama “kesinleştirildi” denen bir işlem geri gelmeyebilir. Bu ayar, ACID’in kalıcılık maddesinden bilerek vazgeçmek anlamına gelir ve yalnız kaybı göze alınabilen veriler için savunulabilir.
Özet
- Kesinleştirmede veri sayfalarını diske yazmak dağınık yazma, bölünmüş yazma ve aynı sayfanın tekrar tekrar yazılması sorunlarını doğurur.
- Yazma öncesi kural, bir veri sayfası diske inmeden önce o değişikliği anlatan günlük kaydının kalıcı olmasını ister; kesinleştirme günlüğün kalıcı olmasıyla tamamlanır.
- Ölçümde beş bin satırlık bir ekleme kesinleştiğinde veri dosyası hiç değişmedi, günlük dosyası 24.752 bayttan 173.072 bayta çıktı.
- Yalnız veri dosyası kopyalandığında 50.000, günlükle birlikte kopyalandığında 55.000 satır görüldü: kesinleştirilmiş veri günlüğün içindedir.
- Öldürülen bir süreçten sonra veri dosyası tek sayfalık kalmasına rağmen iki bin satırın tamamı günlükten kurtarıldı.
- Günlük kipi ve eşitleme düzeyi ölçülebilir fark yaratır; eşitlemeyi gevşetmek bu ortamda yirmi yedi kat hızlandırdı ve karşılığında kalıcılıktan vazgeçti.
Sonraki Adım
Bu dersteki koşumlarda kendiliğinden denetim noktası kapatılmıştı ve günlük dosyası hiç küçülmeden büyüdü. Bu sürdürülebilir değildir: sınırsız büyüyen bir günlük diski doldurur ve kurtarma süresini uzatır, çünkü açılışta okunacak kayıt sayısı artar. Günlükteki değişikliklerin veri dosyasına işlenip günlüğün kısalması gerekir. Sonraki ders bu işlemi ele alır: denetim noktası ne yapar, ne sıklıkta yapılmalıdır ve sıklık ile kurtarma süresi arasındaki ödünleşim nasıl kurulur.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.