Skip to content
ZERONE
Torna agli insights
Sistemi distribuiti2026-04-18 · 4 min di letturaDa Case 01

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