İçeriğe geç
academia.sh

Ders 05 / 21

Bağlantı Havuzu

Bağlantının pahalı kaynak olarak yönetilmesi: açma maliyetinin ölçülmesi, sabit boyutlu havuz ve kuyruk, havuz boyutu ile bekleme süresi ilişkisinin paralel isteklerle ölçülmesi, doygunlukta kuyruk zaman aşımı ve oturum durumunun taşınması.

İçindekiler

Önceki iki ders sorguyu tek bir noktadan üretilir hâle getirdi. Sorgunun çalışabilmesi için bir şey daha gerekir: bağlantı. Şimdiye kadar her betik kendi bağlantısını açtı, işini bitirdi ve süreç sona erdi.

Sürekli çalışan bir servis böyle davranamaz. Bağlantı açmak bedava değildir ve veritabanının kabul edeceği eşzamanlı bağlantı sayısı sınırlıdır. Bağlantı havuzu (connection pool), önceden açılmış bağlantıları saklayan ve isteklere ödünç veren yapıdır. Bu ders havuzu kurar, havuz boyutunu değiştirerek isteklerin kuyrukta ne kadar beklediğini ölçer ve havuzun getirdiği iki tuzağı gösterir.

Açma Maliyeti

İlk soru maliyetin gerçekten var olup olmadığıdır.

// acma-maliyeti.mjs — her istekte baglanti acmak ile tek baglantiyi yeniden kullanmak
import { DatabaseSync } from "node:sqlite";

const N = 2000;
const SORGU = "SELECT count(*) AS n FROM odunc WHERE iade_tarihi IS NULL";

let t = performance.now();
for (let i = 0; i < N; i++) {
  const db = new DatabaseSync("kutuphane.db");
  db.prepare(SORGU).get();
  db.close();
}
const acKapa = performance.now() - t;

const db = new DatabaseSync("kutuphane.db");
t = performance.now();
for (let i = 0; i < N; i++) db.prepare(SORGU).get();
const yenidenKullan = performance.now() - t;
db.close();

console.log(`her istekte ac-kapa : ${acKapa.toFixed(0)} ms  (${(acKapa / N).toFixed(3)} ms/istek)`);
console.log(`yeniden kullan      : ${yenidenKullan.toFixed(0)} ms  (${(yenidenKullan / N).toFixed(3)} ms/istek)`);
console.log(`oran                : ${(acKapa / yenidenKullan).toFixed(1)}x`);

Aşağıdaki komut şemayı kurar ve ölçümü çalıştırır.

rm -f kutuphane.db
sqlite3 kutuphane.db <<'SQL'
CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL,
                    uye_id INTEGER NOT NULL, alis_tarihi TEXT NOT NULL, iade_tarihi TEXT);
INSERT INTO odunc VALUES (1,1,1,'2025-01-10','2025-01-24'),(3,1,2,'2025-02-11',NULL),
  (7,5,4,'2025-04-21',NULL),(10,3,5,'2025-06-03',NULL),(12,4,4,'2025-06-20','2025-07-04');
SQL
node acma-maliyeti.mjs
her istekte ac-kapa : 113 ms  (0.057 ms/istek)
yeniden kullan      : 10 ms  (0.005 ms/istek)
oran                : 11.3x

Süreler donanıma ve dosya sistemine bağlıdır; başka bir makinede sayılar değişir. Anlamlı olan orandır: aynı işi yapmak için bağlantıyı her seferinde açmak, açık bir bağlantıyı yeniden kullanmaktan on bir kat pahalıya geldi. Bu ölçüm yerel bir dosya üzerindedir. Ağ üzerinden konuşulan bir veritabanında açma maliyeti ayrıca oturum kurulumu, kimlik doğrulama ve şifreleme el sıkışması içerir; oran büyür.

Havuz ve Kuyruk

Havuz iki durumu yönetir: boştaki bağlantıların listesi ve boş bağlantı bekleyenlerin kuyruğu. Boş bağlantı varsa istek anında karşılanır; yoksa kuyruğa girer.

// havuz.mjs — sabit boyutlu kaynak havuzu ve kuyrugu
export class Havuz {
  constructor(uret, boyut, kuyrukZamanAsimiMs) {
    this.bos = Array.from({ length: boyut }, (_, i) => uret(i));
    this.kuyruk = [];
    this.kuyrukZamanAsimiMs = kuyrukZamanAsimiMs;
    this.boyut = boyut;
  }
  al() {
    if (this.bos.length > 0) return Promise.resolve(this.bos.pop());
    return new Promise((coz, hata) => {
      const kayit = { coz, hata };
      kayit.sayac = setTimeout(() => {
        this.kuyruk.splice(this.kuyruk.indexOf(kayit), 1);
        hata(new Error("kuyruk zaman asimi"));
      }, this.kuyrukZamanAsimiMs);
      this.kuyruk.push(kayit);
    });
  }
  birak(baglanti) {
    const kayit = this.kuyruk.shift();
    if (kayit === undefined) { this.bos.push(baglanti); return; }
    clearTimeout(kayit.sayac);
    kayit.coz(baglanti);
  }
}

birak yönteminin davranışı dikkat ister: bırakılan bağlantı boş listeye geri konmadan önce kuyruğa bakılır. Kuyrukta bekleyen varsa bağlantı doğrudan ona devredilir. Bu, boş listeye koyup sonra yeniden almaktan hem daha hızlıdır hem de bekleyenlerin sırasını korur.

Havuzun bir bağlantıyı sonsuza kadar bekletmemesi gerekir. kuyrukZamanAsimiMs süresi dolduğunda bekleyen istek kuyruktan çıkarılır ve hata alır. Bu kuyruk zaman aşımıdır (queue timeout); yokluğunda yavaşlayan bir veritabanı, bütün isteklerin süresiz beklemesine ve uygulamanın tamamen durmasına yol açar.

Havuzlu Servis

Servis her istekte havuzdan bir bağlantı alır, sorguyu çalıştırır ve bağlantıyı finally bloğunda geri bırakır. finally zorunludur: hata durumunda bırakılmayan bağlantı havuzdan kalıcı olarak eksilir.

// sunucu.mjs — her istek havuzdan bir baglanti alir, sorguyu calistirir, geri birakir
import { createServer } from "node:http";
import { DatabaseSync } from "node:sqlite";
import { Havuz } from "./havuz.mjs";

const BOYUT = Number(process.env.HAVUZ_BOYUTU ?? 4);
const GIDIS_DONUS_MS = Number(process.env.GIDIS_DONUS_MS ?? 40);
const ZAMAN_ASIMI_MS = Number(process.env.ZAMAN_ASIMI_MS ?? 2000);

const havuz = new Havuz(() => new DatabaseSync("kutuphane.db"), BOYUT, ZAMAN_ASIMI_MS);
const olcumler = [];
const bekle = (ms) => new Promise((c) => setTimeout(c, ms));

const sunucu = createServer(async (istek, yanit) => {
  const yol = new URL(istek.url, "http://yerel").pathname;

  if (yol === "/kapat") { yanit.end("kapaniyor\n"); sunucu.close(); return; }

  if (yol === "/rapor") {
    const b = olcumler.map((o) => o.bekleme).sort((x, y) => x - y);
    const yuzdelik = (p) => (b.length === 0 ? 0 : +b[Math.min(b.length - 1, Math.floor(b.length * p))].toFixed(1));
    yanit.writeHead(200, { "content-type": "application/json" });
    yanit.end(JSON.stringify({
      havuz_boyutu: BOYUT, istek: olcumler.length,
      reddedilen: olcumler.filter((o) => o.reddedildi).length,
      bekleme_ortanca_ms: yuzdelik(0.5), bekleme_p95_ms: yuzdelik(0.95),
      bekleme_en_yuksek_ms: b.length === 0 ? 0 : +b[b.length - 1].toFixed(1),
    }));
    return;
  }

  const kuyrugaGiris = performance.now();
  let baglanti;
  try {
    baglanti = await havuz.al();
  } catch {
    olcumler.push({ bekleme: performance.now() - kuyrugaGiris, reddedildi: true });
    yanit.writeHead(503, { "content-type": "text/plain" });
    yanit.end("havuz dolu\n");
    return;
  }
  const bekleme = performance.now() - kuyrugaGiris;
  try {
    const satir = baglanti.prepare(
      "SELECT count(*) AS n FROM odunc WHERE iade_tarihi IS NULL").get();
    await bekle(GIDIS_DONUS_MS);          // veritabani gidis-donusunun modeli
    olcumler.push({ bekleme, reddedildi: false });
    yanit.writeHead(200, { "content-type": "text/plain" });
    yanit.end(`acik kayit: ${satir.n}  bekleme_ms: ${bekleme.toFixed(1)}\n`);
  } finally {
    havuz.birak(baglanti);
  }
});
sunucu.listen(8080, () => console.log(`havuz boyutu ${BOYUT}, port 8080`));

GIDIS_DONUS_MS süresi bir modeldir ve bu ders boyunca model olarak kalır. Buradaki veritabanı süreç içinde çalıştığı için gerçek bir ağ gidiş-dönüşü yoktur; ayrı bir sunucuda duran veritabanının cevap süresi bu bekleme ile temsil edilir. Ölçülen şey gidiş-dönüşün kendisi değil, bağlantının o süre boyunca meşgul olmasının kuyruğa etkisidir.

Havuz Boyutu ve Bekleme Süresi

Ölçüm düzeneği basittir: aynı anda yirmi dört istek gönder, havuz boyutunu değiştir, isteklerin kuyrukta ne kadar beklediğine bak.

# olc.sh — havuz boyutunu degistirerek 24 paralel istegin bekleme suresini olcer
for boyut in 1 2 4 8 24; do
  HAVUZ_BOYUTU=$boyut GIDIS_DONUS_MS=40 node sunucu.mjs >/dev/null &
  sunucu=$!
  sleep 0.6
  istekler=""
  baslangic=$(date +%s%N)
  for i in $(seq 24); do
    curl -s -o /dev/null http://localhost:8080/odunc &
    istekler="$istekler $!"
  done
  wait $istekler
  bitis=$(date +%s%N)
  printf 'toplam_ms=%s  ' $(( (bitis - baslangic) / 1000000 ))
  curl -s http://localhost:8080/rapor
  echo
  curl -s -o /dev/null http://localhost:8080/kapat
  wait $sunucu
done
sh olc.sh
toplam_ms=1035  {"havuz_boyutu":1,"istek":24,"reddedilen":0,"bekleme_ortanca_ms":493.8,"bekleme_p95_ms":904.2,"bekleme_en_yuksek_ms":945.1}
toplam_ms=533  {"havuz_boyutu":2,"istek":24,"reddedilen":0,"bekleme_ortanca_ms":241.7,"bekleme_p95_ms":444.5,"bekleme_en_yuksek_ms":445.1}
toplam_ms=284  {"havuz_boyutu":4,"istek":24,"reddedilen":0,"bekleme_ortanca_ms":115.3,"bekleme_p95_ms":196.3,"bekleme_en_yuksek_ms":196.7}
toplam_ms=161  {"havuz_boyutu":8,"istek":24,"reddedilen":0,"bekleme_ortanca_ms":37,"bekleme_p95_ms":76.5,"bekleme_en_yuksek_ms":76.5}
toplam_ms=83  {"havuz_boyutu":24,"istek":24,"reddedilen":0,"bekleme_ortanca_ms":0,"bekleme_p95_ms":0.1,"bekleme_en_yuksek_ms":0.3}

Mutlak süreler makineye bağlıdır; ilişki değildir. Havuz boyutu her ikiye katlandığında ortanca bekleme yarıya iniyor: 494, 242, 115, 37 milisaniye. Bunun nedeni doğrudan görülebilir. Yirmi dört istek, boyut bb olan bir havuzda 24/b\lceil 24/b \rceil turda işlenir; her tur bir hizmet süresi kadar sürer, dolayısıyla kuyruktaki ortalama bekleme tur sayısıyla orantılıdır.

Havuz istek sayısına eşitlendiğinde bekleme sıfırlanıyor. Bu, “havuzu büyütmek her zaman iyidir” sonucunu vermez. Beklemenin sıfırlanması, işin daha hızlı yapılması demek değildir; kuyruğun uygulamadan veritabanına taşınması demektir. Veritabanının kabul edebileceği eşzamanlı bağlantı sayısı ve işleyebileceği eşzamanlı sorgu sayısı sınırlıdır; havuz o sınırın üstüne çıkarsa bekleme uygulamada değil veritabanında birikir ve orada ölçmek zorlaşır.

İkinci nokta çarpımdır. Uygulama birden çok kopya hâlinde çalışıyorsa toplam bağlantı sayısı kopya sayısı ile havuz boyutunun çarpımıdır. Sekiz kopya ve yirmi bağlantılık havuz, veritabanından yüz altmış bağlantı ister.

Doygunluk

Havuzdaki bütün bağlantılar meşgulken ve kuyruk beklemesi zaman aşımını geçtiğinde havuz doygunluğa ulaşmıştır. Bu durumda istek reddedilir.

# doygunluk.sh — havuz kucuk, kuyruk zaman asimi kisa: reddedilen istekler
HAVUZ_BOYUTU=2 GIDIS_DONUS_MS=40 ZAMAN_ASIMI_MS=200 node sunucu.mjs >/dev/null &
sunucu=$!
sleep 0.6
istekler=""
for i in $(seq 24); do
  curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8080/odunc >> kodlar.txt &
  istekler="$istekler $!"
done
wait $istekler
echo "yanit kodu dagilimi:"
sort kodlar.txt | uniq -c
rm -f kodlar.txt
curl -s http://localhost:8080/rapor; echo
curl -s -o /dev/null http://localhost:8080/kapat
wait $sunucu
sh doygunluk.sh
yanit kodu dagilimi:
  12 200
  12 503
{"havuz_boyutu":2,"istek":24,"reddedilen":12,"bekleme_ortanca_ms":200.4,"bekleme_p95_ms":201.6,"bekleme_en_yuksek_ms":201.7}

Yirmi dört isteğin yarısı yanıtlandı, yarısı iki yüz milisaniyeyi bekledikten sonra 503 ile reddedildi. Kabul edilen ve reddedilen isteklerin oranı makinenin o andaki yüküne göre çalıştırmadan çalıştırmaya değişir; değişmeyen, reddedilen isteklerin beklemesinin zaman aşımı değerinin hemen üstünde toplanmasıdır. Bu, sınırın çalıştığının işaretidir.

Reddetmek başarısızlık gibi görünür, ama alternatifi daha kötüdür. Zaman aşımı olmasaydı bu on iki istek beklemeye devam eder, istemci tarafındaki zaman aşımları devreye girer, istemciler yeniden dener ve kuyruk daha da uzardı. Sınırlı bir kuyruk, hızlı ve okunabilir bir hata üretir.

Oturum Durumunun Taşınması

Havuzun ikinci tuzağı bağlantının kendisiyle ilgilidir. Bir bağlantı yalnız bir borudan ibaret değildir; üzerinde geçici tablolar, ayarlar ve açık bir işlem bulunabilir. Bu birikime oturum durumu (session state) denir ve bırakılan bağlantıyla birlikte sonraki alıcıya geçer.

// oturum-durumu.mjs — birakilan baglantidaki artik durum sonraki alana gecer
import { DatabaseSync } from "node:sqlite";
import { Havuz } from "./havuz.mjs";

const havuz = new Havuz(() => new DatabaseSync("kutuphane.db"), 1, 1000);

const birinci = await havuz.al();
birinci.exec("CREATE TEMP TABLE gecici_rapor (n INTEGER)");
birinci.exec("INSERT INTO gecici_rapor VALUES (42)");
havuz.birak(birinci);                       // temizlik yapilmadi

const ikinci = await havuz.al();
const kalinti = ikinci.prepare(
  "SELECT count(*) AS n FROM temp.sqlite_master WHERE name = 'gecici_rapor'").get().n;
console.log("ikinci alicinin gordugu artik tablo sayisi:", kalinti);
if (kalinti > 0) console.log("icindeki deger:", ikinci.prepare("SELECT n FROM gecici_rapor").get().n);

ikinci.exec("DROP TABLE IF EXISTS gecici_rapor");   // birakmadan once temizlik
havuz.birak(ikinci);
const ucuncu = await havuz.al();
console.log("temizlikten sonra artik tablo sayisi     :", ucuncu.prepare(
  "SELECT count(*) AS n FROM temp.sqlite_master WHERE name = 'gecici_rapor'").get().n);
node oturum-durumu.mjs
ikinci alicinin gordugu artik tablo sayisi: 1
icindeki deger: 42
temizlikten sonra artik tablo sayisi     : 0

İkinci alıcı, hiç oluşturmadığı bir tabloyu ve içindeki değeri gördü. Bu, iki isteğin birbirinin durumunu okuması demektir. Havuzun bırakma yolunda bir temizlik adımı olmak zorundadır: açık işlem geri alınır, geçici nesneler düşürülür, oturum ayarları varsayılana döner.

İlişkisel Veritabanı Yönetimi kursunda tanıtılan havuzlama düzeyleri bu soruna verilen farklı yanıtlardır. Oturum düzeyinde havuzlamada bağlantı istemciye oturum boyunca aittir ve oturum durumu korunur. İşlem düzeyinde havuzlamada bağlantı her işlem sonunda geri alınır; bu, hazır deyimler ve geçici tablolar gibi oturuma bağlı özelliklerin kullanılamaması pahasına havuz kullanımını yükseltir.

Özet

  • Bağlantıyı her istekte açmak, açık bir bağlantıyı yeniden kullanmaktan bu ölçümde on bir kat pahalıya geldi; havuzun varlık nedeni bu farktır.
  • Havuz iki yapı tutar: boştaki bağlantılar ve bekleyenlerin kuyruğu. Bırakılan bağlantı boş listeye dönmeden önce kuyruğa devredilir.
  • Yirmi dört paralel istekte havuz boyutu ikiye katlandıkça ortanca bekleme yarıya indi (494 → 242 → 115 → 37 ms); havuz istek sayısına eşitlendiğinde bekleme sıfırlandı.
  • Havuzu büyütmek beklemeyi yok etmez, kuyruğu veritabanına taşır; toplam bağlantı sayısı uygulama kopyası sayısıyla çarpılır.
  • Doygunlukta kuyruk zaman aşımı isteği hızla reddeder; sınırsız kuyruk yerine hızlı hata tercih edilir.
  • Bırakılan bağlantıdaki oturum durumu sonraki alıcıya geçer; bırakma yolunda temizlik zorunludur.

Sonraki Adım

Havuz kurulduğunda bütün bağlantılar aynı şemayı görür. Bu varsayım, şema değişene kadar doğrudur. Yeni bir sütun eklendiğinde, bir sütun kaldırıldığında ya da bir alan adı değiştiğinde uygulamanın çalışan kopyaları ile veritabanının durumu birbirinden ayrılır; eski kopya yeni şemayı, yeni kopya eski şemayı görebilir. Sonraki ders şema değişikliğini sürümlenmiş göç adımlarına böler, ileri ve geri yönde çalıştırır ve bir sütunun hizmet kesilmeden nasıl yeniden adlandırıldığını üç adımlı bir düzenle uygulayarak gösterir.

İ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