Mensajería y streaming de eventos

Arquitectura de Apache Kafka: topics, particiones, replicación, consumer groups y KRaft

Intermedio12 min de lecturaActualizado

Esta página se tradujo del inglés con ayuda de IA y se cotejó con el original. Leer la versión en inglés

Kafka no es una cola con funciones extra: es un log particionado y replicado que los consumidores leen a su propio ritmo. Esta guía construye ese modelo desde cero —registros, particiones, offsets, segmentos, retención y compactación, réplicas líder y seguidoras, el conjunto de réplicas sincronizadas, consumer groups y rebalanceo, y el quórum de metadatos KRaft que sustituyó a ZooKeeper— para que después cobren sentido la configuración del productor, la operación y las decisiones de diseño.

Lo que aprenderás

  • Explicar por qué Kafka es un log y no una cola, y qué cambia eso para los consumidores
  • Describir particiones, offsets, segmentos, retención y compactación, y cómo interactúan
  • Explicar la replicación líder/seguidor, el conjunto de réplicas sincronizadas, acks y min.insync.replicas
  • Describir los consumer groups, el commit de offsets y el rebalanceo, y su efecto en el orden y el paralelismo
  • Explicar el papel del quórum de controladores KRaft y cómo sustituyó a ZooKeeper
  • Decidir el número de particiones, las claves y el factor de replicación de un topic nuevo

Apache Kafka nació como una forma de mover logs de actividad a la escala de LinkedIn y acabó convirtiéndose en la columna vertebral por defecto del event streaming. A menudo se presenta como «una cola de mensajes que escala», y ese malentendido lleva a usarlo como si lo fuera. Kafka es un commit log distribuido: una secuencia de registros de solo inserción (append-only), dividida en particiones repartidas entre brokers, replicada por seguridad, retenida durante un tiempo configurado y leída por consumidores que recuerdan su propia posición. Todo lo demás se deriva de ese diseño. Esta guía cubre Kafka 3.x en modo KRaft y trata ZooKeeper como historia.

Registros, topics y particiones

Un registro (record) es una clave, un valor, una marca de tiempo y cabeceras opcionales. Para Kafka, clave y valor son arrays de bytes; la serialización es asunto exclusivo del cliente. Un topic es una categoría de registros con nombre, y se divide en una o más particiones. Cada partición es una secuencia ordenada e inmutable: un registro añadido a la partición 3 recibe el siguiente offset de la partición 3, un número de 64 bits que nunca cambia y nunca se repite dentro de esa partición.

Tres consecuencias condicionan cualquier diseño con Kafka:

  • El orden existe dentro de una partición y en ningún otro sitio. Dos registros de particiones distintas no tienen un orden definido.
  • El paralelismo está limitado por el número de particiones. Una partición es una unidad de almacenamiento en un broker y una unidad de consumo para un único consumidor de un grupo.
  • La clave decide la partición. El particionador por defecto calcula el hash de la clave y le aplica el módulo del número de particiones, de modo que todos los registros de customer-4711 caen en la misma partición y, por tanto, se consumen en orden. Los registros con clave nula se reparten entre particiones por lotes.
topic "orders" with 3 partitions

partition 0 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |      ← offsets; new records append on the right
partition 1 | 0 | 1 | 2 | 3 |
partition 2 | 0 | 1 | 2 | 3 | 4 | 5 |

key "customer-4711" → hash → partition 1, always

La posición de un registro queda totalmente identificada por (topic, partición, offset), y esa terna es lo que un consumidor guarda como progreso. No hay confirmación por registro, ni estado «sin confirmar», ni cola de reentrega; solo existe «el consumer group ha leído hasta aquí».

Segmentos, retención y compactación

En disco, una partición es un directorio de ficheros de segmento. El segmento activo recibe las inserciones; cuando alcanza log.segment.bytes (1 GB por defecto) o log.roll.ms, se cierra y empieza uno nuevo. Cada segmento tiene al lado un índice de offsets y un índice temporal, de forma que un consumidor que pide «el offset 1.234.567» o «el primer registro después de las 09:00» se atiende con un par de seeks en lugar de un recorrido completo. Las escrituras son inserciones secuenciales que pasan por la page cache del sistema operativo, y las lecturas de datos recientes se sirven desde esa caché; eso, más que cualquier código ingenioso, es lo que permite a Kafka mover cientos de megabytes por segundo por broker con discos corrientes.

Los registros se eliminan por retención, no por consumo. Hay dos políticas por topic:

cleanup.policyReglaUso
delete (por defecto)Elimina segmentos completos más antiguos que retention.ms (7 días por defecto) o que exceden retention.bytesFlujos de eventos, logs, cualquier cosa acotada en el tiempo
compactConserva al menos el último registro de cada clave; los registros antiguos con la misma clave se eliminan en segundo planoChangelogs, estado actual por entidad, configuración, el topic interno de offsets
compact,deleteAmbas: compacta y además elimina los datos más antiguos que la retenciónChangelogs que no deben conservar para siempre claves muertas hace tiempo

La compactación convierte un topic en una instantánea clave-valor que se puede reconstruir leyéndolo desde el principio; así es como los state stores de Kafka Streams y muchos servicios de «vistas materializadas» se recuperan tras un reinicio. Un registro con valor nulo es un tombstone: marca la clave como borrada y él mismo se elimina pasado delete.retention.ms. La compactación solo actúa sobre segmentos cerrados y se ejecuta cuando la proporción de datos obsoletos lo justifica, así que un topic compactado siempre contiene algunos duplicados; los lectores deben contar con ello.

La retención es lo que hace que el log se pueda reproducir. Un consumer group nuevo puede empezar desde el offset más antiguo y procesar una semana de historial; un grupo que ha encontrado un bug puede rebobinar hasta ayer y volver a procesar. Ninguna cola ofrece eso, y es la razón de más peso para elegir Kafka.

Brokers, replicación y el conjunto de réplicas sincronizadas

Un broker es un proceso servidor de Kafka. Cada partición tiene un factor de replicación —tres es lo habitual en producción— y sus réplicas viven en otros tantos brokers. Una de ellas es la líder (leader): todas las peticiones de producción y, por defecto, todas las de fetch de esa partición van a ella. Las demás son seguidoras (followers) que hacen fetch continuamente de la líder para mantenerse al día, exactamente igual que los consumidores.

La líder lleva la cuenta de qué seguidoras están al día. Una seguidora que ha llegado hasta el final del log de la líder dentro de replica.lag.time.max.ms (30 segundos por defecto) forma parte del conjunto de réplicas sincronizadas (ISR). El ISR importa por dos motivos:

  1. Un registro está confirmado (committed) cuando todas las réplicas del ISR lo han escrito. Los consumidores solo pueden leer registros confirmados, hasta el high watermark.
  2. Si la líder falla, el controlador elige una nueva líder de entre el ISR, lo que garantiza que no se pierde ningún registro confirmado. Con unclean.leader.election.enable=false (el valor por defecto) nunca se elige una réplica desincronizada, así que una partición con el ISR vacío queda no disponible en lugar de perder datos.

La opción acks del productor decide cuánto espera:

acksEl productor retorna cuandoRiesgo
0El registro se ha entregado a la redPérdida ante cualquier fallo; ni siquiera se notifica un error del broker
1La líder lo ha escritoPérdida si la líder falla antes de que las seguidoras lo recojan
all (por defecto desde 3.0)Todos los miembros del ISR lo han escritoNinguno por caída de un broker, siempre que el ISR tenga más de un miembro

Esa última condición es la razón de ser de min.insync.replicas. Con factor de replicación 3 y min.insync.replicas=2, la líder rechaza las escrituras con acks=all cuando hay menos de dos réplicas sincronizadas, sacrificando disponibilidad a cambio de garantizar que un registro confirmado existe en al menos dos brokers. La terna habitual en producción es factor de replicación 3, min.insync.replicas 2 y acks=all: un broker puede estar parado por mantenimiento sin bloquear a los productores, y ningún registro confirmado depende de una sola máquina.

Desde Kafka 2.4 se puede indicar a los consumidores que lean de la réplica más cercana (replica.selector.class en el broker y client.rack en el consumidor), lo que reduce el tráfico entre zonas en despliegues en la nube.

Consumer groups y offsets

Un consumer group es un conjunto de instancias consumidoras que comparten un group.id. Kafka asigna cada partición de los topics a los que está suscrito el grupo a exactamente uno de sus miembros; un grupo con más miembros que particiones deja ociosos a los miembros sobrantes. Dos grupos independientes que leen el mismo topic reciben cada uno todos los registros: así es como Kafka ofrece a la vez reparto de trabajo (dentro de un grupo) y publish-subscribe (entre grupos).

El progreso de cada grupo es un conjunto de offsets confirmados (committed offsets), uno por partición, guardados en el topic interno __consumer_offsets (compactado, con clave grupo, topic y partición). Un offset confirmado es «el siguiente registro que hay que leer», así que hacer commit del offset 42 significa que los registros del 0 al 41 están procesados. Los consumidores hacen commit periódicamente o de forma explícita; al reiniciar o tras una reasignación, el nuevo propietario de la partición empieza desde el último offset confirmado. Todo lo que haya entre el último commit y la caída se vuelve a leer: eso es entrega at-least-once, y el motivo por el que la guía de consumidores dedica tanto espacio a cuándo hacer commit.

group "billing"     partition 0: committed 1,204   partition 1: committed 988   partition 2: committed 1,377
group "analytics"   partition 0: committed 1,190   partition 1: committed 980   partition 2: committed 1,377
log end offsets     partition 0: 1,210             partition 1: 991             partition 2: 1,377

lag(billing) = (1,210 − 1,204) + (991 − 988) + 0 = 9 records

El consumer lag —offset de fin de log menos offset confirmado, sumado sobre todas las particiones— es la métrica más importante de una aplicación consumidora. Un lag creciente significa que los consumidores no dan abasto o se han detenido.

Rebalanceo

Cuando un miembro entra o sale, o cambia el número de particiones del topic, el grupo se rebalancea: el group coordinator (un broker elegido a partir del hash del id del grupo) dirige un protocolo en el que los miembros acuerdan una nueva asignación. En un rebalanceo eager clásico, cada miembro renuncia a todas sus particiones y recibe otras nuevas, lo que detiene todo el grupo durante segundos; el assignor cooperative sticky, disponible desde la 2.4, solo mueve las particiones que tienen que moverse. Kafka 4.0 introdujo un nuevo protocolo de grupos (KIP-848) que traslada la lógica de asignación al coordinador y hace que los rebalanceos sean incrementales por diseño.

Un miembro se da por muerto cuando deja de enviar heartbeats durante session.timeout.ms o cuando no llama a poll() dentro de max.poll.interval.ms. El segundo caso es el que duele: un consumidor que tarda seis minutos en procesar un lote queda expulsado del grupo, sus particiones se reasignan, su trabajo sin confirmar se rehace en otro sitio y, cuando por fin hace commit, falla con un error de rebalanceo. Ajustar a la vez el tamaño de los lotes y los timeouts es una tarea operativa recurrente.

Metadatos del clúster: el controlador KRaft

Alguien tiene que saber qué brokers existen, dónde vive cada partición, quién es el líder de cada una y qué réplicas están sincronizadas, y alguien tiene que tomar decisiones cuando desaparece un broker. Ese alguien es el controlador.

Hasta Kafka 2.8, ese estado vivía en ZooKeeper, un servicio de coordinación aparte; un broker se elegía como controlador mediante un lock de ZooKeeper y propagaba los cambios de metadatos a los demás. Funcionaba, pero obligaba a operar y asegurar dos sistemas distribuidos, limitaba el número de particiones que podía manejar un clúster porque el failover del controlador tenía que recargarlo todo desde ZooKeeper, y generaba divergencias sutiles entre lo que guardaba ZooKeeper y lo que creían los brokers.

KRaft (Kafka Raft) lo sustituye por la propia maquinaria de Kafka. Un pequeño quórum de nodos controladores —tres o cinco— ejecuta el protocolo de consenso Raft sobre un log interno de metadatos, __cluster_metadata. El líder elegido de ese quórum es el controlador activo; los demás son standbys en caliente que ya tienen todos los metadatos en memoria, así que el failover es casi instantáneo. Los brokers hacen fetch del log de metadatos como cualquier consumidor y aplican los cambios en orden. Los controladores pueden ejecutarse en nodos dedicados (process.roles=controller) o, en clústeres pequeños y en desarrollo, en los mismos procesos que los brokers (process.roles=broker,controller).

Modo ZooKeeper (heredado)Modo KRaft
Almacén de metadatosEnsemble de ZooKeeperLog Raft interno en el quórum de controladores
Failover del controladorDe segundos a minutos, recarga desde ZooKeeperMilisegundos, el standby tiene el estado en memoria
Particiones por clústerDecenas de milesMillones
Sistemas que operarDosUno
EstadoObsoleto en 3.5, eliminado en 4.0Listo para producción desde 3.3, único modo en 4.0

Para quien desarrolla aplicaciones, KRaft no cambia nada en la API cliente. Para quien opera el clúster, elimina un sistema entero del despliegue: bootstrap.servers más un quórum de controladores es toda la historia.

Más allá del log: Connect y Streams

Dos componentes vienen con Kafka y reutilizan todo lo anterior. Kafka Connect es un framework de conectores source y sink que copian datos entre Kafka y sistemas externos —bases de datos mediante change data capture, almacenamiento de objetos, motores de búsqueda— encargándose por ti de offsets, reintentos y escalado. Kafka Streams es una biblioteca Java para procesar registros de topic a topic con joins, ventanas y state stores respaldados por topics changelog compactados, que se ejecuta dentro de tu propia aplicación sin ningún clúster adicional. Ambos explican por qué se habla de Kafka como plataforma y no como broker, y ambos dependen de la semántica de log descrita aquí: particiones reproducibles, orden por clave y compactación.

Diseñar un topic

Llevar el modelo a la práctica significa responder a unas cuantas preguntas antes de ejecutar kafka-topics.sh --create.

  • Clave. Elige la clave según el orden que necesites: un id de pedido mantiene en secuencia los eventos de un pedido; un id de cliente mantiene en secuencia todo lo relativo a un cliente, pero concentra a los clientes muy activos en una sola partición. Una clave nula maximiza el reparto y renuncia al orden.
  • Particiones. Parte del throughput objetivo dividido entre lo que puede procesar un consumidor, redondea hacia arriba a un múltiplo del número de brokers y deja margen: añadir particiones más adelante reasigna las claves. Un número muy alto cuesta memoria y descriptores de fichero, y alarga las elecciones de líder.
  • Factor de replicación y min.insync.replicas. 3 y 2 para cualquier cosa que importe; 1 solo para datos de desarrollo desechables.
  • Retención. Lo bastante larga para reconstruir desde cero un consumidor aguas abajo o para cubrir la mayor caída a la que quieras sobrevivir; compactación si el topic representa estado y no historial.
  • Esquema. A Kafka le da igual qué sean los bytes, así que un schema registry con Avro, Protobuf o JSON Schema, junto con una regla de versionado para cambios compatibles, marca la diferencia entre un contrato duradero y un montón de JSON sin documentar.

Errores comunes

  • Esperar semántica de cola. No hay confirmación por registro, ni dead-letter queue, ni reentrega; un registro fallido lo tiene que gestionar el consumidor (saltarlo, reintentarlo o escribirlo en un topic de errores). Los equipos que quieren esas funciones sobre Kafka las construyen ellos mismos o usan una cola.
  • Una sola partición «para mantener el orden». Funciona hasta el día en que un consumidor no da abasto, y entonces la única solución es reparticionar, lo que cambia el orden de todos modos. Ordena por clave desde el principio.
  • Añadir particiones a un topic con claves en producción. Después, las claves caen en particiones distintas; el orden por clave se rompe en ese cambio y cualquier estado compactado que dependiera de la localidad de las claves queda inconsistente.
  • Factor de replicación 3 con min.insync.replicas 1. La escritura en un solo broker se confirma como duradera. Ponlo a 2.
  • Ignorar el lag. Un consumer lag creciente es la primera señal de un consumidor atascado, un grupo infradimensionado o un poison record. Configura alertas sobre él.
  • Tratar el topic de metadatos o los controladores como opcionales. En modo KRaft, perder la mayoría de los controladores detiene las elecciones de líder y los cambios de metadatos en todo el clúster. Ejecuta tres o cinco controladores en dominios de fallo separados.

Ideas clave

  • Kafka es un log particionado, replicado y retenido; los consumidores controlan sus offsets y nada se borra por haberse leído.
  • Tanto el orden como el paralelismo son por partición, y la clave del registro decide la partición, así que elegir la clave es la decisión de diseño central.
  • Los segmentos y la retención (delete o compact) gobiernan el almacenamiento; la compactación conserva el último valor por clave y convierte un topic en una instantánea reconstruible.
  • La replicación con líder y conjunto de réplicas sincronizadas, acks=all y min.insync.replicas=2 sobre un factor de replicación 3 es la configuración de durabilidad estándar.
  • Los consumer groups se reparten las particiones y hacen commit de offsets en __consumer_offsets; los rebalanceos y los timeouts de poll son donde las aplicaciones consumidoras fallan con más frecuencia.
  • Los controladores KRaft guardan los metadatos del clúster en un log Raft interno; ZooKeeper está obsoleto en 3.x y desaparece en 4.0.

Fuentes y lecturas recomendadas

Preguntas frecuentes

¿Un topic de Kafka está ordenado?
Una partición sí está ordenada; un topic con más de una partición, no. Los registros con la misma clave van a la misma partición (con el particionador por defecto y un número de particiones fijo), así que el orden se mantiene por clave. Si necesitas un orden global, necesitas una sola partición, lo que limita el throughput a lo que puedan dar un único broker y un único consumidor.
¿Cuántas particiones debería tener un topic?
Las suficientes para repartir la carga entre los brokers y dar al consumer group más grande el paralelismo que necesita, ya que una partición la lee como mucho un consumidor dentro de un grupo. Empieza con un múltiplo pequeño del número de brokers —seis o doce para un clúster de tres brokers— y recuerda que se pueden añadir particiones pero no quitarlas, y que añadirlas cambia la partición a la que va cada clave.
¿Kafka borra un registro una vez consumido?
No. Los registros se eliminan según la política de retención —pasada cierta antigüedad o cuando la partición supera un tamaño— o los sustituye otro más reciente con la misma clave cuando hay compactación. Consumir solo hace avanzar el offset de un consumer group. Eso es lo que permite que varios grupos lean los mismos datos y que cualquier grupo pueda rebobinar.
¿Sigo necesitando ZooKeeper?
No para clústeres nuevos. El modo KRaft, en el que un quórum de controladores de Kafka guarda los metadatos del clúster en un log interno, está listo para producción desde Kafka 3.3 y es el único modo en Kafka 4.0. Los clústeres 3.x basados en ZooKeeper deben migrarse con el procedimiento de migración documentado antes de actualizar a 4.x.