API İsteklerinde Timeout ve Retry Yönetimi
Prodüksiyonda bir API entegrasyonu kuruyorsunuz, her şey test ortamında mükemmel çalışıyor. Deployment sonrası üretim trafiği gelmeye başlayınca aniden timeout hataları, yarım kalan istekler ve cascading failure’lar baş gösteriyor. Bu senaryo her sysadmin’in kabusu ve maalesef oldukça yaygın. Timeout ve retry mekanizmalarını doğru kurgulamak, sistemin ayakta kalması ile çökmesi arasındaki farkı belirliyor.
Timeout Neden Bu Kadar Kritik?
Timeout olmayan bir sistem, cevap vermeyen bir servise sonsuza kadar bağlı kalır. Thread’ler bloke olur, connection pool dolar, kaynak tükenmesi domino etkisiyle tüm sistemi çökertir. Circuit breaker pattern bu yüzden icat edildi ama ondan önce temel timeout yapılandırmasını doğru yapmak gerekiyor.
Bir API isteğinde aslında birden fazla timeout tipi söz konusu:
- Connection Timeout: Sunucuya TCP bağlantısı kurulana kadar geçen süre
- Read Timeout: Bağlantı kurulduktan sonra yanıt almak için beklenen süre
- Write Timeout: İstek gönderilirken geçen süre
- Total/Overall Timeout: Tüm işlemin toplam süresi
Bu dört değeri birbirinden bağımsız düşünmek gerekiyor. Örneğin bir dosya upload servisi için write timeout’u yüksek tutmak mantıklıyken, connection timeout her zaman kısa olmalı.
Python ile Temel Timeout Yapılandırması
Python’daki requests kütüphanesi ile başlayalım. Çoğu geliştirici sadece tek bir timeout değeri giriyor, bu yanlış:
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
# Yanlış kullanım - sadece tek timeout
response = requests.get('https://api.example.com/data', timeout=30)
# Doğru kullanım - connection ve read timeout ayrı ayrı
response = requests.get(
'https://api.example.com/data',
timeout=(3.05, 27) # (connection_timeout, read_timeout)
)
# Session ile yeniden kullanılabilir yapılandırma
session = requests.Session()
retry_strategy = Retry(
total=3,
backoff_factor=1,
status_forcelist=[429, 500, 502, 503, 504],
allowed_methods=["HEAD", "GET", "POST"],
raise_on_status=False
)
adapter = HTTPAdapter(
max_retries=retry_strategy,
pool_connections=10,
pool_maxsize=20
)
session.mount("https://", adapter)
session.mount("http://", adapter)
try:
response = session.get(
'https://api.example.com/data',
timeout=(3.05, 27)
)
response.raise_for_status()
except requests.exceptions.ConnectTimeout:
print("Bağlantı timeout - sunucu erişilemiyor")
except requests.exceptions.ReadTimeout:
print("Read timeout - sunucu yanıt vermedi")
except requests.exceptions.RetryError as e:
print(f"Maksimum retry aşıldı: {e}")
Burada backoff_factor=1 değeri önemli. İlk retry hemen, ikincisi 2 saniye, üçüncüsü 4 saniye bekleyerek yapılır. Bu exponential backoff stratejisi, sunucuyu boğmamak için kritik.
Curl ile Timeout ve Retry
Bash scriptlerinde ve monitoring araçlarında curl kullanımı kaçınılmaz. Curl’ün timeout seçenekleri biraz kafa karıştırıcı olabiliyor:
#!/bin/bash
API_URL="https://api.example.com/health"
MAX_RETRIES=3
RETRY_DELAY=2
# --connect-timeout: TCP bağlantısı için maksimum süre (saniye)
# --max-time: Toplam işlem için maksimum süre (saniye)
# --retry: Otomatik retry sayısı
# --retry-delay: Retry'lar arası bekleme süresi
# --retry-max-time: Tüm retry döngüsü için maksimum süre
curl
--connect-timeout 5
--max-time 30
--retry 3
--retry-delay 2
--retry-max-time 90
--retry-all-errors
--silent
--show-error
--output /dev/null
--write-out "%{http_code}"
"$API_URL"
# Manuel retry döngüsü - daha fazla kontrol için
make_api_request() {
local url=$1
local attempt=1
local response_code
while [ $attempt -le $MAX_RETRIES ]; do
echo "Deneme $attempt / $MAX_RETRIES"
response_code=$(curl
--connect-timeout 5
--max-time 30
--silent
--output /dev/null
--write-out "%{http_code}"
"$url")
if [ "$response_code" -eq 200 ]; then
echo "Başarılı: HTTP $response_code"
return 0
elif [ "$response_code" -eq 429 ]; then
echo "Rate limit - daha uzun bekliyoruz"
sleep $((RETRY_DELAY * attempt * 3))
else
echo "Hata: HTTP $response_code"
sleep $((RETRY_DELAY * attempt))
fi
attempt=$((attempt + 1))
done
echo "Maksimum retry aşıldı"
return 1
}
make_api_request "$API_URL"
Node.js ile Axios Timeout Yönetimi
Node.js ortamlarında Axios yaygın kullanılıyor. Axios’ta interceptor kullanarak merkezi bir retry mekanizması kurmak mümkün:
const axios = require('axios');
const axiosRetry = require('axios-retry');
// Temel axios instance oluşturma
const apiClient = axios.create({
baseURL: 'https://api.example.com',
timeout: 10000, // 10 saniye global timeout
headers: {
'Content-Type': 'application/json',
'X-Request-ID': () => generateRequestId()
}
});
// axios-retry ile yapılandırma
axiosRetry(apiClient, {
retries: 3,
retryDelay: (retryCount) => {
// Exponential backoff + jitter
const base = Math.pow(2, retryCount) * 1000;
const jitter = Math.random() * 1000;
return base + jitter;
},
retryCondition: (error) => {
// Network hataları ve belirli HTTP kodlarında retry
return axiosRetry.isNetworkOrIdempotentRequestError(error) ||
error.response?.status === 429 ||
error.response?.status >= 500;
},
onRetry: (retryCount, error, requestConfig) => {
console.log(`Retry ${retryCount}: ${requestConfig.url} - ${error.message}`);
}
});
// Request interceptor - her istekte timeout header'ı ekle
apiClient.interceptors.request.use((config) => {
config.metadata = { startTime: Date.now() };
return config;
});
// Response interceptor - latency loglama
apiClient.interceptors.response.use(
(response) => {
const duration = Date.now() - response.config.metadata.startTime;
console.log(`${response.config.url} - ${duration}ms`);
return response;
},
(error) => {
if (error.code === 'ECONNABORTED') {
console.error('Timeout hatası:', error.config?.url);
}
return Promise.reject(error);
}
);
// Kullanım
async function fetchUserData(userId) {
try {
const response = await apiClient.get(`/users/${userId}`, {
timeout: 5000 // Bu endpoint için özel timeout
});
return response.data;
} catch (error) {
if (error.response) {
throw new Error(`API Hatası: ${error.response.status}`);
} else if (error.request) {
throw new Error('Sunucuya ulaşılamıyor');
}
throw error;
}
}
Jitter Neden Önemli?
Retry mekanizmasında thundering herd problemi gerçek bir tehlike. 100 client aynı anda başarısız olursa ve hepsi 2 saniye sonra aynı anda retry yaparsa, sunucuyu tekrar eziyorsunuz. Jitter bunu engelliyor:
import random
import time
import math
def calculate_backoff(attempt, base_delay=1, max_delay=60):
"""
Exponential backoff with full jitter
attempt: Kaçıncı deneme olduğu (0'dan başlar)
base_delay: Temel bekleme süresi (saniye)
max_delay: Maksimum bekleme süresi (saniye)
"""
# Pure exponential (thundering herd problemi yaratır)
# delay = base_delay * (2 ** attempt)
# Exponential backoff with full jitter (önerilen)
cap = min(max_delay, base_delay * (2 ** attempt))
delay = random.uniform(0, cap)
return delay
def calculate_backoff_equal_jitter(attempt, base_delay=1, max_delay=60):
"""
Equal jitter - daha tutarlı davranış için
"""
cap = min(max_delay, base_delay * (2 ** attempt))
v = cap / 2
return v + random.uniform(0, v)
# Test edelim
print("Full Jitter örnekleri:")
for i in range(5):
delays = [calculate_backoff(i) for _ in range(3)]
print(f"Deneme {i}: {[f'{d:.2f}s' for d in delays]}")
# Retry decorator implementasyonu
import functools
def retry_with_backoff(max_retries=3, base_delay=1, max_delay=60,
exceptions=(Exception,)):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
last_exception = None
for attempt in range(max_retries + 1):
try:
return func(*args, **kwargs)
except exceptions as e:
last_exception = e
if attempt == max_retries:
raise
delay = calculate_backoff(attempt, base_delay, max_delay)
print(f"Hata: {e}. {delay:.2f}s sonra tekrar denenecek...")
time.sleep(delay)
raise last_exception
return wrapper
return decorator
@retry_with_backoff(max_retries=3, base_delay=1, exceptions=(requests.RequestException,))
def call_payment_api(payload):
response = requests.post(
'https://payment.example.com/charge',
json=payload,
timeout=(3, 15)
)
response.raise_for_status()
return response.json()
Nginx Upstream Timeout Yapılandırması
API gateway veya reverse proxy katmanında da timeout’ları doğru ayarlamak gerekiyor. Nginx’te upstream servislere yapılan istekler için:
upstream api_backend {
server api1.internal:8080 weight=3 max_fails=3 fail_timeout=30s;
server api2.internal:8080 weight=2 max_fails=3 fail_timeout=30s;
server api3.internal:8080 backup;
keepalive 32;
keepalive_requests 100;
keepalive_timeout 60s;
}
server {
listen 443 ssl;
server_name api.example.com;
location /api/ {
proxy_pass http://api_backend;
# Bağlantı timeout'ları
proxy_connect_timeout 5s;
proxy_send_timeout 30s;
proxy_read_timeout 60s;
# Retry yapılandırması
# error: bağlantı hatalarında retry
# timeout: timeout durumunda retry
# invalid_header: geçersiz response'da retry
# http_500, http_502, http_503, http_504: bu kodlarda retry
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
proxy_next_upstream_tries 3;
proxy_next_upstream_timeout 10s;
# Buffer ayarları
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
# Timeout header'ı client'a ilet
add_header X-Upstream-Response-Time $upstream_response_time;
}
}
proxy_next_upstream_tries: Kaç farklı upstream sunucusu denenecek proxy_next_upstream_timeout: Tüm retry döngüsü için maksimum süre fail_timeout: Bir sunucu başarısız sayıldıktan sonra ne kadar süre devre dışı kalacak
Circuit Breaker Pattern
Retry mekanizması tek başına yeterli değil. Servis tamamen aşağıdaysa sürekli retry yapmak kaynakları tüketiyor. Circuit Breaker burada devreye giriyor:
import time
import threading
from enum import Enum
class CircuitState(Enum):
CLOSED = "closed" # Normal çalışma
OPEN = "open" # Devre açık, istekler reddediliyor
HALF_OPEN = "half_open" # Test aşaması
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.success_count = 0
self.last_failure_time = None
self._lock = threading.Lock()
def call(self, func, *args, **kwargs):
with self._lock:
if self.state == CircuitState.OPEN:
if time.time() - self.last_failure_time > self.recovery_timeout:
print("Circuit half-open durumuna geçiyor")
self.state = CircuitState.HALF_OPEN
self.success_count = 0
else:
raise Exception("Circuit breaker OPEN - istek reddedildi")
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:
if self.state == CircuitState.HALF_OPEN:
self.success_count += 1
if self.success_count >= self.half_open_max_calls:
print("Circuit CLOSED durumuna döndü")
self.state = CircuitState.CLOSED
self.failure_count = 0
elif self.state == CircuitState.CLOSED:
self.failure_count = 0
def _on_failure(self):
with self._lock:
self.failure_count += 1
self.last_failure_time = time.time()
if self.failure_count >= self.failure_threshold:
if self.state != CircuitState.OPEN:
print(f"Circuit OPEN durumuna geçti - {self.failure_count} hata")
self.state = CircuitState.OPEN
# Kullanım
payment_circuit = CircuitBreaker(
failure_threshold=5,
recovery_timeout=30,
half_open_max_calls=2
)
def make_payment_request(amount):
return payment_circuit.call(
call_payment_api,
{"amount": amount, "currency": "TRY"}
)
Gerçek Dünya Senaryosu: E-ticaret Ödeme Entegrasyonu
Bir e-ticaret platformunda ödeme API’si entegrasyonunu düşünelim. Bu senaryoda birden fazla katmanda timeout ve retry yönetimi gerekiyor:
import asyncio
import aiohttp
import logging
from dataclasses import dataclass
from typing import Optional
logger = logging.getLogger(__name__)
@dataclass
class PaymentConfig:
base_url: str
api_key: str
connect_timeout: float = 3.0
read_timeout: float = 15.0
max_retries: int = 2 # Ödeme işlemleri için düşük retry
idempotency_key_header: str = "X-Idempotency-Key"
class PaymentAPIClient:
def __init__(self, config: PaymentConfig):
self.config = config
self.circuit_breaker = CircuitBreaker(
failure_threshold=10,
recovery_timeout=60
)
async def charge(self, amount: int, currency: str,
idempotency_key: str) -> dict:
"""
Ödeme işlemi - idempotency key ile güvenli retry
Ödeme işlemlerinde dikkat: duplicate charge riski!
"""
timeout = aiohttp.ClientTimeout(
connect=self.config.connect_timeout,
total=self.config.read_timeout
)
headers = {
"Authorization": f"Bearer {self.config.api_key}",
self.config.idempotency_key_header: idempotency_key,
"Content-Type": "application/json"
}
payload = {
"amount": amount,
"currency": currency
}
last_error = None
for attempt in range(self.config.max_retries + 1):
try:
async with aiohttp.ClientSession(
timeout=timeout,
headers=headers
) as session:
async with session.post(
f"{self.config.base_url}/charges",
json=payload
) as response:
if response.status == 200:
return await response.json()
elif response.status == 402:
# Kart reddedildi - retry YAPMA
error_data = await response.json()
raise ValueError(f"Kart reddedildi: {error_data}")
elif response.status == 429:
# Rate limit - Retry-After header'ına bak
retry_after = int(response.headers.get('Retry-After', 60))
logger.warning(f"Rate limit, {retry_after}s bekleniyor")
await asyncio.sleep(retry_after)
elif response.status >= 500:
logger.error(f"Sunucu hatası: {response.status}")
if attempt < self.config.max_retries:
delay = calculate_backoff(attempt)
await asyncio.sleep(delay)
except asyncio.TimeoutError:
logger.error(f"Timeout (deneme {attempt + 1})")
last_error = TimeoutError("Ödeme servisi yanıt vermedi")
if attempt < self.config.max_retries:
await asyncio.sleep(calculate_backoff(attempt))
except aiohttp.ClientConnectionError as e:
logger.error(f"Bağlantı hatası: {e}")
last_error = e
break # Bağlantı hatalarında hemen dur
raise last_error or RuntimeError("Ödeme işlemi başarısız")
Bu senaryoda dikkat edilmesi gereken kritik nokta: ödeme gibi idempotent olmayan işlemlerde retry sayısını minimumda tutmak ve mutlaka idempotency key kullanmak. Duplicate charge, hem müşteri hem şirket için büyük sorun.
Monitoring ve Alerting
Timeout ve retry’ları izlemeden yönetmek kör uçmak gibi. Prometheus ile metrik toplama:
# Prometheus metrik örnekleri - uygulamanızdan expose edin
# API istek süreleri histogram olarak
api_request_duration_seconds_bucket{endpoint="/payment",le="0.5"} 245
api_request_duration_seconds_bucket{endpoint="/payment",le="1.0"} 287
api_request_duration_seconds_bucket{endpoint="/payment",le="5.0"} 299
# Timeout sayacı
api_timeout_total{endpoint="/payment",type="connect"} 3
api_timeout_total{endpoint="/payment",type="read"} 12
# Retry sayacı
api_retry_total{endpoint="/payment",attempt="1"} 45
api_retry_total{endpoint="/payment",attempt="2"} 12
api_retry_total{endpoint="/payment",attempt="3"} 4
# Circuit breaker durumu
circuit_breaker_state{service="payment"} 0 # 0=closed, 1=open, 2=half_open
# Grafana alert kuralı (PromQL)
# Retry oranı %10'u geçerse alert ver
sum(rate(api_retry_total[5m])) / sum(rate(api_request_total[5m])) > 0.1
Yaygın Hatalar ve Çözümleri
Prodüksiyonda sıkça görülen hatalar:
- Tüm hatalarda retry yapmak: 400 Bad Request, 401 Unauthorized, 402 Payment Required gibi client hatalarında retry yapmak anlamsız ve zararlı. Sadece 5xx ve network hatalarında retry yapın.
- Retry’da aynı isteği değiştirmeden göndermek: Timeout sebebiyle request ID veya timestamp içeren isteklerde sorun çıkabilir. Idempotency key kullanın.
- Sonsuz retry döngüsü: Mutlaka maksimum retry sayısı ve toplam timeout belirleyin.
max_retries=math.infgibi bir şey yazmak sistemi öldürür.
- Connection pool’u küçük tutmak: Yüksek trafikte yeni connection açmak pahalı. Pool boyutunu iş yüküne göre ayarlayın.
- Downstream’in timeout’unu hesaba katmamak: Eğer downstream servisin timeout’u 10 saniyeyse ve siz 30 saniye bekliyorsanız, timeout’larınız uyumsuz. Downstream timeout < upstream timeout olmalı.
- Read timeout’u çok kısa tutmak: Büyük response dönen endpoint’lerde read timeout too short olursa sürekli başarısız olursunuz. Endpoint bazlı timeout değerleri belirleyin.
Sonuç
API timeout ve retry yönetimi, “bir kez yap unut” değil sürekli izlenmesi ve ayarlanması gereken dinamik bir yapı. Başlangıç için şu adımları izleyin:
- Önce connection ve read timeout‘ları ayrı ayrı ayarlayın, asla tek bir değerle geçiştirmeyin.
- Exponential backoff with jitter kullanın, sabit gecikmeli retry thundering herd’e davet çıkarmaktır.
- Circuit breaker olmadan retry tek başına yetersiz. Devre kesici pattern’ı entegre edin.
- İdempotent olmayan işlemler için retry sayısını minimumda tutun ve idempotency key kullanın.
- Her şeyi metrik olarak izleyin. Timeout ve retry oranlarındaki artış, sistem sorunlarının erken uyarısıdır.
- Timeout değerlerini p99 latency bazında belirleyin, ortalama değerlere göre değil. Ortalamanın üç katı iyi bir başlangıç noktası.
Doğru yapılandırılmış bir timeout ve retry sistemi, kötü günlerde dahi uygulamanızın ayakta kalmasını sağlar. Zaten iyi sysadmin’liğin özü de bu: her şey yolundayken hazırlık yapmak.
