Незаслуженно забытый ZooKeeper

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

Это не так. Начинать новый проект с Зукипером или встраивать его в существующий проект в 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/

@BugM
05.07.2021 13:19 UTC
Первоисточник

Комментарии

@chemtech
05.07.2021 08:44 UTC
0

Спасибо за пост. Почему стоит использовать zookeeper, а не etcd?

@BugM
05.07.2021 09:07 UTC
0

Java api Зукипера гораздо удобнее чем у etcd. Apache Curator на самом деле великолепен.

Вероятность что Гугл решит убить etcd гораздо выше чем вероятность что Apache решит убить Зукипер.

Мигрировать на другую БД это всегда сложно, дорого и чевевато багами в старом проверенном коде. Оставаться на неподдерживаемой БД тоже плохо.

@strangeman
13.07.2021 13:21 UTC
0

etcd по моему опыту намного более чувствителен к IO и очень плохо переживает ситуации типа "одна из нод начала подтупливать", Zk в этом отношении куда стабильнее.

@PrinceKorwin
05.07.2021 09:29 UTC
+1
У ZooKeeper есть три момента которые могут мешать его использованию:
проблема №1: отсутствие аудита
проблема №2: очень непривычное поведение ACL (если вы хотите, чтобы к какой-то ноде и её чилдам не было чтения/записи, то это нужно запрещать «вручную» на всем уровне вложенности)
проблема№3: если вам не нужен анонимный доступ, то его отключение может быть сложным/невозможным

Если для вас три пункта не критичны, то ZK хороший вариант.
@sojey80135
05.07.2021 10:01 UTC
-6

"Незаслуженно забытый ZooKeeper"

заслуженно забытый и не нужный. Таже Кафка скоро от него избавиться

@buldo
05.07.2021 19:28 UTC
0

Можете подробнее? Не совсем шарю в этой теме. В статье вроде бы привели довольно полезные кейсы использования. На что обменять zookeeper и главное почему?

@ZillahGiovanni
05.07.2021 19:30 UTC
0
К сожалению «а воз и ныне там», кафка собиралась от него избавиться еще в прошлом году.
На настоящий момент обещают не раньше конца 2021, но есть нюанс…
07.07.2021 14:20 UTC
0
Гуглим KIP-500 и вырезаем этот зукипер к фигам собачим. Джава пожирающая память надоела.
07.07.2021 19:05 UTC
0
Вы точно знаете на чем написана Кафка?
07.07.2021 20:16 UTC
0

Кафку так просто не вырезать, я в курсе что она тоже на джава.

07.07.2021 21:02 UTC
0
Хорошо. А вы точно уверены что Джава пожирает память нерационально?

Допустим на примере Зукипера.
Он требует не сильно больше памяти чем размер снапшота данных. Хипа примерно +30% от размера снапшота ему хватит.
С учетом что снапшот в несколько гигабайт это очень много (больше сотни серверов точно нужно чтобы так нагрузить при минимально разумном написании кода) это не выглядит большими расходами.

Вы считаете это пожиранием?

PS: Рекомендации по хипу надо проверять на вашем паттерне нагрузки и обязательно мониторить. Не стоит просто так катить в прод без мониторинга. +30% это хорошая отправная точка.
07.07.2021 21:41 UTC
0

Когда у вас одно такое приложение, это норм. Даже десяток можно настроить. Но когда за пол сотни набегает и разрабы турбонаддувом выжирают память и не собираются указывать корректные значения памяти, потому что недостаточно квалификации, то надоедает рутина по профилированию и нагрузочному тестированию каждой поделки. Это даже не моя основная задача, а основные задачи при этом страдают. Субъективная нелюбовь к джава, просто жалоба в пустоту.

07.07.2021 21:55 UTC
0
О, спасибо. Теперь я знаю еще хороший довод зачем придуманы все эти девопсы и дежурства.

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

Написать плохо, так что софт ест слишком много железа, можно на любом языке. Тут скорее пусть лучше будет плохо на джаве написанно, чем на плюсах. Джаву переписать хорошо проще и быстрее чем плюсы.
08.07.2021 09:28 UTC
0
Если говнокодить, то не важно на чём. Вместо зоопарка языков и пофигистичного отношения к работе, хотелось бы увидеть чувство ответственности.
13.07.2021 13:22 UTC
0

Так а зукипер-то тут причем? :)

13.07.2021 13:22 UTC
0
Субъективная нелюбовь к джава, просто жалоба в пустоту.

Я выше писал.