Neblokující opakování v Spring Kafka: Diagnostika zpoždění a řešení
Při implementaci asynchronního opakování pro Kafka s využitím @RetryableTopic v Spring Boot 3.2 a Spring Kafka 3.1 se při zátěži objevují zpoždění na retry-tópicích. Hlavní tópic je zpracováván rovnoměrně, ale retry-0 hromadí zprávy po dobu 10–12 minut, poté se částečně vyprázdní a znovu stagnuje. To vede k trhavému zpracování.
Metriky ukazují vzácné špičky fetch-požadavků namísto rovnoměrného pollování. Consumer periodicky dotazuje broker pro heartbeat, ale skutečné čtení probíhá sporadicky s vysokou latencí.
Klíčové metriky pro analýzu
Pro diagnostiku shromážděte následující metriky Spring Kafka:
kafka_consumer_time_between_poll_avg/max— intervaly mezi pollováním.kafka_consumer_last_poll_seconds_ago— čas od posledního pollování.kafka_consumer_poll_idle_ratio_avg— podíl nečinného pollování.kafka_consumer_fetch_manager_fetch_rate/fetch_latency_avg— rychlost a zpoždění fetchování.spring_kafka_listener_seconds_count/sum— čas zpracování listeneru.
Grafy fetch-rate a volání listenerů potvrzují: zpracování se aktivuje trhavě, poll idle je vysoký.
Diagnostika pomocí threaddump
Threaddump odhalil ~180 vláken ve stavu TIMED_WAITING na ScheduledThreadPoolExecutor$DelayedWorkQueue.take. To ukazuje na přetížení scheduleru, který se používá pro odložené obnovení partií.
Mechanismus pozastavení v RetryableTopic
@RetryableTopic používá ContainerPartitionPausingBackOffManager: při prvním zpracování retry-zprávy zkontroluje backoff, pozastaví partii na now() + delay a naplánuje obnovení přes ListenerContainerPauseService.
Kód pozastavení:
private final TaskScheduler scheduler;
public void pausePartition(MessageListenerContainer messageListenerContainer,
TopicPartition partition,
Duration pauseDuration) {
Instant resumeAt = Instant.now().plusMillis(pauseDuration.toMillis());
messageListenerContainer.pausePartition(partition);
this.scheduler.schedule(() -> {
messageListenerContainer.resumePartition(partition);
}, resumeAt);
}
Při vysoké zátěži se fronta úloh scheduleru přeplní. Ve výchozím nastavení je spring.task.scheduling.pool.size=1, což vytváří úzké hrdlo: úlohy na obnovení se hromadí, fetchování se nespouští včas.
Řešení: Zvětšení fondu scheduleru
Nastavte spring.task.scheduling.pool.size=10 v application.properties.
Výsledky:
- Zpoždění retry-0 se stabilizovalo na přijatelné úrovni (neklesá na 0 kvůli backoffu).
- Poll-intervaly se zkrátily, fetch-rate vzrostl.
- Volání listenerů se stala rovnoměrnými.
Graf zpoždění po opravě: drží se vodorovně bez růstu. Fetch latence se snížila, pollování je častější.
Co je důležité
- Omezený scheduler: výchozí fond s 1 vláknem nezvládá hromadná pozastavení/obnovení při zátěži na retry.
- Metriky primárně: sledujte fetch-rate, poll-idle, listener-time před threaddumpem.
- Transakce sekundárně:
ack-mode=record,max.poll.records=1neovlivňují zpoždění. - Concurrency OK: Spring Kafka správně škáluje podle partií, i s násobením tópiců.
- Alternativa: pro highload zvažte vlastní retry s kontrolou časového razítka ve smyčce místo pozastavení.
Doporučení pro monitoring
Přidejte dashboardy:
- Commit/rollback rate pro exactly-once.
- Velikost fronty scheduleru (vlastní metrika).
- Události pozastavení/obnovení partií.
- Rozdíl mezi backoff časovým razítkem a aktuálním časem.
Testujte pod zátěží před nasazením do produkce. Zdrojové kódy Spring Kafka poskytují úplný obraz mechaniky.
— Editorial Team
Zatím žádné komentáře.