ZERONE
Zurück zu Insights
Verteilte Systeme2026-04-18 · 4 Min Lesezeit

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 ... SELECT in 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