Ders 01 / 25
Süreç ve Bellek Mimarisi
Sunucu tabanlı ve süreç içi motor kurulumlarının farkı, arka plan süreçlerinin işbölümü, paylaşılan tampon havuzu ile bağlantı başına ayrılan bellek arasındaki ayrım, havuz boyutunun isabet oranına ölçülen etkisi ve paylaşılan havuzun özel önbelleklere üstünlüğü.
İçindekiler
İleri SQL kursu, sorgunun ne kadar işle çalıştığını ölçmeyi kurdu: dizin tanımı, koşul
yazımı, birleştirme sırası, istatistik tazeliği. Bütün bu kararlar sorgu düzeyindeydi ve
motorun kendisi kara kutu olarak kaldı. Plan çıktısında SCAN odunc yazdığında iki milyon
satırın okunduğu biliniyordu; o satırların nereden okunduğu, bellekte mi diskte mi
bulundukları, okuyanın kim olduğu sorulmadı.
Bu ders o kutuyu açar. Sorusu şudur: bir sorgu geldiğinde onu hangi süreç karşılar, veri hangi bellekten okunur ve o belleğin boyutu sonucu ne kadar değiştirir? Bir tabloyu bellekte tutmakla diskten okumak arasındaki fark, iyi bir dizinle kötü bir dizin arasındaki farktan küçük değildir.
Motorun İki Kurulum Biçimi
İlişkisel motorlar iki ayrı biçimde kurulur ve bu seçim mimarinin geri kalanını belirler.
Sunucu tabanlı motor, uygulamadan bağımsız bir süreç kümesi olarak çalışır. Uygulama bir ağ bağlantısı üzerinden istek gönderir, motor isteği kendi süreçlerinde işler, sonucu geri yollar. Veri dosyalarına yalnız bu süreçler dokunur. Aynı veritabanına yüzlerce uygulama örneği bağlanabilir; hepsi tek bir yetkili sürece konuşur.
Süreç içi motor, uygulamanın kendi sürecine bağlanan bir kitaplıktır. Ayrı bir sunucu yoktur; sorgu, uygulamanın çağırdığı bir işlev çağrısıyla aynı süreçte çalışır. Veri dosyasına doğrudan işletim sistemi çağrılarıyla erişilir; eşzamanlılık, dosya kilitleri üzerinden yürütülür.
Ayrım bir kalite sıralaması değildir. Süreç içi motor ağ gecikmesini ve süreçler arası
iletişimi tümüyle ortadan kaldırır, kurulum ve yönetim gerektirmez; buna karşılık merkezî
bir denetim noktası olmadığı için çok yazarlı yükleri ve ayrıntılı yetkilendirmeyi
sunucu tabanlı kurulum kadar iyi karşılamaz. Bu kurstaki koşumlar süreç içi bir motorla
(sqlite3) yapılır; sunucu tabanlı kurulumun farklı davrandığı yerler metinde ayrıca
belirtilir.
Arka Plan Süreçlerinin İşbölümü
Sunucu tabanlı bir motorda gelen sorguyu karşılayan süreç, işin yalnız bir bölümünü yapar. Geri kalanı, kimsenin isteği olmadan sürekli çalışan arka plan süreçlerine (background process) düşer. Adları motora göre değişir; işlevleri değişmez.
Bağlantı karşılayıcı. Yeni bağlantıları kabul eder, kimlik doğrulamasını yapar ve o bağlantıya bir çalışan süreç ya da iş parçacığı atar. Sorguyu ayrıştıran, planlayan ve yürüten bu çalışandır.
Günlük yazıcı. Değişiklik kayıtlarını, kesinleştirme anında diske yazılmayı bekleyen tampondan alıp dosyaya aktarır. Sonraki iki ders bu sürecin ne yazdığını ayrıntısıyla ele alır.
Denetim noktası süreci. Bellekte değişmiş sayfaları belirli aralıklarla veri dosyalarına yazar. Görevi, kurtarmanın ne kadar geriye gitmesi gerektiğini sınırlamaktır.
Temizlik süreci. Silinmiş ya da güncellenmiş satırların artık kimsenin görmediği eski sürümlerini toplar ve yerini yeniden kullanılabilir kılar. Bu kursta Ölü Satır Temizliği dersinin konusudur.
İstatistik toplayıcı. Tabloların satır sayısı, değer dağılımı ve erişim sayaçları gibi bilgileri günceller. İleri SQL kursundaki İstatistikler ve Kardinalite dersinde planlayıcının tahmin kaynağı olarak geçen sayılar bu süreçten gelir.
Bu ayrıştırmanın nedeni, sorguyu bekletmemektir. Bir sorgu, kendi ürettiği çöpü toplamak ya da bütün değişmiş sayfaları diske yazmak zorunda kalsaydı, yanıt süresi kullanıcının işiyle ilgisiz işlerle şişerdi. Arka plan süreçleri bu işleri sorgunun dışına taşır; karşılığında yönetim, bu süreçlerin ne sıklıkta ve ne kadar agresif çalışacağını ayarlamak olur.
Paylaşılan Bellek ve Bağlantı Başına Bellek
Motorun kullandığı bellek tek bir havuz değildir; iki ayrı sınıfa ayrılır ve bu ayrım sunucu boyutlandırmasının temelidir.
Paylaşılan tampon havuzu (shared buffer pool), veri dosyasından okunan sayfaların tutulduğu ortak alandır. Bütün bağlantılar aynı havuzu kullanır. Bir sorgu bir sayfayı okuduğunda sayfa havuza girer; başka bir bağlantı aynı sayfayı isterse disk hiç okunmaz. Havuz aynı zamanda yazma yolunda da durur: değiştirilen sayfa önce havuzda değiştirilir, diske yazılması sonraya bırakılır.
Bağlantı başına bellek, tek bir sorgunun kendi işi için ayırdığı alandır: sıralama tamponu, karma birleştirmenin karma tablosu, geçici sonuç kümeleri. Bu alan paylaşılmaz; her bağlantı kendi payını ayrı ayrı ister.
İki sınıfın ayrımı boyutlandırmada işe yarar, çünkü ölçekleme biçimleri farklıdır. Paylaşılan havuz sabit bir maliyettir: iki kat bağlantı, iki kat havuz gerektirmez. Bağlantı başına bellek ise bağlantı sayısıyla çarpılır; sorgu başına ayrılan sıralama tamponu iki yüz eşzamanlı bağlantıda iki yüz kez ayrılabilir. Bellek yetersizliğiyle sonuçlanan yapılandırmaların çoğu bu çarpımın gözden kaçmasından doğar.
Tampon Havuzunun Ölçülen Etkisi
Süreç içi motorda sayfa önbelleği bağlantı başınadır ve boyutu bir yapılandırma değeriyle belirlenir. Aşağıdaki koşum aynı sorguyu iki farklı önbellek boyutuyla üç kez çalıştırır ve sayfa önbelleği isabet/ıska sayaçlarını yazdırır. Veri kümesi, önceki kurslardan devam eden kütüphane ödünç kayıtlarıdır.
rm -f kutuphane.db sqlite3 kutuphane.db <<'SQL' CREATE TABLE kitap ( kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL, yazar TEXT NOT NULL, yil INTEGER NOT NULL, sube_id INTEGER NOT NULL ); INSERT INTO kitap (kitap_id, baslik, yazar, yil, sube_id) WITH RECURSIVE sayac(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM sayac WHERE n < 200000) SELECT n, 'Kitap ' || n, 'Yazar ' || (n % 4000), 1950 + (n % 75), 1 + (n % 8) FROM sayac; SQL sqlite3 kutuphane.db "SELECT 'kitap tablosu sayfa sayisi:', count(*) FROM dbstat WHERE name='kitap';" for boyut in 200 2000; do printf '=== onbellek %s sayfa ===\n' "$boyut" sqlite3 kutuphane.db <<SQL 2>&1 | grep -E 'Page cache (hits|misses)' PRAGMA cache_size = $boyut; .stats on SELECT count(*) FROM kitap WHERE yil > 2000; SELECT count(*) FROM kitap WHERE yil > 2000; SELECT count(*) FROM kitap WHERE yil > 2000; SQL done
kitap tablosu sayfa sayisi:|1780 === onbellek 200 sayfa === Page cache hits: 2 Page cache misses: 1781 Page cache hits: 2 Page cache misses: 1779 Page cache hits: 2 Page cache misses: 1779 === onbellek 2000 sayfa === Page cache hits: 2 Page cache misses: 1781 Page cache hits: 1781 Page cache misses: 0 Page cache hits: 1781 Page cache misses: 0
Sayılar sorunun tamamını anlatıyor. Tablo 1780 sayfa tutuyor. Önbellek 200 sayfa olduğunda her tarama neredeyse bütün sayfaları yeniden okumak zorunda: ilk taramanın sonunda önbellekte yalnız son 200 sayfa vardır, ikinci tarama ise baştan başlar ve orada bulunmayan sayfaları ister. İsabet oranı üç taramada da sıfıra yakın kalır.
Önbellek 2000 sayfa olduğunda birinci tarama yine 1781 ıska üretir — veri henüz bellekte değildir. İkinci ve üçüncü taramalar ise tek bir ıska bile üretmez: bütün sayfalar havuzdadır. Aynı sorgu, aynı plan, aynı adım sayısı; tek fark sayfaların nereden geldiğidir.
Çıktının okunmasında bir uyarı gerekir: .stats on sayaçları motorun kendi sayfa
önbelleğine aittir ve işletim sisteminin dosya önbelleğini kapsamaz. Bir ıska, verinin
mutlaka diskten geldiği anlamına gelmez — yalnız motorun havuzunda bulunmadığı anlamına
gelir. Sayfa sayısı ise ortamdan bağımsızdır: aynı veri ve aynı sayfa boyutu her makinede
1780 sayfa verir.
Havuz Boyutu ve Çalışma Kümesi
Ölçüm bir uçtan diğerine geçişi gösterdi. Aradaki eğri, havuz boyutunu seçerken asıl bakılması gereken şeydir ve gerçek bir motorda tek tek denemek pahalıdır. Aşağıdaki model onu ucuza gösterir: sayfa isteklerinin bir bölümü tablonun küçük bir kesimine yığılır, havuz en az kullanılanı atarak yer açar.
Bu bir modeldir; motorun kendi tampon havuzu değildir. Tahliye politikası gerçek motorlarda daha ayrıntılıdır ve okuma dışında yazma da havuzu etkiler. Modelin gösterdiği şey, boyut ile isabet oranı arasındaki ilişkinin biçimidir.
cat > havuz.mjs <<'JS' // Model: en az kullanilani atan bir sayfa havuzu. class Havuz { constructor(kapasite) { this.kapasite = kapasite; this.sayfalar = new Map(); this.isabet = 0; this.iska = 0; } oku(sayfa) { if (this.sayfalar.has(sayfa)) { this.sayfalar.delete(sayfa); this.sayfalar.set(sayfa, 1); this.isabet++; return; } this.iska++; if (this.sayfalar.size >= this.kapasite) this.sayfalar.delete(this.sayfalar.keys().next().value); this.sayfalar.set(sayfa, 1); } oran() { return this.isabet / (this.isabet + this.iska); } } function uretec(tohum) { // belirlenimci sozde rastgele uretec let a = tohum >>> 0; return () => { a = (a + 0x9e3779b9) >>> 0; let t = a; t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } const TABLO = 2000, SICAK = 200, ISTEK = 500000; function yukUret(tohum) { // isteklerin %90'i sicak 200 sayfaya gidiyor const r = uretec(tohum), l = new Int32Array(ISTEK); for (let i = 0; i < ISTEK; i++) l[i] = r() < 0.9 ? Math.floor(r() * SICAK) : SICAK + Math.floor(r() * (TABLO - SICAK)); return l; } console.log('— tek havuz: kapasite ile isabet oranı —'); const yuk = yukUret(2024); for (const k of [20, 50, 100, 200, 400, 1000, 2000]) { const h = new Havuz(k); for (const s of yuk) h.oku(s); console.log(String(k).padStart(6), 'sayfa →', (h.oran() * 100).toFixed(1).padStart(5), '%'); } console.log('— aynı toplam bellek: dört özel önbellek / bir paylaşılan havuz —'); const yukler = [1, 2, 3, 4].map((i) => yukUret(2024 + i)); const ozel = [1, 2, 3, 4].map(() => new Havuz(100)); let i_ozel = 0, k_ozel = 0; yukler.forEach((y, i) => { for (const s of y) ozel[i].oku(s); }); ozel.forEach((h) => { i_ozel += h.isabet; k_ozel += h.iska; }); const paylasilan = new Havuz(400); for (let i = 0; i < ISTEK; i++) for (const y of yukler) paylasilan.oku(y[i]); console.log('dört × 100 sayfa özel →', ((i_ozel / (i_ozel + k_ozel)) * 100).toFixed(1), '%'); console.log('bir × 400 sayfa paylaşılan →', (paylasilan.oran() * 100).toFixed(1), '%'); JS node havuz.mjs
— tek havuz: kapasite ile isabet oranı —
20 sayfa → 8.1 %
50 sayfa → 20.1 %
100 sayfa → 39.4 %
200 sayfa → 73.6 %
400 sayfa → 91.1 %
1000 sayfa → 94.4 %
2000 sayfa → 99.6 %
— aynı toplam bellek: dört özel önbellek / bir paylaşılan havuz —
dört × 100 sayfa özel → 39.4 %
bir × 400 sayfa paylaşılan → 91.1 %
Eğrinin biçimi tektiptir: havuz, sık istenen sayfa kümesini alacak kadar büyüyene kadar isabet oranı hızla yükselir, o noktadan sonra kazanç yassılaşır. Bu kümeye çalışma kümesi (working set) denir. Modelde çalışma kümesi 200 sayfadır; havuz 200’e ulaştığında oran %73,6, 400’de %91,1 olur, sonrasında iki katı bellek yalnız üç puan getirir.
Yönetim kararı buradan çıkar: havuzu tablonun tamamına göre değil, gerçekten dokunulan bölümüne göre boyutlandırmak gerekir. Yüz gigabaytlık bir veritabanının çalışma kümesi birkaç gigabayt olabilir; belleği o eşiğin biraz üstüne çıkarmak, veritabanının tamamını belleğe sığdırmaya çalışmaktan hem ucuz hem de neredeyse aynı ölçüde etkilidir.
Modelin ikinci bölümü, mimarinin başındaki ayrımı sayıya çevirir. Toplam bellek her iki düzende de 400 sayfadır. Dört bağlantı kendi 100 sayfalık önbelleğini tuttuğunda isabet oranı %39,4’te kalır; aynı bellek tek bir paylaşılan havuz olduğunda %91,1’e çıkar. Neden açıktır: dört bağlantının çalışma kümeleri büyük ölçüde örtüşür, özel önbelleklerde aynı sıcak sayfalar dört kez saklanır ve her kopya için ayrı ayrı yer harcanır. Sunucu tabanlı motorların tampon havuzunu paylaştırmasının gerekçesi budur.
Özet
- Sunucu tabanlı motor ayrı bir süreç kümesi olarak çalışır ve veri dosyalarına yalnız o süreçler dokunur; süreç içi motor uygulamanın sürecine bağlanan bir kitaplıktır.
- Arka plan süreçleri günlük yazma, denetim noktası, temizlik ve istatistik toplama işlerini sorgunun yanıt yolundan çıkarır.
- Bellek iki sınıfa ayrılır: paylaşılan tampon havuzu bağlantı sayısıyla ölçeklenmez, bağlantı başına ayrılan sıralama ve karma alanları ölçeklenir.
- Ölçümde 1780 sayfalık bir tablo, 200 sayfalık önbellekte her taramada yeniden okundu; 2000 sayfalık önbellekte ikinci taramadan sonra hiç ıska üretmedi.
- Model, isabet oranının çalışma kümesi havuza sığana kadar hızla yükselip sonra yassılaştığını ve aynı toplam belleğin paylaşılan havuzda %39,4 yerine %91,1 isabet verdiğini gösterdi.
Sonraki Adım
Bu derste sayfa, bellekte tutulan bir birim olarak geçti: havuza giren, atılan, isabet ya da ıska üreten bir şey. Sayfanın kendisinin ne olduğu, içine kaç satırın girdiği, satır sığmadığında ne olduğu ve bütün bu sayfaların diskte hangi dosyalara nasıl yerleştiği sorulmadı. Sonraki ders veriyi diskteki hâliyle ele alır: sayfa boyutu, sayfa sayısı ve dosya boyutu arasındaki ilişki ölçülür, tablo ile dizinin dosya içindeki payı ayrı ayrı sayılır ve tablo alanı kavramı tanımlanır.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.