Più sistemi che devono funzionare insieme, anche quando uno di essi cade.
Progettiamo e operiamo piattaforme composte da più servizi, con bus di messaggi, retry con limite, chiavi di idempotenza, circuit breaker e pubblicazione con rollback automatico. Ogni pattern qui sotto funziona in un sistema proprio, non in un diagramma.
Già costruitoOperiamo quattro piattaforme proprie con architetture diverse e possiamo aprirle tutte. I numeri sono misurati oggi con comandi, non presi dalla documentazione: aichat ha 28 directory di servizio, 235 modelli di dati su quattro schemi e un bus di messaggi con 297 code e 17.525 collegamenti; Kallina ha 472 funzioni edge, 559 migrazioni e 28 attività pianificate nel database; MEGA CRM ha un backend di 9.516 righe con 99 route e 179 politiche di accesso per riga; Taskin esegue 21 attività pianificate senza alcun Docker. La riserva che diciamo noi: in due punti la documentazione interna è rimasta indietro rispetto al codice — un’affermazione sul numero di righe era superata del 16%, e un diagramma di infrastruttura descriveva un server sul quale non eseguiamo più nulla. Abbiamo usato la misurazione, non il documento.
“Architettura complessa” non significa molte caselle in un disegno. Significa sapere cosa succede quando una di esse non risponde. Un sistema con tre servizi che si chiamano in modo sincrono, senza limite di retry e senza idempotenza, è più fragile di un monolite — perché ogni nuovo collegamento è un nuovo modo di guasto, e i modi di guasto si moltiplicano più in fretta delle funzionalità.
Il nostro lavoro parte dalla separazione delle responsabilità e arriva a ciò che si vede quando qualcosa cede. In una delle piattaforme proprietarie, i messaggi non passano direttamente tra servizi, ma attraverso una dorsale con 297 code, 9 exchange e 17.525 collegamenti, di cui 278 code sono generate per cliente, non scritte a mano. Otto code hanno dead letter exchange e otto hanno un tempo di vita del messaggio di cinque minuti — cioè un messaggio che non può essere elaborato finisce in un posto dove può essere visto, non scompare.
Gli schemi che applichiamo sono pochi e si ripetono: retry con limite scritto nel codice, chiavi di idempotenza che rendono innocua la seconda consegna dello stesso evento, un interruttore che blocca le chiamate verso un servizio che va in errore ripetutamente, una porta di consumo prima delle azioni che costano denaro, e alert con soglia di ripetizione così che un guasto intermittente non seppellisca il resto.
L’ultima parte, quella che si guarda: la pubblicazione. Una delle piattaforme si pubblica tramite sincronizzazione di file, senza Docker, con la verifica che la pagina servita contenga esattamente l’identificatore del pacchetto appena pubblicato — non solo che il server risponda 200 — e con rollback automatico alla versione precedente se la verifica fallisce. La regola è nata dopo un incidente reale in cui una verifica superficiale ha annullato una pubblicazione buona.
Cosa comprende
Il lavoro, per componenti
Separamos i servizi in base a ciò che può rompersi in modo indipendente
In una piattaforma proprietaria ci sono 28 directory di servizio, di cui 16 hanno un punto di ingresso proprio, e in produzione girano cinque processi sotto un gestore di processi. Ogni canale di comunicazione ha il proprio servizio e la propria porta, proprio per fare in modo che un guasto in uno non fermi il resto. I dati sono modellati su quattro schemi separati, con 235 modelli in totale — la separazione non è solo a livello di processo, ma anche a livello di schema.
Mettiamo una dorsale di messaggi, non una catena di chiamate sincrone
297 code, 9 exchange, 17.525 collegamenti. La denominazione delle code è per cliente e, in alcuni casi, per thread — cioè la topologia si genera, non si scrive a mano. Esiste anche un exchange con consegna differita, per le cose che devono accadere più tardi, non adesso. Otto code hanno configurato il dead letter exchange e otto hanno un tempo di vita del messaggio di cinque minuti.
Retry con limite, non all’infinito
Tre limiti diversi per tre situazioni diverse, tutti scritti nel codice: cinque retry per i messaggi pianificati, con il contatore persistito, non tenuto in memoria; tre retry a livello di coda, conteggiati in un header del messaggio; e tre retry con aumento esponenziale della pausa per l’invio di e-mail, su due canali di provider. Un retry senza limite non è resilienza, è un loop.
Chiavi di idempotenza, così che la seconda consegna non rovini nulla
Per gli eventi di terze parti usiamo un inserimento che fallisce se l’evento è già stato visto: la violazione di unicità del database viene trattata come “duplicato”, non come errore, insieme a una verifica di freschezza del timestamp. Per i pagamenti, la chiave è un flag sulla transazione di credito, quindi la reinvio dello stesso webhook non accredita due volte. Per i messaggi, la deduplicazione avviene per identificatore, con tempo di vita e pulizia.
Un interruttore di circuito sull’integrazione fragile
L’integrazione che dipende dalla sessione di un sito terzo ha un interruttore reale, non solo una nota nella documentazione: dopo dieci errori consecutivi, le chiamate si fermano per due minuti. Senza di esso, un sito che non risponde trasforma un canale caduto in una piattaforma lenta, perché tutti aspettano nella richiesta.
Porta di consumo prima delle azioni che costano
In una delle piattaforme, ogni azione a pagamento passa attraverso una funzione di verifica a quattro livelli, ciascuno con il proprio motivo restituito in chiaro: account sospeso, limite mensile di conversazioni raggiunto, tetto giornaliero di spesa superato, saldo insufficiente. In caso di eccezione restituisce «non consentito», non «consentito» — la porta si chiude, non si apre, quando qualcosa non funziona. Dietro c’è un registro di eventi di consumo con costo per evento, ripartito per fornitore, modello e modalità.
Limitazione di velocità che sopravvive al riavvio
Finestra scorrevole mantenuta nel database, con una mappa in memoria solo come scorciatoia all’interno di una singola invocazione. Il motivo è scritto proprio in cima al file: i processi che servono le richieste sono di breve durata, quindi la loro memoria non può essere la fonte della verità. Quindici funzioni la usano, e la quota di un fornitore esterno ha il proprio limitatore, separato.
Osservabilità che distingue «ha risposto» da «ha funzionato»
Una risposta 200 non significa che il compito sia riuscito. L’involucro con cui eseguiamo i compiti programmati percorre ricorsivamente la risposta alla ricerca di liste di fallimenti o errori e tratta «200 con fallimenti» come risultato distinto. Gli avvisi hanno una soglia di ripetizione — dieci minuti in un sistema, due ore nell’altro — e il contrassegno viene cancellato al primo successo, così che un guasto ricorrente possa avvisare di nuovo subito.
Pubblicazione con verifica e rollback automatico
La pubblicazione verifica non solo che l’indirizzo risponda 200, ma che la pagina servita contenga esattamente l’identificatore del pacchetto appena pubblicato; altrimenti ripristina la versione precedente. Per il servizio in background, la pubblicazione confronta l’albero tramite somme di controllo e salta il riavvio se non è cambiato nulla — un riavvio inutile uccide un ciclo in corso.
Come si presenta
Il percorso, passo dopo passo.
01
Disegniamo la mappa delle modalità di guasto, non delle scatole
Per ogni collegamento tra due sistemi: cosa succede se l’altro è lento, se è giù, se risponde due volte, se risponde in modo errato. Consegnamo: l’elenco dei collegamenti con il comportamento atteso in ciascuna delle quattro situazioni, più la decisione sincrono/asincrono per ciascuno.
02
Impostiamo il bus e i contratti dei messaggi
Code, exchange, lettere morte, tempo di vita per messaggio e chiave di idempotenza per ogni tipo di evento. Consegnamo: la topologia, i contratti dei messaggi e il comportamento documentato alla ritrasmissione.
03
Aggiungiamo le porte: rate, consumo, interruttore
Limitazione di velocità su finestra scorrevole nel database, porta di consumo prima delle azioni che costano e interruttore sulle integrazioni fuori dal nostro controllo. Consegnamo: le soglie impostate, i motivi restituiti in chiaro e i test per ciascun livello di rifiuto.
04
Costruiamo la pubblicazione e il rollback
Pubblicazione con verifica del contenuto, non solo del codice di risposta, versione precedente conservata accanto e rollback automatico. Consegnamo: la procedura di pubblicazione, la procedura di rollback esercitata almeno una volta, e le sonde di salute — una superficiale per «vive», una profonda per «funziona davvero».
05
Consegniamo con la documentazione verificata rispetto al codice
Il documento viene verificato rispetto alla misurazione prima della consegna, perché una documentazione rimasta indietro è più pericolosa della sua assenza. Consegnamo: il diagramma, le procedure e l’elenco esplicito dei punti in cui documento e codice sono stati trovati in disaccordo, con ciò che abbiamo corretto.
I sistemi non si chiamano in catena: tra loro sta un bus con code per cliente, exchange separati per tipo di traffico e dead letter per ciò che non si può elaborare. Attorno ad esso, i controlli — rate limiting, controllo di consumo, circuit breaker — e le sonde di salute, una superficiale e una profonda.
I dati
Cosa tocchiamo, dove risiedono e quanto rimangono
Le domande che pone chiunque abbia un responsabile della protezione dei dati — poste qui prima che le ponga lui.
Dove stanno i dati, per piattaforma
Non esiste una risposta unica, ed è bene che sia così: una piattaforma usa MySQL su server proprio, due usano PostgreSQL tramite Supabase — una ospitata, una installata sulla nostra infrastruttura — e una tiene i contenuti come file statici, senza alcun database. La scelta si fa in base ai requisiti, non all’abitudine.
L’accesso si applica nel database, non solo nell’applicazione
In un nostro sistema interno, la sicurezza a livello di riga è attiva su 43 tabelle, tramite 90 istruzioni, con 179 politiche scritte. La regola che seguiamo: se una chiamata aggira l’applicazione e colpisce direttamente il database, non deve comunque vedere le righe di altri.
Registro dei consumi
Una riga per evento con tipo, fornitore, quantità separate per modalità — secondi di conversazione, token di input, token di output, caratteri — più costo in valuta con sei decimali e i crediti scalati. Da esso derivano il tetto di spesa giornaliero, il costo per chiamata e la proiezione di consumo.
Le migrazioni sono la cronologia, non la documentazione
559 migrazioni in una piattaforma, 80 in un’altra. Lo schema cambia tramite migrazioni versionate, quindi lo stato del database può essere ricostruito e letto in ordine cronologico. Quando il documento e la migrazione non concordano, ha ragione la migrazione.
Le attività pianificate stanno nel database o in cron, non nel processo
28 attività pianificate girano all’interno del database in una piattaforma; in un’altra, 21 attività girano tramite cron di sistema. Il motivo è scritto nel file: i timer all’interno di un servizio si azzerano a ogni riavvio, quindi un ciclo ogni tre ore in un servizio che viene ripubblicato non parte mai.
Un caso
Una topologia di messaggi che si genera da sola per ogni cliente
La situazione
Una piattaforma multi-cliente, ciascuno con i propri canali di comunicazione, i propri thread di discussione e le proprie notifiche. La variante ingenua — una coda comune e un filtro sull’identificatore del cliente — fa sì che un cliente con volume alto blocchi gli altri, mentre un errore su un thread fermi la coda per tutti.
Cosa abbiamo costruito
La topologia si genera per cliente, non si scrive a mano: su 297 code, 278 sono del tipo «notifiche per un determinato account», e alcune scendono fino al livello di un singolo thread di discussione. Sopra di esse, nove exchange separano i tipi di traffico — notifiche, token, webhook, canali — ed esiste un exchange con consegna ritardata per ciò che deve accadere più tardi. Otto code hanno un dead-letter exchange, otto hanno un TTL per messaggio di cinque minuti. Le regole di orchestrazione si possono ricaricare a caldo, tramite un canale di pubblicazione, senza riavviare i consumer.
Cosa è emerso
Un cliente con volume alto non rallenta gli altri clienti, e un messaggio che non può essere elaborato finisce in un luogo dove può essere visto e ripreso, invece di sparire o bloccare la coda. Il numero di collegamenti nella bus — 17.525 — mostra esattamente perché la topologia deve essere generata: nessuno mantiene manualmente una cosa del genere.
Cosa non dice il caso
Il prezzo è operativo: una bus di queste dimensioni ha bisogno di monitoraggio proprio e di un piano per le code abbandonate, altrimenti cresce all’infinito. E la generazione per cliente implica che l’eliminazione di un cliente elimini anche la sua topologia — se questo passaggio manca, si accumulano code morte.
Domande
Cosa ci chiedono le persone prima di chiamare
Come faccio a sapere che non mi state vendendo una complessità di cui non ho bisogno?
Perché il primo consiglio che diamo spesso è di non separare. Ogni nuovo servizio è un nuovo modo di fallire, e i modi di fallire si moltiplicano più in fretta delle funzionalità. Un esempio dai nostri sistemi: il backend di uno di essi è un unico file di 9.516 righe con 99 route. Non è elegante e lo diciamo, ma si pubblica in un solo passaggio e si fa debugging in un solo posto. La separazione si fa quando c’è un motivo misurabile — un servizio che deve essere scalato separatamente, un team separato, un ritmo di rilascio separato.
Cosa succede quando un sistema nella catena non risponde?
Dipende da ciò che abbiamo deciso insieme nella fase di mappa, ed è proprio questa l’idea. Nei nostri sistemi: il messaggio entra in coda e viene ritentato per un numero massimo di volte, poi arriva allo scambio delle lettere morte, dove può essere visto; l’integrazione instabile ha un interruttore che, dopo dieci fallimenti consecutivi, si ferma per due minuti invece di tenere le richieste in attesa; e le azioni che costano denaro sono bloccate da un gate che, in caso di eccezione, rifiuta, non permette.
Come evitate che lo stesso evento venga elaborato due volte?
Con una chiave di idempotenza, non con un controllo “l’ho già visto?” che presenta una race condition sui dati. In concreto: inseriamo una riga con l’identificatore dell’evento e, se il database rifiuta per violazione dell’unicità, trattiamo quel codice di errore come segnale di duplicato, non come guasto. Per i pagamenti, il marker sta sulla transazione di credito, quindi il reinvio dello stesso webhook non accredita due volte.
Usate Docker oppure no?
Entrambi, e la scelta la motiviamo ogni volta. Un sistema gira in un container, ma senza volumi montati, il che significa che la pubblicazione è una copia nel container più un riavvio — un `docker rm` lì perde lo stato, ed è scritto nel documento operativo esattamente così. Un’altra piattaforma non ha alcun Docker: la pubblicazione è sincronizzazione di file, e gli script di pubblicazione sono installati manualmente con privilegi di amministratore ed esposti al flusso automatico tramite esattamente due percorsi consentiti. Il motivo è scritto nel codice: uno script che il flusso può riscrivere è uno script tramite cui il flusso può eseguire un’escalation.
Come sapete che un’attività pianificata ha davvero eseguito?
Per esperienza negativa. Un nostro report giornaliero è rimasto fermo per undici giorni, tra il 7 e il 17 agosto, mentre l’attività pianificata si attivava ogni giorno — il comando usato usciva in silenzio in caso di errore e non scriveva nulla. Da allora ogni attività viene eseguita in un wrapper che analizza la risposta secondo elenchi di fallimenti, tratta “200 con fallimenti” come risultato distinto, ha un tempo massimo di esecuzione e scrive una riga strutturata a ogni esecuzione. Gli alert hanno una soglia di ripetizione, e il marker viene rimosso al primo successo.
Quanto conta l’astrazione sul fornitore?
Molto, se il fornitore si trova in un’area che si muove rapidamente. Per la voce abbiamo tre bridge separati — uno per ciascun fornitore di speech in tempo reale — che implementano la stessa interfaccia. Il bridge esegue la conversione audio tra la centrale telefonica e il fornitore, su una porta dedicata, con il server di health collegato solo all’interfaccia locale, non esposto. Cambiare fornitore diventa una decisione, non una riscrittura.
La vostra documentazione è aggiornata?
Non ovunque, e preferiamo dirlo chiaramente. Durante la preparazione di questa pagina abbiamo misurato due sistemi e abbiamo trovato il documento interno in ritardo: un’affermazione sulla dimensione di un file era inferiore di circa 16% rispetto alla realtà, e un diagramma dell’infrastruttura descriveva un server da cui ci eravamo spostati. La regola che applichiamo e che chiediamo nei progetti: quando il documento e la misurazione non sono d’accordo, la misurazione ha ragione, e il documento si corregge nello stesso passaggio.
Che cosa non fate?
Non promettiamo obiettivi di disponibilità senza misurazione — una percentuale del tipo “99,9%” richiede dati sul campo per un periodo, e dove non li abbiamo non la affermiamo. Non progettiamo sistemi che non possiamo operare o consegnare: se il risultato è un’architettura che il tuo team non può mantenere, è un’architettura sbagliata. E non facciamo certificazioni di conformità — possiamo costruire i controlli, non possiamo emettere il certificato.
Su cosa si basano le affermazioni sopra (23 fonti)
23 di esse sono codice e file dei nostri repository. Non ne pubblichiamo il nome né la riga: insieme, in un'unica pagina, descriverebbero con troppa precisione come sono costruiti sistemi che non sono solo nostri. Le esaminiamo con te, nel repository, su richiesta — la verifica resta possibile, solo che avviene in una discussione.