Несмотря на наличие работающего решения немалой части распределенных проблем о нем мало пишут и создается впечатление, что это что-то устаревшее и не заслуживающее внимания.

Это не так. Начинать новый проект с Зукипером или встраивать его в существующий проект в 2021 году можно и нужно.

Зукипер просто работает

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

Производительность

Зукипер держит большой RPS. О производительности, как правило, можно не думать. С большой вероятностью ее вам хватит для любого разумного применения.

Оптимальная конфигурация для любых разумных применений это 3 средние ноды. Постарайтесь расположить эти ноды так чтобы все три вместе упасть никак не могли. Разные датацентры - идеальное расположение. Конфигурация серверов на картинке "dual 2Ghz Xeon and two SATA 15K RPM drive".

Зукипер это дерево

Вы можете легко на одном кластере держать все ваши микросерсивисы и операции. Просто аккуратно разложите их по разным поддеревьям. Об этом лучше подумать сразу и организовать хранение так что любой сервис живет только в своем поддереве.

Конкретные примеры использования Зукипера

Все примеры написанны с помощью Apache Curator Framework. Большая часть взята прямо с https://curator.apache.org/curator-recipes/index.html

Код всех примеров подразумевает что вы его запускаете на нескольких нодах. Минимум две ноды, практика говорит что три ноды надежнее.

Выбор мастера

Иногда встречаются master-slave системы. В них есть 2-3 ноды. Одна из них мастер и работает, остальные ждут пока мастер станет недоступен. При недоступности мастера проходят выборы и одна из slave нод становится новым мастером. Шардирование обычно лучше, но иногда оно просто не нужно. Одного работающего мастера хватает на все про все с запасом.

LeaderSelector leaderSelector = new LeaderSelector(client, "/someservice/leaderElection",
        new LeaderSelectorListenerAdapter() {
            @Override
            public void takeLeadership(CuratorFramework curatorFramework) throws Exception {
                System.out.println("I'm master. Start working");
                doWorkInThisThread();
            }
        });
leaderSelector.start();

Очередь

Отлично подходит для случая когда вам нужна распределенная отказоустойчивая очередь, но использование полноценных решений вроде Кафки выглядит оверкилом. Например, у вас немного данных в очереди и поток событий небольшой.

QueueBuilder<Data> queueBuilder = QueueBuilder.builder(client, new QueueConsumer<>() {
            @Override
            public void consumeMessage(Data message) throws Exception {
                System.out.println(message.i);
            }

            @Override
            public void stateChanged(CuratorFramework curatorFramework, ConnectionState connectionState) {
                System.out.println("State changed. New state: " + connectionState.name());
            }
        }, new QueueSerializer<>() {
            @Override
            public byte[] serialize(Data item) {
                return new byte[]{item.i};
            }

            @Override
            public Data deserialize(byte[] bytes) {
                return new Data(bytes[0]);
            }
        },
        "/someService/someQueue");

DistributedQueue<Data> queue = queueBuilder.buildQueue();
queue.start();
queue.put(new Data((byte) 1));

И простейшие данные для примера

public static class Data {
    byte i;

    public Data(byte i) {
        this.i = i;
    }
}

Распределенные семафоры

К вам пришли из соседней команды и поругались на пиковую нагрузку от вас. И вы теперь не хотите со всех 100 ваших нод одновременно ходить в соседний сервис за данными, которые вам нужны не очень срочно. А хотите ходить не более чем с 10 нод одновременно.

InterProcessSemaphoreV2 semaphore = new InterProcessSemaphoreV2(client, "/someService/semaphoreToSmallExternalService", 10);
Lease lease = semaphore.acquire();
try {
    callToExternalService();
} finally {
    semaphore.returnLease(lease);
}

Метаинформация

Вам надо хранить метаинформацию о каких-то ваших объектах. Чтобы она была доступна другим инстансам вашего сервиса. Допустим информацию о пачке данных которую вы сейчас обрабатываете. Записи много, чтения много, данных не очень много. Обычные SQL БД такой паттерн нагрузки не любят.

Просто запишите в Зукипер. И используйте в любой админке для показа, управления или любых других действий. Иметь возможность наблюдать за распределенной обработкой это очень хорошая практика. Без наблюдения системы иногда переходят в непонятное состояние, куда бежать смотреть что где происходит непонятно.

PersistentNode node = new PersistentNode(client,
        CreateMode.EPHEMERAL,
        false,
        "/someService/serviceNodeGroup/" + data.hashCode(),
        "any node data you need".getBytes());
node.start();
try {
    anyDataProcessing();
}

Распределенный счетчик

Регулярно бывает нужна самая обычная последовательность интов с автоинкрементом. Сиквенсы из БД по какой-либо причине не подходят. И как обычно есть кучка инстансов вашего сервиса, которые должны быть согласованы.

Например, простой счетчик вызовов внешнего сервиса нужный для мониторинга и отчетов. Графана такие счетчики хорошо рисует на графиках и по ним можно наблюдать за активность использования внешнего сервиса вами. Сиквенс из БД не очень хорошо подходит, а счетчик хочется. Как обычно, просто возьмите Зукипер.

DistributedAtomicLong externalServiceCallCount = new DistributedAtomicLong(client, "/someService/externalServiceCallCounter", new RetryOneTime(1));
externalServiceCallCount.increment();

//В любом другом сервисе читаем
DistributedAtomicLong externalServiceCallCount = new DistributedAtomicLong(client, "/someService/externalServiceCallCounter", new RetryOneTime(1));
externalServiceCallCount.get();

Конфиги

В Зукипере можно хранить ваши конфиги.

Минусы: Конфиги сложно наблюдаемы и нетривиально редактируемы.

Плюсы: Ваше приложение подписывается на изменение и получает новые значения без рестарта. И, как обычно, никакого специального кода для этого писать не нужно.

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

Пример подписки на события изменения данных:

CuratorCache config = CuratorCache.builder(client, "someService/configuration").build();
config.start();

config.listenable().addListener((type, oldData, data1) -> {
    updateApplicationProperties(...);
});

Транзакции

При построении конвейера обработки данных хочется иметь возможность обрабатывать данные транзакционно. В идеале exactly once. И как обычно писать сложный код не хочется. Такие вещи сложно отлаживать и поддерживать. Да и баги в них постоянно встречаются.

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

byte[] readedData = client.getData().forPath("/someService/collection1/data1");
byte[] data2 = processData();
CuratorOp createOp = client.transactionOp().create().forPath("/someService/collection2/data2", data2);
CuratorOp deleteOp = client.transactionOp().delete().forPath("/someService/collection1/data1");

Collection<CuratorTransactionResult> results = client.transaction().forOperations(createOp, deleteOp);

for ( CuratorTransactionResult result : results )
{
    System.out.println(result.getForPath() + " - " + result.getType());
}

Стоит следить за записываемыми в сторонние БД данными. Если processData() из примера что-то куда-то пишет, то это что-то должно быть удалено даже при откате транзакции Зукипера. Базы с поддержкой TTL зарекомендовали себя лучше всего. Данные удалят сами себя. Если у вас не такая, то нужно придумать как-то другой механизм для очистки неконсистентных данных.

Мониторинг, как обычно, обязателен. По TTL можно случайно удалить нужные данные, стоит это мониторить и избегать такого.

Особенности использования Зукипера

У зукипера есть не только плюсы. Есть и особенности о которых надо знать перед как вводить его в продакшен системы.

Зукипер не риалтайм

Можно прочитать не то что записали. Не прочитать только что записанные данные это абсолютно нормальная ситуация. Системы надо строить с учетом этого.

Если очень надо, то можно попробовать записать в ту же ноду что-то. При провале этого действия мы будем точно знать что нода существует, несмотря на то что она не прочиталась. И можно попробовать снова ее прочитать через небольшое время. Disclamer: Так не стоит делать, это один из рецептов на крайний случай. Когда код уже в проде и надо срочно доделать чтобы работало.

Зукипер не база данных

Зукипер хорошо работает с базой размером в единицы гигабайт. Не надо в нем хранить ваши данные. Храните их в БД, или в S3, или в любом другом предназначенном для хранения данных месте которое вам нравится. А в Зукипер пишите метаинформацию и указатель на ваши данные.

Разумный предел для одного значения - 1 килобайт. Запас из документации до мегабайта лучше оставить на экстренные случаи.

Зукипер не самое лучшее kv хранилище

Зукипер можно использовать в роли kv хранилища. Обычно это горячий кеш.

Но лучше посмотреть в сторону более специализированного софта. Redis/Tarantul удобнее для использования в этой роли и более эффективно утилизируют железо при чистой kv нагрузке.

zxcid

У Зукипера есть архитектурная проблема - zxcid.

zxcid это внутренний 32 битный счетчик операций Зукипера. Когда он переполняется кластер разваливается на время единиц секунд до десятков минут. Надо быть к этому готовым и мониторить текущее значение zxcid. Хорошее решение будет в версии 3.8.0 https://issues.apache.org/jira/browse/ZOOKEEPER-2789 Ждем, верим, надеемся.

Переходить на новую версию сразу после ее выхода не стоит. Выждите хотя бы квартал.

Забытые данные

В древовидной структуре можно легко насоздавать сотни тысяч и даже миллионы нод в далеком и заброшенном узле дерева. И забыть их удалить. Чтобы этого избегать стоит писать код без багов(шутка) и мониторить размер базы Зукипера и общее число нод в нем. Если эти цифры начали подозрительно расти, то стоит что-то с этим сделать.

Софт изначально стоит проектировать так что любая созданная нода точно удалится.

Никогда неудаляемые ноды (например конфиг) стоит создавать очень аккуратно и ни в коем случае не массово.

Ноды со сложным жизненным циклом стоит покрыть отдельными мониторингами.

Например: одно приложение создает неудаляемую автоматически ноду, а второе ее читает обрабатывает и удаляет потом. Стоит сделать мониторинг на общее количество и на самую старую ноду. Тогда в случае любых проблем вы сразу это увидите.

Типовые удобства 2021 года

Все, как полагается.

Докер образ тут

WEB-UI чтобы быстренько что-то посмотреть или поправить пару значений есть на любой вкус. Можно выбрать вот отсюда или просто из Гугла по своему вкусу. Мне нравится старенький и похоже что мертвый zk-web, но это дело вкуса. Поставить любой UI очень рекомендую. Они помогают решить множество мелких и регулярных проблем.

ZooKeeper в Амазоне тут https://docs.aws.amazon.com/emr/latest/ReleaseGuide/emr-zookeeper.html Не пользовался, ничего сказать не могу.

Клиенты для всех распространенных языков тут

Stackoverflow

Maven

И официальный сайт напоследок https://zookeeper.apache.org/