Batch finalizacija na razini kontejnera: zašto monitor 83 minute nije vidio ništa
Pipeline s N paralelnih sub-jobova finalizira status na razini batcha — svi workeri rade, ali monitor javlja zastoj dok zadnji kontejner ne završi. Rješenje: finalizirati po kontejneru, ne po batchu.
Bug nije bio da je nešto pokvareno. Bug je bio da je monitor tvrdio kako ništa ne radi — dok je sve radilo.
Setup: discovery daemon svakih 30 minuta razdjeljuje 3 000 upita na 8 paralelnih Docker kontejnera. Svaki kontejner obrađuje ~375 upita po 9/min = 41 minuta po kontejneru. Kako duljine redova variraju, vrijeme završetka varira između 8 i 17 minuta po kontejneru — ali zadnji ide do 83 minute.
Monitor je pollao svakih 5 minuta: "koliko je batch završio u zadnjem satu?". Dok batch nije bio u potpunosti gotov, javljao je 0 done/h. Operatorski dashboardi su pokazivali "Discovery pipeline inactive". U stvarnosti je osam kontejnera radilo punim kapacitetom.
Arhitektonski grijeh
Originalni kod je imao jedan finalize() poziv na kraju batch wrappera:
def run_batch(queries):
assign_to_containers(queries)
wait_for_all_containers()
finalize(batch_id) # ← tek se ovdje status propagira
Znači: dok zadnji kontejner ne završi zadnji upit, signal uspjeha ne postoji ni u jednoj tablici koju monitor čita.
Popravak
Svaki kontejner javlja vlastiti završetak:
def run_container(container_id, queries):
for q in queries:
process(q)
write_result(q, container_id)
# Svaki finish event se odmah propagira
finalize_container(container_id, batch_id, count=len(queries))
Uz to: batch wrapper na kraju radi samo zaključni finalize_batch(batch_id) poziv za batch-level statistiku (ukupno trajanje itd.), ne više za row-level progress.
Monitor sada vidi nove brojeve pri svakom završetku kontejnera. Iz "0 done/h" postaje "37, 284, 531, …" unutar prvih 20 minuta.
Pravilo kalibracije
Iz incidenta smo izveli numeričko pravilo koje od tada primjenjujemo na svakom batch pipelineu:
Veličina batcha = Workeri × Throughput/min × Ciljanih minuta
Za ciljano vrijeme završetka od 15 minuta pri 9 upita/min s 4 workera: 4 × 9 × 15 = 540 upita/batch (zaokruženo na 600). S 8 workera: 8 × 9 × 15 = 1 080 (zaokruženo na 1 200).
Ranija veličina batcha od 3 000 bila je palac-pravilo bez obzira na granularnost monitora. S batchevima od 600, svaka iteracija sada traje ispod 20 minuta — monitor vidi nove finish evente svakih 8 minuta.
Prenosivi pattern
Anti-pattern nije ograničen na web crawlere. Pronašli smo ga u tri druga setupa:
- ETL pipelineovi koji pune staging tablice po batchu i tek na kraju guraju u produkciju kroz
INSERT ... SELECT. - ML trening koji checkpoint piše tek na kraju svake epohe — monitoring pokazuje "stale" 40+ minuta na velikim epohama.
- Backup jobovi koji status postavljaju na ✅ tek nakon što su svi chunkovi gotovi — 6 h status-sljepoće dok backup traje.
Operativni protulijek je uvijek isti: finalizirati što granularnije. Per-container, per-shard, per-epoch, per-chunk. Sve što čini granularnost monitoringa znatno kraćom od ukupnog runtimea je ispravna odluka.