Un rebalanceo de grupo de consumidores en Kafka se supone que debería ser invisible. Redistribuye particiones cuando un consumidor se une, falla, o un broker cambia de coordinador, y en un clúster saludable eso ocurre en milisegundos. En un pipeline de liquidación financiera, una ruta de ingesta de CDR de telecomunicaciones, o un flujo de sensores industriales, rara vez se mantiene invisible. Con la configuración predeterminada, el ajuste de grupos de consumidores de Kafka se trata como algo secundario hasta que aparece un bloqueo de varios segundos en un dashboard de latencia que una mesa de trading o un operador de SCADA está mirando en ese momento.

El mecanismo detrás de ese bloqueo está bien entendido, y tiene una solución que existe desde Kafka 2.4.0. La mayoría de los equipos simplemente no la ha activado, o no ha dado el paso adicional de combinarla con membresía estática. Esa combinación es la diferencia entre un rebalanceo que nadie nota y uno que rompe un SLA.

Nosotros operamos nuestra propia columna vertebral de event streaming sobre NATS JetStream en producción, y gestionamos pipelines compatibles con Kafka para clientes por encima de ella. El modo de fallo para el que hemos tenido que diseñar directamente es el mismo que enfrenta cualquier operador de Kafka: un grupo de consumidores que funciona bien en estado estable y se desmorona bajo churn, ya sea que ese churn venga de un despliegue rolling, un desalojo de pod en Kubernetes, o un corte de red momentáneo.

El protocolo original de rebalanceo de Kafka es “eager” (voraz), y eager significa detener todo el mundo. Cuando el coordinador detecta un cambio, cada consumidor del grupo deja de consumir, cede sus particiones y se vuelve a unir desde cero, luego espera a que el líder del grupo calcule y reparta una asignación completamente nueva. No importa que solo un consumidor se haya unido o salido; todo el grupo se detiene. Para un grupo con un puñado de particiones esto es un error de redondeo. Para un grupo con cientos de particiones y procesamiento con estado detrás, se mide en segundos, a veces decenas de segundos, y ocurre en cada despliegue.

El assignor cooperative sticky: incremental en lugar de detener todo el mundo

KIP-429 reemplazó ese modelo con rebalanceo cooperativo incremental. En lugar de revocar todas las particiones y reasignar el conjunto completo, el cooperative sticky assignor calcula el delta mínimo entre la asignación anterior y la nueva, y solo ese delta se mueve. Los consumidores siguen procesando las particiones que ya poseen mientras el traspaso ocurre para el resto.

La mecánica se ejecuta en dos fases. Los consumidores reportan su propiedad actual al coordinador; el assignor calcula el nuevo layout y le indica a un consumidor que va a ganar una partición que espere, porque el propietario anterior todavía la mantiene. El consumidor que la pierde ve que ya no tiene esa partición asignada, la revoca, y esa revocación dispara un segundo rebalanceo breve para completar el traspaso. No es instantáneo, pero está acotado a las particiones que realmente se mueven, no a todo el grupo.

Los números lo respaldan. Benchmarks citados por Stanislav Kozlovski, exmiembro del PMC de Kafka, mostraron que un rolling bounce de 10 instancias de Kafka Streams pasó de 37 segundos de pausa total bajo el protocolo eager a 3,5 segundos bajo cooperative, una reducción de aproximadamente 10 veces. Pruebas separadas sobre Kafka Connect mostraron tareas iniciando 21 veces más rápido y cerrando 12,5 veces más rápido bajo el modelo cooperative.

Un rebalanceo que detiene todo el mundo no distingue entre que se mueva una partición o todas. Un rebalanceo cooperativo mueve exactamente lo que cambió y nada más, que es precisamente el punto.

Activarlo es un cambio de configuración de una sola línea: asignar partition.assignment.strategy a CooperativeStickyAssignor. Si estás actualizando un grupo activo desde un assignor más antiguo, lista tanto RangeAssignor como CooperativeStickyAssignor durante la transición para que los miembros no discrepen sobre el protocolo a mitad del despliegue. Una advertencia a tener en cuenta: los offsets no pueden confirmarse mientras un consumidor está esperando a que termine un rebalanceo, así que un consumidor puede seguir haciendo fetch durante la transición pero aun así queda atrapado detrás de la barrera de commit hasta que el traspaso se completa.

Membresía estática de grupo: evitar que los reinicios disparen un rebalanceo

El rebalanceo cooperativo hace que los rebalanceos sean más económicos. La membresía estática de grupo, de KIP-345, evita disparar uno directamente para la causa más común: un consumidor que se reinicia. Sin ella, cada reinicio de pod, cada despliegue rolling, cada desconexión transitoria envía una solicitud de leave-group, y el grupo se rebalancea aunque el mismo consumidor esté a punto de reincorporarse segundos después.

Configurar group.instance.id en cada consumidor le da una identidad estable a través de los reinicios. Un consumidor con ese ID configurado no envía una solicitud de leave-group cuando se cae; el coordinador confía únicamente en que expire session.timeout.ms antes de reasignar las particiones de ese consumidor. Combínalo con un timeout de sesión más largo y el consumidor puede caerse, reiniciarse, y reanudar su asignación anterior sin que el grupo se rebalancee nunca. Una limitación a conocer: si el consumidor específico que sale y se reincorpora resulta ser el líder del grupo, el coordinador actualiza su estado interno pero la referencia al líder del lado del cliente queda obsoleta, un vacío que se abordó más adelante con KIP-814. Planifica tu lógica dependiente del líder en consecuencia.

El triángulo de timeouts que realmente reduce las tormentas de rebalanceo

La asignación cooperativa y la membresía estática resuelven la forma de un rebalanceo. Los valores de timeout resuelven si uno ocurre siquiera bajo carga. Tres valores importan juntos, no por separado:

  • session.timeout.ms: elevado de 10 a 45 segundos por defecto en Kafka 3.0 (KIP-735), específicamente porque los cortes transitorios de red disparaban rebalanceos con mucha más frecuencia que fallos reales de consumidores.
  • max.poll.interval.ms: por defecto son cinco minutos, y determina cuánto tiempo puede permanecer un consumidor dentro de su bucle de procesamiento antes de que Kafka asuma que está atascado y lo expulse.
  • heartbeat.interval.ms: por defecto son 3 segundos; la recomendación de siempre es mantener el timeout de sesión al menos tres veces este valor para que el jitter normal de red no se confunda con un consumidor muerto.

Las tormentas de rebalanceo ocurren cuando estos tres valores quedan desproporcionados respecto a tu tiempo real de procesamiento. Un consumidor que tarda más por mensaje de lo que permite su timeout de sesión es expulsado a mitad de un batch, se reincorpora, se le vuelve a asignar la misma partición lenta, y el ciclo se repite. Este es el patrón que más aparece en entornos dinámicos como Kubernetes, donde los retrasos de scheduling de pods y el throttling de recursos empujan rutinariamente el tiempo de procesamiento más allá de un timeout predeterminado que nunca se ajustó para la carga de trabajo. Corregirlo suele significar elevar max.poll.interval.ms hasta superar cómodamente tu peor caso de procesamiento de batch, no solo ajustar ligeramente el timeout de sesión.

Dónde ejecutas el pipeline también importa aquí, porque la sensibilidad al rebalanceo se agrava con la varianza de red. Operamos AMS1, nuestro sitio en Ámsterdam, específicamente para cargas de trabajo que necesitan peering denso y de baja latencia a través de Benelux, el Reino Unido y Europa Occidental vía AMS-IX; para un pipeline compatible con Kafka que alimenta un sistema de trading o telecomunicaciones donde cada milisegundo de jitter de red consume tu presupuesto de timeout de sesión, esa densidad de peering es la diferencia entre un consumidor que ocasionalmente parpadea y uno que no. Si tu grupo de consumidores ya está ajustado con precisión, no lo deshagas ejecutando los brokers en algún lugar con latencia de tránsito impredecible. Para los equipos que están evaluando dónde deberían estar físicamente los brokers, nuestro artículo sobre arquitectura de despliegue de Kafka en bare metal profundiza en el lado del hardware de esa decisión.

Nada de esto es exótico. El cooperative sticky assignor y la membresía estática han sido estables desde Kafka 2.4.0, y los valores predeterminados de timeout ya se han movido en la propia historia de Kafka hacia tolerar condiciones reales de red. La brecha es casi siempre operativa: equipos que ejecutan grupos de consumidores con configuración heredada, sin revisar nunca el assignor ni el triángulo de timeouts hasta que una tormenta de rebalanceo durante un despliegue rutinario tumba un pipeline sensible a la latencia frente a las personas que dependen de que siga funcionando.