Сервис Apache ZooKeeper применяется в качестве сервера метаданных и конфигураций сервисов и часто используется для реализации механизма поиска сервисов (Service Discovery). Например, в Apache Kafka, популярном инструменте для организации обмена сообщениями в распределенных системах, до версии 4.0 ZooKeeper может использоваться в качестве хранилища метаданных кластера. Как и любой другой компонент распределённой системы ZooKeeper желательно запускать в кластере для обеспечения отказоустойчивости системы, чему посвящена данная статья.
Я преднамеренно вынес тему запуска ZooKeeper в отдельную статью, так как из Apache Kafka поддержка ZooKeeper будет удалена с релизом версии 4.0, но ZooKeeper может использоваться и в других ваших проектах.
Установка и запуск ZooKeeper
Для начала работы с ZooKeeper нужно скачать дистрибутив с официального сайта проекта. Скачайте и распакуйте архив с ZooKeeper в любую удобную для вас директорию. Для запуска достаточно скопировать шаблон файла настроек conf/zoo_sample.cfg
в conf/zoo.cfg
и выполнить следующую команду:
1 2 3 4 |
$ bin/zkServer.sh start ZooKeeper JMX enabled by default Using config: /home/akosarev/opt/apache-zookeeper-3.8.4-bin/bin/../conf/zoo.cfg Starting zookeeper ... STARTED |
По умолчанию в ZooKeeper включён интерфейс администратора, и после запуска вы можете открыть в браузере страницу http://localhost:8080/commands
для доступа к командам.
Если посмотреть на файл конфигурации, то он достаточно простой:
1 2 3 4 5 6 7 8 |
# Параметры синхронизации tickTime=2000 initLimit=10 syncLimit=5 # Директория данных ZooKeeper dataDir=/tmp/zookeeper # Порт для подключения клиентов clientPort=2181 |
Локальный кластер ZooKeeper
Теперь мы можем попробовать запустить локальный кластер из трёх экземпляров ZooKeeper. Для этого нужно выполнить следующие шаги:
- Создать директорию данных для каждого сервиса
- В директории данных каждого сервиса создать файл
myid
с уникальным идентификатором сервиса (от 0 до 255) - Создать копию директории
conf
для каждого сервиса и добавить вzoo.cfg
список сервисов, входящих в кластер
Описание сервиса выглядит следующим образом: server.{id}={host}:{quorum_port}:{leader_election_port}{;client_port}
, ниже приведён для наглядности файл настроек первого сервиса:
conf/i1/zoo.cfg
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# Параметры синхронизации tickTime=2000 initLimit=10 syncLimit=5 # Директория данных ZooKeeper dataDir=/tmp/zookeeper # Порт для подключения клиентов clientPort=2181 # Отключим страницу администратора, чтобы не занимать порт 8080 admin.enableServer=false # Включение сервисных команды stat и srvr 4lw.commands.whitelist=stat,srvr # Участники кворума server.1=localhost:2888:3888 server.2=localhost:12888:13888 server.3=localhost:22888:23888 |
Порт для клиентских соединений можно указывать как в свойстве clientPort
, так и в конце server
, отделяя его при помощи ;
. Сервис использует запись server
со своим идентификатором для определения, какие порты он должен прослушивать для обмена данными кворума (2888) и выбора лидера (3888). Остальные записи server
используются для взаимодействия с другими сервисами.
Конечно же, узлы кластера лучше разворачивать на отдельных физических или виртуальных серверах, а в последнем случае желательно, чтобы виртуальные серверы были расположены на разных физических.
Файлы настроек двух остальных сервисов будут аналогичны:
conf/i2/zoo.cfg
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# Параметры синхронизации tickTime=2000 initLimit=10 syncLimit=5 # Директория данных ZooKeeper dataDir=/tmp/zookeeper-2 # Порт для подключения клиентов clientPort=12181 # Отключим страницу администратора, чтобы не занимать порт 8080 admin.enableServer=false # Включение сервисных команды stat и srvr 4lw.commands.whitelist=stat,srvr # Участники кворума server.1=localhost:2888:3888 server.2=localhost:12888:13888 server.3=localhost:22888:23888 |
conf/i3/zoo.cfg
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# Параметры синхронизации tickTime=2000 initLimit=10 syncLimit=5 # Директория данных ZooKeeper dataDir=/tmp/zookeeper-3 # Порт для подключения клиентов clientPort=22181 # Отключим страницу администратора, чтобы не занимать порт 8080 admin.enableServer=false # Включение сервисных команды stat и srvr 4lw.commands.whitelist=stat,srvr # Участники кворума server.1=localhost:2888:3888 server.2=localhost:12888:13888 server.3=localhost:22888:23888 |
Теперь нужно запустить все три экземпляра командами:
1 2 3 4 5 |
$ bin/zkServer.sh --config conf/i1 start $ bin/zkServer.sh --config conf/i2 start $ bin/zkServer.sh --config conf/i3 start |
Теперь можно проверить состояние участников кворума, отправив команды stat
или srvr
при помощи nc
или telnet
:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
$ echo stat | nc localhost 22181 Zookeeper version: 3.8.4-9316c2a7a97e1666d8f4593f34dd6fc36ecc436c, built on 2024-02-12 22:16 UTC Clients: /127.0.0.1:54518[0](queued=0,recved=1,sent=0) Latency min/avg/max: 0/0.0/0 Received: 4 Sent: 3 Connections: 1 Outstanding: 0 Zxid: 0x20000000a Mode: follower Node count: 39 |
Сервис, являющийся лидером кворума, в Mode будет иметь значение leader
, остальные — follower
.
Кластер ZooKeeper в Docker
Для запуска ZooKeeper в контейнерах доступен официальный образ zookeeper
. Настроить сервис можно как файлом свойств, который должен быть смонтирован в /conf/zoo.cfg
, так и при помощи переменных окружения.
Переменные окружения начинаются с префикса ZOO_
и повторяют свойства из файла свойств в верхнем регистре и знаком _
для разделения слов, например 4lw.commands.whitelist
соответствует переменная окружения ZOO_4LW_COMMANDS_WHITELIST
. Идентификатор сервиса указывается при помощи переменной ZOO_MY_ID
, а список сервисов — при помощи ZOO_SERVERS
.
Пример запуска кворума ZooKeeper из трёх экземпляров в Docker:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
$ docker run -d -p 2181:2181 -p 2888:2888 -p 3888:3888 \ -e ZOO_MY_ID=1 \ -e ZOO_SERVERS='server.1=0.0.0.0:2888:3888;2181 server.2=172.17.0.1:12888:13888 server.3=172.17.0.1:22888:23888' \ -e ZOO_4LW_COMMANDS_WHITELIST=srvr,stat \ zookeeper $ docker run -d -p 12181:2181 -p 12888:2888 -p 13888:3888 \ -e ZOO_MY_ID=2 \ -e ZOO_SERVERS='server.1=172.17.0.1:2888:3888 server.2=0.0.0.0:2888:3888;2181 server.3=172.17.0.1:22888:23888' \ -e ZOO_4LW_COMMANDS_WHITELIST=srvr,stat \ zookeeper $ docker run -d -p 22181:2181 -p 22888:2888 -p 23888:3888 \ -e ZOO_MY_ID=3 \ -e ZOO_SERVERS='server.1=172.17.0.1:2888:3888 server.2=172.17.0.1:12888:13888 server.3=0.0.0.0:2888:3888;2181' \ -e ZOO_4LW_COMMANDS_WHITELIST=srvr,stat \ zookeeper |
Для межсервисного взаимодействия я использовал в этом примере ip-адрес хост-системы 172.17.0.1
, если у вас он отличается, то замените адрес на свой.
В Docker Compose кворум ZooKeeper будет выглядеть следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
services: zk1: image: zookeeper hostname: zk1 ports: - "2181:2181" environment: ZOO_MY_ID: 1 ZOO_SERVERS: server.1=zk1:2888:3888;2181 server.2=zk2:2888:3888 server.3=zk3:2888:3888 ZOO_4LW_COMMANDS_WHITELIST: srvr,stat zk2: image: zookeeper hostname: zk2 ports: - "12181:2181" environment: ZOO_MY_ID: 2 ZOO_SERVERS: server.1=zk1:2888:3888 server.2=zk2:2888:3888;2181 server.3=zk3:2888:3888 ZOO_4LW_COMMANDS_WHITELIST: srvr,stat zk3: image: zookeeper hostname: zk3 ports: - "22181:2181" environment: ZOO_MY_ID: 3 ZOO_SERVERS: server.1=zk1:2888:3888 server.2=zk2:2888:3888 server.3=zk3:2888:3888;2181 ZOO_4LW_COMMANDS_WHITELIST: srvr,stat |