Bir web uygulamasında saniyede yüzlerce sorgu çalışıyorsa asıl darboğaz çoğu zaman sorguların kendisi değil, o sorgular için sürekli açılıp kapatılan bağlantılardır. Her HTTP isteğinde yeni bir TCP oturumu kurmak, kimlik doğrulamasını yapmak, oturum değişkenlerini ayarlamak ve iş bitince bağlantıyı kapatmak veritabanı sunucusu tarafında gerçek bir maliyettir. Connection pool, yani bağlantı havuzu, tam olarak bu maliyeti ortadan kaldırmak için vardır: önceden açılmış bir grup bağlantıyı hazır tutar, uygulama işini bitirince bağlantıyı kapatmak yerine havuza geri bırakır ve bir sonraki istek aynı bağlantıyı devralır.
Bu rehberde bağlantı kurmanın neden pahalı olduğunu, havuzun içeride nasıl çalıştığını, havuz boyutunu hangi hesapla seçmeniz gerektiğini ve uygulama tarafında (PDO, HikariCP, SQLAlchemy, mysql2) ile sunucu tarafında (PgBouncer, ProxySQL) yapılandırmayı gerçek parametrelerle anlatacağım. Ayrıca havuzu izlemek için hangi sorguları çalıştıracağınızı ve "havuz koydum ama daha da yavaşladı" dedirten klasik hataları da göreceksiniz.
Bağlantı Kurmak Neden Pahalıdır#
Bir istemci MySQL'e bağlandığında sırasıyla şunlar olur: TCP el sıkışması tamamlanır, sunucu bir karşılama paketi gönderir, istemci kullanıcı adı ve parola karmasını yollar, sunucu yetki tablolarını kontrol eder, ardından oturum için karakter seti, saat dilimi, sql_mode gibi değişkenler ayarlanır. TLS kullanıyorsanız buna bir de sertifika doğrulaması ve anahtar değişimi eklenir. Tüm bu tur, aynı makinede bile milisaniyeler sürer; ağ üzerinden ve TLS ile birlikte onlarca milisaniyeye çıkabilir. Sorgunuz 2 ms sürüyorsa, 30 ms'lik bir bağlantı kurulumu maliyetin tamamını bağlantıya kaydırır.
İkinci ve daha ciddi maliyet bellek tarafındadır. PostgreSQL her bağlantı için ayrı bir işlem (process) çatallar; MySQL her bağlantı için bir iş parçacığı (thread) ayırır ve bu iş parçacığına sort_buffer_size, join_buffer_size, read_buffer_size gibi oturum bazlı tamponlar tahsis edilir. Yani 500 eşzamanlı bağlantı yalnızca 500 soket değil, yüzlerce megabayt oturum belleği demektir. Bağlantı sayısı kontrolsüz büyüdüğünde sunucu takas alanına düşer, sonra da Too many connections hatası gelir. Bu hatayı daha önce aldıysanız MySQL Too many connections hatası yazısı hatanın acil çözümünü ayrıca anlatıyor; burada kalıcı çözümü, yani havuzu kuruyoruz.
Connection Pool Nasıl Çalışır#
Havuz, uygulama ile veritabanı arasında duran bir "bağlantı deposu"dur. Uygulama açılırken havuz belirlenen sayıda bağlantıyı açar ve boşta bekletir. Bir istek geldiğinde kod new Connection() yerine pool.getConnection() çağırır; havuz boştaki bir bağlantıyı verir, iş bitince bağlantı fiziksel olarak kapanmaz, havuza geri döner ve "boşta" işaretlenir.
Akışı adım adım şöyle düşünün:
- Uygulama başlar, havuz
minimumIdlekadar bağlantıyı hemen açar. - İstek gelir, havuzdan boşta bir bağlantı istenir.
- Boşta bağlantı varsa anında verilir, yoksa havuz
maximumPoolSizesınırına kadar yeni bağlantı açar. - Sınır dolduysa istek
connectionTimeoutsüresince kuyrukta bekler; süre dolarsa hata döner. - İş biter, bağlantı havuza iade edilir; açık işlem varsa geri alınır (rollback) ve oturum değişkenleri sıfırlanır.
- Bağlantı
maxLifetimesüresini doldurduğunda havuz onu kapatıp yerine yenisini açar.
Buradaki en kritik davranış beşinci maddedir: iyi bir havuz, bağlantıyı iade ederken temizler. Aksi halde bir istekte açılan geçici tablo ya da değiştirilen autocommit ayarı bir sonraki isteğe sızar ve teşhisi çok zor hatalar üretir.
Havuz Boyutunu Hesaplamak#
En yaygın yanlış inanış, havuzu büyütmenin performansı artıracağıdır. Gerçek tam tersidir. Veritabanı sunucusu sınırlı sayıda CPU çekirdeğine ve sınırlı disk paralelliğine sahiptir; aynı anda çalışan sorgu sayısı bu sınırı aştığında iş parçacıkları birbirini bekler, bağlam değiştirme (context switch) maliyeti artar ve toplam verim düşer. 20 bağlantıyla saniyede 8.000 sorgu işleyen bir sunucu, 400 bağlantıyla 4.000 sorguya inebilir.
Pratikte iyi çalışan başlangıç formülü şudur:
havuz_boyutu = (CPU_çekirdek_sayısı * 2) + eşzamanlı_disk_sayısı
4 çekirdekli, tek NVMe diskli bir VDS için bu yaklaşık (4 * 2) + 1 = 9 eder; yani 10 civarı bir havuz mantıklı bir başlangıçtır. Sayı size küçük geliyorsa, havuzun kuyruk oluşturduğunu ve kuyruğun aslında sunucuyu koruduğunu hatırlayın. Uygulama sunucusu sayısıyla birlikte de düşünmeniz gerekir:
| Senaryo | Uygulama düğümü | Düğüm başına havuz | Toplam bağlantı | Notlar |
|---|---|---|---|---|
| Tek VDS, PHP-FPM | 1 | 10 | 10 | max_connections 151 fazlasıyla yeter |
| 3 uygulama sunucusu | 3 | 10 | 30 | Toplamı max_connections ile karşılaştırın |
| Kubernetes, 20 pod | 20 | 5 | 100 | Pod sayısı arttıkça havuzu küçültün |
| Sunucusuz (serverless) | Değişken | 1 | Öngörülemez | Mutlaka PgBouncer gibi ara katman kullanın |
Toplam bağlantı, veritabanının max_connections değerinin altında kalmalı ve üstüne bakım işleri için pay bırakmalısınız. MySQL'de varsayılan max_connections 151, PostgreSQL'de 100'dür; bunları yükseltmeden önce her bağlantının bellek maliyetini hesaplayın. Bellek ve tampon ayarlarının tümünü birlikte planlamak isterseniz MySQL ve MariaDB performans optimizasyonu rehberi bu tabloyu tamamlar.
Uygulama Tarafında Havuz Yapılandırması#
Java tarafında fiilî standart HikariCP'dir ve ayarları isimlendirmesiyle diğer havuzlara da örnek olur:
# HikariCP - application.properties
spring.datasource.hikari.maximum-pool-size=10
spring.datasource.hikari.minimum-idle=10
# Havuzdan bağlantı beklerken sabır sınırı (ms)
spring.datasource.hikari.connection-timeout=3000
# Bağlantının azami ömrü; DB tarafındaki wait_timeout'tan KISA olmalı
spring.datasource.hikari.max-lifetime=1500000
# Bağlantı sızıntısı şüphesinde uyarı üret (ms)
spring.datasource.hikari.leak-detection-threshold=20000
Python ve SQLAlchemy kullanıyorsanız karşılıkları şunlardır:
from sqlalchemy import create_engine
engine = create_engine(
"postgresql+psycopg2://uygulama:[email protected]:5432/magaza",
pool_size=10, # sürekli tutulan bağlantı
max_overflow=5, # ani yükte açılabilecek ek bağlantı
pool_timeout=3, # kuyrukta bekleme sınırı (saniye)
pool_recycle=1800, # 30 dk sonra bağlantıyı tazele
pool_pre_ping=True, # kullanmadan önce bağlantı canlı mı diye yokla
)
Node.js tarafında mysql2 havuzu doğrudan sürücünün içindedir:
const mysql = require("mysql2/promise");
const pool = mysql.createPool({
host: "185.12.34.56",
user: "uygulama",
database: "magaza",
waitForConnections: true, // havuz doluysa hata verme, kuyruğa al
connectionLimit: 10,
queueLimit: 0, // kuyruk uzunluğu sınırsız
enableKeepAlive: true,
});
PHP tarafında durum farklıdır ve burada bir uyarı gerekir: PDO'nun PDO::ATTR_PERSISTENT seçeneği gerçek bir havuz değildir. Yaptığı şey, PHP-FPM işçisinin (worker) bağlantıyı istek bitince kapatmayıp aynı işçiye tekrar vermesidir. Bu, bağlantı kurma maliyetini azaltır ama havuz boyutunu siz belirlemezsiniz; toplam bağlantı sayınız doğrudan PHP-FPM pm.max_children değerine eşitlenir. Yani PHP'de havuz boyutunu ayarlamanın yolu, PHP-FPM işçi sayısını ayarlamaktan geçer.
; /etc/php/8.3/fpm/pool.d/www.conf
pm = static
pm.max_children = 20 ; aynı anda en fazla 20 kalıcı DB bağlantısı demektir
Sunucu Tarafında Havuz: PgBouncer ve ProxySQL#
Uygulama düğümü sayısı arttıkça uygulama içi havuz yetmez: her düğüm kendi havuzunu tutar ve toplam bağlantı çarpılarak büyür. Bu noktada veritabanının önüne bir havuzlayıcı koymak gerekir. PostgreSQL için standart araç PgBouncer'dır; tek bir hafif işlemdir ve binlerce istemci bağlantısını onlarca gerçek sunucu bağlantısına indirger.
; /etc/pgbouncer/pgbouncer.ini
[databases]
magaza = host=127.0.0.1 port=5432 dbname=magaza
[pgbouncer]
listen_addr = 127.0.0.1
listen_port = 6432
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt
; en verimli mod: bağlantı, işlem (transaction) bitince serbest kalır
pool_mode = transaction
; PostgreSQL'e açılacak gerçek bağlantı sayısı
default_pool_size = 20
; istemcilerden kabul edilecek bağlantı sayısı
max_client_conn = 1000
server_idle_timeout = 600
pool_mode seçimi kritiktir ve üç değeri vardır:
| Mod | Bağlantı ne zaman serbest kalır | Uygunluk | Kısıt |
|---|---|---|---|
| session | İstemci bağlantıyı kapatınca | Eski uygulamalar | Havuzlamadan kazanç neredeyse yok |
| transaction | Her işlem bitince | Web uygulamaları | Oturum değişkeni ve prepared statement dikkat ister |
| statement | Her sorgudan sonra | Yalnız otomatik commit | Çok satırlı işlem yapılamaz |
Çoğu web uygulaması için doğru cevap transaction'dır; ancak bu modda oturum düzeyi ayarlara ve SET komutlarına güvenemezsiniz, çünkü bir sonraki sorgunuz başka bir fiziksel bağlantıya düşebilir. MySQL tarafında benzer görevi ProxySQL üstlenir; 6033 numaralı portundan sorguları alır, 6032'deki yönetim arayüzünden yapılandırılır ve havuzlamanın yanında sorgu yönlendirme ile okuma/yazma ayrımı da yapabilir.
Havuzu İzlemek ve Sorun Gidermek#
Havuzu kurmak yarısıdır; ikinci yarısı doğru çalıştığını ölçmektir. MySQL tarafında bakılacak ilk sayaçlar şunlardır:
-- Şu an açık bağlantı sayısı
SHOW GLOBAL STATUS LIKE 'Threads_connected';
-- Sunucu açıldığından beri görülen en yüksek eşzamanlı bağlantı
SHOW GLOBAL STATUS LIKE 'Max_used_connections';
-- Yarıda kesilen bağlantılar: havuz ile wait_timeout çakışıyor olabilir
SHOW GLOBAL STATUS LIKE 'Aborted_clients';
-- Anlık olarak kim ne yapıyor
SHOW PROCESSLIST;
PostgreSQL'de aynı bilgiyi pg_stat_activity verir:
-- Duruma göre bağlantı dağılımı
SELECT state, count(*) FROM pg_stat_activity GROUP BY state;
-- 5 dakikadan uzun süredir işlem içinde boşta bekleyen bağlantılar
SELECT pid, usename, state, now() - state_change AS sure, query
FROM pg_stat_activity
WHERE state = 'idle in transaction'
AND now() - state_change > interval '5 minutes';
idle in transaction satırları gördüğünüzde havuz değil kodunuz suçludur: bir yerde işlem açılmış ve commit veya rollback yapılmadan bırakılmıştır. Bu bağlantı havuza dönmez, kilitleri tutmaya devam eder ve zamanla havuzu tüketir. HikariCP'nin leak-detection-threshold ayarı tam olarak bu durumu yakalamak içindir.
Havuzun kaç bağlantı kullandığını sürekli izlemek isterseniz uygulamanızın metrik uçları genelde active, idle ve pending sayaçlarını yayımlar. pending sürekli sıfırdan büyükse havuz küçüktür ya da sorgularınız yavaştır; ikincisi çok daha sık karşılaşılan sebeptir ve çözümü havuzu büyütmek değil, yavaş sorguları bulup düzeltmektir.
Sık Yapılan Hatalar#
Havuzu büyüterek yavaşlığı çözmeye çalışmak. En yaygın refleks budur ve neredeyse her zaman durumu kötüleştirir. Havuz kuyruğu doluyorsa önce yavaş sorguya, eksik indekse ya da kilit çakışmasına bakın.
maxLifetime değerini veritabanının zaman aşımından uzun tutmak. MySQL varsayılan wait_timeout değeri 28800 saniyedir ama birçok yönetilen ortamda ya da araya giren güvenlik duvarında bu süre çok daha kısadır. Havuz, sunucunun çoktan kapattığı bir bağlantıyı canlı sanıp size verirse MySQL server has gone away hatası alırsınız. Kural basittir: havuzun bağlantı ömrü, sunucunun zaman aşımından en az bir dakika kısa olmalı.
Bağlantıyı geri bırakmayı unutmak. Havuzdan alınan bağlantı finally bloğunda ya da with yapısında mutlaka iade edilmelidir. Bir istisna (exception) yolunda iade atlanırsa bağlantı sonsuza kadar havuzun dışında kalır; buna bağlantı sızıntısı denir ve belirtisi, uygulamanın birkaç saat sonra donmasıdır.
Her istek için ayrı havuz oluşturmak. Havuz nesnesi uygulama ömrü boyunca tek olmalıdır. İstek başına createPool() çağıran kod havuzun tüm faydasını yok eder, üstüne bir de bağlantı sızdırır.
Transaction modunda oturum değişkenine güvenmek. PgBouncer'ı transaction modunda çalıştırıp SET search_path gibi oturum ayarları yapmak, ayarın rastgele kaybolmasına yol açar. Bu ayarları her sorguda tekrar göndermeniz ya da bağlantı dizesinde tanımlamanız gerekir.
Sıkça Sorulan Sorular#
Connection pool ile kalıcı bağlantı (persistent connection) aynı şey mi#
Aynı şey değildir. Kalıcı bağlantı, bir işçi işlemin bağlantısını istek sonunda kapatmayıp saklamasıdır; bağlantı sayısını siz yönetmezsiniz, işçi sayısı belirler. Gerçek havuz ise ayrı bir yönetici katmandır: boyut sınırı, kuyruk, sağlık kontrolü, ömür yönetimi ve temizleme mantığı içerir. PHP'de tipik olarak kalıcı bağlantı, Java veya Python'da gerçek havuz kullanılır.
Havuz boyutunu kaç yapmalıyım#
Çekirdek sayısının iki katı artı disk paralelliği kadar bir değerle başlayın; 4 çekirdekli bir sunucu için 10 civarı iyi bir başlangıçtır. Sonra kuyruk bekleme süresini ölçün: bekleme yoksa havuz zaten yeterlidir, büyütmenin faydası olmaz. Birden fazla uygulama sunucunuz varsa düğüm başına havuzu bölün, toplamın veritabanının max_connections değerini aşmadığından emin olun.
PgBouncer ücretsiz mi ve kurulumu ne kadar sürer#
PgBouncer açık kaynaklı ve ücretsizdir, çoğu dağıtımın depolarında hazır paket olarak bulunur. Kurulum tek bir apt install pgbouncer komutu ve pgbouncer.ini ile userlist.txt dosyalarının düzenlenmesinden ibarettir; deneyimli birinin işi yarım saatten kısa sürer. Asıl zaman, uygulamanın bağlantı dizesini 5432 yerine 6432 portuna çevirip transaction modunun yan etkilerini test etmeye gider.
Havuz kullanınca Too many connections hatası tamamen biter mi#
Havuz bu hatayı büyük ölçüde önler çünkü toplam bağlantıya bir tavan koyar. Ancak uygulama düğümü eklemeye devam eder ve her düğüme aynı havuz boyutunu verirseniz toplam yine sınırı aşabilir. Bu yüzden düğüm sayısı arttıkça ya havuzu küçültmeli ya da veritabanının önüne PgBouncer veya ProxySQL gibi merkezi bir havuzlayıcı koymalısınız.
Bağlantı sızıntısını nasıl kontrol ederim#
En hızlı yöntem veritabanı tarafından bakmaktır: PostgreSQL'de pg_stat_activity içinde uzun süredir idle in transaction durumunda kalan satırlar, MySQL'de SHOW PROCESSLIST çıktısında Sleep durumunda saatlerce duran iş parçacıkları sızıntıya işaret eder. Uygulama tarafında HikariCP'nin leakDetectionThreshold ayarı, belirlenen süreden uzun tutulan bağlantı için yığın izini loglar. Sızıntının kaynağı neredeyse her zaman istisna yolunda kapatılmayan bir bağlantıdır.
Paylaşımlı hostingte connection pool kurabilir miyim#
Paylaşımlı hostingte PgBouncer gibi bir servisi kendiniz çalıştıramazsınız, çünkü sunucu üzerinde kök yetkiniz yoktur. Bu ortamlarda pratikte elinizde kalan seçenek, PHP tarafında kalıcı bağlantıyı dikkatli kullanmak ve sorguları azaltmaktır. Kendi havuzlayıcınızı çalıştırmak istiyorsanız tam yetkili bir sanal sunucuya geçmeniz gerekir.
Havuz kurunca uygulamam neden daha yavaşladı#
Bu genellikle havuzun çok küçük olmasından değil, sorguların yavaş olmasından kaynaklanır: havuz sınırı, önceden gizli kalan yavaşlığı görünür bir kuyruğa çevirir. Önce kuyruk bekleme süresine ve yavaş sorgu kaydına bakın. İkinci sık sebep, maxLifetime veya pool_recycle ayarının veritabanının zaman aşımıyla çakışması ve havuzun sürekli ölü bağlantı tazelemesidir.
Kapanış#
Bağlantı havuzu, veritabanı performansında en yüksek getiriyi en az kodla veren düzenlemelerden biridir. Aklınızda kalması gereken dört alışkanlık şunlar: havuzu küçük tutun ve çekirdek sayısına göre hesaplayın, bağlantı ömrünü sunucunun zaman aşımından kısa ayarlayın, bağlantıyı her yolda iade eden kod yazın ve havuz doluyorsa önce yavaş sorguya bakın. Uygulama düğümü sayınız büyümeye başladığında da merkezi bir havuzlayıcıya geçmeyi geciktirmeyin.
Bu ayarları kendi sunucunuzda özgürce yapmak istiyorsanız tam root erişimi veren VDS ve sanal sunucu paketlerimiz PgBouncer, ProxySQL ya da özel PHP-FPM havuz yapılandırmaları için uygun bir zemin sunar. Kurulumu ve izlemeyi bizim üstlenmemizi tercih ederseniz sunucu yönetimi hizmetimiz veritabanı ayarlarını da kapsar; daha küçük projeler için ise ayarları hazır gelen web hosting paketleri çoğu zaman fazlasıyla yeterlidir.