В наш век распределенных микросервисов постоянно возникают распределеные проблемы. Это самые обычные computer science проблемы, но они распределены по разным серверам или еще хуже по разным датацентрам. Для их решения часто выдумывают много разного. Не делайте так, просто возьмите Зукипер. Зукипер, несмотря на то что придумат бумерами, просто работает. Он решит немалую часть ваших проблем с распределенными системами.

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

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

<img src="https://zookeeper.apache.org/doc/r3.5.4-beta/images/zkperfRW-3.2.jpg" alt="Zookeper performance" align="left" />

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

Конкретные юзкейсы Все примеры написанны с помощью Apache Curatur 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 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]);
                }
            },
            "/myservice/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();
    }
    node.close();

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

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

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

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

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

Пример подписки на события изменения данных: CuratorCache config = CuratorCache.builder(client, "someService/configuration").build(); config.start();

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

У зукипера есть не только плюсы. Есть и особенности о которых надо знать. Зукипер не риалтайм. Можно прочитать не то что записали. Не прочитать только что записанные данные это абсолютно нормальная ситуация. Системы надо строить с учетом этого. Если очень надо, то можно попробовать записать в ту же ноду что-то. При провале этого действия мы будем точно знать что нода сущесвует, несмотря на то что не прочиталась. И можно попробовать снова ее прочитать через небольшое время.

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

У Зукипера есть архитектурная проблема - zxcid zxcid это внутренний 32 битный счетчки операций Зукипера. Когда он переполняется кластер разваливается на время единиц секунд до десятков минут. Надо быть к этому готовым и мониторить текущее значение zxcid. Хорошее решение будет в версии 3.8.0 https://issues.apache.org/jira/browse/ZOOKEEPER-2789?page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel&focusedCommentId=16282229#comment-16282229 Ждем, верим, надеемся.

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

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