Отказоустойчивый Kafka кластер в Docker

Kafka кластер в Docker

До сегодняшнего дня мы запускали Apache Kafka на одном хосте, в кластере из одного брокера и всего с одним монолитным топиком. Для платформы, которая проектировалась под распределенные сценарии, это, наверное, печально. Поэтому сегодня мы станем на шаг ближе к представлению о том, как Kafka работает в нагруженных системах.

Мы создадим кластер из нескольких брокеров, добавим туда разбитый на разделы топик и включим его резервное копирование. При такой конфигурации нужно будет очень постараться, чтобы с кластером случилось что-то неприятное.

Создаём кластер

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

Чтобы сэмитировать изолированные хосты, я помещу  ZooKeeper и всех Кафка-брокеров в собственные Docker контейнеры и запущу их через docker-compose. Последний и упросит конфигурацию, и позаботится о настройке сети.

Dockerfile

Чтобы создать образы контейнеров для Kafka и ZooKeeper нужно всего две вещи: JDK и Kafka инсталлер. Образы будут настолько похожи, что, в принципе, можно обойтись одним Dockerfile и для брокеров, и для координатора.

Брокеров к тому же нужно еще настроить. Например, по-умолчанию Kafka подключается к ZooKeeper через localhost:2181, который внутри контейнера ведёт в никуда. С другой стороны, если дать контейнеру с ZooKeeper внятное название, например — zookeeper, то тогда к нему можно подключаться по zookeeper:2181. Нужно просто немного подредактировать конфигурацию брокера

Второй момент: у каждого брокера внутри кластера должен быть уникальный broker id. Он задаётся в server.properties, и в нашем случае его можно передать прямо из docker-compose в Dockerfile через ARG инструкцию, и потом уже добавить в конфигурацию, например, через  sed . Вот в итоге что у меня получилось:

В образе openjdk есть JDK, а ADD инструкция добавит и распакует архив с Кафкой, который я предусмотрительно скачал до этого. Потом в RUN переименовываем папку с Кафкой и обновляем server.properties. Наконец, EXPOSE оставит в контейнере два открытых порта: 2181 (ZooKeeper) и 9092 (Kafka).

Проще простого.

docker-compose.yml

docker-compose.yml для запуска зоопарка контейнеров будет посложнее. Ведь чтобы получить внятный и функционирующий кластер, понадобятся контейнеры с ZooKeeper, тремя Kafka серверами, и продюсером и консьюмером сообщений (ну чтобы был хоть какой поток данных). Итого — шесть контейнеров. Это раз в шесть больше, чем обычно.

ZooKeeper

Конфигурация контейнера с ZooKeeper самая простая:

Она просто соберет Dockerfile, созданный на предыдущем этапе, запустит ZooKeeper внутри контейнера и оставит порт 2181 открытым для хоста. Вдруг нам захочется с ним пообщаться.

Kafka сервера

Конфигурация для Kafka серверов чуть-чуть сложнее, но всё еще вменяемая. Конфигураций будет три, и для сервера под номером 2 она будет такой:

depends_on  поможет запустить контейнер с Кафкой обязательно после контейнера с ZooKeeper, а  brokerId: 2  пойдёт прямиком в Dockerfile, откуда перекочует в server.properties.

Продюсер

Продюсер — это маленький филиал ада. И всё из-за команды  command . Ведь чтобы запустить продюсер, отправляющий сообщения, нужно всего-то:

Что в переводе на наш означает:

  • подожди 4 секунды (решать race condition через задержки — плохой тон, но с воспитанием в нашу эпоху действительно не очень),
  • создай топик под названием ‘dates’, раздели его на два раздела (partitions) и храни по три копии каждого,
  • раз в секунду отправляй в топик сообщение — текущую дату.

Просто ведь. Но если ужать это в одну строчку, то получится вот что:

Дурдом.

Консьюмер

По сравнению с продюсером, консьюмер практически не уродлив:

И продюсер, и консюмер получили в качестве параметров списки всех Kafka серверов. Это нужно для того, чтобы когда кого-то из брокеров не станет, у сервисов был запасной вариант.

Результат

Вот на что похож docker-compose.yml целиком:

Запускаем кластер

Всё просто. Запускаем  docker-compose up  и через десяток секунд монологов от очень разговорчивых контейнеров наступит тишина, иногда прерываемая сообщениями от консьюмера:

Кажется, оно действительно запустилось.

Так как 2181-й порт у ZooKeeper остался открыт, можно запросить у него статистику по топикам:

Ответ ZooKeeper’а весьма интересный: у топика ‘dates’ есть два раздела — 0 и 1, а их лидеры — брокеры 3 и 1 соответственно. Лидер раздела — это брокер, через который походит чтение и запись, и который отвечает за синхронизацию его реплик. В нашем случае реплики разделов хранятся у брокеров 3,1,2 и 1,2,3, и все они синхронизированы (Isr — in-sync replica), то есть все резервные копии на месте.

Так как обычно я пишу эти посты ближе к часу ночи, то будет справедливо, если кластер пострадает вместе со мной.

Издеваемся над Kafka кластером

Когда все брокеры в кластере работают, кластеру, разумеется, хорошо. Что будет, если какого-то из брокеров не станет? Выясняем ID контейнера kafka2 (docker-compose ведь изменил имя) через  docker ps , останавливаем его, и смотрим, что получилось:

Что здорово, consumer-контейнер как писал в консоль свои сообщения, так и продолжил без паузы. Но если посмотреть статистику топика, то кластер изменился:

Лидеры разделов остались теми же, но реплика с номером 2 пропала.

А что будет, если ещё кого-нибудь прибить?

Если остановить kafka3, то с потоком сообщений всё останется в порядке, но лидер нулевого раздела поменяется:

Теперь оставшийся брокер kafka1 — лидер всего. Реплика 3, разумеется, пропала.

Наконец, что будет, если снова вернуть kafka2 и kafka3 в строй?

ZooKeeper их заметил. Лидер разделов остался старым, но пропущенные реплики пересоздались и уютно расположились в «новых» брокерах. Кластер вернулся в сбалансированное состояние.

Мораль

Только что произошло немного чёрной уличной магии. Мы создали кластер из трёх хостов с фрагментированным топиком, а затем убили два хоста из трёх, и это никак не повлияло на нашу возможность публиковать и получать сообщения. Когда хосты вернулись назад, кластер перегруппировался и продолжил работать, как ни в чём не бывало.

Сегодняшный пример получился намного сложнее, чем предыдущий, но вся сложность пришла от Докера. Единственное изменение в конфигурации Кафка-сервера по сравнению с кластером из одного хоста — добавился broker id. То есть изменив всего один численный параметр мы можем добавлять сервера в кластер пачками. Невероятно.

Отказоустойчивый Kafka кластер в Docker: 8 комментариев

  1. Павел, спасибо за интересный пост!
    Попробуйте убрать broker.id вообще из конфигов. Я случайно наткнулся на то, что kafka будет их генерировать начиная с максимального и, если следующий брокер увидит занятый идентификатор в зукипере, то будет его уменьшать на -1 до свободного.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *