Fortsetzung der Übersetzung eines kleinen Buches:
„Understanding Message Brokers“
Autor: Jakub Korab, Verlag: O’Reilly Media, Inc., Erscheinungsdatum: Juni 2017, ISBN: 9781492049296.
Der vorherige übersetzte Abschnitt:
KAPITEL 3
Kafka
Kafka wurde bei LinkedIn entwickelt, um einige Einschränkungen traditioneller Nachrichtenbroker zu umgehen und die Notwendigkeit zu vermeiden, mehrere Nachrichtenbroker für verschiedene „Point-to-Point“-Interaktionen einzurichten, was in diesem Buch im Abschnitt „Vertikale und horizontale Skalierung“ auf Seite 28 beschrieben wird. Die Anwendungsfälle bei LinkedIn basierten hauptsächlich auf der unidirektionalen Verarbeitung sehr großer Datenmengen, wie z.B. Seitenaufrufen und Zugriffsprotokollen, während gleichzeitig mehreren Systemen der Zugriff auf diese Daten ermöglicht wurde, ohne die Leistung der Produzenten oder anderer Verbraucher zu beeinträchtigen. Tatsächlich besteht der Grund für die Existenz von Kafka darin, eine solche Architektur des Nachrichtenaustauschs zu schaffen, wie sie die Universal Data Pipeline beschreibt.
In Anbetracht dieses Endziels sind natürlich auch weitere Anforderungen entstanden. Kafka muss:
- äußerst schnell sein
- Eine hohe Durchsatzrate beim Verarbeiten von Nachrichten bieten
- Die Modelle „Publisher-Subscriber“ und „Point-to-Point“ unterstützen
- Bei der Hinzufügung von Verbrauchern nicht langsamer werden. Beispielsweise verschlechtert sich die Leistung von Warteschlangen und Themen in ActiveMQ bei steigender Anzahl von Verbrauchern am Endpunkt.
- Horizontale Skalierbarkeit ermöglichen; wenn ein Broker, der Nachrichten speichert, dies nur mit der maximalen Geschwindigkeit der Festplatte tun kann, macht es Sinn, über ein einzelnes Broker-Exemplar hinauszugehen, um die Leistung zu steigern.
- Den Zugang zum Speichern und Abrufen von Nachrichten zu differenzieren.
Um all dies zu erreichen, hat Kafka eine Architektur angenommen, die die Rollen und Aufgaben von Clients und Message-Brokern neu definiert hat. Das JMS-Modell ist stark brokerzentriert, wo der Broker für die Verbreitung von Nachrichten verantwortlich ist, während die Clients sich nur um das Senden und Empfangen von Nachrichten kümmern müssen. Kafka hingegen ist clientzentriert, wobei der Client viele Funktionen des traditionellen Brokers übernimmt, wie die faire Verteilung relevanter Nachrichten unter den Verbrauchern, im Austausch für einen extrem schnellen und skalierbaren Broker. Für Menschen, die mit traditionellen Nachrichtenaustauschsystemen gearbeitet haben, erfordert die Arbeit mit Kafka grundlegende Änderungen in der Sichtweise.
Dieser Ingenieursansatz hat zur Schaffung einer Messaging-Infrastruktur geführt, die die Durchsatzkapazität um viele Größenordnungen im Vergleich zu einem herkömmlichen Broker steigern kann. Wie wir sehen werden, ist dieser Ansatz mit Kompromissen verbunden, die darauf hindeuten, dass Kafka nicht für bestimmte Arten von Lasten und etablerter Software geeignet ist.
Einheitliches Adressatmodell
Um die oben beschriebenen Anforderungen zu erfüllen, hat Kafka den Nachrichtenaustausch im "Publisher-Subscriber"- und "Point-to-Point"-Modell in einem einzigen Adressatstyp zusammengeführt — dem Topic. Dies verwirrt Menschen, die mit Messaging-Systemen gearbeitet haben, bei denen das Wort "Topic" sich auf einen Broadcast-Mechanismus bezieht, bei dem das Lesen (aus dem Topic) nicht zuverlässig (is nondurable) ist. Die Topics in Kafka sollten als hybride Art von Adressat betrachtet werden, gemäß der Definition, die in der Einleitung zu diesem Buch gegeben wurde.
Im verbleibenden Teil dieses Kapitels wird, sofern nicht ausdrücklich anders angegeben, der Begriff "Topic" sich auf das Kafka-Topic beziehen.
Um vollständig zu verstehen, wie sich die Topics verhalten und welche Garantien sie bieten, müssen wir zunächst untersuchen, wie sie in Kafka implementiert sind.
Jedes Topic in Kafka hat sein eigenes Protokoll.
Produzenten, die Nachrichten an Kafka senden, fügen diesem Protokoll Einträge hinzu, während Konsumenten aus dem Protokoll mit Hilfe von Zeigern lesen, die ständig vorwärts bewegt werden. Periodisch entfernt Kafka die ältesten Teile des Protokolls, unabhängig davon, ob diese Teile gelesen wurden oder nicht. Ein zentrales Merkmal von Kafkas Design ist, dass der Broker sich nicht darum kümmert, ob die Nachrichten gelesen wurden oder nicht — das ist die Verantwortung des Clients.
Die Begriffe „Protokoll“ und „Zeiger“ kommen in . Diese gut bekannten Begriffe werden hier verwendet, um das Verständnis zu erleichtern.
Dieses Modell unterscheidet sich vollständig von ActiveMQ, wo Nachrichten aus allen Warteschlangen in einem einzigen Protokoll gespeichert werden und der Broker die Nachrichten als gelöscht markiert, nachdem sie gelesen wurden.
Lassen Sie uns nun etwas tiefer eintauchen und das Protokoll eines Themas genauer betrachten.
Das Kafka-Protokoll besteht aus mehreren Partitionen (). Kafka garantiert eine strenge Reihenfolge in jeder Partition. Das bedeutet, dass Nachrichten, die in einer bestimmten Reihenfolge in eine Partition geschrieben werden, in derselben Reihenfolge gelesen werden. Jede Partition wird als zirkuläre Protokolldatei (rolling log file) implementiert, die ein Teilmenge (subset) aller Nachrichten enthält, die von seinen Produzenten an das Thema gesendet wurden. Standardmäßig enthält das erstellte Thema eine Partition. Die Idee der Partitionen ist das zentrale Konzept von Kafka für horizontale Skalierung.

Abbildung 3-1. Kafka-Partitionen
Wenn ein Produzent eine Nachricht an das Kafka-Thema sendet, entscheidet er, in welche Partition er die Nachricht senden möchte. Darauf werden wir später genauer eingehen.
Nachrichten lesen
Ein Client, der Nachrichten lesen möchte, verwaltet einen benannten Zeiger, der als Verbrauchergruppe (consumer group)bezeichnet wird, der auf das Offset einer Nachricht in der Partition zeigt. Das Offset ist eine Position mit fortlaufender Nummer, die bei 0 am Anfang der Partition beginnt. Diese Verbrauchergruppe, die in der API über eine benutzerdefinierte Kennung group_id referenziert wird, entspricht einem logischen Verbrauchenden oder System..
Die meisten Systeme, die Messaging verwenden, lesen Daten vom Empfänger durch mehrere Instanzen und Streams zur parallelen Verarbeitung von Nachrichten. Daher gibt es normalerweise viele Instanzen von Konsumenten, die sich eine Gruppe von Konsumenten teilen.
Das Leseproblem kann wie folgt dargestellt werden:
- Ein Thema hat mehrere Partitionen
- Mehrere Gruppen von Konsumenten können gleichzeitig ein Thema nutzen
- Eine Gruppe von Konsumenten kann mehrere separate Instanzen haben
Dies ist ein nicht triviales «viele zu viele»-Problem. Um zu verstehen, wie Kafka mit den Beziehungen zwischen Gruppen von Konsumenten, Instanzen von Konsumenten und Partitionen umgeht, betrachten wir eine Reihe von zunehmend komplexeren Lese-Szenarien.
Konsumenten und Gruppen von Konsumenten
Lassen Sie uns als Ausgangspunkt ein Thema mit einer Partition nehmen ().

Abbildung 3-2. Konsument liest aus der Partition
Wenn sich eine Konsumenteninstanz mit ihrer eigenen group_id mit diesem Thema verbindet, wird ihr eine Partition zum Lesen und ein Offset in dieser Partition zugewiesen. Die Position dieses Offsets wird im Client konfiguriert, entweder als Zeiger auf die letzte Position (die neueste Nachricht) oder auf die früheste Position (die älteste Nachricht). Der Konsument fordert (polls) Nachrichten aus dem Thema an, was zu deren sequenzieller Lesung aus dem Log führt.
Die Position des Offsets wird regelmäßig in Kafka zurückcommitted und als Nachrichten im internen Thema gespeichert _consumer_offsets. Gelesene Nachrichten werden jedoch nicht gelöscht, im Gegensatz zu einem herkömmlichen Broker, und der Client kann das Offset zurückspulen, um bereits gesehene Nachrichten erneut zu verarbeiten.
Wenn ein zweiter logischer Konsument mit einer anderen group_id verbunden wird, verwaltet er einen zweiten Zeiger, der unabhängig von dem ersten ist (). Somit fungiert das Kafka-Thema wie eine Warteschlange, in der es einen Konsumenten gibt, und wie ein übliches Publisher-Subscriber-Thema (pub-sub), auf das mehrere Konsumenten abonniert sind, mit dem zusätzlichen Vorteil, dass alle Nachrichten gespeichert werden und mehrmals verarbeitet werden können.

Abbildung 3-3. Zwei Konsumenten in verschiedenen Gruppen von Konsumenten lesen aus einer Partition
Konsumenten in der Gruppe von Konsumenten
Wenn ein Verbraucher-Exemplar Daten aus einer Partition liest, kontrolliert es den Zeiger vollständig und verarbeitet die Nachrichten, wie im vorherigen Abschnitt beschrieben.
Wenn mehrere Verbraucher-Exemplare mit derselben group_id an ein Thema mit einer Partition angeschlossen sind, wird das zuletzt angeschlossene Exemplar die Kontrolle über den Zeiger übernehmen und von diesem Zeitpunkt an alle Nachrichten erhalten ().

Abbildung 3-4. Zwei Verbraucher in derselben Verbrauchergruppe lesen aus einer Partition
Dieser Verarbeitungsmodus, in dem die Anzahl der Verbraucher-Exemplare die Anzahl der Partitionen übersteigt, kann als eine Art monopolartiger Verbraucher betrachtet werden. Dies kann nützlich sein, wenn Sie eine "aktiv-passive" (oder "heiße-warme") Clusterung Ihrer Verbraucher-Exemplare benötigen, obwohl der parallele Betrieb mehrerer Verbraucher ("aktiv-aktiv" oder "heiße-heiße") viel typischer ist als Verbraucher im Wartemodus.
Dieses oben beschriebene Verhalten bei der Nachrichtenverteilung kann im Vergleich zu einer normalen JMS-Warteschlange überraschend sein. In diesem Modell werden die in die Warteschlange gesendeten Nachrichten gleichmäßig auf zwei Verbraucher verteilt.
Häufig schaffen wir mehrere Verbraucher-Exemplare, entweder zur parallelen Verarbeitung von Nachrichten, zur Erhöhung der Lesegeschwindigkeit oder zur Verbesserung der Robustheit des Leseprozesses. Da immer nur ein Verbraucher-Exemplar gleichzeitig Daten aus einer Partition lesen kann, wie wird dies in Kafka erreicht?
Eine Möglichkeit, dies zu tun, besteht darin, ein Verbraucher-Exemplar zu verwenden, um alle Nachrichten zu lesen und sie an einen Thread-Pool weiterzugeben. Obwohl dieser Ansatz die Verarbeitungsbandbreite erhöht, steigert er die Komplexität der Verbraucherlogik und tut nichts zur Verbesserung der Robustheit des Lesevorgangs. Wenn ein Verbraucher-Exemplar aufgrund eines Stromausfalls oder eines ähnlichen Ereignisses ausfällt, wird die Lesung gestoppt.
Der kanonische Weg, dieses Problem in Kafka zu lösen, besteht darin, mehr Partitionen zu verwenden.OPartitionierung
Partitionierung
Partitionen sind das Hauptmechanismus zur Parallelisierung des Lesens und zur Skalierung eines Themas über die Kapazität einer einzelnen Brokerinstanz hinaus. Um das besser zu verstehen, betrachten wir ein Szenario, in dem es ein Thema mit zwei Partitionen gibt und ein Consumer dieses Thema abonniert.).

Abbildung 3-5. Ein Consumer liest von mehreren Partitionen
In diesem Szenario hat der Consumer die Kontrolle über die Zeiger, die seiner group_id in beiden Partitionen entsprechen, und beginnt, Nachrichten aus beiden Partitionen zu lesen.
Wenn ein zusätzlicher Consumer für dasselbe group_id zu diesem Thema hinzugefügt wird, weist Kafka eine der Partitionen vom ersten auf den zweiten Consumer neu zu. Danach wird jeder Consumer-Instanz aus einer Partition des Themas lesen.).
Um die Verarbeitung von Nachrichten parallel in 20 Threads zu gewährleisten, benötigen Sie mindestens 20 Partitionen. Wenn es weniger Partitionen gibt, bleiben Consumers übrig, die nichts zu tun haben, wie bereits zuvor in der Diskussion über monopolartige Consumers beschrieben.

Abbildung 3-6. Zwei Consumers in derselben Consumer-Gruppe lesen aus verschiedenen Partitionen
Dieses Schema verringert erheblich die Komplexität der Arbeit des Kafka-Brokers im Vergleich zur Verteilung von Nachrichten, die zur Unterstützung einer JMS-Warteschlange erforderlich ist. Hier müssen Sie sich nicht um folgende Punkte kümmern:
- Welcher Consumer die nächste Nachricht basierend auf einer zirkulären (Round-Robin) Verteilung, dem aktuellen Pufferplatz oder vorherigen Nachrichten (wie bei JMS-Nachrichtengruppen) erhalten soll.
- Welche Nachrichten an welche Consumers gesendet wurden und ob sie im Falle eines Fehlers erneut zugestellt werden müssen.
Alles, was der Kafka-Broker tun muss, ist, die Nachrichten dem Consumer nacheinander zu übergeben, wenn dieser sie anfordert.
Die Anforderungen an die Parallelisierung des Lesens und das erneute Senden fehlerhafter Nachrichten entfallen jedoch nicht - die Verantwortung dafür geht einfach vom Broker auf den Client über. Das bedeutet, dass diese Aspekte in Ihrem Code berücksichtigt werden müssen.
Nachrichten senden
Die Verantwortung für die Entscheidung, in welche Partition eine Nachricht gesendet werden soll, liegt beim Producer dieser Nachricht. Um den Mechanismus zu verstehen, mit dem dies geschieht, müssen wir zunächst betrachten, was genau wir tatsächlich senden.
Während wir in JMS eine Nachrichtenstruktur mit Metadaten (Kopfzeilen und Eigenschaften) und einem Körper, der die Nutzlast (payload) enthält, verwenden, ist eine Nachricht in Kafka ein Paar "Schlüssel-Wert". Der Nachrichten-Nutzlast wird als Wert (value) gesendet. Der Schlüssel hingegen wird hauptsächlich für die Partitionierung verwendet und sollte einen geschäftsspezifischen Schlüssel, enthalten, um verwandte Nachrichten in derselben Partition unterzubringen.
In Kapitel 2 haben wir das Szenario von Online-Wetten besprochen, in dem verwandte Ereignisse in der Reihenfolge von einem Konsumenten verarbeitet werden müssen:
- Das Benutzerkonto ist eingerichtet.
- Geld wird auf das Konto überwiesen.
- Eine Wette wird platziert, die Geld vom Konto abzieht.
Wenn jedes Ereignis eine Nachricht darstellt, die an ein Thema gesendet wird, dann wäre in diesem Fall die natürliche Wahl des Schlüssels die Kontowendung.
Wenn eine Nachricht mit der Kafka Producer API gesendet wird, wird sie an die Partitionierungsfunktion weitergeleitet, die unter Berücksichtigung der Nachricht und des aktuellen Zustands des Kafka-Clusters die Partitions-ID zurückgibt, an die die Nachricht gesendet werden soll. Diese Funktion wird in Java über das Partitioner-Interface implementiert.
Dieses Interface sieht folgendermaßen aus:
interface Partitioner {
int partition(String topic,
Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster);
}Die Implementierung des Partitioner zur Bestimmung der Partition verwendet standardmäßig einen Hash-Algorithmus für den Schlüssel (allgemeiner Hashing-Algorithmus über den Schlüssel) oder Round-Robin, wenn kein Schlüssel angegeben ist. Dieser Standardwert funktioniert in den meisten Fällen gut. Sie werden jedoch in Zukunft vielleicht Ihre eigene Strategie schreiben wollen.
Schreiben einer eigenen Partitionierungsstrategie
Lassen Sie uns ein Beispiel betrachten, wenn Sie Metadaten zusammen mit der Nutzlast der Nachricht senden möchten. In unserem Beispiel besteht die Nutzlast aus einer Anweisung zur Einzahlung auf das Spielkonto. Die Anweisung ist das, was wir garantieren möchten, dass es bei der Übertragung nicht modifiziert wird, und wir möchten sicherstellen, dass nur ein vertrauenswürdiges übergeordnetes System diese Anweisung initiieren kann. In diesem Fall einigen sich die sendenden und empfangenden Systeme auf die Verwendung einer Signatur zur Authentifizierung der Nachricht.
In einem normalen JMS definieren wir einfach die Eigenschaft „Nachrichtensignatur“ und fügen sie der Nachricht hinzu. Kafka bietet uns jedoch keinen Mechanismus zur Übertragung von Metadaten — nur Schlüssel und Wert.
Da der Wert die Nutzlast der Banküberweisung (bank transfer payload) ist, deren Integrität wir wahren möchten, bleibt uns keine andere Wahl, als eine Datenstruktur zu definieren, die im Schlüssel verwendet wird. Angenommen, wir benötigen eine Kontoinformation zur Partitionierung, da alle Nachrichten, die sich auf das Konto beziehen, in der richtigen Reihenfolge verarbeitet werden müssen, dann werden wir die folgende JSON-Struktur entwerfen:
{
"signature": "541661622185851c248b41bf0cea7ad0",
"accountId": "10007865234"
}Da der Wert der Signatur je nach Nutzlast variieren wird, wird die Standard-Hash-Strategie des Partitioner-Interfaces nicht zuverlässig verwandte Nachrichten gruppieren. Daher müssen wir unsere eigene Strategie schreiben, die diesen Schlüssel analysiert und den value accountId partitioniert.
Kafka enthält Prüfziffern zur Erkennung von beschädigten Nachrichten im Speicher und verfügt über eine vollständige Palette von Sicherheitsfunktionen. In diesem Fall können jedoch manchmal branchenspezifische Anforderungen auftreten, wie das oben angegebene Beispiel.
Die benutzerdefinierte Partitionierungsstrategie muss sicherstellen, dass alle verwandten Nachrichten in einer Partition landen. Obwohl dies einfach erscheint, kann das Erfordernis durch die Wichtigkeit der Reihenfolge verwandter Nachrichten und die Festigkeit der Anzahl der Partitionen im Thema kompliziert 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 die ursprünglichen Erwartungen übersteigt. Somit können die Nachrichtenschlüssel mit der Partition verbunden sein, in die sie ursprünglich gesendet wurden, was einen Teil des Zustands impliziert, der zwischen den Instanzen des Produzenten verteilt werden muss.
Ein weiterer Faktor, den es zu berücksichtigen gilt, ist die Gleichmäßigkeit der Verteilung von Nachrichten zwischen den Partitionen. In der Regel werden Schlüssel nicht gleichmäßig auf die Nachrichten verteilt, und Hashfunktionen garantieren keine faire Verteilung von Nachrichten bei einer kleinen Anzahl von Schlüsseln.
Es ist wichtig zu beachten, dass, egal wie Sie die Nachrichten aufteilen, der Separator möglicherweise wiederverwendet werden muss.
Betrachten wir die Anforderung zur Replikation von Daten zwischen Kafka-Clustern an unterschiedlichen geografischen Standorten. Zu diesem Zweck wird Kafka mit einem Kommandozeilenwerkzeug namens MirrorMaker geliefert, das verwendet wird, um Nachrichten aus einem Cluster zu lesen und sie in ein anderes zu übertragen.
MirrorMaker muss die Schlüssel des replizierten Themas verstehen, um die relative Reihenfolge der Nachrichten bei der Replikation zwischen Clustern aufrechtzuerhalten, da die Anzahl der Partitionen für dieses Thema in den beiden Clustern unterschiedlich sein kann.
Benutzerdefinierte Partitionierungsstrategien kommen relativ selten vor, da das Standard-Hashing oder die zyklische Verteilung in den meisten Szenarien erfolgreich funktioniert. Wenn Sie jedoch strenge Garantien für die Sortierung benötigen oder Metadaten aus den Payloads extrahieren müssen, sollten Sie sich etwas genauer mit der Partitionierung befassen.
Die Vorteile der Skalierbarkeit und Performance von Kafka ergeben sich daraus, dass einige der Aufgaben eines traditionellen Brokers auf den Client übertragen werden. In diesem Fall wird entschieden, dass potenziell verwandte Nachrichten auf mehrere parallel arbeitende Consumer verteilt werden.
Auch JMS-Broker müssen mit solchen Anforderungen umgehen. Interessanterweise erfordert der Mechanismus zur Sendung verwandter Nachrichten an denselben Consumer, der über JMS Message Groups (eine Art Sticky Load Balancing-Strategie) realisiert wird, dass der Sender die Nachrichten als verknüpft kennzeichnet. Im Fall von JMS ist der Broker dafür verantwortlich, diese Gruppe verbundener Nachrichten an einen von vielen Consumern zu senden und die Eigentumsrechte an der Gruppe zu übertragen, falls der Consumer ausfällt.
Vereinbarungen mit dem Producer
Die Partitionierung ist nicht das einzige, was beim Senden von Nachrichten zu berücksichtigen ist. Lassen Sie uns die Methoden send() der Klasse Producer im Java-API betrachten:
Future send(ProducerRecord record);
Future send(ProducerRecord record, Callback callback);Es sollte sofort darauf hingewiesen werden, dass beide Methoden ein Future zurückgeben, was darauf hinweist, dass die Sendebetrieb nicht sofort ausgeführt wird. Infolgedessen wird die Nachricht (ProducerRecord) in den Sendepuffer für jede aktive Partition geschrieben und im Hintergrund von einem Thread in der Kafka-Client-Bibliothek an den Broker übermittelt. Obwohl dies die Arbeit unglaublich schnell macht, bedeutet es, dass eine unerfahren geschriebene Anwendung Nachrichten verlieren kann, wenn ihr Prozess angehalten wird.
Wie immer gibt es einen Weg, die Sendebetriebs zuverlässiger zu gestalten, ohne die Leistung zu beeinträchtigen. Die Größe dieses Puffers kann auf 0 gesetzt werden, und der Thread der sendenden Anwendung muss warten, bis die Übertragung der Nachricht an den Broker abgeschlossen ist, wie folgt:
RecordMetadata metadata = producer.send(record).get();Noch einmal zum Lesen von Nachrichten
Das Lesen von Nachrichten bringt zusätzliche Komplikationen mit sich, über die es nachzudenken gilt. Im Gegensatz zur JMS-API, die einen Nachrichtenhörer (message listener) in Reaktion auf den Empfang einer Nachricht starten kann, wird das Interface Consumer Kafka nur gepollt (polling). Lassen Sie uns die Methode poll (), die für diesen Zweck verwendet wird, näher betrachten:
ConsumerRecords poll(long timeout);Der Rückgabewert der Methode ist eine Containerstruktur, die mehrere Objekte enthält. ConsumerRecord aus potenziell mehreren Partitionen. ConsumerRecord ist selbst ein Objekt-Holder für ein Schlüssel-Wert-Paar mit den entsprechenden Metadaten, wie der Partition, aus der es stammt.
Wie in Kapitel 2 diskutiert, müssen wir uns ständig bewusst sein, was mit Nachrichten passiert, nachdem sie erfolgreich oder nicht erfolgreich verarbeitet wurden, z. B. wenn der Client eine Nachricht nicht verarbeiten kann oder wenn er abstürzt. Bei JMS wurde dies durch den Bestätigungsmodus (acknowledgement mode) behandelt. Der Broker löscht entweder die erfolgreich verarbeiteten Nachrichten oder liefert die unbearbeiteten oder fehlgeschlagenen Nachrichten erneut aus (vorausgesetzt, es wurden Transaktionen verwendet).
Kafka funktioniert ganz anders. Nachrichten werden nach dem Auslesen nicht im Broker gelöscht, und die Verantwortung dafür, was bei einem Ausfall passiert, liegt im Code, der ausliest.
Wie bereits erwähnt, ist eine Consumer-Gruppe mit dem Offset im Log verknüpft. Die Position im Log, die mit diesem Offset verknüpft ist, entspricht der nächsten Nachricht, die als Antwort ausgegeben wird. poll (). Die entscheidende Bedeutung beim Lesen hat der Zeitpunkt, zu dem diese Verschiebung steigt.
Zurück zur zuvor betrachteten Lesemodel, die Verarbeitung der Nachricht besteht aus drei Phasen:
- Nachricht zum Lesen abrufen.
- Nachricht verarbeiten.
- Nachricht bestätigen.
Der Kafka-Consumer wird mit einer Konfigurationsoption geliefert enable.auto.commit. Dies ist eine häufig verwendete Standardeinstellung, wie es bei Einstellungen mit dem Wort „auto“ üblich ist.
Bis Kafka 0.10 setzte der Client, der diesen Parameter verwendete, den Offset der zuletzt gelesenen Nachricht beim nächsten Aufruf poll () nach der Verarbeitung. Das bedeutete, dass alle bereits abgerufenen Nachrichten erneut verarbeitet werden konnten, wenn der Client sie bereits verarbeitet hatte, aber unerwartet beendet wurde, bevor er den Aufruf tätigte poll (). Da der Broker keinen Zustand hinsichtlich der Anzahl der gelesenen Nachrichten speichert, wird der nächste Konsument, der diese Nachricht abruft, nicht wissen, dass etwas schiefgelaufen ist. Dieses Verhalten war pseudotransaktional. Das Offset wurde nur im Falle einer erfolgreichen Verarbeitung der Nachricht festgeschrieben, aber wenn der Client die Arbeit unterbrach, sendete der Broker die gleiche Nachricht erneut an einen anderen Client. Dieses Verhalten entsprach der Versandgarantie von Nachrichten.mindestens einmal«.
In Kafka 0.10 wurde der Client-Code so geändert, dass das Commit regelmäßig von der Client-Bibliothek ausgeführt wurde, gemäß der Einstellung auto.commit.interval.ms. Dieses Verhalten liegt irgendwo zwischen den Modi JMS AUTO_ACKNOWLEDGE und DUPS_OK_ACKNOWLEDGE. Bei Verwendung des Auto-Commits konnten Nachrichten unabhängig davon bestätigt werden, ob sie tatsächlich verarbeitet wurden – dies konnte im Falle eines langsamen Consumers geschehen. Wenn der Consumer abbrach, wurden die Nachrichten vom nächsten Consumer abgerufen, beginnend mit der bestätigten Position, was zu einem Übersehen von Nachrichten führen konnte. In diesem Fall verlor Kafka keine Nachrichten, der lesende Code verarbeitete sie einfach nicht.
Dieser Modus hat die gleichen Perspektiven wie in Version 0.9: Nachrichten können verarbeitet werden, aber im Falle eines Fehlers könnte der Offset nicht bestätigt werden, was potenziell zu einer doppelten Lieferung führen kann. Je mehr Nachrichten Sie beim Abrufen ziehen poll (), desto größer ist dieses Problem.
Wie im Abschnitt „Nachrichten aus der Warteschlange lesen“ auf Seite 21 diskutiert, gibt es im Nachrichtenaustauschsystem kein Konzept der einmaligen Nachrichtenzustellung, wenn man die Ausfallmodi berücksichtigt.
In Kafka gibt es zwei Möglichkeiten, den Offset zu fixieren (zu committen): automatisch und manuell. In beiden Fällen können Nachrichten mehrmals verarbeitet werden, wenn eine Nachricht verarbeitet wurde, aber ein Fehler vor dem Commit aufgetreten ist. Sie können eine Nachricht auch vollständig ignorieren, wenn das Commit im Hintergrund erfolgt und Ihr Code abgeschlossen ist, bevor er mit der Verarbeitung begonnen hat (möglicherweise in Kafka 0.9 und früheren Versionen).
Um den Commit-Prozess des Offsets manuell zu steuern, können Sie im Kafka-Consumer-API die Einstellung enable.auto.commit auf false setzen und explizit eine der folgenden Methoden aufrufen:
void commitSync();
void commitAsync();Wenn Sie eine Nachricht „mindestens einmal“ verarbeiten möchten, müssen Sie den Offset manuell mit commitSync (), ausführen, indem Sie diesen Befehl direkt nach der Verarbeitung der Nachrichten ausführen.
Diese Methoden erlauben es nicht, Nachrichten zu bestätigen (acknowledged), bevor sie verarbeitet werden, jedoch bieten sie nichts zur Behebung einer möglichen doppelten Verarbeitung, während sie gleichzeitig den Anschein von Transaktionsfähigkeit erwecken. In Kafka gibt es keine Transaktionen. Der Client hat nicht die Möglichkeit, Folgendes zu tun:
- Eine fehlgeschlagene Nachricht automatisch zurückzurollen (roll back). Die Consumer müssen selbst mit Ausnahmen umgehen, die durch problematische Payloads und Backend-Ausfälle entstehen, da sie nicht auf die erneute Zustellung von Nachrichten durch den Broker vertrauen können.
- Nachrichten in mehrere Topics im Rahmen einer atomaren Operation zu senden. Wie wir bald sehen werden, kann die Kontrolle über verschiedene Topics und Partitionen auf unterschiedlichen Maschinen im Kafka-Cluster liegen, die keine Transaktionen bei der Sendung koordinieren. Zum Zeitpunkt der Erstellung dieses Artikels wurde bereits etwas Arbeit geleistet, um dies mit KIP-98 möglich zu machen.
- Das Lesen einer Nachricht aus einem Topic mit dem Senden einer anderen Nachricht in ein anderes Topic zu verknüpfen. Erneut hängt die Architektur von Kafka von vielen unabhängigen Maschinen ab, die als ein Bus arbeiten, und es werden keine Versuche unternommen, dies zu verbergen. Zum Beispiel gibt es keine API-Komponenten, die es ermöglichen würden, Consumer und Producer in der Transaktion. In JMS wird dies durch ein Objekt gewährleistet Sitzung, aus dem erstellt wird MessageProducers und MessageConsumers.
Wenn wir uns nicht auf Transaktionen verlassen können, wie können wir dann eine Semantik gewährleisten, die näher an der liegt, die traditionelle Nachrichtenübermittlungssysteme bieten?
Wenn die Möglichkeit besteht, dass sich der Offset des Consumers erhöht, bevor die Nachricht verarbeitet wurde, zum Beispiel während eines Ausfalls des Consumers, hat der Consumer keine Möglichkeit zu wissen, ob seine Consumer-Gruppe beim Zuweisen einer Partition Nachrichten verpasst hat. Eine der Strategien besteht daher darin, den Offset auf die vorherige Position zurückzusetzen. Die Kafka-Consumer-API bietet die folgenden Methoden dafür an:
void seek(TopicPartition partition, long offset);
void seekToBeginning(Collection partitions); Methode seek () kann zusammen mit der Methode verwendet werden
offsetsForTimes (Map timestampsToSearch) um auf einen Zustand zu einem bestimmten Zeitpunkt in der Vergangenheit zurückzusetzen.
Implizit bedeutet die Verwendung dieses Ansatzes, dass es sehr wahrscheinlich ist, dass einige Nachrichten, die zuvor bearbeitet wurden, erneut gelesen und bearbeitet werden. Um dies zu vermeiden, können wir idempotente Lesevorgänge verwenden, wie in Kapitel 4 beschrieben, um zuvor gesehene Nachrichten nachzuverfolgen und Duplikate auszuschließen.
Alternativ kann der Code Ihres Consumers einfach sein, wenn der Verlust oder die Duplizierung von Nachrichten akzeptabel ist. Wenn wir die Anwendungsfälle betrachten, für die Kafka typischerweise verwendet wird, wie die Verarbeitung von Protokollen, Metriken, Klickverfolgung usw., verstehen wir, dass der Verlust einzelner Nachrichten wahrscheinlich keinen signifikanten Einfluss auf die umgebenden Anwendungen haben wird. In solchen Fällen sind Standardwerte durchaus akzeptabel. Andererseits, wenn Ihre Anwendung Zahlungen übermitteln muss, müssen Sie sorgfältig auf jede einzelne Nachricht achten. Alles hängt vom Kontext ab.
Persönliche Beobachtungen zeigen, dass mit der Zunahme der Nachrichtenintensität der Wert jeder einzelnen Nachricht abnimmt. Große Nachrichtenmengen werden in der Regel als wertvoll betrachtet, wenn sie in aggregierter Form betrachtet werden.
Hohe Verfügbarkeit (High Availability)
Der Ansatz von Kafka in Bezug auf hohe Verfügbarkeit unterscheidet sich wesentlich von dem Ansatz von ActiveMQ. Kafka wurde auf der Grundlage horizontal skalierbarer Cluster entwickelt, in denen alle Brokerinstanzen Nachrichten gleichzeitig empfangen und verteilen.
Ein Kafka-Cluster besteht aus mehreren Brokerinstanzen, die auf verschiedenen Servern arbeiten. Kafka wurde für den Betrieb auf normaler, eigenständiger Hardware entwickelt, bei der jeder Knoten seinen eigenen dedizierten Speicher hat. Die Verwendung von Netzwerkspeichern (SAN) wird nicht empfohlen, da mehrere Rechenknoten um die Zeitintervalle des Speichers konkurrieren und Konflikte erzeugen können.„Die Zeitintervalle des Speichers und Konflikte erzeugen können.
Kafka ist ein ständig aktives System. Viele große Nutzer von Kafka schalten ihre Cluster nie aus, und die Software sorgt ständig für Aktualisierungen durch sequenzielle Neustarts. Dies wird erreicht, indem die Kompatibilität mit der vorherigen Version für Nachrichten und Interaktionen zwischen den Brokern garantiert wird.
Die Broker sind mit dem Servercluster , verbunden, der als Konfigurationsdatenregister fungiert und zur Koordination der Rollen jedes Brokers verwendet wird. ZooKeeper ist selbst ein verteiltes System, das hohe Verfügbarkeit durch die Replikation von Informationen gewährleistet, indem es ein Quorum.
bildet. Im Basisfall wird ein Topic im Kafka-Cluster mit den folgenden Eigenschaften erstellt:
- 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 gibt an, wie viele Brokerinstanzen im Cluster die Protokolle für diese Partition enthalten müssen.
Durch die Verwendung von ZooKeepers zur Koordination versucht Kafka, neue Partitionen gerecht zwischen den Brokern im Cluster zu verteilen. Dies geschieht durch eine Instanz, die die Rolle des Controllers übernimmt.
Zur Laufzeit wird für jede Topic-Partition Controller eine Brokerrolle als Leader zugewiesen (leader, master, führend) und Follower (Follower, Sklaven, Untergebene). Der Broker, der als Führer für diese Partition fungiert, ist verantwortlich für den Empfang aller Nachrichten, die von Produzenten an ihn gesendet werden, und für die Verbreitung der Nachrichten an die Konsumenten. Beim Senden von Nachrichten an die Partition des Themas werden sie auf alle Broker-Knoten repliziert, die als Nachfolger für diese Partition fungieren. Jeder Knoten, der Protokolle für die Partition enthält, wird genannt Replik. Der Broker kann für einige Partitionen als Führer und für andere als Nachfolger fungieren.
Der Nachfolger, der alle Nachrichten enthält, die beim Führer gespeichert sind, wird genannt synchronisierte Replik (Replik, die sich im synchronisierten Zustand befindet, in-sync replica). Wenn der Broker, der als Führer für die Partition fungiert, ausfällt, kann jeder Broker, der sich im aktualisierten oder synchronisierten Zustand für diese Partition befindet, die Rolle des Führers übernehmen. Dies ist ein unglaublich robuster Entwurf.
Ein Teil der Konfiguration des Produzenten ist der Parameter acks, der bestimmt, wie viele Replikate den Erhalt der Nachricht bestätigen (acknowledge) müssen, bevor der Anwendungsstrom mit dem Senden fortfährt: 0, 1 oder alle. Wenn ein Wert von allfestgelegt ist, sendet der Führer beim Empfang der Nachricht eine Bestätigung (confirmation) zurück an den Produzenten, sobald er mehrere Bestätigungen (acknowledgements) von Replikaten (einschließlich sich selbst) erhalten hat, die in der Themenkonfiguration festgelegt sind. min.insync.replicas (standardmäßig 1). Wenn die Nachricht nicht erfolgreich repliziert werden kann, löst der Produzent eine Ausnahme für die Anwendung aus (NotEnoughReplicas oder NotEnoughReplicasAfterAppend).
In einer typischen Konfiguration wird ein Thema mit einem Replikationsfaktor von 3 (1 Führer, 2 Nachfolger für jede Partition) erstellt und der Parameter min.insync.replicas auf 2 gesetzt. In diesem Fall wird der Cluster es erlauben, dass einer der Broker, die die Partition des Themas verwalten, ausfallen kann, ohne dass dies Auswirkungen auf die Client-Anwendungen hat.
Das bringt uns zu dem uns bereits bekannten Kompromiss zwischen Leistung und Zuverlässigkeit. Die Replikation erfolgt durch zusätzliche Wartezeit auf Bestätigungen (acknowledgments) von den Folgesystemen. Da sie jedoch parallel durchgeführt wird, hat die Replikation, zumindest auf drei Knoten, dieselbe Leistung wie auf zwei (unter Ignorierung der erhöhten Netzwerkbandbreitennutzung).
Durch die Verwendung dieses Replikationsschemas meidet Kafka geschickt die Notwendigkeit, jede Nachricht physisch auf der Festplatte durch eine Operation sync()aufzuzeichnen. Jede vom Producer gesendete Nachricht wird im Journal der Partition aufgezeichnet, aber wie in Kapitel 2 besprochen, erfolgt die Aufzeichnung in die Datei zunächst im Puffer des Betriebssystems. Wenn diese Nachricht auf eine andere Kafka-Instanz repliziert wird und sich im Speicher befindet, bedeutet der Verlust des Leaders nicht, dass die Nachricht selbst verloren gegangen ist – diese kann von einer synchronen Replik übernommen werden.
Der Verzicht auf die Notwendigkeit, die Operation sync() auszuführen, bedeutet, dass Kafka Nachrichten mit der Geschwindigkeit empfangen kann, mit der sie im Speicher aufgezeichnet werden können. Umgekehrt gilt: Je länger das Speichern (Flushing) im Speicher auf die Festplatte vermieden werden kann, desto besser. Aus diesem Grund ist es nicht ungewöhnlich, Kafka-Brokern 64 GB oder mehr Speicher zuzuweisen. Diese Speichernutzung bedeutet, dass eine Kafka-Instanz Geschwindigkeiten erreichen kann, die viele tausend Mal schneller sind als die eines traditionellen Nachrichtenbrokers.
Kafka kann auch so konfiguriert werden, dass die Operation sync() auf Nachrichtengruppen angewendet wird. Da in Kafka alles auf die Arbeit mit Gruppen ausgerichtet ist, funktioniert dies tatsächlich ziemlich gut für viele Nutzungsszenarien und ist ein nützliches Werkzeug für Benutzer, die sehr starke Garantien verlangen. Der Großteil der reinen Leistung von Kafka hängt von Nachrichten ab, die in Gruppen an den Broker gesendet werden und die aus dem Broker in aufeinanderfolgende Blöcke gelesen werden. Operationen (bei denen keine Aufgabe zum Kopieren von Daten aus einem Speicherbereich in einen anderen ausgeführt wird). Dies stellt einen großen Vorteil in Bezug auf Leistung und Ressourcen dar und ist nur durch die Verwendung der zugrunde liegenden Protokolldatenstruktur möglich, die das Partitionsschema definiert.
In einem Kafka-Cluster ist eine deutlich höhere Leistung möglich als bei der Verwendung eines einzelnen Kafka-Brokers, da die Topic-Partitionen horizontal auf vielen separaten Maschinen skaliert werden können.
Ergebnisse
In diesem Kapitel haben wir untersucht, wie die Kafka-Architektur die Beziehungen zwischen Clients und Brokern neu definiert, um einen unglaublich robusten Messaging-Pipeline zu bieten, mit einer viel höheren Durchsatzrate als bei einem herkömmlichen Nachrichtenbroker. Wir haben die Funktionen besprochen, die sie nutzt, um dieses Ziel zu erreichen, und einen kurzen Überblick über die Anwendungsarchitektur gegeben, die diese Funktionalität gewährleistet. Im nächsten Kapitel werden wir die häufigen Probleme betrachten, mit denen Messaging-basierte Anwendungen konfrontiert sind, und Strategien zu ihrer Lösung erörtern. Wir schließen das Kapitel ab, indem wir skizzieren, wie man über Messaging-Technologien insgesamt nachdenken kann, damit Sie ihre Eignung für Ihre Anwendungsfälle bewerten können.
Der vorherige übersetzte Abschnitt:
Übersetzung von:
Fortsetzung folgt…
Nur registrierte Benutzer können an der Umfrage teilnehmen. .
Wird Kafka in Ihrer Organisation verwendet?
Ja
Nein
Wurde früher verwendet, jetzt nicht mehr
Geplant zu verwenden
38 Nutzer haben abgestimmt. 8 Nutzer haben sich enthalten.
Quelle: habr.com
