Pattern di osservabilità, configurazione e rilascio¶
Un sistema a microservizi è fatto di tanti processi che parlano fra loro in rete. Quando qualcosa va storto, nessun singolo servizio ha la visione completa. Questi pattern servono a vedere cosa succede dentro il sistema, a configurarlo senza ricostruirlo e a rilasciare nuove versioni senza fermare il negozio.
flowchart LR
subgraph vedere["🔍 Vedere cosa succede"]
la["📄 Log Aggregation"]
cid["🔗 Correlation ID"]
dt["⏱️ Distributed Tracing"]
gs["📊 Metrics e Golden Signals"]
end
subgraph base["🧱 Base comune"]
ec["⚙️ Externalized Configuration"]
st["📦 Service Template / Chassis"]
end
subgraph rilasciare["🚀 Rilasciare senza paura"]
ft["🔘 Feature Toggle"]
bg["🔵🟢 Blue/Green"]
cr["🐤 Canary Release"]
ru["🔄 Rolling Update"]
cdc["🤝 Contract Testing"]
end
base --> vedere
vedere --> rilasciare
| Pattern | Problema che risolve | Usalo quando |
|---|---|---|
| Log Aggregation | I log sono sparsi su decine di container e spariscono | Hai più di un servizio o più di una istanza |
| Correlation ID | Non riesci a collegare i log di una stessa richiesta | Una richiesta attraversa più servizi |
| Distributed Tracing | Non sai dove una richiesta perde tempo | Devi capire latenze e dipendenze fra servizi |
| Metrics e Golden Signals | Non sai se il sistema è sano prima che gli utenti si lamentino | Vuoi dashboard e allarmi utili |
| Externalized Configuration | Cambiare un parametro richiede una nuova immagine | Lo stesso servizio gira in più ambienti |
| Service Template / Chassis | Ogni servizio reinventa log, health, metriche | Hai più team che creano servizi |
| Feature Toggle | Rilasciare codice vuol dire attivarlo subito per tutti | Vuoi attivare funzioni gradualmente o spegnerle al volo |
| Blue/Green Deployment | Il rilascio ferma il servizio e il rollback è lento | Serve rollback istantaneo e puoi pagare doppia infrastruttura |
| Canary Release | Un bug in produzione colpisce tutti gli utenti insieme | Vuoi testare in produzione su pochi utenti |
| Rolling Update | Il rilascio ferma il servizio | Rilasci frequenti, versioni compatibili fra loro |
| Consumer-Driven Contract Testing | Un cambio di API rompe un consumer senza che nessuno se ne accorga | Più team rilasciano servizi in modo indipendente |
I tre pilastri¶
L'osservabilità si regge su tre tipi di dati. Ognuno risponde a una domanda diversa.
- Log: "Cosa è successo in questo punto preciso?" Sono eventi testuali con un timestamp. Utili per il dettaglio, pesanti da leggere in massa.
- Metriche: "Come sta il sistema adesso e nel tempo?" Sono numeri aggregati (contatori, medie, percentili). Leggere, perfette per dashboard e allarmi.
- Trace: "Che percorso ha fatto questa richiesta e dove ha perso tempo?" Collegano i passaggi di una richiesta attraverso i servizi.
I tre pilastri si completano: una metrica ti avvisa che la latenza è salita, una trace ti dice in quale servizio, un log ti dice perché.
flowchart LR
metriche["Metriche: la latenza e salita"] --> trace["Trace: il tempo si perde in Pagamenti"]
trace --> log["Log: timeout verso il gateway bancario"]
Log Aggregation¶
In una frase: Ogni servizio scrive log strutturati e un collettore li raccoglie in un unico posto dove puoi cercarli.
Problema che risolve: Con dieci servizi e tre istanze ciascuno hai trenta flussi di log diversi. Quando un container muore, i suoi log muoiono con lui. Fare ssh su ogni macchina per cercare un errore non è possibile.
Come funziona:
flowchart LR
ordini["📦 Ordini"] -->|"log JSON"| coll["🧹 Collettore"]
pay["💳 Pagamenti"] -->|"log JSON"| coll
mag["🏭 Magazzino"] -->|"log JSON"| coll
coll --> archivio[("🗄️ Archivio centrale")]
dev(["🧑💻 Sviluppatore"]) -->|"cerca order_id=A123"| archivio
flowchart LR
ordini["Ordini"] --> coll["Collettore log"]
pagamenti["Pagamenti"] --> coll
magazzino["Magazzino"] --> coll
notifiche["Notifiche"] --> coll
coll --> store["Archivio centrale"]
store --> ui["Interfaccia di ricerca"]
- Ogni servizio scrive i log su stdout in formato strutturato (JSON), con campi fissi: timestamp, livello, servizio, messaggio.
- Un collettore (agente sul nodo o sidecar) legge stdout e invia i log all'archivio centrale.
- L'archivio li indicizza. Puoi filtrare per servizio, livello, campo.
- Un'interfaccia di ricerca permette di seguire un errore attraverso tutti i servizi.
Quando usarlo: - Sempre, appena hai più di una istanza o più di un servizio. - Quando i container sono effimeri e i log locali spariscono.
Quando NON usarlo / rischi: - Log troppo verbosi costano in archiviazione e rete. Scegli i livelli con cura. - Log non strutturati (testo libero) rendono la ricerca inutile. Decidi il formato prima. - Non mettere dati sensibili nei log: finiscono in un archivio condiviso.
Esempio pratico: Un cliente dice che il suo ordine A123 risulta pagato ma non spedito. Cerchi order_id=A123 nell'archivio centrale e vedi in un secondo i log di Ordini, Pagamenti e Magazzino. Scopri che Magazzino ha ricevuto l'evento ma ha scartato il messaggio per un campo mancante.
Pattern correlati: Correlation ID, Distributed Tracing, Service Template / Chassis
Correlation ID¶
In una frase: Ogni richiesta riceve un identificativo unico all'ingresso e lo porta con sé in ogni chiamata successiva.
Problema che risolve: Hai i log centralizzati, ma la richiesta di un cliente genera log in cinque servizi diversi. Senza un filo comune non riesci a dire quali righe appartengono alla stessa richiesta, soprattutto sotto carico, quando centinaia di ordini passano insieme.
Come funziona:
flowchart LR
cliente(["🧑 Cliente"]) -->|"POST /ordini"| gw["🚪 API Gateway"]
gw -->|"🏷️ abc123"| ordini["📦 Ordini"]
ordini -->|"🏷️ abc123"| pay["💳 Pagamenti"]
ordini -->|"🏷️ abc123"| mag["🏭 Magazzino"]
pay -->|"🏷️ abc123"| mail["✉️ Notifiche"]
sequenceDiagram
participant C as Cliente
participant G as API Gateway
participant O as Servizio Ordini
participant P as Servizio Pagamenti
participant M as Servizio Magazzino
C->>G: POST /ordini
Note over G: genera X-Correlation-ID abc123
G->>O: POST /ordini [X-Correlation-ID abc123]
O->>P: POST /pagamenti [X-Correlation-ID abc123]
P-->>O: 200 OK
O->>M: POST /prenota [X-Correlation-ID abc123]
M-->>O: 200 OK
O-->>G: 201 Created
G-->>C: 201 Created
- Il gateway controlla se la richiesta ha già un header
X-Correlation-ID. Se manca, lo genera. - Ogni servizio legge l'header, lo mette in ogni riga di log che scrive e lo copia in ogni chiamata in uscita (HTTP o messaggio).
- Alla fine, cercando
abc123nei log trovi tutte le righe di quella richiesta, in ordine, in tutti i servizi.
Quando usarlo: - Sempre, appena una richiesta attraversa più di un servizio. - Anche con i messaggi asincroni: l'id viaggia come header del messaggio.
Quando NON usarlo / rischi: - Se un servizio dimentica di propagare l'header, la catena si spezza. Metti la propagazione nel Service Template. - Non dice quanto tempo ha preso ogni passaggio: per questo serve il Distributed Tracing.
Esempio pratico: Un pagamento risulta "in attesa" per sempre. Cerchi il correlation id del checkout e vedi che Ordini ha chiamato Pagamenti, Pagamenti ha risposto OK, ma Notifiche non ha mai ricevuto l'evento: il messaggio è rimasto in coda per un errore di serializzazione.
Pattern correlati: Log Aggregation, Distributed Tracing, API Gateway
Distributed Tracing¶
In una frase: Ogni richiesta diventa una trace, composta da span annidati che misurano quanto tempo prende ogni passaggio in ogni servizio.
Problema che risolve: Il checkout prende tre secondi e non sai perché. I log dicono cosa è successo, ma non quanto tempo ha preso ogni chiamata né come le chiamate si annidano. Senza una visione gerarchica dei tempi non trovi il collo di bottiglia.
Come funziona:
flowchart TD
subgraph trace["⏱️ Trace checkout - 3000 ms"]
ordini["📦 Ordini - 2950 ms"]
ordini --> pay["💳 Pagamenti - 2400 ms"]
pay --> banca>"☁️ Gateway bancario - 2350 ms 🐢"]
ordini --> mag["🏭 Magazzino - 300 ms"]
ordini --> mail["✉️ Notifiche - 150 ms"]
end
trace -->|"span inviati"| coll[("🗄️ Collettore OpenTelemetry")]
flowchart TD
trace["Trace checkout - 3000 ms"]
trace --> s1["Span Ordini crea ordine - 2950 ms"]
s1 --> s2["Span Pagamenti autorizza - 2400 ms"]
s2 --> s3["Span chiamata gateway bancario - 2350 ms"]
s1 --> s4["Span Magazzino prenota - 300 ms"]
s1 --> s5["Span Notifiche invia email - 150 ms"]
- All'ingresso nasce una trace con un
trace_id. Ogni operazione significativa apre uno span con inizio, fine, nome eparent_span_id. - Il contesto (
trace_id,span_id) viaggia negli header, come il correlation id. Lo standard è W3C Trace Context, implementato da OpenTelemetry. - Ogni servizio invia i propri span a un collettore. Il collettore li ricompone in un albero.
- Nell'interfaccia vedi il diagramma a cascata: nel nostro esempio il tempo si perde quasi tutto nel gateway bancario.
Quando usarlo: - Quando devi capire la latenza di una richiesta che attraversa più servizi. - Quando vuoi una mappa delle dipendenze reali fra servizi, costruita dal traffico vero.
Quando NON usarlo / rischi: - Tracciare il 100% delle richieste costa molto. Usa il campionamento (ad esempio 1 su 100), con il 100% sugli errori. - Serve strumentare tutti i servizi: un servizio senza tracing è un buco nell'albero. - Non sostituisce log e metriche: le trace sono campionate, le metriche no.
Esempio pratico: Usi OpenTelemetry in ogni servizio. Durante i saldi la latenza del checkout sale. Apri una trace lenta e vedi che lo span Magazzino prenota è passato da 300 ms a 2 secondi: la query di prenotazione fa una scansione completa della tabella. Aggiungi l'indice.
Pattern correlati: Correlation ID, Log Aggregation, Metrics e Golden Signals
Metrics e Golden Signals¶
In una frase: Ogni servizio espone pochi numeri chiave, i Golden Signals, che bastano a dire se è sano.
Problema che risolve: Puoi raccogliere centinaia di metriche e non sapere comunque se il sistema funziona. Senza un insieme piccolo e standard di segnali, ogni servizio ha una dashboard diversa e gli allarmi scattano per cose sbagliate.
Come funziona:
flowchart LR
pay["💳 Pagamenti"] -->|"/metrics"| segnali
subgraph segnali["📊 Golden Signals"]
lat["⏱️ Latency p95"]
tra["🚦 Traffic req/s"]
err["❌ Errors %"]
sat["🌡️ Saturation pool DB"]
end
segnali --> dash["📈 Dashboard"]
segnali -->|"errori sopra 2%"| oncall(["📟 Chi è di turno"])
flowchart LR
subgraph servizio["Servizio Pagamenti"]
app["Applicazione"] --> ep["Endpoint /metrics"]
end
ep --> prom["Raccoglitore metriche"]
prom --> dash["Dashboard"]
prom --> alert["Allarmi"]
alert --> oncall["Chi è di turno"]
I quattro Golden Signals, per ogni servizio:
- Latency: quanto tempo prende una richiesta. Guarda i percentili (p50, p95, p99), non la media. Separa le richieste riuscite da quelle fallite.
- Traffic: quante richieste al secondo arrivano. Dice quanto carico il servizio sta reggendo.
- Errors: quante richieste falliscono, in percentuale. Conta sia gli errori espliciti (HTTP 500) sia quelli nascosti (200 con contenuto sbagliato).
- Saturation: quanto il servizio è vicino al suo limite: CPU, memoria, connessioni al database, lunghezza delle code.
Il servizio espone i numeri su un endpoint, un raccoglitore li legge a intervalli regolari e li salva come serie storiche. Gli allarmi scattano su soglie dei segnali, non su metriche interne.
Quando usarlo: - Sempre, su ogni servizio, con gli stessi nomi di metrica. - Per decidere se un Canary Release è sano prima di allargarlo.
Quando NON usarlo / rischi: - Troppe metriche con troppe etichette (ad esempio una per utente) fanno esplodere i costi del raccoglitore. - Allarmi su cause (CPU alta) invece che su sintomi (latenza alta) producono rumore. - La media nasconde i problemi: il p99 è quello che vedono i clienti più sfortunati.
Esempio pratico: Pagamenti espone http_request_duration_seconds, http_requests_total con etichetta status, e db_pool_in_use. Un allarme scatta se il p95 della latenza supera 1 secondo per 5 minuti o se gli errori superano il 2%. Durante un picco vedi la saturazione del pool di connessioni al 100% prima che la latenza salga: aumenti il pool in tempo.
Pattern correlati: Distributed Tracing, Canary Release, Health Check, Circuit Breaker
Externalized Configuration¶
In una frase: Il servizio legge la configurazione dall'ambiente in cui gira, non dal suo codice o dalla sua immagine.
Problema che risolve: Lo stesso servizio Pagamenti deve puntare a un gateway bancario di prova in staging e a quello vero in produzione. Se l'URL sta nel codice, serve un'immagine diversa per ambiente, e un cambio di parametro richiede un nuovo rilascio. Le password nell'immagine finiscono nel registro e nella cronologia git.
Come funziona:
flowchart LR
img["📦 Immagine Pagamenti - unica"]
subgraph staging["🧪 Staging"]
env1["📄 BANK_URL=simulatore"] --> s1["💳 Pagamenti"]
end
subgraph prod["🏭 Produzione"]
env2["📄 BANK_URL=banca vera"] --> s2["💳 Pagamenti"]
sec[("🔐 Gestore segreti")] -->|"API key"| s2
end
img --> s1
img --> s2
flowchart LR
img["Immagine Pagamenti - unica"]
env["Variabili ambiente"] --> svc["Pagamenti in esecuzione"]
cfg["Config server"] --> svc
sec["Gestore segreti"] --> svc
img --> svc
- L'immagine del servizio è una sola per tutti gli ambienti. Non contiene URL, credenziali o soglie.
- All'avvio il servizio legge i valori da tre fonti, in ordine di sensibilità: variabili d'ambiente per i parametri semplici, un config server per la configurazione condivisa e versionata, un gestore di segreti per password e chiavi.
- La configurazione è validata all'avvio: se manca un valore obbligatorio, il servizio non parte e lo dice chiaramente.
- Alcuni valori possono essere ricaricati a caldo (soglie, feature toggle), altri richiedono un riavvio.
Quando usarlo: - Sempre: lo stesso artefatto deve girare in sviluppo, staging e produzione. - Quando le credenziali devono ruotare senza ricostruire nulla.
Quando NON usarlo / rischi: - Troppi parametri esterni rendono il servizio imprevedibile: nessuno sa con quale configurazione gira. Logga la configurazione effettiva all'avvio (senza i segreti). - Un config server è un punto di guasto: il servizio deve avere valori di default sensati o un cache locale. - Non mettere i segreti nelle variabili d'ambiente se altri processi possono leggerle.
Esempio pratico: Pagamenti legge BANK_GATEWAY_URL e BANK_TIMEOUT_MS dall'ambiente, le soglie del circuit breaker dal config server e BANK_API_KEY dal gestore segreti. In staging l'URL punta al simulatore della banca. Per ruotare la chiave API aggiorni il segreto e riavvii i pod: nessuna nuova immagine.
Pattern correlati: Feature Toggle, Service Template / Chassis
Service Template / Chassis¶
In una frase: Un progetto base, o una libreria comune, che dà a ogni nuovo servizio logging, health check, metriche, tracing e configurazione già pronti.
Problema che risolve: Ogni team che crea un servizio deve rifare le stesse cose trasversali: formato dei log, propagazione del correlation id, endpoint /health e /metrics, lettura della configurazione. Se ognuno le fa a modo suo, l'osservabilità diventa incoerente e i nuovi servizi nascono con pezzi mancanti.
Come funziona:
flowchart TD
subgraph chassis["📦 ecommerce-chassis"]
log["📄 Log JSON"]
health["💓 Health"]
met["📊 Metrics"]
tr["⏱️ Tracing"]
end
chassis -->|"importa"| ordini["📦 Ordini: solo logica ordini"]
chassis -->|"importa"| mail["✉️ Notifiche: solo invio email"]
chassis -->|"importa"| cat["🛍️ Catalogo: solo ricerca"]
flowchart TD
chassis["Chassis comune"]
chassis --> log["Log strutturati"]
chassis --> cid["Propagazione Correlation ID"]
chassis --> health["Endpoint health"]
chassis --> metrics["Endpoint metrics"]
chassis --> trace["Tracing OpenTelemetry"]
chassis --> cfg["Lettura configurazione"]
chassis --> ordini["Servizio Ordini"]
chassis --> notifiche["Servizio Notifiche"]
- Il chassis è una libreria (o un template di progetto) mantenuta da un team di piattaforma.
- Un nuovo servizio la importa e ottiene tutte le funzioni trasversali con una riga di inizializzazione.
- Il team del servizio scrive solo la logica di dominio.
- Quando lo standard cambia (nuovo campo nei log, nuovo header di tracing), si aggiorna il chassis e i servizi lo adottano con un incremento di versione.
Quando usarlo: - Quando hai più di due o tre servizi e vuoi che si comportino allo stesso modo. - Quando vuoi che un nuovo servizio sia osservabile dal primo giorno.
Quando NON usarlo / rischi: - Un chassis per ogni linguaggio: se usi Python e Go, servono due chassis. Il service mesh sposta parte di questo lavoro fuori dal processo. - Un chassis troppo grosso diventa un framework che vincola tutti. Tieni dentro solo le funzioni trasversali. - Gli aggiornamenti vanno coordinati: un servizio fermo a una versione vecchia del chassis ha log diversi dagli altri.
Esempio pratico: Il team di piattaforma pubblica ecommerce-chassis. Il nuovo servizio Notifiche lo importa, chiama setup_service("notifiche") e ha subito log JSON con correlation id, /health, /metrics e tracing. Il team scrive solo l'invio delle email.
Pattern correlati: Log Aggregation, Correlation ID, Externalized Configuration, Sidecar
Feature Toggle¶
In una frase: Il codice della nuova funzione va in produzione spento, e si accende con un interruttore di configurazione, senza un nuovo rilascio.
Problema che risolve: Rilasciare codice e attivare una funzione sono la stessa cosa: se la nuova funzione ha un bug, l'unico rimedio è un rollback di tutto. Le funzioni grandi restano su branch separati per settimane e l'integrazione diventa dolorosa.
Come funziona:
flowchart LR
cliente(["🧑 Cliente"]) -->|"checkout"| sw{{"🔘 Toggle checkout_single_step"}}
sw -->|"acceso"| nuovo["✨ Nuovo checkout in un passo"]
sw -->|"spento"| vecchio["🐢 Vecchio checkout in tre passi"]
cfg[("⚙️ Servizio toggle: 10% utenti")] -.-> sw
flowchart LR
req["Richiesta checkout"] --> check{"Toggle nuovo checkout attivo per questo utente"}
check -- si --> nuovo["Nuovo flusso checkout"]
check -- no --> vecchio["Vecchio flusso checkout"]
cfg["Servizio toggle"] -.-> check
- Il nuovo codice vive accanto al vecchio, dietro un
ifche legge lo stato del toggle. - Lo stato sta fuori dal codice: in un servizio di toggle o nella configurazione esterna. Può dipendere dall'utente, dalla percentuale, dall'ambiente.
- Il rilascio porta il codice in produzione con il toggle spento. Nessun utente lo vede.
- Si accende per il team interno, poi per il 5% degli utenti, poi per tutti. Se qualcosa va male, si spegne in un secondo.
- Quando la funzione è stabile per tutti, si rimuove il toggle e il vecchio codice.
Quando usarlo: - Per separare il momento del rilascio dal momento dell'attivazione. - Per esperimenti A/B e per attivazioni graduali. - Come interruttore di emergenza per spegnere una funzione costosa sotto carico.
Quando NON usarlo / rischi:
- I toggle dimenticati si accumulano e il codice diventa un labirinto di if. Ogni toggle ha una data di rimozione.
- Due toggle che interagiscono generano combinazioni mai testate.
- Il servizio di toggle è una dipendenza: se non risponde, serve un default sicuro.
Esempio pratico: Il nuovo checkout in un solo passo va in produzione dietro checkout_single_step. Lo accendi per i dipendenti, poi per il 10% dei clienti. Le metriche mostrano lo stesso tasso di errore e una conversione più alta. Lo porti al 100% e dopo due settimane rimuovi il vecchio flusso.
Pattern correlati: Externalized Configuration, Canary Release, Strategy
Blue/Green Deployment¶
In una frase: Tieni due ambienti identici, uno attivo (blue) e uno con la nuova versione (green), e sposti tutto il traffico da uno all'altro in un colpo solo.
Problema che risolve: Rilasciare sopra l'ambiente in uso vuol dire un periodo di indisponibilità e, se la nuova versione è rotta, un rollback che richiede di ricostruire quella vecchia mentre i clienti aspettano.
Come funziona:
flowchart LR
utenti(["🧑 Utenti"]) --> router{{"🔀 Router"}}
router -->|"100% traffico"| blue
router -.->|"0% - test di fumo"| green
subgraph blue["🔵 Blue - Ordini v1"]
b1["📦 istanza"]
b2["📦 istanza"]
end
subgraph green["🟢 Green - Ordini v2"]
g1["📦 istanza"]
g2["📦 istanza"]
end
blue --> db[("🗄️ Database condiviso")]
green --> db
flowchart LR
utenti["Utenti"] --> router["Router o load balancer"]
router -- "100% traffico" --> blue["Blue - Ordini v1"]
router -. "0% traffico" .-> green["Green - Ordini v2"]
test["Test di fumo"] --> green
- Blue serve tutto il traffico con la versione attuale.
- Rilasci la nuova versione su green, che è identico a blue ma non riceve traffico reale.
- Esegui i test di fumo su green chiamandolo direttamente.
- Se i test passano, il router sposta il 100% del traffico su green. Il cambio è istantaneo.
- Blue resta acceso con la versione vecchia. Se qualcosa va male, il router torna su blue: il rollback dura un secondo.
- Quando green è stabile, blue diventa l'ambiente per il prossimo rilascio.
Quando usarlo: - Quando il rollback deve essere immediato. - Quando puoi permetterti di avere due ambienti completi in parallelo.
Quando NON usarlo / rischi: - Costa il doppio dell'infrastruttura durante il rilascio. - Il database è condiviso: le migrazioni di schema devono essere compatibili con entrambe le versioni, altrimenti il rollback non è più possibile. - Le sessioni e le connessioni lunghe aperte su blue si interrompono al cambio. - Tutti gli utenti passano alla nuova versione insieme: un bug colpisce tutti.
Esempio pratico: Ordini v2 va su green. I test di fumo creano un ordine di prova e lo pagano con una carta di test. Il router passa a green alle 10:00. Alle 10:03 le metriche mostrano errori 500 su /ordini/{id}. Torni a blue alle 10:04. Nessun ordine perso, quattro minuti di errori parziali invece di un'ora di ricostruzione.
Pattern correlati: Canary Release, Rolling Update, Metrics e Golden Signals
Canary Release¶
In una frase: Rilasci la nuova versione a una piccola percentuale di utenti, guardi le metriche, e allarghi solo se tutto è sano.
Problema che risolve: Anche con tutti i test, un bug si vede solo in produzione con il traffico vero. Se la nuova versione va a tutti insieme, il bug colpisce tutti insieme. Il nome viene dai canarini in miniera: un piccolo gruppo che segnala il pericolo prima degli altri.
Come funziona:
flowchart LR
utenti(["🧑 Utenti"]) --> router{{"🔀 Router"}}
router -->|"95%"| v1["🛍️ Catalogo v1 - 19 istanze"]
router -->|"5%"| v2["🐤 Catalogo v2 - 1 istanza canarino"]
v1 --> met["📊 Confronto metriche v1 vs v2"]
v2 --> met
met -->|"sano"| allarga["➕ Allarga al 25%"]
met -->|"peggiora"| rollback["↩️ Rollback a v1"]
flowchart TD
start["Rilascia v2 su poche istanze"] --> p5["5% traffico su v2 - 95% su v1"]
p5 --> check5{"Errori e latenza di v2 uguali o migliori di v1"}
check5 -- no --> rollback["Rollback - 100% su v1"]
check5 -- si --> p25["25% traffico su v2"]
p25 --> check25{"Metriche sane"}
check25 -- no --> rollback
check25 -- si --> p50["50% traffico su v2"]
p50 --> check50{"Metriche sane"}
check50 -- no --> rollback
check50 -- si --> p100["100% traffico su v2 - spegni v1"]
- La nuova versione gira su poche istanze accanto a quelle vecchie. Il router manda una piccola quota di traffico (5%) alla nuova.
- Confronti i Golden Signals di v2 con quelli di v1 sullo stesso periodo: tasso di errore, latenza p95, saturazione.
- Se v2 è sana, aumenti la quota a scalini. Se peggiora, torni al 100% su v1.
- Arrivato al 100%, spegni le istanze v1.
- La scelta degli utenti canary può essere casuale, per regione, o per gruppo (prima i dipendenti).
Quando usarlo: - Quando hai metriche affidabili per confrontare le due versioni. - Quando il traffico è abbastanza alto perché il 5% sia significativo. - Per cambi rischiosi: nuova logica di prezzo, nuovo algoritmo di raccomandazione.
Quando NON usarlo / rischi: - Senza buone metriche il canary non dice niente: diventa un rilascio lento e basta. - Due versioni convivono a lungo: schema del database, formato dei messaggi ed eventi devono essere compatibili. - Con poco traffico, il 5% è una manciata di richieste e i confronti non sono statisticamente utili. - Un utente può vedere v2 in una richiesta e v1 nella successiva se il router non è "sticky".
Esempio pratico: Catalogo v2 cambia l'algoritmo di ricerca. Lo rilasci al 5% degli utenti. Dopo trenta minuti il tasso di errore è identico ma il p95 della latenza è salito da 120 a 400 ms. Torni a v1, ottimizzi la query, riprovi il giorno dopo. Nessun cliente, oltre a quel 5% per mezz'ora, ha visto il rallentamento.
Pattern correlati: Metrics e Golden Signals, Feature Toggle, Blue/Green Deployment, Distributed Tracing
Rolling Update¶
In una frase: Sostituisci le istanze una alla volta (o a piccoli gruppi) con la nuova versione, finché non le hai sostituite tutte.
Problema che risolve: Vuoi rilasciare senza fermare il servizio e senza pagare un ambiente doppio. Con più istanze dietro un load balancer puoi aggiornarne una per volta: le altre continuano a servire.
Come funziona:
flowchart LR
lb{{"🔀 Load balancer"}} --> ist
subgraph ist["✉️ Notifiche - 6 istanze"]
a["🟢 v2"]
b["🟢 v2"]
c["🟡 v2 in avvio"]
d["⚪ v1"]
e["⚪ v1"]
f["⚪ v1"]
end
c -->|"pronto?"| health["💓 Health check"]
flowchart LR
subgraph t1["Passo 1"]
a1["v1"] --- b1["v1"] --- c1["v1"]
end
subgraph t2["Passo 2"]
a2["v2"] --- b2["v1"] --- c2["v1"]
end
subgraph t3["Passo 3"]
a3["v2"] --- b3["v2"] --- c3["v1"]
end
subgraph t4["Passo 4"]
a4["v2"] --- b4["v2"] --- c4["v2"]
end
t1 --> t2 --> t3 --> t4
- L'orchestratore avvia una nuova istanza v2 e aspetta che il suo health check risponda "pronto".
- Il load balancer inizia a mandarle traffico e smette di mandarne a una istanza v1.
- L'istanza v1 finisce le richieste in corso e si spegne.
- Si ripete finché tutte le istanze sono v2. Se un health check fallisce, l'aggiornamento si ferma da solo.
- Il rollback è un rolling update al contrario, verso v1: funziona ma non è istantaneo.
Quando usarlo: - È il default di Kubernetes e degli orchestratori: va bene per la maggior parte dei rilasci. - Quando le versioni v1 e v2 possono convivere senza problemi.
Quando NON usarlo / rischi: - Durante il rilascio v1 e v2 girano insieme: devono essere compatibili fra loro e con il database. - Il rollback richiede minuti, non secondi. - Senza un health check serio, l'orchestratore manda traffico a istanze non pronte. - Non controlli quale utente vede quale versione: non puoi confrontare le metriche come nel canary.
Esempio pratico: Notifiche ha sei istanze. Rilasci v2 con maxUnavailable: 1 e maxSurge: 1: l'orchestratore ne aggiorna una alla volta, in sei minuti totali. Il servizio non ha mai meno di cinque istanze attive. L'health check di v2 verifica la connessione al provider email prima di dichiararsi pronto.
Pattern correlati: Blue/Green Deployment, Canary Release, Health Check
Blue/Green vs Canary vs Rolling¶
| Aspetto | Blue/Green | Canary | Rolling |
|---|---|---|---|
| Rollback | Istantaneo, cambio del router | Veloce, riporta il traffico a v1 | Lento, nuovo rolling verso v1 |
| Costo infrastruttura | Doppio durante il rilascio | Poche istanze in più | Una o poche istanze in più |
| Rischio per gli utenti | Tutti insieme sulla nuova versione | Solo una piccola quota, controllata | Casuale, cresce man mano |
| Serve metriche di confronto | No, bastano test di fumo | Sì, è il cuore del pattern | No |
| Convivenza di versioni | Solo sul database | Lunga, va progettata | Breve, va comunque gestita |
| Quando | Rilasci rari, rollback critico | Cambi rischiosi, traffico alto | Rilasci frequenti, cambi piccoli |
Consumer-Driven Contract Testing¶
In una frase: Il consumer di un'API scrive un contratto con le richieste che fa e le risposte che si aspetta, e il provider lo verifica nella sua pipeline.
Problema che risolve: Il team Pagamenti rinomina un campo nella risposta. I suoi test passano. Il team Ordini, che legge quel campo, scopre la rottura in produzione. I test end-to-end con tutti i servizi accesi sono lenti, fragili e arrivano tardi.
Come funziona:
flowchart LR
ordini["📦 Team Ordini - consumer"] -->|"scrive"| contratto["📄 Contratto: POST /pagamenti risponde id, stato"]
contratto -->|"pubblica"| broker[("🗄️ Broker contratti")]
broker -->|"scarica"| pipe["🔧 Pipeline Pagamenti"]
pipe -->|"verifica"| pay["💳 Team Pagamenti - provider"]
pipe --> esito["❌ Rinomina stato in status: FALLISCE"]
sequenceDiagram
participant O as Team Ordini - consumer
participant B as Broker contratti
participant P as Team Pagamenti - provider
O->>O: scrive test contro un mock di Pagamenti
O->>B: pubblica il contratto - richieste e risposte attese
P->>B: scarica i contratti dei suoi consumer
P->>P: esegue le richieste del contratto contro Pagamenti reale
P->>B: pubblica il risultato della verifica
B-->>O: posso rilasciare Ordini con questa versione di Pagamenti
B-->>P: posso rilasciare Pagamenti senza rompere Ordini
- Il consumer (Ordini) scrive un test che descrive esattamente la richiesta che fa a Pagamenti e i campi della risposta che usa. Il test gira contro un mock generato dallo strumento (Pact), senza Pagamenti acceso.
- Il test produce un file di contratto, pubblicato su un broker.
- Nella pipeline di Pagamenti, lo strumento scarica i contratti di tutti i consumer e li esegue contro il servizio reale.
- Se Pagamenti rompe un campo che Ordini usa, la pipeline di Pagamenti fallisce prima del rilascio.
- Il broker risponde alla domanda "posso rilasciare?" per entrambe le parti.
Quando usarlo: - Quando più team rilasciano servizi in modo indipendente e devono sapere se si rompono a vicenda. - Al posto di test end-to-end lenti per verificare l'integrazione fra due servizi.
Quando NON usarlo / rischi: - Il contratto copre solo i campi che il consumer dichiara: un test troppo generico non protegge nulla. - Serve disciplina: il provider deve eseguire i contratti nella propria pipeline, altrimenti sono carta. - Per API pubbliche con consumer sconosciuti non funziona: non puoi raccogliere i loro contratti. - Non verifica la logica di business del provider, solo la forma dello scambio.
Esempio pratico: Ordini dichiara nel contratto che POST /pagamenti risponde con {"id": string, "stato": "AUTORIZZATO" | "RIFIUTATO"}. Pagamenti vuole rinominare stato in status: la sua pipeline fallisce con il messaggio "contratto di Ordini: campo stato mancante". Il team aggiunge il nuovo campo mantenendo il vecchio, Ordini migra, poi il vecchio viene tolto.
Pattern correlati: API Gateway, Canary Release, Service Template / Chassis
Come scegliere¶
flowchart TD
q1{"Non riesci a trovare i log di una richiesta"} -- si --> la["Log Aggregation + Correlation ID"]
q1 -- no --> q2{"Non sai dove una richiesta perde tempo"}
q2 -- si --> dt["Distributed Tracing"]
q2 -- no --> q3{"Non sai se il sistema e sano prima degli utenti"}
q3 -- si --> gs["Metrics e Golden Signals"]
q3 -- no --> q4{"Serve un'immagine diversa per ogni ambiente"}
q4 -- si --> ec["Externalized Configuration"]
q4 -- no --> q5{"Ogni servizio rifa log, health e metriche"}
q5 -- si --> st["Service Template / Chassis"]
q5 -- no --> q6{"Vuoi attivare una funzione senza rilasciare"}
q6 -- si --> ft["Feature Toggle"]
q6 -- no --> q7{"Devi rilasciare una nuova versione"}
q7 -- si --> q8{"Il rollback deve essere istantaneo e puoi pagare il doppio"}
q8 -- si --> bg["Blue/Green Deployment"]
q8 -- no --> q9{"Il cambio e rischioso e hai buone metriche"}
q9 -- si --> cr["Canary Release"]
q9 -- no --> ru["Rolling Update"]
q7 -- no --> q10{"Un cambio di API rompe un altro team senza preavviso"}
q10 -- si --> cdc["Consumer-Driven Contract Testing"]