Quando HAProxy è il punto giusto per bilanciare una server farm
Se hai più nodi web dietro un front-end unico, HAProxy è spesso la scelta più lineare per distribuire le connessioni senza reinventare la ruota. Il punto non è “mettere un bilanciatore davanti” e basta: il punto è decidere come distribuire, cosa considerare sano e quando togliere un nodo dal giro. In una server farm fatta bene, il bilanciamento non maschera i problemi: li isola, li rende visibili e limita il danno.
In pratica, HAProxy sta tra client e backend e prende decisioni su ogni nuova connessione o richiesta, a seconda del protocollo e della modalità usata. Se il traffico è HTTP, puoi ragionare su header, cookie, path, rate e health check applicativi. Se il traffico è TCP puro, ti sposti su criteri più bassi: disponibilità della porta, tempi di risposta e stabilità della sessione. La differenza sembra teorica, ma in produzione cambia tutto: un bilanciatore che “vede” solo la porta aperta può continuare a mandare traffico a un nodo che in realtà sta restituendo errori applicativi.
La decisione architetturale che evita metà dei guai
Prima di scrivere una riga di configurazione, conviene scegliere il modello. Per una farm web classica hai quasi sempre tre opzioni pratiche:
- Round robin: distribuzione semplice e prevedibile, adatta quando i nodi sono omogenei.
- Leastconn: utile quando le richieste hanno durata variabile e vuoi evitare che un nodo si riempia prima degli altri.
- Hash o stickiness: serve quando la sessione utente deve rimanere coerente sullo stesso backend, per esempio con applicazioni che non condividono lo stato.
La tentazione è usare stickiness ovunque. È una scorciatoia che spesso risolve un problema di sessioni locali, ma ne crea altri: sbilanciamento, recovery più lento, failover meno pulito. Se puoi, sposta lo stato fuori dal web tier: Redis, database, session store condiviso o token stateless. HAProxy deve distribuire traffico, non compensare un’architettura che conserva sessioni solo in RAM sul nodo.
Modalità HTTP e TCP: non sono dettagli da ignorare
HAProxy può lavorare in mode http o mode tcp. La scelta condiziona il tipo di osservabilità e le regole che puoi applicare. In HTTP hai visibilità su host, path, cookie, status code e header. Puoi fare redirect, riscritture, ACL più intelligenti e controlli più fini sui backend. In TCP, invece, sei più vicino al trasporto: ottimo per TLS passthrough, database, SMTP, o quando non vuoi terminare il protocollo sul bilanciatore.
Per una server farm web standard, terminare TLS su HAProxy è spesso utile: semplifica i certificati lato backend, abilita regole applicative e centralizza la gestione del cifrario. Ma non è sempre la scelta migliore. Se hai vincoli di compliance, end-to-end encryption o la necessità di far vedere al backend il traffico cifrato, puoi lasciare TLS in passthrough. In quel caso però perdi parte del controllo fine sul layer applicativo.
Health check: il vero filtro tra nodo vivo e nodo utile
Il controllo di salute è il pezzo che fa la differenza tra un bilanciatore decorativo e uno che protegge davvero la disponibilità. Un backend può rispondere su porta 80, ma non essere in grado di servire correttamente l’app. Per questo conviene usare controlli che misurino qualcosa di significativo: un endpoint dedicato, una pagina di stato, una query minima al database se è sostenibile, oppure un check applicativo che verifichi dipendenze essenziali.
Il criterio non deve essere “il server risponde”, ma “il server risponde in modo utile”. Se il check è troppo pesante, però, rischi di trasformarlo in un carico inutile proprio quando il nodo è sotto stress. La regola pratica è semplice: il check deve essere abbastanza profondo da scoprire il guasto che ti interessa, ma abbastanza leggero da non peggiorare la situazione.
Un esempio tipico: per un sito PHP dietro Nginx o Apache, un endpoint /health che restituisce 200 solo se il web server e la connessione al backend critico sono OK. Se il sito ha problemi di database, quel check deve fallire e il nodo deve uscire dalla rotazione. Se invece il DB è solo uno dei servizi secondari, puoi separare il controllo “liveness” da quello “readiness”, così eviti di togliere un nodo per un malfunzionamento non bloccante.
Configurazione essenziale: il minimo che serve davvero
Una configurazione base, leggibile e manutenibile, batte sempre una configurazione “furba” ma opaca. Il pattern più sano è separare frontend, backend e controlli, con timeout espliciti e log utili. Ecco uno scheletro realistico per una farm HTTP semplice:
global
log /dev/log local0
log /dev/log local1 notice
maxconn 20000
user haproxy
group haproxy
daemon
defaults
log global
mode http
option httplog
option dontlognull
timeout connect 5s
timeout client 30s
timeout server 30s
timeout http-request 10s
frontend fe_http
bind *:80
default_backend be_web
backend be_web
balance leastconn
option httpchk GET /health
http-check expect status 200
server web1 10.0.0.11:80 check
server web2 10.0.0.12:80 check
server web3 10.0.0.13:80 check
Ci sono tre scelte da notare. Primo: timeout espliciti, perché i default impliciti sono spesso troppo permissivi o troppo stretti. Secondo: leastconn, che in molti casi distribuisce meglio di round robin quando le richieste non sono uniformi. Terzo: httpchk, che verifica un endpoint controllato, non solo la porta aperta.
Se vuoi fare terminazione TLS su HAProxy, il frontend cambia poco come concetto, ma aggiungi certificato e policy TLS. In quel caso il bilanciatore diventa anche punto di gestione della cifratura, quindi il blast radius di una modifica ai certificati o ai parametri TLS è più ampio: tocca direttamente la capacità di servire traffico pubblico.
Distribuzione delle connessioni: round robin, leastconn e pesi
Round robin è facile da capire, ma non sempre è il più giusto. Se i backend sono identici e le richieste hanno durata simile, funziona bene. Se invece una parte delle richieste è lenta e un’altra è veloce, round robin può mandare in saturazione un nodo mentre un altro resta sottoutilizzato. In quel caso, leastconn tende a essere più robusto perché guarda al numero di connessioni attive, non solo al turno.
I pesi servono quando i nodi non sono uguali. Puoi assegnare più traffico a server più capienti o più vicini al carico atteso. È una soluzione pratica durante una migrazione graduale, oppure quando una parte della farm è su hardware diverso. Però attenzione: i pesi non risolvono problemi di saturazione applicativa. Se un nodo ha CPU più alta ma il database è lo stesso collo di bottiglia per tutti, aumentare il peso può solo spostare il punto di rottura.
Un dettaglio che in molti trascurano è il comportamento in caso di degradazione parziale. Se hai un backend che risponde ma con latenza alta, i check base non lo escludono. Per questo, oltre al successo/fallimento, conviene osservare metriche come p95 latency, error rate e backlog delle code. HAProxy può dirti molto, ma non può leggere la salute dell’app al posto tuo se l’app risponde comunque con lentezza cronica.
Stickiness: quando serve davvero e quando è un debito tecnico mascherato
La persistenza di sessione ha senso quando non puoi eliminare lo stato locale in tempi brevi. Per esempio, una vecchia applicazione che salva la sessione in memoria o un flusso utente che non tollera il cambio di nodo a metà transazione. In HAProxy puoi usare cookie di persistenza, hashing su IP o altre chiavi applicative. Ma ogni forma di stickiness riduce la libertà del bilanciatore: se un nodo diventa caldo, i client già “attaccati” continueranno a gravitarci sopra.
Il consiglio operativo è semplice: usa stickiness come misura temporanea o mirata, non come architettura di base. Se la tua farm cresce, la persistenza può diventare una trappola: rende i failover meno puliti, complica i deploy rolling e crea distribuzioni sbilanciate difficili da leggere. Quando possibile, preferisci sessioni condivise o applicazioni stateless. È meno comodo all’inizio, ma più pulito nel tempo.
Osservabilità: senza log e metriche stai guidando al buio
Un bilanciatore senza osservabilità ti dice poco oltre il “funziona/non funziona”. In produzione ti serve sapere almeno: quante richieste arrivano, quante falliscono, quali backend sono in salute, quanto tempo impiegano le connessioni e dove si accumula la coda. Il log HTTP di HAProxy è utile perché conserva status code, tempi e backend selezionato; con quei dati puoi capire se il problema è a monte o a valle.
Se hai un sistema di monitoring, esporta le metriche e guarda trend e non solo istantanee. Un backend che passa da 20 ms a 300 ms di latenza media prima di cadere ti sta già dando un segnale. Anche il numero di connessioni attive per server è un indicatore utile: se uno sale costantemente sopra gli altri, il bilanciamento o la performance applicativa stanno diverging. Non aspettare il 502 per accorgertene.
Per una verifica rapida, puoi usare:
sudo systemctl status haproxy
sudo journalctl -u haproxy --since "15 min ago"
ss -ltnp | grep haproxy
Il primo comando conferma che il servizio sia attivo, il secondo ti mostra errori recenti o reload falliti, il terzo verifica le porte effettivamente in ascolto. Se il servizio è up ma il traffico non passa, spesso il problema sta nei backend, nei firewall o nei controlli di salute.
Failover, manutenzione e rollout senza tagliare il ramo su cui sei seduto
Una server farm non si amministra solo in condizioni normali. Devi sapere come togliere un nodo dalla rotazione senza interrompere il servizio. HAProxy supporta la manutenzione in modo abbastanza pulito: puoi disabilitare un server, attendere che le connessioni esistenti finiscano e poi applicare patch o riavvii. Questo è il classico caso in cui il bilanciamento fa risparmiare tempo, ma solo se i timeout e la gestione delle connessioni sono stati pensati bene prima.
Nel rollout, la sequenza corretta è quasi sempre: aggiornare un backend alla volta, verificare health check e risposta applicativa, poi proseguire. Se fai un deploy simultaneo su tutti i nodi, il bilanciatore non può proteggerti da un bug introdotto nello stesso momento su tutta la farm. Qui il blast radius è evidente: più nodi tocchi insieme, più alta è la probabilità di trasformare un difetto locale in un outage globale.
Il rollback va tenuto semplice: versione precedente della configurazione, pacchetto precedente se necessario, e un modo rapido per riattivare i backend stabili. In altre parole, la via di ritorno deve essere più corta della via di andata. Se per tornare indietro devi ricostruire tutto a mano, il tuo bilanciatore è già un rischio operativo.
Errori tipici che vedo ripetersi
Il primo errore è confondere bilanciamento con alta disponibilità. HAProxy distribuisce il traffico, ma se il punto unico di ingresso è uno solo, hai comunque un single point of failure. Per questo in ambienti seri il layer di bilanciamento va ridondato con un IP virtuale, un cluster, un servizio cloud o un meccanismo equivalente.
Il secondo errore è usare check troppo banali. “Porta aperta” non significa “servizio sano”. Il terzo è non allineare timeout di HAProxy, web server e applicazione. Se il backend chiude dopo 60 secondi ma il proxy si aspetta una risposta in 300, il comportamento percepito dagli utenti sarà incoerente e difficile da diagnosticare. Il quarto è non testare il failover con traffico reale o almeno realistico: in laboratorio tutto sembra ok, poi in produzione emergono sessioni appese, keep-alive troppo lunghe o nodi che rientrano in rotazione troppo presto.
Una regola pratica per tenere la farm stabile
Se vuoi una sintesi operativa, tieni questa: bilancia il traffico in base a ciò che sai misurare, togli dalla rotazione ciò che non è affidabile e non usare HAProxy per coprire debiti architetturali permanenti. Una configurazione sobria, con health check sensati, timeout coerenti e metriche leggibili, vale più di una configurazione piena di eccezioni che nessuno vuole più toccare.
Quando la farm cresce, il valore vero non è “quanti server regge”, ma quanto rapidamente riesci a capire dove si sta rompendo e quanto poco traffico perdi mentre lo capisci. È lì che HAProxy fa la differenza: non solo distribuisce connessioni, ma ti dà una disciplina operativa per gestire il carico senza improvvisare.
