Alta disponibilità sui provider europei
Un sistema ad alta disponibilità sui provider europei (OVH, Scaleway e Hetzner) con AWS come riferimento. Parte 1: il cluster.
Perché questa prova#
Sviluppando Flui, una piattaforma open source per il deploy e la gestione del ciclo di vita delle applicazioni sui propri server o sui provider europei, mi sono chiesto che soluzioni adottare per l’alta disponibilità. In particolare, il tipo di resilienza che cerco di testare è la caduta totale di un datacenter.
La risposta da manuale è semplice: tre zone di disponibilità, un cluster Kubernetes distribuito sopra, il database replicato. Volevo però sapere cosa succede davvero quando una zona viene a mancare, e lo volevo misurare sui provider europei: si tratta di funzionalità date per scontate sugli hyperscaler ma che celano una certa complessità. Le motivazioni per cui adottare questi provider possono essere diverse: la spinta di normative come DORA (e il CADA, in discussione), una questione di sensibilità sulla sovranità dei dati, oppure ragioni banalmente economiche. In questo articolo ci soffermiamo sull’aspetto tecnologico.
Così ho costruito lo stesso cluster K3s (Distribuzione leggera di Kubernetes) su OVHcloud e Scaleway a Milano, su Hetzner in tre città, e su AWS, sempre a Milano. Stessi script, stesse prove, tre ripetizioni per ogni guasto.
Una premessa su AWS. È la mia asticella di riferimento: ci ho lavorato per anni e so cosa mi dà. Questo non è un benchmark alla pari, perché le condizioni sono diverse: macchine non identiche, vCPU di tipo diverso, dischi diversi. Ma è quello che offre il mercato, e il confronto va fatto, costo compreso. Ogni servizio ha le sue peculiarità, e ognuno sceglie quello che gli serve. Io metto i numeri uno accanto all’altro e per ogni differenza dico da cosa dipende. Hetzner lo tratto a parte, perché non ha zone di disponibilità ma dei datacenter collocati in diverse città e il confronto in questo caso non sarebbe stato sensato.
Questa è la prima parte, sul cluster. Database e volumi e altri aspetti che sono degni di essere raccontati seguiranno.
In breve#
Cosa ho costruito#
Il cluster usa K3s: sta in un solo eseguibile e ha già dentro tutto quello che serve, quindi è adatta a cluster piccoli come questo.
Tre macchine, una per zona, ognuna con un nodo che fa sia da control plane sia da worker. Nessun servizio gestito: niente EKS, niente database gestiti. K3s è installato con le impostazioni di default; l’unico cambio mio è il DNS interno in tre copie, e più avanti spiego perché. Gli script sono pubblici e ogni giro costa pochi euro.
Kubernetes tiene tutto lo stato del cluster in un piccolo database, etcd: quali app girano, dove, con quali impostazioni. Qui ne gira una copia per zona, e le tre copie si mettono d’accordo con un meccanismo che si chiama Raft. In breve funziona così:
- una sola copia fa da leader: riceve le scritture e le passa alle altre due;
- una scrittura è confermata quando ce l’ha la maggioranza, cioè due copie su tre. Per questo tre copie reggono la perdita di una, ma non di due;
- il leader manda di continuo un segnale di vita alle altre copie. Se smettono di sentirlo per qualche secondo, eleggono un nuovo leader, e anche qui serve la maggioranza dei voti;
- finché il nuovo leader non c’è, lo stato del cluster non si può modificare. Le app che girano già continuano a rispondere.
Ho scelto la macchina da 2 vCPU e 4 GB che ogni provider vende in tutte e tre le zone di Milano: sufficiente per fare i nostri esperimenti con costi abbordabili.
| AWS | OVHcloud | Scaleway | |
|---|---|---|---|
| Dove | Milano, 3 zone | Milano, 3 zone | Milano, 3 zone |
| Macchina | c7i.large | c3-4 | BASIC3-X2C-4G |
| CPU | Intel Xeon 8488C | AMD EPYC Milan | AMD EPYC 9555P |
| vCPU / RAM | 2 / 4 GB | 2 / 4 GB | 2 / 4 GB |
| Tipo di vCPU | 1 core con 2 thread, non condivise | risorse garantite, dichiara OVH | condivise |
| Steal della CPU misurato | sotto lo 0,2% | sotto lo 0,1% | da circa 1% a 13%, secondo il nodo |
| Disco | di rete gp3, 5.000 IOPS di cui 2.000 a pagamento | locale NVMe, non replicato | di rete, 5.000 IOPS, replicato in 3 copie |
CPU. Su AWS le 2 vCPU sono un core fisico con due thread, non condivisi con altri clienti (AWS). OVH dichiara risorse garantite per la famiglia di cui fa parte la c3 (OVHcloud). Scaleway BASIC3 ha vCPU condivise (Scaleway), e a Milano le dedicate non ci sono in tutte e tre le zone: per un cluster a tre zone la condivisa è l’unica scelta. Questo il motivo della scelta.
Steal. Ho misurato lo steal su tutti e tre, durante gli stessi test. Lo steal è il tempo in cui la vCPU vorrebbe lavorare, ma il server fisico sta dando il core a un’altra macchina. Su AWS e OVH è praticamente zero. Su Scaleway va da circa l’1% al 13%, secondo il nodo e il lavoro che fa.
Dischi. OVH usa un NVMe locale, che il listino chiama ephemeral: non è replicato (listino). Per etcd va bene, perché tiene già tre copie dei dati, una per zona; per un database è un altro discorso, e lo vedremo in un esperimento dedicato. Scaleway usa un disco di rete replicato in tre copie (Scaleway). AWS usa gp3, che include 3.000 IOPS (AWS): per parità di condizione con Scaleway che ha dischi da 5.000 ho alzato la soglia di 2.000 pagando la differenza.
Le zone di Milano: rete, disco e scritture#
Una premessa su questa parte. Le differenze di prestazioni che seguono pesano con volumi alti o con database sotto stress, con un uso normale non le sentirete. Il comportamento nei guasti, riguarda invece tutti.
Tutti e tre i provider descrivono le loro zone come data center separati: AWS, OVHcloud (e la pagina di Milano), Scaleway. Su distanze e indipendenza i dettagli sono spesso generici, non specifici per Milano, quindi mi limito a quello che ho misurato.
Rete#
Il primo pannello conta per etcd: ogni scrittura aspetta la risposta di un’altra zona, quindi la latenza tra zone entra in ogni scrittura. Qui sta tra 0,5 e 1,4 ms su tutti e tre, e nessuno si distingue.
Il secondo pannello conta quando si spostano tanti dati, per esempio repliche di database e backup. OVH fa tra 3,7 e 4,1 Gbit/s; Scaleway 0,36, che è il limite dichiarato per questa taglia, non della sua rete. AWS fa 4,96 Gbit/s su tutte e tre le coppie, ma va letto bene: è il tetto di 5 Gbit/s che AWS mette a un singolo flusso (AWS). La banda garantita di questa macchina è 0,78 Gbit/s, con picchi fino a 12,5 che di solito durano tra 5 e 60 minuti (AWS). I miei dieci minuti di prova stanno dentro il picco, quindi non dicono quanto regge un trasferimento lungo.
Su AWS il traffico tra zone si paga, 0,01 $ per GB in ogni direzione, circa 0,9 centesimi di euro (listino). OVH non fa pagare il traffico sulla rete privata (OVHcloud).
Disco#
È la misura che conta di più per etcd, perché etcd non conferma una scrittura finché il disco non dice di averla salvata. OVH fa 0,34 ms, con il p99 a 0,43: NVMe locale, e si vede. AWS sta sui 3 ms, e anche le scritture più lente restano lì vicino, p99 a 3,4: è il disco di rete più regolare che ho misurato. Scaleway nel caso tipico è più veloce di AWS, 1,1 ms, ma una scrittura su cento arriva a 6-9 ms, e vista da etcd arriva a 12 ms.
Questi numeri però non dicono che un disco è migliore dell’altro: sono due cose diverse. OVH usa un disco locale, attaccato al server fisico: velocissimo, ma il listino lo chiama ephemeral, non è replicato, e se il server si rompe i dati su quel disco non sono garantiti. Scaleway e AWS usano dischi di rete, che pagano qualcosa in velocità ma danno altro:
- i dati sopravvivono alla macchina: se il server fisico si rompe, il disco si riattacca a una macchina nuova nella stessa zona;
- sono replicati: Scaleway tiene tre copie di ogni dato (Scaleway), e AWS replica i dati di ogni disco su più server della stessa zona (AWS);
- si staccano e si spostano: si possono riattaccare a un’altra macchina, ingrandire o salvare con uno snapshot, senza toccare il server;
Per etcd, che tiene già tre copie su tre zone, il disco locale veloce è la scelta ideale. Per un database in una sola copia, o per i dati di un’app, il disco di rete può salvarti quando muore la macchina. Dipende da cosa ci metti sopra. OVH vende anche dischi di rete a parte, che qui non ho misurato. E nessuno dei due sopravvive alla perdita dell’intera zona.
Scritture su Kubernetes#
Qui si vede tutto insieme. Una scrittura costa 2,8-3,6 ms su OVH, 3,4-5,0 su AWS e 4,9-6,3 su Scaleway. Il p99 sta sotto i 9 ms su AWS e OVH. Su Scaleway sta a 18-19 ms, tranne in una prova, quella con il leader in it-mil-1, dove arriva a 97 ms. Non dipende dalla zona: in un altro giro la stessa prova si è fermata a 16 ms. In quel minuto le scritture lente sono state solo più frequenti.
Il dettaglio interessante è il contrasto con il grafico del disco: nel caso tipico Scaleway ha il disco più veloce di AWS, ma Kubernetes ci mette di più a confermare una scrittura. Quindi il disco da solo non lo spiega.
Per capire da dove vengono le scritture lente di Scaleway ho fatto un giro in più, solo lì, misurando ogni 100 ms lo steal e il disco. Il risultato: sono tutti e due, con ruoli diversi. Quando il nodo che riceve le scritture deve aspettare la CPU, perché il server fisico la sta dando a un’altra macchina, la probabilità di una scrittura lenta sale dallo 0,35% a circa il 9%. Le punte sopra i 100 ms vengono invece dal disco di rete, che ogni tanto si ferma per decine di millisecondi. Quando le due cose capitano insieme, più di una scrittura su quattro è lenta. Conta il nodo che riceve le scritture, molto meno quello che fa da leader; e con il traffico dell’app di prova in più le scritture lente aumentavano di circa la metà. Con macchine piccole e condivise conviene quindi non caricare quel nodo di altro lavoro.
Come ho simulato la perdita di una zona#
Non ho spento niente: ho isolato una zona dalla rete con nftables. I suoi nodi restano accesi, ma non parlano con nessuno e nessuno parla con loro.
Perché basta a simulare la perdita di una zona: per il resto del cluster, una zona spenta e una zona irraggiungibile sono la stessa cosa. In tutti e due i casi arriva solo silenzio, e né etcd né Kubernetes possono distinguerli. Anzi, l’isolamento è il caso più difficile, perché i nodi isolati sono vivi e continuano a provare a lavorare.
Cosa non simula: i servizi del provider in quella zona, cioè dischi di rete, API e load balancer, restano su. E non provo i guasti parziali, una zona lenta o che perde pacchetti, che sono un altro tipo di guasto. Durante l’isolamento resta aperta solo la porta SSH verso il mio indirizzo, per poter intervenire: il cluster non la usa, e la regola si toglie da sola alla scadenza.
Come secondo guasto ho fatto un crash: il nodo del leader si riavvia di colpo, senza scrivere niente su disco. È un server che si spegne all’improvviso.
Durante il guasto una sonda scrive senza sosta sull’API di Kubernetes da una zona che non tocco, con un timeout di 2 secondi per scrittura, e misura quanto restano ferme le scritture. Misura solo le scritture.
Intanto gira un’app di prova: un piccolo server web con tre repliche, una per zona, raggiungibili da un unico indirizzo interno. Da un nodo sano un client le chiama di continuo, una richiesta alla volta, e dà per fallita quella che non risponde entro 2 secondi. La regola di una replica per zona l’ho messa io, e più avanti conta: è il motivo per cui la replica persa non riparte in un’altra zona.
La zona resta isolata per 7 minuti, perché Kubernetes rinuncia a un nodo perso dopo circa 6 e volevo vederlo succedere. Ogni guasto l’ho ripetuto tre volte per provider. Tutto il resto di Kubernetes mantiene le configurazione di default, per simulare quello che trova chi installa K3s oggi.
Il cluster sopravvive, e i tempi sono uguali per tutti#
Su tutti e tre i provider di Milano, e anche su Hetzner, il cluster perde una zona e continua a funzionare, tre prove su tre. Il grafico mostra tre prove diverse, dall’alto in basso:
- leader zone isolated: isolo la zona dove sta il leader. Le altre due copie di etcd non lo sentono più, aspettano e ne eleggono un altro.
- leader crash: il nodo del leader si riavvia di colpo. Anche qui le copie rimaste devono eleggere un nuovo leader.
- zone without leader: isolo una zona dove il leader non c’è. Il leader ha ancora la maggioranza, due copie su tre, e continua a lavorare: nessuna elezione. Le pause stanno tra 0,03 e 0,07 secondi, per questo i punti sono tutti vicino allo zero.
Quando serve un’elezione, Kubernetes non accetta scritture per 6-10 secondi, poi riparte da solo, ogni volta con un solo cambio di leader. È il tempo che etcd aspetta prima di considerare perso il leader ed eleggerne un altro. K3s lo imposta più lungo del normale, per tollerare reti lente, ed etcd lo varia un po’ a ogni elezione, apposta. Per questo lo stesso provider dà 6 secondi in una prova e 10 in quella dopo. I valori sono sempre 6,1, 8,1 o 10,1 perché la mia sonda riprova ogni 2 secondi.
Non c’è quindi una classifica da fare. Su 30 guasti, su tutti i provider, Hetzner compreso, le pause si dividono come prevede il caso: 13, 13 e 4 volte, contro le 15, 12 e 3 attese. I dettagli sono nel repository.
Questi tempi si possono abbassare, ma con reti più lente aumentano le elezioni a vuoto. Ho lasciato il default, perché è quello che trova chi installa K3s. E le vCPU condivise di Scaleway qui non c’entrano: i tempi li decide etcd, non la macchina.
Cosa vede la tua app#
Il cluster che sopravvive è una buona notizia per chi lo gestisce. Chi ci fa girare un’app vuole sapere un’altra cosa: cosa vedono gli utenti.
Kubernetes ha due tempi. Ci mette circa un minuto a smettere di mandare traffico a una zona persa, e circa 6 minuti a ricostruire altrove quello che c’era. Un’app con tre repliche sente solo il primo. Un componente che esiste in una copia sola sente il secondo.
Le repliche nelle zone sane continuano a rispondere. Ma finché Kubernetes non segna il nodo perso, tra 43 e 78 secondi dopo l’isolamento, continua a mandare richieste anche alla replica persa. Nelle mie prove in quella finestra è fallita tra un quarto e la metà delle richieste, circa una su tre, e ognuna solo dopo aver aspettato i 2 secondi del timeout. Non è l’app che va giù, ma per quel minuto una richiesta su tre resta appesa e poi fallisce. Non l’ho misurato, ma il rimedio più semplice sta nel client: un timeout breve e un nuovo tentativo, che con buona probabilità finisce su una replica sana.
Dopo circa 6 minuti, tra 344 e 379 secondi, Kubernetes rinuncia al nodo e cancella la replica persa. Ma non la ricrea altrove: ho imposto una replica per zona, quindi aspetta che la zona torni e rinasce lì. Al ritorno della zona tutto si riallinea entro un minuto e mezzo.
Cosa K3s installa in una copia sola#
C’è una cosa che avevo dato per scontato: che un cluster su tre zone fosse in alta disponibilità in tutte le sue parti. Non è così: K3s installa tre componenti in una copia sola (CoreDNS, Traefik e metrics-server). Se la zona persa è quella dove girano, il cluster regge ma loro no, finché Kubernetes non li ricrea altrove, dopo quei 6 minuti, o finché la zona non torna.
| Componente | Cosa smette di funzionare | Per quanto |
|---|---|---|
| Traefik, l’ingress | l’app con tre repliche non è raggiungibile da fuori | 352-376 s a Milano, fino a 379 s su Hetzner; controllato dall’interno del cluster, da un nodo sano |
| metrics-server | kubectl top, e l’autoscaling dei pod resta senza dati | 395-409 s a Milano, da 372 s su Hetzner |
| CoreDNS, il DNS interno | niente nomi nel cluster: tutto ciò che si ricollega per nome aspetta | fino al ritorno della zona o alla ricreazione altrove; misurato nei giri precedenti, prima della correzione |
In pratica: tre repliche in tre zone non servono a niente se l’unico ingress sta nella zona persa. L’app è viva, ma nessuno la raggiunge da fuori per 6 minuti.
Il problema del DNS è venuto fuori nelle prove sui database: quando si perdeva la zona con il DNS, un database replicato restava fermo fino al ritorno della zona, invece di ripartire in poco più di un minuto. Ci tornerò quando parlerò dei database. Per questo in tutte le prove di questo articolo CoreDNS gira in tre copie.
L’alta disponibilità va controllata anche sui componenti che non hai installato tu. Una zona può diventare più importante delle altre senza che nessuno l’abbia deciso.
Hetzner: tre città non sono tre zone#
Hetzner non ha zone di disponibilità dentro una sede, e non ha annunciato piani per averle da quello che ho potuto reperire (Locations, What’s New). Per avere tre “zone” ho usato le sue tre sedi europee: Falkenstein e Norimberga in Germania, Helsinki in Finlandia. In Europa non ce ne sono altre, quindi non c’era una scelta più vicina. Il confronto con Milano non è alla pari, e per questo Hetzner sta a parte.
Due macchine: cpx22, 2 vCPU condivise e 4 GB, la stessa taglia di Milano; e ccx13, 2 vCPU dedicate e 4 GB, come controllo. Perché la cpx22 e non la CX23, che costa meno: le serie Cost-Optimized sono disponibili in numero limitato, durante le mie prove la CX23 risultava non disponibile, anche se avessi aspettato la disponibilità avrei reso molto difficile riprodurre l’esperimento vista la frequenza con cui risulta disponibile su tutte e tre le zone. La serie CPX sembrerebbe l’offerta standard di Hetzner, su hardware di nuova generazione (Hetzner).
Il cluster sopravvive come sugli altri, con gli stessi 6-10 secondi, e anche l’app si comporta come a Milano: rotazione corretta in 44-73 secondi, replica persa tolta dopo circa 6 minuti. Le vCPU dedicate non cambiano i tempi del guasto, perché li decide etcd.
La macchina si compra, la distanza no. Nelle due sedi tedesche la ccx13 ha il disco circa quattro volte più veloce della cpx22: 0,27 ms contro 1,1-1,2. Anche le scritture più lente su Kubernetes, il p99, scendono a un terzo: 7-9 ms invece di 21-23. A Helsinki invece il disco della ccx13 va come quello della cpx22, circa 1 ms. Ma con il leader a Helsinki ogni scrittura costa 63 ms, su tutte e due le macchine, da 6 a 12 volte il normale.
Il motivo è la maggioranza. Una scrittura è confermata quando ce l’hanno due copie su tre. Con il leader in Germania basta l’altro nodo tedesco, a 3 ms. Con il leader a Helsinki serve per forza un nodo in Germania, a 24-36 ms. Dopo un guasto o un riavvio il leader può finire a Helsinki, e non c’è modo di impedirlo: si può solo spostarlo a mano.
Su Hetzner, poi, una prova sola non basta. Tre cluster cpx22 creati lo stesso giorno hanno dato una banda tra Falkenstein e Norimberga di 4,9, 0,26 e 1,6 Gbit/s: ogni creazione pesca server fisici diversi.
Prezzi di listino per un nuovo ordine, al 30/10/2026, IVA esclusa: cpx22 19,49 € e ccx13 42,99 € al mese, più 0,50 € di IPv4 per server (fonte). Da giugno 2026 valgono per i nuovi ordini: chi ha server attivati prima può pagare meno. Il traffico interno non si paga (Hetzner). Con la cpx22 il cluster costa circa 60 € al mese, circa la metà degli altri. Con la ccx13 circa 130 €, più o meno come OVH a Milano: “Hetzner costa la metà” vale solo con le vCPU condivise.
Quanto costa avere tre zone#
Il costo vero di tre zone è il prezzo delle macchine: la latenza tra zone aggiunge solo pochi millisecondi alle scritture.
| Cluster di 3 nodi | Al mese |
|---|---|
| AWS, c7i.large | circa 259 € |
| OVHcloud, c3-4 | circa 122 € |
| Scaleway, BASIC3-X2C-4G | circa 109 € |
Prezzi di listino on-demand al 30/10/2026, IVA esclusa, con disco e IPv4 compresi. AWS è convertito dai dollari a euro. Su OVH a Milano il disco locale e l’IP si pagano a parte rispetto alla macchina, e sono compresi nel totale.
AWS costa poco più del doppio: circa 2,1 volte OVH e 2,4 volte Scaleway. In più su AWS si paga il traffico tra zone, che su OVH è incluso.
Cosa si compra con la differenza su AWS. Il disco di rete più regolare che ho misurato. Le vCPU non condivise. E tutto quello che non ho usato: EKS, RDS, i servizi gestiti. Chi li usa paga anche per non costruire a mano quello che c’è in questo articolo. La banda invece non la metterei tra i vantaggi sicuri: la parte garantita per questa macchina è 0,78 Gbit/s, e i miei 4,96 stanno dentro il picco.
Due avvertenze. Sono prezzi di listino, senza piani di risparmio. E un’altra famiglia di macchine su AWS, come t3 o Graviton, cambierebbe il rapporto.
Gli errori durante le misure#
Prima di pubblicare ho trovato e corretto tre errori.
- La sonda misurava anche la connessione. Apriva una connessione TLS nuova a ogni scrittura, e le scritture su Kubernetes risultavano 1,5-3 ms più lente su tutti i provider. Corretto e rifatto tutto.
- Il DNS in una copia sola, la storia raccontata sopra. Me ne sono accorto perché alcune prove duravano il doppio delle altre; ho tenuto solo i dati con tre copie.
Cosa non copre questo articolo#
- Tre ripetizioni per provider: mostrano il comportamento, non sono statistiche.
- La sonda misura a passi di circa 2 secondi, e solo le scritture: le letture durante l’elezione non le ho misurate.
- Non è il guasto vero di una zona: isolo i nodi, ma dischi di rete, API del provider e load balancer restano su. I guasti parziali, una zona lenta o che perde pacchetti, non li ho provati.
- Macchine piccole, nessun carico reale durante il guasto.
- Su AWS solo macchine: niente EKS né RDS.
- Fuori da questa parte: il database, i volumi delle app, e come arriva il traffico dall’esterno, cioè load balancer e DNS pubblico, che affronterò con esperimenti dedicati.
Conclusioni#
Dove un provider offre tre zone vicine, non c’è nessuna ragione per rinunciare all’alta disponibilità: tutto dipende dal software che ci si costruisce sopra. Su OVH e Scaleway a Milano il cluster ha retto la perdita di una zona in tutte le prove, esattamente come su AWS. Se nella zona persa c’è il leader, le scritture si fermano per 6-10 secondi, gli stessi su tutti i provider; altrimenti quasi non se ne accorgono.
Le differenze stanno altrove. Nel disco, nella CPU e nel prezzo, che vanno scelti sapendo cosa si compra. Su Hetzner, dove le zone sono tre città e, con il leader a Helsinki, la distanza si paga a ogni scrittura. E nei componenti che K3s installa in una copia sola, che rendono una zona qualsiasi quella che non puoi perdere.
Per Flui questo sposta il problema. Tre zone sui provider europei sono un’opzione reale, e il cluster regge da solo. L’alta disponibilità si gioca su quello che gli sta sopra: i componenti di sistema, che devono avere una copia in ogni zona, e poi i database e i volumi, che devono sopravvivere alla zona persa. È questo il lavoro che Flui deve fare al posto di chi lo usa.
Il prossimo passo è proprio il database: Postgres su tre zone, quanto costa non perdere un commit e quanto resta fermo.
Gli script e i dati sono nel repository dawit-io/ha-eu-bench, con i dati scrittura per scrittura di ogni prova e la fonte di ogni numero dichiarato.
Flui è su flui.cloud e il codice è su GitHub.