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.
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:
| Warteschlange | Log | |
|---|---|---|
| Nach dem Lesen | Nachricht wird gelöscht | Nachricht bleibt, bis ihre Aufbewahrungszeit abläuft |
| Mehrere Leser | teilen sich die Nachrichten – Lastverteilung | jede Gruppe liest alles – unabhängig voneinander |
| Zurückspulen | nicht möglich | jederzeit: Position zurücksetzen und neu verarbeiten |
| Wer merkt sich den Fortschritt? | der Vermittler pro Nachricht | der Leser – eine einzige Zahl, der Offset |
| Stärken | Aufgaben verteilen, flexible Weiterleitung, Prioritäten | riesiger Durchsatz, Ereignisströme, Wiederholbarkeit |
| Beispiele | RabbitMQ, ActiveMQ, Amazon SQS | Apache 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:
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:
„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));
}| Einstellung | Wofür |
|---|---|
acks=all | Der Sender gilt erst als erfolgreich, wenn alle synchronen Kopien die Nachricht haben – sonst kann sie beim Ausfall des Anführers verloren gehen. |
enable.idempotence=true | Der Producer nummeriert seine Nachrichten, damit Neuversuche keine Doppelten im Log erzeugen. In aktuellen Versionen Standard. |
enable.auto.commit=false | Offsets erst nach erfolgreicher Verarbeitung bestätigen – Spring Kafka macht das standardmäßig so. |
retention.ms | Wie lange Nachrichten aufbewahrt werden – Tage, Wochen oder für immer. Mit „log compaction“ bleibt pro Schlüssel nur der jeweils letzte Wert. |
| Spring AMQP / JMS | Für Warteschlangen-Vermittler wie RabbitMQ: @RabbitListener bzw. @JmsListener nach demselben Prinzip. |