Kafka Grundlagen: Topic, Partition und Offset

1. Überblick

Kafka ist ein verteiltes Message-Broker-System zum Datentransfer zwischen Services mit hohem Durchsatz. Bevor du deinen ersten Producer/Consumer schreibst, musst du drei Kernkonzepte verstehen: Topic, Partition, Offset. Dieser Artikel erklärt jedes Konzept mit konkreten Spring-Boot-Beispielen. Nach dem Lesen wirst du verstehen, wie Daten in Kafka organisiert und gelesen werden, und häufige Anfängerfehler vermeiden.

2. Was ist ein Topic?

Ein Topic ist wie eine „Tabelle” oder ein „Kanal”, der Nachrichten enthält. Producer senden Nachrichten in ein Topic, Consumer lesen Nachrichten aus dem Topic. Ein Topic kann mehrere Producer und mehrere Consumer gleichzeitig haben.

Beispielsweise könnten Sie in einem E-Commerce-System diese Topics haben:

orders.created
orders.cancelled
payments.processed

Jedes Topic enthält Nachrichten, die sich auf einen bestimmten Ereignistyp beziehen. Kafka erzwingt kein Schema für Nachrichten — du entscheidest selbst über das Format (JSON, Avro, Protobuf…).

Erstelle ein Topic per CLI, um es besser zu verstehen:

kafka-topics.sh --create \
  --topic orders.created \
  --bootstrap-server localhost:9092 \
  --partitions 3 \
  --replication-factor 1

Dieser Befehl erstellt das Topic orders.created mit 3 Partitionen. Das ist das nächste Konzept.

3. Partition — Parallelisierungseinheit

3.1. Warum Partitionen?

Ein Topic wird in mehrere Partitionen aufgeteilt. Jede Partition ist ein unabhängiges Log-File, Nachrichten werden am Ende angefügt und nie in der Mitte gelöscht oder geändert. Partitionen ermöglichen Kafka,

  • Daten auf mehreren Brokern zu verteilen.
  • mehreren Consumern zu ermöglichen, parallel zu lesen (ein Consumer pro Partition).
Topic: orders.created (3 Partitionen)

Partition 0: [msg0][msg1][msg4][msg7]...
Partition 1: [msg2][msg5][msg8]...
Partition 2: [msg3][msg6][msg9]...

3.2. Welche Partition bestimmt der Key?

Beim Versenden einer Nachricht verwendet Kafka den Key, um die Partition zu berechnen (via Hash), was sicherstellt, dass Nachrichten mit demselben Key immer in dieselbe Partition gehen — dies erhält die richtige Reihenfolge für zusammenhängende Nachrichten.

// Angenommen, du sendest Events mit orderId als Key
ProducerRecord<String, String> record =
        new ProducerRecord<>("orders.created", "order-123", payloadJson);
kafkaTemplate.send(record);

Diese Zeile erstellt einen ProducerRecord mit dem Key "order-123". Alle Events dieser Bestellung landen immer in derselben Partition, sodass der Consumer sie in der richtigen Reihenfolge liest.

Hinweis: Wenn du keinen Key übergibst (Key = null), verteilt Kafka Nachrichten per Round-Robin (oder mit Sticky Partitioner seit Kafka 2.4+) und garantiert keine Reihenfolge zwischen den Nachrichten.

3.3. Reihenfolge ist nur innerhalb einer Partition garantiert

Das ist der häufigste Verwirrungspunkt: Kafka garantiert nur die Reihenfolge innerhalb einer Partition, nicht über das gesamte Topic hinweg. Wenn du absolute Reihenfolge für eine Nachrichtengruppe benötigst (z.B. alle Events einer orderId), verwende denselben Key für diese Gruppe.

4. Offset — die Leseposition in einer Partition

Jede Nachricht in einer Partition hat eine aufsteigende Sequenznummer, die Offset genannt wird und bei 0 beginnt.

Partition 0:
offset:  0    1    2    3    4
message: m0   m1   m2   m3   m4

Der Offset ist eine Zahl, die eindeutig innerhalb einer Partition ist, nicht über das gesamte Topic. Consumer nutzen Offsets, um zu wissen, „bis wohin sie gelesen haben”.

4.1. Consumer-Gruppe und Offset-Commit

Kafka verfolgt gelesene Offsets über Consumer-Gruppen. Jede Consumer-Gruppe hat ihren eigenen Offset für jede Partition, gespeichert im internen Topic __consumer_offsets.

# application.properties
spring.kafka.consumer.group-id=order-service
spring.kafka.consumer.auto-offset-reset=earliest
spring.kafka.consumer.enable-auto-commit=false
  • group-id: Consumer-Gruppen-Identifier. Mehrere Instanzen mit derselben group-id teilen sich die Partitionen des Topics.
  • auto-offset-reset=earliest: Wenn kein Offset gespeichert ist (neue Consumer-Gruppe), lesen vom Anfang des Topics.
  • enable-auto-commit=false: Deaktivieren Sie Auto-Commit, um selbst zu steuern, wann Offsets committed werden (um Nachrichtenverlust bei Fehlern während der Verarbeitung zu vermeiden).

4.2. Mehrere Consumer in derselben Gruppe

Kafka verteilt Partitionen auf Consumer in derselben Gruppe nach folgendem Prinzip: Eine Partition wird zu jedem Zeitpunkt nur von einem Consumer in der Gruppe gelesen.

Topic orders.created hat 3 Partitionen
Consumer-Gruppe "order-service" hat 2 Instanzen:

Consumer A -> Partition 0, Partition 1
Consumer B -> Partition 2

Wenn du 4 Consumer für ein Topic mit 3 Partitionen ausführst, wird der 4. Consumer untätig und erhält keine Partition. Daher ist die maximale Anzahl nützlicher Consumer = Anzahl der Partitionen.

5. Kafka in Spring Boot einrichten

5.1. Abhängigkeit

<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
</dependency>

5.2. Producer-Nachrichten versenden

@Service
public class OrderEventProducer {

    private final KafkaTemplate<String, String> kafkaTemplate;

    public OrderEventProducer(KafkaTemplate<String, String> kafkaTemplate) {
        this.kafkaTemplate = kafkaTemplate;
    }

    public void publishOrderCreated(String orderId, String payloadJson) {
        kafkaTemplate.send("orders.created", orderId, payloadJson);
    }
}

kafkaTemplate.send(topic, key, value) sendet eine Nachricht an das Topic orders.created und nutzt orderId als Key. Spring Boot erstellt automatisch eine KafkaTemplate-Bean basierend auf der Konfiguration spring.kafka.bootstrap-servers.

5.3. Consumer liest Nachrichten und committed Offsets manuell

@Component
public class OrderEventConsumer {

    @KafkaListener(topics = "orders.created", groupId = "order-service")
    public void onOrderCreated(
            ConsumerRecord<String, String> record,
            Acknowledgment ack) {

        System.out.printf("partition=%d offset=%d key=%s value=%s%n",
                record.partition(), record.offset(), record.key(), record.value());

        // Geschäftslogik wird hier verarbeitet
        ack.acknowledge(); // Offset nach erfolgreicher Verarbeitung committen
    }
}
  • record.partition() und record.offset() sagen dir, in welcher Partition und an welcher Position diese Nachricht ist.
  • ack.acknowledge() committed den Offset nur nach erfolgreicher Verarbeitung — wenn der Service abstürzt, wird die Nachricht beim nächsten Neustart erneut gelesen (dank enable-auto-commit=false kombiniert mit AckMode.MANUAL).

Du musst zusätzliche Konfiguration hinzufügen, damit Acknowledgment funktioniert:

spring.kafka.listener.ack-mode=manual

Ausgabe beim Testen (3 Nachrichten mit verschiedenen Keys versenden):

partition=1 offset=0 key=order-123 value={"status":"CREATED"}
partition=0 offset=0 key=order-456 value={"status":"CREATED"}
partition=2 offset=0 key=order-789 value={"status":"CREATED"}

Jeder Key landet in einer anderen Partition aufgrund verschiedener Hashes — genau wie in Abschnitt 3.2 erläutert.

6. Test mit JUnit 5

Verwende spring-kafka-test, um Kafka im Speicher in Unit-Tests auszuführen, ohne einen echten Broker zu benötigen.

<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka-test</artifactId>
    <scope>test</scope>
</dependency>
@SpringBootTest
@EmbeddedKafka(partitions = 1, topics = "orders.created")
class OrderEventProducerTest {

    @Autowired
    private OrderEventProducer producer;

    @Autowired
    private EmbeddedKafkaBroker embeddedKafka;

    @Test
    void shouldPublishMessageWithGivenKey() throws Exception {
        Consumer<String, String> consumer = createTestConsumer();
        embeddedKafka.consumeFromAnEmbeddedTopic(consumer, "orders.created");

        producer.publishOrderCreated("order-123", "{\"status\":\"CREATED\"}");

        ConsumerRecord<String, String> record =
                KafkaTestUtils.getSingleRecord(consumer, "orders.created");

        assertThat(record.key()).isEqualTo("order-123");
        assertThat(record.value()).contains("CREATED");
        assertThat(record.offset()).isEqualTo(0L);
    }

    private Consumer<String, String> createTestConsumer() {
        Map<String, Object> props = KafkaTestUtils.consumerProps(
                "test-group", "true", embeddedKafka);
        props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
        DefaultKafkaConsumerFactory<String, String> factory =
                new DefaultKafkaConsumerFactory<>(props);
        return factory.createConsumer();
    }
}

@EmbeddedKafka startet einen In-Memory-Kafka-Broker nur zum Testen. KafkaTestUtils.getSingleRecord holt die erste Nachricht aus dem Topic, und wir assertieren, dass Key, Value und Offset wie erwartet sind — Offset 0, weil dies die erste Nachricht der einzigen Partition ist.

7. Häufige Fehler

  • Verwechslung zwischen Anzahl der Partitionen und Consumern: Zu viele Consumer für wenige Partitionen führt dazu, dass einige Consumer untätig sind. Die Anzahl der Partitionen bestimmt den maximalen Parallelisierungsgrad.
  • Key nicht setzen, wenn Reihenfolge erforderlich ist: Wenn du benötigst, dass Events derselben Entity (Bestellung, Benutzer…) in der richtigen Reihenfolge verarbeitet werden, musst du einen Key verwenden, nicht null lassen.
  • Partitionen nach der Produktivnahme erhöhen: Das Erhöhen der Partition-Anzahl eines Topics mit bestehenden Daten ändert die Key-Hash-Berechnung → Nachrichten mit demselben Key könnten in andere Partitionen gehen und die Reihenfolge-Garantie für alte Daten brechen. Bestimme die Anzahl der Partitionen von Anfang an sorgfältig.
  • Standard Auto-Commit (enable-auto-commit=true): Offsets werden in festen Intervallen (Standard 5s) committed, nicht wenn die Nachrichtverarbeitung abgeschlossen ist — dies führt leicht zu Nachrichtenverlust, wenn der Consumer während der Verarbeitung abstürzt.

Hinweis: Wenn deine Anwendung keine strikte Reihenfolge benötigt und Durchsatz kein Problem ist, kannst du Key/Partition-Optimierung überspringen — Kafka funktioniert mit der Standardkonfiguration einwandfrei. Optimiere nicht zu früh, wenn keine tatsächliche Anforderung besteht.

8. Fazit

Ein Topic ist ein Nachrichtenspeicher nach Thema, eine Partition ist die Einheit für Parallelisierung und Verteilung, ein Offset ist die Leseposition in jeder Partition. Das Verständnis dieser drei Konzepte hilft dir, richtig zu designen: Wähle einen angemessenen Key für die Reihenfolge, passe die Partitionsanzahl an die Anzahl der Consumer an, und kontrolliere die Offset-Commits, um Datenverlust bei Fehlern zu vermeiden. Das ist das notwendige Fundament, bevor du dich in fortgeschrittenere Themen wie Rebalancing, Exactly-Once-Semantik oder Kafka Streams vertiefst.

kafkaspring-bootmessagingjava