Nachrichtenbroker verstehen. Erlernen der Mechanismen des Messaging mit ActiveMQ und Kafka. Kapitel 3. Kafka

Fortsetzung der Übersetzung eines kleinen Buches:
Nachrichtenbroker verstehen
Autor: Jakub Korab, Herausgeber: O'Reilly Media, Inc., Erscheinungsdatum: Juni 2017, ISBN: 9781492049296.

Vorheriger übersetzter Teil: Nachrichtenbroker verstehen. Erlernen der Mechanismen des Messaging mit ActiveMQ und Kafka. Kapitel 1 Einleitung

P "P> RђR'Rђ 3

Kafkaeske Zustände

Kafka wurde bei LinkedIn entwickelt, um einige der Einschränkungen herkömmlicher Nachrichtenbroker zu umgehen und zu vermeiden, dass mehrere Nachrichtenbroker für verschiedene Punkt-zu-Punkt-Interaktionen eingerichtet werden müssen, was in diesem Buch unter „Skalieren nach oben und unten“ auf Seite 28 beschrieben wird . Anwendungsfälle LinkedIn hat sich weitgehend auf die einseitige Erfassung sehr großer Datenmengen, wie Seitenklicks und Zugriffsprotokolle, verlassen und gleichzeitig die Nutzung dieser Daten durch mehrere Systeme ermöglicht, ohne die Produktivität von Produzenten oder anderen Verbrauchern zu beeinträchtigen. Tatsächlich besteht der Grund für die Existenz von Kafka darin, die Art von Messaging-Architektur zu erhalten, die die Universal Data Pipeline beschreibt.

Angesichts dieses Endziels ergaben sich natürlich auch andere Anforderungen. Kafka sollte:

  • Seien Sie extrem schnell
  • Stellen Sie beim Arbeiten mit Nachrichten mehr Bandbreite bereit
  • Unterstützt Publisher-Subscriber- und Point-to-Point-Modelle
  • Machen Sie beim Hinzufügen von Verbrauchern nicht langsamer. Beispielsweise nimmt die Leistung sowohl der Warteschlange als auch des Themas in ActiveMQ ab, wenn die Anzahl der Verbraucher am Ziel wächst.
  • Seien Sie horizontal skalierbar; Wenn ein Broker, der Nachrichten dauerhaft speichert, dies nur mit maximaler Festplattengeschwindigkeit tun kann, ist es sinnvoll, über eine einzelne Broker-Instanz hinauszugehen, um die Leistung zu steigern
  • Beschränken Sie den Zugriff auf das Speichern und erneute Abrufen von Nachrichten

Um all dies zu erreichen, hat Kafka eine Architektur eingeführt, die die Rollen und Verantwortlichkeiten von Kunden und Messaging-Brokern neu definiert. Das JMS-Modell ist sehr Broker-orientiert, wobei der Broker für die Verteilung von Nachrichten verantwortlich ist und sich Clients nur um das Senden und Empfangen von Nachrichten kümmern müssen. Kafka hingegen ist kundenorientiert, wobei der Kunde viele der Merkmale eines traditionellen Brokers übernimmt, wie etwa die faire Verteilung relevanter Nachrichten an Verbraucher, im Gegenzug für einen extrem schnellen und skalierbaren Broker. Für Menschen, die mit traditionellen Nachrichtensystemen gearbeitet haben, erfordert die Arbeit mit Kafka einen grundlegenden Sinneswandel.
Diese technische Richtung hat zur Schaffung einer Messaging-Infrastruktur geführt, die den Durchsatz im Vergleich zu einem herkömmlichen Broker um viele Größenordnungen steigern kann. Wie wir sehen werden, ist dieser Ansatz mit Kompromissen verbunden, was bedeutet, dass Kafka für bestimmte Arten von Workloads und installierter Software nicht geeignet ist.

Einheitliches Zielmodell

Um die oben beschriebenen Anforderungen zu erfüllen, hat Kafka Publish-Subscribe- und Point-to-Point-Messaging unter einer Art von Ziel zusammengefasst: Thema. Dies ist für Leute, die mit Messaging-Systemen gearbeitet haben, verwirrend, da sich das Wort „Thema“ auf einen Übertragungsmechanismus bezieht, von dem (aus dem Thema) das Lesen nicht dauerhaft ist. Kafka-Themen sollten als hybrider Zieltyp betrachtet werden, wie in der Einleitung zu diesem Buch definiert.

Für den Rest dieses Kapitels bezieht sich der Begriff „Thema“, sofern wir nicht ausdrücklich etwas anderes angeben, auf ein Kafka-Thema.

Um vollständig zu verstehen, wie sich Themen verhalten und welche Garantien sie bieten, müssen wir uns zunächst ansehen, wie sie in Kafka implementiert werden.
Jedes Thema in Kafka hat sein eigenes Protokoll.
Produzenten, die Nachrichten an Kafka senden, schreiben in dieses Protokoll, und Verbraucher lesen aus dem Protokoll mithilfe von Zeigern, die sich ständig vorwärts bewegen. Kafka löscht regelmäßig die ältesten Teile des Protokolls, unabhängig davon, ob die Nachrichten in diesen Teilen gelesen wurden oder nicht. Ein zentraler Bestandteil von Kafkas Design ist, dass es dem Broker egal ist, ob Nachrichten gelesen werden oder nicht – dafür ist der Kunde verantwortlich.

Die Begriffe „Protokoll“ und „Zeiger“ kommen in nicht vor Kafka-Dokumentation. Diese bekannten Begriffe werden hier zum besseren Verständnis verwendet.

Dieses Modell unterscheidet sich grundlegend von ActiveMQ, wo Nachrichten aus allen Warteschlangen im selben Protokoll gespeichert werden und der Broker die Nachrichten nach dem Lesen als gelöscht markiert.
Lassen Sie uns nun etwas tiefer gehen und uns das Themenprotokoll genauer ansehen.
Das Kafka-Protokoll besteht aus mehreren Partitionen (Abbildung 3-1). Kafka garantiert eine strikte Reihenfolge in jeder Partition. Dies bedeutet, dass Nachrichten, die in einer bestimmten Reihenfolge auf die Partition geschrieben werden, in derselben Reihenfolge gelesen werden. Jede Partition wird als fortlaufende Protokolldatei implementiert, die Folgendes enthält: Eine Teilmenge (Teilmenge) aller Nachrichten, die von seinen Erstellern an das Thema gesendet wurden. Das erstellte Thema enthält standardmäßig eine Partition. Die Idee der Partitionen ist die zentrale Idee von Kafka zur horizontalen Skalierung.

Nachrichtenbroker verstehen. Erlernen der Mechanismen des Messaging mit ActiveMQ und Kafka. Kapitel 3. Kafka
Abbildung 3-1. Kafka-Partitionen

Wenn ein Produzent eine Nachricht an ein Kafka-Thema sendet, entscheidet er, an welche Partition die Nachricht gesendet werden soll. Wir werden uns das später noch genauer ansehen.

Nachrichten lesen

Der Client, der die Nachrichten lesen möchte, verwaltet einen benannten Zeiger namens Verbrauchergruppe, was darauf hinweist versetzt Nachrichten in der Partition. Ein Offset ist eine inkrementelle Position, die am Anfang einer Partition bei 0 beginnt. Diese Verbrauchergruppe, auf die in der API über die benutzerdefinierte Gruppen-ID verwiesen wird, entspricht ein logischer Verbraucher oder ein logisches System.

Die meisten Messaging-Systeme lesen Daten vom Ziel mithilfe mehrerer Instanzen und Threads, um Nachrichten parallel zu verarbeiten. Daher gibt es in der Regel viele Verbraucherinstanzen, die sich dieselbe Verbrauchergruppe teilen.

Das Problem des Lesens lässt sich wie folgt darstellen:

  • Das Thema hat mehrere Partitionen
  • Mehrere Verbrauchergruppen können ein Thema gleichzeitig nutzen
  • Eine Gruppe von Verbrauchern kann mehrere separate Instanzen haben

Dies ist ein nicht triviales Viele-zu-Viele-Problem. Um zu verstehen, wie Kafka mit Beziehungen zwischen Verbrauchergruppen, Verbraucherinstanzen und Partitionen umgeht, schauen wir uns eine Reihe zunehmend komplexerer Leseszenarien an.

Verbraucher und Verbrauchergruppen

Nehmen wir als Ausgangspunkt ein Thema mit einer Partition (Abbildung 3-2).

Nachrichtenbroker verstehen. Erlernen der Mechanismen des Messaging mit ActiveMQ und Kafka. Kapitel 3. Kafka
Abbildung 3-2. Der Verbraucher liest von der Partition

Wenn eine Verbraucherinstanz mit ihrer eigenen Gruppen-ID eine Verbindung zu diesem Thema herstellt, wird ihr eine Lesepartition und ein Offset in dieser Partition zugewiesen. Die Position dieses Offsets ist im Client als Zeiger auf die aktuellste Position (neueste Nachricht) oder früheste Position (älteste Nachricht) konfigurierbar. Der Verbraucher fordert Nachrichten vom Thema an (fragt sie ab), was dazu führt, dass sie nacheinander aus dem Protokoll gelesen werden.
Die Offset-Position wird regelmäßig an Kafka zurückgegeben und als Nachrichten in einem internen Thema gespeichert _consumer_offsets. Gelesene Nachrichten werden im Gegensatz zu einem regulären Broker immer noch nicht gelöscht, und der Client kann den Offset zurückspulen, um bereits angezeigte Nachrichten erneut zu verarbeiten.

Wenn ein zweiter logischer Verbraucher eine Verbindung unter Verwendung einer anderen Gruppen-ID herstellt, verwaltet er einen zweiten Zeiger, der vom ersten unabhängig ist (Abbildung 3-3). Somit verhält sich ein Kafka-Thema wie eine Warteschlange, in der es einen Verbraucher gibt, und wie ein normales Publish-Subscribe-Thema (Pub-Sub), das von mehreren Verbrauchern abonniert wird, mit dem zusätzlichen Vorteil, dass alle Nachrichten gespeichert werden und mehrfach verarbeitet werden können.

Nachrichtenbroker verstehen. Erlernen der Mechanismen des Messaging mit ActiveMQ und Kafka. Kapitel 3. Kafka
Abbildung 3-3. Zwei Verbraucher in unterschiedlichen Verbrauchergruppen lesen von derselben Partition

Verbraucher in einer Verbrauchergruppe

Wenn eine Verbraucherinstanz Daten von einer Partition liest, hat sie die volle Kontrolle über den Zeiger und verarbeitet Nachrichten wie im vorherigen Abschnitt beschrieben.
Wenn mehrere Instanzen von Verbrauchern mit derselben Gruppen-ID mit einem Thema mit einer Partition verbunden waren, erhält die Instanz, die sich zuletzt verbunden hat, die Kontrolle über den Zeiger und empfängt von diesem Moment an alle Nachrichten (Abbildung 3-4).

Nachrichtenbroker verstehen. Erlernen der Mechanismen des Messaging mit ActiveMQ und Kafka. Kapitel 3. Kafka
Abbildung 3-4. Zwei Verbraucher in derselben Verbrauchergruppe lesen von derselben Partition

Diese Verarbeitungsart, bei der die Anzahl der Verbraucherinstanzen die Anzahl der Partitionen übersteigt, kann als eine Art exklusiver Verbraucher betrachtet werden. Dies kann nützlich sein, wenn Sie ein „Aktiv-Passiv“-Clustering (oder „Hot-Warm“) Ihrer Verbraucherinstanzen benötigen, obwohl die parallele Ausführung mehrerer Verbraucher („Aktiv-Aktiv“ oder „Hot-Hot“) viel typischer ist Verbraucher. Im Standby.

Dieses oben beschriebene Nachrichtenverteilungsverhalten kann im Vergleich zum Verhalten einer normalen JMS-Warteschlange überraschend sein. In diesem Modell werden an die Warteschlange gesendete Nachrichten gleichmäßig auf die beiden Verbraucher verteilt.

Wenn wir mehrere Instanzen von Verbrauchern erstellen, tun wir dies meist, um Nachrichten parallel zu verarbeiten, die Lesegeschwindigkeit zu erhöhen oder die Stabilität des Lesevorgangs zu erhöhen. Wie wird dies in Kafka erreicht, da jeweils nur eine Verbraucherinstanz Daten von einer Partition lesen kann?

Eine Möglichkeit hierfür besteht darin, eine einzelne Verbraucherinstanz zu verwenden, um alle Nachrichten zu lesen und sie an den Thread-Pool weiterzuleiten. Während dieser Ansatz den Verarbeitungsdurchsatz erhöht, erhöht er die Komplexität der Verbraucherlogik und trägt nicht dazu bei, die Robustheit des Lesesystems zu erhöhen. Wenn eine Kopie des Verbrauchers aufgrund eines Stromausfalls oder eines ähnlichen Ereignisses ausfällt, stoppt die Subtraktion.

Der kanonische Weg, dieses Problem in Kafka zu lösen, ist die Verwendung von bОmehr Partitionen.

Partitionierung

Partitionen sind der Hauptmechanismus zum Parallelisieren des Lesens und Skalierens eines Themas über die Bandbreite einer einzelnen Broker-Instanz hinaus. Um dies besser zu verstehen, betrachten wir eine Situation, in der es ein Thema mit zwei Partitionen gibt und ein Verbraucher dieses Thema abonniert (Abbildung 3-5).

Nachrichtenbroker verstehen. Erlernen der Mechanismen des Messaging mit ActiveMQ und Kafka. Kapitel 3. Kafka
Abbildung 3-5. Ein Verbraucher liest von mehreren Partitionen

In diesem Szenario erhält der Verbraucher die Kontrolle über die Zeiger, die seiner Gruppen-ID in beiden Partitionen entsprechen, und beginnt, Nachrichten von beiden Partitionen zu lesen.
Wenn diesem Thema ein zusätzlicher Verbraucher für dieselbe Gruppen-ID hinzugefügt wird, weist Kafka eine der Partitionen vom ersten zum zweiten Verbraucher neu zu. Danach liest jede Instanz des Verbrauchers aus einer Partition des Themas (Abbildung 3-6).

Um sicherzustellen, dass Nachrichten in 20 Threads parallel verarbeitet werden, benötigen Sie mindestens 20 Partitionen. Wenn es weniger Partitionen gibt, bleiben Ihnen Verbraucher übrig, an denen nichts zu bearbeiten ist, wie bereits in der Diskussion über exklusive Verbraucher beschrieben.

Nachrichtenbroker verstehen. Erlernen der Mechanismen des Messaging mit ActiveMQ und Kafka. Kapitel 3. Kafka
Abbildung 3-6. Zwei Verbraucher in derselben Verbrauchergruppe lesen von unterschiedlichen Partitionen

Dieses Schema reduziert die Komplexität des Kafka-Brokers im Vergleich zur Nachrichtenverteilung, die zur Verwaltung der JMS-Warteschlange erforderlich ist, erheblich. Hier brauchen Sie sich um folgende Punkte keine Sorgen zu machen:

  • Welcher Verbraucher die nächste Nachricht erhalten soll, basierend auf der Round-Robin-Zuteilung, der aktuellen Kapazität der Prefetch-Puffer oder vorherigen Nachrichten (wie bei JMS-Nachrichtengruppen).
  • Welche Nachrichten an welche Verbraucher gesendet werden und ob sie im Fehlerfall erneut zugestellt werden sollen.

Der Kafka-Broker muss lediglich Nachrichten nacheinander an den Verbraucher weiterleiten, wenn dieser sie anfordert.

Die Anforderungen an die Parallelisierung des Korrekturlesens und des erneuten Versendens fehlgeschlagener Nachrichten verschwinden jedoch nicht – die Verantwortung dafür geht einfach vom Broker auf den Kunden über. Dies bedeutet, dass sie in Ihrem Code berücksichtigt werden müssen.

Nachrichten senden

Es liegt in der Verantwortung des Erstellers dieser Nachricht, zu entscheiden, an welche Partition eine Nachricht gesendet werden soll. Um den Mechanismus zu verstehen, mit dem dies geschieht, müssen wir zunächst überlegen, was genau wir tatsächlich senden.

Während wir in JMS eine Nachrichtenstruktur mit Metadaten (Header und Eigenschaften) und einem Textkörper verwenden, der die Nutzlast (Payload) enthält, ist die Nachricht in Kafka die Paar „Schlüsselwert“. Die Nachrichtennutzlast wird als Wert gesendet. Der Schlüssel hingegen dient hauptsächlich der Partitionierung und muss enthalten Geschäftslogik-spezifischer Schlüsselum verwandte Nachrichten in derselben Partition abzulegen.

In Kapitel 2 haben wir das Online-Wettszenario besprochen, bei dem zusammengehörige Ereignisse von einem einzelnen Verbraucher der Reihe nach verarbeitet werden müssen:

  1. Das Benutzerkonto ist konfiguriert.
  2. Das Geld wird dem Konto gutgeschrieben.
  3. Es wird eine Wette abgeschlossen, bei der Geld vom Konto abgebucht wird.

Wenn es sich bei jedem Ereignis um eine zu einem Thema gepostete Nachricht handelt, wäre der natürliche Schlüssel die Konto-ID.
Wenn eine Nachricht über die Kafka Producer API gesendet wird, wird sie an eine Partitionsfunktion übergeben, die angesichts der Nachricht und des aktuellen Status des Kafka-Clusters die ID der Partition zurückgibt, an die die Nachricht gesendet werden soll. Diese Funktion wird in Java über die Partitioner-Schnittstelle implementiert.

Diese Schnittstelle sieht folgendermaßen aus:

interface Partitioner {
    int partition(String topic,
        Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster);
}

Die Partitioner-Implementierung verwendet den standardmäßigen Allzweck-Hashing-Algorithmus über den Schlüssel, um die Partition zu bestimmen, oder Round-Robin, wenn kein Schlüssel angegeben ist. Dieser Standardwert funktioniert in den meisten Fällen gut. In Zukunft möchten Sie jedoch Ihre eigenen schreiben.

Schreiben Sie Ihre eigene Partitionierungsstrategie

Schauen wir uns ein Beispiel an, bei dem Sie Metadaten zusammen mit der Nachrichtennutzlast senden möchten. Die Nutzlast in unserem Beispiel ist eine Anweisung, eine Einzahlung auf das Spielkonto vorzunehmen. Bei einer Anweisung möchten wir sicherstellen, dass sie bei der Übertragung nicht verändert wird, und wir möchten sicherstellen, dass nur ein vertrauenswürdiges Upstream-System diese Anweisung initiieren kann. In diesem Fall vereinbaren das sendende und das empfangende System die Verwendung einer Signatur zur Authentifizierung der Nachricht.
Im normalen JMS definieren wir einfach eine Eigenschaft „Nachrichtensignatur“ und fügen sie der Nachricht hinzu. Kafka stellt uns jedoch keinen Mechanismus zur Übergabe von Metadaten zur Verfügung, sondern lediglich einen Schlüssel und einen Wert.

Da es sich bei dem Wert um eine Nutzlast einer Banküberweisung handelt, deren Integrität wir wahren wollen, haben wir keine andere Wahl, als die Datenstruktur zu definieren, die im Schlüssel verwendet werden soll. Unter der Annahme, dass wir für die Partitionierung eine Konto-ID benötigen, da alle Nachrichten, die sich auf ein Konto beziehen, der Reihe nach verarbeitet werden müssen, erstellen wir die folgende JSON-Struktur:

{
  "signature": "541661622185851c248b41bf0cea7ad0",
  "accountId": "10007865234"
}

Da der Wert der Signatur je nach Nutzlast variiert, gruppiert die Standard-Hashing-Strategie der Partitioner-Schnittstelle zusammengehörige Nachrichten nicht zuverlässig. Daher müssen wir unsere eigene Strategie schreiben, die diesen Schlüssel analysiert und den AccountId-Wert partitioniert.

Kafka enthält Prüfsummen zur Erkennung beschädigter Nachrichten im Speicher und verfügt über umfassende Sicherheitsfunktionen. Dennoch treten manchmal branchenspezifische Anforderungen wie die oben genannte auf.

Die Partitionierungsstrategie des Benutzers muss sicherstellen, dass alle zugehörigen Nachrichten in derselben Partition landen. Obwohl dies einfach erscheint, kann die Anforderung durch die Wichtigkeit der Reihenfolge verwandter Beiträge und die Festlegung der Anzahl der Partitionen in einem Thema erschwert werden.

Die Anzahl der Partitionen in einem Thema kann sich im Laufe der Zeit ändern, da sie hinzugefügt werden können, wenn der Datenverkehr über die ursprünglichen Erwartungen hinausgeht. Somit können Nachrichtenschlüssel der Partition zugeordnet werden, an die sie ursprünglich gesendet wurden, was bedeutet, dass es sich um einen Zustand handelt, der zwischen den Produzenteninstanzen geteilt werden soll.

Ein weiterer zu berücksichtigender Faktor ist die gleichmäßige Verteilung von Nachrichten über Partitionen. Typischerweise werden Schlüssel nicht gleichmäßig auf Nachrichten verteilt, und Hash-Funktionen garantieren keine gerechte Verteilung von Nachrichten für einen kleinen Schlüsselsatz.
Es ist wichtig zu beachten, dass das Trennzeichen selbst möglicherweise wiederverwendet werden muss, unabhängig davon, wie Sie Nachrichten aufteilen.

Berücksichtigen Sie die Anforderung, Daten zwischen Kafka-Clustern an verschiedenen geografischen Standorten zu replizieren. Zu diesem Zweck bringt Kafka ein Befehlszeilentool namens MirrorMaker mit, mit dem Nachrichten von einem Cluster gelesen und an einen anderen übertragen werden können.

MirrorMaker muss die Schlüssel des replizierten Themas verstehen, um bei der Replikation zwischen Clustern die relative Reihenfolge zwischen Nachrichten aufrechtzuerhalten, da die Anzahl der Partitionen für dieses Thema in zwei Clustern möglicherweise nicht gleich ist.

Benutzerdefinierte Partitionierungsstrategien sind relativ selten, da Standard-Hashing oder Round-Robin in den meisten Szenarien gut funktionieren. Wenn Sie jedoch starke Ordnungsgarantien benötigen oder Metadaten aus Nutzlasten extrahieren müssen, sollten Sie sich die Partitionierung genauer ansehen.

Die Skalierbarkeits- und Leistungsvorteile von Kafka ergeben sich aus der Verlagerung einiger Verantwortlichkeiten des traditionellen Brokers auf den Kunden. In diesem Fall wird entschieden, potenziell verwandte Nachrichten auf mehrere parallel arbeitende Verbraucher zu verteilen.

Auch JMS-Broker müssen sich mit solchen Anforderungen auseinandersetzen. Interessanterweise erfordert der Mechanismus zum Senden verwandter Nachrichten an denselben Verbraucher, der über JMS Message Groups (eine Variation der SLB-Strategie (Sticky Load Balancing)) implementiert wird, auch, dass der Absender Nachrichten als verwandt markiert. Im Fall von JMS ist der Broker dafür verantwortlich, diese Gruppe zusammengehöriger Nachrichten an einen von vielen Verbrauchern zu senden und das Eigentum an der Gruppe zu übertragen, wenn der Verbraucher ausfällt.

Produzentenvereinbarungen

Beim Versenden von Nachrichten ist nicht nur die Partitionierung zu berücksichtigen. Werfen wir einen Blick auf die send()-Methoden der Producer-Klasse in der Java-API:

Future < RecordMetadata > send(ProducerRecord < K, V > record);
Future < RecordMetadata > send(ProducerRecord < K, V > record, Callback callback);

Es sollte sofort beachtet werden, dass beide Methoden Future zurückgeben, was darauf hinweist, dass der Sendevorgang nicht sofort ausgeführt wird. Das Ergebnis ist, dass für jede aktive Partition eine Nachricht (ProducerRecord) in den Sendepuffer geschrieben und als Hintergrundthread in der Kafka-Clientbibliothek an den Broker gesendet wird. Das macht die Sache zwar unglaublich schnell, bedeutet aber auch, dass eine unerfahrene Anwendung Nachrichten verlieren kann, wenn ihr Prozess gestoppt wird.

Wie immer gibt es eine Möglichkeit, den Sendevorgang auf Kosten der Leistung zuverlässiger zu machen. Die Größe dieses Puffers kann auf 0 gesetzt werden, und der sendende Anwendungsthread wird wie folgt gezwungen zu warten, bis die Nachrichtenübertragung an den Broker abgeschlossen ist:

RecordMetadata metadata = producer.send(record).get();

Mehr zum Lesen von Nachrichten

Das Lesen von Nachrichten bringt zusätzliche Komplexität mit sich, über die spekuliert werden muss. Im Gegensatz zur JMS-API, die als Reaktion auf eine Nachricht einen Nachrichten-Listener ausführen kann, ist die Privatkunden Kafka nur Umfragen. Schauen wir uns die Methode genauer an Umfrage()Zu diesem Zweck verwendet:

ConsumerRecords < K, V > poll(long timeout);

Der Rückgabewert der Methode ist eine Containerstruktur, die mehrere Objekte enthält Verbraucherdatensatz aus möglicherweise mehreren Partitionen. Verbraucherdatensatz ist selbst ein Halterobjekt für ein Schlüssel-Wert-Paar mit zugehörigen Metadaten, beispielsweise der Partition, von der es abgeleitet ist.

Wie in Kapitel 2 besprochen, müssen wir bedenken, was mit Nachrichten passiert, nachdem sie erfolgreich oder nicht erfolgreich verarbeitet wurden, beispielsweise wenn der Client die Nachricht nicht verarbeiten kann oder sie abbricht. In JMS wurde dies über einen Bestätigungsmodus gehandhabt. Der Broker löscht entweder die erfolgreich verarbeitete Nachricht oder stellt die Roh- oder Fake-Nachricht erneut zu (vorausgesetzt, es wurden Transaktionen verwendet).
Kafka funktioniert ganz anders. Nachrichten werden nach dem Korrekturlesen nicht im Broker gelöscht, und was bei einem Fehler passiert, liegt in der Verantwortung des Korrekturlesecodes selbst.

Wie bereits erwähnt, ist die Verbrauchergruppe mit dem Offset im Protokoll verknüpft. Die diesem Offset zugeordnete Protokollposition entspricht der nächsten Nachricht, auf die als Antwort ausgegeben wird Umfrage(). Entscheidend für die Ablesung ist der Zeitpunkt, zu dem dieser Offset zunimmt.

Zurück zum zuvor besprochenen Lesemodell: Die Nachrichtenverarbeitung besteht aus drei Phasen:

  1. Rufen Sie eine Nachricht zum Lesen ab.
  2. Verarbeiten Sie die Nachricht.
  3. Nachricht bestätigen.

Der Kafka-Consumer verfügt über eine Konfigurationsoption enable.auto.commit. Dies ist eine häufig verwendete Standardeinstellung, wie sie häufig bei Einstellungen vorkommt, die das Wort „auto“ enthalten.

Vor Kafka 0.10 sendete ein Client, der diese Option nutzte, beim nächsten Aufruf den Offset der zuletzt gelesenen Nachricht Umfrage() nach der Bearbeitung. Dies bedeutete, dass alle bereits abgerufenen Nachrichten erneut verarbeitet werden konnten, wenn der Client sie bereits verarbeitet hatte, aber vor dem Aufruf unerwartet zerstört wurde Umfrage(). Da der Broker keinen Status darüber behält, wie oft eine Nachricht gelesen wurde, weiß der nächste Verbraucher, der diese Nachricht abruft, nicht, dass etwas Schlimmes passiert ist. Dieses Verhalten war pseudotransaktional. Der Offset wurde nur festgeschrieben, wenn die Nachricht erfolgreich verarbeitet wurde. Wenn der Client jedoch abbrach, sendete der Broker dieselbe Nachricht erneut an einen anderen Client. Dieses Verhalten stand im Einklang mit der Nachrichtenübermittlungsgarantie.mindestens einmal«.

In Kafka 0.10 wurde der Client-Code geändert, sodass der Commit gemäß der Konfiguration regelmäßig von der Client-Bibliothek ausgelöst wird auto.commit.interval.ms. Dieses Verhalten liegt irgendwo zwischen den JMS-Modi AUTO_ACKNOWLEDGE und DUPS_OK_ACKNOWLEDGE. Bei Verwendung von Autocommit konnten Nachrichten unabhängig davon festgeschrieben werden, ob sie tatsächlich verarbeitet wurden – dies könnte im Fall eines langsamen Verbrauchers passieren. Wenn ein Verbraucher abbricht, werden die Nachrichten vom nächsten Verbraucher beginnend an der festgeschriebenen Position abgerufen, was zu einer verpassten Nachricht führen kann. In diesem Fall hat Kafka die Nachrichten nicht verloren, der Lesecode hat sie lediglich nicht verarbeitet.

Dieser Modus verspricht das gleiche Versprechen wie in Version 0.9: Nachrichten können verarbeitet werden, aber wenn dies fehlschlägt, wird der Offset möglicherweise nicht festgeschrieben, was möglicherweise zu einer Verdoppelung der Zustellung führt. Je mehr Nachrichten Sie bei der Ausführung abrufen Umfrage(), desto größer ist dieses Problem.

Wie unter „Lesen von Nachrichten aus einer Warteschlange“ auf Seite 21 erläutert, gibt es in einem Nachrichtensystem keine einmalige Zustellung einer Nachricht, wenn Fehlermodi berücksichtigt werden.

In Kafka gibt es zwei Möglichkeiten, einen Offset (Offset) festzuschreiben: automatisch und manuell. In beiden Fällen können Nachrichten mehrmals verarbeitet werden, wenn die Nachricht zwar verarbeitet wurde, aber vor dem Commit fehlgeschlagen ist. Sie können sich auch dafür entscheiden, die Nachricht überhaupt nicht zu verarbeiten, wenn der Commit im Hintergrund erfolgt und Ihr Code abgeschlossen wurde, bevor er verarbeitet werden konnte (vielleicht in Kafka 0.9 und früher).

Sie können den manuellen Offset-Commit-Prozess in der Kafka-Consumer-API steuern, indem Sie den Parameter festlegen enable.auto.commit auf false setzen und explizit eine der folgenden Methoden aufrufen:

void commitSync();
void commitAsync();

Wenn Sie die Nachricht „mindestens einmal“ verarbeiten möchten, müssen Sie den Offset manuell mit festschreiben commitSync()indem Sie diesen Befehl unmittelbar nach der Verarbeitung der Nachrichten ausführen.

Bei diesen Methoden ist es nicht möglich, Nachrichten vor der Verarbeitung zu bestätigen. Sie tragen jedoch nicht dazu bei, potenzielle Verarbeitungsverzögerungen zu beseitigen, und erwecken gleichzeitig den Anschein, als ob es sich um eine Transaktion handelt. In Kafka gibt es keine Transaktionen. Der Kunde ist nicht in der Lage, Folgendes zu tun:

  • Setzen Sie eine gefälschte Nachricht automatisch zurück. Verbraucher müssen Ausnahmen aufgrund problematischer Nutzlasten und Backend-Ausfälle selbst bewältigen, da sie sich nicht darauf verlassen können, dass der Broker Nachrichten erneut zustellt.
  • Senden Sie Nachrichten an mehrere Themen in einem atomaren Vorgang. Wie wir gleich sehen werden, kann die Kontrolle über verschiedene Themen und Partitionen auf verschiedenen Maschinen im Kafka-Cluster liegen, die beim Senden keine Transaktionen koordinieren. Zum Zeitpunkt des Verfassens dieses Artikels wurden einige Arbeiten durchgeführt, um dies mit dem KIP-98 zu ermöglichen.
  • Verknüpfen Sie das Lesen einer Nachricht zu einem Thema mit dem Senden einer anderen Nachricht zu einem anderen Thema. Auch hier hängt die Architektur von Kafka davon ab, dass viele unabhängige Maschinen als ein Bus laufen, und es wird kein Versuch unternommen, dies zu verbergen. Beispielsweise gibt es keine API-Komponenten, die eine Verknüpfung ermöglichen würden Verbraucher и Prouduser in einer Transaktion. In JMS wird dies durch das Objekt bereitgestellt Sessionaus denen entstehen Nachrichtenproduzenten и MessageConsumers.

Wenn wir uns nicht auf Transaktionen verlassen können, wie können wir dann eine Semantik bereitstellen, die der von herkömmlichen Messaging-Systemen näher kommt?

Wenn die Möglichkeit besteht, dass sich der Offset des Verbrauchers erhöht, bevor die Nachricht verarbeitet wurde, beispielsweise während eines Verbraucherabsturzes, kann der Verbraucher nicht wissen, ob seine Verbrauchergruppe die Nachricht verpasst hat, als ihm eine Partition zugewiesen wurde. Eine Strategie besteht also darin, den Offset auf die vorherige Position zurückzuspulen. Die Kafka-Consumer-API stellt hierfür folgende Methoden zur Verfügung:

void seek(TopicPartition partition, long offset);
void seekToBeginning(Collection < TopicPartition > partitions);

Verfahren suchen() kann mit Methode verwendet werden
offsetsForTimes(Map timestampsToSearch) zu einem bestimmten Zeitpunkt in der Vergangenheit zurückspulen.

Implizit bedeutet die Verwendung dieses Ansatzes, dass es sehr wahrscheinlich ist, dass einige Nachrichten, die zuvor verarbeitet wurden, erneut gelesen und verarbeitet werden. Um dies zu vermeiden, können wir wie in Kapitel 4 beschrieben das idempotente Lesen verwenden, um den Überblick über zuvor angezeigte Nachrichten zu behalten und Duplikate zu entfernen.

Alternativ kann Ihr Verbrauchercode einfach gehalten werden, solange der Verlust oder die Vervielfältigung von Nachrichten akzeptabel ist. Wenn wir Anwendungsfälle betrachten, für die Kafka häufig verwendet wird, wie z. B. die Verarbeitung von Protokollereignissen, Metriken, Klickverfolgung usw., verstehen wir, dass der Verlust einzelner Nachrichten wahrscheinlich keine wesentlichen Auswirkungen auf umliegende Anwendungen haben wird. In solchen Fällen sind die Standardwerte durchaus akzeptabel. Wenn Ihre Anwendung hingegen Zahlungen senden muss, müssen Sie jede einzelne Nachricht sorgfältig bearbeiten. Es kommt alles auf den Kontext an.

Persönliche Beobachtungen zeigen, dass mit zunehmender Intensität der Nachrichten der Wert jeder einzelnen Nachricht abnimmt. Große Nachrichten sind in der Regel wertvoll, wenn sie in aggregierter Form betrachtet werden.

Hohe Verfügbarkeit

Kafkas Ansatz zur Hochverfügbarkeit unterscheidet sich stark vom Ansatz von ActiveMQ. Kafka ist auf Scale-out-Cluster ausgelegt, bei denen alle Broker-Instanzen gleichzeitig Nachrichten empfangen und verteilen.

Ein Kafka-Cluster besteht aus mehreren Broker-Instanzen, die auf verschiedenen Servern ausgeführt werden. Kafka wurde für die Ausführung auf gewöhnlicher Standalone-Hardware entwickelt, wobei jeder Knoten über einen eigenen dedizierten Speicher verfügt. Die Verwendung von Network Attached Storage (SAN) wird nicht empfohlen, da mehrere Rechenknoten um Zeit konkurrieren können.Ыe Speicherintervalle und verursachen Konflikte.

Kafka ist immer auf System. Viele große Kafka-Benutzer fahren ihre Cluster nie herunter und die Software wird immer mit einem sequenziellen Neustart aktualisiert. Dies wird erreicht, indem die Kompatibilität mit der Vorgängerversion für Nachrichten und Interaktionen zwischen Brokern gewährleistet wird.

Mit einem Servercluster verbundene Broker Zookeeper, das als Konfigurationsdatenregister fungiert und zur Koordinierung der Rollen jedes Brokers verwendet wird. ZooKeeper selbst ist ein verteiltes System, das durch die Replikation von Informationen durch Etablierung eine hohe Verfügbarkeit bietet Quorum.

Im Basisfall wird in einem Kafka-Cluster ein Thema mit den folgenden Eigenschaften erstellt:

  • Die Anzahl der Partitionen. Wie bereits erwähnt, hängt der genaue Wert, der hier verwendet wird, vom gewünschten Grad des parallelen Lesens ab.
  • Der Replikationsfaktor (Faktor) bestimmt, wie viele Broker-Instanzen im Cluster Protokolle für diese Partition enthalten sollen.

Mithilfe von ZooKeepers zur Koordination versucht Kafka, neue Partitionen gerecht auf die Broker im Cluster zu verteilen. Dies erfolgt durch eine einzelne Instanz, die als Controller fungiert.

Zur Laufzeit für jede Themenpartition Controller Weisen Sie einem Makler Rollen zu der Leiter (Leiter, Meister, Moderator) und Anhänger (Anhänger, Sklaven, Untergebene). Der Broker fungiert als Leiter dieser Partition und ist dafür verantwortlich, alle von den Produzenten an ihn gesendeten Nachrichten zu empfangen und die Nachrichten an die Verbraucher zu verteilen. Wenn Nachrichten an eine Themenpartition gesendet werden, werden sie auf alle Broker-Knoten repliziert, die als Follower für diese Partition fungieren. Jeder Knoten, der Protokolle für eine Partition enthält, wird aufgerufen Replik. Ein Broker kann für einige Partitionen als Leader und für andere als Follower fungieren.

Ein Follower, der alle vom Leiter gehaltenen Nachrichten enthält, wird aufgerufen synchronisiertes Replikat (ein Replikat, das sich in einem synchronisierten Zustand befindet, ein synchronisiertes Replikat). Wenn ein Broker, der für eine Partition als Leader fungiert, ausfällt, kann jeder Broker, der für diese Partition auf dem neuesten Stand oder synchronisiert ist, die Leader-Rolle übernehmen. Es ist ein unglaublich nachhaltiges Design.

Ein Teil der Producer-Konfiguration ist der Parameter Arsch, die bestimmt, wie viele Replikate den Empfang einer Nachricht bestätigen (bestätigen) müssen, bevor der Anwendungsthread mit dem Senden fortfährt: 0, 1 oder alle. Wenn eingestellt auf alleWenn dann eine Nachricht empfangen wird, sendet der Leiter eine Bestätigung an den Produzenten zurück, sobald er Bestätigungen (Bestätigungen) des Datensatzes von mehreren durch die Themeneinstellung definierten Cues (einschließlich sich selbst) erhält min.insync.replicas (Standard 1). Wenn die Nachricht nicht erfolgreich repliziert werden kann, löst der Produzent eine Anwendungsausnahme aus (NotEnoughReplicas oder NotEnoughReplicasAfterAppend).

Eine typische Konfiguration erstellt ein Thema mit einem Replikationsfaktor von 3 (1 Leader, 2 Follower pro Partition) und dem Parameter min.insync.replicas ist auf 2 gesetzt. In diesem Fall lässt der Cluster zu, dass einer der Broker, der die Themenpartition verwaltet, ausfällt, ohne dass sich dies auf Clientanwendungen auswirkt.

Dies bringt uns zurück zum bereits bekannten Kompromiss zwischen Leistung und Zuverlässigkeit. Die Replikation erfolgt auf Kosten zusätzlicher Wartezeit auf Bestätigungen (Bestätigungen) von Followern. Obwohl die Replikation auf mindestens drei Knoten parallel ausgeführt wird, hat sie die gleiche Leistung wie auf zwei (ohne Berücksichtigung der erhöhten Netzwerkbandbreitennutzung).

Durch die Verwendung dieses Replikationsschemas vermeidet Kafka geschickt die Notwendigkeit, jede Nachricht mit dem Vorgang physisch auf die Festplatte zu schreiben sync(). Jede vom Produzenten gesendete Nachricht wird in das Partitionsprotokoll geschrieben, aber wie in Kapitel 2 besprochen, erfolgt das Schreiben in eine Datei zunächst im Puffer des Betriebssystems. Wenn diese Nachricht auf eine andere Kafka-Instanz repliziert wird und sich in deren Speicher befindet, bedeutet der Verlust des Leaders nicht, dass die Nachricht selbst verloren ging – sie kann von einem synchronisierten Replikat übernommen werden.
Weigerung, die Operation durchzuführen sync() bedeutet, dass Kafka Nachrichten genauso schnell empfangen kann, wie es sie in den Speicher schreiben kann. Umgekehrt gilt: Je länger Sie das Leeren von Speicher auf die Festplatte vermeiden können, desto besser. Aus diesem Grund ist es nicht ungewöhnlich, dass Kafka-Brokern 64 GB oder mehr Speicher zugewiesen werden. Diese Speichernutzung bedeutet, dass eine einzelne Kafka-Instanz problemlos mit Geschwindigkeiten ausgeführt werden kann, die um ein Vielfaches höher sind als die eines herkömmlichen Nachrichtenbrokers.

Kafka kann auch so konfiguriert werden, dass der Vorgang angewendet wird sync() zu Nachrichtenpaketen. Da in Kafka alles paketorientiert ist, funktioniert es für viele Anwendungsfälle tatsächlich recht gut und ist ein nützliches Werkzeug für Benutzer, die sehr starke Garantien benötigen. Ein Großteil der reinen Leistung von Kafka beruht auf den Nachrichten, die als Pakete an den Broker gesendet werden und die mithilfe dieser Nachrichten in aufeinanderfolgenden Blöcken vom Broker gelesen werden Nullkopie Operationen (Operationen, bei denen die Aufgabe, Daten von einem Speicherbereich in einen anderen zu kopieren, nicht ausgeführt wird). Letzteres stellt einen großen Leistungs- und Ressourcengewinn dar und ist nur durch die Verwendung einer zugrunde liegenden Protokolldatenstruktur möglich, die das Partitionsschema definiert.

In einem Kafka-Cluster ist eine wesentlich bessere Leistung möglich als mit einem einzelnen Kafka-Broker, da Themenpartitionen über viele separate Maschinen hinweg skaliert werden können.

Ergebnisse

In diesem Kapitel haben wir untersucht, wie die Kafka-Architektur die Beziehung zwischen Clients und Brokern neu gestaltet, um eine unglaublich robuste Messaging-Pipeline bereitzustellen, deren Durchsatz um ein Vielfaches höher ist als der eines herkömmlichen Message-Brokers. Wir haben die Funktionalität besprochen, die es verwendet, um dies zu erreichen, und einen kurzen Blick auf die Architektur der Anwendungen geworfen, die diese Funktionalität bereitstellen. Im nächsten Kapitel werden wir uns mit den häufigsten Problemen befassen, die Messaging-basierte Anwendungen lösen müssen, und Strategien für den Umgang mit ihnen diskutieren. Am Ende des Kapitels erläutern wir, wie Sie über Messaging-Technologien im Allgemeinen sprechen, damit Sie deren Eignung für Ihre Anwendungsfälle beurteilen können.

Vorheriger übersetzter Teil: Nachrichtenbroker verstehen. Erlernen der Mechanismen des Messaging mit ActiveMQ und Kafka. Kapitel 1

Übersetzung erledigt: tele.gg/middle_java

To be continued ...

An der Umfrage können nur registrierte Benutzer teilnehmen. Einloggenbitte.

Wird Kafka in Ihrer Organisation verwendet?

  • Ja

  • Nein

  • Früher verwendet, jetzt nicht mehr

  • Wir planen zu verwenden

38 Benutzer haben abgestimmt. 8 Benutzer enthielten sich der Stimme.

Source: habr.com

Kommentar hinzufügen