8. September 2026 · 8 Min. Lesezeit
Der Fehler war 25 Stunden alt. Der Client war die ganze Zeit gesund.
Ein Produktions-Producer hat den größten Teil eines Tages jede Anfrage abgelehnt. Jedes Gesundheitssignal blieb die gesamte Zeit grün: Restart-Zähler 0, TCP-Sitzungen zu den Kafka-Brokern ESTABLISHED, Liveness- und Readiness-Probes bestanden. Der Fehler, den er zurückgab, war byte-identisch mit einem, den sein darunterliegender Client 25 Stunden zuvor erzeugt hatte, einschließlich des Zeitwerts. Der Client hatte sich binnen Sekunden nach dem Vorfall erholt. Der Wrapper hat es nie bemerkt.
Das Muster, das funktioniert
Wie viele Plattformen betreiben wir Kafka-Clients hinter einer gemeinsamen Wrapper-Bibliothek, und die Antwort des Wrappers auf Brokerverlust ist bewusst grob: Fehler zählen und, sobald eine Schwelle überschritten ist, panisch werden. Der Orchestrator startet den Prozess neu, und der Dienst kommt kalt und sauber zurück.
Es ist leicht, über Crash-to-Recover zu spotten, bis man einen Client in einem halb defekten Zustand debuggt hat, verklemmt in einer Ecke seiner Reconnect-Zustandsmaschine, die kein Test abdeckt und kein Runbook benennt. Der Panic verwandelt einen unbegrenzten Raum solcher Zustände in genau einen Zustand: einen frischen Prozess. Und er komponiert mit Maschinerie, die die Plattform ohnehin hat. Der Wrapper reimplementiert keine Erholung; er delegiert an den Neustart, den einen Erholungspfad, in den bereits jeder investiert hat.
Während des Vorfalls im Zentrum dieses Beitrags hat die Plattform ihre Broker gerollt, und die ausgelasteten Dienste mit diesem Wrapper verhielten sich exakt wie entworfen. Ein Stream-Processor berührt Kafka ständig, also erzeugt ein Broker-Roll mehrfach pro Sekunden Fehler; die Schwelle fällt in Sekunden, der Prozess wird panisch, der Orchestrator ersetzt ihn, und der Dienst ist binnen einer Minute sauber. Über ein Dutzend Dienste auf derselben Bibliotheksversion haben genau das getan, in der Sekunde, in der ihr Broker herunterfuhr. Crash-to-Recover hat sich diese Nacht bezahlt gemacht.
Warum Untätigkeit es bricht
Die Schwelle zählt Fehler, und Fehler entstehen nur, wenn der Dienst Arbeit verrichtet. Das ist der gesamte Defekt.
Ein Stream-Processor produziert Fehler schnell genug, um jede Schwelle zu erreichen. Ein anfragegetriebener Producer, ein Dienst, der Kafka nur berührt, wenn ihn etwas aufruft, und der ungefähr einmal täglich aufgerufen wird, produziert etwa einen Fehler pro Tag. Der Zähler überschreitet nie etwas. Und das Verhalten des Wrappers bei einer Schwelle, die nie fällt, ist nicht „weiter versuchen“. Es ist, den letzten Fehler zu verankern und ihn für jeden weiteren Versuch unbegrenzt zurückzugeben. Wir haben einen dabei 22 Stunden zusehen können, und er endete nur, weil ein Mensch den Pod neu startete.
Erholung proportional zum Traffic ist keine Erholung. Die Dienste, die sie am wenigsten brauchen, bekommen sie; die, die sie am dringendsten brauchen, nie. Der stille Dienst ist genau der Dienst, dessen Versagen unbemerkt bleibt, und der Mechanismus, der ihn retten soll, wird von der einen Sache ratenlimitiert, die ihm fehlt: Traffic.
Zweimal falsch
Der Weg zur richtigen Antwort lief über zwei falsche, beide sind es wert, behalten zu werden.
Die erste war Korrelation im Kostüm der Ursache. Die Broker-Logs zeigten einen Benutzer, der über Wochen flach tausendfach täglich die Authentifizierung verfehlte. Das Audit-Trail pinnte es fest: Ein automatisierter Aufräum-Job hatte die Credential dieses Benutzers gelöscht. Genau eine Credential war gelöscht worden, und genau ein Benutzer scheiterte. Ich habe die Credential wiederhergestellt, das Versiegen der Fehler beobachtet und den Vorfall als ursächlich geklärt gemeldet.
Es war nicht der Vorfall. Der scheiternde Dienst authentifizierte sich als ein völlig anderer Benutzer, was eine einzige Lektüre seiner Deployment-Konfiguration gezeigt hätte, bevor jemand etwas anfasste. Die Wiederherstellung war harmlos und hat einen tatsächlich defekten Client repariert, aber es war ein anderer Client. „Nur eine Sache ist fehlgeschlagen, und nur eine Sache hat sich geändert“ ist eine Eigenschaft Ihrer Suche, nicht des Systems. Einzigartigkeit ist ein Anlass zu verifizieren, kein Beweis.
Ein Detail hat die erste Runde aktiv in die Irre geführt: Der Dienst, dessen Logs alle lasen, war nicht der Dienst, der nach Kafka produzierte. Er hatte den Fehler des Producers im Körper einer HTTP-500-Antwort erhalten und als seinen eigenen geloggt, also trug ein Java-Log die Fehlermeldung einer C-Bibliothek. Der Melder und der Akteur waren verschiedene Dienste, und das Log, das alle durchsuchten, beantwortete eine andere Frage als die gestellte.
Die zweite falsche Antwort war die Version. Der gesunde Dienst lief auf einer neueren Client-Bibliothek als der festsitzende, also wirkte ein Versionssprung wie die Lösung, und ein Ticket wurde erstellt. Dann kam eine Flotteninventur: die eingebetteten Go-Build-Infos aus /proc/1/exe jedes Pods greppen, und man weiß exakt, welche Bibliotheksversion jeder Dienst fährt. Dutzende Dienste auf dem Wrapper, die meisten auf derselben Version wie der festsitzende, und über ein Dutzend dieser diesselbe-Version-Dienste waren während des Rolls panisch geworden und hatten sich selbst geheilt. Die Version trennt nichts.
Keine Richtung des üblichen Arguments überlebt das. „Ein anderer Dienst auf dieser Version war gesund“ entlastet nichts, und „der gesunde läuft auf neuerer“ überführt nichts. Teilen Sie die Flotte nach Traffic-Profil, bevor Sie nach einem Versionsdiff greifen. Wer das falsch macht, schickt jemanden zu einem Upgrade, das das Problem nicht beheben kann, und es war der Widerspruch eines Kollegen, mit einem Crash-Fenster aus den Logs des Clients, den meine Geschichte nicht unterbringen konnte, der es zutage förderte.
Das Beweisstück in den Ziffern
Das entscheidende Beweisstück stand die ganze Zeit im Fehlerstring.
Der Fehler des festsitzenden Producers war byte-identisch mit einem, den der darunterliegende Client während des Broker-Rolls erzeugt hatte, und zwar identisch bis zum Zeitwert, dem Fragment „nach N Millisekunden“. Der darunterliegende Client hatte seitdem nichts geloggt, weil er sich binnen Minuten normal neu verbunden hatte. Der Wrapper hatte das Fehlerobjekt gepuffert und einen Tag lang wiederverwendet.
Identische Zeitwerte können keine zwei unabhängigen Ereignisse sein. Eine verstrichene Zeit ist eine Messung, und zwei Messungen stimmen nicht auf die Ziffer überein. Sobald das sitzt, lautet die Frage nicht mehr „warum schlägt es fortwährend fehl“, sondern „warum wird der Fehler nie geräumt“, und diese Frage hat eine Antwort. Wenn eine Fehlermeldung eine Dauer oder einen Zähler trägt, differenzieren Sie diese Ziffern über die Vorkommen. Gleiche Ziffern bedeuten einen gepufferten Fehler, der wiederserviert wird. Es kostet nichts und dreht die Untersuchung um.
Wie ein verankerter Prozess aussieht
Nichts auf dem Standard-Dashboard unterscheidet festsitzend von untätig.
Der Restart-Zähler steht auf 0, weil der Prozess nach eigener Logik nie gerettet werden musste. Die Sockets zu den Brokern sind ESTABLISHED, weil der Transport sich tatsächlich erholt hatte; Socket-Zustand ist Evidenz von einer Schicht unter der Behauptung, und ich hatte die Sache einmal auf diese Evidenz hin wiederhergestellt genannt, was verfrüht war. Liveness und Readiness bestehen, weil keine der Probes einen Produce ausübt. Die Logs zeigen nichts, weil ein stiller Dienst nichts loggt, ob er defekt ist oder nur still. Und App-seitige Retries bekommen denselben gepufferten Fehler wenige Sekunden später, weshalb „fügen Sie Retries hinzu“ der falsche Reflex ist. Aus einem Cache kommen Sie nicht durch Retries heraus.
Was es schließt
Drei Fixes, in fallendem Wert.
Räumen Sie den Fehlerzustand bei Erfolg. Ein erfolgreicher Produce oder eine erfolgreiche Reconnect-Anbindung sollte den gepufferten Fehler pensionieren. Das Versäumnis genau dessen ist der gesamte Bug; alles andere ist Milderung.
Lassen Sie die Health-Probe den Produce-Pfad ausüben. Die Readiness-Behauptung eines Producers lautet „ich kann produzieren“, also genau das sollte die Probe testen. Ein Producer, der gesund meldet, während er jeden Produce ablehnt, lügt auf der einen Achse, die jemand abfragt.
Machen Sie die Schwelle zeit- wie zählbasiert. Ein stiller Dienst erholt sich dann irgendwann so wie ein ausgelasteter, über die Uhr statt über den Zähler.
Und akzeptieren Sie den wiederkehrenden Auslöser, denn er verschwindet nicht. Eine managed Kafka-Plattform rollt ihre Broker nach einem Wartungsplan, bei uns monatlich, und jedes Roll wiederholt dieses Experiment an jedem Client. Nach jedem Roll seitens des Providers sind die anfragegetriebenen Producer die zu prüfenden, und der einzige verlässliche Check ist, eine Anfrage durch sie zu schieben oder sie neu zu starten. Es gibt eine kleine Menge stiller Producer mit wenig Traffic, die sich vom Festsitzenden nicht ohne eine durchgeschobene Anfrage unterscheiden lassen; sie kommen auf die Liste für das nächste Roll.
Wo dies verallgemeinert, und die Falle
Jeder Erholungsmechanismus mit ratenbasiertem Auslöser hat diesen Fehlermodus. Circuit Breaker brauchen Fehler zum Auslösen. Watchdogs zählen Ereignisse. Verbindungs-Pools validieren beim Auschecken, also validiert ein Pool, den niemand auschecket, nie etwas. Health-Checks, die nur den Happy Path üben, bestehen ewig neben einem Defekten. Wenig Traffic macht jeden davon lautlos zum No-Op: nichts ist deaktiviert, nichts ist fehlkonfiguriert, der Mechanismus wartet schlicht auf Evidenz, die nicht kommt.
Zwei Regeln verlassen diese Seite. Wenn ein Erholungsmechanismus ratenbasiert ist, fragen Sie, was geschieht, wenn der Traffic die Rate nicht erreicht. Und wenn ein Fehler einen Zähler trägt, ist der Zähler Evidenz.
Die Falle ist, Erholung als Eigenschaft des Versagens zu behandeln. Sie ist eine Eigenschaft des Traffics. Ein Client, der sich nur erholt, während er versagt, erholt sich genau dann, wenn er oft genug versagt, und der Dienst, zu still, um oft genug zu versagen, bekommt nie die Gelegenheit. Er wird dort sitzen, auf jedem Dashboard gesund, und den Fehler von gestern servieren.