Fortsetzung der Übersetzung eines kleinen Buches:
"Understanding Message Brokers",
Autor: Jakub Korab, Verlag: O’Reilly Media, Inc., Veröffentlichungsdatum: Juni 2017, ISBN: 9781492049296.
Vorheriger übersetzter Teil:
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 unterschiedliche Punkt-zu-Punkt-Interaktionen einzurichten. Dies wird in diesem Buch im Abschnitt "Vertikale und horizontale Skalierung" auf Seite 28 beschrieben. Die Anwendungsfälle bei LinkedIn basierten hauptsächlich auf der einseitigen Verarbeitung von sehr großen Datenmengen, wie Seitenklicks und Zugriffsprotokollen, und ermöglichten es gleichzeitig, dass mehrere Systeme diese Daten nutzen, ohne die Leistung von Produzenten oder anderen Konsumenten zu beeinträchtigen. Tatsächlich besteht der Grund für die Existenz von Kafka darin, eine solche Architektur für den Nachrichtenaustausch zu schaffen, wie sie in der Universal Data Pipeline beschrieben wird.
Angesichts dieses Ziels entstanden natürlich auch andere Anforderungen. Kafka muss:
- äußerst schnell sein
- eine hohe Bandbreite bei der Nachrichtenverarbeitung bieten
- die Modelle „Publisher-Subscriber“ und „Point-to-Point“ unterstützen
- Zögern Sie nicht, neue Verbraucher hinzuzufügen. Beispielsweise nimmt die Leistung und die Warteschlange eines Themas in ActiveMQ ab, wenn die Anzahl der Verbraucher am Zielort steigt.
- Skalierbarkeit in der Breite; 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.
- Zugriff auf die Speicherung und das Abrufen von Nachrichten trennen.
Um all dies zu erreichen, verwendet Kafka eine Architektur, die die Rollen und Aufgaben von Clients und Message Brokers 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 clientorientiert, wobei der Client viele Funktionen eines traditionellen Brokers übernimmt, z. B. die faire Verteilung relevanter Nachrichten unter den Verbrauchern, und im Gegenzug einen extrem schnellen und skalierbaren Broker erhält. Für Personen, die mit traditionellen Messaging-Systemen gearbeitet haben, erfordert die Arbeit mit Kafka grundlegende Veränderungen in der Denkweise.
Diese Ingenieuransatz führte zur Schaffung einer Messaging-Infrastruktur, die die Durchsatzkapazität im Vergleich zu herkömmlichen Brokern um ein Vielfaches steigern kann. Wie wir sehen werden, ist dieser Ansatz jedoch mit Kompromissen verbunden, die bedeuten, dass Kafka für bestimmte Arten von Lasten und Software nicht geeignet ist.
Einheitliches Adressmodell
Um die oben beschriebenen Anforderungen zu erfüllen, hat Kafka die Nachrichtenübermittlungstypen "Veröffentlichung-Abonnement" und "Punkt-zu-Punkt" innerhalb eines Typs von Adressaten kombiniert — Thema. Dies kann Personen verwirren, die mit Messaging-Systemen gearbeitet haben, bei denen das Wort "Thema" sich auf einen Broadcast-Mechanismus bezieht, bei dem das Lesen (aus dem Thema) nicht zuverlässig ist (nicht dauerhaft). Themen in Kafka sollten als hybrider Adresstyp betrachtet werden, gemäß der Definition, die in der Einleitung zu diesem Buch gegeben wird.
In dem restlichen Teil dieses Kapitels, sofern nicht ausdrücklich anders angegeben, bezieht sich der Begriff "Thema" auf das Thema Kafka.
Um vollständig zu verstehen, wie sich Themen verhalten und welche Garantien sie bieten, müssen wir zunächst betrachten, wie sie in Kafka implementiert sind.
Jedes Thema in Kafka hat sein eigenes Journal.
Produzenten, die Nachrichten an Kafka senden, fügen diesen Journaleinträgen hinzu, während Konsumenten aus dem Journal mit Hilfe von Zeigern lesen, die stetig nach vorne bewegt werden. Periodisch entfernt Kafka die ältesten Teile des Journals, unabhängig davon, ob die Nachrichten in diesen Teilen gelesen wurden oder nicht. Ein zentrales Element des Designs von Kafka ist, dass der Broker nicht darauf achtet, ob Nachrichten gelesen wurden oder nicht — das ist die Verantwortung des Clients.
Die Begriffe „Journal“ und „Zeiger“ sind nicht in der . Diese allgemein 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 Journal gespeichert werden und der Broker die Nachrichten als gelöscht markiert, nachdem sie gelesen wurden.
Lassen Sie uns nun etwas tiefer gehen und das Journal des Themas genauer betrachten.
Das Kafka-Journal besteht aus mehreren Partitionen (). Kafka gewährleistet eine strikte Reihenfolge in jeder Partition. Das bedeutet, dass Nachrichten, die in einer bestimmten Reihenfolge in eine Partition geschrieben werden, auch in der gleichen Reihenfolge gelesen werden. Jede Partition wird als kreisförmige (rolling) Logdatei umgesetzt, die enthält eine Teilmenge (Subset) aller Nachrichten, die von seinen Produzenten an das Topic gesendet wurden. Das erstellte Topic hat standardmäßig 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 ein Kafka-Topic sendet, entscheidet er, in welche Partition die Nachricht gesendet werden soll. Wir werden dies später ausführlicher betrachten.
Nachrichten lesen
Ein Client, der Nachrichten lesen möchte, verwaltet einen benannten Zeiger, der als Consumer-Gruppe (consumer group)bezeichnet wird, der auf den Offset der Nachricht in der Partition verweist. Der Offset ist eine Position mit einer wachsenden Nummer, die bei 0 zu Beginn der Partition beginnt. Diese Consumer-Gruppe, auf die im API über eine benutzerdefinierte Identifikation group_id verwiesen wird, entspricht einem logischen Verbraucher oder System..
Die meisten Messaging-Systeme lesen Daten vom Empfänger über mehrere Instanzen und Streams zur parallelen Verarbeitung von Nachrichten. In der Regel werden daher viele Consumer-Instanzen die gleiche Consumer-Gruppe gemeinsam nutzen.
Das Leseproblem kann folgendermaßen dargestellt werden:
- Ein Topic hat mehrere Partitionen.
- Verschiedene Consumer-Gruppen können gleichzeitig auf dasselbe Topic zugreifen.
- Eine Consumer-Gruppe kann mehrere separate Instanzen haben.
Dies stellt ein nicht triviales "viele zu viele"-Problem dar. Um zu verstehen, wie Kafka die Beziehungen zwischen Consumer-Gruppen, Consumer-Instanzen und Partitionen handhabt, betrachten wir eine Reihe von zunehmend komplexeren Lese-Szenarien.
Consumer und Consumer-Gruppen
Lassen Sie uns als Ausgangspunkt ein Topic mit einer Partition betrachten ().

Abbildung 3-2. Der Consumer liest aus der Partition
Wenn eine Consumer-Instanz sich mit ihrer eigenen group_id mit diesem Topic verbindet, wird ihr eine Lese-Partition und ein Offset in dieser Partition zugewiesen. Der Wert 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 Consumer fragt Nachrichten aus dem Topic ab, was zu einer sequentiellen Wiedergabe aus dem Log führt.
Die Offset-Position wird regelmäßig zurück in Kafka committed und als Nachrichten im internen Topic gespeichert. _consumer_offsets. Gelesene Nachrichten werden dennoch nicht gelöscht, im Gegensatz zu einem normalen Broker, und der Client kann das Offset zurückspulen, um bereits betrachtete Nachrichten erneut zu verarbeiten.
Wenn ein zweiter logischer Consumer mit einer anderen group_id verbunden wird, verwaltet dieser einen zweiten Zeiger, der unabhängig vom ersten ist (). Auf diese Weise fungiert ein Kafka-Topic als Warteschlange, in der es einen Consumer gibt und zusätzlich ein typisches Publisher-Subscriber-Topic (pub-sub), bei dem mehrere Consumer abonniert sind. Ein weiterer Vorteil ist, dass alle Nachrichten gespeichert werden und mehrfach verarbeitet werden können.

Abbildung 3-3. Zwei Consumer in verschiedenen Consumer-Gruppen lesen aus einer Partition.
Consumer in der Consumer-Gruppe.
Wenn eine Instanz eines Consumers Daten aus einer Partition liest, hat sie die vollständige Kontrolle über den Zeiger und verarbeitet die Nachrichten wie im vorherigen Abschnitt beschrieben.
Wenn mehrere Instanzen von Consumers mit derselben group_id zu einem Topic mit einer Partition verbunden sind, wird die Kontrolle über den Zeiger an die Instanz übergeben, die zuletzt verbunden ist, und ab diesem Zeitpunkt erhält sie alle Nachrichten ().

Abbildung 3-4. Zwei Consumer in derselben Consumer-Gruppe lesen aus einer Partition.
Dieser Verarbeitungsmodus, bei dem die Anzahl der Verbraucherinstanzen die Anzahl der Partitionen übersteigt, kann als eine Art monopolistischer Verbraucher betrachtet werden. Dies kann nützlich sein, wenn Sie eine „aktiv-passive“ (oder „heiß-warm“) Clusterung Ihrer Verbraucherinstanzen benötigen, obwohl der parallele Betrieb mehrerer Verbraucher („aktiv-aktiv“ oder „heiß-heiß“) viel typischer ist als Verbraucher im Bereitschaftsmodus.
Das oben beschriebene Verhalten der Nachrichtenverteilung mag im Vergleich zu einer herkömmlichen JMS-Warteschlange überraschend erscheinen. In diesem Modell werden die in die Warteschlange gesendeten Nachrichten gleichmäßig zwischen zwei Verbrauchern verteilt.
Häufig erstellen wir mehrere Verbraucherinstanzen entweder für die parallele Verarbeitung von Nachrichten, um die Lesegeschwindigkeit zu erhöhen, oder um die Stabilität des Leseprozesses zu verbessern. Da immer nur eine Verbraucherinstanz gleichzeitig Daten aus einer Partition lesen kann, wie wird dies in Kafka erreicht?
Eine Möglichkeit, dies zu erreichen, besteht darin, eine einzelne Instanz des Consumers zu verwenden, um alle Nachrichten zu lesen und sie an einen Pool von Threads weiterzugeben. Obwohl dieser Ansatz die Verarbeitungsdurchsatz erhöht, erhöht er die Komplexität der Logik des Consumers und trägt nichts zur Widerstandsfähigkeit des Lesesystems bei. Wenn eine Instanz des Consumers aufgrund eines Stromausfalls oder ähnlicher Ereignisse ausfällt, wird das Lesen eingestellt.
Der kanonische Weg, dieses Problem in Kafka zu lösen, besteht darin, bereitere Partitions zu verwenden.
Partitionierung
Partitionen sind der Hauptmechanismus zur Parallelisierung des Lesens und zur Skalierung eines Topics über die Durchsatzkapazität einer einzelnen Broker-Instanz hinaus. Um dies besser zu verstehen, betrachten wir eine Situation, in der es ein Topic mit zwei Partitionen gibt und ein Consumer sich für dieses Topic anmeldet ().

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

Abbildung 3-6. Zwei Konsumenten in derselben Konsumentengruppe lesen aus verschiedenen Partitionen.
Dieses Schema reduziert die Komplexität der Arbeit des Kafka-Brokers erheblich 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 Konsument die nächste Nachricht erhalten soll, basierend auf der zyklischen (Round-Robin) Verteilung, der aktuellen Kapazität der Vorab-Puffer oder vorherigen Nachrichten (wie für JMS-Nachrichtengruppen).
- Welche Nachrichten an welche Konsumenten gesendet wurden und ob sie im Falle eines Fehlers erneut zugestellt werden müssen.
Alles, was ein Kafka-Broker tun muss, ist, Nachrichten sequenziell an den Consumer zu übermitteln, wenn dieser sie anfordert.
Die Anforderungen an die Parallelisierung der Lesevorgänge und das erneute Versenden gescheiterter Nachrichten bleiben jedoch bestehen – die Verantwortung dafür geht einfach vom Broker auf den Client über. Das bedeutet, dass sie in Ihrem Code berücksichtigt werden müssen.
Nachrichten senden
Die Entscheidung, in welche Partition die Nachricht gesendet werden soll, liegt beim Produzenten dieser Nachricht. Um den Mechanismus zu verstehen, wie dies geschieht, müssen wir zunächst betrachtet, was genau wir tatsächlich senden.
Während wir in JMS eine Nachrichtenstruktur mit Metadaten (Kopfzeilen und Eigenschaften) und einem Körper verwenden, der die Nutzlast (payload) enthält, ist in Kafka eine Nachricht ein Paar „Schlüssel-Wert“. Die Nutzlast der Nachricht wird als Wert (value) gesendet. Der Schlüssel hingegen wird hauptsächlich für die Partitionierung verwendet und muss einen geschäftsspezifischen Schlüsselenthalten, um verwandte Nachrichten in dieselbe Partition zu bringen.
In Kapitel 2 haben wir das Szenario der Online-Wetten behandelt, bei dem verbundene Ereignisse der Reihe nach von einem einzelnen Verbraucher bearbeitet werden müssen:
- Das Benutzerkonto ist eingerichtet.
- Geld wird dem Konto gutgeschrieben.
- Es wird eine Wette platziert, die Geld vom Konto abhebt.
Wenn jedes Ereignis eine Nachricht ist, die an ein Topic gesendet wird, dann ist der natürliche Schlüssel die Kontonummer.
Wenn eine Nachricht mit der Kafka Producer API gesendet wird, wird sie der Partitionierungsfunktion übergeben, die unter Berücksichtigung der Nachricht und des aktuellen Zustands des Kafka-Clusters die ID der Partition zurückgibt, in die die Nachricht gesendet werden soll. Diese Funktion ist in Java über das Interface Partitioner 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 Hashing-Algorithmus für den Schlüssel oder eine Rundlaufmethode, wenn kein Schlüssel angegeben ist. Dieser Standardwert funktioniert in den meisten Fällen gut. In Zukunft möchten Sie jedoch möglicherweise Ihre eigene Implementierung schreiben.
Eigene Partitionierungsstrategie schreiben
Lassen Sie uns ein Beispiel betrachten, bei dem Sie Metadaten zusammen mit der Payload einer Nachricht senden möchten. Die Payload in unserem Beispiel ist eine Anweisung zur Einzahlung auf ein Spielkonto. Die Anweisung ist das, was wir sicherstellen möchten, dass es bei der Übertragung nicht modifiziert wird, und wir möchten sicher sein, dass nur ein vertrauenswürdiges übergeordnetes System diese Anweisung initiieren kann. In diesem Fall einigen sich absendende und empfangende Systeme auf die Verwendung einer Signatur zur Authentifizierung der Nachricht.
Im 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 einer Banküberweisung (bank transfer payload) ist, deren Integrität wir wahren möchten, bleibt uns nichts anderes übrig, als die Datenstruktur für die Verwendung im Schlüssel zu definieren. Angenommen, wir benötigen eine Kontonummer für die Partitionierung, da alle Nachrichten, die sich auf das Konto beziehen, der Reihenfolge nach bearbeitet werden müssen, dann entwerfen wir die folgende JSON-Struktur:
{
"signature": "541661622185851c248b41bf0cea7ad0",
"accountId": "10007865234"
}Da der Wert der Signatur je nach Nutzlast variieren wird, wird die Standard-Hashing-Strategie der Partitioner-Schnittstelle die zusammenhängenden Nachrichten nicht zuverlässig gruppieren. Daher müssen wir unsere eigene Strategie schreiben, die diesen Schlüssel analysiert und den Wert accountId partitioniert.
Kafka beinhaltet Prüfziffern zur Erkennung von Beschädigungen an Nachrichten in der Speicherung und bietet ein umfassendes Sicherheitsangebot. Trotzdem gibt es manchmal branchenspezifische Anforderungen, wie die oben angeführten.
Die Benutzerstrategie zur Partitionierung sollte sicherstellen, dass alle verwandten Nachrichten in einer Partition zusammengeführt werden. Auch wenn dies einfach erscheint, kann die Anforderung komplizierter werden, da die Reihenfolge der verwandten Nachrichten wichtig ist und wie fest die Anzahl der Partitionen im Thema ist.
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 überschreitet. So können die Schlüssel der Nachrichten 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äßige Verteilung der Nachrichten über die Partitionen. Im Allgemeinen werden Schlüssel nicht gleichmäßig über die Nachrichten verteilt, und Hash-Funktionen garantieren keine faire Verteilung der Nachrichten für eine kleine Menge von Schlüsseln.
Es ist wichtig zu beachten, dass, egal wie Sie sich entscheiden, die Nachrichten zu partitionieren, der Partitionierer möglicherweise wiederverwendet werden muss.
Betrachten wir die Anforderungen an die Datenreplikation zwischen Kafka-Clustern an verschiedenen geografischen Standorten. Für diesen Zweck wird Kafka mit einem Befehlszeilenwerkzeug namens MirrorMaker geliefert, das zum Lesen von Nachrichten aus einem Cluster und deren Übertragung in ein anderes verwendet wird.
MirrorMaker muss die Schlüssel des replizierten Themas verstehen, um die relative Reihenfolge der Nachrichten bei der Replikation zwischen den Clustern aufrechtzuerhalten, da die Anzahl der Partitionen für dieses Thema in beiden Clustern unterschiedlich sein kann.
Benutzerdefinierte Partitionierungsstrategien kommen relativ selten vor, da die Standardmethoden Hashing oder Rundlauf in den meisten Szenarien erfolgreich funktionieren. Wenn jedoch strikte Reihenfolgenzusicherungen erforderlich sind oder wenn Sie Metadaten aus den Nutzlasten extrahieren müssen, sollten Sie die Partitionierung näher betrachten.
Die Vorteile von Skalierbarkeit und Leistung in Kafka resultieren daraus, dass einige Aufgaben des traditionellen Brokers auf den Client übertragen werden. In diesem Fall wird die Entscheidung getroffen, potenziell verwandte Nachrichten auf mehrere parallel arbeitende Verbraucher zu verteilen.
Auch JMS-Broker müssen solche Anforderungen berücksichtigen. Interessanterweise erfordert der Mechanismus zur Zustellung verwandter Nachrichten an denselben Verbraucher, der über JMS Message Groups (eine Art Sticky Load Balancing (SLB)-Strategie) implementiert wird, dass der Sender die Nachrichten als verwandt kennzeichnet. Im Fall von JMS ist der Broker dafür verantwortlich, diese Gruppe verwandter Nachrichten an einen von vielen Verbrauchern zu senden und die Eigentumsrechte an der Gruppe zu übertragen, falls der Verbraucher ausfällt.
Vereinbarungen über Produzenten
Partitionierung ist nicht das einzige, was beim Senden von Nachrichten berücksichtigt werden muss. Lassen Sie uns die Methoden send() der Klasse Producer in der Java-API betrachten:
Future send(ProducerRecord record);
Future send(ProducerRecord record, Callback callback);Es sollte sofort angemerkt werden, dass beide Methoden ein Future zurückgeben, was darauf hinweist, dass der Versand der Operation nicht sofort erfolgt. Dies führt dazu, dass die Nachricht (ProducerRecord) für jede aktive Partition in den Versandpuffer geschrieben wird und im Hintergrund von einem Thread in der Kafka-Client-Bibliothek an den Broker übermittelt wird. Obwohl dies die Arbeit unglaublich schnell macht, bedeutet es, dass eine unvorsichtig geschriebene Anwendung Nachrichten verlieren kann, wenn ihr Prozess gestoppt wird.
Wie immer gibt es einen Weg, die Versandoperation zuverlässiger zu gestalten, jedoch auf Kosten der Leistung. Die Größe dieses Puffers kann auf 0 gesetzt werden, und der sendende Anwendungs-Thread 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 Schwierigkeiten mit sich, über die nachgedacht werden muss. Im Gegensatz zur JMS-API, die einen Nachrichten-Poller (message listener) in Reaktion auf den Eingang einer Nachricht starten kann, fragt die Consumer Kafka nur ab (polling). Lassen Sie uns die verwendete Methode genauer betrachten poll (), die dafür verwendet wird:
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 Halterobjekt für das Schlüssel-Wert-Paar mit entsprechenden Metadaten wie der Partition, aus der es stammt.
Wie in Kapitel 2 besprochen, müssen wir uns ständig darüber im Klaren sein, was mit den Nachrichten nach ihrer erfolgreichen oder nicht erfolgreichen Verarbeitung geschieht, beispielsweise wenn der Client eine Nachricht nicht verarbeiten kann oder die Verarbeitung unterbricht. In JMS wurde dies über den Bestätigungsmodus (acknowledgement mode) gehandhabt. Der Broker entfernt entweder die erfolgreich verarbeiteten Nachrichten oder liefert die nicht verarbeiteten oder fehlgeschlagenen Nachrichten erneut aus (vorausgesetzt, es wurden Transaktionen verwendet).
Kafka funktioniert ganz anders. Nachrichten werden im Broker nach dem Lesen nicht gelöscht und die Verantwortung für das, was im Falle eines Fehlers geschieht, liegt beim Lese-Code selbst.
Wie bereits erwähnt, ist eine Consumer-Gruppe mit dem Offset im Log verbunden. Die Position im Log, die mit diesem Offset verknüpft ist, entspricht der nächsten Nachricht, die als Antwort ausgegeben wird. poll ()Der Zeitpunkt, an dem dieses Offset zunimmt, ist entscheidend für das Lesen.
Zurück zur zuvor besprochenen Lesemodell besteht die Verarbeitung einer Nachricht aus drei Phasen:
- Nachricht zum Lesen extrahieren.
- Nachricht verarbeiten.
- Nachricht bestätigen.
Der Kafka-Consumer wird mit der Konfigurationsoption enable.auto.commit. Dies ist eine häufig verwendete Standardeinstellung, wie es oft bei Optionen mit dem Wort „auto“ der Fall ist.
Bis zu Kafka 0.10 sendete der Client, der dieses Flag nutzte, das Offset der letzten gelesenen Nachricht beim nächsten Aufruf poll () nach der Verarbeitung. Das bedeutete, dass bereits extrahierte (fetched) Nachrichten erneut verarbeitet werden konnten, wenn der Client sie bereits verarbeitet hatte, aber unerwartet vor dem Aufruf abgestürzt ist. poll (). Da der Broker keinerlei Informationen darüber speichert, wie oft eine Nachricht gelesen wurde, weiß der nächste Consumer, der diese Nachricht abruft, nicht, dass etwas Schlechtes passiert ist. Dieses Verhalten war pseudo-transaktional. Die Verschiebung wurde nur bei erfolgreicher Verarbeitung der Nachricht festgeschrieben, aber wenn der Client die Arbeit unterbrach, sendete der Broker dieselbe Nachricht erneut an einen anderen Client. Dieses Verhalten entsprach der Garantie für die Nachrichtenlieferung „mindestens einmal«.
In Kafka 0.10 wurde der Client-Code so geändert, dass das Commit periodisch von der Client-Bibliothek in Übereinstimmung mit der Einstellung auto.commit.interval.ms. Dieses Verhalten liegt irgendwo zwischen den Modi JMS AUTO_ACKNOWLEDGE und DUPS_OK_ACKNOWLEDGE. Bei der Verwendung von Auto-Commit konnten Nachrichten bestätigt werden, unabhängig davon, ob sie tatsächlich verarbeitet wurden – dies konnte bei einem langsamen Verbraucher auftreten. Wenn der Verbraucher unterbrochen wurde, wurden die Nachrichten vom nächsten Verbraucher ab der bestätigten Position abgerufen, was zum Verlust einer Nachricht führen konnte. In diesem Fall hat Kafka die Nachrichten nicht verloren, der Lese-Code hat sie einfach nicht verarbeitet.
Dieser Modus weist dieselben Perspektiven auf wie in der Version 0.9: Nachrichten können verarbeitet werden, aber im Falle eines Fehlers könnte der Offset nicht bestätigt werden, was potenziell zu doppelter Zustellung führen kann. Je mehr Nachrichten Sie beim Abruf poll (), desto größer wird dieses Problem.
Wie im Abschnitt »Nachrichten aus der Warteschlange lesen« auf Seite 21 besprochen, gibt es im Nachrichtenaustausch kein Konzept der einmaligen Zustellung von Nachrichten, wenn man die Fehlermodi berücksichtigt.
In Kafka gibt es zwei Möglichkeiten, einen Offset zu speichern (commit): automatisch und manuell. In beiden Fällen können Nachrichten mehrfach verarbeitet werden, wenn die Nachricht bereits bearbeitet wurde, aber ein Fehler aufgetreten ist, bevor der Commit durchgeführt wurde. Es kann auch vorkommen, dass eine Nachricht gar nicht verarbeitet wird, wenn der Commit im Hintergrund erfolgt ist und Ihr Code bereits beendet wurde, bevor er mit der Verarbeitung begonnen hat (vielleicht in Kafka 0.9 und früheren Versionen).
Sie können den Commit-Prozess manuell im Kafka-Consumer-API steuern, indem Sie den Parameter enable.auto.commit auf false setzen und explizit eine der folgenden Methoden aufrufen:
void commitSync();
void commitAsync();Wenn Sie darauf abzielen, eine Nachricht "mindestens einmal" zu verarbeiten, müssen Sie den Offset manuell mit commitSync()committieren, indem Sie diesen Befehl sofort nach der Verarbeitung der Nachrichten ausführen.
Diese Methoden ermöglichen es nicht, Nachrichten zu bestätigen (acknowledged), bevor sie verarbeitet werden, aber sie tun nichts, um potenzielle doppelte Verarbeitung zu vermeiden und sorgen gleichzeitig für ein Gefühl von Transaktionsfähigkeit. In Kafka gibt es keine Transaktionen. Der Client hat nicht die Möglichkeit, Folgendes durchzuführen:
- Rollback von fehlgeschlagenen Nachrichten automatisch durchführen. Konsumenten müssen selbst Ausnahmen behandeln, die durch fehlerhafte Payloads und Backend-Ausfälle entstehen, da sie sich nicht auf die erneute Zustellung von Nachrichten durch den Broker verlassen können.
- Nachrichten in mehrere Themen im Rahmen einer einzigen atomaren Operation senden. Wie wir gleich sehen werden, kann die Kontrolle über verschiedene Themen und Partitionen auf verschiedenen Maschinen im Kafka-Cluster liegen, die keine Koordination der Transaktionen beim Versand vornehmen. Zum Zeitpunkt des Verfassens dieses Artikels wurde bereits Arbeit geleistet, um dies mit KIP-98 möglich zu machen.
- Das Lesen einer Nachricht aus einem Thema mit dem Senden einer anderen Nachricht in ein anderes Thema verknüpfen. Auch hier basiert die Architektur von Kafka auf vielen unabhängigen Maschinen, die als ein Bus fungieren, und es werden keine Versuche unternommen, dies zu verbergen. Beispielsweise gibt es keine API-Komponenten, die dies ermöglichen würden. Konsument und Produzent in der Transaktion. In JMS wird dies durch ein Objekt sichergestellt, Wir öffnen das Programm Putty, wählen die Kategorieaus dem MessageProducers erstellt werden. und MessageConsumers.
Wenn wir uns nicht auf Transaktionen verlassen können, wie können wir dann eine Semantik gewährleisten, die näher an dem liegt, was traditionelle Nachrichtensysteme bieten?
Falls die Möglichkeit besteht, dass der Consumer-Offset steigen kann, bevor die Nachricht verarbeitet wurde, beispielsweise während eines Consumer-Ausfalls, hat der Consumer keine Möglichkeit zu erfahren, ob seine Consumer-Gruppe Nachrichten übersprungen hat, wenn ihm eine Partition zugewiesen wird. Eine der Strategien besteht daher darin, den Offset auf die vorherige Position zurückzuspulen. Die Kafka-Consumer-API bietet folgende Methoden dafür an:
void seek(TopicPartition partition, long offset);
void seekToBeginning(Collection partitions); Methode seek () kann in Verbindung mit der Methode verwendet werden
offsetsForTimes (Map timestampsToSearch) um auf einen bestimmten Zustand in der Vergangenheit zurückzuspulen.
Implizit bedeutet diese Herangehensweise, dass es sehr wahrscheinlich ist, dass einige Nachrichten, die bereits verarbeitet wurden, erneut gelesen und verarbeitet werden. Um dies zu vermeiden, können wir idempotentes Lesen verwenden, wie in Kapitel 4 beschrieben, um zuvor gesehene Nachrichten zu verfolgen und Duplikate auszuschließen.
Alternativ kann der Code Ihres Verbrauchers einfach sein, wenn der Verlust oder die Duplizierung von Nachrichten akzeptabel ist. Wenn wir Anwendungsfälle betrachten, für die Kafka typischerweise verwendet wird, wie z.B. die Verarbeitung von Ereignisprotokollen, Metriken, Klick-Tracking usw., verstehen wir, dass der Verlust einzelner Nachrichten wahrscheinlich keine signifikanten Auswirkungen auf die umgebenden Anwendungen hat. In solchen Fällen sind Standardwerte durchaus akzeptabel. Andererseits, wenn Ihre Anwendung Zahlungen verarbeiten muss, sollten Sie jedes einzelne Nachricht sorgfältig behandeln. Alles hängt vom Kontext ab.
Persönliche Erfahrungen zeigen, dass mit zunehmender Nachrichtenintensität der Wert jeder einzelnen Nachricht sinkt. Nachrichten in großem Umfang sind in der Regel dann wertvoll, wenn sie aggregiert betrachtet werden.
Hohe Verfügbarkeit (High Availability)
Der Ansatz von Kafka zur hohen Verfügbarkeit unterscheidet sich grundlegend vom Ansatz von ActiveMQ. Kafka ist auf horizontal skalierbaren Clustern aufgebaut, in denen alle Broker-Instanzen Nachrichten gleichzeitig empfangen und verteilen.
Ein Kafka-Cluster besteht aus mehreren Broker-Instanzen, die auf unterschiedlichen Servern arbeiten. Kafka wurde entwickelt, um auf herkömmlicher, dedizierter Hardware zu funktionieren, wobei jeder Knoten über seinen eigenen, zugewiesenen Speicher verfügt. Die Verwendung von Netzwerk-Speichern (SAN) wird nicht empfohlen, da mehrere Rechenknoten um Zeitintervalle im Speicher konkurrieren und Konflikte verursachen können.Äe Intervalle im Speicher und Konflikte verursachen.
Kafka ist stets aktiv System. Viele große Nutzer von Kafka schalten ihre Cluster niemals aus, und die Software sorgt ständig für Aktualisierungen durch sequenzielles Neustarten. Dies wird durch die Gewährleistung der Kompatibilität mit der vorherigen Version für Nachrichten und Interaktionen zwischen Brokern erreicht.
Die Broker sind mit dem Server-Cluster verbunden , der als Konfigurationsdatenregister fungiert und zur Koordination der Rollen jedes Brokers verwendet wird. ZooKeeper selbst ist ein verteiltes System, das hohe Verfügbarkeit durch die Replikation von Informationen gewährleistet, indem es einen Quorum.
Im einfachsten Fall 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 an parallelem Lesen ab.
- Der Replikationsfaktor bestimmt, wie viele Instanzen des Brokers im Cluster die Protokolle für diese Partition enthalten müssen.
Mit ZooKeepers zur Koordination versucht Kafka, neue Partitionen gerecht zwischen den Broker im Cluster zu verteilen. Dies geschieht durch eine Instanz, die die Rolle des Controllers übernimmt.
Zur Laufzeit für jede Partition des Themas Controller weist Brokern Rollen zu als Führer (Leader, Master, Vorreiter) und Nachfolgern (Followers, Sklaven, Untergeordneten). Der Broker, der als Führer für eine bestimmte Partition fungiert, ist verantwortlich für die Annahme aller Nachrichten, die von Produzenten an ihn gesendet werden, und die Verbreitung dieser Nachrichten an Konsumenten. Beim Senden von Nachrichten an die Partition eines Themas werden diese auf alle Knoten des Brokers, die als Nachfolger für diese Partition fungieren, repliziert. Jeder Knoten, der Protokolle für die Partition enthält, wird als Replikbezeichnet. Ein Broker kann als Führer für einige Partitionen und als Nachfolger für andere agieren.
Der Nachfolger, der alle Nachrichten enthält, die beim Führer gespeichert sind, wird als synchronisierte Replik (in einem synchronisierten Zustand befindliche, in-sync Replica). Wenn der Broker, der als Leader für die Partition fungiert, ausfällt, kann jeder Broker, der sich in einem aktuellen oder synchronisierten Zustand für diese Partition befindet, die Führungsrolle übernehmen. Dies ist ein äußerst robuster Entwurf.
Ein Teil der Konfiguration des Producers ist der Parameter acks, der definiert, wie viele Replikate den Erhalt einer Nachricht bestätigen müssen, bevor der Anwendungsstream mit dem Senden fortfährt: 0, 1 oder alle. Wenn der Wert gesetzt ist all, wird der Leader beim Erhalt der Nachricht eine Bestätigung (confirmation) an den Producer senden, sobald er die Bestätigungen (acknowledgements) der Aufzeichnung von mehreren Replikaten (einschließlich sich selbst) erhalten hat, die durch die Einstellung des Themas min.insync.replicas vorgeschrieben sind (Standardwert 1). Wenn die Nachricht nicht erfolgreich repliziert werden kann, wird der Producer eine Ausnahme für die Anwendung auslösen (NotEnoughReplicas oder NotEnoughReplicasAfterAppend).
In einer typischen Konfiguration wird ein Thema mit einem Replikationsfaktor von 3 erstellt (1 Leader, 2 Follower für jede Partition) und der Parameter min.insync.replicas wird auf 2 gesetzt. In diesem Fall kann der Cluster erlauben, dass einer der Broker, die die Partition des Themas verwalten, ohne Einfluss auf die Clientanwendungen abgeschaltet wird.
Das bringt uns zurück zu dem bereits bekannten Kompromiss zwischen Leistung und Zuverlässigkeit. Die Replikation erfolgt durch zusätzliche Wartezeiten für Bestätigungen von den Folgesystemen. Da sie jedoch parallel erfolgt, hat die Replikation, mindestens auf drei Knoten, die gleiche Leistung wie auf zwei (ohne die erhöhte Nutzung der Netzbandbreite zu berücksichtigen).
Durch die Verwendung dieses Replikationsschemas umgeht Kafka geschickt die Notwendigkeit, jede Nachricht physisch auf die Festplatte mit der Operation sync(). Jede von einem Producer gesendete Nachricht wird im Partition-Log aufgezeichnet, aber wie in Kapitel 2 besprochen, erfolgt die initiale Speicherung in einer Datei im Puffer des Betriebssystems. Wenn diese Nachricht auf eine andere Kafka-Instanz repliziert wird und sich im Speicher dieser Instanz befindet, bedeutet der Verlust des Leaders nicht, dass die Nachricht selbst verloren geht – sie kann von einer synchronen Replik übernommen werden.
Verzicht auf die Notwendigkeit, eine Operation auszuführen sync() bedeutet, dass Kafka Nachrichten in dem Tempo empfangen kann, in dem es sie im Speicher aufzeichnen kann. Und umgekehrt, je länger man das Flushen des Speichers auf die Festplatte vermeiden kann, desto besser. Aus diesem Grund ist es nicht ungewöhnlich, dass Kafka-Broker 64 GB oder mehr RAM zugewiesen bekommen. Diese Speichernutzung ermöglicht es einem Kafka-Exemplar, Geschwindigkeiten zu erreichen, die viele Tausend Mal schneller sind als bei traditionellen Message Brokern.
Kafka kann auch für den Einsatz von Operationen konfiguriert werden sync() zu Nachrichtenpaketen. Da alles in Kafka auf die Arbeit mit Paketen ausgerichtet ist, funktioniert dies in der Tat ziemlich gut für viele Anwendungsfälle und ist ein nützliches Werkzeug für Nutzer, die sehr starke Garantien benötigen. Der Großteil der reinen Leistung von Kafka hängt mit den Nachrichten zusammen, die als Pakete an den Broker gesendet werden, und damit, dass diese Nachrichten in aufeinanderfolgen Blöcken vom Broker gelesen werden. Operationen (Vorgänge, bei denen keine Daten von einem Speicherbereich in einen anderen kopiert werden). Letzteres ist ein großer Gewinn in Bezug auf Leistung und Ressourcen und ist nur durch die zugrunde liegende Datenstruktur des Logs möglich, die das Partitionierungsschema bestimmt.
Im Kafka-Cluster ist eine weitaus höhere Leistung möglich als bei der Verwendung eines einzelnen Kafka-Brokers, da die Topic-Partitionen horizontal über viele separate Maschinen skalierbar sind.
Ergebnisse
In diesem Kapitel haben wir untersucht, wie die Architektur von Kafka die Beziehungen zwischen Clients und Brokern neu definiert, um einen unglaublich stabilen Messaging-Kanal zu gewährleisten, der eine weit überlegene Kapazität im Vergleich zu herkömmlichen Messaging-Brokern bietet. Wir haben die Funktionen besprochen, die dafür verwendet werden, und einen kurzen Überblick über die Architektur der Anwendungen gegeben, die diese Funktionen ermöglichen. Im nächsten Kapitel werden wir uns mit den allgemeinen Herausforderungen befassen, die Messaging-basierte Anwendungen bewältigen müssen, und Strategien zu deren Lösung diskutieren. Wir schließen das Kapitel mit Überlegungen zur Handhabung von Messaging-Technologien insgesamt ab, sodass Sie deren Eignung für Ihre Anwendungsfälle besser einschätzen können.
Vorheriger übersetzter Teil:
Übersetzung abgeschlossen:
Die Fortsetzung folgt…
Nur registrierte Benutzer können an der Umfrage teilnehmen. Sind Sie an Contour interessiert?
Wird Kafka in Ihrer Organisation eingesetzt?
Ja
Nein
Wurde früher verwendet, jetzt nicht mehr
Planen wir einzusetzen
38 Nutzer haben abgestimmt. 8 Nutzer haben sich enthalten.
Quelle: habr.com
