Несмотря на наличие работающего решения немалой части распределенных проблем о нем мало пишут и создается впечатление, что это что-то устаревшее и не заслуживающее внимания.
Это не так. Начинать новый проект с Зукипером или встраивать его в существующий проект в 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 Не пользовался, ничего сказать не могу.
Клиенты для всех распространенных языков тут
И официальный сайт напоследок https://zookeeper.apache.org/