İçeriğe Atla
Mustafa Erbay
Teknoloji veritabani-derinlemesine · 15 dk okuma · görüntülenme Read in English

Veritabanı Connection Pool Doygunluğu ve Latency Geri Besleme Döngüsü

Connection pool tıkanınca oluşan kuyruk, timeout ve retry döngüsünü sinyallerle yakalayıp güvenli müdahale etmek için pratik çerçeve.

100%

Üretimde veritabanı incident’larının en sinsi olanı şudur: CPU düşük, veritabanı “ayakta”, ama uygulama donmuş gibi davranır. Kök neden çoğu zaman query’nin yavaş olması değil; connection pool’un doygunluğu ve bunun ürettiği kuyruklanma döngüsüdür.

Bu yazı; özellikle PostgreSQL + uygulama pool (Hikari/pgx/SQLAlchemy vb.) ve/veya PgBouncer kullanan yapılarda, pool doygunluğunu “teknik detay” değil operasyonel kontrol noktası olarak ele alır. Önce temeli kuralım — pool’un ne işe yaradığını ve neden bu kadar kritik olduğunu — sonra incident runbook’una geçeriz.

0) Temeller: connection pool ne çözer, neyi çözmez

Doygunluğu konuşmadan önce pool’un neden var olduğunu netleştirmek gerekir. Çünkü incident anında verilen kararların yarısı, “pool’un işi ne?” sorusunun yanlış cevaplanmasından doğar.

Her veritabanı bağlantısı kurmak pahalıdır: TCP handshake, kimlik doğrulama, yetkilendirme, sunucu tarafında bellek/CPU tahsisi. Saniyede yüzlerce-binlerce isteğin her biri için bağlantı açıp kapatmak, DB sunucusunu boğar ve yanıt sürelerini patlatır. Connection pool bu maliyetli adımları sıcak yoldan çıkarır: bağlantıları önceden açar, bir havuzda tutar, uygulama ödünç alır → kullanır → iade eder. Bağlantı fiziksel olarak kapanmaz, havuza geri döner ve bir sonraki istek için hazır bekler.

0.1 Rakamlarla etki: “daha hızlı” yetmez

Pool’un faydasını somut görmek, ileride pool boyutu kararlarını sakinleştirir. Bir bağlantı kurmak tipik olarak 20-100 ms sürer; saniyede 1000 istekte her istek için bağlantı açıp kapatırsanız, sadece bağlantı yönetimi için saniyede 20-100 saniyelik iş üretirsiniz — iş mantığına neredeyse zaman kalmaz. Pool’la bu maliyet milisaniyelerin altına iner.

Tipik kalıp şudur: operasyon ekranlarının her istek için ayrı bağlantı açtığı bir sistemde, yoğun saatlerde DB sunucusunda CPU tırmanır ve sorgu süreleri uzar. Pooling (ör. HikariCP) devreye alındığında aynı yük altında CPU yükü geriler, sorgu süreleri milisaniye bandına iner. Bu kalıp, yüksek eşzamanlılıkta yanıt veremeyen sistemlerde tekrarlanır: bağlantı yeniden kullanımı devreye girince, sabit bir pool boyutuyla çok daha fazla iş sorunsuz akabilir.

İşte tipik kazançlar:

  • Bağlantı kurma maliyeti sıfırlanır: sorgu başlatma süresi milisaniyeye iner.
  • Sunucu kaynağı verimli kullanılır: sürekli açıp-kapamak yerine yeniden kullanım, DB CPU/bellek yükünü düşürür.
  • Eşzamanlılık artar, tail latency stabilize olur: daha az kaynak israfı, daha öngörülebilir p95/p99.

0.2 Pooler seçim matrisi

Pool’u nerede çalıştıracağın, doygunluk davranışını doğrudan belirler:

Çözüm Katman Ne zaman Not
HikariCP Uygulama (JVM) Modern Java / Spring Boot varsayılanı Düşük gecikme, leakDetectionThreshold ile sızıntı tespiti
c3p0 Uygulama (JVM) Eski/legacy JVM yığınları Hikari kadar hızlı değil ama esnek ve olgun
Apache Commons DBCP Uygulama (JVM) Yaygın ama performansta Hikari’nin gerisinde Tomcat ekosisteminde sık
SQLAlchemy QueuePool / pg.Pool Uygulama (Python / Node) Dile gömülü, ORM’le gelir Aşağıda konfigürasyonu var
PgBouncer DB önünde (proxy) Çok sayıda uygulama/instance aynı DB’ye bağlanıyorsa DB’deki gerçek bağlantı sayısını tek elden yönetir

Pratikte tercih çoğu Java işinde HikariCP; ama birden çok uygulamanın aynı PostgreSQL’e bağlandığı senaryoda PgBouncer ön plana geçer — DB tarafındaki bağlantı sayısını yönetilebilir tutmanın en temiz yolu odur. İkisi birlikte de kullanılır: uygulama → HikariCP → PgBouncer → PostgreSQL.

0.3 Pooling ne zaman gereksiz olur

Pooling neredeyse her yerde standarttır, ama körü körüne değil:

  • Çok düşük trafikli işler: ayda bir çalışan rapor betiği, birkaç kullanıcılı iç araç. Havuz yönetiminin getirdiği ek karmaşıklık, sağladığı faydayı gölgeler.
  • Tekil, kısa ömürlü süreçler: açılıp tek sorgu yapıp kapanan CLI aracı. Bağlantıyı aç-kullan-kapat, havuz kurmaktan daha verimli.
  • Bazı serverless mimariler: fonksiyon her çağrıda yeni ortamda kalkabilir; klasik pooling’in verimi düşer (warm start’ta yeniden kullanım var, cold start’ta yok). Burada dışsal bir pooler (ör. PgBouncer / serverless-uyumlu proxy) genelde daha doğru cevaptır.

Bu istisnalar dışında, sürekli aktif ve yüksek istekli sistemlerde (web, mikroservis, API) pooling olmadan performans kabul edilemez seviyeye düşer. Kural basit: performans önemliyse, pool kullan.

1) Mental model: problem veritabanında değil, kuyruğun önünde başlar

Basit zincir:

  1. Trafik artar → daha çok istek DB’ye ulaşmak ister
  2. Pool dolar → thread/worker bekler
  3. Bekleyen istekler timeout olur → retry devreye girer
  4. Retry, DB’ye yeni baskı yaratır → latency daha da artar

Sonuç: latency geri besleme döngüsü (kuyruk büyürken sistem “daha çok deniyor”).

2) Triage: 10 dakikada pool doygun mu?

2.1 Uygulama metrikleri (en değerli sinyal)

Aradığın metrik sınıfları:

  • Active connections (kullanım)
  • Pending / wait queue (bekleyen)
  • Acquire time / wait time (bağlantı alma süresi)
  • Timeout count

Bu metrikler yoksa, incident anında ilk aksiyonlardan biri şudur: pool metriklerini standartlaştırmak.

2.2 PostgreSQL tarafı: oturum ve bekleme

-- aktif oturumların genel görünümü
select
  state,
  count(*) as sessions
from pg_stat_activity
group by 1
order by 2 desc;

Triage yorumu:

  • Çok sayıda active oturum: DB gerçekten çalışıyor olabilir ama doygun
  • Çok sayıda idle in transaction: uygulama transaction’ı açık bırakıyor (en pahalı hata)

Bekleme sınıflarını okumak için:

select
  wait_event_type,
  wait_event,
  count(*) as sessions
from pg_stat_activity
where wait_event is not null
group by 1,2
order by 3 desc;

2.3 PgBouncer varsa: “asıl kuyruk burada olabilir”

PgBouncer admin DB üzerinden:

show pools;
show stats;

Aradığın sinyaller:

  • Waiting client sayısı
  • Server connection sayısı (DB’ye giden gerçek bağlantı)

3) Pool parametre sözlüğü ve dile göre konfigürasyon

Triage’da gördüğün doygunluğun kalıcı cevabı, çoğu zaman birkaç parametrenin doğru ayarıdır. Kütüphaneler isimleri farklı koysa da temel knob’lar aynıdır:

  • maximumPoolSize — havuzun aynı anda tutabileceği maksimum aktif bağlantı. DB’nin max_connections değerinin %70-80’ini aşma; uygulamanın thread/işlem sayısı ve CPU çekirdeğiyle ilişkilendir. Çok yüksek → DB’yi döver; çok düşük → contention.
  • minimumIdle — havuzda her zaman hazır bekleyen minimum bağlantı. Ani yük artışında yeni bağlantı açma gecikmesini önler. Genelde maximumPoolSize’dan çok küçük ama steady-state ihtiyacı karşılayacak kadar.
  • connectionTimeout — havuzdan bağlantı almak için beklenecek maksimum süre (ms). Dolarsa hata fırlar. Bu, runbook’taki pool acquire timeout’un ta kendisidir.
  • idleTimeout — bir bağlantının boşta ne kadar kalıp tahliye edileceği. minimumIdle üzerindeki bağlantılar için geçerli; boşta kaynağı temizler.
  • maxLifetime — bir bağlantının havuzda kalabileceği en uzun süre; dolunca boşta olsa bile emekliye ayrılır. Uzun ömürlü bağlantı sorunlarını (sunucunun server-side kapatması, bellek) önler. idleTimeout’tan kısa olmamalı.
  • validationQuery / connectionTestQuery — bağlantının canlı olduğunu doğrulayan sorgu (SELECT 1). Tahsis öncesi veya periyodik çalışır; sunucunun sessizce öldürdüğü bağlantının dağıtılmasını engeller.

3.1 Java / Spring Boot — HikariCP

# DataSource
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
spring.datasource.username=user
spring.datasource.password=password
spring.datasource.driver-class-name=org.postgresql.Driver

# HikariCP
spring.datasource.hikari.maximumPoolSize=10
spring.datasource.hikari.minimumIdle=2
spring.datasource.hikari.connectionTimeout=30000
spring.datasource.hikari.idleTimeout=600000
spring.datasource.hikari.maxLifetime=1800000
spring.datasource.hikari.poolName=MyHikariPool
spring.datasource.hikari.validationTimeout=5000
spring.datasource.hikari.leakDetectionThreshold=30000

maximumPoolSize=10 ile zirvede en fazla 10 aktif bağlantı tutulur. Spring Data JPA gibi bir ORM kullanıyorsan havuz yönetimi otomatiktir; bağlantıyı elle alıp iade etmen gerekmez.

3.2 Python — SQLAlchemy

from sqlalchemy import create_engine, text

DATABASE_URL = "postgresql://user:password@localhost/mydb"

engine = create_engine(
    DATABASE_URL,
    pool_size=10,          # maximumPoolSize (varsayılan: 5)
    max_overflow=5,        # havuz dolunca geçici ek bağlantı
    pool_timeout=30,       # connectionTimeout (saniye)
    pool_recycle=1800,     # maxLifetime (saniye, 30 dk)
    pool_pre_ping=True,    # validationQuery eşdeğeri — bağlantı canlı mı?
)

def fetch_data():
    # 'with' bloğu bağlantıyı otomatik havuza iade eder
    with engine.connect() as connection:
        result = connection.execute(text("SELECT id, name FROM users LIMIT 5"))
        for row in result:
            print(f"ID: {row.id}, Name: {row.name}")

if __name__ == "__main__":
    fetch_data()
    print("Veri başarıyla çekildi.")

pool_size, HikariCP’deki maximumPoolSize’a karşılık gelir; max_overflow kısa yük artışlarında havuzu esnetir; pool_pre_ping=True her tahsisde küçük bir sorguyla bağlantının canlılığını doğrular.

3.3 Node.js — pg (node-postgres)

const { Pool } = require('pg');

const pool = new Pool({
    user: 'user',
    host: 'localhost',
    database: 'mydb',
    password: 'password',
    port: 5432,
    max: 10,                       // maximumPoolSize
    idleTimeoutMillis: 30000,      // idleTimeout (ms)
    connectionTimeoutMillis: 2000, // connectionTimeout (ms)
    statement_timeout: 5000,       // DB statement timeout (ms) — kuyruk hizalaması için kritik
});

async function getUsers() {
    let client;
    try {
        client = await pool.connect();           // havuzdan bağlantı al
        const res = await client.query('SELECT id, name FROM users LIMIT 5');
        console.log(res.rows);
    } catch (err) {
        console.error('Veritabanı hatası', err.stack);
    } finally {
        if (client) {
            client.release();                    // bağlantıyı havuza geri bırak
        }
    }
}

getUsers();

finally içindeki client.release(), hata durumunda bile bağlantının havuza dönmesini garanti eder — bu olmazsa sızıntı başlar (bkz. 5.3). statement_timeout’u açıkça verdim: runbook’taki “uygulama timeout < pool acquire < DB statement” hiyerarşisini kodda görünür kılmak için.

4) Hızlı mitigasyon: kuyruğu küçült, retry’ı kes

Hedef: DB’yi “daha güçlü” yapmak değil; önce sistemi stabil yapmak.

4.1 Trafiği kontrollü azalt (shed load)

Uygulanabilir seçenekler:

  • Rate limit / concurrency limit (gateway katmanında)
  • Cache ile DB read baskısını düşür (kısa süreli bile olsa)
  • İş kritik olmayan endpoint’leri degrade et

4.2 Retry budget uygula (kritik)

Incident sırasında:

  • Retry sayısını düşür (1-2)
  • Exponential backoff + jitter zorunlu
  • Idempotent olmayan işlemlerde retry’ı kapat

4.3 Timeout’ları “kuyruk”a göre hizala

Sık hata: uygulama timeout’u 30s, DB statement timeout yok.

Pratik kural:

  • Uygulama request timeout < pool acquire timeout < DB statement timeout

Örnek (yaklaşım):

  • Request: 10s
  • Pool acquire: 3s
  • Statement: 2s (kritik sorgular için daha kısa)

4.4 Backpressure: akışı kaynağında yönet

Timeout bir isteğin ne kadar bekleyeceğini belirler; backpressure ise gelen istek akışını kaynağında kısarak havuzun hiç doygunlaşmamasını hedefler. Üretici (uygulama), tüketicinin (havuz) hızını aşıyorsa yavaşlamalıdır. Üç temel strateji:

  • Kuyruklama (queueing): Bağlantı taleplerini sınırlı bir kuyrukta beklet; havuz boşaldıkça sıradakine ver. Geçici sıçramaları yutar — ama kuyruk sınırsızsa veya bekleme şişerse yine timeout üretir. Kuyruğu bounded tut.
  • Kısma (throttling): Birim zamanda yapılabilecek DB isteği sayısını sınırla (rate limit). Aşırı yükte genel istikrarı korur; bazı istekleri geciktirir/reddeder.
  • Yük atma (load shedding): Sistem doygunken daha az kritik istekleri bilerek reddet. Son çare — en kritik işlevleri ayakta tutmak için.

5) Kalıcı tasarım: pool boyutu bir “kapasite kontratı”dır

5.1 Pool’u büyütmek çoğu zaman çözüm değildir

Pool’u büyütmek, DB’ye daha fazla concurrency iter. Eğer DB’nin CPU/IO kapasitesi sabitse:

  • Latency artar
  • Lock contention artar
  • Tail latency patlar

Sonuçta “daha çok connection” yerine “daha az concurrency ama daha stabil throughput” daha iyi olur.

5.2 Transaction sınırını netleştir

En sık kök neden:

  • Uygulama, transaction’ı gereksiz uzun tutar
  • IO/HTTP çağrısı transaction içinde kalır
  • “idle in transaction” birikir

Operasyonel kontrol:

  • Transaction içinde network çağrısı yasak
  • ORM lazy-load sürprizleri gözlenebilir olmalı

5.3 Bağlantı sızıntısını kökten kes

Doygunluğun en sinsi sebebi yük değil, sızıntıdır: alınan bir bağlantı havuza iade edilmezse, “kullanımda” olarak kilitli kalır ve havuz yavaşça tükenir. Çözüm dile gömülü kaynak yönetimidir — bağlantıyı kapatmayı “unutma” ihtimalini ortadan kaldır:

  • Java: try-with-resources — blok bitince bağlantı/statement/result set otomatik kapanıp havuza döner.
  • Python: with statement (yukarıdaki SQLAlchemy örneğindeki gibi).
  • Node.js: finally içinde client.release() — istisnasız.
  • Tespit: HikariCP’de leakDetectionThreshold (ör. 30000 ms) iade edilmeyen bağlantıyı stack trace ile loglar.
// Java'da try-with-resources ile güvenli bağlantı kullanımı
try (Connection conn = dataSource.getConnection();
     PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE id = ?")) {
    stmt.setLong(1, userId);
    try (ResultSet rs = stmt.executeQuery()) {
        while (rs.next()) {
            // sonuçları işle
            System.out.println(rs.getString("name"));
        }
    }
} catch (SQLException e) {
    // hata yönetimi: logla, gerekirse yukarı fırlat
    log.error("Sorgu başarısız", e);
}
// conn, stmt ve rs otomatik kapanır ve bağlantı havuza döner

5.4 PgBouncer mod seçimi

PgBouncer kullanıyorsan:

  • transaction mode: en verimli, çoğu web iş yükü için iyi
  • session mode: bazı özellikler için gerekli ama kapasiteyi daha hızlı tüketir

Yanlış seçim, “pool var ama hiçbir şey akmıyor” incident’ına dönüşebilir.

6) Runbook kapanış: doğrulama ve iz

Stabilite doğrulaması:

  • Pool wait queue düşüyor mu?
  • Timeout sayısı düşüyor mu?
  • DB wait event’leri normalleşiyor mu?

Postmortem için delil:

  • Peak concurrency (app + pgbouncer + db)
  • En pahalı sorgu sınıfları (p95/p99)
  • Retry davranışı (hangi katman retry etti?)

Connection pool, uygulamanın DB’ye verdiği sözleşmedir. Bu sözleşme görünür değilse incident’lar “DB yavaş” diye başlar, ama asıl sorun kuyruk yönetimidir. Sağlam sistemler, kuyruğu saklamaz; ölçer, sınırlar ve runbook’a bağlar.

Paylaş:

Bu yazı faydalı oldu mu?

Yükleniyor...

Bu yazı nasıldı?

Sıkça Sorulanlar

Bu makale ile ilgili okurların sorduğu yaygın sorular.

Uygulamamda connection pooling'i nasıl etkinleştiririm ve hangi kütüphaneleri tercih etmeliyim?
Ben genellikle Node.js projelerimde `pg-pool` (PostgreSQL) ya da `mysql2`'nin dahili havuzunu kullanıyorum. İlk adım olarak veri tabanı konfigürasyon dosyasına `max`, `idleTimeoutMillis` ve `connectionTimeoutMillis` gibi parametreleri ekliyorum. Örneğin, `new Pool({ connectionString: process.env.DATABASE_URL, max: 20, idleTimeoutMillis: 30000 })` şeklinde bir nesne oluşturuyorum. Kütüphane, uygulama başlatıldığında otomatik olarak bir ön havuz oluşturur ve istek geldiğinde mevcut bir bağlantıyı verir. Bu sayede her istek için yeni TCP handshake yapmazsınız. Eğer ORM kullanıyorsanız, TypeORM ya da Sequelize gibi araçların kendi pool ayarlarını da aynı şekilde yapılandırabilirsiniz.
Connection pool büyüklüğünü artırmanın avantajları ve dezavantajları nelerdir? Çok büyük bir havuzun sorun yaratabileceği durumlar var mı?
Deneyimlerime göre, pool'u 10‑20 bağlantıya çıkarmak, yüksek trafikli API'lerde yanıt süresini belirgin biçimde düşürür çünkü bekleme süresi azalır. Ancak havuzu çok büyütmek (örneğin 200+ bağlantı) veri tabanının kaynaklarını zorlayabilir; CPU ve bellek tüketimi artar, hatta veri tabanı sunucusu yeni bağlantıları reddedebilir. Ayrıca, çok büyük bir havuz, bağlantıların uzun süre idle kalmasına ve zaman aşımına uğramasına yol açar. Ben genellikle uygulamanın ortalama eşzamanlı istek sayısının 2‑3 katı kadar bir `max` değeri seçiyorum ve ardından izleme araçlarıyla CPU, RAM ve query latency'yi kontrol ediyorum.
Pool'dan bir bağlantı alıp kullanırken sık karşılaştığım hatalar nelerdir ve bu hataları nasıl teşhis edip çözebilirim?
En sık gördüğüm hata `Error: timeout exceeded` mesajıdır; bu, havuzda uygun bir boş bağlantı bulunamadığında ortaya çıkar. Ben önce `max` ve `idleTimeoutMillis` değerlerini artırarak deniyorum. Bir diğer yaygın sorun `Connection terminated unexpectedly` hatasıdır; genellikle veri tabanı sunucusunun yeniden başlatılması ya da ağ kesintisi nedeniyle oluşur. Bu durumda, `pool.on('error', handler)` ile hatayı loglayıp, otomatik yeniden bağlanma mantığını ekliyorum. Ayrıca, bağlantıyı kullandıktan sonra `release()` çağrısını unutmak havuzun tükenmesine sebep olur; bu yüzden `try…finally` blokları içinde `client.release()` koyuyorum.
Connection pooling'in performans kazancı her ortamda aynı mı? Özellikle düşük trafikli uygulamalarda pool kullanmak gereksiz mi?
Ben düşük trafikli bir mikroservis üzerinde test ettiğimde, pool kullanmanın getirdiği overhead (hafıza ve nesne oluşturma) çok az bir fark yaratıyor; hatta bazen tek bir bağlantı açıp kapatmak daha basit olabiliyor. Ancak, uygulama büyüdükçe ve istek sayısı artınca pool'un faydası belirginleşiyor. Özetle, düşük trafikli bir ortamda `max: 2` gibi minimal bir havuz yeterli olur ve konfigürasyon maliyetini düşük tutar. Ancak ölçeklenebilirlik hedefliyorsanız, başlangıçta küçük bir pool tanımlayıp izleme ile dinamik olarak artırmak, gelecekteki performans sorunlarını önceden engeller.
ME

Mustafa Erbay

Sistem Mimarisi · Network Uzmanı · Altyapı, Güvenlik ve Yazılım

2006'dan bu yana sistem mimarisi, network, sunucu altyapıları, büyük yapıların kurulumu, yazılım ve sistem güvenliği ekseninde çalışıyorum. Bu blogda sahada karşılığı olan teknik deneyimlerimi paylaşıyorum.

Kişisel Notlar

Bu notlar sadece sizde saklanır. Tarayıcınızda yerel olarak tutulur.

Hazır 0 karakter

Yorumlar

Sunucu Taraflı AI Moderasyon

Yorumlar sunucuda yapay zeka ile denetlenir ve kalıcı olarak saklanır.

?
0/2000

Sunucu taraflı AI denetim

✉️ Ücretsiz · Spam yok · İstediğin an çık

Yeni yazılardan haberdar olun

Yeni içerikler ve teknik notlar e-postanıza gelsin.

  • 📌
    Haftanın en iyisi Sadece okumaya değer tek yazı
  • 🔧
    Alet çantası Bu hafta kullandığım araçlar
  • 🧠
    Perde arkası Blog'a girmeyen notlar

Spam yapmıyoruz. İstediğiniz zaman ayrılabilirsiniz. · Sadece Umami (self-hosted, Google yok) ile takip.

Okuma İstatistikleriniz

0

Yazı Okundu

0dk

Okuma Süresi

0

Gün Serisi

-

Favori Kategori

İlgili Yazılar