API Bağlantı Havuzu: Connection Pooling ile Performans Optimizasyonu
Üretim ortamında her saniye binlerce API isteği işleyen bir sistem yönetiyorsunuz ve aniden response time’larınız tavan yapıyor. Logları inceliyorsunuz, CPU ve RAM normal, network de sorun değil. Derinden bakınca sorunun her istek için yeni bir TCP bağlantısı açılıp kapatılmasından kaynaklandığını görüyorsunuz. İşte bu klasik senaryo, connection pooling’in neden bu kadar kritik olduğunu anlatıyor.
Connection Pooling Nedir ve Neden Önemlidir
API geliştirme dünyasında en sık yapılan hatalardan biri, her HTTP isteği için yeni bir bağlantı kurulması. TCP handshake, TLS negotiation, DNS çözümleme… Bunların her biri ciddi gecikme demek. Düşük trafikte fark edilmez ama yük arttığında sistem çöküşe geçer.
Connection pooling, önceden belirli sayıda bağlantı açarak bunları bir havuzda tutma ve gelen isteklere bu hazır bağlantıları tahsis etme tekniğidir. İstek tamamlandığında bağlantı kapatılmaz, havuza geri döner ve bir sonraki istek tarafından kullanılır.
Bir bağlantı kurmanın maliyetine bakalım:
- DNS çözümleme: 20-120ms
- TCP 3-way handshake: 1 RTT (round trip time)
- TLS 1.2 handshake: 2 RTT
- TLS 1.3 handshake: 1 RTT (0-RTT ile daha da az)
- HTTP request/response: asıl iş bu
Saniyede 1000 istek gelen bir sistemde her seferinde bu maliyeti ödemek, gereksiz yük oluşturur ve latency’yi ciddi artırır.
Temel Kavramlar
Pool Size ve Workers İlişkisi
Connection pool boyutunu belirlerken uygulamanızın concurrency modelini anlamanız gerekir. Yanlış yapılandırılmış bir pool, yokluğundan daha zararlı olabilir.
- min_connections: Havuzda her zaman hazır bekleyen minimum bağlantı sayısı
- max_connections: Havuzun açabileceği maksimum bağlantı sayısı
- idle_timeout: Boşta kalan bağlantının kapatılma süresi
- connection_timeout: Yeni bağlantı açarken beklenecek maksimum süre
- max_lifetime: Bir bağlantının havuzda kalabileceği maksimum ömür
Genel kural olarak: pool_size = (core_count * 2) + effective_spindle_count formülü iyi bir başlangıç noktasıdır. Ama bu kesin değil, yük testleriyle fine-tune edilmesi gerekir.
Keep-Alive vs Connection Pooling Farkı
HTTP Keep-Alive, aynı bağlantı üzerinden birden fazla istek göndermeyi sağlar ama uygulama katmanında bağlantı yönetimi yoktur. Connection pooling ise uygulama seviyesinde aktif bağlantı yönetimi demektir: bağlantı durumları takip edilir, sağlık kontrolleri yapılır, maksimum kullanım limitleri uygulanır.
Python ile Connection Pooling
Python ekosisteminde requests kütüphanesi çok popüler ama her istek için session kullanmadan çağrı yapmak büyük hata. httpx ve aiohttp ise async ortamlar için güçlü alternatifler.
requests.Session ile Temel Pooling
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
def create_session(
pool_connections=10,
pool_maxsize=20,
max_retries=3,
backoff_factor=0.5
):
session = requests.Session()
retry_strategy = Retry(
total=max_retries,
backoff_factor=backoff_factor,
status_forcelist=[429, 500, 502, 503, 504],
allowed_methods=["HEAD", "GET", "POST", "PUT", "DELETE"]
)
adapter = HTTPAdapter(
pool_connections=pool_connections, # farklı host'lar için pool sayisi
pool_maxsize=pool_maxsize, # her pool'daki max baglanti
max_retries=retry_strategy,
pool_block=False # pool dolu olunca exception firlatir, beklemez
)
session.mount("https://", adapter)
session.mount("http://", adapter)
# varsayilan timeout ayarla
session.request = lambda method, url, **kwargs:
requests.Session.request(session, method, url,
timeout=kwargs.pop('timeout', (5, 30)),
**kwargs)
return session
# kullanim
api_session = create_session(pool_connections=5, pool_maxsize=15)
response = api_session.get("https://api.ornekservis.com/v1/users")
httpx ile Async Connection Pool
import asyncio
import httpx
from contextlib import asynccontextmanager
# uygulama omru boyunca yasayan tek bir client instance
_client = None
@asynccontextmanager
async def get_http_client():
global _client
if _client is None:
limits = httpx.Limits(
max_keepalive_connections=20,
max_connections=50,
keepalive_expiry=30.0 # saniye
)
timeout = httpx.Timeout(
connect=5.0,
read=30.0,
write=10.0,
pool=5.0 # pool'dan baglanti almak icin bekleme suresi
)
_client = httpx.AsyncClient(
limits=limits,
timeout=timeout,
http2=True # HTTP/2 aktif, daha verimli multiplexing
)
try:
yield _client
except Exception as e:
raise e
async def fetch_user_data(user_id: int):
async with get_http_client() as client:
response = await client.get(
f"https://api.ornekservis.com/v1/users/{user_id}"
)
response.raise_for_status()
return response.json()
async def bulk_fetch(user_ids: list):
async with get_http_client() as client:
tasks = [
client.get(f"https://api.ornekservis.com/v1/users/{uid}")
for uid in user_ids
]
responses = await asyncio.gather(*tasks, return_exceptions=True)
results = []
for resp in responses:
if isinstance(resp, Exception):
results.append({"error": str(resp)})
else:
results.append(resp.json())
return results
Node.js ile Connection Pooling
Node.js’in event loop mimarisi connection pooling için ideal bir zemin sunar. axios ve node-fetch kullanırken dikkat edilmesi gereken bazı noktalar var.
axios ile Global Agent Konfigürasyonu
const axios = require('axios');
const http = require('http');
const https = require('https');
// varsayilan agent'lari override et
const httpAgent = new http.Agent({
keepAlive: true,
keepAliveMsecs: 30000, // 30 saniye keep-alive
maxSockets: 50, // host basina max socket
maxFreeSockets: 20, // bosta bekleyen max socket
timeout: 60000 // socket timeout
});
const httpsAgent = new https.Agent({
keepAlive: true,
keepAliveMsecs: 30000,
maxSockets: 50,
maxFreeSockets: 20,
timeout: 60000,
rejectUnauthorized: true // SSL dogrulama acik kalsin
});
const apiClient = axios.create({
baseURL: 'https://api.ornekservis.com/v1',
httpAgent,
httpsAgent,
timeout: 30000,
headers: {
'Content-Type': 'application/json',
'Accept-Encoding': 'gzip, deflate, br'
}
});
// interceptor ile merkezi hata yonetimi
apiClient.interceptors.response.use(
response => response,
async error => {
const { config, response } = error;
if (!config || !config.retryCount) {
config.retryCount = 0;
}
const retryableStatuses = [429, 500, 502, 503, 504];
if (
config.retryCount < 3 &&
response &&
retryableStatuses.includes(response.status)
) {
config.retryCount++;
const delay = Math.pow(2, config.retryCount) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
return apiClient(config);
}
return Promise.reject(error);
}
);
module.exports = apiClient;
Go ile Connection Pooling
Go’nun standart kütüphanesi connection pooling için oldukça güçlü araçlar sunar. http.Transport yapısı doğrudan havuz konfigürasyonuna izin verir.
package main
import (
"context"
"fmt"
"net"
"net/http"
"time"
)
func createOptimizedClient() *http.Client {
dialer := &net.Dialer{
Timeout: 5 * time.Second, // baglanti kurma timeout
KeepAlive: 30 * time.Second, // TCP keep-alive interval
}
transport := &http.Transport{
DialContext: dialer.DialContext,
MaxIdleConns: 100, // toplam max idle baglanti
MaxIdleConnsPerHost: 20, // host basina max idle
MaxConnsPerHost: 50, // host basina max toplam
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
DisableCompression: false,
ForceAttemptHTTP2: true,
}
return &http.Client{
Transport: transport,
Timeout: 30 * time.Second,
}
}
// singleton pattern ile global client
var globalClient = createOptimizedClient()
func fetchWithPool(ctx context.Context, url string) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("request olusturulamadi: %w", err)
}
req.Header.Set("Accept-Encoding", "gzip")
resp, err := globalClient.Do(req)
if err != nil {
return nil, fmt.Errorf("istek basarisiz: %w", err)
}
defer resp.Body.Close()
// response body okuma...
return nil, nil
}
Gerçek Dünya Senaryosu: Mikro Servis Mimarisinde Pool Yönetimi
Bir e-ticaret platformunda çalıştığınızı düşünün. Order servisi, payment servisi ve inventory servisiyle konuşuyor. Her servise ayrı pool konfigürasyonu gerekiyor çünkü kritiklik seviyeleri ve response time beklentileri farklı.
import httpx
from dataclasses import dataclass
from typing import Dict
@dataclass
class ServiceConfig:
base_url: str
max_connections: int
max_keepalive: int
timeout_connect: float
timeout_read: float
# servis bazinda farkli pool konfigurasyonlari
SERVICE_CONFIGS: Dict[str, ServiceConfig] = {
"payment": ServiceConfig(
base_url="https://payment.internal:8443",
max_connections=10, # kritik servis, kontrollü erisim
max_keepalive=5,
timeout_connect=2.0,
timeout_read=15.0 # odeme islemi uzun surebilir
),
"inventory": ServiceConfig(
base_url="https://inventory.internal:8080",
max_connections=30, # yuksek frekansta sorgulanir
max_keepalive=15,
timeout_connect=1.0,
timeout_read=5.0
),
"notification": ServiceConfig(
base_url="https://notification.internal:8080",
max_connections=50, # fire-and-forget, yuksek hacim
max_keepalive=25,
timeout_connect=1.0,
timeout_read=3.0
)
}
class ServiceClientPool:
def __init__(self):
self._clients: Dict[str, httpx.AsyncClient] = {}
async def get_client(self, service_name: str) -> httpx.AsyncClient:
if service_name not in self._clients:
config = SERVICE_CONFIGS[service_name]
limits = httpx.Limits(
max_connections=config.max_connections,
max_keepalive_connections=config.max_keepalive
)
timeout = httpx.Timeout(
connect=config.timeout_connect,
read=config.timeout_read
)
self._clients[service_name] = httpx.AsyncClient(
base_url=config.base_url,
limits=limits,
timeout=timeout,
verify="/etc/ssl/certs/internal-ca.pem"
)
return self._clients[service_name]
async def close_all(self):
for client in self._clients.values():
await client.aclose()
self._clients.clear()
# uygulama startup/shutdown ile entegrasyon (FastAPI ornegi)
from fastapi import FastAPI
app = FastAPI()
client_pool = ServiceClientPool()
@app.on_event("startup")
async def startup():
# client'lari onceden olustur, ilk istek gecikmesi olmasin
for service in SERVICE_CONFIGS:
await client_pool.get_client(service)
@app.on_event("shutdown")
async def shutdown():
await client_pool.close_all()
Pool Sağlığını İzleme ve Metrikler
Connection pool’u yapılandırmak yetmez, izlemeniz de gerekir. Prometheus ile entegrasyon şart.
from prometheus_client import Gauge, Counter, Histogram
import threading
# prometheus metrikleri tanimla
pool_active_connections = Gauge(
'http_pool_active_connections',
'Havuzda aktif olarak kullanimda olan baglanti sayisi',
['service', 'host']
)
pool_idle_connections = Gauge(
'http_pool_idle_connections',
'Havuzda bosta bekleyen baglanti sayisi',
['service', 'host']
)
pool_wait_duration = Histogram(
'http_pool_wait_seconds',
'Pool'dan baglanti almak icin beklenen sure',
['service'],
buckets=[0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1.0, 5.0]
)
connection_errors_total = Counter(
'http_connection_errors_total',
'Baglanti hatalarinin toplam sayisi',
['service', 'error_type']
)
class MonitoredHTTPAdapter(HTTPAdapter):
"""requests adapter'ini izleme ozellikleriyle genislet"""
def __init__(self, service_name: str, *args, **kwargs):
self.service_name = service_name
super().__init__(*args, **kwargs)
def send(self, request, *args, **kwargs):
import time
start = time.time()
try:
response = super().send(request, *args, **kwargs)
return response
except Exception as e:
error_type = type(e).__name__
connection_errors_total.labels(
service=self.service_name,
error_type=error_type
).inc()
raise
finally:
# pool istatistiklerini guncelle
self._update_pool_metrics()
def _update_pool_metrics(self):
"""urllib3 pool manager'dan istatistikleri cek"""
try:
for host, pool in self.poolmanager.connection_pool_kw.items():
pass # gercek implementasyonda pool.pool.qsize() kullanilir
except Exception:
pass
Yaygın Hatalar ve Çözümleri
Connection Leak Problemi
En tehlikeli sorunlardan biri. Response body okunmadan bağlantı havuza dönmez.
import requests
# YANLIS - response body okunmuyor, baglanti leak oluyor
def hatali_kullanim(session):
response = session.get("https://api.example.com/data")
if response.status_code == 200:
return True # body okunmadi!
# DOGRU - context manager kullan
def dogru_kullanim(session):
with session.get("https://api.example.com/data", stream=True) as response:
response.raise_for_status()
return response.json()
# DOGRU - streaming icin iter_content kullan
def streaming_kullanim(session):
with session.get("https://api.example.com/largefile", stream=True) as response:
response.raise_for_status()
chunks = []
for chunk in response.iter_content(chunk_size=8192):
if chunk:
chunks.append(chunk)
return b"".join(chunks)
Pool Tükenmesi ve Circuit Breaker
Pool tamamen dolduğunda ne olacak? Circuit breaker pattern bu durumu yönetir.
import time
from enum import Enum
from threading import Lock
class CircuitState(Enum):
CLOSED = "closed" # normal calisma
OPEN = "open" # servis kapali, istek gecme
HALF_OPEN = "half_open" # test asamasi
class CircuitBreaker:
def __init__(
self,
failure_threshold=5,
recovery_timeout=60,
half_open_max_calls=3
):
self.failure_threshold = failure_threshold
self.recovery_timeout = recovery_timeout
self.half_open_max_calls = half_open_max_calls
self.state = CircuitState.CLOSED
self.failure_count = 0
self.last_failure_time = None
self.half_open_calls = 0
self._lock = Lock()
def call(self, func, *args, **kwargs):
with self._lock:
if self.state == CircuitState.OPEN:
elapsed = time.time() - self.last_failure_time
if elapsed > self.recovery_timeout:
self.state = CircuitState.HALF_OPEN
self.half_open_calls = 0
else:
raise Exception(
f"Circuit breaker OPEN durumda. "
f"{self.recovery_timeout - elapsed:.1f}s sonra tekrar dene."
)
if self.state == CircuitState.HALF_OPEN:
if self.half_open_calls >= self.half_open_max_calls:
raise Exception("Half-open limit asildi")
self.half_open_calls += 1
try:
result = func(*args, **kwargs)
self._on_success()
return result
except Exception as e:
self._on_failure()
raise
def _on_success(self):
with self._lock:
self.failure_count = 0
self.state = CircuitState.CLOSED
def _on_failure(self):
with self._lock:
self.failure_count += 1
self.last_failure_time = time.time()
if self.failure_count >= self.failure_threshold:
self.state = CircuitState.OPEN
Nginx ve HAProxy ile Upstream Pool Yönetimi
Uygulama katmanı dışında, reverse proxy seviyesinde de pool yönetimi kritik.
# nginx upstream keepalive konfigurasyonu
upstream backend_api {
server api1.internal:8080 weight=3 max_fails=3 fail_timeout=30s;
server api2.internal:8080 weight=3 max_fails=3 fail_timeout=30s;
server api3.internal:8080 weight=2 max_fails=3 fail_timeout=30s;
# her upstream worker'i icin keepalive baglanti sayisi
keepalive 32;
# baglanti canliligi icin bekleme suresi
keepalive_timeout 65s;
# her baglanti uzerinden kac istek gecsin
keepalive_requests 1000;
}
server {
listen 443 ssl http2;
server_name api.orneksirket.com;
location /v1/ {
proxy_pass http://backend_api;
# HTTP/1.1 zorunlu, keepalive icin
proxy_http_version 1.1;
proxy_set_header Connection ""; # connection header'i temizle
proxy_connect_timeout 5s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
}
}
Performans Testleri ile Pool Boyutunu Belirlemek
Teorik hesaplamalar yetmez, gerçek yük testleri yapmanız şart.
#!/bin/bash
# wrk ile farkli pool boyutlarini test et
# parametreler
TARGET_URL="https://api.orneksirket.com/v1/health"
DURATION="30s"
THREADS=4
echo "=== Connection Pool Performans Testi ==="
echo "Tarih: $(date)"
echo "Hedef: $TARGET_URL"
echo ""
# farkli concurrent baglanti sayilariyla test
for CONNECTIONS in 10 25 50 100 200; do
echo "--- Concurrent: $CONNECTIONS baglanti ---"
# wrk ile test calistir
wrk -t$THREADS -c$CONNECTIONS -d$DURATION
--latency
--timeout 10s
-H "Authorization: Bearer test-token"
$TARGET_URL
echo ""
sleep 5 # servisin toparlanmasi icin bekle
done
# sonuclari analiz et
echo "Optimum pool boyutunu belirlemek icin:"
echo "- En dusuk p99 latency degeri hangi connection sayisinda?"
echo "- Requests/sec hangi noktadan sonra plato yapıyor?"
echo "- Hata oranı hangi noktada yükselmeye başlıyor?"
Sonuç
Connection pooling, yüksek trafikli API sistemlerinde temel bir optimizasyon değil zorunluluktur. Doğru yapılandırılmış bir pool, latency’yi dramatik biçimde düşürür, sunucu kaynaklarını korur ve sistemin yük altında kararlı kalmasını sağlar.
Özetleyecek olursak:
- Her zaman session veya client nesnelerini yeniden kullanın. Her istek için yeni nesne oluşturmak birincil hata kaynağıdır.
- Pool boyutunu iş yüküne göre ölçün. Core count formülü başlangıç noktası, gerçek ölçüm hedef.
- Timeout değerlerini servis SLA’larına göre ayarlayın. Ödeme servisi ile bildirim servisi aynı timeout’u hak etmiyor.
- Circuit breaker ile pool tükenmesine karşı savunma katmanı ekleyin. Havuz dolduğunda sistem cascade failure’a gitmemeli.
- Prometheus ile pool metriklerini izleyin. Aktif bağlantı sayısı, bekleme süresi ve hata oranı temel göstergeniz olsun.
- Nginx gibi reverse proxy’lerde upstream keepalive’ı unutmayın. Uygulama katmanında ne kadar optimize ederseniz edin, proxy katmanı düzgün ayarlanmamışsa kazanç yarıda kalır.
Bu teknikleri uyguladıktan sonra yük testleri yapın, metrikleri karşılaştırın ve ortamınıza özel optimal değerleri bulun. Connection pooling tek seferlik yapılandırma değil, sistemin büyümesiyle birlikte sürekli gözden geçirilmesi gereken bir konudur.
