nginx: Load balancing
Ciljevi lekcije
Nakon ove lekcije trebalo bi da:
- definišeš grupu backend servera kroz
upstreamblok - biraš metod raspodele prema prirodi aplikacije
- podesiš keep-alive konekcije prema backend-u i znaš zašto je to obavezno
- razumeš pasivne provere zdravlja i njihova ograničenja u besplatnoj verziji
- rešiš pitanje sesija kada aplikacija nije bezstanjna
1. upstream blok
Umesto jedne adrese u proxy_pass, definišeš imenovanu grupu:
upstream aplikacija {
server 10.0.1.10:3000;
server 10.0.1.11:3000;
server 10.0.1.12:3000;
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://aplikacija;
include proxy_params;
}
}
upstream ide u http kontekst, van server blokova. Ime grupe je proizvoljno i koristiš ga u proxy_pass na mestu gde bi inače stajao host.
Isti mehanizam radi i sa unix socket-ima, što je korisno kada više instanci aplikacije radi na istoj mašini:
upstream aplikacija {
server unix:/run/app-1.sock;
server unix:/run/app-2.sock;
server unix:/run/app-3.sock;
}
2. Metodi raspodele
Round-robin (podrazumevano)
Zahtevi idu redom: prvi, drugi, treći, prvi... Ne moraš ništa da navodiš.
Dobro radi kada su serveri jednaki i zahtevi približno jednako skupi. Loše kada neki zahtevi traju sekundu, a neki minut — server koji je zaglavljen na dugom zahtevu i dalje uredno dobija svoj deo novih.
Težine
upstream aplikacija {
server 10.0.1.10:3000 weight=3; # dobija 3 od svakih 5 zahteva
server 10.0.1.11:3000 weight=1;
server 10.0.1.12:3000 weight=1;
}
Koristi kada mašine nisu iste snage, ili pri postepenom uvođenju nove verzije — pustiš joj weight=1 naspram weight=9 i posmatraš.
least_conn — najmanje aktivnih konekcija
upstream aplikacija {
least_conn;
server 10.0.1.10:3000;
server 10.0.1.11:3000;
}
Novi zahtev ide serveru koji trenutno opslužuje najmanje konekcija. Ovo je bolji podrazumevani izbor od round-robina za većinu realnih aplikacija, jer se sam prilagođava kada zahtevi imaju neujednačeno trajanje.
ip_hash — vezivanje po IP adresi
upstream aplikacija {
ip_hash;
server 10.0.1.10:3000;
server 10.0.1.11:3000;
}
Isti klijent uvek pada na isti server. Rešava problem sesija u aplikacijama koje ih drže lokalno.
Ograničenja su ozbiljna i treba ih znati unapred: korisnici iza istog NAT-a (cela firma, mobilni operater) računaju se kao jedan klijent, pa raspodela postaje neravnomerna. Kada dodaš ili ukloniš server, hash se menja i veliki deo korisnika gubi sesiju. Klijenti sa promenljivom IP adresom gube sesiju sami od sebe.
Uzmi ovo kao privremeno rešenje dok ne prebaciš sesije u Redis ili bazu.
hash — proizvoljni ključ
upstream aplikacija {
hash $request_uri consistent;
server 10.0.1.10:3000;
server 10.0.1.11:3000;
}
Raspodela po bilo kojoj promenljivoj. Odličan za keš servere — isti URI uvek ide istom serveru, pa svaki server kešira samo svoj deo sadržaja.
Parametar consistent uključuje konzistentno heširanje (ketama algoritam): kada dodaš ili ukloniš server, preraspoređuje se samo mali deo ključeva umesto skoro svih. Ako koristiš hash, praktično uvek želiš i consistent.
Vezivanje po sesijskom kolačiću:
hash $cookie_sessionid consistent;
random
upstream aplikacija {
random two least_conn;
server 10.0.1.10:3000;
server 10.0.1.11:3000;
server 10.0.1.12:3000;
}
Bira dva servera nasumično, pa između njih onaj sa manje konekcija. Zvuči čudno, ali je dokazano dobar algoritam ("power of two choices") u postavkama sa više load balancera, gde least_conn na svakom od njih može da dovede do toga da svi istovremeno preplave isti server.
Kako birati
| Situacija | Izbor |
|---|---|
| Bezstanjni API, ujednačeni zahtevi | round-robin |
| Neujednačeno trajanje zahteva | least_conn |
| Sesije u memoriji aplikacije | ip_hash (privremeno) ili hash $cookie_... |
| Keš serveri | hash $request_uri consistent |
| Više load balancera ispred istih backend-ova | random two least_conn |
3. Keep-alive prema backend-u
Ovo je podešavanje koje se najčešće izostavi, a ima najveći uticaj na performanse.
Podrazumevano nginx otvara novu TCP konekciju za svaki zahtev ka backend-u i zatvara je odmah nakon odgovora. Pri par hiljada zahteva u sekundi to znači stalno uspostavljanje i rušenje konekcija, gomilanje socket-a u TIME_WAIT stanju i nepotrebno kašnjenje.
Rešenje ima tri dela i sva tri su obavezna:
upstream aplikacija {
least_conn;
server 10.0.1.10:3000;
server 10.0.1.11:3000;
keepalive 32; # 1. bazen zadržanih konekcija
}
server {
location / {
proxy_pass http://aplikacija;
include proxy_params;
proxy_http_version 1.1; # 2. keep-alive ne postoji u HTTP/1.0
proxy_set_header Connection "";# 3. ukloni "Connection: close"
}
}
Ako izostaviš bilo koji od ova tri reda, keep-alive neće raditi — a nećeš dobiti nikakvo upozorenje. Tiho ćeš i dalje otvarati konekciju po zahtevu.
Treći red zaslužuje objašnjenje. nginx podrazumevano šalje Connection: close ka backend-u. Postavljanje na prazan niz uklanja zaglavlje, pa se primenjuje HTTP/1.1 podrazumevano ponašanje, a to je zadržana konekcija.
Koliko? Broj u keepalive je maksimum neaktivnih konekcija po worker procesu, ne ukupan limit. Za većinu postavki 16–32 je dovoljno. Previše velika vrednost troši deskriptore na obe strane bez koristi.
Proveri i da backend ne zatvara konekcije prerano — Node.js, Gunicorn i ostali imaju svoje keep-alive tajmaute koji treba da budu duži od nginx-ovog, inače dolazi do trke gde backend zatvori konekciju taman kad nginx šalje zahtev, što se u logu vidi kao povremeni 502.
4. Provere zdravlja
Pasivne provere (besplatna verzija)
nginx prati neuspehe tokom normalnog saobraćaja:
upstream aplikacija {
server 10.0.1.10:3000 max_fails=3 fail_timeout=30s;
server 10.0.1.11:3000 max_fails=3 fail_timeout=30s;
}
Čita se: ako server tri puta ne uspe u roku od 30 sekundi, izbaci ga iz rotacije na 30 sekundi. Nakon isteka, nginx mu pošalje jedan zahtev da proveri da li se oporavio.
Podrazumevano je max_fails=1 fail_timeout=10s, što je često prestrogo — jedan slučajan neuspeh izbacuje server. Vrednost 2–3 je razumnija.
Šta se računa kao neuspeh kontroliše se sa proxy_next_upstream:
proxy_next_upstream error timeout http_502 http_503 http_504;
proxy_next_upstream_tries 2;
proxy_next_upstream_timeout 10s;
Podrazumevano su to samo error i timeout — HTTP greške poput 502 se ne računaju osim ako ih ne navedeš. Aplikacija koja vraća 500 zbog buga u kodu ne treba da izbacuje server iz rotacije, ali 502 od zaglavljenog procesa treba.
Oprez sa non_idempotent: podrazumevano nginx ne ponavlja POST i druge neidempotentne zahteve na sledećem serveru. Ako to uključiš, rizikuješ dupli upis u bazu.
Ograničenje pasivnih provera: server koji ne prima saobraćaj se ne proverava. Ako je izbačen, saznaćeš da je opet živ tek kad mu pošalješ zahtev — a to znači da jedan korisnik dobija grešku umesto tebe.
Aktivne provere
Prave aktivne provere (health_check direktiva, koja periodično sama šalje zahtev na /health endpoint) postoje samo u NGINX Plus. To je jedna od glavnih razlika između verzija.
U besplatnoj verziji imaš nekoliko opcija:
- spoljni monitoring koji, kada primeti pad, prepiše konfiguraciju i uradi reload
- third-party modul
nginx_upstream_check_module(traži kompajliranje iz izvora) - service discovery — Consul Template,
confdi slično, koji generišuupstreamblok iz registra servisa - premeštanje odgovornosti na sloj ispod: Kubernetes servisi, HAProxy, cloud load balancer
Za većinu manjih postavki pasivne provere sa razumnim max_fails su sasvim dovoljne.
5. Rezervni serveri i planirano isključivanje
upstream aplikacija {
server 10.0.1.10:3000;
server 10.0.1.11:3000;
server 10.0.1.12:3000 backup; # samo kad svi ostali padnu
server 10.0.1.13:3000 down; # trajno isključen
}
backup server ne prima saobraćaj dok je bar jedan primarni živ. Korisno za stranicu sa porukom o održavanju ili za rezervnu lokaciju.
down označava server kao trajno nedostupan. Koristi ovo umesto komentarisanja reda kada nekog isključuješ radi održavanja — konfiguracija ostaje čitljiva i vidi se da server postoji.
Napomena: backup se ne može kombinovati sa hash ni random metodama.
Postupak za bezbedno isključivanje servera
Kada treba da izbaciš server iz rotacije radi nadogradnje:
- dodaj
downuz njegov red uupstreambloku sudo nginx -t && sudo systemctl reload nginx- sačekaj da postojeći zahtevi na tom serveru završe
- radi šta si naumio
- ukloni
down, ponovi reload
Zbog graceful reload-a iz lekcije 1, nijedan zahtev u toku se ne prekida.
Ograničavanje konekcija po serveru
server 10.0.1.10:3000 max_conns=100;
Kada backend ima ograničen broj workera, ovo sprečava da ga nginx preoptereti. Zahtevi preko limita čekaju u redu umesto da ruše aplikaciju.
6. Deljena zona
upstream aplikacija {
zone aplikacija 64k;
least_conn;
server 10.0.1.10:3000;
server 10.0.1.11:3000;
}
Bez zone, svaki worker proces vodi sopstvenu evidenciju o broju konekcija i o tome koji su serveri pali. Na mašini sa osam jezgara to znači osam nezavisnih pogleda na stanje — least_conn ne vidi celu sliku, a server koji je jedan worker izbacio i dalje prima saobraćaj od ostalih.
Sa zone stanje je u deljenoj memoriji i svi workeri ga vide isto. Ovo praktično uvek želiš kada imaš više od jednog backend servera.
7. Dinamičko razrešavanje imena
Ako backend serveri menjaju IP adrese (kontejneri, auto-scaling), imaš problem: nginx razrešava imena jednom, pri startu, i drži rezultat zauvek.
resolver 127.0.0.53 valid=10s ipv6=off;
server {
location / {
set $backend "http://aplikacija.servis.local:3000";
proxy_pass $backend;
}
}
Korišćenje promenljive prisiljava nginx da razrešava ime u vreme svakog zahteva, uz keširanje po valid parametru.
Cena je gubitak upstream bloka sa svim njegovim mogućnostima. U ozbiljnijim postavkama bolje je koristiti alat koji generiše konfiguraciju iz registra servisa i radi reload.
Na Ubuntu 24.04, lokalni DNS resolver je systemd-resolved na 127.0.0.53.
8. Praćenje raspodele
Dodaj u log koji je server obradio zahtev:
log_format lb '$remote_addr - $status "$request" '
'upstream=$upstream_addr '
'vreme_backend=$upstream_response_time '
'status_backend=$upstream_status '
'ukupno=$request_time';
access_log /var/log/nginx/lb.log lb;
Kada je zahtev pokušan na više servera, ove promenljive sadrže vrednosti razdvojene zarezima:
upstream=10.0.1.10:3000, 10.0.1.11:3000 status_backend=502, 200
To je zapis da je prvi server odbio zahtev, a drugi ga uspešno obradio — tačno ono što želiš da vidiš pri dijagnostici.
Brza provera raspodele:
sudo awk '{print $6}' /var/log/nginx/lb.log | sort | uniq -c | sort -rn
Praktična vežba
1. Pokreni tri instance jednostavnog backend-a, svaku na svom portu, tako da se u odgovoru vidi koja je:
mkdir -p ~/lb-test && cd ~/lb-test
cat > server.js <<'EOF'
const http = require('http');
const port = process.argv[2];
http.createServer((req, res) => {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end(`backend na portu ${port}\n`);
}).listen(port, '127.0.0.1');
EOF
node server.js 3001 & node server.js 3002 & node server.js 3003 &
2. Podesi round-robin i posmatraj raspodelu:
upstream test_lb {
server 127.0.0.1:3001;
server 127.0.0.1:3002;
server 127.0.0.1:3003;
}
server {
listen 80;
server_name lb.local;
location / {
proxy_pass http://test_lb;
include proxy_params;
}
}
for i in $(seq 1 9); do curl -s -H "Host: lb.local" http://localhost/; done
Trebalo bi da vidiš uredno smenjivanje 3001 → 3002 → 3003.
3. Dodaj težine (weight=3 prvom serveru), primeni i pošalji 20 zahteva. Prebroj:
for i in $(seq 1 20); do curl -s -H "Host: lb.local" http://localhost/; done | sort | uniq -c
4. Simuliraj pad servera:
kill %1 # gasi 3001
for i in $(seq 1 6); do curl -s -H "Host: lb.local" http://localhost/; done
Prvi zahtev može da bude sporiji ili da vrati grešku — to je trenutak u kome nginx otkriva da server ne odgovara. Nakon toga saobraćaj ide samo na preostala dva. Pogledaj error log.
Pokreni server ponovo (node server.js 3001 &) i posmatraj koliko treba da se vrati u rotaciju sa različitim vrednostima fail_timeout.
5. Uporedi ip_hash i round-robin. Dodaj ip_hash; u upstream blok, primeni i pošalji deset zahteva sa iste mašine — svi treba da idu na isti server.
6. Izmeri efekat keep-alive konekcija. Prvo bez njih, sa ab ili hey:
sudo apt install apache2-utils
ab -n 2000 -c 50 -H "Host: lb.local" http://localhost/
Zapamti broj zahteva u sekundi. Zatim dodaj sva tri reda za keep-alive, primeni i ponovi test. Uporedi i rezultat i broj socket-a u TIME_WAIT stanju:
ss -tan state time-wait | wc -l
7. Podesi lb log format, pošalji zahteve dok je jedan server oboren i pronađi red u kome upstream sadrži dve adrese.
Rezime
upstreamblok ide uhttpkontekst; ime grupe koristiš uproxy_pass.least_connje bolji podrazumevani izbor od round-robina za većinu realnih aplikacija.ip_hashrešava sesije, ali loše podnosi NAT i menjanje sastava grupe. Prava sesija ide u Redis.- Keep-alive prema backend-u traži tri stvari:
keepalive Nu upstream bloku,proxy_http_version 1.1iproxy_set_header Connection "". Bez svih tri ne radi, i to tiho. - Pasivne provere (
max_fails,fail_timeout) su jedino što besplatna verzija nudi; aktivne su NGINX Plus. proxy_next_upstreampodrazumevano ne obuhvata HTTP greške — 502 i 503 moraš navesti izričito.zoneje potreban da bi svi workeri delili stanje o konekcijama i o palim serverima.downje čistiji način da isključiš server od komentarisanja reda.- Zapiši
$upstream_addri$upstream_response_timeu log pre nego što ti zatreba.
Pitanja za proveru
- Zašto sam
keepalive 32;uupstreambloku nije dovoljan? - Aplikacija drži sesije u memoriji i imaš tri servera. Koja su tvoja rešenja i koje su mane svakog?
- Backend vraća 502 kada je preopterećen, ali nginx i dalje šalje zahteve na njega. Šta si propustio?
- Šta znači
zone aplikacija 64k;i šta se dešava bez toga na mašini sa osam jezgara? - Kako bezbedno isključuješ jedan server iz rotacije radi nadogradnje?
- U logu vidiš
upstream=10.0.1.10:3000, 10.0.1.11:3000. Šta se dogodilo? - Zašto je opasno dodati
non_idempotentuproxy_next_upstream?
Sledeća lekcija: HTTPS i TLS — Let's Encrypt sertifikati, preusmeravanje sa HTTP-a, izbor protokola i šifri, HSTS.
Comments
Post a Comment