Nginx ile Load Balancing: Yük Dengeleme Stratejileri
Üretim ortamında tek bir sunucuya güvenmek, bir noktada sizi çok kötü bir gece geçirtecektir. Trafik artışı, donanım arızası veya yazılım güncellemesi sırasında yaşanan kesintiler, load balancing’in neden vazgeçilmez olduğunu acı bir şekilde öğretir. Nginx, bu iş için hem hafif hem de son derece güçlü bir araç. Yıllarca Apache ile çalıştıktan sonra Nginx’e geçtiğimde, konfigürasyon sadeliği ve performansı gerçekten şaşırttı beni. Bu yazıda Nginx’in sunduğu yük dengeleme stratejilerini, gerçek dünya senaryolarıyla birlikte ele alacağız.
Temel Kavramlar ve Nginx’in Mimarisi
Nginx, event-driven mimarisiyle çalışır. Apache’nin her bağlantı için yeni bir thread açmasının aksine, Nginx az sayıda worker process ile binlerce eş zamanlı bağlantıyı yönetebilir. Bu yapı, onu hem reverse proxy hem de load balancer olarak kullanmak için ideal kılar.
Load balancing temel olarak gelen isteklerin birden fazla backend sunucusuna dağıtılması işlemidir. Bu sayede:
- Tek bir sunucunun aşırı yüklenmesi engellenir
- Bir sunucu çökse bile servis kesintisiz devam eder
- Yatay ölçeklendirme (horizontal scaling) kolaylaşır
- Bakım pencerelerinde sıfır kesinti sağlanır
Nginx’te load balancing konfigürasyonu upstream bloğu üzerinden yapılır. Bu blok, backend sunucularını gruplandırır ve hangi algoritmaya göre trafik dağıtılacağını belirler.
Upstream Bloğunun Temel Yapısı
Her şey bir upstream tanımıyla başlar. Basit bir örnek:
http {
upstream backend_pool {
server 192.168.1.10:8080;
server 192.168.1.11:8080;
server 192.168.1.12:8080;
}
server {
listen 80;
server_name api.sirketim.com;
location / {
proxy_pass http://backend_pool;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
}
Bu yapıda Nginx, gelen istekleri backend_pool içindeki üç sunucuya varsayılan olarak round-robin yöntemiyle dağıtır. Şimdi bu yöntemleri tek tek inceleyelim.
Load Balancing Algoritmaları
Round Robin (Varsayılan)
En basit ve yaygın kullanılan yöntemdir. İstekler sırayla her sunucuya iletilir. 1. istek birinci sunucuya, 2. istek ikinci sunucuya, 3. istek üçüncü sunucuya, 4. istek tekrar birinci sunucuya gider.
Sunucularınızın kapasitesi eşit değilse, weight parametresiyle ağırlıklandırma yapabilirsiniz:
upstream backend_pool {
server 192.168.1.10:8080 weight=3; # Daha güçlü sunucu
server 192.168.1.11:8080 weight=2;
server 192.168.1.12:8080 weight=1; # Eski/zayıf sunucu
}
Bu konfigürasyonda her 6 istekten 3’ü birinci sunucuya, 2’si ikinci sunucuya, 1’i üçüncü sunucuya gider. Örneğin, farklı donanım konfigürasyonlarına sahip sunucularınız varsa bu yaklaşım oldukça işe yarar. Ben genellikle eski bir sunucuyu aşamalı olarak devre dışı bırakırken bu yöntemi kullanırım, weight değerini yavaş yavaş düşürürüm.
Least Connections
Round robin, her isteğin eşit sürede işlendiğini varsayar. Oysa gerçek dünyada bazı istekler saniyeler içinde tamamlanırken bazıları dakikalar alabilir. least_conn direktifi, yeni isteği aktif bağlantı sayısı en az olan sunucuya gönderir:
upstream api_backend {
least_conn;
server 192.168.1.10:8080;
server 192.168.1.11:8080;
server 192.168.1.12:8080;
}
Bu yöntem özellikle video streaming, uzun polling veya WebSocket bağlantıları gibi uzun süren işlemlerde round robin’e göre çok daha adil bir dağılım sağlar. E-ticaret projelerinde ödeme işlemleri gibi değişken süreli API çağrıları için de ideal bir seçimdir.
IP Hash
Bazen aynı istemcinin her zaman aynı backend sunucusuna gitmesini istersiniz. Özellikle session bilgisini sunucu tarafında saklayan legacy uygulamalarda bu kritik önem taşır. ip_hash direktifi, istemcinin IP adresine göre bir hash hesaplar ve her zaman aynı sunucuya yönlendirir:
upstream stateful_app {
ip_hash;
server 192.168.1.10:8080;
server 192.168.1.11:8080;
server 192.168.1.12:8080;
}
Önemli bir uyarı: IPv6 ile IP hash kullanırken dikkatli olun. Ayrıca, kullanıcılarınızın büyük çoğunluğu aynı NAT arkasındaysa (kurumsal ağlar gibi) tüm trafik tek bir sunucuya yığılabilir.
Generic Hash
IP hash’in daha esnek bir versiyonudur. Hash için herhangi bir Nginx değişkenini kullanabilirsiniz. URL bazlı yönlendirme için çok kullanışlıdır:
upstream content_servers {
hash $request_uri consistent;
server 192.168.1.10:8080;
server 192.168.1.11:8080;
server 192.168.1.12:8080;
}
consistent parametresi, consistent hashing algoritmasını etkinleştirir. Bu sayede bir sunucu eklendiğinde veya çıkarıldığında yalnızca o sunucuya ait istekler yeniden dağıtılır, tüm dağılım bozulmaz. CDN veya önbellekleme katmanı oluştururken bu özellik altın değerinde.
Least Time (Nginx Plus)
Bu algoritma yalnızca ticari Nginx Plus sürümünde mevcuttur. En düşük ortalama yanıt süresine ve en az aktif bağlantıya sahip sunucuyu seçer. Açık kaynak alternatifiniz için upstream_fair modülüne bakabilirsiniz.
Sağlık Kontrolü ve Yük Devretme
Sunuculardan biri çökerse Nginx’in bunu algılaması ve trafiği diğer sunuculara yönlendirmesi gerekir. Pasif sağlık kontrolü, başarısız istekleri takip ederek çalışır:
upstream resilient_backend {
server 192.168.1.10:8080 max_fails=3 fail_timeout=30s;
server 192.168.1.11:8080 max_fails=3 fail_timeout=30s;
server 192.168.1.12:8080 max_fails=3 fail_timeout=30s;
# Yedek sunucu - sadece diğerleri başarısız olduğunda devreye girer
server 192.168.1.20:8080 backup;
}
max_fails: Bir sunucunun “başarısız” sayılması için gereken hata sayısı fail_timeout: Bu süre içinde max_fails kadar hata olursa sunucu devre dışı kalır, aynı zamanda sunucunun ne kadar süre devre dışı kalacağını da belirler backup: Tüm aktif sunucular başarısız olduğunda devreye giren yedek sunucu
Bir sunucuyu geçici olarak devre dışı bırakmak istiyorsanız:
upstream maintenance_example {
server 192.168.1.10:8080;
server 192.168.1.11:8080;
server 192.168.1.12:8080 down; # Bakım modunda
}
Gerçek Dünya Senaryosu 1: Mikroservis API Gateway
Bir e-ticaret platformu için API gateway yapısı kurduğumuzu düşünelim. Farklı servisler için farklı upstream grupları tanımlıyoruz:
http {
# Ürün servisi - yoğun okuma trafiği
upstream product_service {
least_conn;
server 10.0.1.10:3000 weight=2;
server 10.0.1.11:3000 weight=2;
server 10.0.1.12:3000 weight=1;
keepalive 32;
}
# Sipariş servisi - durum bilgisi önemli
upstream order_service {
ip_hash;
server 10.0.2.10:4000 max_fails=2 fail_timeout=20s;
server 10.0.2.11:4000 max_fails=2 fail_timeout=20s;
server 10.0.2.20:4000 backup;
keepalive 16;
}
# Kullanıcı servisi
upstream user_service {
server 10.0.3.10:5000;
server 10.0.3.11:5000;
keepalive 8;
}
server {
listen 443 ssl http2;
server_name api.eticaret.com;
ssl_certificate /etc/nginx/ssl/api.crt;
ssl_certificate_key /etc/nginx/ssl/api.key;
# Ürün endpoint'leri
location /api/v1/products {
proxy_pass http://product_service;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_connect_timeout 5s;
proxy_read_timeout 30s;
}
# Sipariş endpoint'leri
location /api/v1/orders {
proxy_pass http://order_service;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_connect_timeout 5s;
proxy_read_timeout 60s;
}
# Kullanıcı endpoint'leri
location /api/v1/users {
proxy_pass http://user_service;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
}
keepalive direktifine dikkat edin. Bu parametre, upstream sunucularla olan bağlantıların açık tutulmasını sağlar. Her istek için yeni bir TCP bağlantısı kurmak pahalıdır, keepalive ile bu maliyeti önemli ölçüde azaltırsınız.
Gerçek Dünya Senaryosu 2: Mavi-Yeşil Dağıtım
Sıfır kesintili güncelleme için mavi-yeşil (blue-green) deployment stratejisi uygulayabiliriz:
upstream blue_env {
server 10.0.1.10:8080;
server 10.0.1.11:8080;
}
upstream green_env {
server 10.0.2.10:8080;
server 10.0.2.11:8080;
}
# Aktif ortamı belirleyen dosya
geo $upstream_pool {
default blue_env;
}
server {
listen 80;
location / {
proxy_pass http://$upstream_pool;
}
}
Pratikte bu geçişi Nginx konfigürasyon dosyasını değiştirerek ve nginx -s reload komutuyla gerçekleştirirsiniz. Yeni versiyonu green ortamına deploy ettikten sonra, trafiği yavaş yavaş green’e çekebilirsiniz.
Canary Deployment ile Kademeli Yayın
Yeni bir özelliği önce küçük bir kullanıcı grubuna açmak istiyorsanız:
upstream stable_backend {
server 10.0.1.10:8080 weight=9;
server 10.0.1.11:8080 weight=9;
}
upstream canary_backend {
server 10.0.2.10:8080 weight=1;
}
split_clients "${remote_addr}${http_user_agent}" $backend_pool {
10% canary_backend;
* stable_backend;
}
server {
listen 80;
location /api/ {
proxy_pass http://$backend_pool;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
add_header X-Served-By $upstream_addr; # Debug için
}
}
Bu konfigürasyonla trafiğin %10’u yeni versiyona giderken %90’ı eski versiyona gitmeye devam eder. İzleme araçlarınızda canary sunucusunun metriklerini takip ederek sorun yoksa yüzdeyi artırırsınız.
Bağlantı Zaman Aşımı ve Hata Yönetimi
Gerçek bir prodüksiyon ortamında zaman aşımı ve hata durumları için dikkatli bir yapılandırma şarttır:
upstream robust_backend {
server 10.0.1.10:8080;
server 10.0.1.11:8080;
server 10.0.1.12:8080;
}
server {
listen 80;
location / {
proxy_pass http://robust_backend;
# Zaman aşımı ayarları
proxy_connect_timeout 3s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
# Hata durumunda sonraki sunucuya geç
proxy_next_upstream error timeout http_500 http_502 http_503 http_504;
proxy_next_upstream_tries 3;
proxy_next_upstream_timeout 10s;
# Tampon ayarları
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
# Header'lar
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Özel hata sayfası
error_page 502 503 504 /maintenance.html;
location = /maintenance.html {
root /var/www/html;
internal;
}
}
proxy_next_upstream direktifi kritik bir öneme sahiptir. Bir backend sunucu hata döndürdüğünde Nginx, isteği otomatik olarak bir sonraki sunucuya iletir. Bu direktifin değerleri:
- error: Bağlantı hatası
- timeout: Zaman aşımı
- http_500: Internal Server Error
- http_502: Bad Gateway
- http_503: Service Unavailable
- http_504: Gateway Timeout
Rate Limiting ile Load Balancer’ı Korumak
Backend sunucularınızı aşırı yükten korumak için rate limiting eklemek iyi bir pratiktir:
http {
# IP bazlı rate limiting zone'u
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=100r/m;
# Bağlantı sayısı limitlerne
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
upstream api_backend {
least_conn;
server 10.0.1.10:8080;
server 10.0.1.11:8080;
keepalive 32;
}
server {
listen 80;
location /api/ {
limit_req zone=api_limit burst=20 nodelay;
limit_conn conn_limit 10;
proxy_pass http://api_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
}
Upstream Durumunu İzlemek
Hangi sunucuların aktif olduğunu, kaç istek aldığını görmek için stub_status modülünü kullanabilirsiniz. Daha kapsamlı izleme için özel bir status endpoint açabilirsiniz:
server {
listen 8080;
server_name localhost;
# Sadece iç ağdan erişime izin ver
allow 10.0.0.0/8;
allow 127.0.0.1;
deny all;
location /nginx_status {
stub_status on;
access_log off;
}
}
Bu endpoint size şu bilgileri verir:
- Aktif bağlantı sayısı
- Toplam kabul edilen, işlenen ve yapılan istek sayıları
- Okuma, yazma ve bekleme durumundaki bağlantılar
Prometheus ve Grafana kullanıyorsanız, nginx-prometheus-exporter aracıyla bu metrikleri görsel olarak izleyebilirsiniz. Ben her zaman load balancer üzerinde en az temel bir izleme kurulumu yaparım; sorun çıktığında geriye dönük log analizi yapmak çok zaman alır.
TCP ve UDP Load Balancing
Nginx yalnızca HTTP trafiğini değil, TCP ve UDP trafiğini de dengeleyebilir. Veritabanı bağlantıları veya özel protokoller için stream modülünü kullanırsınız:
stream {
upstream mysql_cluster {
least_conn;
server 10.0.3.10:3306 weight=2;
server 10.0.3.11:3306 weight=1;
}
upstream redis_cluster {
hash $remote_addr consistent;
server 10.0.4.10:6379;
server 10.0.4.11:6379;
server 10.0.4.12:6379;
}
server {
listen 3306;
proxy_pass mysql_cluster;
proxy_connect_timeout 3s;
proxy_timeout 300s;
}
server {
listen 6379;
proxy_pass redis_cluster;
}
}
MySQL gibi durum bilgisi olan servislerde hash $remote_addr kullanmak, aynı uygulamanın her zaman aynı veritabanı replikasına bağlanmasını sağlar. Redis Cluster kullanmıyorsanız ve kendi sharding mantığınızı oluşturuyorsanız bu yaklaşım oldukça işe yarar.
Konfigürasyon Test ve Reload
Her değişiklikten sonra konfigürasyonun doğruluğunu test edin, direkt reload etmeyin:
# Sözdizimi kontrolü
nginx -t
# Daha ayrıntılı test
nginx -T | grep -A5 upstream
# Kesintisiz yeniden yükleme
nginx -s reload
# Worker process'lerin durumunu kontrol et
ps aux | grep nginx
# Aktif bağlantıları izle
ss -tuln | grep nginx
Prodüksiyon ortamında konfigürasyon değişikliklerini her zaman önce staging’de test ederim. nginx -t komutu sözdizimi hatalarını yakalar ama mantık hatalarını yakalamaz; bu yüzden staging testi hayat kurtarır.
Sonuç
Nginx ile load balancing, doğru strateji seçildiğinde hem uygulamanızın dayanıklılığını hem de performansını dramatik biçimde artırır. Round robin basit ve etkilidir, least_conn uzun süren işlemler için daha adaletlidir, ip_hash durum bilgisi olan uygulamalarda hayat kurtarır.
Yeni bir load balancing yapısı kurarken şu sırayı takip etmenizi öneririm: Önce basit round robin ile başlayın, ardından uygulamanızın davranışını gözlemleyin. Hangi backend’in ne kadar süre meşgul kaldığını, hangi hata kodlarının üretildiğini izleyin. Sonra bu verilere dayanarak algoritmayı ve parametreleri optimize edin.
Keepalive bağlantılarını ihmal etmeyin; özellikle yüksek trafikli ortamlarda TCP el sıkışma maliyeti ciddi bir yük oluşturur. proxy_next_upstream ile otomatik failover yapılandırın, tek bir sunucu çöktüğünde kullanıcılarınız fark bile etmemeli. Rate limiting ekleyerek hem backend’lerinizi hem de Nginx’in kendisini olası saldırılardan koruyun.
Son olarak, izleme olmadan load balancing yarım kalır. Prometheus, Grafana veya en azından temel bir stub_status sayfasıyla backend’lerinizin sağlığını sürekli gözlemleyin. Sorun çıktığında metriklere bakarak hangi sunucunun, ne zaman, hangi hataları ürettiğini anında görebilmek, gece 2’de alarm aldığınızda saatlerce log karıştırmaktan çok daha iyidir.
