Nginx Upstream ile Yük Dengeleme Algoritmaları

Yük dengeleme, modern web altyapısının bel kemiğidir. Tek bir sunucuya yığılan trafik hem performansı düşürür hem de tek nokta arızası (SPOF) riski yaratır. Nginx’in upstream modülü, bu sorunu çözmek için son derece güçlü ve esnek araçlar sunar. Bu yazıda Nginx’in desteklediği yük dengeleme algoritmalarını gerçek dünya senaryolarıyla birlikte derinlemesine inceleyeceğiz.

Upstream Modülüne Giriş

Nginx’te yük dengeleme, upstream bloğu üzerinden yapılandırılır. Bu blok, arkadaki sunucu grubunu tanımlar ve Nginx bu gruba gelen istekleri dağıtır. Temel yapı şu şekildedir:

upstream backend_grubu {
    server 192.168.1.10:8080;
    server 192.168.1.11:8080;
    server 192.168.1.12:8080;
}

server {
    listen 80;
    server_name example.com;

    location / {
        proxy_pass http://backend_grubu;
        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ılandırmada Nginx, gelen istekleri üç backend sunucusu arasında dağıtır. Peki hangi algoritmayı kullanır? Herhangi bir direktif belirtmediğinde Nginx varsayılan olarak Round Robin algoritmasını kullanır.

Round Robin (Varsayılan)

Round Robin, en basit ve yaygın yük dengeleme algoritmasıdır. İstekler sırayla her sunucuya iletilir: birinci istek ilk sunucuya, ikinci istek ikinci sunucuya, üçüncü istek üçüncü sunucuya, dördüncü istek yeniden ilk sunucuya gider.

upstream app_servers {
    server 192.168.1.10:3000;
    server 192.168.1.11:3000;
    server 192.168.1.12:3000;
}

Round Robin’in güzel yanı, sunucularınızın benzer donanım kapasitesine sahip olduğu durumlarda mükemmel çalışmasıdır. Ancak gerçek hayatta her sunucu eşit güçte olmayabilir. Bu durumda ağırlıklı Round Robin devreye girer:

upstream app_servers {
    server 192.168.1.10:3000 weight=5;
    server 192.168.1.11:3000 weight=3;
    server 192.168.1.12:3000 weight=2;
}

Bu örnekte her 10 istekten 5’i birinci sunucuya, 3’ü ikinci sunucuya, 2’si üçüncü sunucuya gider. Diyelim ki yeni bir sunucu aldınız ve eski sunuculardan daha güçlü. Trafiğin büyük bölümünü oraya yönlendirmek istiyorsunuz. İşte bu senaryoda weight parametresi biçilmiş kaftandır.

Gerçek Dünya Senaryosu: Bir e-ticaret sitesi düşünün. Black Friday öncesi bir sunucuyu upgrade ettiniz ama diğerleri hâlâ eski donanımda. Yeni sunucuya weight=10, eski sunuculara weight=5 verirseniz trafiği kapasiteyle orantılı dağıtırsınız.

Least Connections (En Az Bağlantı)

Round Robin teorik olarak güzel görünür ama pratikte sorun yaratabilir. Bazı istekler çok hızlı tamamlanırken bazıları dakikalarca açık kalabilir. Round Robin bu farkı görmezden gelir ve tüm sunuculara eşit sayıda istek gönderir. Sonuç: bazı sunucular boğulurken diğerleri neredeyse boş oturur.

Least Connections bu sorunu çözer. Nginx, mevcut aktif bağlantı sayısına bakar ve en az bağlantıya sahip sunucuya yeni isteği iletir.

upstream app_servers {
    least_conn;
    server 192.168.1.10:8080;
    server 192.168.1.11:8080;
    server 192.168.1.12:8080;
}

Bu algoritmayı ağırlıkla da kullanabilirsiniz:

upstream app_servers {
    least_conn;
    server 192.168.1.10:8080 weight=3;
    server 192.168.1.11:8080 weight=1;
    server 192.168.1.12:8080 weight=2;
}

Gerçek Dünya Senaryosu: WebSocket bağlantıları veya dosya yükleme/indirme işlemleri gibi uzun süren bağlantılarda Least Connections algoritması çok daha iyi sonuç verir. Bir video streaming platformunda her kullanıcının bağlantısı dakikalarca açık kalır. Round Robin bu durumda bazı sunucuları aşırı yüklerken Least Connections yükü dengeli tutar.

IP Hash

IP Hash algoritması, istemcinin IP adresine göre bir hash hesaplar ve aynı IP’den gelen istekleri her zaman aynı sunucuya yönlendirir. Bu yaklaşıma session persistence veya sticky sessions de denir.

upstream app_servers {
    ip_hash;
    server 192.168.1.10:8080;
    server 192.168.1.11:8080;
    server 192.168.1.12:8080;
}

Neden önemli? Bazı uygulamalar, oturum bilgisini sunucu belleğinde tutar. Kullanıcı her istekte farklı bir sunucuya giderse oturum bilgisi kaybolur ve kullanıcı sürekli çıkış yapılmış gibi görür. IP Hash bu sorunu çözer.

Ancak IP Hash’in de dezavantajları var. IPv4 adresleri genellikle NAT arkasında olduğundan, bir ofisten yüzlerce kullanıcı aynı IP adresini kullanıyor olabilir. Bu durumda tüm o kullanıcılar tek bir sunucuya yığılır.

Bakım için bir sunucuyu geçici olarak devre dışı bırakmak istiyorsanız down parametresini kullanın:

upstream app_servers {
    ip_hash;
    server 192.168.1.10:8080;
    server 192.168.1.11:8080 down;
    server 192.168.1.12:8080;
}

down olarak işaretlenen sunucu hash hesaplamasında tutulur böylece kalan sunuculara yapılan yeniden dağılım minimize edilir.

Generic Hash

Generic Hash, IP Hash’in daha esnek bir versiyonudur. Hash için kullanılacak anahtarı siz belirlersiniz. Bu anahtar; IP adresi, URI, cookie değeri veya bunların kombinasyonu olabilir.

upstream app_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ı (ketama) etkinleştirir. Bu özellikle önbellek sunucuları için kritiktir. Sunucu havuzuna sunucu eklendiğinde veya çıkarıldığında tüm önbellek geçersiz olmaz, sadece etkilenen anahtarlar yeniden dağıtılır.

Cookie tabanlı yapılandırma:

upstream app_servers {
    hash $cookie_session_id consistent;
    server 192.168.1.10:8080;
    server 192.168.1.11:8080;
    server 192.168.1.12:8080;
}

Gerçek Dünya Senaryosu: Memcached veya Redis cluster’ı olmayan bir ortamda her web sunucusunun kendi yerel önbelleği varsa, aynı URL’ye ait isteklerin hep aynı backend’e gitmesini istersiniz. hash $request_uri consistent ile bu sağlanır. Böylece önbellek hit oranı dramatik biçimde artar.

Least Time (Nginx Plus)

Bu algoritma Nginx Plus’a özgüdür, açık kaynak sürümünde kullanılamaz. Bağlantı sayısının yanı sıra sunucunun ortalama yanıt süresini de dikkate alır.

upstream app_servers {
    least_time header;
    server 192.168.1.10:8080;
    server 192.168.1.11:8080;
    server 192.168.1.12:8080;
}

Parametre olarak header (ilk byte alınma süresi) veya last_byte (tam yanıt süresi) kullanılabilir. Açık kaynak alternatifi olarak üçüncü taraf modüller mevcuttur ama bunlar derleme gerektirdiğinden bu yazının kapsamı dışında.

Sunucu Parametreleri ve Sağlık Kontrolleri

Yük dengeleme algoritmalarından bağımsız olarak, her sunucu için çeşitli parametreler tanımlayabilirsiniz.

upstream app_servers {
    least_conn;

    server 192.168.1.10:8080 weight=3 max_fails=3 fail_timeout=30s;
    server 192.168.1.11:8080 weight=2 max_fails=3 fail_timeout=30s;
    server 192.168.1.12:8080 backup;
}

Parametrelerin açıklamaları:

  • weight: Sunucunun göreli ağırlığı, varsayılan değer 1’dir
  • max_fails: Belirtilen süre içinde başarısız olması kabul edilecek maksimum deneme sayısı
  • fail_timeout: Hem başarısız denemelerin sayıldığı süre hem de sunucunun başarısız sayıldıktan sonra ne kadar süre devre dışı kalacağı
  • backup: Diğer tüm sunucular başarısız olduğunda devreye giren yedek sunucu
  • down: Sunucuyu kalıcı olarak devre dışı bırakır

Yukarıdaki yapılandırmada 192.168.1.12, diğer iki sunucu da çöktüğünde devreye girecek yedek sunucudur. Yedek sunucu normal operasyonda asla trafik almaz.

Gerçek Bir Senaryo: Node.js Uygulama Sunucuları

Diyelim ki üç Node.js uygulama sunucunuz var ve arkalarında PostgreSQL var. Oturum bilgisini Redis’te tutuyorsunuz. Bu durumda IP Hash’e gerek yok çünkü Redis merkezi bir oturum deposu sağlıyor. Least Connections daha mantıklı bir seçim olur:

upstream nodejs_backend {
    least_conn;

    server 10.0.0.10:3000 weight=2 max_fails=3 fail_timeout=20s;
    server 10.0.0.11:3000 weight=2 max_fails=3 fail_timeout=20s;
    server 10.0.0.12:3000 weight=1 max_fails=3 fail_timeout=20s;

    keepalive 32;
}

server {
    listen 80;
    server_name myapp.com;

    location / {
        proxy_pass http://nodejs_backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        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;

        proxy_connect_timeout 5s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }
}

keepalive 32 direktifi önemlidir. Bu, Nginx’in upstream sunucularla en fazla 32 adet kalıcı bağlantı tutmasını sağlar. Her istek için yeni TCP bağlantısı açmak gereksiz overhead yaratır. proxy_http_version 1.1 ve proxy_set_header Connection "" ise HTTP/1.1 keepalive’ı aktif eder.

Yük Dengeleme ile Sağlık Kontrolü Entegrasyonu

Nginx açık kaynak sürümü pasif sağlık kontrolü yapar: bir sunucu yanıt vermediğinde onu geçici olarak devre dışı bırakır. Nginx Plus aktif sağlık kontrolü sunar. Açık kaynak için alternatif bir yaklaşım:

upstream api_backend {
    least_conn;

    server 172.16.0.10:8080 max_fails=2 fail_timeout=10s;
    server 172.16.0.11:8080 max_fails=2 fail_timeout=10s;
    server 172.16.0.12:8080 max_fails=2 fail_timeout=10s;
    server 172.16.0.13:8080 backup;
}

server {
    listen 443 ssl http2;
    server_name api.example.com;

    ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;

    location /health {
        access_log off;
        return 200 "healthyn";
        add_header Content-Type text/plain;
    }

    location / {
        proxy_pass http://api_backend;
        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;

        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;
    }
}

proxy_next_upstream direktifi kritik bir özelliktir. Bir upstream sunucu belirtilen koşullarda başarısız olursa Nginx isteği otomatik olarak bir sonraki sunucuya iletir. Bu sayede tek bir sunucunun hatası kullanıcıya yansımaz.

Farklı Lokasyonlar için Farklı Backend Grupları

Monolitik bir yapıdan mikro servislere geçiş sürecindeyseniz, farklı URL yollarını farklı backend gruplarına yönlendirmek gerekebilir:

upstream frontend_servers {
    least_conn;
    server 10.10.0.10:3000;
    server 10.10.0.11:3000;
    keepalive 16;
}

upstream api_servers {
    least_conn;
    server 10.10.1.10:8080 weight=3;
    server 10.10.1.11:8080 weight=3;
    server 10.10.1.12:8080 weight=2;
    keepalive 32;
}

upstream static_servers {
    hash $request_uri consistent;
    server 10.10.2.10:80;
    server 10.10.2.11:80;
    keepalive 8;
}

server {
    listen 80;
    server_name www.example.com;

    location /api/ {
        proxy_pass http://api_servers;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    location /static/ {
        proxy_pass http://static_servers;
        proxy_cache_valid 200 1d;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
    }

    location / {
        proxy_pass http://frontend_servers;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Bu yapıda statik dosyalar için URI tabanlı hash kullanılıyor. Aynı dosya her zaman aynı sunucudan gelir, önbellek verimliliği artar. API sunucuları ise Least Connections ile yönetiliyor çünkü API istekleri değişken sürelerde tamamlanabilir.

Upstream Durum İzleme

Hangi sunucunun ne kadar trafik aldığını, kaç başarısız istek yaşandığını görmek istersiniz. Nginx’in stub_status modülü temel bilgiler sunar:

server {
    listen 8080;
    server_name localhost;

    location /nginx_status {
        stub_status;
        allow 127.0.0.1;
        allow 10.0.0.0/8;
        deny all;
    }
}

Daha ayrıntılı izleme için ngx_http_upstream_module‘ün sağladığı değişkenleri log formatına ekleyebilirsiniz:

log_format upstream_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_response_time: $upstream_response_time '
                        'request_time: $request_time';

server {
    listen 80;
    access_log /var/log/nginx/access.log upstream_log;
    ...
}

Bu log formatıyla hangi upstream sunucunun ne kadar sürede yanıt verdiğini, hangi durum kodunu döndürdüğünü takip edebilirsiniz. Prometheus ve Grafana ile entegre etmek isteyenler için nginx-prometheus-exporter aracını incelemenizi öneririm.

Sık Yapılan Hatalar

Yük dengeleme yapılandırmalarında sıkça karşılaştığım hatalar şunlar:

  • Yanlış algoritma seçimi: Oturum bilgisini sunucu belleğinde tutan bir uygulamada IP Hash kullanmadan Round Robin veya Least Connections tercih edilirse kullanıcılar sürekli oturum kaybeder. Uygulamanızın yapısını iyi anlayın.
  • keepalive eksikliği: Her istek için yeni TCP bağlantısı açmak ciddi performans kaybı yaratır. Özellikle yüksek trafikli ortamlarda keepalive direktifini kullanmak büyük fark yaratır.
  • fail_timeout yanlış ayarlanması: Çok kısa fail_timeout değerleri sunucuların sık sık pool’a eklenip çıkarılmasına neden olur. Çok uzun değerler ise gerçekten düzelmiş bir sunucunun uzun süre devre dışı kalmasına yol açar. 30 saniye ile 1 dakika arası genellikle makul bir değerdir.
  • proxy_next_upstream eksikliği: Bu direktif olmadan bir backend sunucusunun anlık hatası doğrudan kullanıcıya 502 olarak yansır. Kesinlikle eklenmeli.
  • Ağırlıkları güncellememeyi unutmak: Sunucu kapasitesi değiştiğinde ağırlıkları güncellemeyi unutanlar var. Nginx’i yeniden yüklemek yeterli, tam yeniden başlatmaya gerek yok: nginx -s reload

Yapılandırma Testi ve Yeniden Yükleme

Herhangi bir değişiklik yapmadan önce mutlaka yapılandırmayı test edin:

# Yapılandırma dosyasını test et
nginx -t

# Hata yoksa yeniden yükle (bağlantıları kesmeden)
nginx -s reload

# Nginx servis durumunu kontrol et
systemctl status nginx

# Upstream durumunu log'lardan takip et
tail -f /var/log/nginx/access.log | grep upstream

nginx -s reload komutu mevcut bağlantıları kesmez, yeni bağlantılar güncel yapılandırmayı kullanırken eski bağlantılar normal şekilde tamamlanır. Üretim ortamında bu özellik son derece değerlidir.

Sonuç

Nginx’in upstream modülü, yük dengeleme için ihtiyaç duyabileceğiniz hemen her senaryoyu karşılayabilecek esnekliktedir. Algoritmayı seçerken uygulamanızın özelliklerini göz önünde bulundurun:

  • Sunucular eşit kapasitedeyse ve oturum yönetimi merkezi ise Round Robin yeterlidir
  • Değişken süreli istekler söz konusuysa Least Connections daha iyi sonuç verir
  • Sunucu taraflı oturum yönetimi varsa IP Hash kullanın
  • Önbellek verimliliği kritikse Generic Hash ile consistent parametresini tercih edin

Doğru algoritmanın yanı sıra keepalive, proxy_next_upstream, max_fails ve fail_timeout parametrelerini de doğru ayarlamak, yüksek trafikli ve güvenilir bir sistem için zorunludur. Yapılandırmalarınızı mutlaka izleyin; log analizi ve metrik takibi olmadan hangi algoritmanın gerçekten daha iyi çalıştığını anlayamazsınız.

Yorum yapın