Die Kurzfassung
Wenn eine Kafka-Partition verstummt: Ist sie fertig, oder nur langsam? Von außen sieht beides identisch aus — und die Antwort entscheidet darüber, ob man auf Basis des Vorhandenen sicher handeln kann. Netflix, Uber und Walmart haben jeweils veröffentlicht, wie sie damit umgehen, und jeder gibt etwas auf: eine zweite Pipeline, eine Spur Parallelität, Korrektheit beim Wiedereinspielen. Jedes Mal solides Engineering. Keiner von ihnen stellt Kafka die eine Frage, deren Antwort Kafka bereits kennt.
Ich stieß darauf bei einem Bestandsdatenstrom für ein Fortune-50-Unternehmen: dreizehn Kafka-Topics, je drei Partitionen, Hunderte Millionen Events pro Tag, jedes Event eines Produkts in strikter Event-Time-Reihenfolge erforderlich — sieben Tage Historie von Grund auf einlesen und dann direkt in den Live-Spitzenverkehr übergehen. Das erste Design stellte allem ein dreißigminütiges Fenster voran, brauchte dennoch einen zweiten Job, um aufzusammeln, was dem Fenster entging, und ließ trotzdem 5,5 % der Datensätze in falscher Reihenfolge durch.
Die Version, die in Produktion ging, wartet nicht auf eine Uhr. Sie liest die Offsets von Kafka.
So funktioniert es — und warum die Idee älter ist als Kafka.
Der Kompromiss, den alle eingehen / §01
Das ist nicht das Problem eines einzelnen Teams. Vier starke Engineering-Organisationen, vier vertretbare Kompromisse:
- Walmart bindet jedes Artikel-Filial-Paar an eine einzige Partition, sodass es stets nur ein Consumer aktualisiert. Ordnung durch festgelegte Schlüssel — der Preis ist Parallelität.
- Uber betreibt eine separate Backfill-Pipeline, um nach verspäteten und ungeordneten Events neu zu berechnen, weil der Streaming-Pfad „Ungenauigkeiten verursachen kann, indem er Events verwirft, die nach dem Watermarking eintreffen.“ Der Preis ist eine zweite Pipeline.
- Netflix stellte beim Backfilling von Flink-Pipelines fest, dass wiedereingespielte Historie eine andere Reihenfolge und ausufernden State erzeugte. Der Preis sind Korrektheit und State.
- Confluents eigene Empfehlung für eine inaktive Kafka-Partition: auf sie warten und blockieren, oder sie ignorieren und „viele verspätete Nachrichten akzeptieren, sobald die Partition wieder aktiv wird.“ Der Preis sind Blockaden oder verspätete Daten.
Jeder einzelne ist vernünftig. Der rote Faden ist, dass alle Vollständigkeit aus der Zeit ableiten — ein Fenster, ein Timeout, eine Wartezeit — obwohl die Partitions-Offsets exakt angeben, wo die Daten enden. Genau diese Lücke wird hier geschlossen.
Man kann für dieselbe Einheit nicht auf „verkauft“ reagieren, bevor „auf Lager“ eingetroffen ist.
Warum der Stream durcheinander ist / §02
Ein Kafka-Topic ist in Partitionen aufgeteilt, und jede Partition ist ihr eigenes Gleis. Innerhalb einer Partition ist die Reihenfolge garantiert: Datensatz eins, dann Datensatz elf, immer. Über Partitionen hinweg koordiniert nichts irgendetwas — die Consumer, die sie lesen, sind nie zur selben Zeit an derselben Stelle. Kafka tut exakt, was es verspricht, die Consumer tun exakt, was sie versprechen, und die kombinierte Ausgabe ist trotzdem durcheinander für alles, dem Reihenfolge wichtig ist.
Man kann für dieselbe Einheit nicht auf „verkauft“ reagieren, bevor „auf Lager“ eingetroffen ist. Bevor eine Pipeline also irgendetwas freigibt, muss sie eine Frage beantworten: Habe ich alles bis zu diesem Zeitpunkt? Fast jede heute übliche Antwort ist ein Timer — das Fenster, das Idle-Timeout, der Abgleich-Job. Jede davon ist eine Mutmaßung über Stille.
Eine alte Lösung: melden statt warten / §03
Es gibt einen Präzedenzfall, und er ist keine bessere Uhr. Frühe Eisenbahnen fuhren im Zeitabstandsbetrieb: Ein folgender Zug fuhr ab, sobald ein festes Intervall verstrichen war — die Strecke „musste“ also frei sein.
Im Juni 1889 blieb ein Zug an einer steilen Steigung bei Armagh in Irland liegen. Die hinteren Wagen wurden abgekuppelt, rollten den Hang zurück und prallten auf den nachfolgenden Zug. Rund achtzig Menschen starben.
Der nachfolgende Zug hatte Armagh verlassen, sobald die vorgeschriebenen zwanzig Minuten verstrichen waren. Zwanzig Minuten waren vergangen, also musste die Strecke frei sein. Niemand in Armagh hatte eine Möglichkeit zu wissen, dass sie es nicht war.
Großbritannien antwortete nicht mit einer besseren Uhr. Das Parlament gab dem Board of Trade die Befugnis, auf Personenstrecken den absoluten Blockbetrieb vorzuschreiben: Kein Zug fährt in einen Streckenabschnitt ein, bevor das Stellwerk voraus bestätigt hat, dass der Abschnitt frei ist. Die Freigabe war nicht länger eine Zeitspanne, die jemand absaß, sondern eine Tatsache, die jemand meldete. Ein liegengebliebener Zug und ein abgefahrener sahen nicht mehr gleich aus, weil niemand mehr etwas in Stille hineinlas.
Das ist der ganze Schritt. Die Offsets von Kafka zu lesen ist das Stellwerk.
Leiten Sie Vollständigkeit nicht aus Stille ab. Fragen Sie das System, das es weiß.
Freigabe ist eine Zeitspanne, die man absitzt. Ein liegengebliebener Zug und ein abgefahrener sehen exakt gleich aus.
Freigabe ist eine Tatsache, die jemand meldet. Niemand liest mehr etwas in Stille hinein.
Der Aufbau — drei Bausteine / §04
Das Prinzip ist kurz: Die Zeit schreitet nur voran, wenn die Evidenz beweist, dass sie es darf. Dies umzusetzen erforderte drei Bausteine, und der schwierige Teil lag nicht dort, wo man ihn erwartet.
Erstens: Die Timer-Warteschlange ist die Sortierung. Jeder Datensatz wird im Keyed State unter seiner eigenen Event Time geparkt, mit einem Event-Time-Timer für exakt diese Millisekunde. Noch wird nichts ausgegeben. Erreicht die Watermark diesen Zeitstempel, feuert der Timer, und alles dort Geparkte verlässt den State gemeinsam. Flink feuert die Timer eines Schlüssels in aufsteigender Zeitstempel-Reihenfolge, sodass wir nie etwas sortieren. Es ist eine Methode und ein Stück State — die Schwierigkeit lag nie hier.
Speichern, dann einen Trigger für die eigene Event Time registrieren.
Die Uhr erreicht ihn, den Schlüssel lesen, alles darunter freigeben.
| Event Time | Datensätze | Timer |
|---|---|---|
| 10:00:18 | — | gefeuert, entfernt |
| 10:00:26 | B, C | jetzt fällig |
| 10:00:33 | D | wartet |
| 10:00:41 | E, F, G | wartet |
◆ Die Timer-Warteschlange von Flink ist nach Zeitstempel geordnet: Timer feuern in aufsteigender Reihenfolge. Das ist die Sortierung.
Zweitens: Der Stream ohne Event Time wird zur Uhr. Eines der dreizehn Topics hatte überhaupt keine Event Time — es veröffentlichte nur gültige Produkte, mit denen alles andere angereichert wurde. Es hatte also nichts zu sortieren und konnte, was die anderen nicht konnten: Watermarks aus der Verarbeitungszeit emittieren, stets der spätestmögliche Zeitstempel. Der Haken: Die Uhr einer Pipeline ist das Minimum über ihre Eingänge, also ändert ein Stream, der auf „jetzt“ läuft, nichts, solange die Historien-Streams noch mitgezählt werden. Ich baute diesen Baustein zuerst, und er hatte null Wirkung — er war nicht falsch, er war in der falschen Reihenfolge. Der Kontroll-Stream zählt erst, wenn die anderen das Minimum verlassen, und ein Stream verlässt das Minimum, wenn er als inaktiv markiert ist.
Drei Kanäle bereit, null freigegeben. Es muss das Minimum sein.
Der Letzte, der übrig bleibt. Die Uhr springt auf jetzt, und der Rest wird in einem Schub ausgespült.
Drittens: Kafka entscheidet, wann ein Stream fertig ist — nicht ein Timeout. Die eingebaute Inaktivitätserkennung von Flink ist ein Timer: eine Weile keine Daten, als inaktiv markieren. Aber ein Timeout kann „startet gerade“ nicht von „fertig“ unterscheiden, und der Start ist der schlimmste Fall — Verbindungen bauen sich unterschiedlich schnell auf, ein Topic bleibt still, das Timeout feuert, die anderen treiben die Uhr über Event Times hinaus, die es noch nicht gelesen hat, und sein erster Datensatz kommt bereits verspätet an. Das ist keine Verzögerung, das ist eine Verletzung. Also musste das Timeout weg. Beim Start, noch vor dem Lesen eines Datensatzes, fragt der Job Kafka nach dem letzten Offset jeder Partition und hält ihn fest. Ein Topic ist erst dann inaktiv, wenn jede Partition diese Zahl erreicht hat — nicht die meisten, jede einzelne. Keine Zeitspanne zum Feinjustieren, denn das Tor ist eine Tatsache von Kafka.
// once, at job startup
Map<TopicPartition, Long> endOffsets = consumer.endOffsets(partitions);
// as records arrive
if (offset + 1 >= startupEndOffsets.get(partition)) markCaughtUp(partition);Stille eines fertigen Streams und Stille eines langsamen sind dasselbe Signal. Kein Schwellenwert unterscheidet sie.
Jede Partition muss den Offset erreichen, der beim Start existierte. Eine im Rückstand bedeutet: Das Topic ist nicht inaktiv.
Was es gebracht hat / §05
Das Fenster hielt jeden Datensatz zurück, bis es sich schloss — dreißig Minuten allein, um die Historie einzulesen, bei jedem Neustart erneut. Jetzt wird ein Datensatz freigegeben, sobald die Watermark ihn passiert: im Schnitt unter 100 Millisekunden, einschließlich der Datensätze, die zurückgehalten wurden, damit sie nicht in falscher Reihenfolge herauskommen. Ein Datensatz, der in Minute eins ankommt, geht in Minute eins hinaus. Mit der Reihenfolge verlief es genauso: Zwei Schichten Timeout-Inaktivität ließen 5,5 % der Datensätze in falscher Reihenfolge durch; eine Schicht 1,5 %; Inaktivität, entschieden durch die Start-Offsets von Kafka: null. Das Fenster ist weg und der Nachlauf-Job ebenso — ein Job liest sieben Tage Historie und läuft dann live.
| Vorher | Nachher | |
|---|---|---|
| Wartezeit, bis ein Datensatz in Reihenfolge freigegeben wird | Bis zu 30 Minuten (das Fenster) | < 100 ms im Schnitt* |
| Datensätze außerhalb der Event-Time-Reihenfolge | 5,5 % (Timeout-Inaktivität) | 0 |
| Zu betreibende Jobs | Stream + Nachlauf | Einer |
*Latenz Ende-zu-Ende beim Client gemessen, bei Hunderten Millionen Events pro Tag. Reihenfolge gemessen an der Referenzimplementierung, die ich für diesen Beitrag nachgebaut habe.
Datensätze, die außerhalb der Event-Time-Reihenfolge freigegeben wurden. Referenzimplementierung, für diesen Beitrag nachgebaut.
Was es nicht verspricht / §06
Deterministisch heißt nicht hellsichtig. Ein Event existiert für die Pipeline erst, wenn es in Kafka gelandet ist. Hält ein Producer einen Datensatz zurück und landet dieser, nachdem die Uhr seine Event Time bereits passiert hat, geht er sofort hinaus, außer der Reihe, und der Verletzungszähler erfasst es. Kein Stream-Prozessor kann ein Event ordnen, das er noch nicht sehen kann. Innerhalb dieser Grenze ist die Garantie exakt, und der Watermark-Lag bestimmt, wie weit die Grenze reicht.
Kein Prozessor kann ein Event ordnen, das er noch nicht sehen kann. Innerhalb der Grenze ist die Garantie exakt — der Watermark-Lag bestimmt, wie weit die Grenze reicht.
Dieselbe Frage, überall / §07
Hat man es einmal gesehen, erkennt man es in jedem System, das aus Streams, die nicht gleichzeitig eintreffen, ableitet, was wahr ist.
Ein Flusspegel verstummt während einer Flut. Ist der Fluss stabil, oder hat das Wasser den Sensor erreicht? 2018 meldete die USGS Pegelmessstationen, die durch Hurrikan Florence ausfielen — ausgerechnet die wichtigsten. „Keine Nachricht“ ist das Falsche, was man in diese Stille hineinlesen kann.
Ein Kamera-Setup. Erfasst man einen Athleten mit mehreren Kameras, ist jede ihr eigener Stream, wie eine Partition; eine 3D-Pose für einen Moment lässt sich erst fusionieren, wenn jede Kamera ihn gemeldet hat. Eine stumme Kamera könnte ein Frame verloren haben oder jemanden beobachten, der stillhält. Bei i3d.ai ist es dieselbe Frage, Dutzende Male pro Sekunde.
Ein Agent. Ein KI-Agent ruft ein Tool auf und hört nichts zurück. Agenten sind sehr gut darin, Stille als Erfolg zu deuten — kein Fehler kam zurück, also weiter zum nächsten Schritt in Maschinengeschwindigkeit. Ein Agent, der auf dem Stand eines Kundenkontos von vor einer Stunde handelt, ist ein Zug, der nach Stoppuhr abgefertigt wurde. Die Lösung ist die, die die Eisenbahnen fanden: Kein Schritt schreitet auf Stille hin voran; jeder Schritt schreitet auf eine Bestätigung hin voran, auf die er verweisen kann.
Bleibt der Fluss stabil — oder hat das Wasser gerade den Sensor erreicht?
Ein verlorenes Frame — oder ein Athlet, der vollkommen stillhält?
War er erfolgreich, ist er gescheitert — oder läuft er noch?
Kein Schritt schreitet auf Stille hin voran. Jeder Schritt schreitet auf eine Bestätigung hin voran, auf die er verweisen kann.
Der günstigste Zuverlässigkeitsgewinn im Streaming ist keine schnellere Engine und kein größeres Fenster. Es ist, aufzuhören, „fertig“ aus Stille abzuleiten, und das System zu fragen, das es bereits weiß. Das Fenster wartet für jeden Datensatz den schlimmsten Fall ab; die Offset-Prüfung wartet exakt so lange, wie die Evidenz es erfordert, und keine Millisekunde länger.
Die abgespeckte Referenzimplementierung — Generator, Handler, Aufhol-Tracker — ist das Nächste, was ich veröffentliche.
Wartet Ihre Pipeline auf eine Uhr, oder liest sie das Manifest?
Jeffrey Patrick Carter · NeoSavant
Historische Grundlage
Die Kompromisse von Walmart, Uber, Netflix und Confluent sind aus den unten genannten Engineering-Quellen zitiert. Der Präzedenzfall des absoluten Blockbetriebs (Regulation of Railways Act 1889) und das von ihm abgelöste Zeitabstandssystem stammen aus den öffentlichen Aufzeichnungen. Die Mechanismen von Flink und Kafka beziehen sich auf die Design-Dokumente und den Quellcode der Projekte selbst. Die Zahlen zur Reihenfolge wurden an einem Nachbau des Produktions-Jobs gemessen; die Latenzzahl stammt Ende-zu-Ende beim Client aus dem ursprünglichen Produktionssystem.
Quellen
Uber Engineering — Designing a Production-Ready Kappa Architecture for Timely Data Stream Processing
Netflix — Backfill Streaming Data Pipelines in Kappa Architecture (Data + AI Summit 2022)
Confluent — Why Is My Apache Flink Job Not Producing Results?
Apache Flink — FLIP-180: Adjust StreamStatus and Idleness definition · FLINK-35886: Idleness incorrectly detected · Generating Watermarks (DataStream) · InternalTimerServiceImpl (Reihenfolge der Timer-Auslösung)
Apache Kafka — KIP-32: Add timestamps to Kafka message (CreateTime / LogAppendTime)
Wikipedia — Regulation of Railways Act 1889 (absoluter Blockbetrieb) · Eisenbahnunglück von Armagh
USGS — Working to keep vital flood information flowing (Hurrikan Florence, 2018)
Library of Congress — Jack Delano — Weichensteller der Indiana Harbor Belt RR mit Signalfackel, 1943 (Item 2017878076)
Jeffrey Patrick Carter
Principal Streaming Engineer, NeoSavant
Jeffrey Patrick Carter is a Principal Streaming Engineer at NeoSavant, with 10+ production Flink and Kafka implementations — including Allstate and i3d.ai.
