Apache Zookeeper, наверное, один из самых забавных и сложных распределенных фреймворков. Обычно он используется в качестве посредника для синхронизации распределенных серверов. Самостоятельная реализация синхронизации может привести к нескольким состояниям гонки. Итак, разработчики используют Zookeeper в различных системах, даже не задумываясь о какой-либо альтернативе. Широкое распространение свидетельствует о его надежности и производительности.

Затем возникает вопрос: как Zookeeper удается синхронизировать свои серверы. Вы не можете использовать родительский зоопарк для управления дочерним зоопарком, создавая таким образом начальный сценарий без неопределенности.

Надеюсь, эта статья попытается ответить на вышеуказанный вопрос.

Лидер и последователи

На серверах Zookeeper всегда есть один руководитель, который управляет всеми данными. Все записи сначала происходят на лидере, а затем синхронизируются с подписчиками. Лидер помогает определять власть над правильным государством за один раз и обеспечивает единую точку координации.

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

Чтобы синхронизировать последователей и лидеров, Zookeeper использует протокол ZAB.

ZAB

Zookeeper Leader синхронизирует всех подписчиков, используя собственный протокол под названием ZAB (Zookeeper Atomic Broadcast Protocol). Это специальный протокол, разработанный для Zookeeper для решения таких задач, как поддержание причинного порядка в совместно используемых данных. Все изменения состояния (известные как транзакции) в ZAB идемпотентны и инкрементальны. Таким образом, применение изменения состояния несколько раз дает один и тот же результат. Однако необходимо поддерживать порядок изменений.
Транзакция в ZAB идентифицируется уникальным идентификатором zxid, который представляет собой 64-битное целое число, состоящее из двух 32-битных частей:

  • Эпоха - целое число, которое увеличивается каждый раз при выборах лидера.
  • Счетчик - целочисленный счетчик, который увеличивается после каждой действительной транзакции.

Протокол ZAB состоит из трех этапов -

1. Обнаружение - определите, кто является лидером и сколько данных отсутствует.

2. Синхронизация - синхронизируйте все серверы, чтобы обновить недостающие данные.

3. Трансляция - начать передачу транзакции в реальном времени.

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

ZAB предоставляет 3 необходимых гарантии, а именно:

  • Целостность - если процесс получает транзакцию с zxid Z, чем какой-либо другой процесс транслирует транзакцию с zxid Z
  • Общий заказ - если процесс доставляет транзакцию с zxid Z перед транзакцией с zxid Z ', чем любой другой процесс, который доставляет Z' также должен доставить z, и он должен поставить Z перед Z '
  • Соглашение. Если один процесс предоставляет z, а другой - Z ', то либо первый процесс должен доставить Z', либо второй процесс должен доставить Z. Это гарантирует, что состояния двух процессов не расходятся.

Эти три свойства безопасности гарантируют, что окончательное состояние всех серверов будет согласованным после того, как все транзакции будут доставлены друг другу.

Все фазы, происходящие в ZAB, очень похожи на двухэтапные коммиты. Сначала предложение отправляется лидером, ведомый на принимающей стороне отправляет ACK, а затем лидер отправляет фиксацию для завершения транзакции.

Открытие

На этом этапе лидер и ведомый решают, на каком сервере хранится истинная история транзакций, которые произошли до сих пор.
Перспективный лидер выбирается первым с помощью простого алгоритма выбора лидера.
Этот процесс включает в себя следующие обмены:

  1. Последователь отправляет предполагаемому лидеру последнюю предложенную эпоху.
  2. Лидер получает последнюю принятую эпоху от кворума последователей и отправляет новую эпоху, которая больше, чем все эпохи, которые он получил.
  3. Если новая эпоха больше, чем последняя предложенная эпоха, последователи обновляют свою предложенную эпоху и отправляют свою последнюю подтвержденную эпоху вместе со своим последним zxid лидерам.
  4. Лидер выбирает историю последователя с наивысшим zxid и эпохой как истину.

Zookeeper выполняет оптимизацию, при которой выбирает сервер с наивысшей эпохой и zxid в качестве предполагаемого лидера, чтобы у него уже были все данные, которые необходимо синхронизировать. Следующий фрагмент кода в FastLeaderElection.java в Zookeeper проверяет следующее:

/**
     * Check if a pair (server id, zxid) succeeds our
     * current vote.
     *
     * @param id    Server identifier
     * @param zxid  Last zxid observed by the issuer of this vote
     */
    protected boolean totalOrderPredicate(long newId, long newZxid, long newEpoch, long curId, long curZxid, long curEpoch) {
        LOG.debug("id: " + newId + ", proposed id: " + curId + ", zxid: 0x" +
                Long.toHexString(newZxid) + ", proposed zxid: 0x" + Long.toHexString(curZxid));
        if(self.getQuorumVerifier().getWeight(newId) == 0){
            return false;
        }

        /*
         * We return true if one of the following three cases hold:
         * 1- New epoch is higher
         * 2- New epoch is the same as current epoch, but new zxid is higher
         * 3- New epoch is the same as current epoch, new zxid is the same
         *  as current zxid, but server id is higher.
         */

        return ((newEpoch > curEpoch) ||
                ((newEpoch == curEpoch) &&
                ((newZxid > curZxid) || ((newZxid == curZxid) && (newId > curId)))));
    }

Синхронизация

На этом этапе история транзакций синхронизируется между всеми подписчиками.

  1. Перспективный лидер предлагает себя в качестве нового лидера, поскольку у него наивысший zxid и эпоха.
  2. Если последнее принятое предложение последователя относится к той же эпохе, что и новый лидер, он устанавливает его текущую эпоху как такую ​​же, отправляет ACK лидеру и начинает принимать все недостающие транзакции через вызов DIFF.
  3. После получения ACK от кворума последователей лидер отправляет сообщение фиксации и доставляет все недостающие транзакции последователям.

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

Транслировать

Это этап, который происходит после того, как кворум серверов выбрал лидера, добавил недостающие данные и готов принять новые транзакции.

  1. Лидер предлагает транзакцию с zxid выше, чем все предыдущие идентификаторы
  2. Подписчики принимают предложенную транзакцию от лидера и добавляют ее в свою историю. Сообщение ACK отправляется после записи транзакции в долговременное хранилище.
  3. Если лидер получил ACK от кворума последователей для транзакции, он отправляет сообщение фиксации.
  4. Последователи при получении сообщения о фиксации транслируют транзакции друг другу.

Каждый сервер в zookeeper выполняет одну итерацию этого протокола за раз. В случае исключения, например, эпохи, не совпадающей с лидером, серверы могут начать новую итерацию, начиная с первой фазы.

Вы можете перейти по следующим ссылкам, чтобы узнать больше о внутреннем устройстве Zookeeper:

  1. Заб: Высокопроизводительное вещание для систем первично-резервное копирование
  2. ZooKeeper Internals
  3. ZooKeeper: распределенная координация процессов