Finalizzazione batch a livello di container: perché il monitor non ha visto nulla per 83 minuti
Una pipeline con N sub-job paralleli finalizza lo stato a livello di batch — tutti i worker stanno girando, ma il monitor segnala blocco finché l'ultimo container non finisce. La soluzione: finalizzare per container, non per batch.
Il bug non era che qualcosa fosse rotto. Il bug era che il monitor sosteneva che non stesse girando nulla — mentre tutto girava.
Il setup: un discovery daemon distribuisce 3 000 query ogni 30 minuti su 8 container Docker paralleli. Ogni container elabora ~375 query a 9/min = 41 minuti per container. Poiché le lunghezze delle code differiscono, il tempo di finish varia tra 8 e 17 minuti per container — ma l'ultimo arriva fino a 83 minuti.
Il monitor faceva polling ogni 5 minuti: "quanti job ha completato il batch nell'ultima ora?". Finché il batch non era completamente concluso, segnalava 0 done/h. Le dashboard degli operatori dicevano "Discovery pipeline inactive". In realtà, otto container giravano a pieno regime.
Il peccato architetturale
Il codice originale aveva una sola chiamata finalize() alla fine del batch wrapper:
def run_batch(queries):
assign_to_containers(queries)
wait_for_all_containers()
finalize(batch_id) # ← solo qui lo stato viene propagato
Significa: finché l'ultimo container non finisce la sua ultima query, il segnale di successo non esiste in nessuna tabella che il monitor legge.
Il fix
Ogni container annuncia la propria conclusione:
def run_container(container_id, queries):
for q in queries:
process(q)
write_result(q, container_id)
# Ogni evento di finish viene propagato subito
finalize_container(container_id, batch_id, count=len(queries))
In più: il batch wrapper alla fine fa solo una chiamata finale finalize_batch(batch_id) per le statistiche di batch (durata totale, ecc.), non per il progresso per riga.
Il monitor ora vede nuovi numeri a ogni finish di container. "0 done/h" diventa "37, 284, 531, …" entro i primi 20 minuti.
La regola di calibrazione
Dall'incidente abbiamo tratto una regola numerica che applichiamo da allora a ogni pipeline batch:
Dimensione batch = Worker × Throughput/min × Minuti target
Per un finish target di 15 minuti a 9 query/min con 4 worker: 4 × 9 × 15 = 540 query/batch (arrotondato a 600). Con 8 worker: 8 × 9 × 15 = 1 080 (arrotondato a 1 200).
I 3 000 di batch-size precedenti erano una regola del pollice senza riguardo per la granularità del monitor. Con batch da 600 ogni iterazione gira sotto i 20 minuti — il monitor vede nuovi eventi di finish ogni 8 minuti.
Pattern trasferibile
L'anti-pattern non è confinato ai web crawler. L'abbiamo trovato in altri tre setup:
- Pipeline ETL che riempiono tabelle di staging per batch e solo alla fine pushano in produzione via
INSERT ... SELECT. - Training di machine learning che scrive checkpoint solo alla fine di ogni epoch — il monitoring mostra "stale" per 40+ minuti su epoch grandi.
- Job di backup che impostano lo stato a ✅ solo dopo che tutti i chunk sono pronti — 6 h di cecità di stato mentre il backup gira.
L'antidoto operativo è sempre lo stesso: finalizzare il più granularmente possibile. Per container, per shard, per epoch, per chunk. Tutto ciò che rende la granularità di monitoring sostanzialmente più breve del runtime totale è la scelta giusta.
Stesso fuoco anche da te?
Probabilmente abbiamo già visto qualcosa di simile. Parliamone.
Iniziamo a parlarne→