Grundlagen

Messaging

Nachrichten statt Aufrufe: wie ein Vermittler Dienste entkoppelt, warum eine Warteschlange und ein Log grundverschieden sind, wie Kafka mit Partitionen Reihenfolge und Tempo zugleich schafft – und was passiert, wenn ein Empfänger mitten in der Arbeit abstürzt.

Lesestand

Beim Messaging schicken Dienste einander keine Aufrufe, sondern Nachrichten über einen Vermittler (Broker). Der Absender legt eine Nachricht ab und arbeitet weiter; der Empfänger holt sie, wann er kann. Fällt der Empfänger aus oder ist überlastet, warten die Nachrichten – nichts geht verloren, und der Absender merkt nichts davon.

Es gibt zwei grundverschiedene Modelle. In einer Warteschlange (RabbitMQ, JMS, Amazon SQS) bekommt jede Nachricht genau ein Empfänger, danach ist sie weg – ideal, um Arbeit zu verteilen. In einem Log (Apache Kafka) bleiben Nachrichten gespeichert, jeder Leser merkt sich nur seine Position und kann jederzeit zurückspulen – ideal, wenn viele unabhängige Systeme denselben Strom von Ereignissen brauchen.

Warteschlange oder Log?

Ein Bestelldienst veröffentlicht Ereignisse. Zwei Leser interessieren sich dafür. Schau, was in beiden Modellen passiert – und was, wenn später ein dritter dazukommt:

Zwei Modelle, dieselben Nachrichten
WarteschlangeLog
Nach dem LesenNachricht wird gelöschtNachricht bleibt, bis ihre Aufbewahrungszeit abläuft
Mehrere Leserteilen sich die Nachrichten – Lastverteilungjede Gruppe liest alles – unabhängig voneinander
Zurückspulennicht möglichjederzeit: Position zurücksetzen und neu verarbeiten
Wer merkt sich den Fortschritt?der Vermittler pro Nachrichtder Leser – eine einzige Zahl, der Offset
StärkenAufgaben verteilen, flexible Weiterleitung, Prioritätenriesiger Durchsatz, Ereignisströme, Wiederholbarkeit
BeispieleRabbitMQ, ActiveMQ, Amazon SQSApache Kafka, Redpanda, Amazon Kinesis

Ein Log ist im Grunde die einfachste denkbare Datenstruktur: eine Datei, an die nur hinten angehängt wird. Genau deshalb ist es so schnell – Schreiben und Lesen sind fast immer sequenziell, und das Betriebssystem bedient die meisten Leser aus seinem Seitencache. Kafka schafft so auf gewöhnlicher Hardware Hunderttausende Nachrichten pro Sekunde und Rechner.

Kafka: Topics, Partitionen, Gruppen

Ein einzelnes Log hätte eine Grenze: Es liegt auf einem Rechner und wird von einem Leser pro Gruppe gelesen. Kafka teilt deshalb jedes Topic in Partitionen – mehrere unabhängige Logs. Jede Nachricht hat einen Schlüssel, etwa die Kunden-ID, und der Schlüssel bestimmt per Hash die Partition. In einer Consumer Group bekommt jeder Leser eigene Partitionen zugeteilt:

Partitionen und Leser
Partitionen
Leser in der Gruppe

24 Bestellereignisse von 6 Kunden; Farbe = Kunde. Die Nummer ist die Reihenfolge, in der die Ereignisse gesendet wurden.

Daraus folgen die zwei Regeln, die man über Kafka wissen muss:

  • Reihenfolge gibt es nur innerhalb einer Partition. Alle Ereignisse eines Kunden landen über den Schlüssel in derselben Partition und werden in Reihenfolge gelesen. Über Partitionen hinweg gibt es keine Reihenfolge. Wer „Bestellung angelegt“ und „Bestellung storniert“ mit verschiedenen Schlüsseln schickt, kann sie in falscher Reihenfolge bekommen.
  • Die Zahl der Partitionen begrenzt die Parallelität. Mehr Leser als Partitionen bringt nichts – die überzähligen sitzen untätig herum. Partitionen später zu erhöhen, ändert die Zuordnung der Schlüssel; deshalb plant man sie großzügig.

Jede Partition wird außerdem auf mehrere Rechner kopiert. Ein Rechner ist Anführer der Partition, die anderen folgen – das Muster aus dem Eintrag zu verteilten Systemen. Seit Version 4.0 verwaltet Kafka das mit einem eigenen Raft-basierten Verfahren (KRaft) statt mit dem früher nötigen ZooKeeper.

Wenn ein Leser abstürzt

Ein Leser holt eine Nachricht, verarbeitet sie und meldet dann dem Vermittler „erledigt“ – bei Kafka, indem er seinen Offset weiterschreibt (commit). Was passiert, wenn er zwischen diesen Schritten abstürzt? Die Antwort hängt an der Reihenfolge:

Absturz mitten in der Arbeit

    „Erst verarbeiten, dann bestätigen“ ist der übliche Weg: Nichts geht verloren, dafür kann eine Nachricht doppelt kommen – at least once. Also müssen Empfänger mit Doppelten umgehen können, genau wie im Eintrag zu verteilten Systemen beschrieben: idempotente Operationen oder eine Tabelle bereits verarbeiteter Nachrichten-IDs.

    Giftnachrichten

    Eine Nachricht, die der Empfänger nie verarbeiten kann – kaputtes JSON, ein Wert, den der Code nicht erwartet –, ist eine Giftnachricht. Weil der Empfänger sie nie bestätigt, bekommt er sie wieder und wieder, und in Kafka blockiert sie dabei die ganze Partition: Alles dahinter wartet. Die Lösung ist eine Dead Letter Queue: Nach einigen gescheiterten Versuchen wird die Nachricht in ein eigenes Topic verschoben, der Strom läuft weiter, und ein Mensch schaut sich den Fall später an.

    Für Entwickler übersetzt

    // Senden mit Spring Kafka – der Schlüssel bestimmt die Partition
    kafka.send("bestellungen", kundeId, new BestellungAufgegeben(bestellId, kundeId, summeCent));
    
    // Empfangen: eine Methode, Spring kümmert sich um Gruppe, Partitionen und Offsets
    @KafkaListener(topics = "bestellungen", groupId = "versand")
    void beiBestellung(BestellungAufgegeben e) {
        if (verarbeitet.existsById(e.bestellId())) return;   // Doppelte ignorieren
        versand.planen(e);
    }
    
    // Nach 3 gescheiterten Versuchen in die Dead Letter Queue ("bestellungen.DLT")
    @Bean
    DefaultErrorHandler fehlerbehandlung(KafkaTemplate<Object, Object> t) {
        return new DefaultErrorHandler(new DeadLetterPublishingRecoverer(t), new FixedBackOff(1000, 2));
    }
    EinstellungWofür
    acks=allDer Sender gilt erst als erfolgreich, wenn alle synchronen Kopien die Nachricht haben – sonst kann sie beim Ausfall des Anführers verloren gehen.
    enable.idempotence=trueDer Producer nummeriert seine Nachrichten, damit Neuversuche keine Doppelten im Log erzeugen. In aktuellen Versionen Standard.
    enable.auto.commit=falseOffsets erst nach erfolgreicher Verarbeitung bestätigen – Spring Kafka macht das standardmäßig so.
    retention.msWie lange Nachrichten aufbewahrt werden – Tage, Wochen oder für immer. Mit „log compaction“ bleibt pro Schlüssel nur der jeweils letzte Wert.
    Spring AMQP / JMSFür Warteschlangen-Vermittler wie RabbitMQ: @RabbitListener bzw. @JmsListener nach demselben Prinzip.

    Häufige Irrtümer

    „Kafka ist eine Warteschlange.“ – Kafka ist ein Log. Nachrichten verschwinden nicht beim Lesen, und mehrere Gruppen lesen alles. Für klassische Aufgabenverteilung mit Prioritäten oder Einzelbestätigung sind Warteschlangen oft die bessere Wahl.
    „Nachrichten kommen in der Reihenfolge an, in der sie gesendet wurden.“ – Nur innerhalb einer Partition. Was zusammen in Reihenfolge bleiben muss, braucht denselben Schlüssel.
    „Mehr Leser, mehr Durchsatz.“ – Nur bis zur Zahl der Partitionen. Danach warten die zusätzlichen Leser untätig.
    „Jede Nachricht wird genau einmal verarbeitet.“ – Kafka kann innerhalb seiner selbst mit Transaktionen „exactly once“ herstellen. Sobald die Verarbeitung eine Datenbank oder eine externe API berührt, muss der Empfänger Doppelte selbst erkennen.
    „Asynchron heißt: Fehler sind nicht mehr mein Problem.“ – Sie tauchen nur später und woanders auf. Ohne Dead Letter Queue, Überwachung der Rückstände (consumer lag) und Alarme bleiben sie unbemerkt.

    Verwandte Begriffe