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 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 - найди топик и посмотри сообщения в нём

Зарегистрируйтесь бесплатно, чтобы пройти квиз, решить задание с автопроверкой и вести прогресс.