Torna agli insights
HAProxyKeepalivedFailover

Failover automatico con HAProxy e Keepalived: guida pratica

Come eliminare il single point of failure davanti a un servizio web: due bilanciatori, un IP virtuale e un failover che avviene in pochi secondi.

Giuseppe Pelligra5 min di lettura
Failover automatico con HAProxy e Keepalived: guida pratica

Capita spesso: due, tre server applicativi dietro un load balancer, e ci si sente coperti. Ma se il load balancer è uno solo, l'alta affidabilità è un'illusione. È lui il punto di guasto unico: quando si ferma, i server dietro, per quanto ridondati, diventano irraggiungibili. Questa guida mostra come chiudere quel buco con due strumenti collaudati, HAProxy e Keepalived, e con le configurazioni che si usano davvero.

Il problema: il bilanciatore come single point of failure

Un load balancer fa due cose: presenta un punto di ingresso unico e distribuisce il traffico sui backend. Finché è uno solo, però, eredita il difetto che doveva risolvere. La soluzione non è un bilanciatore più robusto: è un secondo bilanciatore, e un meccanismo che faccia passare il traffico da uno all'altro senza che il client se ne accorga.

L'architettura: due nodi, un IP virtuale

L'idea portante è l'IP virtuale (VIP). I client non si connettono all'indirizzo di un nodo specifico: si connettono al VIP. Il VIP, in ogni istante, è "posseduto" da uno solo dei due nodi, quello in stato MASTER. Se il MASTER cade, il nodo BACKUP rileva l'assenza e si prende il VIP. Da fuori, il servizio non si è mai spostato.

  • Nodo A e nodo B: ognuno con HAProxy (bilancia) e Keepalived (gestisce il VIP).
  • VIP (es. 192.0.2.10): l'indirizzo pubblico del servizio, su cui punta il DNS.
  • Backend: i server applicativi reali (10.0.0.11, 10.0.0.12...).

HAProxy si occupa della distribuzione, Keepalived del failover dell'indirizzo. Due ruoli, due strumenti.

HAProxy: il bilanciatore

La configurazione di HAProxy è identica sui due nodi. In /etc/haproxy/haproxy.cfg:

global
    log /dev/log local0
    maxconn 4096

defaults
    mode http
    log global
    option httplog
    timeout connect 5s
    timeout client 30s
    timeout server 30s

frontend web_in
    bind 192.0.2.10:443 ssl crt /etc/haproxy/certs/site.pem
    default_backend web_servers

backend web_servers
    balance roundrobin
    option httpchk GET /health
    http-check expect status 200
    server web1 10.0.0.11:80 check inter 2s fall 3 rise 2
    server web2 10.0.0.12:80 check inter 2s fall 3 rise 2

I punti che contano:

  • Il bind è sul VIP, non sull'indirizzo del nodo. Per permettere a HAProxy di mettersi in ascolto su un IP che il nodo non possiede ancora, serve net.ipv4.ip_nonlocal_bind=1 in /etc/sysctl.conf.
  • option httpchk GET /health con http-check expect status 200: HAProxy interroga attivamente un endpoint di salute su ogni backend.
  • check inter 2s fall 3 rise 2: controlla ogni 2 secondi; dopo 3 fallimenti toglie il backend dal giro, dopo 2 successi lo riammette. Questo è il primo livello di affidabilità, quello sui server applicativi.

Verifica sempre la sintassi prima di ricaricare: haproxy -c -f /etc/haproxy/haproxy.cfg.

Keepalived: il failover dell'IP

Keepalived implementa il protocollo VRRP: i due nodi si scambiano annunci, e quando il BACKUP smette di sentire il MASTER si promuove. In /etc/keepalived/keepalived.conf, sul nodo A (MASTER):

vrrp_script chk_haproxy {
    script "/usr/bin/killall -0 haproxy"
    interval 2
    weight -20
}

vrrp_instance VI_1 {
    state MASTER
    interface eth0
    virtual_router_id 51
    priority 110
    advert_int 1
    authentication {
        auth_type PASS
        auth_pass <password-condivisa>
    }
    virtual_ipaddress {
        192.0.2.10
    }
    track_script {
        chk_haproxy
    }
}

Sul nodo B (BACKUP) la configurazione è identica, con due differenze: state BACKUP e priority 100.

Il dettaglio che molti tralasciano è il vrrp_script. Senza, Keepalived sposta il VIP solo se l'intero nodo cade. Ma può succedere che il nodo sia vivo e sia HAProxy a essere morto: in quel caso il VIP resterebbe su un nodo che non bilancia più nulla. Lo script chk_haproxy controlla che il processo esista; se non c'è, applica weight -20 alla priorità. Il MASTER scende da 110 a 90, sotto il BACKUP a 100, e il VIP migra. Il failover segue la salute del servizio, non solo quella della macchina.

Testare il failover

Una configurazione di HA non verificata non è alta affidabilità: è una speranza ben documentata. Il test va fatto, e va fatto vedere.

Da un terzo host, tieni una richiesta in loop sul VIP:

while true; do curl -so /dev/null -w "%{http_code} %{time_total}s\n" \
  https://192.0.2.10/health; sleep 1; done

Poi, sul nodo MASTER, simula i due scenari:

  1. HAProxy muore: systemctl stop haproxy. Lo script chk_haproxy fallisce, la priorità scende, il VIP passa al nodo B. Sul loop di curl vedrai una manciata di richieste perse, poi la ripresa.
  2. Il nodo cade: stacca la rete del nodo A, o spegnilo. Il BACKUP smette di ricevere gli annunci VRRP e si promuove.

Controlla dove sta il VIP con ip addr show eth0: l'indirizzo 192.0.2.10 deve comparire sul nodo attivo e sparire dall'altro. Quando il nodo A torna su, con priorità più alta si riprende il VIP (comportamento preemptive): valuta se è ciò che vuoi o se preferisci nopreempt per evitare un secondo spostamento.

I limiti da conoscere

Questa architettura risolve l'alta affidabilità del livello di bilanciamento, ed è solida. Ma due punti vanno tenuti a mente.

Il primo: HAProxy e Keepalived non rendono affidabile ciò che sta dietro. Se i tuoi backend condividono un solo database, quel database resta il vero punto di guasto unico: l'alta affidabilità del dato è un problema a parte, che ho trattato per i database nell'articolo su MySQL senza downtime.

Il secondo: lo stato. Il round robin presuppone backend senza stato locale. Se l'applicazione tiene le sessioni in memoria sul singolo server, un failover le perde. La soluzione corretta non è la session persistence sul bilanciatore, ma spostare lo stato fuori dai backend, su uno store condiviso.

Chiuso questo, il risultato è concreto: un servizio web che sopravvive alla perdita di un intero nodo di bilanciamento con qualche secondo di disservizio, non con un'interruzione. E un failover che hai visto funzionare con i tuoi occhi, non che speri funzioni.