Cos'è il load balancing e come funziona
Come funziona il load balancing: algoritmi di distribuzione, health check, sessioni sticky, layer 4 e layer 7, e quando invece non ti serve affatto.
Il sito cresce, il server inizia ad arrancare nelle ore di punta, e la parola che senti ripetere è "bilanciamento di carico". Sembra la risposta ovvia: metti due server invece di uno. Solo che, quando ci provi, saltano fuori le sessioni che si perdono, i server morti che continuano a ricevere traffico e una complessità che non avevi preventivato. In questo articolo trovi come funziona davvero, quali algoritmi esistono e cosa cambia tra loro, i problemi concreti che incontri — e perché in molti casi la risposta giusta è non farlo.
Cos'è
Il load balancing è la distribuzione delle richieste in arrivo su più server, in modo che nessuno di loro riceva più traffico di quanto riesca a gestire.
Chi visita il sito vede un solo indirizzo. Dietro, un componente chiamato bilanciatore riceve ogni richiesta e decide a quale server passarla.
Il secondo beneficio, spesso più importante del primo, è la disponibilità. Con un server solo, se quel server cade il sito è offline. Con tre server dietro un bilanciatore, se uno cade gli altri due continuano a rispondere e la maggior parte degli utenti non se ne accorge.
Perché serve (e le due ragioni sono diverse)
Vale la pena separarle, perché portano a scelte diverse.
Capacità. Un server ha un limite. Quando lo raggiungi, o compri una macchina più grande (scalabilità verticale) o ne aggiungi altre uguali (orizzontale). Il bilanciatore è ciò che rende possibile la seconda strada.
Tolleranza ai guasti. Anche se un server ti basterebbe in termini di potenza, due server ti permettono di riavviare, aggiornare o perdere una macchina senza andare offline. È il motivo per cui alcuni mettono un bilanciatore davanti a due server scarichi: non serve la capacità, serve non svegliarsi con il sito giù.
Se ti serve solo la seconda, esistono soluzioni più semplici — un secondo server in standby, o un provider con failover automatico — che costano meno complessità.
Gli algoritmi di distribuzione
Il bilanciatore deve decidere, richiesta per richiesta, dove mandarla. I criteri principali sono tre.
Round robin
Il più semplice: a turno. Prima richiesta al server 1, seconda al 2, terza al 3, quarta di nuovo al 1.
upstream backend {
server 10.0.0.11:3000;
server 10.0.0.12:3000;
server 10.0.0.13:3000;
}
Funziona bene se i server sono identici e le richieste costano più o meno uguale. Il limite è che conta le richieste, non il lavoro. Se una richiesta su venti è un'esportazione CSV che occupa il server per dieci secondi, il round robin non lo sa e continua a mandargli traffico.
Esiste la variante pesata, utile quando le macchine non sono uguali:
upstream backend {
server 10.0.0.11:3000 weight=3; # macchina più potente
server 10.0.0.12:3000 weight=1;
}
Least connections
Manda la richiesta al server che in quel momento ha meno connessioni attive.
upstream backend {
least_conn;
server 10.0.0.11:3000;
server 10.0.0.12:3000;
}
È quasi sempre la scelta migliore quando le richieste hanno durate molto diverse. Il server occupato dall'esportazione CSV ha una connessione aperta in più, e riceve automaticamente meno traffico nuovo. Costa un pelo in più al bilanciatore, che deve tenere il conto, ma è irrilevante.
IP hash
Calcola una funzione hash sull'indirizzo IP di chi chiama e usa il risultato per scegliere il server. Lo stesso IP finisce sempre sullo stesso server.
upstream backend {
ip_hash;
server 10.0.0.11:3000;
server 10.0.0.12:3000;
}
Serve a risolvere il problema delle sessioni, di cui tra poco. Ha due difetti seri: distribuisce male quando molti utenti condividono lo stesso IP (uffici, reti mobili, CDN o proxy davanti), e se togli o aggiungi un server la mappatura cambia per tutti, non solo per quelli del server rimosso.
| Algoritmo | Quando usarlo | Attenzione |
|---|---|---|
| Round robin | Server uguali, richieste simili | Ignora quanto è occupato un server |
| Round robin pesato | Macchine di potenza diversa | I pesi vanno rivisti a mano |
| Least connections | Richieste di durata variabile | Praticamente sempre una buona scelta |
| IP hash | Serve continuità di sessione | Distribuisce male dietro NAT o proxy |
Gli health check
Un bilanciatore senza controlli di salute è peggio di nessun bilanciatore. Se il server 2 muore e il bilanciatore non se ne accorge, un terzo dei tuoi utenti riceve errori — e con il round robin li riceve a rotazione, quindi il sito sembra "rotto a intermittenza", che è il tipo di guasto più difficile da diagnosticare.
Ci sono due modi di controllare:
- Passivo: il bilanciatore osserva le risposte reali. Se un server sbaglia N richieste di fila, viene messo da parte per un po'. È quello che fa Nginx nella versione libera.
- Attivo: il bilanciatore chiama periodicamente un endpoint dedicato, indipendentemente dal traffico. Lo fanno HAProxy, Traefik, i bilanciatori dei provider cloud e Nginx Plus.
upstream backend {
least_conn;
server 10.0.0.11:3000 max_fails=3 fail_timeout=30s;
server 10.0.0.12:3000 max_fails=3 fail_timeout=30s;
}
Fai in modo che l'endpoint di controllo sia significativo. Un /health che restituisce sempre 200 OK senza verificare niente dichiara "sto bene" anche quando l'applicazione ha perso la connessione al database. Ma non esagerare nell'altro senso: se /health interroga il database e il database rallenta, il bilanciatore toglie tutti i server insieme e il sito va giù del tutto invece che a metà.
// Un compromesso ragionevole
app.get('/health', (req, res) => {
res.status(200).send('ok'); // il processo risponde
});
app.get('/health/deep', async (req, res) => {
// usato dal monitoraggio, non dal bilanciatore
await db.query('SELECT 1');
res.status(200).send('ok');
});
Il problema delle sessioni sticky
È il punto dove quasi tutti si scontrano con la realtà.
Un utente fa il login sul server 1. Il server 1 tiene la sessione in memoria. La richiesta successiva, per via del round robin, finisce sul server 2 — che non sa niente di quell'utente e lo rimanda alla pagina di login. Risultato: gli utenti vengono buttati fuori a caso.
La soluzione immediata è la sessione sticky: legare ogni utente a un server, con IP hash o con un cookie posto dal bilanciatore.
upstream backend {
ip_hash;
server 10.0.0.11:3000;
server 10.0.0.12:3000;
}
Funziona, ed è un cerotto. I problemi che ti porti dietro sono tre: se il server a cui un utente è legato cade, quell'utente perde comunque la sessione; il carico si distribuisce peggio; e non puoi riavviare un server per aggiornarlo senza scollegare i suoi utenti.
La soluzione vera è togliere lo stato dai server applicativi. Metti le sessioni in un archivio condiviso — Redis è la scelta standard — e a quel punto ogni server è intercambiabile:
// express-session con Redis: qualunque server può servire qualunque utente
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
}));
Lo stesso vale per i file caricati dagli utenti: se finiscono sul disco locale del server 1, il server 2 non li vede. Vanno su uno storage condiviso o su un object storage.
Usa le sessioni sticky come misura temporanea mentre sistemi lo stato condiviso, non come architettura definitiva.
Layer 4 o layer 7
Due modi di lavorare, con capacità diverse.
Layer 4 (trasporto). Il bilanciatore guarda solo indirizzi IP e porte. Non apre il contenuto della connessione, la inoltra e basta. È velocissimo, consuma pochissimo, e funziona con qualunque protocollo — non solo HTTP, ma anche database, code, connessioni TCP generiche.
Layer 7 (applicativo). Il bilanciatore legge la richiesta HTTP: dominio, percorso, intestazioni, cookie. Può quindi decidere in base al contenuto:
location /api/ {
proxy_pass http://api_backend;
}
location /immagini/ {
proxy_pass http://static_backend;
}
Con il layer 7 puoi terminare HTTPS nel bilanciatore, instradare per percorso o dominio, riscrivere le intestazioni, fare caching e sessioni sticky basate su cookie. Costa qualche microsecondo in più per richiesta, che nella pratica non noti.
| Layer 4 | Layer 7 | |
|---|---|---|
| Cosa vede | IP e porte | La richiesta HTTP completa |
| Protocolli | Qualunque TCP/UDP | HTTP/HTTPS |
| Instradamento per percorso | No | Sì |
| Terminazione TLS | No (passa attraverso) | Sì |
| Costo | Minimo | Leggermente superiore |
| Quando | Database, TCP puro, traffico enorme | Quasi sempre, per il web |
Per un sito o un'API, layer 7. Il layer 4 serve quando bilanci qualcosa che non è HTTP, o quando i volumi sono tali da rendere significativo anche quel margine.
Quando NON ti serve
Questa è la sezione che le guide saltano, ed è quella che ti fa risparmiare più tempo.
Verticale prima di orizzontale. Raddoppiare la RAM e i core di un VPS è un'operazione di dieci minuti e un clic. Passare a due server significa gestire lo stato condiviso, sincronizzare le versioni deployate, moltiplicare i log, aggiungere un pezzo che può guastarsi. Una macchina moderna da 8 core regge molto più traffico di quanto la maggior parte dei siti veda mai. Sali di taglia finché puoi permettertelo: è quasi sempre più economico dell'ora di lavoro che ti costa la complessità.
Prima verifica se il problema è davvero la capacità. Nove volte su dieci il server non è saturo, è bloccato: query senza indici, chiamate esterne senza timeout, assenza di caching. Aggiungere un secondo server a un'applicazione con una query lenta ti dà due server lenti. Prima misura — il tema è in come velocizzare un sito web.
Se il problema sono i file statici e la latenza geografica, ti serve una CDN, non un bilanciatore. Costa meno, si attiva in un pomeriggio e toglie dal server la maggior parte delle richieste.
Se ti serve solo protezione dai picchi anomali, guarda a Cloudflare e alla gestione degli attacchi DDoS: un bilanciatore non ti difende da un attacco, distribuisce solo il danno.
Attenzione al bilanciatore come singolo punto di guasto. Hai messo tre server per non andare offline, e ora tutto passa da una macchina sola. I bilanciatori gestiti dei provider cloud sono già ridondati; se te lo fai in casa con un Nginx su un VPS, hai spostato il problema, non risolto.
In sintesi
Il load balancing distribuisce le richieste su più server, per aumentare la capacità e per non andare offline quando una macchina cade.
L'algoritmo predefinito ragionevole è least connections. Round robin va bene se i server sono uguali e le richieste simili; IP hash risolve le sessioni ma distribuisce male.
Gli health check non sono opzionali, e devono controllare qualcosa di vero senza però mettere tutti i server fuori uso appena il database rallenta.
Le sessioni sticky sono un cerotto: la soluzione è rendere i server senza stato, con sessioni e file su archivi condivisi.
Per il web, layer 7. Il layer 4 serve per protocolli diversi da HTTP.
E prima di tutto: prova a crescere in verticale. Un server più grande è quasi sempre la mossa giusta prima di passare a due.
Il pezzo che sta sotto a tutto questo è il reverse proxy, e il server con cui lo realizzi lo scegli in Nginx o Apache. Se stai andando verso più macchine gestite in modo automatico, il passo successivo è Kubernetes.