Redis Pub/Sub-Nachrichtenverlust: Ursachen und zuverlässige Alternativen

Entdecken Sie, warum Redis Pub/Sub Nachrichten bei Netzwerkunterbrechungen oder langsamen Verbrauchern verliert, und erkunden Sie Muster wie Redis Streams und listenbasierte Warteschlangen für garantierte Zustellung.

Redis Pub/Sub-Nachrichtenverlust: Ursachen und zuverlässige Alternativen

Ich erinnere mich an das erste Mal, als mich Redis Pub/Sub verbrannt hat. Es war spät, gegen 23 Uhr, und unser Benachrichtigungssystem begann, Nachrichten zu verlieren. Nicht alle – nur genug, dass Benutzer es bemerkten, bevor wir es taten. Der diensthabende Ingenieur (ich, leider) verbrachte zwei Stunden damit, Anwendungsprotokolle zu durchforsten, bevor die offensichtliche Wahrheit ans Licht kam: Redis Pub/Sub stellt nichts in die Warteschlange. Es ist kein Nachrichtenbroker. Es ist ein Feuerwehrschlauch, und wenn Sie nicht direkt davor stehen mit offenem Mund, werden Sie etwas verpassen.

Das ist die Sache, die Ihnen niemand sagt, wenn Sie zum ersten Mal zu Redis Pub/Sub greifen. Es steht technisch gesehen in der Dokumentation, aber es ist leicht zu übersehen, wenn Sie begeistert sind, wie einfach die API ist. Sie veröffentlichen auf der einen Seite, Sie abonnieren auf der anderen, und es funktioniert. Bis es das nicht tut.

Die Fire-and-Forget-Realität

Redis Pub/Sub arbeitet nach einem brutal einfachen Prinzip: Wenn Sie eine Nachricht veröffentlichen, schiebt Redis sie zu jedem verbundenen Abonnenten in diesem Kanal genau in diesem Moment. Wenn ein Abonnent nicht verbunden ist oder zwar verbunden ist, aber nicht mithalten kann, verdampft die Nachricht. Es gibt keine Persistenzschicht, keinen Bestätigungsmechanismus, keine Dead-Letter-Queue. Die Nachricht existiert nur während der Übertragung.

Lassen Sie mich ein konkretes Beispiel geben. Angenommen, Sie haben einen Dienst, der Bestellstatusaktualisierungen veröffentlicht, und einen anderen Dienst, der abonniert, um Bestätigungs-E-Mails zu senden. Unter normaler Last läuft alles rund. Dann stolpert Ihr E-Mail-Dienst – vielleicht ist der SMTP-Relay langsam, vielleicht gibt es eine Garbage-Collection-Pause. Während dieses Stolperns schiebt Redis weiter Nachrichten. Der TCP-Puffer des Abonnenten füllt sich. Irgendwann bricht die Verbindung ab. Wenn der Abonnent sich wieder verbindet, nimmt er ab jetzt auf, nicht ab dem Punkt, an dem er aufgehört hat. Jede Nachricht, die während des Trennungsfensters veröffentlicht wurde, ist weg.

Ich habe dies in der Praxis mit einem einfachen Testaufbau gemessen: ein Herausgeber, der 10.000 Nachrichten pro Sekunde feuert, und ein Abonnent, der gelegentlich für 50 Millisekunden blockiert. Selbst bei einer einzigen kurzen Pause verlieren Sie Dutzende von Nachrichten. Der Abonnent weiß nie, dass sie gesendet wurden. Der Herausgeber weiß nie, dass sie verloren gegangen sind. Redis ist vollkommen zufrieden – es hat genau das getan, wofür es entwickelt wurde.

Was tatsächlich Nachrichtenverlust verursacht

Es gibt drei Hauptszenarien, in denen Pub/Sub Nachrichten verliert, und es lohnt sich, sie alle zu verstehen, weil sie auf unterschiedliche Weise auftauchen.

Netzwerkinstabilität ist das offensichtlichste. Jede vorübergehende Netzwerkpartition zwischen dem Abonnenten und Redis trennt die Verbindung. Redis erkennt dies über das Client-Timeout (Standard 60 Sekunden, aber Sie könnten es niedriger eingestellt haben). Während dieses Fensters gehen alle veröffentlichten Nachrichten für diesen Abonnenten verloren. Andere Abonnenten könnten sie problemlos erhalten, was das Debuggen besonders unterhaltsam macht – Sie sehen inkonsistente Zustände zwischen Diensten und fragen sich, ob Sie verrückt werden.

Langsame Verbraucher sind heimtückischer, weil die Verbindung offen bleibt. Redis verwendet ein Push-Modell, d.h. es schreibt auf Abonnenten-Sockets so schnell, wie Herausgeber produzieren. Wenn ein Abonnent Nachrichten nicht schnell genug verarbeiten kann, füllt sich der TCP-Empfangspuffer des Kernels. Sobald dieser Puffer voll ist, kann Redis keine weiteren Daten schreiben, und die Verbindung bricht schließlich ab. Der Abonnent bemerkt möglicherweise nicht einmal, dass er zurückliegt, bis die Trennung erfolgt.

Ich habe dies mit Abonnenten erlebt, die für jede Nachricht synchrone Datenbankschreibvorgänge durchführen. Bei geringem Volumen ist es in Ordnung. Bei Spitzenlast wird die Datenbank zum Engpass, der Abonnent fällt zurück, und Nachrichten stauen sich im TCP-Puffer. Wenn dieser Puffer überläuft, wird die Verbindung zurückgesetzt, und der Abonnent verliert alles, was er noch nicht aus dem Socket gelesen hatte.

Client-Trennungen während Bereitstellungen oder Neustarts sind die dritte große Kategorie. Wenn Sie rollierende Bereitstellungen durchführen und eine Abonnenteninstanz ausfällt, verpasst sie alles, was während ihrer Abwesenheit veröffentlicht wurde. Es gibt keinen „Holen Sie mich auf“-Mechanismus. Wenn sie wieder online kommt, beginnt sie von vorne.

Eine Sache, die mich überrascht hat: Selbst ein sauberes Herunterfahren hilft nicht. Wenn Ihr Abonnent sich vor dem Beenden ordnungsgemäß abmeldet, verpasst er dennoch Nachrichten, die zwischen der Abmeldung und der Rückkehr veröffentlicht wurden. Die Abmeldung erfolgt sofort – es gibt keine Option „Halten Sie meine Nachrichten für eine Minute“.

Wann Pub/Sub tatsächlich in Ordnung ist

Ich möchte nicht den Eindruck erwecken, dass Redis Pub/Sub nutzlos ist. Es ist ausgezeichnet für bestimmte Anwendungsfälle, und ich verwende es immer noch regelmäßig. Der Schlüssel ist zu verstehen, was diese Anwendungsfälle sind.

Echtzeit-Benachrichtigungen, bei denen gelegentlicher Verlust akzeptabel ist, funktionieren wunderbar. Denken Sie an Live-Sportergebnisse, Aktienticker oder Tippindikatoren in einer Chat-App. Wenn ein Benutzer eine Ergebnisaktualisierung verpasst, kommt die nächste sowieso in ein paar Sekunden. Die Daten haben eine kurze Haltbarkeit und keine Haltbarkeitsanforderung.

Serviceerkennung und Konfigurationsübertragung sind ein weiterer idealer Bereich. Wenn Sie ein Feature-Flag ändern und an alle Anwendungsinstanzen veröffentlichen, ist es in Ordnung, wenn eine Instanz, die gerade neu startet, die Aktualisierung verpasst – sie holt den aktuellen Zustand beim nächsten periodischen Refresh oder beim Wiedereinschalten nach.

Ich habe Pub/Sub auch erfolgreich für Cache-Invalidierung über mehrere Anwendungsserver hinweg verwendet. Veröffentlichen Sie einen Cache-Schlüssel zum Invalidieren, und jeder Server löscht seinen lokalen Cache. Wenn ein Server die Nachricht verpasst, ist das Schlimmste, dass er veraltete Daten serviert, bis der Cache-Eintrag natürlich abläuft. Nicht ideal, aber auch nicht katastrophal.

Der gemeinsame Nenner hier: Pub/Sub funktioniert, wenn Nachrichten von Natur aus flüchtig sind, wenn Verlust durch andere Mechanismen behebbar ist und wenn Sie keine Garantien für die Reihenfolge oder Exactly-Once-Zustellung benötigen.

Redis Streams: die eingebaute Alternative

Redis Streams, eingeführt in Redis 5.0, ist das, was ich jetzt verwende, wenn ich zuverlässige Nachrichtenzustellung benötige. Es ist nicht Pub/Sub mit angehängter Persistenz – es ist ein grundlegend anderes Modell, näher an einem verteilten Log wie Kafka als an einem Broadcast-Mechanismus.

Mit Streams werden Nachrichten an ein Log angehängt und bleiben dort, bis sie explizit bestätigt werden. Verbraucher können sich trennen, neu starten, zurückfallen und dennoch aufholen. Der Stream behält Nachrichten basierend auf entweder einer maximalen Länge oder einer Aufbewahrungsdauer, sodass Sie steuern, wie viel Verlauf Sie behalten.

So unterscheidet sich das mentale Modell. Bei Pub/Sub abonnieren Sie einen Kanal, und Nachrichten fließen zu Ihnen. Bei Streams ziehen Sie Nachrichten in Ihrem eigenen Tempo. Eine Verbrauchergruppe verfolgt, welche Nachrichten jeder Verbraucher bestätigt hat, sodass Sie mehrere Verbraucher haben können, die aus demselben Stream lesen, ohne Duplikate (oder mit absichtlichen Duplikaten, wenn Sie Fan-Out wünschen).

Ein grundlegender Streams-Aufbau sieht etwa so aus:

XADD orders * status confirmed order_id 12345

Das hängt eine Nachricht an den orders-Stream an. Das * weist Redis an, eine ID automatisch zu generieren. Dann liest Ihr Verbraucher mit:

XREADGROUP GROUP email-processor worker-1 COUNT 10 STREAMS orders >

Das > bedeutet „Gib mir Nachrichten, die noch keinem Verbraucher in dieser Gruppe zugestellt wurden.“ Nach der Verarbeitung bestätigt der Verbraucher:

XACK orders email-processor <message-id>

Wenn der Verbraucher vor der Bestätigung abstürzt, bleibt die Nachricht ausstehend. Ein anderer Verbraucher in der Gruppe kann sie nach einem Timeout mit XCLAIM übernehmen. Dies ist der Bestätigungs- und Wiederzustellungsmechanismus, der Pub/Sub völlig fehlt.

Das Verbrauchergruppenmodell in der Praxis

Verbrauchergruppen machen Streams wirklich nützlich für zuverlässige Verarbeitung. Jede Gruppe behält ihre eigene Position im Stream, sodass Sie eine Gruppe für E-Mail-Benachrichtigungen, eine andere für Analysen und eine weitere für Audit-Protokollierung haben können – alle lesen denselben Stream unabhängig voneinander.

Innerhalb einer Gruppe werden Nachrichten auf Verbraucher verteilt. Dies gibt Ihnen horizontale Skalierbarkeit: Fügen Sie mehr Verbraucherinstanzen hinzu, und sie teilen sich die Last. Wenn eine Instanz ausfällt, werden ihre ausstehenden Nachrichten für andere Instanzen verfügbar, um sie zu übernehmen.

Ich habe festgestellt, dass die Liste der ausstehenden Einträge für die Überwachung unschätzbar ist. Sie können XPENDING ausführen, um zu sehen, welche Nachrichten nicht bestätigt wurden und wie lange sie bereits ausstehen. Dies zeigt langsame Verbraucher sofort an – viel besser, als Nachrichtenverlust Tage später durch Benutzerbeschwerden zu entdecken.

Eine Einschränkung bei Streams: Nachrichten-IDs sind monoton steigende Zeitstempel, was bedeutet, dass Sie Nachrichten nicht einfach außerhalb der Reihenfolge einfügen können. Wenn Sie eine strenge Reihenfolge innerhalb eines Streams benötigen, ist dies tatsächlich ein Feature. Wenn Sie bestimmte Nachrichten priorisieren müssen, benötigen Sie mehrere Streams oder einen anderen Ansatz.

Listenbasierte Warteschlangen für einfachere Bedürfnisse

Bevor Streams existierten, war das Standardmuster für zuverlässiges Messaging mit Redis listenbasierte Warteschlangen mit blockierenden Pops. Dieses Muster ist immer noch vollkommen praktikabel, besonders wenn Sie eine ältere Redis-Version verwenden oder etwas ganz Einfaches wünschen.

Die Idee ist unkompliziert: Produzenten LPUSH oder RPUSH Nachrichten auf eine Liste, und Verbraucher führen BLPOP oder BRPOP aus, um zu blockieren, bis eine Nachricht ankommt. Der blockierende Pop ist entscheidend – ohne ihn würden Sie abfragen, was CPU verschwendet und Latenz erhöht.

Die Zuverlässigkeit kommt von einer sekundären „Verarbeitungs“-Liste. Der Verbraucher verschiebt eine Nachricht atomar von der ausstehenden Warteschlange in eine Verarbeitungswarteschlange mit BRPOPLPUSH (oder LMOVE in Redis 6.2+). Nach der Verarbeitung entfernt er die Nachricht aus der Verarbeitungswarteschlange. Wenn der Verbraucher abstürzt, behält die Verarbeitungswarteschlange die Nachricht, und ein Überwachungsprozess kann veraltete Elemente zurück in die ausstehende Warteschlange verschieben.

Ich habe dieses Muster mehrmals gebaut, und es funktioniert, aber es ist mehr Code, als Sie erwarten würden. Sie müssen Timeouts behandeln, entscheiden, wie lange eine Nachricht in der Verarbeitungswarteschlange bleiben kann, bevor Sie sie als aufgegeben betrachten, und sich mit Randfällen um doppelte Verarbeitung befassen. Streams formalisieren im Wesentlichen all dies, weshalb ich meistens von handgemachten Listenwarteschlangen abgerückt bin.

Der einzige Ort, an dem ich immer noch listenbasierte Warteschlangen verwende, sind Arbeitswarteschlangen, bei denen die Verarbeitungsreihenfolge keine Rolle spielt und ich die absolut einfachste Implementierung wünsche. Manchmal reichen eine Liste und eine BLPOP-Schleife aus, und das Hinzufügen von Streams wäre Overengineering.

Pub/Sub-Sharding in Redis 7

Redis 7 hat Sharded Pub/Sub eingeführt, was erwähnenswert ist, weil es ein anderes Problem als Nachrichtenverlust löst. Bei regulärem Pub/Sub wird jede Nachricht an jeden Knoten in einem Cluster gesendet, selbst wenn kein Abonnent auf einem bestimmten Knoten diesen Kanal interessiert. Dies verschwendet Cluster-Interconnect-Bandbreite.

Sharded Pub/Sub bindet Kanäle an bestimmte Cluster-Slots, sodass Nachrichten nur an Knoten weitergeleitet werden, die tatsächlich Abonnenten für diesen Kanal haben. Es ist eine Leistungsoptimierung, kein Zuverlässigkeitsfeature. Sie werden immer noch Nachrichten bei Trennung verlieren. Aber wenn Sie Pub/Sub in großem Maßstab in einer Cluster-Umgebung betreiben, ist es wissenswert.

Die Wahl treffen: Pub/Sub vs. Streams vs. Listen

Nach Jahren mit diesen Mustern hat sich mein Entscheidungsprozess auf einige Fragen vereinfacht.

Erstens: Können Sie Nachrichtenverlust tolerieren? Wenn ja, und wenn die Daten flüchtig sind, ist Pub/Sub wahrscheinlich in Ordnung. Sie erhalten die niedrigste Latenz und das einfachste Betriebsmodell.

Zweitens: Benötigen Sie Nachrichtenpersistenz und -wiederholung? Wenn ja, ist Streams die Antwort. Die Fähigkeit, Nachrichten nach einem Fehlerbehebungs-Update des Verbrauchers erneut zu verarbeiten, hat mich mehr als einmal gerettet. Bei Pub/Sub, wenn Ihr Verbraucher einen Fehler hatte, der dazu führte, dass er eine Stunde lang Nachrichten falsch behandelte, sind diese Nachrichten für immer verloren. Mit Streams können Sie die Position der Verbrauchergruppe zurücksetzen und sie erneut abspielen.

Drittens: Benötigen Sie mehrere unabhängige Verbrauchergruppen, die dieselben Daten lesen? Streams behandelt dies nativ. Bei Pub/Sub erhält jeder Abonnent jede Nachricht, was vielleicht das ist, was Sie wollen, aber es gibt keine Möglichkeit, verschiedene Gruppen von Abonnenten zu haben, die unabhängige Positionen beibehalten.

Viertens: Was ist Ihre Redis-Version? Wenn Sie auf etwas älterem als 5.0 festsitzen, ist Streams nicht verfügbar, und Sie betrachten listenbasierte Warteschlangen oder einen externen Nachrichtenbroker. Ich war in dieser Situation, und ehrlich gesagt, wenn Sie zuverlässiges Messaging benötigen und Streams nicht verwenden können, würde ich in Betracht ziehen, ob Redis überhaupt das richtige Werkzeug ist. RabbitMQ oder NATS könnten besser passen.

Die betriebliche Seite, über die niemand spricht

Hier ist etwas, das ich auf die harte Tour gelernt habe: Die Überwachung von Pub/Sub ist trügerisch schwierig. Sie können Verbindungszahlen und Kanalabonnements mit PUBSUB NUMSUB überwachen, aber Sie können nicht sehen, wie viele Nachrichten verloren gehen. Es gibt keine Metrik für „veröffentlichte, aber nicht empfangene Nachrichten“, weil Redis das nicht verfolgt.

Mit Streams erhalten Sie Sichtbarkeit. XINFO GROUPS zeigt Ihnen die Verbraucherverzögerung. XPENDING zeigt Ihnen nicht bestätigte Nachrichten. Sie können Warnungen einrichten, wenn die Verzögerung einen Schwellenwert überschreitet. Diese betriebliche Sichtbarkeit allein hat den Wechsel zu Streams für mich gelohnt.

Speicherverwaltung ist eine weitere Überlegung. Pub/Sub-Nachrichten existieren nur im Speicher und nur während der Übertragung, daher ist der Speicherverbrauch durch Ihre Veröffentlichungsrate und Verbrauchergeschwindigkeit begrenzt. Streams speichern Nachrichten, bis sie beschnitten werden, daher müssen Sie über Aufbewahrungsrichtlinien nachdenken. Ich setze normalerweise eine maximale Stream-Länge (MAXLEN) basierend auf erwartetem Durchsatz und verfügbarem Speicher fest und überwache die Stream-Länge, um unerwartete Ansammlungen zu erkennen.

Was ich jetzt tatsächlich tue

Heutzutage greife ich standardmäßig zu Redis Streams für jeden neuen Messaging-Anwendungsfall, der Zuverlässigkeit erfordert. Die API ist etwas komplexer als Pub/Sub, aber nicht viel, und die Zuverlässigkeitsgarantien sind es wert. Ich behalte Pub/Sub für die flüchtigen Dinge – Cache-Invalidierung, Echtzeit-Präsenz, diese Art von Dingen.

Für besonders kritische Nachrichten (Zahlungsabwicklung, Auftragsabwicklung) bin ich vollständig von Redis weggegangen und verwende dedizierte Nachrichtenbroker. Redis ist in vielen Dingen fantastisch, aber es ist nicht für die datenträgerbasierte Persistenz von hochvolumigen Nachrichtenwarteschlangen optimiert. Wenn Nachrichten einen vollständigen Redis-Neustart ohne Verlust überleben sollen, müssen Sie AOF-Persistenz mit appendfsync always konfigurieren, was die Schreibleistung beeinträchtigt. An diesem Punkt macht etwas wie Kafka oder Pulsar mehr Sinn.

Aber für die große Mitte – wo Nachrichtenverlust ärgerlich oder kostspielig, aber nicht katastrophal wäre, und wo Sie im Redis-Ökosystem bleiben möchten, das Sie bereits kennen – trifft Streams einen idealen Punkt. Es war für mich in der Produktion zuverlässig genug, und die betriebliche Einfachheit, keine neue Infrastrukturkomponente einzuführen, hat echten Wert.

Der ursprüngliche Fehler, den ich mit Pub/Sub gemacht habe, lag nicht wirklich an der Technologie. Es lag daran, das Kleingedruckte nicht zu lesen, anzunehmen, dass „Messaging“ „Nachrichtenzustellungsgarantien“ impliziert. Redis Pub/Sub macht keine solchen Garantien und tut nicht so, als ob. Sobald Sie das verstehen, können Sie es angemessen verwenden und zu Streams greifen, wenn Sie mehr benötigen.