Kafka в Docker Compose: KRaft без ZooKeeper
Kafka в Docker Compose: KRaft без ZooKeeper
До Kafka 2.8 брокер требовал ZooKeeper для хранения метаданных кластера: какие партиции где живут, кто из брокеров активен, где лидер. Это два процесса, два набора настроек, два набора логов. С Kafka 3.x KRaft (Kafka Raft) стал стабильным, и ZooKeeper официально deprecated. В Kafka 4.0 ZooKeeper-режим убран полностью.
KRaft встраивает Raft-консенсус прямо в брокер. Брокер сам ведёт метаданные, сам выбирает лидера. Для локальной разработки это означает один контейнер вместо двух.
Минимальный docker-compose.yml
version: "3.9"
services:
kafka:
image: confluentinc/cp-kafka:7.6.0
hostname: kafka
ports:
- "9092:9092"
environment:
KAFKA_NODE_ID: 1
KAFKA_PROCESS_ROLES: broker,controller
KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT
KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true"
CLUSTER_ID: "MkU3OEVBNTcwNTJENDM2Qk"
Разберём каждую переменную:
KAFKA_NODE_ID: 1 - уникальный числовой идентификатор узла в кластере. При одном узле всегда 1.
KAFKA_PROCESS_ROLES: broker,controller - узел совмещает роли: broker принимает запросы продюсеров и консьюмеров, controller управляет метаданными. В production-кластере роли разделяют.
KAFKA_LISTENERS - на каких адресах слушать. PLAINTEXT://0.0.0.0:9092 для клиентов, CONTROLLER://0.0.0.0:9093 для внутреннего Raft-протокола между контроллерами.
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092 - адрес, который брокер отдаёт клиентам как «сюда подключайся». Именно здесь важно написать localhost, а не kafka - клиент из хост-машины не знает про Docker-сеть.
KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093 - список всех контроллеров кластера. Формат: nodeId@hostname:port. Здесь один контроллер - сам этот узел, по имени kafka внутри Docker-сети.
CLUSTER_ID - уникальный идентификатор кластера в base64. Генерируется один раз командой:
docker run --rm confluentinc/cp-kafka:7.6.0 kafka-storage random-uuid
В примере захардкожен для воспроизводимости. Не меняй его без пересоздания volumes.
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 - топик __consumer_offsets хранит смещения consumer groups. При одном брокере replication factor должен быть 1, иначе Kafka не запустится.
С Kafka UI для удобной инспекции
В разработке удобно видеть топики, consumer groups и сообщения в веб-интерфейсе:
version: "3.9"
services:
kafka:
image: confluentinc/cp-kafka:7.6.0
hostname: kafka
ports:
- "9092:9092"
environment:
KAFKA_NODE_ID: 1
KAFKA_PROCESS_ROLES: broker,controller
KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT
KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true"
CLUSTER_ID: "MkU3OEVBNTcwNTJENDM2Qk"
kafka-ui:
image: provectuslabs/kafka-ui:latest
ports:
- "8080:8080"
environment:
KAFKA_CLUSTERS_0_NAME: local
KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:9092
depends_on:
- kafka
После docker compose up -d открывай http://localhost:8080 - увидишь все топики, партиции, consumer groups и lag.
Управление топиками
Запустив Kafka, можно работать с топиками через CLI внутри контейнера:
# Создать топик вручную (если KAFKA_AUTO_CREATE_TOPICS_ENABLE=false)
docker compose exec kafka \
kafka-topics --create \
--bootstrap-server localhost:9092 \
--topic lesson-events \
--partitions 3 \
--replication-factor 1
# Список всех топиков
docker compose exec kafka \
kafka-topics --list --bootstrap-server localhost:9092
# Детальная информация: партиции, лидеры, реплики
docker compose exec kafka \
kafka-topics --describe \
--bootstrap-server localhost:9092 \
--topic lesson-events
# Изменить количество партиций (только увеличивать)
docker compose exec kafka \
kafka-topics --alter \
--bootstrap-server localhost:9092 \
--topic lesson-events \
--partitions 6
Вывод --describe выглядит так:
Topic: lesson-events PartitionCount: 3 ReplicationFactor: 1
Topic: lesson-events Partition: 0 Leader: 1 Replicas: 1 Isr: 1
Topic: lesson-events Partition: 1 Leader: 1 Replicas: 1 Isr: 1
Topic: lesson-events Partition: 2 Leader: 1 Replicas: 1 Isr: 1
Leader: 1 - брокер с Node ID 1 является лидером для этой партиции. Isr: 1 - список In-Sync Replicas (про ISR подробнее в уроке о гарантиях доставки).
Тест через console producer и consumer
Быстрая проверка что всё работает - через встроенные CLI-утилиты:
# Терминал 1: consumer (слушает с начала топика)
docker compose exec kafka \
kafka-console-consumer \
--bootstrap-server localhost:9092 \
--topic lesson-events \
--from-beginning
# Терминал 2: producer (пишем сообщения построчно)
docker compose exec kafka \
kafka-console-producer \
--bootstrap-server localhost:9092 \
--topic lesson-events
# Вводим сообщения:
> {"event":"lesson.completed","userId":42}
> {"event":"lesson.completed","userId":43}
В терминале 1 появятся оба сообщения. Consumer с --from-beginning читает с offset=0 - при каждом запуске увидишь все накопленные сообщения.
Полезные команды для диагностики
# Посмотреть offset consumer group
docker compose exec kafka \
kafka-consumer-groups \
--bootstrap-server localhost:9092 \
--describe \
--group my-service
# Вывод:
# GROUP TOPIC PARTITION CURRENT-OFFSET LOG-END-OFFSET LAG
# my-service lesson-events 0 42 45 3
# my-service lesson-events 1 38 38 0
# my-service lesson-events 2 51 51 0
# Сбросить offset на начало (для replay)
docker compose exec kafka \
kafka-consumer-groups \
--bootstrap-server localhost:9092 \
--group my-service \
--topic lesson-events \
--reset-offsets \
--to-earliest \
--execute
# Удалить топик
docker compose exec kafka \
kafka-topics --delete \
--bootstrap-server localhost:9092 \
--topic lesson-events
Kafka в общем docker-compose.yml проекта
Если в проекте уже есть PostgreSQL и RabbitMQ, добавляй Kafka в тот же файл:
version: "3.9"
services:
postgres:
image: postgres:16
environment:
POSTGRES_DB: backendstart
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
ports:
- "5432:5432"
rabbitmq:
image: rabbitmq:3-management
ports:
- "5672:5672"
- "15672:15672"
kafka:
image: confluentinc/cp-kafka:7.6.0
hostname: kafka
ports:
- "9092:9092"
environment:
KAFKA_NODE_ID: 1
KAFKA_PROCESS_ROLES: broker,controller
KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT
KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true"
CLUSTER_ID: "MkU3OEVBNTcwNTJENDM2Qk"
api:
build: .
depends_on:
- postgres
- rabbitmq
- kafka
environment:
KAFKA_BROKERS: kafka:9092
RABBITMQ_URL: amqp://guest:guest@rabbitmq:5672/
DATABASE_URL: postgres://app:secret@postgres:5432/backendstart
Внимание: в KAFKA_ADVERTISED_LISTENERS для api-сервиса нужен не localhost, а kafka:9092 - потому что api-контейнер обращается к kafka через Docker-сеть. Это решается через две listener-группы или через отдельный docker-compose.override.yml для разработки.
Типичные ошибки
CLUSTER_ID не задан - Kafka 3.x в KRaft-режиме требует CLUSTER_ID. Без него контейнер стартует, но при попытке создать топик падает с ошибкой:
ERROR Exiting Kafka due to fatal exception during startup
org.apache.kafka.common.KafkaException: No `meta.properties` or `--cluster-id` was provided
Решение: добавь CLUSTER_ID в переменные, либо сгенерируй uuid через kafka-storage random-uuid.
advertised.listeners указывает на внутренний hostname - если KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092, то Go-приложение на хост-машине не найдёт хост kafka. Клиент подключается к bootstrap, получает адрес партиции-лидера из метаданных - и именно этот адрес используется для реальных запросов. Решение: PLAINTEXT://localhost:9092 для dev, и отдельный listener для приложений внутри Docker.
Kafka не стартует после удаления volume - если удалить volume без пересоздания CLUSTER_ID, Kafka не найдёт метаданные и упадёт. Решение: при полном пересоздании удаляй volume и указывай тот же CLUSTER_ID.
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 3 при одном брокере - Kafka не создаст внутренний топик __consumer_offsets, потому что не может удовлетворить replication factor. Решение: при одном брокере все три *REPLICATION_FACTOR и MIN_ISR должны быть 1.
Связанные уроки
- Docker - docker-compose - базовый синтаксис compose-файлов
- Message Brokers - Intro - сравнение RabbitMQ и Kafka, когда что выбирать
- Message Brokers - RabbitMQ compose - запуск RabbitMQ аналогичным способом
Мини-задание
- Запусти
docker compose up -dс конфигом выше и убедись что Kafka стартовала (docker compose logs kafka | grep "Kafka Server started") - Создай топик
lesson-eventsс 3 партициями черезkafka-topics --create - Открой два терминала: в первом запусти console-consumer с
--from-beginning, во втором - console-producer. Отправь 3 сообщения и проверь что они дошли - Выполни
kafka-topics --describeи запиши: кто лидер каждой партиции и что такое ISR - Добавь
kafka-uiв compose и откройhttp://localhost:8080- найди топик и посмотри сообщения в нём