Изучаем Apache Kafka с нуля. Урок 4. Kafka Docker KRaft 3х узловый кластер

Изучаем Apache Kafka с нуля. Урок 4. Kafka Docker KRaft 3х узловый кластер

Изучаем Apache Kafka с нуля.
Урок 4. Kafka Docker KRaft 3х узловый кластер

 

ПО: Apache Kafka 4.2.0 (образ apache/kafka:4.2.0)  |  Docker: Engine 28.1, Compose v2.35  |  Окружение: Ubuntu 22.04 LTS / macOS 14+  |  Уровень: начинающий+

 

В прошлом уроке мы подняли один брокер Kafka в Docker за несколько минут и убедились, что образ apache/kafka работает без лишних телодвижений. Один брокер удобен для отладки, но в нём нет ни отказоустойчивости, ни нормальной репликации. Если контейнер упадёт — вся очередь сообщений пропадёт вместе с ним.

В этом уроке мы развернём кластер из трёх брокеров через Docker Compose. По ходу разберём, как KRaft распределяет роли контроллеров между узлами, как настроить внутреннюю сеть для межброкерного общения и что происходит с кластером, когда один из брокеров отключается. В конце вы увидите реальную картину — топик с репликацией 3, лидер партиции, ISR-список и смену лидера при аварии.

Если вы планируете администрировать Kafka в продакшне — курс KAFKA. Администрирование кластера Kafka даёт именно такую практику: продакшн-топологии, настройку репликации и мониторинг кластера на живых примерах.

 

Зачем нужны три брокера?

Три — минимальное число узлов для кворума в KRaft. Понять почему легко через математику: кворум — это большинство, то есть N/2 + 1 узлов. При трёх брокерах кворум составляет два узла. Это значит, что кластер продолжает работать, если один брокер недоступен, но не более одного.

Три брокера дают три важных свойства, которых нет в однонодовом варианте.

  • Отказоустойчивость. Кластер переживает падение одного брокера без потери данных и без остановки работы.
  • Репликация данных. Топик с replication-factor 3 хранит копию каждой партиции на всех трёх брокерах. Данные не теряются даже при аварии.
  • Распределённый кворум KRaft. Все три узла участвуют в выборах лидера контроллера. Metadata-лог реплицируется между всеми тремя, и нет единой точки отказа для метаданных.

В продакшне минимальный рекомендуемый размер кластера — именно три брокера. Два брокера не дают кворума при потере одного, а четыре — избыточны без явных причин для масштабирования.

 

Как Kafka KRaft строит кворум в кластере

В уроке 2 мы разбирали KRaft в общих чертах. Теперь посмотрим конкретно на то, как это работает в кластере. В нашей конфигурации каждый брокер совмещает роли — одновременно является и брокером (принимает сообщения от клиентов), и контроллером (участвует в кворуме метаданных). Это допустимо для небольших кластеров и удобно для учебного окружения.

Параметр KAFKA_CONTROLLER_QUORUM_VOTERS перечисляет всех участников кворума в формате nodeId@hostname:controllerPort. Этот список одинаковый для всех трёх брокеров — каждый знает обо всех остальных ещё до старта. Один из контроллеров становится active controller и управляет metadata-логом. При его падении оставшиеся два выбирают нового лидера.

Схема Kafka KRaft кластера из трёх брокеров с распределением партиций и ролями controller и broker на каждом узле

Ключевой момент: межброкерное общение идёт по внутренней Docker-сети, а внешние клиенты (ваш терминал, приложение) подключаются через проброшенные порты на localhost. Поэтому нам нужны два разных listener-а — один для внутреннего трафика, другой для внешнего.

 

Структура проекта по развертыванию Kafka KRaft кластера в Docker

Создаём отдельную директорию — лучше не смешивать с файлами из урока 3.

mkdir kafka-lesson4
cd kafka-lesson4

Нам понадобится один файл docker-compose.yml. Никаких дополнительных конфигов и Dockerfile — вся настройка через переменные окружения.

 

docker-compose.yml для трёхнодового кластера

Вот полный файл. Разберём его по частям сразу после листинга.

# apache/kafka:4.2.0, Docker Compose v2.35
# Трёхнодовый KRaft-кластер: каждый узел совмещает роли broker+controller

services:

  kafka1:
    image: apache/kafka:4.2.0
    container_name: kafka-kraft-1
    ports:
      - "9092:9092"
    environment:
      KAFKA_NODE_ID: 1
      KAFKA_PROCESS_ROLES: broker,controller
      KAFKA_LISTENERS: PLAINTEXT_HOST://0.0.0.0:9092,PLAINTEXT://0.0.0.0:29092,CONTROLLER://0.0.0.0:9093
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT_HOST://localhost:9092,PLAINTEXT://kafka1:29092
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
      KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
      KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
      KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka1:9093,2@kafka2:9093,3@kafka3:9093
      CLUSTER_ID: "MkU3OEVBNTcwNTJENDM2Qk"
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 3
      KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 3
      KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 2
      KAFKA_DEFAULT_REPLICATION_FACTOR: 3
      KAFKA_MIN_INSYNC_REPLICAS: 2
    networks:
      - kafka-net

  kafka2:
    image: apache/kafka:4.2.0
    container_name: kafka-kraft-2
    ports:
      - "9094:9092"
    environment:
      KAFKA_NODE_ID: 2
      KAFKA_PROCESS_ROLES: broker,controller
      KAFKA_LISTENERS: PLAINTEXT_HOST://0.0.0.0:9092,PLAINTEXT://0.0.0.0:29092,CONTROLLER://0.0.0.0:9093
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT_HOST://localhost:9094,PLAINTEXT://kafka2:29092
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
      KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
      KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
      KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka1:9093,2@kafka2:9093,3@kafka3:9093
      CLUSTER_ID: "MkU3OEVBNTcwNTJENDM2Qk"
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 3
      KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 3
      KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 2
      KAFKA_DEFAULT_REPLICATION_FACTOR: 3
      KAFKA_MIN_INSYNC_REPLICAS: 2
    networks:
      - kafka-net

  kafka3:
    image: apache/kafka:4.2.0
    container_name: kafka-kraft-3
    ports:
      - "9096:9092"
    environment:
      KAFKA_NODE_ID: 3
      KAFKA_PROCESS_ROLES: broker,controller
      KAFKA_LISTENERS: PLAINTEXT_HOST://0.0.0.0:9092,PLAINTEXT://0.0.0.0:29092,CONTROLLER://0.0.0.0:9093
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT_HOST://localhost:9096,PLAINTEXT://kafka3:29092
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
      KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
      KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
      KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka1:9093,2@kafka2:9093,3@kafka3:9093
      CLUSTER_ID: "MkU3OEVBNTcwNTJENDM2Qk"
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 3
      KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 3
      KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 2
      KAFKA_DEFAULT_REPLICATION_FACTOR: 3
      KAFKA_MIN_INSYNC_REPLICAS: 2
    networks:
      - kafka-net

networks:
  kafka-net:
    driver: bridge

Разбор ключевых переменных окружения

Большинство переменных вы уже видели в уроке 3. Сосредоточимся на том, что появилось впервые или изменилось по сравнению с однонодовым вариантом.

Три listener-а вместо двух

В уроке 3 нам хватало двух listener-ов: PLAINTEXT для внешних клиентов и CONTROLLER для кворума. В кластере добавляется третий — PLAINTEXT_HOST для внешних клиентов через localhost, и PLAINTEXT становится внутренним межброкерным каналом.

  • PLAINTEXT_HOST://0.0.0.0:9092. Порт, который пробрасывается на хост. Через него вы подключаетесь с localhost:9092 (kafka1), localhost:9094 (kafka2), localhost:9096 (kafka3).
  • PLAINTEXT://0.0.0.0:29092. Внутренний порт для межброкерного трафика. Снаружи не виден, работает только внутри Docker-сети kafka-net. Брокеры обращаются друг к другу как kafka1:29092, kafka2:29092, kafka3:29092.
  • CONTROLLER://0.0.0.0:9093. Порт кворума KRaft. Используется только контроллерами для репликации metadata-лога.

Конфигурация listeners для кластера Apache Kafka в Kraft режиме

Переменная KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT говорит Kafka, какой listener использовать для репликации данных между брокерами. Именно поэтому в KAFKA_ADVERTISED_LISTENERS каждый брокер анонсирует себя по имени контейнера: kafka1:29092, kafka2:29092 и т.д.

 

CLUSTER_ID — одинаковый для всех

Значение CLUSTER_ID должно быть идентичным во всех трёх сервисах. Это Base64-строка, которая однозначно идентифицирует кластер. Если хотите сгенерировать своё — используйте команду внутри контейнера или любой UUID-генератор с переводом в Base64 без паддинга. Для урока можете оставить значение из примера.

 

Параметры репликации

Четыре переменные регулируют поведение репликации на уровне кластера.

  • KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 3. Системный топик __consumer_offsets будет храниться на всех трёх брокерах.
  • KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 3. То же для топика транзакций.
  • KAFKA_DEFAULT_REPLICATION_FACTOR: 3. Все новые топики по умолчанию создаются с репликацией 3.
  • KAFKA_MIN_INSYNC_REPLICAS: 2. Запись считается подтверждённой, только если как минимум 2 реплики записали данные. Это гарантирует, что данные не потеряются при падении одного брокера.

Параметр KAFKA_MIN_INSYNC_REPLICAS работает в связке с настройкой acks=all на продюсере. Без неё производитель может не дожидаться подтверждения от реплик — и тогда при аварии часть сообщений рискует потеряться.

Запуск кластера Apache Kafka Kraft в Docker

Убедитесь, что порты 9092, 9094 и 9096 свободны. Если контейнер из урока 3 ещё жив — остановите его.

# Остановить контейнер из урока 3, если он запущен
docker stop kafka-kraft-single

Теперь запускаем кластер из директории kafka-lesson4.

# Проверено: Apache Kafka 4.2.0, Docker Compose v2.35
docker compose up -d

Docker Compose подтянет образ (если ещё нет в кэше), создаст сеть kafka-net и запустит три контейнера. Убедитесь, что все три в статусе running.

docker compose ps

Вывод должен показать три контейнера kafka-kraft-1, kafka-kraft-2, kafka-kraft-3 со статусом Up. Подождите 15-20 секунд после старта — контроллерам нужно время на выборы лидера и инициализацию metadata-лога.

Проверка работы кластера Kafka в Docker

Проверим кластер изнутри первого контейнера. Для этого используем docker exec и утилиту kafka-metadata-quorum.sh из папки bin/ образа.

# Проверено: Apache Kafka 4.2.0, Docker Compose v2.35
docker exec -it kafka-kraft-1 kafka-metadata-quorum.sh \
  --bootstrap-server kafka1:29092 \
  describe --status

В выводе увидите строчку LeaderId — это номер узла, который сейчас управляет metadata-логом. Также видны CurrentVoters (все три контроллера) и CurrentObservers (пусто, т.к. у нас нет отдельных брокеров без роли контроллера).

# Посмотреть список брокеров через kafka-broker-api-versions.sh
docker exec -it kafka-kraft-1 kafka-broker-api-versions.sh \
  --bootstrap-server kafka1:29092 | grep Node

Три строки Node 1, Node 2, Node 3 подтверждают, что все брокеры видят друг друга и регистрируются в кластере.

Создание топика и проверка репликации

Создадим тестовый топик с тремя партициями и фактором репликации 3.

# Проверено: Apache Kafka 4.2.0, Docker Compose v2.35
docker exec -it kafka-kraft-1 kafka-topics.sh \
  --bootstrap-server kafka1:29092 \
  --create \
  --topic cluster-test \
  --partitions 3 \
  --replication-factor 3

Теперь посмотрим, как Kafka распределила партиции и реплики между брокерами.

docker exec -it kafka-kraft-1 kafka-topics.sh \
  --bootstrap-server kafka1:29092 \
  --describe \
  --topic cluster-test

Вывод будет выглядеть примерно так.

Topic: cluster-test   TopicId: ...   PartitionCount: 3   ReplicationFactor: 3
  Topic: cluster-test   Partition: 0   Leader: 1   Replicas: 1,2,3   Isr: 1,2,3
  Topic: cluster-test   Partition: 1   Leader: 2   Replicas: 2,3,1   Isr: 2,3,1
  Topic: cluster-test   Partition: 2   Leader: 3   Replicas: 3,1,2   Isr: 3,1,2

Разберём что здесь что. Каждая партиция имеет одного лидера — брокер, который принимает записи от продюсеров. Replicas — полный список брокеров, хранящих копии этой партиции (порядок определяет приоритет при выборах лидера). Isr — это In-Sync Replicas, реплики, которые синхронизированы с лидером в данный момент. Пока все брокеры работают, Isr совпадает с Replicas.

Обратите внимание: Kafka автоматически распределила лидерство между тремя брокерами. Партиция 0 живёт на брокере 1, партиция 1 — на брокере 2, партиция 2 — на брокере 3. Нагрузка сбалансирована.

Симуляция отказа брокера Kafka

Вот ради чего мы всё это строили. Остановим второй брокер и посмотрим, что произойдёт с кластером.

# Останавливаем kafka-kraft-2
docker stop kafka-kraft-2

Подождём 10-15 секунд и снова проверим топик.

docker exec -it kafka-kraft-1 kafka-topics.sh \
  --bootstrap-server kafka1:29092 \
  --describe \
  --topic cluster-test

Вывод изменится — и это самое интересное.

Topic: cluster-test   Partition: 0   Leader: 1   Replicas: 1,2,3   Isr: 1,3
Topic: cluster-test   Partition: 1   Leader: 3   Replicas: 2,3,1   Isr: 3,1
Topic: cluster-test   Partition: 2   Leader: 3   Replicas: 3,1,2   Isr: 3,1

Что произошло. Брокер 2 выпал из всех ISR-списков — Kafka поняла, что он не отвечает. Партиция 1, у которой лидером был брокер 2, выбрала нового лидера из ISR — им стал брокер 3. Оставшиеся два брокера продолжают работу, кластер живёт. Параметр KAFKA_MIN_INSYNC_REPLICAS: 2 при этом выполняется — в ISR осталось по два узла у каждой партиции.

Вернём брокер 2 обратно.

docker start kafka-kraft-2

Через несколько секунд брокер 2 переподключится, наверстает отставание в репликации и снова войдёт в ISR. Лидер при этом автоматически не вернётся к брокеру 2 — для этого нужна команда kafka-leader-election.sh, которую разберём в уроке 21.

Остановка и очистка Kafka кластер Docker

Когда работа с кластером закончена — останавливаем всё одной командой.

# Остановить кластер, сохранив данные
docker compose down

# Остановить и удалить все данные (volumes)
docker compose down -v

Флаг -v удаляет анонимные volumes, в которых Kafka хранит данные. Без него данные сохраняются между запусками. Для учёбы удобнее каждый раз стартовать с чистого листа — используйте -v.

Что дальше?

Мы развернули трёхнодовый кластер Kafka в Docker, увидели репликацию партиций и проверили поведение кластера при отказе одного брокера. Это базовая топология для любой дальнейшей работы — именно такой кластер мы будем использовать в примерах из следующих уроков.

В уроке 5 начнём систематически разбирать утилиты из папки bin/. Первым делом разберёмся с переменными окружения, путями и разницей между флагами —bootstrap-server и —controller-quorum-voters — это база, без которой остальные уроки воспринимаются сложнее.

Референсные ссылки

Все уроки курса

Тема Ссылка
1 Установка Kafka с Zookeeper https://bigdataschool.ru/blog/news/lesson1-kafka-zookeeper-install/
2 Установка Kafka в режиме KRaft https://bigdataschool.ru/blog/news/lesson2-kafka-kraft-install/
3 Docker KRaft: однонодовый кластер https://bigdataschool.ru/blog/news/lesson3-kafka-docker-single/
4 Docker KRaft: 3-нодовый кластер https://bigdataschool.ru/blog/news/lesson4-kafka-docker-cluster/
5 Утилиты bin/: переменные окружения и основы https://bigdataschool.ru/blog/news/lesson5-kafka-bin-intro/
6 kafka-topics.sh: управление топиками https://bigdataschool.ru/blog/news/lesson6-kafka-topics/
7 kafka-console-producer.sh https://bigdataschool.ru/blog/news/lesson7-kafka-console-producer/
8 kafka-console-consumer.sh https://bigdataschool.ru/blog/news/lesson8-kafka-console-consumer/
9 kafka-server-start.sh / kafka-server-stop.sh https://bigdataschool.ru/blog/news/lesson9-kafka-server-start-stop/
10 kafka-storage.sh https://bigdataschool.ru/blog/news/lesson10-kafka-storage/
11 kafka-cluster.sh https://bigdataschool.ru/blog/news/lesson11-kafka-cluster/
12 kafka-metadata-quorum.sh https://bigdataschool.ru/blog/news/lesson12-kafka-metadata-quorum/
13 kafka-metadata-shell.sh https://bigdataschool.ru/blog/news/lesson13-kafka-metadata-shell/
14 kafka-features.sh https://bigdataschool.ru/blog/news/lesson14-kafka-features/
15 kafka-configs.sh https://bigdataschool.ru/blog/news/lesson15-kafka-configs/
16 kafka-log-dirs.sh https://bigdataschool.ru/blog/news/lesson16-kafka-log-dirs/
17 kafka-dump-log.sh https://bigdataschool.ru/blog/news/lesson17-kafka-dump-log/
18 kafka-delete-records.sh https://bigdataschool.ru/blog/news/lesson18-kafka-delete-records/
19 kafka-consumer-groups.sh https://bigdataschool.ru/blog/news/lesson19-kafka-consumer-groups/
20 kafka-streams-application-reset.sh https://bigdataschool.ru/blog/news/lesson20-kafka-streams-reset/
21 kafka-leader-election.sh https://bigdataschool.ru/blog/news/lesson21-kafka-leader-election/
22 kafka-reassign-partitions.sh https://bigdataschool.ru/blog/news/lesson22-kafka-reassign-partitions/
23 kafka-replica-verification.sh https://bigdataschool.ru/blog/news/lesson23-kafka-replica-verification/
24 kafka-acls.sh https://bigdataschool.ru/blog/news/lesson24-kafka-acls/
25 kafka-broker-api-versions.sh https://bigdataschool.ru/blog/news/lesson25-kafka-broker-api-versions/
26 kafka-get-offsets.sh https://bigdataschool.ru/blog/news/lesson26-kafka-get-offsets/
27 kafka-verifiable-producer/consumer.sh https://bigdataschool.ru/blog/news/lesson27-kafka-verifiable/
28 kafka-producer-perf-test.sh https://bigdataschool.ru/blog/news/lesson28-kafka-producer-perf/
29 kafka-consumer-perf-test.sh https://bigdataschool.ru/blog/news/lesson29-kafka-consumer-perf/
30 kafka-mirror-maker.sh https://bigdataschool.ru/blog/news/lesson30-kafka-mirror-maker/
31 connect-standalone.sh https://bigdataschool.ru/blog/news/lesson31-kafka-connect-standalone/
32 connect-distributed.sh https://bigdataschool.ru/blog/news/lesson32-kafka-connect-distributed/
33 kcat: альтернативный CLI https://bigdataschool.ru/blog/news/lesson33-kcat/
Изменение базового тарифа с 1 января 2026 года Подробнее