Wie Kafka zur RealitÀt wurde.

Wie Kafka zur RealitÀt wurde.

Hallo, Habr!

Ich arbeite im Team von Tinkoff, das fĂŒr die Entwicklung unseres eigenen Benachrichtigungszentrums zustĂ€ndig ist. HauptsĂ€chlich programmiere ich in Java mit Spring Boot und löse verschiedene technische Probleme, die im Projekt auftreten.

Die meisten unserer Mikrodienste interagieren asynchron ĂŒber einen Nachrichtenbroker. Zuvor verwendeten wir IBM MQ als Broker, der mit der Last nicht mehr zurechtkam, aber gleichzeitig hohe Liefersicherheit bot.

Als Ersatz wurde uns Apache Kafka vorgeschlagen, das ĂŒber ein hohes Skalierungspotenzial verfĂŒgt, jedoch leider eine nahezu individuelle Anpassung der Konfiguration fĂŒr verschiedene Szenarien erfordert. Außerdem erlaubte der Standardmechanismus fĂŒr „at least once delivery“ in Kafka nicht, das erforderliche Konsistenzniveau sofort zu gewĂ€hrleisten. Im Folgenden teile ich unsere Erfahrungen mit der Konfiguration von Kafka und erklĂ€re, wie man „exactly once delivery“ einrichtet und damit umgeht.

Garantiezustellung und mehr

Die nachfolgenden Parameter helfen, eine Reihe von Problemen mit den Standardverbindungseinstellungen zu vermeiden. ZunÀchst möchten wir jedoch einem Parameter besonderes Augenmerk schenken, der das mögliche Debugging erleichtert.

Das wird unterstĂŒtzt durch client.id fĂŒr Producer und Consumer. Auf den ersten Blick kann als Wert der Name der Anwendung verwendet werden, was in den meisten FĂ€llen funktioniert. Allerdings fĂŒhrt die Situation, in der mehrere Consumer in der Anwendung verwendet werden und Sie ihnen denselben client.id zuweisen, zu folgender Warnung:

org.apache.kafka.common.utils.AppInfoParser — Fehler bei der Registrierung von AppInfo mbean javax.management.InstanceAlreadyExistsException: kafka.consumer:type=app-info,id=kafka.test-0

Wenn Sie JMX in einer Anwendung mit Kafka verwenden möchten, könnte dies ein Problem darstellen. In diesem Fall ist es am besten, als Wert fĂŒr client.id eine Kombination aus dem Namen der Anwendung und beispielsweise dem Namen des Topics zu verwenden. Das Ergebnis unserer Konfiguration kann im Output des Befehls kafka-consumer-groups aus den Utilities von Confluent angesehen werden:

Wie Kafka zur RealitÀt wurde.

Nun betrachten wir das Szenario der garantierten NachrichtenĂŒbermittlung. Der Kafka Producer hat den Parameter acks, der es ermöglicht, einzustellen, nach wie vielen Acknowledgments der Cluster-Leiter die Nachricht als erfolgreich gespeichert betrachtet. Dieser Parameter kann folgende Werte annehmen:

  • 0 — Acknowledgments werden nicht gezĂ€hlt.
  • 1 — Standardparameter, Acknowledgment ist nur von 1 Replikat erforderlich.
  • −1 — Acknowledgments von allen synchronisierten Replikaten sind erforderlich (Cluster-Konfiguration). min.insync.replicas).

Von den genannten Werten gibt acks gleich −1 die stĂ€rksten Garantien, dass die Nachricht nicht verloren geht.

Wie wir alle wissen, sind verteilte Systeme unzuverlĂ€ssig. Um sich vor vorĂŒbergehenden AusfĂ€llen zu schĂŒtzen, bietet der Kafka Producer den Parameter retries, der die Anzahl der Wiederholungsversuche wĂ€hrend delivery.timeout.msfestlegt. Da der Parameter retries den Standardwert Integer.MAX_VALUE (2147483647) hat, kann die Anzahl der Wiederholungen der Nachricht nur durch Anpassung von delivery.timeout.ms geregelt werden.

Gehen wir zur Exactly-once-Zustellung ĂŒber.

Die genannten Einstellungen ermöglichen es unserem Producer, Nachrichten mit hoher ZuverlĂ€ssigkeit zu liefern. Lassen Sie uns nun darĂŒber sprechen, wie wir sicherstellen können, dass nur eine Kopie einer Nachricht in ein Kafka-Topic geschrieben wird? Im einfachsten Fall muss der Producer dazu den Parameter enable.idempotence auf den Wert true setzen. Die Idempotenz gewĂ€hrleistet, dass nur eine Nachricht in eine bestimmte Partition eines Topics geschrieben wird. Voraussetzung fĂŒr die Aktivierung der Idempotenz sind die Werte acks = all, retry > 0, max.in.flight.requests.per.connection ≀ 5. Wenn diese Parameter vom Entwickler nicht definiert sind, werden die oben genannten Werte automatisch gesetzt.

Sobald die Idempotenz konfiguriert ist, muss sichergestellt werden, dass dieselben Nachrichten immer in dieselben Partitionen gelangen. Dies kann durch das Einrichten des SchlĂŒssels und des Parameters partitioner.class im Producer erreicht werden. Beginnen wir mit dem SchlĂŒssel. FĂŒr jede Sendung sollte er gleich sein. Dies lĂ€sst sich leicht erreichen, indem man eine beliebige GeschĂ€ftskennung aus der ursprĂŒnglichen Nachricht verwendet. Der Parameter partitioner.class hat den Standardwert — DefaultPartitioner. Bei dieser Standardpartitionierungsstrategie verhalten wir uns wie folgt:

  • Wenn eine Partition bei der Nachrichtensendung explizit angegeben ist, verwenden wir sie.
  • Wenn die Partition nicht angegeben ist, aber ein SchlĂŒssel bereitgestellt wird, wĂ€hlen wir die Partition basierend auf dem Hash des SchlĂŒssels.
  • Wenn weder die Partition noch der SchlĂŒssel angegeben sind, wĂ€hlen wir die Partitionen im Round-Robin-Verfahren aus.

DarĂŒber hinaus ermöglicht die Verwendung eines SchlĂŒssels und idempotenter Sendung mit dem Parameter max.in.flight.requests.per.connection = 1 eine geordnete Verarbeitung der Nachrichten auf dem Consumer. Es ist wichtig zu beachten, dass Sie, falls in Ihrem Cluster ein Zugriffsmanagement eingerichtet ist, die Berechtigungen fĂŒr idempotente Aufzeichnungen im Topic benötigen.

Falls Ihnen die idempotente Sendung nach SchlĂŒssel nicht ausreicht oder die Logik auf der Seite des Producers die Konsistenz der Daten zwischen verschiedenen Partitionen erfordert, können Transaktionen helfen. Zudem können Sie mit einer Ketten-Transaktion die Aufzeichnung in Kafka beispielsweise mit einer Datenbank-Aufzeichnung bedingt synchronisieren. Damit die transaktionale Sendung beim Producer möglich ist, muss er idempotent sein und zusĂ€tzlich transactional.id. Wenn in Ihrem Kafka-Cluster die Zugriffsverwaltung konfiguriert ist, benötigen Sie fĂŒr die transaktionale Speicherung, wie auch fĂŒr die idempotente Speicherung, Schreibberechtigungen, die ĂŒber eine Maske mit dem Wert, der in transactional.id gespeichert ist, erteilt werden können.

Formell kann jede Zeichenfolge, z. B. der Name der Anwendung, als Transaktions-ID verwendet werden. Wenn Sie jedoch mehrere Instanzen derselben Anwendung mit identischem transactional.id ausfĂŒhren, wird die zuerst gestartete Instanz mit einem Fehler gestoppt, da Kafka sie als Zombie-Prozess betrachtet.

org.apache.kafka.common.errors.ProducerFencedException: Der Producer hat einen Vorgang mit einer alten Epoch versucht. Entweder gibt es einen neueren Producer mit derselben transactionalId, oder die Transaktion des Producers wurde vom Broker abgelaufen.

Um dieses Problem zu lösen, fĂŒgen wir dem Anwendungsnamen einen Suffix in Form des Hostnamens hinzu, den wir aus den Umgebungsvariablen erhalten.

Der Producer ist konfiguriert, jedoch steuern Transaktionen in Kafka nur den Geltungsbereich der Nachricht. UnabhÀngig vom Status der Transaktion gelangt die Nachricht sofort in das Topic, besitzt jedoch zusÀtzliche systembedingte Attribute.

Damit solche Nachrichten nicht vorzeitig vom Consumer gelesen werden, muss er den Parameter isolation.level auf den Wert read_committed setzen. Ein solcher Consumer kann weiterhin nicht-transaktionale Nachrichten lesen, wÀhrend er transaktionale Nachrichten erst nach dem Commit lesen kann.
Wenn Sie alle zuvor genannten Einstellungen konfiguriert haben, haben Sie genau einmalige Lieferung eingestellt. Herzlichen GlĂŒckwunsch!

Aber es gibt noch einen weiteren Punkt. Die Transactional.id, die wir oben konfiguriert haben, ist tatsĂ€chlich ein TransaktionsprĂ€fix. An den Transaktionsmanager wird eine fortlaufende Nummer angehĂ€ngt. Die resultierende ID wird auf transactional.id.expiration.ms, die auf dem Kafka-Cluster konfiguriert ist und standardmĂ€ĂŸig auf "7 Tage" gesetzt ist. Wenn innerhalb dieses Zeitraums keine Nachrichten vom Anwender empfangen werden, erhalten Sie bei dem Versuch, die nĂ€chste transaktionale Sendung durchzufĂŒhren, eine InvalidPidMappingException. Danach gibt der Transaktionskoordinator eine neue fortlaufende Nummer fĂŒr die nĂ€chste Transaktion aus. Dabei kann eine Nachricht verloren gehen, wenn die InvalidPidMappingException nicht ordnungsgemĂ€ĂŸ behandelt wird.

Statt der Ergebnisse

Wie man feststellen kann, reicht es nicht aus, Nachrichten nur in Kafka zu senden. Es ist notwendig, eine Kombination von Parametern auszuwĂ€hlen und bereit zu sein, schnelle Änderungen vorzunehmen. In diesem Artikel habe ich versucht, die Konfiguration von Exactly Once Delivery im Detail darzustellen und einige Probleme mit den Konfigurationen client.id und transactional.id zu beschreiben, auf die wir gestoßen sind. Unten sind die Einstellungen fĂŒr Producer und Consumer in kurzer Form aufgefĂŒhrt.

Producer:

  1. acks = all
  2. retries > 0
  3. enable.idempotence = true
  4. max.in.flight.requests.per.connection ≀ 5 (1 — fĂŒr die geordnete Sendung)
  5. transactional.id = ${application-name}-${hostname}

Consumer:

  1. isolation.level = read_committed

Um Fehler in zukĂŒnftigen Anwendungen zu minimieren, haben wir ein Wrapper ĂŒber die Spring-Konfiguration erstellt, in dem bereits Werte fĂŒr einige der genannten Parameter festgelegt sind.

Hier sind ein paar Materialien fĂŒr das Selbststudium:

Quelle: habr.com

ZuverlĂ€ssiges Webhosting mit DDoS-Schutz, VPS- und VDS-Server kaufen đŸ”„ ZuverlĂ€ssiges Webhosting mit DDoS-Schutz, VPS- und VDS-Server kaufen | ProHoster