genro-asgi · documento 2 di 4

Lo stato vivo, e perché un bilanciatore non basta

Perché abbiamo deciso di tenere lo stato in memoria invece di serializzarlo a ogni colpo, perché questo obbliga a legare l'utente al suo processo, e che cosa serve davvero per farlo senza rimanerci intrappolati.

Il caso d'usoL'applicativo che resta aperto

Il primo documento descrive un server che riceve una richiesta, risponde e dimentica. Per una API è il modello giusto, ed è il modello su cui è costruito tutto il web moderno.

Ma c'è una famiglia di applicativi per cui quel modello è sbagliato: il gestionale che una persona tiene aperto per otto ore. Una pagina di quel tipo non è una richiesta — è una sessione di lavoro. Accumula contesto: una griglia filtrata su seicentomila righe, un documento in composizione, un albero di selezioni, un calcolo intermedio. Riceve aggiornamenti quando un collega modifica un dato che quella pagina sta guardando. E dialoga col server in continuazione, non ogni tanto.

La domanda che decide l'architettura è una sola: dove vive quel contesto fra un messaggio e il successivo?

La risposta ortodossa è: da nessuna parte nel processo — lo si scrive su un archivio condiviso, tipicamente Redis, e lo si rilegge a ogni colpo. È la risposta che rende i processi intercambiabili, e per moltissimi casi è quella giusta. Per questo caso non lo è, e vale la pena spiegare esattamente perché.

Come leggere questo documento

Descrive il piano nella sua forma compiuta, e argomenta le decisioni che lo hanno prodotto. Lo stato di avanzamento non sta qui: vive in un documento suo, voce per voce e con la prova accanto, ed è l'unica cosa che cambia mentre il lavoro procede. Il rimando è in appendice.

Prima ragioneIl canale aperto cambia l'aritmetica

Finché il dialogo è HTTP, la frequenza è bassa: una manciata di richieste al minuto per utente. Pagare una serializzazione e una deserializzazione a ogni richiesta è un costo trascurabile — è rumore rispetto al resto.

Con un canale WebSocket aperto l'aritmetica cambia di ordine di grandezza. Una pagina viva non fa richieste: conversa. Ogni tasto in un campo con completamento, ogni movimento in una griglia, ogni cambio di selezione, ogni notifica che scende, ogni battito di presenza è un messaggio. Il traffico diventa continuo e minuto: tanti messaggi piccoli, non pochi messaggi grandi.

Con lo stato fuori dal processo, ogni messaggio paga il costo di ricostruire il mondo prima di poter fare la cosa piccola che gli è stata chiesta.

E il costo non è la rete: è la serializzazione. Ricomporre l'oggetto, deserializzarlo, rimetterlo insieme, e poi riserializzarlo per riscriverlo. Su un messaggio ogni pochi secondi non lo noti; su un dialogo continuo moltiplicato per il numero di utenti collegati diventa la voce di costo dominante del server — e il paradosso è che più l'applicazione è reattiva, più paga.

Questa è la prima ragione per cui, in questo scenario, teniamo lo stato dove serve: in memoria, nel processo che sta servendo quella pagina.

Il canale, in concreto

Il protocollo che porta questo dialogo è WSX — WebSocket eXtended: semantica HTTP sopra un canale WebSocket, così un messaggio che arriva dal socket trova lo stesso albero di rotte, gli stessi plugin e la stessa autorizzazione di una richiesta HTTP. Un solo motore di smistamento, due trasporti innestati.

L'ordine in cui le due cose sono state costruite non è casuale: l'architettura dello stato viene prima del canale, perché è il canale a renderne evidente la necessità. Un WSX appoggiato su un modello senza stato dà un sistema che funziona in dimostrazione e cede sotto carico reale — il traffico che rende bella l'applicazione è lo stesso che ne moltiplica il costo.

Sul formato dei messaggi c'è un'altra leva, e riduce il costo dall'altro lato. Ciò che viaggia è descritto dal protocollo tipizzato, non dal formato in cui è scritto: gli stessi messaggi possono andare in JSON — leggibile, comodo mentre si sviluppa — oppure in MessagePack, forma binaria più compatta e più rapida da comporre e leggere. È un parametro, non una riscrittura, e non tocca una riga di codice applicativo. Su un dialogo fitto è la differenza fra pagare la leggibilità sempre e pagarla solo quando serve.

Seconda ragioneGli oggetti che non entrano nel tubo

C'è un secondo argomento, indipendente dal primo e più tagliente: ci sono oggetti che semplicemente non si serializzano a ogni giro.

Un dataframe con un milione di righe caricato per un'analisi. Una struttura calcolata a caro prezzo su cui l'utente sta iterando. Una connessione già aperta, un cursore, un contesto di calcolo. Sono oggetti grossi e mutabili: l'utente li modifica un pezzetto alla volta, e ogni modifica è piccola rispetto all'oggetto.

Con lo stato esterno, quella modifica piccola costa quanto l'oggetto intero: rileggi tutto, cambi una colonna, riscrivi tutto. Non è una questione di ottimizzazione — l'operazione è strutturalmente sproporzionata al lavoro che compie. E per alcuni di questi oggetti la serializzazione non è nemmeno possibile in modo sensato.

La conseguenza pratica, nei sistemi che scelgono comunque la via senza stato, è che quegli oggetti finiscono per non esistere: l'architettura respinge il caso d'uso. L'utente riceve una pagina che ricarica invece di una che lavora, e la funzionalità viene riprogettata per stare dentro il vincolo tecnico.

Non abbiamo scelto lo stato per comodità. Lo abbiamo scelto perché senza, certe funzionalità non si possono nemmeno proporre.

La conseguenzaSe lo stato sta nel processo, l'utente deve tornarci

Una volta deciso che il contesto di una sessione di lavoro vive nella memoria di un processo, tutto il resto discende per necessità, non per gusto.

Se lo stato di Maria è nel processo 3, ogni messaggio di Maria deve arrivare al processo 3. Non alla macchina meno carica, non a rotazione: a quello. La chiave con cui si partiziona il carico non è più la richiesta — è l'utente.

Da qui il nome: user sticky. L'utente è appiccicato al proprio processo, e il processo è l'unità che possiede una porzione della popolazione.

La seconda unità è la pagina. Una persona apre più schede, e ognuna è una sessione di lavoro a sé, con il proprio contesto. Quindi lo stato vivo si organizza su tre livelli annidati: l'utente possiede le sue connessioni, ogni connessione possiede le sue pagine, ogni pagina possiede il proprio albero di dati.

utente la chiave di partizione connessione una scheda del browser connessione un'altra scheda pagina · il suo stato pagina · il suo stato pagina · il suo stato tutto questo vive in UN processo

L'obiezione ovvia«Questo lo fa già un bilanciatore»

È la prima domanda che riceve chiunque proponga questa architettura, ed è una domanda giusta. Traefik, nginx, HAProxy sanno tutti fare sticky session: emettono un cookie, e le richieste che lo portano tornano allo stesso backend. Perché costruire qualcosa quando esiste già?

Perché instradare è la parte facile, ed è l'unica che un bilanciatore può fare. Un bilanciatore vede pacchetti e connessioni TCP; non sa nulla di ciò che sta dentro il processo a cui li consegna. In particolare:

ServePerché il bilanciatore non può
Instradare per identità, non per cookie L'identità applicativa cambia in corsa: un visitatore arriva anonimo e a metà sessione fa login diventando un utente reale, che magari ha già un'altra scheda aperta altrove. Il bilanciatore vede due cookie diversi; l'applicazione sa che è la stessa persona e che le due sessioni devono ricongiungersi.
Spostare lo stato, non solo il traffico È il punto centrale. Un bilanciatore può mandare Maria a un altro processo, ma non può portarci il suo lavoro: la griglia filtrata, il documento aperto, il dataframe restano nel processo di prima. Reindirizzare senza trasferire non è un bilanciamento — è una perdita di dati travestita.
Misurare la risorsa che conta Un bilanciatore conta richieste al secondo e connessioni. Qui la risorsa che si esaurisce è la memoria, e la satura un utente che sta fermo con un oggetto grosso in pancia — un utente che, in termini di richieste, è invisibile.
Conoscere la struttura della popolazione Per il bilanciatore l'unità è la connessione. Qui una persona ha N connessioni e M pagine, e la decisione — spostare, drenare, sfrattare — si prende sulla persona intera, con tutto ciò che le appartiene.
Ordinare qualcosa al processo Un bilanciatore parla ai backend solo mandando traffico. Qui serve un canale di comando bidirezionale: consegnami l'utente Maria con tutto il suo stato, installa questo pacchetto di sessione, dimmi quanta memoria stai realmente usando. Sono conversazioni che il protocollo di un proxy non prevede.
Ritirare un processo senza danni Rilasciare una versione, sostituire un processo che perde memoria, restringere il pool di notte: tutte operazioni ordinarie che con la sola stickiness significano buttare via le sessioni di chi era collegato.

Nessuna di queste è una critica ai bilanciatori: fanno benissimo il lavoro per cui sono costruiti, e nella nostra architettura uno di essi sta davanti comunque. Il punto è che quel lavoro non è questo lavoro. Il bilanciatore instrada verso una macchina; ciò che decide dove vive una persona, e come ce la si sposta, deve stare dentro l'applicazione, perché solo lì si sa cosa sia una persona.

Il rischio da evitareLa trappola della stickiness

C'è un modo sbagliato di fare esattamente questa cosa, ed è il modo in cui viene fatta di solito: si attiva la sticky session sul bilanciatore, si tiene lo stato in memoria, e funziona. Funziona finché non devi cambiare qualcosa.

Il giorno del rilascio scopri che non puoi sostituire un processo senza scollegare chi ci sta sopra. Il giorno in cui un processo comincia a gonfiarsi scopri che non puoi riavviarlo. Il giorno in cui il carico si sbilancia scopri che non puoi ribilanciare, perché spostare un utente significa perderne il lavoro.

La stickiness senza mobilità dello stato non è un'architettura: è un vincolo che ti impedisce di toccare il sistema mentre è vivo.

La soluzione tipica a quel punto è rassegnarsi: si rilascia di notte, si accetta di scollegare tutti, si sovradimensiona per non dover mai ribilanciare. Sono compromessi che si pagano ogni settimana per sempre.

Per questo la primitiva fondante del nostro piano non è l'instradamento appiccicato — quello è la parte facile — ma lo spostamento a caldo di un utente con tutto il suo stato. È ciò che rende la stickiness sostenibile invece che paralizzante, ed è l'operazione da cui dipende tutto il resto: il ribilanciamento, la sostituzione di un processo, il versionamento e il ritorno dopo un riavvio sono quella stessa primitiva, vista da lati diversi.

Il prezzoCosa serve davvero per farlo

Vale la pena essere espliciti sul costo, perché è la parte che si sottovaluta sempre. «Sposta l'utente da un processo all'altro» è una frase breve che nasconde una quantità di problemi reali.

Chi possiede quale verità

Se due livelli sanno la stessa cosa, prima o poi divergono. La regola è: un solo scrittore per ogni fatto. Il processo possiede la verità delle sue pagine; il supervisore possiede la verità di chi-sta-dove. Nessuno scrive mai dentro il registro di un altro livello.

Il momento in cui l'utente non è da nessuna parte

Durante uno spostamento c'è un istante in cui la sessione è stata staccata dall'origine e non è ancora installata a destinazione. Se una richiesta arriva proprio allora, deve attendere, non fallire — e se l'installazione non riesce, l'utente deve restare dov'era, non sparire.

Le chiamate in volo

Non si può portare via una sessione mentre sta rispondendo a qualcosa. Serve una fase di quiete: si alza una barriera, si aspetta che le chiamate vive finiscano, e solo allora si fa il pacco. Con un tempo massimo, perché aspettare per sempre è un altro modo di rompersi.

L'ordine della rinascita

A destinazione le cose vanno ricostruite in una sequenza precisa: prima l'utente, poi le connessioni, poi le pagine; e i collettori di modifiche vanno attaccati dopo che i dati sono stati reidratati, altrimenti la reidratazione stessa verrebbe scambiata per una raffica di cambiamenti da notificare al browser.

La morte di un processo

Un processo può morire in qualunque momento, anche mentre gli si sta parlando. Va distinto un ritiro voluto da un crollo, rilanciato con un nome nuovo perché nessun messaggio in ritardo venga scambiato per il nuovo arrivato, e liberato tutto ciò che teneva — compresi i permessi di scrittura che aveva in mano.

Sapere quando intervenire

Per decidere serve misurare, e la misura giusta non è il carico medio ma la saturazione della risorsa che si esaurisce per prima. E va misurata onestamente: la memoria che un processo dichiara non è quella che sta davvero usando.

Perché questa parte non si compra

Ognuno di questi problemi è risolvibile; il punto è che vanno risolti insieme, e che le loro soluzioni si vincolano a vicenda. È il motivo per cui non esiste un componente da installare che faccia questo lavoro: la logica di spostamento deve conoscere la forma dello stato applicativo, e la forma dello stato è dell'applicazione, non dell'infrastruttura.

La soluzioneIl piano SPA

Tutto quanto sopra ha una forma concreta. Tre pezzi, un principio.

Il front — un'applicazione come le altre le sue rotte rispondono native · tutto il resto appartiene al sito ospitato e viene inoltrato · non tiene stato proprio Il supervisore — chiavi e posizioni, mai contenuti chi-sta-dove · nascita e morte dei processi · ribilanciamento, riciclo, compattazione · instradamento dei cambiamenti canale di comando Processo i contenuti veri: utenti, connessioni, pagine, con i loro dati vivi Processo esegue gli handler del sito su pool separati da quelli di servizio Processo legge il globale con una chiamata sottoscrizioni alle tabelle

Il principio che tiene insieme il disegno: chiavi e posizioni sopra, contenuti sotto. Il supervisore sa dove si trova ogni persona, e non tocca mai ciò che quella persona possiede. Non ha una copia da tenere allineata, quindi non c'è un duplicato che possa divergere.

Il front è un'applicazione ordinaria che si monta come qualunque altra: le sue rotte rispondono da sé, e ciò che appartiene al sito ospitato viene inoltrato al processo giusto. Non conserva stato proprio — legge quello che il supervisore già sa.

L'identità è una sola, ed è quella del sito. Il front non conia niente: il sito ospitato battezza la propria connessione mentre serve, la risposta riporta quel nome, e il cookie lo conserva. Non esiste una seconda identità da tradurre nella prima, quindi non esiste il punto in cui le due possono divergere — e le mappe del supervisore sono indicizzate su nomi veri, non su un identificatore inventato dal front. Il cookie vive ventiquattr'ore, la vita che il sito dà alla propria connessione.

In sviluppo tutto questo gira in un solo processo: il worker vive dentro il supervisore, attaccato allo stesso canale, che in quel caso è una coppia di code in memoria invece di un socket — ma codifica ogni messaggio esattamente allo stesso modo. Se il protocollo funziona nel ruolo singolo, passare a molti processi cambia solo il filo.

Le capacitàCosa fa, in concreto

Lo stato vivo e la sua consegna

  • Un albero di dati per pagina. Ogni scheda aperta ha il proprio stato sul server, e ciò che cambia le viene consegnato al primo giro utile. La consegna è a richiesta, non spinta: se una pagina tace, i suoi aggiornamenti l'aspettano nel collettore invece di perdersi.
  • Aggiornamenti dai dati. Una pagina sottoscrive le tabelle che la interessano; una modifica raggiunge tutti i sottoscrittori ovunque siedano. Chi ha originato il cambiamento serve prima i propri, poi il resto viaggia: una modifica su una tabella che nessuno guarda non costa un solo invio.
  • Cache invalidata da sé. Un valore memorizzato in una pagina porta il segno della tabella da cui viene; quando quella tabella cambia, il valore viene invalidato con una scrittura vera, che la pagina riceve come ogni altro aggiornamento.
  • Store globale condiviso. Un albero comune a tutti i processi, con un solo scrittore e nessuna replica: il master vive sul supervisore, e un processo lo legge con una chiamata sul canale che già tiene aperto. È la scelta che toglie di mezzo il problema di allineare le copie — non ci sono copie da allineare. Le modifiche in lettura-e-scrittura passano da una concessione ordinata, tutto-o-niente: la concessione porta con sé il contenuto del master, quindi chi la riceve non deve chiedersi se quello che ha in mano è aggiornato, e si libera da sé se chi la teneva muore.

Il governo dei processi

  • Spostamento a caldo, per una strada sola. Tutto ciò che appartiene a una persona si congela dov'è, viene riassegnato, e rinasce al primo colpo che quella persona batte. I passi sono sempre gli stessi quattro — si alza la barriera, si congela, si riassegna, si risveglia — e servono tutti i casi: restringere il pool, sostituire un processo in modo ordinato, riportare dentro chi c'era prima di un riavvio. Il passaggio diretto da un processo all'altro è stato eliminato di proposito: una strada sola è una strada che si prova davvero. Chi arriva mentre la persona è fra due case attende sulla barriera, non fallisce.
  • Bilanciamento sulla risorsa giusta. Si osserva la saturazione per componente, non un carico medio: un processo oltre soglia su una sola risorsa cede utenti anche se tutto il resto sembra tranquillo. Chi va via viene scelto in proporzione a quanto ha consumato di recente.
  • Ricambio prima del limite. Il sistema tiene memoria di come cresce la memoria di ogni processo e ne sostituisce uno prima che raggiunga il tetto, portando i suoi utenti su un processo fresco. Il problema classico dei processi che si gonfiano diventa un'operazione pianificata invece di un incidente.
  • Compattazione. Quando il carico cala, il pool si restringe: si drena il processo meno carico e lo si ritira. Un processo che tiene ancora qualcosa non viene mai ritirato.
  • Sorveglianza che serve a due cose. Una sonda periodica chiede a ogni processo i suoi numeri: la risposta porta i dati e la prova che il processo è ancora vivo per produrli. Il silenzio è esso stesso l'informazione, e un processo muto viene abbattuto e rilanciato.
  • Un riavvio completo non perde le sessioni. Andando giù, ogni gruppo parcheggia i suoi utenti congelati e il supervisore scrive accanto le proprie mappe; la cartella prende il nome definitivo solo quando la fotografia è completa, quindi il nome giusto è la prova che non manca niente. Risalendo, le mappe tornano al loro posto e nessuno viene riacceso in anticipo: la prima richiesta di una persona è ciò che la risveglia. Chi torna dopo il riavvio trova il proprio cookie ancora valido, la propria identità, e nessun nuovo login da fare.
  • Un gruppo può nascere da uno stampo. Un gruppo che dichiara come costruire il proprio motore possiede un processo template: lo costruisce una volta sola, ne congela la memoria, e ogni worker di quel gruppo è un suo fork. Accendere un processo in più costa un fork invece di un avvio da zero.
  • Ogni gruppo sul proprio interprete. La ricetta di un gruppo dichiara con quale Python eseguirlo: il processo del supervisore sta sul suo, i worker di ogni gruppo sul proprio. Gli interpreti li porta l'immagine; il gruppo dice quale gli serve.

Il ponte con l'esistente

  • Un sito già scritto può salire a bordo. Il processo sa sintetizzare l'ambiente standard di un'applicazione WSGI e invocarla al proprio interno, su un pool di thread dedicato e separato da quello delle operazioni di servizio — così una raffica di richieste al sito non affama la macchina di orchestrazione. WSGI come adattatore, mai come trasporto: un applicativo esistente entra nel piano senza essere riscritto, e guadagna sessione appiccicata, stato per pagina e spostamento a caldo.
  • Il monitor. Una chiamata sola compone la popolazione viva: per ogni processo i suoi utenti, le sue connessioni, le sue pagine, con i consumi cumulativi di ciascuno; e per ogni processo lo stato nel ciclo di vita, quando è nato e, se è morto, quando e come. Un processo irraggiungibile non fa cadere la lettura: compare con il proprio stato di irraggiungibilità.
La linea che divide i due documenti

C'è una regola che dice dove vive ogni cosa: ciò che deve sopravvivere alla morte di un processo sta nel server — sessioni, login, stato dei batch, e sono le cose del primo documento; ciò che muore e viene ricostruito col suo processo sta nell'orchestrazione — pagine, attese, sottoscrizioni. È la stessa linea che rende un riavvio un'operazione ordinaria invece di un evento.

Il pannelloCome si guarda un pool vivo

Tutto quanto descritto finora produce informazione, e quell'informazione ha una forma. Le illustrazioni che seguono non sono schermate di un prodotto finito: mostrano quali dati la superficie di supervisione già possiede, e come si leggono insieme.

La cosa da notare è che le due viste rispondono a due domande diverse. La prima guarda i processi: come stanno, quanto reggono, cosa conviene fare. La seconda guarda le persone: chi c'è, dove sta, quanto pesa.

I processi

supervisione · processi 4 attivi · 1 in drenaggio · 187 persone PROCESSO STATO SATURAZIONE MEMORIA PERSONE PAGINE VITA w-7·a41 accoglienza attivo 0.64 memoria 0.64 · thread 0.31 · attese 0.12 1.9 GB pavimento in crescita · limite fra ~6 h 52 96 2 g 4 h w-8·b12 attivo 0.85 oltre la soglia · ribilanciamento in corso 3.4 GB limite fra ~40 min · ricambio programmato 61 128 5 g 1 h w-9·c03 attivo 0.19 appena nato · accoglie i ribilanciati 0.6 GB 18 24 12 min w-6·9fe drenaggio 0.12 fuori da ogni scelta · si ritira quando è vuoto 3.9 GB 6 9 6 g 3 h DECISIONE IN CORSO w-8 oltre soglia sulla memoria → 14 persone verso w-9, le più pesanti per prime · poi ricambio di w-8 su processo fresco

Ogni riga porta le grandezze su cui il sistema decide davvero: la saturazione per componente con il totale accanto — perché è il componente peggiore a vincolare, non la media — e il pavimento di memoria con la stima di quando toccherà il limite, che è ciò su cui si fonda il ricambio programmato. In basso, la decisione che il pool sta eseguendo: mostrarla è ciò che rende un sistema automatico ispezionabile invece che magico.

Le persone

supervisione · persone 187 collegate · 14 anonime PERSONA DOVE CONNESSIONI PAGINE CONSUMO ULTIMA ATTIVITÀ AZIONI m.rossi@acme.it w-7·a41 2 5 · 1 in attesa 1.240 chiam · 82 s 4 s sposta g.bianchi@acme.it w-8·b12 1 3 9.870 chiam · 610 s 1 s sposta l.verdi@acme.it w-8·b12 in trasferimento → w-9 1 8 · 2 in attesa 21.400 chiam · 2.140 s 2 s in corso guest_8f21c4 w-7·a41 1 1 12 chiam · 0.4 s 3 min anonima

Il consumo cumulativo di ciascuna persona è la base su cui il ribilanciamento sceglie chi spostare: chi ha consumato più servizio di recente pesa più di chi ha molte pagine aperte e ferme. La terza riga mostra un trasferimento mentre accade — lo stato che in un sistema con stickiness e senza mobilità semplicemente non esiste.

VersioniI gruppi: due versioni vive nello stesso momento

Un gruppo è un insieme di processi che eseguono la stessa versione dell'applicazione con il proprio runtime. Il pool non è più una fila di processi identici: è un pool di gruppi, e ogni utente appartiene al gruppo del processo che lo tiene.

Serve per la cosa che senza di essa è impossibile: rilasciare una versione nuova mentre la gente lavora. Si porta su un gruppo con il codice nuovo, gli si spostano gli utenti a caldo — con la primitiva che già esiste, quella che porta la sessione intera senza perdere il lavoro in corso — e si ritira il gruppo vecchio quando è vuoto. Chi era collegato non se ne accorge, e chi arriva dopo entra sulla versione nuova.

Se qualcosa va storto, la stessa primitiva riporta indietro le persone: il rollback non è un ripristino, è uno spostamento nella direzione opposta.

Non solo rilasci: mandare qualcuno avanti

Il rilascio in blocco è il caso grosso, ed è il più raro. L'uso quotidiano dei gruppi è un altro, e vale la pena metterlo in evidenza perché è quello che si spende ogni settimana con i clienti.

La maggior parte delle novità non è una versione nuova del prodotto: è un bottone, un report modificato, una variante di comportamento chiesta da qualcuno. Con i gruppi si può portare quella modifica in esercizio e mandarci sopra solo chi deve provarla — un cliente, un reparto, una singola persona — mentre tutti gli altri continuano sulla versione stabile senza accorgersi di nulla.

Il costo dell'operazione è basso, ed è basso per una ragione precisa: il Framework non si tocca. Quello che si costruisce e si spedisce è il bundle dell'applicazione, che pesa quanto l'applicazione e non quanto la piattaforma sotto. Non c'è un artefatto grosso da produrre e distribuire per far provare un bottone a tre persone.

Chi va sul gruppo di prova ci va con lo stesso meccanismo che assegna chiunque altro, e se la prova va male torna indietro a caldo, con la sessione di lavoro intatta. Il betatester non è una categoria speciale del sistema: è una persona assegnata a un gruppo diverso.

La differenza rispetto a un rilascio ordinario

Un deploy classico su processi senza stato è banale: spegni i vecchi, accendi i nuovi, il bilanciatore smette di mandarci traffico. Qui i processi tengono qualcosa, e spegnerli significa buttarlo. È il motivo per cui i gruppi e lo spostamento a caldo sono la stessa idea vista da due lati: la mobilità dello stato è ciò che rende il versionamento possibile.

Il Framework è un'immagine, l'applicazione è un bundle

Ogni gruppo dichiara che cosa esegue. La domanda vera è chi lo costruisce e quando, e la risposta è la decisione che tiene in piedi tutto il meccanismo: le cose che cambiano con ritmi diversi viaggiano separate.

Il Framework — il server, il supervisore, gli interpreti, tutto ciò che è comune — è un'immagine sola, identica per ogni cliente. Si aggiorna come si aggiorna qualsiasi applicazione: si pubblica una versione nuova e si passa a quella. Non si aggiorna per cliente, né per gruppo, né per utente.

L'applicazione viaggia a parte, come bundle: una build esatta del codice del cliente e delle sue dipendenze, costruita dal repository dell'applicazione con la propria CI, archiviata immutabile e ritirata al momento dell'uso. Un gruppo monta un bundle. Non installa niente all'avvio: apre qualcosa che esiste già, verificato.

L'immagine porta la macchina, il bundle l'applicazione

La divisione è quella: l'immagine sa eseguire le applicazioni, il bundle è l'applicazione da eseguire. Un cliente che chiede una modifica non fa ricostruire il Framework a nessuno — la sua CI produce un bundle e basta.

Un nome logico, molte build

Una build fisica non cambia mai. Un nome logico — collaudo-fatture, produzione — punta di volta in volta a una build diversa. Promuovere significa spostare il nome, non ricostruire: la build che il cliente ha accettato va in esercizio esattamente com'era.

Due Python, e più di due

Il control plane sta sul suo interprete, i worker di un gruppo sul proprio. L'immagine porta gli interpreti che deve servire, il bundle dichiara per quale ABI è stato costruito, e la ricetta del gruppo dice quale usare. Un gruppo su una versione di Python e un altro su una diversa convivono sulla stessa macchina.

Costruito una volta, non a ogni avvio

Risolvere le dipendenze è l'operazione lenta e l'unica che può fallire per ragioni esterne — un indice non raggiungibile, una versione ritirata. Farla in CI significa che non può accadere durante un rilascio: al momento del rilascio il bundle esiste già, con la sua impronta verificata prima di essere attivato.

Una modifica piccola tocca molti repository

È il caso che decide il formato. Un cliente chiede una modifica alle fatture; lo sviluppatore interviene sull'applicativo, su un modulo del framework applicativo e su due package condivisi. Quello che va collaudato non è un file: è una combinazione precisa di quattro repository.

Per questo un bundle non si identifica con un numero di versione leggibile: il suo manifesto registra l'origine esatta di ogni pezzo, commit per commit. Un nome di ramo non basta a dire che cosa si sta provando.

La CI sta fuori dal percorso di esecuzione

È una scelta deliberata e vale la pena dichiararla: la CI pubblica il bundle sull'archivio condiviso, e finisce lì il suo compito. Chi avvia un processo lo legge da quell'archivio, in sola lettura, mai dalla forge.

La conseguenza è che il sistema si riavvia, si ribilancia e sostituisce processi anche con la piattaforma di integrazione irraggiungibile. Il momento in cui serve più affidabilità è quello in cui qualcosa è già andato storto, e in quel momento il numero di dipendenze esterne deve essere il più basso possibile.

Lo stesso meccanismo dichiarativo — il gruppo descrive quello che vuole eseguire, e chi avvia i processi lo realizza — ammette altri modi di confezionare un'applicazione senza che nulla cambi nel resto del disegno: cambia l'attuatore, non il contratto.

La videata dei gruppi

supervisione · gruppi 2 versioni vive · rilascio in corso GRUPPO CODICE BUNDLE PROCESSI PERSONE STATO AZIONI stabile predefinito 1.4.2 · 8f21c fatture/012 · py 3.12 build immutabile · canale «produzione» 3 165 in uso in svuotamento nuova 1.5.0 · d0c74 venv b93f2 · py 3.13.1 lockfile diverso · pacchetti comuni deduplicati 2 22 in arrivo promuovi TRAVASO VERSO «NUOVA» 22 di 187 a caldo · nessuna sessione perduta SE QUALCOSA VA STORTO rientro = travaso nella direzione opposta MIGRAZIONI solo additive finché due versioni convivono

Il rilascio ha il suo termometro come un batch: dice quante persone sono già passate e quante restano, e mentre scorre entrambe le versioni servono traffico reale. «In svuotamento» non significa spenta — significa che non riceve più nuovi arrivi. La barra è la stessa primitiva dello spostamento a caldo, applicata a un elenco di persone invece che a una sola.

Lo stato di lavoro attraversa le versioni

C'è una conseguenza del versionamento che riguarda proprio ciò che rende speciale questo piano, e va affrontata di petto: spostare una persona da un gruppo all'altro significa che il pacchetto del suo stato viene prodotto da una versione e installato da un'altra. È lo stesso trasferimento a caldo di sempre, ma con un codice diverso alle due estremità.

Finché i cambiamenti sono additivi — un campo nuovo nello stato di una pagina, una chiave in più — la cosa è trasparente: la versione vecchia ignora ciò che non conosce, la nuova tollera ciò che non trova. È la stessa disciplina che vale per il database, applicata alla memoria invece che al disco.

Quando invece la forma dello stato cambia davvero — una struttura riorganizzata, un oggetto che non ha più lo stesso significato — la regola è netta e vale la pena conoscerla:

Uno stato che la destinazione non riconosce non viene indovinato: la pagina riparte pulita, e lo dice.

Nessuna reidratazione approssimata, nessun tentativo di adattare al volo una struttura a un codice che si aspetta altro. Il costo è dichiarato e circoscritto: chi stava lavorando su quella pagina perde il contesto della pagina — la griglia filtrata, la selezione in corso — e la ritrova pulita. Non perde la sessione, non perde il login, non perde nulla di ciò che vive nel server: quello sopravvive, per la stessa linea di taglio che regge tutto il disegno.

Questo impone una disciplina a chi scrive una versione nuova, ed è esattamente l'analogo delle migrazioni additive: additivo è trasparente, strutturale è una ripartenza dichiarata. Un cambiamento di forma non è vietato — va saputo, perché ha un prezzo visibile per chi in quel momento sta lavorando.

Ciò che rende praticabile tutto questo è un vincolo posto all'origine: i registri contengono dati serializzabili per costruzione, mai oggetti vivi come verità. Un oggetto vivo non si può spedire e non si può versionare — e la regola esiste da prima che servisse, perché aggiungerla dopo avrebbe significato riscrivere ciò che nel frattempo si era appoggiato all'abitudine opposta.

La regola che il versionamento impone al database

Due versioni vive condividono un solo database, e questo è un vincolo, non un dettaglio: i gruppi versionano il codice, non lo schema. Finché due versioni convivono le migrazioni possono essere solo additive — tabelle e colonne nuove, annullabili o con valore predefinito, indici costruiti senza bloccare — e mai rimozioni o ridenominazioni: un rename è una rimozione travestita, e manda in errore la versione che non lo conosce.

La vecchia versione ignora ciò che non conosce, la nuova tollera il dato mancante. Le pulizie si fanno dopo, in una finestra in cui è rimasta una sola versione — e che quella condizione sia vera è un fatto che il sistema sa, perché sa quali gruppi sono vivi.

OsservabilitàIl secondo livello di metriche

Il primo documento descrive le metriche che un processo espone su di sé, nel formato che i raccoglitori consumano. Qui si aggiunge un livello, e con esso un problema di raccolta che vale la pena spiegare perché la soluzione è già nel disegno.

I processi di lavoro non parlano HTTP. È una conseguenza voluta: un processo che tiene stato non si affaccia sulla rete, parla solo sul canale col proprio supervisore. Quindi nessun raccoglitore può interrogarli direttamente, e non è un limite da aggirare — è la stessa proprietà che li tiene fuori dal perimetro esposto.

Il supervisore è l'unico punto di raccolta, e non ha bisogno di un meccanismo nuovo per esserlo.

La sonda che già gira — quella che ogni pochi secondi chiede a ciascun processo i suoi numeri e nel farlo ne verifica la vitalità — è la raccolta. Il supervisore archivia quelle letture per governare il pool, e le ripubblica nel formato standard etichettate per processo. Un raccoglitore interroga un solo indirizzo e ottiene la fotografia di tutti, senza che nulla debba essere esposto in più.

Per ogni processo

Saturazione totale e per singolo componente, memoria viva e pavimento della sua crescita, persone e pagine ospitate, stato nel ciclo di vita, età. Sono le stesse grandezze su cui il pool decide: chi guarda i grafici vede quello che vede il sistema.

Per il pool

Ampiezza attuale, nascite e ritiri con la loro causa — crescita, ricambio, compattazione — spostamenti eseguiti e loro durata, e le morti distinte per genere: ritiro voluto o caduta.

Il traffico dello stato vivo

Aggiornamenti consegnati e in attesa nei collettori, notifiche dai dati e loro fan-out, concessioni sullo store globale e tempo di attesa in coda. È qui che si legge se il costo del dialogo continuo sta crescendo.

Le anomalie che contano

Sonde scadute, processi abbattuti per silenzio, spostamenti falliti e indirizzi non risolti. Ognuna di queste, in un sistema sano, è una curva piatta a zero — ed è per questo che vale la pena guardarla.

Perché lo stesso formato a ogni livello

Le grandezze dei tre livelli sono diverse, il linguaggio è lo stesso. Chi raccoglie non deve imparare tre protocolli né installare tre adattatori, e — soprattutto — gli allarmi si scrivono una volta: la regola «la saturazione di qualcosa sta sopra la soglia da dieci minuti» ha la stessa forma se quel qualcosa è un processo, un pool o un dominio intero.

AppendiceDove siamo oggi

Questo documento descrive il piano nella sua forma compiuta: quello che il sistema è quando il lavoro è finito. Non dichiara, riga per riga, che cosa di tutto ciò sia già scritto — perché quello cambia ogni settimana, e una pagina che lo mescola alla descrizione invecchia tutta insieme.

Lo stato di avanzamento vive in un documento suo: stato attuale, voce per voce, con la prova accanto a ciascuna — un file con la sua copertura, un test che la esercita, oppure la constatazione che non esiste una riga di codice. La parte che riguarda questo capitolo è il ramo il mondo SPA.

La divisione è deliberata. Le pagine dei quattro documenti non cambiano quando una voce di quel documento cambia stato.