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 olan bir havuzda 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.