Reintentos No Bloqueantes en Spring Kafka: Diagnóstico y Solución de Retrasos
Al implementar reintentos asíncronos para Kafka usando @RetryableTopic en Spring Boot 3.2 y Spring Kafka 3.1, se acumulan retrasos en los temas de reintento bajo carga. El tema principal procesa mensajes de manera uniforme, pero retry-0 acumula mensajes durante 10–12 minutos, luego se limpia parcialmente y se vuelve a estancar. Esto provoca un procesamiento desigual.
Las métricas muestran picos esporádicos de solicitudes de fetch en lugar de sondeos consistentes. El consumidor sondea periódicamente al broker para latidos, pero la lectura real ocurre de forma esporádica con alta latencia.
Métricas Clave para el Análisis
Para el diagnóstico, recopile estas métricas de Spring Kafka:
kafka_consumer_time_between_poll_avg/max— intervalos entre sondeos.kafka_consumer_last_poll_seconds_ago— tiempo desde el último sondeo.kafka_consumer_poll_idle_ratio_avg— proporción de sondeos inactivos.kafka_consumer_fetch_manager_fetch_rate/fetch_latency_avg— velocidad y latencia del fetch.spring_kafka_listener_seconds_count/sum— tiempo de procesamiento del listener.
Los gráficos de fetch-rate e invocaciones del listener confirman: el procesamiento se activa en ráfagas, el sondeo inactivo es alto.
Diagnóstico mediante Volcado de Hilos
El volcado de hilos reveló ~180 hilos en TIMED_WAITING en ScheduledThreadPoolExecutor$DelayedWorkQueue.take. Esto indica sobrecarga del programador, utilizado para reanudar particiones retrasadas.
Mecanismo de Pausa en RetryableTopic
@RetryableTopic usa ContainerPartitionPausingBackOffManager: al procesar el primer mensaje de reintento, verifica el backoff, pausa la partición para now() + delay, y programa la reanudación mediante ListenerContainerPauseService.
Código de pausa:
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);
}
Bajo alta carga, la cola de tareas del programador se desborda. Por defecto, spring.task.scheduling.pool.size=1, creando un cuello de botella: las tareas de reanudación se acumulan, el fetch no comienza a tiempo.
Solución: Aumentar el Pool del Programador
Configure spring.task.scheduling.pool.size=10 en application.properties.
Resultados:
- El retraso en Retry-0 se estabilizó en un nivel aceptable (no cae a 0 debido al backoff).
- Los intervalos de sondeo se acortaron, el fetch-rate aumentó.
- Las invocaciones del listener se volvieron consistentes.
Gráfico de retraso después de la corrección: permanece plano sin crecimiento. La latencia del fetch disminuyó, los sondeos son más frecuentes.
Conclusiones Clave
- Programador limitado: el pool predeterminado de 1 hilo no puede manejar pausas/reanudaciones masivas bajo carga de reintentos.
- Métricas primero: monitoree fetch-rate, poll-idle, listener-time antes del volcado de hilos.
- Transacciones secundarias:
ack-mode=record,max.poll.records=1no afectan el retraso. - Concurrencia correcta: Spring Kafka escala correctamente por particiones, incluso con temas multiplicados.
- Alternativa: para alta carga, considere reintentos personalizados con verificaciones de timestamp en un bucle en lugar de pausas.
Recomendaciones de Monitoreo
Agregue paneles para:
- Tasa de commit/rollback para exactamente una vez.
- Tamaño de la cola del programador (métrica personalizada).
- Eventos de pausa/reanudación de particiones.
- Delta entre timestamp de backoff y tiempo actual.
Pruebe bajo carga antes de producción. El código fuente de Spring Kafka proporciona información completa sobre la mecánica.
— Editorial Team
Aún no hay comentarios.