Kafka basique : Topic, Partition et Offset

1. Vue d’ensemble

Kafka est un système de message broker distribué, utilisé pour transmettre des données entre les services avec un débit élevé. Avant d’écrire votre premier producer/consumer, vous devez maîtriser trois concepts clés : topic, partition, offset. Cet article explique chaque concept, accompagné d’exemples Spring Boot concrets. À la fin, vous comprendrez comment les données sont organisées et lues dans Kafka, en évitant les erreurs courantes pour les débutants.

2. Qu’est-ce qu’un Topic

Un topic est comme une « table » ou un « canal » qui contient des messages. Les producers envoient des messages dans un topic, et les consumers les lisent depuis ce topic. Un topic peut avoir plusieurs producers et plusieurs consumers simultanément.

Par exemple, dans un système de e-commerce, vous pourriez avoir les topics :

orders.created
orders.cancelled
payments.processed

Chaque topic contient des messages relatifs à un type d’événement spécifique. Kafka n’impose pas de schéma pour les messages — c’est à vous de décider du format (JSON, Avro, Protobuf…).

Créez un topic via CLI pour mieux visualiser :

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

Cette commande crée un topic orders.created avec 3 partitions. C’est justement le concept suivant.

3. Partition — l’unité de parallélisation

3.1. Pourquoi les partitions

Un topic est divisé en plusieurs partitions. Chaque partition est un fichier log indépendant où les messages sont ajoutés à la fin, jamais modifiés ou supprimés en cours de route. Les partitions permettent à Kafka :

  • De distribuer les données sur plusieurs brokers.
  • De laisser plusieurs consumers lire en parallèle (chacun une partition).
Topic: orders.created (3 partitions)

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

3.2. La clé détermine la partition

Lorsque vous envoyez un message, Kafka utilise la clé pour calculer la partition (via un hash), ce qui garantit que les messages avec la même clé vont toujours dans la même partition — conservant l’ordre pour les messages associés.

// Par exemple, envoyer un événement avec l'orderId comme clé
ProducerRecord<String, String> record =
        new ProducerRecord<>("orders.created", "order-123", payloadJson);
kafkaTemplate.send(record);

Cette ligne crée un ProducerRecord avec la clé "order-123". Tous les événements de cette commande iront toujours dans la même partition, de sorte que le consumer puisse les lire dans l’ordre d’envoi.

Note : Si vous ne passez pas de clé (clé = null), Kafka distribue les messages en round-robin (ou sticky partitioner depuis Kafka 2.4+), sans garantir l’ordre entre les messages.

3.3. L’ordre n’est garanti que dans une partition

C’est le point source de confusion le plus courant : Kafka garantit l’ordre uniquement dans une partition, pas dans tout le topic. Si vous avez besoin d’un ordre absolu pour un groupe de messages (par exemple, tous les événements d’un orderId), utilisez la même clé pour ce groupe.

4. Offset — la position de lecture dans une partition

Chaque message d’une partition a un numéro de séquence croissant appelé offset, commençant à 0.

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

L’offset est un nombre unique dans le cadre d’une partition, pas du topic entier. Le consumer l’utilise pour savoir « où j’en suis ».

4.1. Consumer group et commit d’offset

Kafka suit l’offset lu via un consumer group. Chaque consumer group a son propre offset pour chaque partition, stocké dans un topic interne __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 : identifie le consumer group. Plusieurs instances avec le même group-id se partageront les partitions du topic.
  • auto-offset-reset=earliest : si aucun offset n’est enregistré (nouveau consumer group), lire depuis le début du topic.
  • enable-auto-commit=false : désactiver l’auto-commit pour contrôler manuellement quand faire le commit de l’offset (éviter de perdre les messages en cas d’erreur en cours de traitement).

4.2. Plusieurs consumers dans le même group

Kafka distribue les partitions aux consumers d’un même group selon le principe : une partition ne peut être lue que par un consumer du group à la fois.

Topic orders.created a 3 partitions
Consumer group "order-service" a 2 instances:

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

Si vous lancez 4 consumers pour un topic à 3 partitions, le 4e consumer sera inactif, sans aucune partition. Le maximum de consumers utiles = nombre de partitions.

5. Configuration de Kafka dans Spring Boot

5.1. Dépendance

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

5.2. Producer pour envoyer des messages

@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) envoie un message dans le topic orders.created, en utilisant orderId comme clé. Spring Boot crée automatiquement le bean KafkaTemplate selon la configuration spring.kafka.bootstrap-servers.

5.3. Consumer pour lire les messages et commiter manuellement l’offset

@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());

        // Traiter la logique métier ici
        ack.acknowledge(); // commiter l'offset après traitement réussi
    }
}
  • record.partition() et record.offset() indiquent la partition et la position du message.
  • ack.acknowledge() commite l’offset seulement après traitement — si le service plante en cours, le message sera relu au redémarrage suivant (grâce à enable-auto-commit=false couplé à AckMode.MANUAL).

Vous avez besoin de la configuration supplémentaire pour que Acknowledgment fonctionne :

spring.kafka.listener.ack-mode=manual

Sortie lors d’un test (envoi de 3 messages avec des clés différentes) :

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"}

Chaque clé va dans une partition différente à cause d’un hash différent — exactement comme expliqué dans la section 3.2.

6. Test avec JUnit 5

Utilisez spring-kafka-test pour lancer un Kafka embarqué dans vos tests unitaires, sans avoir besoin d’un broker réel.

<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 lance un broker Kafka en mémoire juste pour les tests. KafkaTestUtils.getSingleRecord récupère le premier message du topic, et vous vérifiez que la clé, la valeur et l’offset sont comme prévu — offset 0 car c’est le premier message de l’unique partition.

7. Erreurs courantes

  • Confusion entre nombre de partitions et nombre de consumers : créer trop de consumers par rapport aux partitions fait que certains consumers restent inactifs. Le nombre de partitions détermine le degré maximal de parallélisation.
  • Ne pas définir de clé quand l’ordre est important : si vous avez besoin que les événements d’une même entité (commande, utilisateur…) soient traités dans le bon ordre, vous devez utiliser une clé, ne pas la laisser null.
  • Augmenter les partitions après avoir lancé en production : augmenter le nombre de partitions d’un topic existant modifie la façon dont les clés se distribuent (hash) → les messages avec la même clé peuvent se retrouver dans une partition différente, cassant la garantie d’ordre pour les anciennes données. Fixez le nombre de partitions correctement dès le départ.
  • Auto-commit activé par défaut (enable-auto-commit=true) : l’offset est committé selon un cycle fixe (5s par défaut), indépendamment du fait que le message soit traité ou non — facile de perdre des messages si le consumer plante en cours de traitement.

Note : Si votre application n’a pas besoin d’un ordre strict et que le débit n’est pas un problème, vous pouvez ignorer l’optimisation des clés/partitions — Kafka fonctionne correctement avec la configuration par défaut. N’optimisez pas trop tôt sans besoin réel.

8. Conclusion

Un topic est le lieu qui contient les messages par sujet, une partition est l’unité qui divise pour paralléliser et distribuer, l’offset est la position de lecture dans chaque partition. Bien comprendre ces trois concepts vous aide à concevoir correctement : choisir une clé judicieuse pour maintenir l’ordre, sélectionner un nombre de partitions approprié au nombre de consumers, et contrôler le commit d’offset pour éviter de perdre des données en cas d’erreur. C’est la base obligatoire avant de plonger dans des sujets plus avancés comme le rebalancing, la sémantique exactly-once ou Kafka Streams.

kafkaspring-bootmessagingjava