genro-asgi · documento 2 di 3

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. La roadmap in appendice dichiara, voce per voce, cosa è già operativo oggi e cosa è in corso di implementazione: è l'unica parte che cambia mentre il lavoro procede.

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 è la ragione per cui il file di test più esercitato dell'intero progetto è quello che verifica lo spostamento: 119 casi, perché è l'operazione da cui dipende tutto il resto.

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 replica dello store globale 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.

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 una replica locale per ciascuno: le letture non costano un giro di rete. Le modifiche in lettura-e-scrittura passano da una concessione ordinata, tutto-o-niente, che viene rilasciata da sé se chi la teneva muore.

Il governo dei processi

  • Spostamento a caldo. Tutto ciò che appartiene a una persona viaggia da un processo all'altro e rinasce nell'ordine giusto. La mappa si aggiorna per ultima: finché l'installazione non è confermata, la persona continua a essere servita dove si trova, e se la destinazione muore le si offre un altro posto.
  • 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ù, il pool raccoglie ogni utente e lo scrive su file; risalendo, li ripiazza prima che qualcosa possa essere instradato.

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: quando la modifica non tocca le dipendenze, il lockfile è lo stesso e i due gruppi condividono l'ambiente. Quello che li distingue è il commit, che viaggia come differenza — kilobyte, non centinaia di megabyte. Non c'è un artefatto grosso da costruire 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 runtime di un gruppo è un artefatto, non un'installazione

Ogni gruppo dichiara il proprio runtime: quale interprete e quale codice. La domanda vera è chi lo costruisce e quando, e la risposta è la decisione che tiene in piedi tutto il meccanismo.

Il runtime non viene assemblato sulla macchina che deve servire le richieste. Lo costruisce la CI, una volta, con uv: uv sync --frozen materializza l'ambiente esattamente come il lockfile lo descrive, e la versione diventa una directory immutabile a percorso canonico che nessuno tocca più. Un gruppo non installa nulla all'avvio: apre una cartella che esiste già.

uv pinna anche l'interprete

È ciò che chiude il buco storico dei virtual environment: un venv tradizionale fissa i pacchetti ma non il Python sotto, e «python3.12» su due macchine può essere due cose diverse. Con gli interpreti standalone di uv la versione dell'ambiente comprende anche l'interprete, quindi due gruppi possono girare su Python differenti 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 l'ambiente esiste già, verificato.

Un artefatto che viaggia

L'ambiente si sposta come archivio rilocabile, con un nome che porta il tag di piattaforma nello stile delle wheel — architettura e versione minima della libreria di sistema. «Linux» non è un'informazione sufficiente, e chi lo scompatta verifica il tag prima di farlo.

Ambienti condivisi quando sono identici

Due gruppi con lo stesso lockfile condividono l'ambiente: nessuna copia. Quando differiscono, la cache di uv deduplica i pacchetti comuni con collegamenti fisici, così due versioni vicine costano poco più di una.

Il codice viaggia per differenza

Una versione è in realtà una coppia: l'ambiente e il commit. Le due metà cambiano con ritmi diversi — l'ambiente raramente, il codice spesso e per differenze minime, a volte tre moduli. Trattarle allo stesso modo significherebbe spedire centinaia di megabyte per cambiare tre file.

Quindi l'ambiente viaggia come archivio, e il codice come albero di lavoro git immutabile: un deposito di oggetti per macchina, e ogni versione è il suo albero al percorso canonico. Il delta si misura in kilobyte, gli oggetti comuni restano condivisi, e il rollback è ripuntare all'albero precedente. Non esiste un aggiornamento in place: quella è la strada che porta alla deriva, dove nessuno sa più con certezza quale codice stia girando.

La CI sta fuori dal percorso di esecuzione

È una scelta deliberata e vale la pena dichiararla: la CI pubblica gli artefatti su un archivio condiviso — l'ambiente e il pacchetto di codice, accanto — e finisce lì il suo compito. Chi avvia un processo li legge da quell'archivio, 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 il runtime che vuole, e chi avvia i processi lo realizza — ammette altri modi di confezionare un ambiente, immagini di container comprese, 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 AMBIENTE PROCESSI PERSONE STATO AZIONI stabile predefinito 1.4.2 · 8f21c venv a41c9 · py 3.12.9 condiviso con nessun altro gruppo 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.

AppendiceRoadmap: dove siamo

Il documento descrive il piano nella sua forma compiuta. Questa appendice è il solo posto dove si legge lo stato di avanzamento, e l'unico che cambia mentre l'implementazione procede.

La parte difficile è quella già fatta: lo spostamento a caldo di un utente con tutto il suo stato è operativo e verificato da 119 casi di test — è la primitiva da cui dipendono il ribilanciamento, il ricambio dei processi e il versionamento.

CapitoloStatoNota
Stato vivo per utente, connessione e paginaoperativo
Instradamento appiccicato all'identitàoperativocompresa la ricongiunzione al login di una sessione anonima
Spostamento a caldo con tutto lo statooperativofase di quiete, ordine di rinascita, mappa aggiornata per ultima
Consegna a richiesta degli aggiornamentioperativo
Sottoscrizioni ai dati e invalidazione della cacheoperativocon esclusione dell'origine dal fan-out
Store globale con replica per processooperativoconcessione ordinata, tutto-o-niente
Bilanciamento sulla saturazione per componenteoperativo
Ricambio dei processi prima del limite di memoriaoperativomisura onesta della memoria, serie storica per processo
Compattazione del pooloperativo
Sorveglianza e rilancio dei processioperativola sonda porta i dati e la prova di vita
Sopravvivenza a un riavvio completooperativoraccolta allo spegnimento, ripiazzamento all'avvio
Monitor della popolazione vivaoperativo
Ponte WSGI per un sito esistenteoperativopool dedicato, separato da quello di servizio
Ruolo singolo per lo sviluppooperativostesso protocollo, canale in memoria invece del socket
Metriche del pool in formato Prometheusin implementazionele letture esistono già — la sonda le raccoglie e il supervisore le archivia; manca la ripubblicazione nel formato standard
Trasporto WSX sul canale del browseroperativoil canale che rende continuo il dialogo con la pagina, sullo stesso motore di smistamento di HTTP
Compatibilità dello stato fra versioniin implementazionela regola è ratificata — uno stato non riconosciuto fa ripartire la pagina pulita, mai una reidratazione approssimata — e il vincolo di serializzabilità dei registri è già rispettato; manca il riconoscimento della forma nel pacchetto, che arriva con i gruppi perché è lì che due versioni si scambiano stato
Gruppi e versioni conviventiin implementazionela colonna del gruppo è già presente in ogni registro; restano l'instradamento per gruppo e la costruzione degli ambienti in CI
Ri-aggancio della pagina dopo un riavvioin implementazioneoggi la sessione applicativa sopravvive; il ri-aggancio della singola pagina per identificatore completa il quadro
Supervisore remoto e più macchinein implementazioneil protocollo è disegnato dall'inizio per due implementazioni, in-processo e su rete; oggi ne esiste una
Batch su processi dedicatiin implementazionemodello ed esecuzione locale completi; la distribuzione riusa questa stessa supervisione

Ultimo aggiornamento della roadmap: 13 agosto 2026. Le voci del corpo del documento non cambiano quando una riga di questa tabella passa a «operativo».

Gli altri due documenti

Il primo descrive il server su cui tutto questo poggia: routing agnostico dal trasporto, grammatiche di configurazione, OpenAPI e MCP sulla stessa superficie, autenticazione, sessioni, task e storage. Vale da solo, anche per chi non avrà mai bisogno di una riga di quanto descritto qui.

Il terzo porta questo stesso disegno oltre il confine della macchina: come si cresce su più nodi senza cedere a nessuno la decisione su dove vive una persona, e come si usa Kubernetes prendendone i muscoli e non il giudizio.