Spingere finché qualcosa cede: quanto regge davvero, e perché mi sono fermato prima dell’isolamento duro
Ultimo articolo della serie sul sandbox per agenti. Cosa dimostrano venti task concorrenti e cosa no; perché gli esperimenti di caos che uccidono i nodi non si possono eseguire su un portatile; e la teoria dietro «l’isolamento dei container finisce qui, dopo ci sono le macchine virtuali» — il punto in cui il mio hardware è finito.
Ognuno dei cinque articoli precedenti ha ripulito il disastro creato da quello prima: isolamento → avvio a freddo → costo a vuoto → stato perduto → code nei picchi. Ogni passo regge preso da solo.
Ma «regge da solo» e «reggono insieme» sono due affermazioni diverse. Quest’ultimo articolo fa due cose: spingere finché qualcosa cede e dire chiaramente dove mi sono fermato e perché.
Il test di carico: venti task lunghi in concorrenza
L’ultimo lab assembla i pezzi precedenti, e lo script di carico è essenziale:
async def main():
print("lancio 20 task concorrenti da 60 secondi...")
tasks = [submit_task(client, i) for i in range(20)]
await asyncio.gather(*tasks)
Venti richieste insieme, ciascuna con circa un minuto di lavoro, contro un pool di pochi container.
Cosa dimostra — le promesse degli articoli precedenti, mantenute:
- nessuno dei venti task va perso; gli eccedenti aspettano in coda;
- l’utente riceve una posizione in fila, non una pagina di errore;
- l’avanzamento è interrogabile in ogni momento, da qualunque container, perché vive sul disco condiviso;
- un container che finisce prende il successivo senza alcun intervento umano.
Cosa non dimostra — la metà più importante:
- venti concorrenti è un numero da portatile. I sistemi veri cedono nell’ordine delle centinaia o migliaia: la coda che si riempie più in fretta di quanto si svuoti, i limiti di connessione, le scritture su disco che rallentano;
- durante l’esecuzione non si è rotto nulla. Nessun container ucciso, nessun nodo ucciso, nessuna rete interrotta. Un test di carico misura l’essere occupati, non l’essere rotti;
- non sono stati misurati né quanto ci vuole ad accorgersi di un guasto né quanto ci vuole a ripristinare: nel sistema non c’è nulla in grado di misurarli.
Gli esperimenti di caos previsti sono esperimenti che non posso eseguire
Il progetto originale: reggere 500 sessioni attive, uccidere il 30% dei nodi del cluster, uccidere il primario della coda e misurare tre numeri — tempo di rilevamento, tempo di ripristino, quanti utenti hanno visto un errore.
Nessuno dei tre è possibile nel mio ambiente, per ragioni concrete:
- uccidere il 30% dei nodi: il cluster locale ha un nodo. Ucciderlo non è una prova, è uno spegnimento;
- uccidere il primario della coda: Redis gira come istanza singola senza replica, quindi non c’è alcun failover da osservare;
- 500 sessioni attive: questo portatile non regge 500 container che consumano memoria ciascuno.
Non è una scusa da «più avanti», è il prezzo d’ingresso di questo tipo di esperimenti: il chaos engineering richiede un cluster capace di assorbire danni, non una macchina capace di eseguire il codice. I primi cinque passi stanno su un portatile perché verificano difetti architetturali. Il settimo verifica il comportamento su scala, e la scala non si può simulare.
Già che siamo in tema di onestà, una cosa che l’articolo precedente aveva già ammesso: nemmeno il vero failover automatico esiste ancora. Quando un container muore, il task che teneva non viene riassegnato — qualcuno deve accorgersene e qualcuno deve rilanciarlo. Per colmarlo servono una tabella di stato dei task (chi lavora su cosa, da quando) e una regola di timeout (fermo troppo a lungo, si riprende e si riassegna). È questa la linea fra «si auto-ripara» e «si può riprendere».
Un passo più in là: l’isolamento dei container finisce qui
L’ultimo tema della serie è un passo che ho elaborato sulla carta senza costruirlo. Dirlo esplicitamente conta, altrimenti sembra fatto.
Torniamo alla domanda del primo articolo: un agente esegue codice che un modello ha generato poco prima. Un container per task ha impedito ai task di interferire tra loro — ma quanto è duro davvero il confine di un container?
Un’analogia: i container sono stanze divise dentro un unico edificio. I muri sono veri, ma fondamenta e impianti sono condivisi. Quegli impianti condivisi sono il kernel del sistema operativo: tutti i container usano lo stesso, e un suo difetto rende decorativi i tramezzi. Per i carichi ordinari il rischio è accettabile, perché il codice che gira è codice revisionato da te. Per il codice generato e non affidabile la premessa cambia.
Il settore offre due strade, entrambe verso «case separate»:
| Approccio | Come funziona | Cosa costa |
|---|---|---|
| gVisor | inserisce un kernel in spazio utente fra il container e quello reale, gestendo le system call al posto suo | parte comunque in fretta, ma alcune system call non sono supportate e certi carichi rallentano sensibilmente |
| Kata Containers | mette ogni container dentro una macchina virtuale leggera con un kernel proprio | il confine più duro, pagato in tempo di avvio e memoria — e richiede la virtualizzazione hardware |
Passare dall’uno all’altro in Kubernetes non è difficile: una RuntimeClass indirizza una classe di carichi verso un runtime diverso, senza modifiche applicative.
Non l’ho implementato, per una ragione precisa: il cluster locale gira dentro un container su un portatile, e annidarci dentro una macchina virtuale richiede la virtualizzazione annidata — e più memoria di quanta la macchina abbia. Questo passo non è poco chiaro: è finito l’hardware. Lo scrivo lo stesso perché sapere dov’è il confine e quanto costa è già il risultato del passo: il codice non affidabile finisce in una macchina virtuale, non in un container, e quel giudizio non richiede di averlo eseguito.
Il bilancio dell’intera serie
A ripensarci, nessun passo è stato semplicemente «meglio». Ognuno ha scambiato qualcosa con qualcos’altro:
| Ottenuto | Pagato |
|---|---|
| Isolamento | 45 secondi di avvio a freddo |
| Risposta immediata | una fila di container ferma in permanenza |
| Avanzamento che sopravvive a un crash | una dipendenza dallo storage e un compromesso sulla frequenza di scrittura |
| Picchi in coda invece che respinti | un componente in più che deve restare vivo |
| (non fatto) un confine duro per il codice non affidabile | tempo di avvio, memoria, requisiti hardware |
L’architettura non si progetta, la impongono le fatture. È per questo che è una sequenza di esperimenti e non un unico diagramma finale: presi singolarmente, il pool caldo, il disco condiviso e la coda sembrano tutti over-engineering. Percorsi nell’ordine dei conti, si vede benissimo cosa ha reso necessario ciascun componente.
Cosa resta sul tavolo, nel mio ordine di priorità: rendere reale il modello pull (il più economico, ed elimina di netto la contesa) → una tabella di stato dei task con riassegnazione a timeout (è questo che significa auto-riparazione) → storage scrivibile da più macchine → e solo dopo l’aggiornamento dell’isolamento.
In una riga: funzionare non è reggere; dichiarare cosa non è stato verificato vale più di un componente in più.
Tutto il codice: agent-sandbox-oss. Le idee sono benvenute come issue.
- Kubernetes
- AI Agent
- Chaos Engineering
- Isolamento e sicurezza
- Architettura