Circuit Breaker Pattern: Nginx ile Uygulama
Mikro servis mimarisine geçiş yaptıktan sonra karşılaşılan en sinir bozucu sorunlardan biri şu: bir backend servis çöktü, ama Nginx hâlâ ona istek yönlendirmeye devam ediyor. Kullanıcılar timeout alıyor, log dosyaları şişiyor, ve sen de gece yarısı pagerdan uyandırılıyorsun. İşte tam bu noktada Circuit Breaker pattern devreye giriyor.
Circuit Breaker, elektrik sigortasından esinlenilmiş bir tasarım deseni. Bir servis belirli sayıda hata verdikten sonra devre açılıyor, o servise giden istekler geçici olarak engelleniyor, bir süre sonra servis tekrar test ediliyor ve sağlıklıysa devre kapanıyor. Bu sayede hem downstream servisler gereksiz yükten kurtarılıyor hem de kullanıcılara anlamlı hata mesajları verilebiliyor.
Nginx bu pattern’i native olarak desteklemiyor, ama ngx_http_upstream_module ve biraz akıllıca konfigürasyon ile oldukça güçlü bir circuit breaker implementasyonu yapabilirsin. Daha gelişmiş seçenekler için ise Nginx Plus veya OpenResty’ye bakabiliriz.
Circuit Breaker Neden Önemli?
Diyelim ki şöyle bir senaryon var: Payment servisin 3 saniyelik timeout ile yanıt vermeye başladı. Anlık 500 concurrent kullanıcın var. Her istek 3 saniye boyunca bir worker thread tuttuğu için Nginx’in upstream connection pool’u doldu, diğer servisler de etkilenmeye başladı. Buna cascading failure deniyor ve production’da yaşanması gerçekten kötü.
Circuit Breaker bu problemi şu şekilde çözüyor:
- Hızlı başarısızlık (fail fast): Bozuk servise istek göndermek yerine hemen hata dönüyor
- Kaynak tasarrufu: Thread ve connection pool boşta beklemek yerine başka isteklere hizmet edebiliyor
- Otomatik iyileşme: Servis düzelince devre otomatik kapanıyor
- Gözlemlenebilirlik: Hangi servislerin sorunlu olduğu net görünüyor
Nginx ile Temel Implementasyon
Nginx’in ticari olmayan açık kaynak versiyonunda tam anlamıyla bir circuit breaker yok. Ama max_fails ve fail_timeout direktifleriyle pasif sağlık kontrolü yapabilirsin. Bu aslında circuit breaker’ın basit bir versiyonu.
upstream payment_service {
server 10.0.1.10:8080 max_fails=3 fail_timeout=30s;
server 10.0.1.11:8080 max_fails=3 fail_timeout=30s;
# Tüm sunucular down olursa son çare
server 10.0.1.99:8080 backup;
keepalive 32;
}
server {
listen 80;
server_name api.example.com;
location /payment/ {
proxy_pass http://payment_service;
proxy_connect_timeout 2s;
proxy_read_timeout 5s;
proxy_send_timeout 5s;
proxy_next_upstream error timeout http_500 http_502 http_503;
proxy_next_upstream_tries 2;
proxy_next_upstream_timeout 10s;
}
}
Burada ne oluyor:
- max_fails=3: 30 saniye içinde 3 başarısız istek gelirse sunucu “down” olarak işaretlenir
- fail_timeout=30s: Sunucu 30 saniye down kalır, sonra tekrar denenir
- proxy_next_upstream: Hata alınca bir sonraki upstream’e geç
- proxy_next_upstream_tries: Maksimum 2 upstream dene
Bu yaklaşımın bir sorunu var: passive health check, yani Nginx gerçekten istek göndermeden önce sunucunun sağlıklı olup olmadığını bilmiyor. Sunucu 30 saniye sonra tekrar “up” işaretleniyor ama gerçekten up olup olmadığından emin olamıyoruz.
Active Health Check ile Daha Güvenilir Circuit Breaker
Nginx Plus’ta health_check direktifi gelir ama açık kaynak versiyonda bunu kendin çözmek zorunda kalırsın. OpenResty kullanıyorsan lua-resty-upstream-healthcheck modülü işini çok kolaylaştırır.
Önce standart Nginx ile ne yapabileceğimize bakalım. Ayrı bir health check endpoint’i ile upstream durumunu yönetebilirsin:
# /etc/nginx/conf.d/upstream_health.conf
upstream order_service {
zone order_service_zone 64k;
server 10.0.2.10:9090 max_fails=5 fail_timeout=60s weight=3;
server 10.0.2.11:9090 max_fails=5 fail_timeout=60s weight=3;
server 10.0.2.12:9090 max_fails=5 fail_timeout=60s weight=1;
}
# Health check için internal endpoint
server {
listen 8080;
server_name _;
allow 127.0.0.1;
allow 10.0.0.0/8;
deny all;
location /upstream-status {
stub_status on;
}
# Manuel circuit breaker kontrolü
location /circuit-breaker/order {
internal;
proxy_pass http://order_service/health;
proxy_connect_timeout 1s;
proxy_read_timeout 2s;
}
}
Şimdi bir script ile bu health check’i otomatikleştirelim:
#!/bin/bash
# /usr/local/bin/nginx_circuit_breaker.sh
NGINX_BIN="/usr/sbin/nginx"
UPSTREAM_NAME="order_service"
HEALTH_ENDPOINT="http://localhost:8080/circuit-breaker/order"
STATE_FILE="/var/run/nginx_cb_${UPSTREAM_NAME}.state"
FAILURE_THRESHOLD=3
SUCCESS_THRESHOLD=2
CHECK_INTERVAL=10
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a /var/log/nginx_circuit_breaker.log
}
check_health() {
local response
response=$(curl -sf -o /dev/null -w "%{http_code}" --max-time 3 "${HEALTH_ENDPOINT}")
echo "${response}"
}
get_state() {
if [ -f "${STATE_FILE}" ]; then
cat "${STATE_FILE}"
else
echo "CLOSED"
fi
}
set_state() {
echo "$1" > "${STATE_FILE}"
log "Circuit state changed to: $1"
}
FAIL_COUNT=0
SUCCESS_COUNT=0
while true; do
CURRENT_STATE=$(get_state)
HTTP_CODE=$(check_health)
if [ "${HTTP_CODE}" = "200" ]; then
FAIL_COUNT=0
SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
if [ "${CURRENT_STATE}" = "HALF_OPEN" ] && [ "${SUCCESS_COUNT}" -ge "${SUCCESS_THRESHOLD}" ]; then
set_state "CLOSED"
SUCCESS_COUNT=0
log "Circuit CLOSED - service recovered"
fi
else
SUCCESS_COUNT=0
FAIL_COUNT=$((FAIL_COUNT + 1))
if [ "${FAIL_COUNT}" -ge "${FAILURE_THRESHOLD}" ] && [ "${CURRENT_STATE}" = "CLOSED" ]; then
set_state "OPEN"
log "Circuit OPENED - failure count: ${FAIL_COUNT}, HTTP code: ${HTTP_CODE}"
# Nginx'e durumu bildir (örn: maintenance moda geç)
$NGINX_BIN -s reload
fi
fi
# OPEN state'te belirli süre sonra HALF_OPEN'a geç
if [ "${CURRENT_STATE}" = "OPEN" ]; then
OPEN_TIME=$(stat -c %Y "${STATE_FILE}")
CURRENT_TIME=$(date +%s)
ELAPSED=$((CURRENT_TIME - OPEN_TIME))
if [ "${ELAPSED}" -ge 60 ]; then
set_state "HALF_OPEN"
SUCCESS_COUNT=0
fi
fi
sleep "${CHECK_INTERVAL}"
done
OpenResty ile Gelişmiş Circuit Breaker
Gerçek anlamda esnek bir circuit breaker istiyorsan OpenResty (Nginx + LuaJIT) kullanman gerekiyor. OpenResty, Nginx’in gücüne Lua programlama dilini ekliyor.
Önce OpenResty kurulumu:
# Ubuntu/Debian için
wget -qO - https://openresty.org/package/pubkey.gpg | sudo apt-key add -
echo "deb http://openresty.org/package/ubuntu $(lsb_release -sc) main"
| sudo tee /etc/apt/sources.list.d/openresty.list
sudo apt-get update
sudo apt-get install openresty
# CentOS/RHEL için
sudo yum install yum-utils
sudo yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
sudo yum install openresty
Şimdi asıl sihir burada. Lua ile circuit breaker implementasyonu:
# /usr/local/openresty/nginx/conf/lua/circuit_breaker.lua
local _M = {}
-- Shared memory zone'u kullan (nginx.conf'ta tanımlanmalı)
local circuit_states = ngx.shared.circuit_states
-- Circuit Breaker konfigürasyonu
local config = {
failure_threshold = 5, -- Kaç hatadan sonra açılsın
success_threshold = 2, -- Kaç başarıdan sonra kapansın
timeout = 60, -- OPEN state'te kaç saniye bekle
half_open_max_calls = 3, -- HALF_OPEN'da kaç istek dene
}
local STATE = {
CLOSED = "CLOSED",
OPEN = "OPEN",
HALF_OPEN = "HALF_OPEN"
}
function _M.get_state(service_name)
local state = circuit_states:get(service_name .. ":state")
if not state then
circuit_states:set(service_name .. ":state", STATE.CLOSED)
circuit_states:set(service_name .. ":failures", 0)
circuit_states:set(service_name .. ":successes", 0)
return STATE.CLOSED
end
return state
end
function _M.can_request(service_name)
local state = _M.get_state(service_name)
if state == STATE.CLOSED then
return true
elseif state == STATE.OPEN then
-- Timeout geçtiyse HALF_OPEN'a geç
local open_time = circuit_states:get(service_name .. ":open_time")
if open_time and (ngx.now() - open_time) >= config.timeout then
circuit_states:set(service_name .. ":state", STATE.HALF_OPEN)
circuit_states:set(service_name .. ":half_open_calls", 0)
ngx.log(ngx.WARN, "Circuit HALF_OPEN for service: " .. service_name)
return true
end
return false
elseif state == STATE.HALF_OPEN then
local calls = circuit_states:get(service_name .. ":half_open_calls") or 0
if calls < config.half_open_max_calls then
circuit_states:incr(service_name .. ":half_open_calls", 1, 0)
return true
end
return false
end
return false
end
function _M.record_success(service_name)
local state = _M.get_state(service_name)
if state == STATE.HALF_OPEN then
local successes = circuit_states:incr(service_name .. ":successes", 1, 0)
if successes >= config.success_threshold then
-- Devre kapat
circuit_states:set(service_name .. ":state", STATE.CLOSED)
circuit_states:set(service_name .. ":failures", 0)
circuit_states:set(service_name .. ":successes", 0)
ngx.log(ngx.WARN, "Circuit CLOSED for service: " .. service_name)
end
elseif state == STATE.CLOSED then
-- Başarılarda failure count'u sıfırla
circuit_states:set(service_name .. ":failures", 0)
end
end
function _M.record_failure(service_name)
local state = _M.get_state(service_name)
if state == STATE.HALF_OPEN then
-- HALF_OPEN'da hata olursa tekrar OPEN'a dön
circuit_states:set(service_name .. ":state", STATE.OPEN)
circuit_states:set(service_name .. ":open_time", ngx.now())
ngx.log(ngx.ERR, "Circuit re-OPENED for service: " .. service_name)
return
end
if state == STATE.CLOSED then
local failures = circuit_states:incr(service_name .. ":failures", 1, 0)
if failures >= config.failure_threshold then
circuit_states:set(service_name .. ":state", STATE.OPEN)
circuit_states:set(service_name .. ":open_time", ngx.now())
ngx.log(ngx.ERR, "Circuit OPENED for service: " .. service_name ..
" after " .. failures .. " failures")
end
end
end
return _M
Bu Lua modülünü Nginx konfigürasyonunda kullanmak için:
# /usr/local/openresty/nginx/conf/nginx.conf
worker_processes auto;
error_log logs/error.log warn;
events {
worker_connections 1024;
use epoll;
multi_accept on;
}
http {
# Shared memory zone - tüm worker process'ler paylaşır
lua_shared_dict circuit_states 10m;
# Lua modüllerinin yolu
lua_package_path "/usr/local/openresty/nginx/conf/lua/?.lua;;";
upstream payment_backend {
server 10.0.1.10:8080;
server 10.0.1.11:8080;
keepalive 32;
}
upstream order_backend {
server 10.0.2.10:9090;
server 10.0.2.11:9090;
keepalive 32;
}
server {
listen 80;
server_name api.example.com;
# Payment servisi
location /api/payment/ {
# İstek öncesi circuit breaker kontrolü
access_by_lua_block {
local cb = require "circuit_breaker"
local service = "payment"
if not cb.can_request(service) then
ngx.status = 503
ngx.header["X-Circuit-State"] = "OPEN"
ngx.header["Retry-After"] = "60"
ngx.say('{"error":"Service temporarily unavailable","circuit":"open"}')
return ngx.exit(503)
end
-- Service adını context'e kaydet
ngx.ctx.cb_service = service
}
proxy_pass http://payment_backend;
proxy_connect_timeout 2s;
proxy_read_timeout 10s;
# Yanıt sonrası circuit breaker güncelle
log_by_lua_block {
local cb = require "circuit_breaker"
local service = ngx.ctx.cb_service
if service then
local status = ngx.status
if status >= 500 or status == 0 then
cb.record_failure(service)
else
cb.record_success(service)
end
end
}
}
# Circuit breaker durumunu gösteren endpoint
location /internal/circuit-status {
allow 10.0.0.0/8;
deny all;
content_by_lua_block {
local circuit_states = ngx.shared.circuit_states
local services = {"payment", "order", "inventory"}
local result = {}
for _, svc in ipairs(services) do
local state = circuit_states:get(svc .. ":state") or "CLOSED"
local failures = circuit_states:get(svc .. ":failures") or 0
result[svc] = {state = state, failures = failures}
end
ngx.header["Content-Type"] = "application/json"
local json = require "cjson"
ngx.say(json.encode(result))
}
}
}
}
Fallback Mekanizması
Circuit breaker açıldığında kullanıcılara sadece hata vermek yerine bir fallback yanıt döndürebilirsin. Bu, gerçek dünya senaryolarında çok önemli.
# Statik fallback yanıtlar için ayrı bir location
server {
listen 80;
server_name api.example.com;
# Fallback yanıtlar için static dosyalar
location /fallback/ {
internal;
root /usr/local/nginx/fallback;
default_type application/json;
}
location /api/inventory/ {
access_by_lua_block {
local cb = require "circuit_breaker"
if not cb.can_request("inventory") then
-- Cache'den son bilinen değeri döndür
local cache = ngx.shared.response_cache
local cached = cache:get(ngx.var.request_uri)
if cached then
ngx.header["Content-Type"] = "application/json"
ngx.header["X-Served-From"] = "cache-fallback"
ngx.header["X-Circuit-State"] = "OPEN"
ngx.say(cached)
return ngx.exit(200)
end
-- Cache de yoksa graceful degradation
ngx.exec("/fallback/inventory_default.json")
return
end
ngx.ctx.cb_service = "inventory"
}
proxy_pass http://inventory_backend;
# Başarılı yanıtları cache'e al
body_filter_by_lua_block {
local cb_service = ngx.ctx.cb_service
if cb_service and ngx.status == 200 then
local cache = ngx.shared.response_cache
local body = ngx.arg[1]
if ngx.arg[2] then -- son chunk
cache:set(ngx.var.request_uri, ngx.ctx.response_body, 300)
else
ngx.ctx.response_body = (ngx.ctx.response_body or "") .. (body or "")
end
end
}
}
}
Fallback JSON dosyasını da oluştur:
# /usr/local/nginx/fallback/inventory_default.json
mkdir -p /usr/local/nginx/fallback
cat > /usr/local/nginx/fallback/inventory_default.json << 'EOF'
{
"status": "degraded",
"message": "Inventory service is temporarily unavailable",
"data": [],
"fallback": true,
"timestamp": null
}
EOF
Monitoring ve Alerting
Circuit breaker implementasyonu yaptın, güzel. Ama onu izlemezsen değeri yarıya düşer. Prometheus ve Grafana ile birleştirdiğinde çok işe yarıyor.
# Nginx log formatını circuit breaker state'ini içerecek şekilde ayarla
# /etc/nginx/nginx.conf
http {
log_format circuit_breaker_log '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'upstream="$upstream_addr" '
'upstream_status="$upstream_status" '
'upstream_rt="$upstream_response_time" '
'cb_state="$upstream_http_x_circuit_state"';
access_log /var/log/nginx/circuit_breaker.log circuit_breaker_log;
}
Basit bir monitoring scripti:
#!/bin/bash
# /usr/local/bin/cb_monitor.sh
# Bu script her dakika çalışır ve metrikleri toplar
CIRCUIT_STATUS_URL="http://localhost/internal/circuit-status"
SLACK_WEBHOOK="${SLACK_WEBHOOK_URL}"
ALERT_FILE="/var/run/cb_alerts"
get_circuit_status() {
curl -sf "${CIRCUIT_STATUS_URL}" 2>/dev/null
}
send_slack_alert() {
local service=$1
local state=$2
local message="*[Circuit Breaker Alert]* Service *${service}* circuit is *${state}*"
if [ -n "${SLACK_WEBHOOK}" ]; then
curl -s -X POST -H 'Content-type: application/json'
--data "{"text":"${message}"}"
"${SLACK_WEBHOOK}"
fi
logger -t circuit_breaker "Service ${service} circuit state: ${state}"
}
STATUS=$(get_circuit_status)
if [ -z "${STATUS}" ]; then
send_slack_alert "nginx" "STATUS_ENDPOINT_DOWN"
exit 1
fi
# Python ile JSON parse et
python3 << PYEOF
import json
import sys
import os
status = json.loads('${STATUS}')
alert_file = '/var/run/cb_alerts'
# Önceki alertleri yükle
previous = {}
if os.path.exists(alert_file):
with open(alert_file) as f:
try:
previous = json.load(f)
except:
previous = {}
current = {}
for service, info in status.items():
state = info.get('state', 'UNKNOWN')
current[service] = state
prev_state = previous.get(service, 'CLOSED')
if state != prev_state:
print(f"STATE_CHANGE:{service}:{state}")
if state == 'OPEN':
print(f"ALERT:CRITICAL:{service} circuit opened!")
elif state == 'HALF_OPEN':
print(f"ALERT:WARNING:{service} circuit in half-open state")
elif state == 'CLOSED' and prev_state in ('OPEN', 'HALF_OPEN'):
print(f"ALERT:INFO:{service} circuit closed - service recovered")
# Yeni durumu kaydet
with open(alert_file, 'w') as f:
json.dump(current, f)
PYEOF
# Cron'a ekle
# * * * * * /usr/local/bin/cb_monitor.sh >> /var/log/cb_monitor.log 2>&1
Gerçek Dünya Senaryosu: E-ticaret Platformu
Bir e-ticaret platformunda şu servislerin önünde Nginx var diyelim:
- Payment Service: En kritik, 99.99% uptime gerekli
- Inventory Service: Stok sorgulama, kısa süreli outage tolere edilebilir
- Recommendation Service: Nice-to-have, down olsa da satış devam eder
- Order Service: Kritik ama payment kadar değil
Her servis için farklı circuit breaker eşikleri mantıklı:
# Farklı servisler için farklı thresholdlar
# /usr/local/openresty/nginx/conf/lua/cb_config.lua
return {
payment = {
failure_threshold = 3, -- Çok kritik, az hatayla aç
success_threshold = 5, -- Çok kritik, çok başarı gerektir
timeout = 30, -- Hızlı recovery dene
half_open_max_calls = 2
},
inventory = {
failure_threshold = 10, -- Biraz toleranslı
success_threshold = 3,
timeout = 60,
half_open_max_calls = 5
},
recommendation = {
failure_threshold = 20, -- Çok toleranslı
success_threshold = 2,
timeout = 120,
half_open_max_calls = 10
},
order = {
failure_threshold = 5,
success_threshold = 3,
timeout = 45,
half_open_max_calls = 3
}
}
Bu konfigürasyonla recommendation servisi down olsa bile kullanıcı siparişini tamamlayabiliyor, circuit breaker hızla açılıp sistemi koruyor, ve servis düzelince otomatik devreye giriyor.
Yaygın Hatalar ve Çözümleri
Circuit breaker implementasyonunda dikkat etmen gereken birkaç konu var.
Timeout değerlerini yanlış ayarlamak: proxy_read_timeout çok uzunsa circuit breaker geç devreye giriyor. Servisinin normal yanıt süresini ölç, ona göre timeout belirle.
Shared memory boyutunu küçük tutmak: lua_shared_dict circuit_states 1m gibi küçük değerler yüzlerce servis için yetersiz kalıyor. 10m-50m arası makul.
Sadece 5xx hatalarına bakmak: Network timeout’ları da failure olarak sayılmalı. ngx.status == 0 durumunu kontrol etmeyi unutma.
Circuit breaker’ı load balancer’ın önüne koymak: Circuit breaker her upstream sunucu için ayrı ayrı çalışmalı, tüm upstream pool için tek bir devre olmamalı.
Sonuç
Circuit Breaker pattern, production ortamlarında cascading failure’ları önlemenin en etkili yollarından biri. Nginx’in native özellikleriyle basit bir implementasyon yapabilirsin, ama gerçek anlamda esnek ve özelleştirilebilir bir çözüm için OpenResty + Lua kombinasyonu çok daha güçlü.
Başlangıç olarak standart Nginx’in max_fails ve fail_timeout direktiflerini kullan, sistemi izle, pattern’i anla. Sonra ihtiyaç duyarsan OpenResty’ye geç. Her şeyden önce şunu unutma: Circuit breaker bir çözüm değil, bir koruma mekanizması. Asıl sorunları yine de düzeltmen gerekiyor, circuit breaker sadece o sürede sistemi ayakta tutuyor.
Monitoring olmayan bir circuit breaker kör uçmak gibi. Hangi servislerin ne kadar sıklıkla açıldığını, recovery sürelerini ve trend’leri mutlaka takip et. Bu veriler hem anlık müdahale için hem de uzun vadeli kapasite planlaması için altın değerinde.
