Kafka-Überwachung: Consumer Lag und wichtige Metriken
1. Überblick
Ein Kafka-System kann »gesund« aussehen: Broker laufen, Producer sendet Nachrichten. Aber der Consumer könnte Millionen von Nachrichten hinter sich herlaufen, ohne dass jemand es bemerkt. Die Metrik, die diesen Zustand widerspiegelt, ist Consumer Lag.
In diesem Artikel lernst du, wie Consumer Lag berechnet wird und wie man es auf drei Wegen misst: über CLI, AdminClient in Java und Micrometer in Spring Boot. Danach schauen wir uns die wichtigsten Broker-, Producer- und Consumer-Metriken an und wie man sinnvolle Alerts für sie einrichtet.
2. Was ist Consumer Lag?
Jede Partition in Kafka ist ein reines Append-Log. Es gibt zwei Offsets, die du beachten musst:
- Log-end offset (LEO): der Offset der nächsten Nachricht, die in die Partition geschrieben wird.
- Committed offset: der Offset, den die Consumer Group committed hat, also die Position, von der die Gruppe nach einem Neustart weiterlesen würde.
Der Lag einer Partition wird wie folgt berechnet:
lag = log-end offset - committed offset
Der Lag der gesamten Consumer Group ist die Summe der Lags über alle Partitionen, die die Group liest.
Ein Lag von 0 bedeutet, dass der Consumer aufgeholt hat. Leichte Schwankungen des Lag sind normal, da Nachrichten ständig ankommen. Ein stetig wachsender Lag ist das Zeichen für ein ernstes Problem. Das bedeutet, der Consumer verarbeitet langsamer als der Producer schreibt.
Hinweis: Bei Topics mit Transaktionen oder Compaction können Offsets »Lücken« aufweisen (Control Records, gelöschte Nachrichten). Lag wird nach Offsets berechnet und kann daher etwas größer sein als die Anzahl der Nachrichten, die tatsächlich noch zu lesen sind. Das ist kein Fehler.
3. Lag über CLI prüfen
Der schnellste Weg ist das Skript, das mit Kafka mitgeliefert wird:
kafka-consumer-groups.sh --bootstrap-server localhost:9092 \
--describe --group order-service
Die Ausgabe sieht so aus:
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
Partition 1 hängt 3430 Nachrichten hinterher, während die anderen zwei Partitionen auf dem Laufenden sind. Ein solches Ungleichgewicht hat normalerweise zwei Ursachen: Entweder gibt es einen »heißen« Key, der viele Nachrichten in eine Partition lenkt, oder der Consumer auf Host 10.0.0.6 hat ein Problem.
Die CLI ist sehr nützlich zur Fehlerbehebung. Allerdings kannst du diesen Befehl nicht jede Minute manuell ausführen. Der nächste Abschnitt zeigt, wie man die Messung automatisiert.
4. Lag mit AdminClient berechnen
Füge die Abhängigkeit kafka-clients hinzu (Spring Boot verwaltet die Version):
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
</dependency>
Zunächst isolieren wir die Berechnung in eine reine Funktion, um sie leicht testen zu können:
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;
}
}
Die Funktion berechnet Lag nur für Partitionen, die sowohl einen Committed Offset als auch einen End Offset haben. Math.max(0L, …) hat seinen Grund. Committed Offset und End Offset werden durch zwei separate Anfragen ermittelt. Daher kann der Committed Offset manchmal »neuer« sein als der gerade geholte End Offset. Das Ergebnis wäre negativ und ist sinnlos, also setzen wir es auf 0.
Als nächstes die Klasse, die AdminClient aufruft:
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);
}
}
Die Schritte sind:
listConsumerGroupOffsetsgibt die Committed Offsets der Group zurück. Der Wert kannnullsein, wenn die Partition noch nie committed wurde, deshalb filtern wir diese heraus.listOffsetsmitOffsetSpec.latest()holt den Log-End Offset jeder Partition.- Jeder Aufruf hat ein Timeout von 10 Sekunden. So wird der Thread nicht unendlich lang blockiert, wenn der Cluster Probleme hat.
Verwendung:
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. Test für LagCalculator
Mit JUnit 5 und AssertJ (bereits in spring-boot-starter-test enthalten):
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. Lag über Micrometer in Spring Boot exportieren
Füge Actuator und die Prometheus Registry hinzu:
<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. Verfügbare Client-seitige Metriken
Wenn MeterRegistry vorhanden ist, bindet Spring Boot automatisch MicrometerConsumerListener in die ConsumerFactory ein. Alle internen Kafka-Consumer-Metriken erscheinen dann unter /actuator/prometheus, darunter:
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 ist der größte Lag, den dieser spezifische Consumer beobachtet hat. Kafka berechnet ihn aus der Fetch-Position, nicht aus dem Committed Offset.
5.2. Gauge für extern gemessenen Lag
Die client-seitige Metrik hat einen großen Schwachpunkt: Wenn der Consumer abstürzt, verschwindet auch die Metrik. Genau dann, wenn der Lag am schnellsten wächst, sind deine Daten weg. Daher solltest du Lag zusätzlich von einem unabhängigen Prozess messen, indem du AdminClient verwendest:
@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;
}
}
Denk daran, Scheduling mit @EnableScheduling auf einer @Configuration-Klasse zu aktivieren. Alle 30 Sekunden wird das Gauge kafka_consumer_group_lag aktualisiert. Dieser Wert basiert auf Committed Offsets und bleibt korrekt, selbst wenn alle Consumer der Group gestoppt sind.
In Production verwenden viele Teams spezialisierte Tools wie kafka-exporter oder Burrow statt des Code-Snippets oben. Das Prinzip dahinter ist identisch: Committed Offset mit Log-End Offset vergleichen.
Hinweis: Platziere dieses Gauge nicht im Consumer-Service selbst, wenn dieser mehrere Instanzen hat. Jede Instanz würde denselben Lag-Wert exportieren, und Summen im Dashboard wären falsch. Führe es stattdessen in einem separaten Monitoring-Service aus oder nur auf einer einzigen Instanz.
6. Zu überwachende Metriken
Lag sagt dir, dass es ein Problem gibt. Diese Metriken helfen dir herauszufinden, wo das Problem liegt.
6.1. Broker
| Metrik (JMX) | Bedeutung | Gewünschter Wert |
|---|---|---|
UnderReplicatedPartitions |
Partitionen mit nicht synchronisierten Replicas | 0 |
OfflinePartitionsCount |
Partitionen ohne Leader | 0 |
ActiveControllerCount |
Anzahl aktiver Controller (gesamt Cluster) | genau 1 |
IsrShrinksPerSec / IsrExpandsPerSec |
ISR wächst oder schrumpft | nahe 0, keine kontinuierlichen Schwankungen |
RequestHandlerAvgIdlePercent |
Auslastung der Request-Handler-Threads | > 0.3 |
NetworkProcessorAvgIdlePercent |
Auslastung der Netzwerk-Threads | > 0.3 |
OfflinePartitionsCount > 0 ist ein kritischer Fehler: diese Partitionen können nicht gelesen oder geschrieben werden. Anhaltend hohe UnderReplicatedPartitions deuten meist auf überladene Broker, langsame Festplatten oder Netzwerkprobleme hin.
6.2. Producer
record-error-rate: Quote der Sendefehler nach Retry-Ablauf. Jeder Wert > 0 verdient Aufmerksamkeit.request-latency-avg: Latenz der Anfragen zum Broker. Wenn das steigt, sinkt der Durchsatz.record-retry-rate: Viele Retries bedeuten Broker- oder Netzwerkprobleme.buffer-available-bytes: Wenn der Buffer aufgebraucht ist, blockiertsend().
6.3. Consumer
records-lag-max: bereits oben erwähnt.records-consumed-rate: Verarbeitungsgeschwindigkeit. Vergleiche mit der Schreibgeschwindigkeit des Producers zum Topic.rebalance-rate-per-hourundlast-rebalance-seconds-ago: Häufiges Rebalancing führt zu Pausen. Das ist eine häufige Ursache für steigende Lags.commit-latency-avg: Langsames Committing reduziert Durchsatz, besonders bei synchronem Commit.last-poll-seconds-ago: Wenn diesmax.poll.interval.msüberschreitet, wird der Consumer aus der Gruppe geworfen.
7. Alerts sinnvoll konfigurieren
Der häufigste Fehler ist ein absoluter Schwellwert wie »Lag > 1000 warnen«. Bei einem Topic mit 50.000 Nachrichten/Sekunde ist 1000 bedeutungslos. Bei einem Topic mit wenigen Nachrichten pro Minute könnte 1000 bereits Stunden Verzögerung bedeuten.
Zwei bessere Strategien gibt es. Die erste ist ein Alert, wenn Lag kontinuierlich steigt:
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: "Lag von {{ $labels.group }} auf {{ $labels.topic }} wächst 15 Minuten kontinuierlich"
Diese Regel feuert nur ab, wenn beide Bedingungen erfüllt sind: Lag hat einen wachsenden Trend (deriv > 0) UND hat bereits ein Mindestmaß überschritten. Die Bedingung muss 15 Minuten anhalten. So vermeidest du Fehlalarme nach Deployments durch kurzzeitige Spitzen.
Die zweite Strategie ist, Lag in Zeit umzurechnen. Teile Lag durch die Verarbeitungsgeschwindigkeit, um zu schätzen, wie viele Sekunden der Consumer hinterherhinkt. Diese Zahl ist direkt an SLAs gekoppelt. Zum Beispiel: »Bestellungen müssen innerhalb von 2 Minuten bearbeitet werden« ist verständlicher als »Lag unter 5000 halten«.
Zusätzlich solltest du einen Alert für fehlende Metriken haben, etwa mit absent(kafka_consumer_group_lag). Keine Daten heißt nicht automatisch, dass alles okay ist.
8. Häufige Fehler
- Nur auf
records-lag-maxverlassen. Wenn der Consumer stirbt, verschwindet diese Metrik. Nutze immer eine externe Quelle zur Lag-Messung. - Lag einer inaktiven Consumer Group lesen. Die CLI zeigt noch den Committed Offset, aber die Spalte
CONSUMER-IDist leer. Wachsender Lag bedeutet hier, dass niemand verarbeitet. - Consumer-Instanzen über die Partition-Anzahl hinzufügen. Überschüssige Consumer sitzen herum, Lag sinkt nicht. Um Parallelität zu erhöhen, musst du Partitionen hinzufügen.
- Rebalancing ignorieren. Wenn die Verarbeitung eines Batches länger dauert als
max.poll.interval.ms, wird der Consumer rausgeworfen. Die Gruppe rebalanciert, und das Spiel beginnt von vorne. Der Lag wächst sägezahnförmig. Reduzieremax.poll.recordsoder lagere schwere Verarbeitung auf andere Threads aus. AdminClientzu häufig aufrufen. Das Abfragen von Offsets jede Sekunde für hunderte Groups erzeugt unnötige Last auf dem Broker. Ein Zyklus alle 15–60 Sekunden reicht für die meisten Systeme.
9. Fazit
Consumer Lag ist die direkteste Metrik dafür, ob dein Kafka-System mit den Daten Schritt hält. Lag wird als Log-End Offset minus Committed Offset berechnet. Du kannst es über kafka-consumer-groups.sh sehen, mit AdminClient berechnen oder über Micrometer zu Prometheus exportieren.
Für effektive Überwachung kombiniere client-seitige Metriken mit einer unabhängigen Messung. Neben Lag solltest du auch Broker-Metriken wie UnderReplicatedPartitions und OfflinePartitionsCount im Auge behalten sowie die Rebalancing-Häufigkeit. Zuletzt: Alerts basierend auf Trends und Verzögerungszeit setzen, nicht auf einem fixen Lag-Wert. So vermeidest du Fehlalarme, ohne echte Probleme zu übersehen.