Zookeeper или как живётся работнику зоопарка - 1

Введение

Приложения в Java часто имеют различные конфигурации. Например, адрес и порт подключения. Например, это могло бы выглядеть следующим образом, если бы использовали класс Properties:
public static void main(String []args) {
	Properties props = new Properties();
	props.setProperty("host", "www.tutorialspoint.com");
	System.out.println("Hello, " + props.getProperty("host"));
}
И этого вроде достаточно, т.к. мы можем Properties получить из файла. И вроде у нас всё на одной машине уживается хорошо. Но представте, что наша система начинает состоять из разных систем, которые разделены между собой? Такая система ещё называется распределённой (Distributed Systems). В википедии можно найти следующее определение: Распределённые системы - это системы, компоненты который расположены на разных сетевых компьютерах, которые общаются между собой и координируют свои действия обмениваясь друг с другом сообщениями. Можно взглянуть на следующую схему:
Zookeeper или как живётся работнику зоопарка - 2
При таком подходе единая система разделена на компоненты. Конфигурирование — отдельный общий компонент. Каждый из других компонентов выступает в роли клиента для компонента конфигурирования. Такой случай называется "Распределённая конфигурация". Существует множество различных реализаций распределённой конфигурации. И в сегодняшем обзоре предлагаю познакомиться с одной из них, которая называется Zookeeper.
Zookeeper или как живётся работнику зоопарка - 3

Zookeeper

Путь к знакомству с Zookeeper начинается с их официального сайта: zookeeper.apache.org На официальном сайте необходимо перейти в раздел "Download". В этом разделе скачиваем архив в формате .tar.gz, например "zookeeper-3.4.13.tar.gz". tar - это формат архива, традиционный для Unit систем. gz - означает, что для сжатия архива используется gzip. Если мы работаем на Windows машине, то нас это не должно смущать. Большинство современных архиваторов (например, 7-zip), прекрасно умеют с ними работать и на Windows. Извлечём содержимое в какой-нибудь каталог. Заодно увидим разницу - на диске в извлечённом состоянии оно будет занимать примерно 60 мегабайт, а скачивали мы архив размером около 35 мегабайт. Как видно, сжатие действительно работает. Теперь нужно запустить Zookeeper. Вообще, Zookeeper является своего рода сервером. Zookeeper может быть запущен в одном из двух режимов: Standalone или Replicated. Рассмотрим самый простой вариант, он же первый вариант — Standalone mode. Чтобы Zookeper запустился, ему нужен конфигурационный файл. Поэтому, создадим его тут: [КаталогРаспаковкиZookeeper]/conf/zoo.cfg. Для Windows воспользуемся рекомендацией из Medium : "Installing Apache ZooKeeper on Windows". Содержание конфигурационного файла будет примерно следующим:
tickTime=2000
dataDir=C:/zookeeper-3.4.13/data
clientPort=2181
Добавим переменную среды окружения ZOOKEEPER_HOME, содержащую путь к корневому каталогу zookeper (как в инструкции на medium), а так же добавим в переменную среды окружения PATH следующий фрагмент: ;%ZOOKEEPER_HOME%\bin; Так же каталог, указанный в dataDir, должен существовать, иначе Zookeeper не сможет запустить сервер. Теперь мы можем смело запускать сервер при помощи команды: zkServer. Благодаря тому, что каталог Zookeeper был добавлен в переменную среды окружения path мы можем вызывать команды Zookeper откуда угодно, а не только из каталога bin.
Zookeeper или как живётся работнику зоопарка - 4

ZNode

Как сказано в "Zookeeper Overview", данные в Zookeper представлены в виде ZNode (узлов), которые объединены в древовидную структуру. То есть каждый ZNode может содержать данные и иметь дочерние ZNode. Подробнее про организацию ZNode можно прочитать в документации Zookeeper: "Data model and the hierarchical namespace". Для работы с Zookeeper и ZNode воспользуемся Zookeeper CLI (Command Line interface - интерфейс командной строки). Ранее мы запустили сервер при помощи команды zkServer. Теперь, для подключения выполним zkCli.cmd -server 127.0.0.1:2181 При успешном выполнении будет создана сессия подключения к Zookeeper и мы увидим примерно следующий вывод:
Zookeeper или как живётся работнику зоопарка - 5
Интересно, что даже сразу после установки Zookeeper уже имеет ZNode. Имеет он следующий путь: /zookeeper/quota
Zookeeper или как живётся работнику зоопарка - 6
Это так называемые "квоты". Как сказано в "Apache ZooKeeper Essentials", каждый ZNode может иметь ассоциированную с ним квоту, ограничивающую хранимые данные. Может быть указано ограничение по количеству znode и на объём хранимых данных. При этом если это ограничение превышено, то операции с ZNode не отменяется, но будет получено предупреждение о превышении лимита. Про ZNode рекомендуется прочитать в "ZooKeeper Programmer's Guide : ZNodes". Несколько примеров от туда, как можно работать с ZNode:
Zookeeper или как живётся работнику зоопарка - 7
Хочется отметить также, что ZNode бывают разные. Обычные ZNode (если не указать дополнительные флаги) являются тип "persistent". Есть ZNode типа "Ephemeral Node". Такие ZNode существуют только на время существование сессии подключения к Zookeeper, в рамках которой они создавались. Есть ZNode типа "Sequence Node". К таким ZNode добавляется номер из последовательности, чтобы гарантировать уникальность. Sequence Node могут быть как persistent, так и ephemeral. Про ZNode так же рекомендуется небольшая справочная информация тут: "Zookeeper ZNodes – Characteristics & Example".
Zookeeper или как живётся работнику зоопарка - 8

ZNode Watcher

Хотелось бы ещё поговорить про наблюдателей (watchers). Подробно про них написано в документации Zookeeper'а: "ZooKeeper Watches". Если кратко, то вотчер — это такой одноразовый триггер, который срабатывает на некоторое событие. Получая данные, выполняя операции getData(), getChildren() или exists() мы можем создать триггер как дополнительное действие. Zookeeper обеспечивает порядок обработки event. Кроме того, в документации указано, что прежде чем мы сможем увидеть новое значение ZNode мы увидим event об изменении старого значения на новое. Подробнее про Watcher'ов можно прочитать здесь: "ZooKeeper Watches – Features & Guarantees". Для того, чтобы это попробовать, снова воспользуемся CLI: Предположим, у нас есть некоторый ZNode со значением, где мы храним статус некоторого сервиса:
[zk: 127.0.0.1:2181(CONNECTED) 0] create /services/service1/status stopped
Created /services/service1/status
[zk: 127.0.0.1:2181(CONNECTED) 1] get /services/service1/status [watch]
stopped
Теперь, если данные в /services/service1/status изменятся, то отработает наш одноразовый триггер:
Zookeeper или как живётся работнику зоопарка - 9
Интересно, что при подключении к Zookeeper'у мы так же видим, как отрабатывает вотчер:
WATCHER::
WatchedEvent state:SyncConnected type:None path:null
SyncConnected является одним из возможных событий Zookeper. Подробнее про него можно посмотреть в описании API.
Zookeeper или как живётся работнику зоопарка - 10

Zookeeper и Java

Теперь у нас есть некоторое базовое представление о том, что может Zookeeper. Давайте теперь с ним поработаем через Java, а не через CLI. И для этого нам понадобится Java приложение, на котором мы увидим, как же с Zookeeper'ом работать. Для создания приложения воспользуемся системой сборки проектов Gradle. При помощи "Gradle Build Init plugin" выполним создание проекта. Для этого выполним команду: gradle init --type java-application В случае, если Gradle нас будет спрашивать уточняющие вопросы, то оставим значения по умолчанию (просто нажимаем Enter). Теперь откроем билд скрипт, т.е. файл build.gradle. В нём описание того, из чего устроен наш проект и от каких артефактов(библиотек, фрэймворков) зависит. Т.к. мы хотим использовать Zookeeper, то надо добавить его. Поэтому, добавим в блок dependencies зависимость от Zookeeper'а:
dependencies {
    implementation 'org.apache.zookeeper:zookeeper:3.4.13'
Подробнее про Gradle можно прочитать в обзоре: "Краткое знакомство с Gradle". Итак, у нас есть Java проект, к нему мы подключили библиотеку Zookeeper'а. Давайте теперь что-нибудь напишем. Как мы помним, при помощи CLI мы подключались примерно так: zkCli.cmd -server 127.0.0.1:2181 Давайте в классе App в main методе объявим атрибут "сервер":
String server = "127.0.0.1:2181";
Подключение — действие не моментальное. Нам придётся как-то в главное потоке выполнения программы ждать, когда произойдёт подключение. Поэтому, нам понадобится лок. Объявим его ниже:
Object lock = new Object();
Теперь нам нужен кто-то, кто скажет, что подключение установлено. Как мы помним, когда мы это делали через CLI, то у нас срабатывал вотчер. Так вот в Java коде всё точно так же. Наш вотчер будет выводить сообщение об успешном выполнении и уведомлять об этом всех ждущих через лок. Напишем вотчер:
Watcher connectionWatcher = new Watcher() {
	public void process(WatchedEvent we) {
		if (we.getState() == Event.KeeperState.SyncConnected) {
			System.out.println("Connected to Zookeeper in " + Thread.currentThread().getName());
			synchronized (lock) {
            	lock.notifyAll();
            }
		}
	}
};
Теперь допишем подключение к серверу zooKeeper'а:
int sessionTimeout = 2000;
ZooKeeper zooKeeper = null;
synchronized (lock) {
	zooKeeper = new ZooKeeper(server, sessionTimeout, connectionWatcher);
	lock.wait();
}
Здесь всё просто. При выполнении main метода в главном потоке программы мы захватываем lock и запрашиваем подключение к zookeeper'у. При этом мы отпускаем лок, и ожидаем, пока кто-то другой не захватит лок и не уведомит нас, что можно продолжать. Когда подключение будет установлено, то сработает вотчер. Он проверит, что пришло событие - SyncConnected (как мы помним, именно его ловил вотчер через CLI), и тогда напишет сообщение. Далее мы захватываем lock (т.к. ранее главный поток его отпустил) и уведомляем все ждущие lock потоки, что можно продолжать. Поток обработки события выходит из synchronized блока, тем самым освобождая lock. Главный поток получил уведомление и дождавшись освобождение lock продолжает выполнение, т.к. пока не получит lock, то он не сможет выйти из synchronized блока и продолжить работу. Таким образом, используя многопоточность и Zookeeper API мы можем выполнять различные действия. Zookeeper API гораздо шире, чем позволяет использовать CLI. Например:
// Создание нового узла
String znodePath = "/zookeepernode2";
List<ACL> acls = ZooDefs.Ids.OPEN_ACL_UNSAFE;
if (zooKeeper.exists(znodePath, false) == null) {
	zooKeeper.create(znodePath, "data".getBytes(), acls, CreateMode.PERSISTENT);
}

// Получение данных из узла
byte[] data = zooKeeper.getData(znodePath, null, null);
System.out.println("Result: " + new String(data, "UTF-8"));
Как видно, при создании узла мы можем настроить ACL. Это ещё одна важная особенность. ACL - это разрешения, которые распостраняются на действия с ZNode. Настроек много, поэтому за подробностями рекомендую обратиться к официальной документации: "Zookeeper ACL Permissions".
Zookeeper или как живётся работнику зоопарка - 11

Заключение

Зачем мы это прочитали? Потому что Zookeeper используется и в других востребованных технологиях. Например, Apache Kafka требует Zookeeper, о чём можно прочитать в "Kafka Quick Start Guide". Так же используется в NOSQL базе данных HBase, о чём можно подробнее прочитать в их "HBase Quickstart Guide". На самом деле, множество других проектов используют Zookeeper. Часть из них приведена в списке в "Использование Zookeeper в реальном мире". Надеюсь, на вопрос "зачем" я ответил. Самый главный вопрос теперь: "Что дальше?". Во-первых, по теме Apache Zookeeper можно почитать следующие книги: Во-вторых, есть отличные видео доклады про Zookeeper. Рекомендуются к просмотру: В-третьих, есть несколько полезных статей, которые дополнят картину мира: Вышел совсем небольшой обзор, но как вводное слово, надеюсь, будет полезным. #Viacheslav