Surveillance de Kafka : consumer lag et métriques à surveiller
1. Aperçu
Un système Kafka peut sembler « en bonne santé » : les brokers tournent, les producteurs envoient des messages. Mais les consommateurs peuvent être en retard de plusieurs millions de messages sans que personne ne le sache. La métrique qui reflète cet état s’appelle consumer lag.
Dans cet article, vous verrez comment le consumer lag est calculé et comment le mesurer de trois façons : CLI, AdminClient en Java, et Micrometer dans Spring Boot. Ensuite, nous examinerons les métriques broker, producer et consumer les plus importantes et comment configurer des alertes pour elles.
2. Qu’est-ce que le consumer lag ?
Chaque partition dans Kafka est un log en ajout seul. Il y a deux offsets auxquels vous devez faire attention :
- Log-end offset (LEO) : l’offset du message suivant qui sera écrit dans la partition.
- Committed offset : l’offset que le groupe de consommateurs a validé, c’est-à-dire la position à partir de laquelle le groupe continuera à lire en cas de redémarrage.
Le lag d’une partition se calcule ainsi :
lag = log-end offset - committed offset
Le lag du groupe de consommateurs est la somme des lags sur toutes les partitions lues par le groupe.
Un lag de 0 signifie que le consommateur est à jour. Des fluctuations légères du lag sont normales, car les messages arrivent continuellement. Un lag qui augmente régulièrement au fil du temps est un signe préoccupant. Dans ce cas, le consommateur traite plus lentement que le producteur n’écrit.
Remarque : Avec les topics utilisant des transactions ou la compaction, les offsets peuvent avoir des « vides » (enregistrements de contrôle, messages supprimés). Le lag calculé en fonction des offsets peut donc être légèrement supérieur au nombre de messages réellement à lire. Ce n’est pas une erreur.
3. Vérifier le lag avec CLI
Le plus rapide est d’utiliser le script fourni avec Kafka :
kafka-consumer-groups.sh --bootstrap-server localhost:9092 \
--describe --group order-service
Voici le résultat :
GROUP TOPIC PARTITION CURRENT-OFFSET LOG-END-OFFSET LAG CONSUMER-ID HOST
order-service orders 0 15230 15232 2 consumer-1-a1b2 /10.0.0.5
order-service orders 1 14980 18410 3430 consumer-1-c3d4 /10.0.0.6
order-service orders 2 15102 15102 0 consumer-1-e5f6 /10.0.0.7
La partition 1 a 3430 messages de retard, tandis que les deux autres partitions sont presque à jour. Un tel décalage de lag est généralement dû à deux raisons. D’abord, il y a peut-être une clé « chaude » concentrée dans une partition. Ensuite, le consommateur sur l’hôte 10.0.0.6 peut avoir un problème.
CLI est très utile pour enquêter sur les problèmes. Cependant, vous ne pouvez pas exécuter cette commande chaque minute manuellement. La section suivante automatisera la mesure du lag.
4. Calculer le lag avec AdminClient
Ajoutez la dépendance kafka-clients (Spring Boot gère la version) :
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
</dependency>
Commencez par extraire le calcul dans une fonction pure pour faciliter les tests :
public final class LagCalculator {
private LagCalculator() {}
public static Map<TopicPartition, Long> compute(Map<TopicPartition, Long> committed,
Map<TopicPartition, Long> endOffsets) {
Map<TopicPartition, Long> lag = new HashMap<>();
committed.forEach((tp, committedOffset) -> {
Long end = endOffsets.get(tp);
if (end != null) {
lag.put(tp, Math.max(0L, end - committedOffset));
}
});
return lag;
}
}
Cette fonction ne calcule le lag que pour les partitions ayant à la fois un committed offset et un end offset. Math.max(0L, …) a une raison d’être. Le committed offset et le end offset sont obtenus par deux requêtes différentes, donc parfois le committed offset peut être « plus récent » que le end offset vient d’être récupéré. Un résultat négatif serait sans sens, nous le ramenons donc à 0.
Vient ensuite la classe qui appelle AdminClient :
public class ConsumerLagChecker {
private final Admin admin;
public ConsumerLagChecker(Admin admin) {
this.admin = admin;
}
public Map<TopicPartition, Long> lagOf(String groupId) throws Exception {
Map<TopicPartition, Long> committed = admin.listConsumerGroupOffsets(groupId)
.partitionsToOffsetAndMetadata()
.get(10, TimeUnit.SECONDS)
.entrySet().stream()
.filter(e -> e.getValue() != null)
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().offset()));
Map<TopicPartition, OffsetSpec> request = committed.keySet().stream()
.collect(Collectors.toMap(tp -> tp, tp -> OffsetSpec.latest()));
Map<TopicPartition, Long> endOffsets = admin.listOffsets(request)
.all()
.get(10, TimeUnit.SECONDS)
.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().offset()));
return LagCalculator.compute(committed, endOffsets);
}
}
Voici comment cela fonctionne étape par étape :
listConsumerGroupOffsetsretourne le committed offset du groupe. La valeur peut êtrenullsi la partition n’a jamais été commitée, nous la filtrons donc.listOffsetsavecOffsetSpec.latest()récupère le log-end offset de chaque partition.- Chaque appel a un délai d’attente de 10 secondes. Cela empêche le thread de se bloquer indéfiniment en cas de problème du cluster.
Utilisation :
try (Admin admin = Admin.create(Map.of(
AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092"))) {
new ConsumerLagChecker(admin).lagOf("order-service")
.forEach((tp, lag) -> System.out.println(tp + " -> " + lag));
}
orders-0 -> 2
orders-1 -> 3430
orders-2 -> 0
4.1. Tests pour LagCalculator
Utilisez JUnit 5 et AssertJ (inclus dans spring-boot-starter-test) :
class LagCalculatorTest {
private final TopicPartition p0 = new TopicPartition("orders", 0);
private final TopicPartition p1 = new TopicPartition("orders", 1);
@Test
void shouldComputeLagPerPartition() {
Map<TopicPartition, Long> lag = LagCalculator.compute(
Map.of(p0, 100L, p1, 50L),
Map.of(p0, 120L, p1, 50L));
assertThat(lag).containsEntry(p0, 20L).containsEntry(p1, 0L);
}
@Test
void shouldClampNegativeLagToZero() {
Map<TopicPartition, Long> lag = LagCalculator.compute(
Map.of(p0, 130L),
Map.of(p0, 120L));
assertThat(lag).containsEntry(p0, 0L);
}
@Test
void shouldSkipPartitionWithoutEndOffset() {
Map<TopicPartition, Long> lag = LagCalculator.compute(
Map.of(p0, 10L), Map.of());
assertThat(lag).isEmpty();
}
}
5. Envoyer le lag à Micrometer dans Spring Boot
Ajoutez Actuator et le registre Prometheus :
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
management.endpoints.web.exposure.include=health,prometheus
5.1. Les métriques côté client fournies
Quand MeterRegistry est disponible, Spring Boot attache automatiquement MicrometerConsumerListener à ConsumerFactory. Toutes les métriques internes du consommateur Kafka apparaissent sur /actuator/prometheus, notamment :
kafka_consumer_fetch_manager_records_lag_max{client_id="consumer-order-service-1",...} 3430.0
kafka_consumer_fetch_manager_records_lag{topic="orders",partition="1",...} 3430.0
records-lag-max est le lag maximal que ce consommateur a observé. Le client Kafka le calcule lui-même à partir de la position de récupération, pas du committed offset.
5.2. Jauge de lag mesurée de l’extérieur
Les métriques côté client ont un grand défaut. Quand le consommateur meurt, la métrique disparaît aussi. C’est juste au moment où le lag augmente le plus vite que vous n’avez plus de données. Pour cette raison, vous devriez mesurer le lag en plus à partir d’un processus indépendant en utilisant AdminClient :
@Component
public class ConsumerLagMetrics {
private final ConsumerLagChecker checker;
private final MeterRegistry registry;
private final Map<TopicPartition, AtomicLong> gauges = new ConcurrentHashMap<>();
public ConsumerLagMetrics(KafkaAdmin kafkaAdmin, MeterRegistry registry) {
this.checker = new ConsumerLagChecker(Admin.create(kafkaAdmin.getConfigurationProperties()));
this.registry = registry;
}
@Scheduled(fixedDelay = 30_000)
public void refresh() throws Exception {
checker.lagOf("order-service").forEach((tp, lag) ->
gauges.computeIfAbsent(tp, this::register).set(lag));
}
private AtomicLong register(TopicPartition tp) {
AtomicLong holder = new AtomicLong();
Gauge.builder("kafka.consumer.group.lag", holder, AtomicLong::get)
.tag("group", "order-service")
.tag("topic", tp.topic())
.tag("partition", String.valueOf(tp.partition()))
.register(registry);
return holder;
}
}
N’oubliez pas d’activer la planification avec @EnableScheduling sur une classe @Configuration. Toutes les 30 secondes, la jauge kafka_consumer_group_lag est mise à jour. Cette valeur est basée sur le committed offset, elle reste donc correcte même si tous les consommateurs du groupe se sont arrêtés.
En production, beaucoup d’équipes utilisent des outils dédiés comme kafka-exporter ou Burrow à la place du code ci-dessus. Leur principe reste identique : comparer le committed offset avec le log-end offset.
Remarque : Ne placez pas cette jauge dans le service consommateur lui-même s’il y a plusieurs instances. Chaque instance exporterait la même valeur de lag, et le tableau de bord agrégé serait incorrect. Exécutez-le dans un service de surveillance séparé, ou seulement sur une instance.
6. Les métriques à surveiller
Le lag indique qu’il y a un problème. Les métriques ci-dessous vous aident à trouver le problème où.
6.1. Broker
| Métrique (JMX) | Signification | Valeur souhaitée |
|---|---|---|
UnderReplicatedPartitions |
Partitions avec réplicas non synchronisées | 0 |
OfflinePartitionsCount |
Partitions sans leader | 0 |
ActiveControllerCount |
Nombre de contrôleurs actifs (cluster entier) | exactement 1 |
IsrShrinksPerSec / IsrExpandsPerSec |
Rétrécissement/expansion ISR | près de 0, pas de fluctuations constantes |
RequestHandlerAvgIdlePercent |
Pourcentage d’inactivité des threads de traitement de requête | > 0.3 |
NetworkProcessorAvgIdlePercent |
Pourcentage d’inactivité des threads réseau | > 0.3 |
OfflinePartitionsCount > 0 est une panne grave : cette partition ne peut pas être lue ni écrite. Un UnderReplicatedPartitions prolongé indique généralement une surcharge du broker, un disque lent ou un problème réseau.
6.2. Producer
record-error-rate: taux d’envois échoués après épuisement des tentatives. Toute valeur différente de 0 mérite investigation.request-latency-avg: latence des requêtes vers le broker. S’il augmente, vous verrez le débit diminuer.record-retry-rate: beaucoup de tentatives signifient que le broker ou le réseau est instable.buffer-available-bytes: quand le buffer s’épuise,send()se bloque.
6.3. Consumer
records-lag-max: déjà discuté ci-dessus.records-consumed-rate: taux de consommation. Comparez-le avec le taux auquel le producteur écrit dans le topic.rebalance-rate-per-houretlast-rebalance-seconds-ago: les rééquilibrages constants font « geler » le consommateur. C’est un coupable courant de l’augmentation du lag.commit-latency-avg: commit lent réduit le débit, surtout en commit synchrone.last-poll-seconds-ago: si cela dépassemax.poll.interval.ms, le consommateur est expulsé du groupe.
7. Définir des alertes pertinentes
L’erreur la plus courante est de définir un seuil absolu comme « lag > 1000 alors alerter ». Avec un topic à 50.000 messages/seconde, 1000 n’a aucun sens. Avec un topic de quelques messages/minute, 1000 pourrait déjà signifier des heures de retard.
Il y a deux meilleures approches. La première est d’alerter quand le lag augmente continuellement :
groups:
- name: kafka-consumer
rules:
- alert: KafkaConsumerLagGrowing
expr: sum by (group, topic) (deriv(kafka_consumer_group_lag[10m])) > 0
and sum by (group, topic) (kafka_consumer_group_lag) > 500
for: 15m
labels:
severity: warning
annotations:
summary: "Le lag de {{ $labels.group }} sur {{ $labels.topic }} augmente continuellement depuis 15 minutes"
Cette règle ne s’active que lorsque les deux conditions sont vraies : le lag tend à augmenter (deriv > 0) et a dépassé un seuil minimum. La condition doit persister pendant 15 minutes. De cette façon, vous ignorez les pics courts après un déploiement.
La deuxième approche consiste à convertir le lag en temps. Divisez le lag par le taux de consommation pour estimer le retard du consommateur en secondes. Ce nombre est directement lié au SLA. Par exemple, « les commandes doivent être traitées en 2 minutes » est bien plus compréhensible que « lag sous 5000 ».
De plus, vous devez avoir une alerte distincte pour le cas où la métrique disparaît, par exemple en utilisant absent(kafka_consumer_group_lag). L’absence de données ne signifie pas que tout va bien.
8. Erreurs courantes
- Compter uniquement sur
records-lag-max. Quand le consommateur meurt, cette métrique disparaît. Ayez toujours une source de mesure du lag externe. - Lire le lag d’un groupe sans consommateur actif. CLI affiche toujours le committed offset, mais la colonne
CONSUMER-IDest vide. Une augmentation du lag ici signifie que personne n’effectue le traitement. - Augmenter le nombre de consommateurs au-delà du nombre de partitions. Les consommateurs excédentaires resteront inactifs, le lag ne diminuera pas. Pour augmenter le parallélisme, vous devez augmenter le nombre de partitions.
- Ignorer le rééquilibrage. Le traitement d’un batch trop long dépasse
max.poll.interval.ms. Le consommateur est retiré, le groupe se rééquilibre, puis se répète. Le lag augmente en dents de scie. Réduisezmax.poll.recordsou déplacez le traitement lourd vers un autre thread. - Appeler
AdminClienttrop souvent. Interroger les offsets toutes les secondes pour des centaines de groupes crée une charge inutile sur le broker. Un cycle de 15-60 secondes est suffisant pour la plupart des systèmes.
9. Conclusion
Le consumer lag est la métrique la plus directe pour voir si le système Kafka suit les données. Le lag se calcule en soustrayant le committed offset du log-end offset. Vous pouvez visualiser le lag avec kafka-consumer-groups.sh, le calculer avec AdminClient ou l’exporter vers Prometheus via Micrometer.
Pour une surveillance efficace, combinez les métriques côté client avec une source de mesure indépendante. En parallèle du lag, suivez les métriques du broker comme UnderReplicatedPartitions et OfflinePartitionsCount, ainsi que la fréquence de rééquilibrage côté consommateur. Enfin, alertez selon la tendance et le temps de latence plutôt que selon un nombre de lag fixe. De cette façon, vous éviterez les faux positifs tout en ne manquant pas les vrais problèmes.