Batch-Finalisierung auf Container-Ebene: Warum der Monitor 83 Minuten lang nichts sah
Eine Pipeline mit N parallelen Sub-Jobs finalisiert ihren Status auf Batch-Ebene — alle Worker laufen, aber der Monitor meldet Stillstand, bis der letzte Container fertig ist. Der Fix: Finalisierung pro Container, nicht pro Batch.
Der Bug war nicht, dass irgendwas kaputt war. Der Bug war, dass der Monitor behauptete, es laufe nichts, während alles lief.
Die Setup-Daten: Ein Discovery-Daemon verteilt alle 30 Minuten einen Batch von 3 000 Queries auf 8 parallele Docker-Container. Jeder Container processiert ~375 Queries à 9 pro Minute = 41 Minuten pro Container. Weil die Container unterschiedliche Queuelängen haben, finish-Zeit variiert von 8 bis 17 Minuten pro Container — aber der letzte braucht bis zu 83 Minuten.
Der Monitor lief alle 5 Minuten und pollte: „Wie viele Jobs hat der Batch in der letzten Stunde abgeschlossen?". Solange der Batch nicht als Ganzes fertig war, meldete er 0 done/h. Operator-Dashboards zeigten: „Discovery-Pipeline inactive". In Wirklichkeit liefen acht Container auf Volllast.
Die Architektur-Sünde
Der originale Code hatte einen finalize()-Aufruf am Ende des Batch-Wrappers:
def run_batch(queries):
assign_to_containers(queries)
wait_for_all_containers()
finalize(batch_id) # ← erst hier wird der Status propagiert
Das heißt: Bevor der letzte Container seinen letzten Query abgearbeitet hat, existiert die Erfolgs-Meldung nirgends in den Tabellen, die der Monitor liest.
Der Fix
Jeder Container meldet seinen eigenen Abschluss:
def run_container(container_id, queries):
for q in queries:
process(q)
write_result(q, container_id)
# Jedes Finishing-Event wird sofort propagiert
finalize_container(container_id, batch_id, count=len(queries))
Plus: Der Batch-Wrapper macht am Ende nur noch einen abschließenden finalize_batch(batch_id)-Aufruf für Batch-Level-Stats (Gesamtdauer etc.), nicht mehr für Row-Level-Progress.
Der Monitor sieht jetzt bei jedem Container-Finish neue Zahlen. Aus „0 done/h" wird „37, 284, 531, …" innerhalb der ersten 20 Minuten.
Die Kalibrierungs-Regel
Wir haben aus dem Incident eine numerische Regel gezogen, die wir seitdem auf jeder Batch-Pipeline prüfen:
Batch-Größe = Worker × Throughput/min × Target-Minutes
Für eine Target-Finish-Zeit von 15 Minuten bei 9 Queries/min und 4 Workern: 4 × 9 × 15 = 540 Queries/Batch (also gerundet 600). Mit 8 Workern: 8 × 9 × 15 = 1 080 (gerundet 1 200).
Die frühere 3 000er-Batch-Größe war eine Faust-Zahl ohne Rücksicht auf die Monitor-Granularität. Mit 600er-Batches läuft jetzt jede Iteration in unter 20 Minuten — der Monitor sieht alle 8 Minuten neue Finish-Events.
Übertragbares Pattern
Das Anti-Pattern ist nicht auf Web-Crawler beschränkt. Wir haben es in drei weiteren Setups gefunden:
- ETL-Pipelines, die Staging-Tabellen pro Batch komplett füllen und erst am Ende via
INSERT ... SELECTin Production pushen. - Machine-Learning-Training, das Checkpoints nur am Ende jeder Epoche schreibt — Monitoring zeigt „stale" für 40+ Minuten bei großen Epochen.
- Backup-Jobs, die den Status erst auf ✅ setzen, nachdem alle Chunks fertig sind — 6 h Status-Blindheit während das Backup läuft.
Das operative Gegenmittel ist immer dasselbe: Finalisierung so granular wie möglich. Container-weise, Shard-weise, Epoch-weise, Chunk-weise. Alles, was die Monitor-Granularität deutlich kürzer macht als die Gesamt-Laufzeit, ist die richtige Entscheidung.
Ähnlicher Brand bei dir?
Wir haben vermutlich schon etwas ähnliches gesehen. Sprich mit uns.
Gespräch starten→