İçeriğe geç
academia.sh

Ders 22 / 25

Bağlantı Havuzlayıcıları

Bağlantı sayısının yönetilmesi: bağlantı açmanın ölçülen maliyeti, oturum ve işlem düzeyinde havuzlamanın farkı, havuzdan devralınan oturum durumunun satır düzeyi güvenliği nasıl delebildiği ve havuz boyutunun kuyruk süresiyle ilişkisi.

İçindekiler

Devralmanın son adımı uygulamaların yeni birincile yönlendirilmesiydi; bu, uygulamanın veritabanına doğrudan değil, araya giren bir katman üzerinden bağlandığı anlamına gelir. Aynı katman, arıza olmadan da her gün karşılaşılan ikinci bir sorunu çözer.

Sorun şudur: uygulama sunucularının sayısı arttıkça bağlantı sayısı da artar. Her uygulama örneği kendi bağlantılarını açtığında, on örnek ve örnek başına yirmi bağlantı iki yüz bağlantı eder. Veritabanı motoru için bağlantı ucuz bir nesne değildir — her biri bellek tüketir, çoğu motorda ayrı bir süreç ya da iş parçacığı gerektirir ve boşta beklerken bile maliyeti sürer. Bağlantı sayısı belirli bir eşiği aştığında motor yavaşlar, sonra yeni bağlantı kabul etmeyi bırakır.

Bir Bağlantının Maliyeti

Bir bağlantının açılması, uzaktan bakıldığında tek bir işlem gibi görünür. İçeride birkaç adım vardır: taşıma katmanı bağlantısının kurulması, şifreli oturumun anlaşması, kimlik doğrulama, sunucu tarafında bir süreç ya da iş parçacığının başlatılması, oturum durumunun hazırlanması. Bunların hiçbiri veri okumaz.

Aşağıdaki blok bu maliyetin en düşük biçimini ölçüyor: dosya tabanlı bir motorda ağ yoktur, şifreleme yoktur, kimlik doğrulama yoktur, sunucu tarafında süreç başlatılmaz. Ölçülen şey yalnız dosyanın açılması ve şema bilgisinin okunmasıdır. Sunucu tabanlı bir motorda bu maliyetin üzerine sayılan adımlar eklenir; ölçüm bir taban değer verir, üst değer değil. Süreler makineye bağlıdır.

rm -f havuz.db

node - <<'EOF'
const { DatabaseSync } = require("node:sqlite");
const SORGU = "SELECT COUNT(*) c FROM odunc WHERE uye_id=?";
const N = 3000;

const kur = new DatabaseSync("havuz.db");
kur.exec("CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, alis TEXT)");
kur.exec("CREATE INDEX odunc_uye ON odunc(uye_id)");
const ekle = kur.prepare("INSERT INTO odunc(kitap_id,uye_id,alis) VALUES(?,?,?)");
kur.exec("BEGIN");
for (let i = 1; i <= 20000; i++) ekle.run(i % 400, i % 250, "2025-06-01");
kur.exec("COMMIT");
kur.close();

const olc = (ad, is) => {
  const t = process.hrtime.bigint();
  is();
  const ms = Number(process.hrtime.bigint() - t) / 1e6;
  console.log(ad.padEnd(38) + " | " + ms.toFixed(1).padStart(9) + " | " +
              (ms * 1000 / N).toFixed(1).padStart(11));
};

console.log("yol                                    | toplam ms | istek basi us");
console.log("---------------------------------------|-----------|--------------");
olc("her istekte yeni baglanti + deyim", () => {
  for (let i = 0; i < N; i++) {
    const db = new DatabaseSync("havuz.db");
    db.prepare(SORGU).get(i % 250);
    db.close();
  }
});
const paylasilan = new DatabaseSync("havuz.db");
olc("paylasilan baglanti, her istekte deyim", () => {
  for (let i = 0; i < N; i++) paylasilan.prepare(SORGU).get(i % 250);
});
const hazir = paylasilan.prepare(SORGU);
olc("paylasilan baglanti, hazir deyim", () => {
  for (let i = 0; i < N; i++) hazir.get(i % 250);
});
EOF
yol                                    | toplam ms | istek basi us
---------------------------------------|-----------|--------------
her istekte yeni baglanti + deyim      |     161.7 |        53.9
paylasilan baglanti, her istekte deyim |      18.2 |         6.1
paylasilan baglanti, hazir deyim       |      11.7 |         3.9

Aynı sorgu üç yolla çalıştırıldı ve istek başına süre on dört kata yakın değişti. Sorgunun kendisi her üç satırda da aynı işi yapıyor; fark, sorgunun etrafındaki hazırlıktır.

İkinci ile üçüncü satır arasındaki fark ayrı bir konuyu gösterir: hazır deyim (prepared statement) yeniden kullanıldığında ayrıştırma ve planlama işi tekrarlanmaz. Bu, İleri SQL kursunda plan önbelleği bağlamında ele alınan kazancın işletim tarafındaki karşılığıdır.

Bağlantı havuzu (connection pool) bu tabloyu birinci satırdan üçüncüye taşır. Havuz, önceden açılmış bir bağlantı kümesi tutar; uygulama bağlantı istediğinde havuzdan ödünç alır, işi bitince geri verir. Bağlantı kapanmaz, bir sonraki istek onu hazır bulur.

Havuzlama Düzeyleri

Havuzun bağlantıyı ne zaman geri aldığı, uygulamanın nelerden vazgeçmesi gerektiğini belirler. Üç düzey vardır.

Oturum düzeyinde havuzlama, bağlantıyı istemci bağlantısı kapanana kadar aynı istemciye verir. Uygulama açısından hiçbir şey değişmez; oturuma bağlı her özellik çalışır. Karşılığı, eşzamanlı istemci sayısı kadar veritabanı bağlantısı gerekmesidir — bağlantı sayısı sorunu tam olarak çözülmez.

İşlem düzeyinde havuzlama, bağlantıyı yalnız bir işlem süresince verir; işlem bitince bağlantı havuza döner ve başka bir istemciye gidebilir. Az sayıda bağlantıyla çok sayıda istemciye hizmet edilmesini bu düzey sağlar. Karşılığı, oturuma ait her şeyin güvenilmez hâle gelmesidir: geçici tablolar, oturum değişkenleri, oturum düzeyindeki kilitler ve bağlantıya iliştirilmiş hazır deyimler.

Deyim düzeyinde havuzlama bağlantıyı her deyimden sonra geri alır. Çok istemcili kurulumlarda en yüksek paylaşımı verir; çok deyimli işlem yazılamadığı için uygulamanın işlem kullanamamasına yol açar.

İşlem düzeyindeki havuzlamanın kısıtı bir başarım ayrıntısı değildir; güvenlik sonucu doğurabilir. Aşağıdaki blok bunu Satır Düzeyi Güvenlik dersindeki düzenek üzerinde gösteriyor: politika oturum değişkenine bakar, bağlantı ise iki istek arasında el değiştirir.

rm -f masa.db

node - <<'EOF'
const { DatabaseSync } = require("node:sqlite");

const db = new DatabaseSync("masa.db");   // havuzdaki tek baglanti
db.exec(`
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, sube_id INT, alis TEXT);
CREATE TABLE oturum(anahtar TEXT PRIMARY KEY, deger TEXT NOT NULL);
`);
const ekle = db.prepare("INSERT INTO odunc(kitap_id,uye_id,sube_id,alis) VALUES(?,?,?,?)");
db.exec("BEGIN");
for (let i = 1; i <= 900; i++)
  ekle.run(i % 400, i % 250, i <= 500 ? 1 : (i <= 800 ? 2 : 3), "2025-06-01");
db.exec("COMMIT");

// Politika oturum degiskenine bakar (Satir Duzeyi Guvenlik dersindeki dizenek).
const gorunen = () => db.prepare(
  "SELECT COUNT(*) c FROM odunc WHERE sube_id=" +
  "(SELECT CAST(deger AS INTEGER) FROM oturum WHERE anahtar='sube_id')").get().c;
const ayarla = (sube) => db.prepare(
  "INSERT INTO oturum VALUES('sube_id',?)" +
  " ON CONFLICT(anahtar) DO UPDATE SET deger=excluded.deger").run(String(sube));
const gercek = (s) => db.prepare("SELECT COUNT(*) c FROM odunc WHERE sube_id=?").get(s).c;

console.log("subelerin gercek kayit sayisi: 1 -> " + gercek(1) + ", 2 -> " + gercek(2));
console.log();

// 1. istek: 1 numarali subenin masasi baglantiyi alir.
ayarla(1);
console.log("1. istek (sube 1) gorunen satir      : " + gorunen());

// Baglanti havuza geri verilir. Havuz oturum durumunu temizlemiyor.
console.log("2. istek (sube 2), oturum ayarlanmadi: " + gorunen() +
            "  <- sube 1'in satirlari");

// Ayni sira, havuz devirde oturum durumunu temizliyor.
db.exec("DELETE FROM oturum");
console.log("2. istek (sube 2), havuz temizledi   : " + gorunen() +
            "  <- hicbir satir gorunmuyor");
ayarla(2);
console.log("2. istek (sube 2), oturum ayarlandi  : " + gorunen());
EOF
subelerin gercek kayit sayisi: 1 -> 500, 2 -> 300

1. istek (sube 1) gorunen satir      : 500
2. istek (sube 2), oturum ayarlanmadi: 500  <- sube 1'in satirlari
2. istek (sube 2), havuz temizledi   : 0  <- hicbir satir gorunmuyor
2. istek (sube 2), oturum ayarlandi  : 300

İkinci satır sorunun kendisidir. İkinci şubenin masası üç yüz kayıt görmesi gerekirken beş yüz kayıt görüyor; gördükleri başka bir şubenin ödünç kayıtları. Politika doğru yazılmış, sorgu doğru, veri doğru — yanlış olan tek şey, bağlantının önceki kullanıcının oturum değerini taşımasıdır.

Üçüncü satır çözümü gösterir: havuz, bağlantıyı devrederken oturum durumunu temizler. Temizlenmiş durumda politika hiçbir satır döndürmez. Bu davranış istenen davranıştır — kapalıya düşmek: bilgi eksikse veri gösterilmez. Uygulamanın her isteğin başında oturum değerini yeniden ayarlaması, dördüncü satırdaki doğru sonucu verir.

Buradan çıkan işletim kuralı, işlem düzeyinde havuzlama kullanan her kurulum için geçerlidir: oturuma bağlı hiçbir bilgi istekler arasında yaşadığı varsayılmadan yazılır ve havuzun devir temizliği yapılandırmadan doğrulanır.

Havuz Boyutu ve Kuyruk

Havuz boyutu, veritabanına aynı anda kaç isteğin girebileceğini belirler. Boyut küçükse istekler kuyrukta bekler; büyükse motorda eşzamanlı çalışan işlem sayısı artar ve işlemler birbirini yavaşlatır — kilit çekişmesi, önbellek paylaşımı ve zamanlayıcı yükü nedeniyle.

Aşağıdaki blok bunu benzetimle inceliyor. Gelişler, taban işlem süresi ve çekişme katsayısı model değerleridir; gerçek bir veritabanı çalıştırılmıyor. Çekişmenin biçimi de bir varsayımdır: eşzamanlı işlem sayısı çekirdek sayısını aştıkça her işlemin süresi doğrusal büyüyor. Gerçek bir sistemde bu eğri ölçülerek bulunur. Modelin gösterdiği şey eğrinin bir en iyisi olduğudur.

node - <<'EOF'
// Model: istekler havuzdan bir baglanti bekler, sonra veritabaninda calisir.
// Es zamanli islem sayisi cekirdek sayisini astikca her islem yavaslar (cekisme).
const TABAN = 10, CEKIRDEK = 8, CEKISME = 0.25;   // ms, adet, katsayi
const HIZ = 700, SURE = 30000, ADIM = 0.25;       // istek/sn, ms, ms
const ZAMAN_ASIMI = 200;                          // ms

let tohum = 20250616;
const rastgele = () => (tohum = (tohum * 1103515245 + 12345) % 2147483648) / 2147483648;
const gelis = [];
for (let t = 0; t < SURE; ) {
  t += -Math.log(1 - rastgele()) * 1000 / HIZ;
  if (t < SURE) gelis.push(t);
}

console.log("yuk: " + HIZ + " istek/sn, taban islem suresi " + TABAN + " ms, " +
            CEKIRDEK + " cekirdek, " + gelis.length + " istek");
console.log();
console.log("havuz | tamamlanan | kuyrukta | islemde | toplam  | zaman");
console.log("boyu  | istek      | (ort ms) | (ort ms)| (ort ms)| asimi");
console.log("------|------------|----------|---------|---------|-------");
for (const havuz of [2, 4, 8, 16, 32, 64]) {
  const kuyruk = [], aktif = [];
  let sonraki = 0, biten = 0, kuyrukToplam = 0, islemToplam = 0, asim = 0;
  for (let t = 0; t < SURE * 1.5; t += ADIM) {
    while (sonraki < gelis.length && gelis[sonraki] <= t) kuyruk.push(gelis[sonraki++]);
    while (kuyruk.length && aktif.length < havuz) {
      const g = kuyruk.shift();
      if (t - g > ZAMAN_ASIMI) { asim++; continue; }
      aktif.push({ bekleme: t - g, kalan: TABAN, basladi: t });
    }
    const yavaslik = 1 + CEKISME * Math.max(0, aktif.length - CEKIRDEK);
    for (const i of aktif) i.kalan -= ADIM / yavaslik;
    for (let i = aktif.length - 1; i >= 0; i--) {
      if (aktif[i].kalan > 0) continue;
      kuyrukToplam += aktif[i].bekleme;
      islemToplam += t - aktif[i].basladi;
      biten++; aktif.splice(i, 1);
    }
    if (sonraki >= gelis.length && !kuyruk.length && !aktif.length) break;
  }
  console.log(String(havuz).padStart(5) + " | " + String(biten).padStart(10) + " | " +
    (kuyrukToplam / biten).toFixed(1).padStart(8) + " | " +
    (islemToplam / biten).toFixed(1).padStart(7) + " | " +
    ((kuyrukToplam + islemToplam) / biten).toFixed(1).padStart(7) + " | " +
    String(asim).padStart(6));
}
EOF
yuk: 700 istek/sn, taban islem suresi 10 ms, 8 cekirdek, 21200 istek

havuz | tamamlanan | kuyrukta | islemde | toplam  | zaman
boyu  | istek      | (ort ms) | (ort ms)| (ort ms)| asimi
------|------------|----------|---------|---------|-------
    2 |       6040 |    197.4 |     9.8 |   207.2 |  15160
    4 |      12080 |    196.4 |     9.8 |   206.1 |   9120
    8 |      21200 |      3.9 |     9.8 |    13.6 |      0
   16 |      16116 |    192.9 |    29.7 |   222.6 |   5084
   32 |      13823 |    193.5 |    69.6 |   263.1 |   7377
   64 |      12919 |    190.9 |   149.2 |   340.1 |   8281

Tablo iki ayrı arıza biçimini aynı sütunda gösteriyor. İki ve dört boyutlu havuzlarda işlemde geçen süre en düşük değerdedir; sorun kuyruktadır. Havuzun geçirdiği iş, gelen işten azdır ve isteklerin çoğu zaman aşımına uğrar. Bu havuz doygunluğudur.

On altı ve üzeri boyutlarda kuyruk aynı derecede kötüdür, ama nedeni başkadır: işlemde geçen süre üç kattan on beş kata kadar büyümüştür. Havuz daha çok isteği içeri aldı, içeridekiler birbirini yavaşlattı ve geçen toplam iş azaldı. Havuzu büyütmek burada durumu kötüleştirdi.

Sekiz boyutlu havuz iki uçtan da uzaktır: bekleme dört milisaniye, toplam yanıt on üç milisaniye, zaman aşımı yok. Bu sayı çekirdek sayısıyla aynıdır ve rastlantı değildir — en iyi havuz boyutu, motorun gerçekten paralel yürütebildiği iş miktarına yakındır. Disk beklemesi baskın olan iş yüklerinde bu sayı çekirdek sayısının üzerine çıkar; işlemci baskın iş yüklerinde çekirdek sayısında kalır. Doğru değeri veren tek yol ölçmektir.

Havuz boyutu tek bir uygulama örneği için değil, sistem bütünü için düşünülür. Toplam bağlantı sayısı, havuz boyutu ile uygulama örneği sayısının çarpımıdır; on örnekli bir kurulumda örnek başına yirmi bağlantı, motora iki yüz bağlantı olarak gelir. Uygulama içindeki havuzların bu çarpımı görmemesi, ortada tek bir havuzlayıcı bulundurmanın başlıca gerekçesidir.

Son bir ayar, boşta bekleyen bağlantıların ömrüdür. Yük düştüğünde havuzun küçülmesi kaynak boşaltır; her seferinde yeniden açmak ise birinci tablodaki maliyeti geri getirir. İki uç arasındaki denge, yükün gün içindeki değişimine göre kurulur.

Özet

  • Bağlantı açmak, sorgunun kendisinden bağımsız bir hazırlık maliyeti taşır; ölçümde aynı sorgu, yeni bağlantı ve hazır deyim yolları arasında on dört kata yakın fark verdi.
  • Havuz, önceden açılmış bağlantıları ödünç vererek bu maliyeti ortadan kaldırır ve motora gelen bağlantı sayısını sınırlar.
  • Oturum düzeyinde havuzlama uygulamayı kısıtlamaz ama bağlantı sayısını azaltmaz; işlem düzeyinde havuzlama azaltır ve oturuma bağlı her şeyi güvenilmez kılar.
  • Havuzdan devralınan oturum değişkeni, satır düzeyi güvenlik politikasının başka bir hesabın satırlarını göstermesine yol açabilir; havuzun devir temizliği doğrulanmalıdır.
  • Havuz boyutu küçükse kuyruk büyür, büyükse eşzamanlı işlemler birbirini yavaşlatır; en iyi değer motorun paralel yürütebildiği iş miktarına yakındır ve ölçülerek bulunur.
  • Toplam bağlantı sayısı havuz boyutu ile uygulama örneği sayısının çarpımıdır.

Sonraki Adım

Bu dersteki her karar bir ölçüme dayanıyordu: bağlantı maliyeti, kuyruk süresi, eşzamanlı işlem sayısı, zaman aşımına uğrayan istek oranı. Bunların hiçbiri kendiliğinden görünmez; havuz doygunluğu, uygulama tarafında yalnız “veritabanı yavaş” olarak fark edilir ve yanlış yerde aranır. Sonraki ders izlemeyi ele alıyor: hangi büyüklüklerin toplanacağı, ortalamanın neden yanıltıcı olduğu ve yüzdelik dilimlerin ne gösterdiği, yavaş sorguların eşikle yakalanması, bekleme olaylarının darboğazı nasıl adlandırdığı ve bir uyarının ne zaman anlamlı olduğu.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat