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.inf gibi 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.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir