Статьи

ZooKeeper, куратор и как работает балансировка нагрузки микросервисов

Как Zookeeper гарантирует, что каждый работник с радостью получит что-то от менеджера по делегированию работы.

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

zookeeper_logo

Однако ZooKeeper довольно низкого уровня, и даже стандартные сценарии использования требуют много строк кода. Вот почему появился Apache Curator — гораздо более дружелюбная и простая в использовании библиотека-обертка над ZooKeeper. Используя Куратор, мы можем доставить больше с меньшим количеством кода и намного более чистым способом.

curator_logo

«Гуава для Java — то же, что куратор для ZooKeeper» — Патрик Хант, коммиттер ZooKeeper

Микросервисы балансировки нагрузки с ZooKeeper

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

В мире микросервисов ситуация аналогична, но у нас есть существенное преимущество перед монолитным подходом: когда нам нужно масштабировать приложение, нам не нужно дублировать всю систему и развертывать ее на сервере, достаточно мощном для бесперебойной работы. Мы можем сосредоточиться только на небольшом модуле, который необходимо масштабировать, чтобы стоимость масштабирования была значительно ниже (как с точки зрения стоимости сервера, так и разработки, необходимой во многих случаях для подготовки модуля к развертыванию).

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

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

Простой рабочий

Давайте начнем с создания простого работника, который будет прослушивать данный порт и возвращать некоторый результат, когда его попросят выполнить свою работу. Для реализации этого крошечного микросервиса мы будем использовать Groovy, легкий контейнер сервлетов Undertow и, конечно, ZooKeeper и куратор.

Наш работник будет состоять из одного небольшого класса с основным методом, который выполняет три вещи:

1
2
3
4
5
6
7
8
class Main {
 
    static final void main(String[] args) {
        // Step 1: Extract name and port number to launch worker
        // Step 2: Configure and start Rest server on given port
        // Step 3: Register worker in ZooKeeper
    }
}

Для краткости здесь я опущу шаги 1 и 2, вы можете проверить полный исходный код проекта GitHub . У нашего работника будет только одна конечная точка GET / work, которая возвращает ответ с именем вызываемого работника:

01
02
03
04
05
06
07
08
09
10
11
12
@Path("/")
class Main {
 
    @GET
    @Path("/work")
    public String work() {
        String response = "Work done by $workerName"
        println response
        return response
    }
 
}

Шаг 3: Зарегистрировать работника в ZooKeeper — это самое интересное, поэтому я объясню это более подробно:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
private static void registerInZookeeper(int port) {
        CuratorFramework curatorFramework = CuratorFrameworkFactory.newClient("localhost:2181", new RetryNTimes(5, 1000))
    curatorFramework.start()
    ServiceInstance<Void> serviceInstance = ServiceInstance.builder()
        .uriSpec(new UriSpec("{scheme}://{address}:{port}"))
        .address('localhost')
        .port(port)
        .name("worker")
        .build()
 
    ServiceDiscoveryBuilder.builder(Void)
        .basePath("load-balancing-example")
        .client(curatorFramework)
        .thisInstance(serviceInstance)
        .build()
        .start()
}
  • Строки 2-3: мы создаем и запускаем клиент CuratorFramework, оборачивая все операции, которые мы хотим выполнить на экземпляре ZooKeeper. Для простоты мы используем localhost с портом по умолчанию (обычно это должен быть URL к работающему экземпляру ZooKeeper)
  • Строки 4-9 создают ServiceInstance, представляющий нашего работника. Мы передаем все «контактные данные», необходимые для вызова этого работника из других микросервисов
  • Строки 11-16 регистрируют наш экземпляр в ZooKeeper, представленном клиентом CuratorFramework.

Начинающий работник

Теперь Worker готов, поэтому мы можем создать толстый jar (с задачей Gradle fatJar ) и затем запустить его, используя:

1
java -jar simple-worker/build/libs/simple-worker-1.0-shadow.jar Worker_1 18005

Перед запуском работника помните, что вам нужен экземпляр ZooKeeper, работающий на порте 2181 по умолчанию!

Чтобы проверить, работает ли работник, вы должны открыть браузер по адресу http: // localhost: 18005 / work и увидеть там текст «Работа выполнена Worker_1». Чтобы убедиться, что работник правильно зарегистрировался в ZooKeeper, запустите клиент командной строки:

1
2
cd <zk_root>/bin
./zkCli.sh</zk_root>

и затем выполните команду ls, чтобы увидеть один узел, зарегистрированный в / load-balancing-example / worker path:

1
2
[zk: localhost:2181(CONNECTED) 1] ls /load-balancing-example/worker <enter>
[f69545e8-8466-40c0-93e9-f493eb7496b4]

Простой менеджер

Теперь, когда у нас есть рабочий, прослушивающий / работающий на запросы, мы можем создать простой сервис менеджера, делегирующий задачи своим подчиненным. Основной метод очень похож на метод в проекте простого рабочего, главное отличие в том, что мы не регистрируемся в ZooKeeper, мы только создаем ServiceProvider, роль которого (удивить, удивить) дает нам экземпляр работника. Итак, основной рабочий процесс:

  1. Ждать запросов на / делегировать
  2. Получить экземпляр рабочего сервиса от ServiceProvider ZooKeeper
  3. Позвоните работнику / работе и верните результаты его выполнения

Чтобы создать ServiceProvider, нам нужно создать клиент CuratorFramework, подключиться к ZooKeeper и затем получить ServiceProvider для сервиса с указанным именем, в нашем случае рабочий :

01
02
03
04
05
06
07
08
09
10
11
CuratorFramework curatorFramework = CuratorFrameworkFactory.newClient("localhost:2181", new RetryNTimes(5, 1000))
curatorFramework.start()
 
ServiceDiscovery<Void> serviceDiscovery = ServiceDiscoveryBuilder
    .builder(Void)
    .basePath("load-balancing-example")
    .client(curatorFramework).build()
serviceDiscovery.start()
 
serviceProvider = serviceDiscovery.serviceProviderBuilder().serviceName("worker").build()
serviceProvider.start()
  • Строки 1-2, создать клиента ZooKeeper, так же, как в простой-рабочий
  • Строки 4-8, создайте ServiceDiscovery, который сможет предоставить нам поставщик услуг.
  • Строки 10-11. создайте и запустите ServiceProvider, который будет использоваться для получения рабочего экземпляра рабочего узла.

Наконец, нам нужна конечная точка Rest, на которой менеджер ожидает делегирования задач:

1
2
3
4
5
6
7
8
9
@GET
@Path("/delegate")
public String delegate() {
    def instance = serviceProvider.getInstance()
    String address = instance.buildUriSpec()
    String response = (address + "/work").toURL().getText()
    println response
    return response
}

Начальный менеджер

Менеджер готов, и после выполнения задачи fatJar мы можем запустить его с помощью:

1
java -jar simple-manager/build/libs/simple-manager-1.0-shadow.jar 18000

Чтобы убедиться, что он работает, мы можем открыть ( http: // localhost: 18000 / делегат ) в браузере, чтобы увидеть сообщение «Работа выполнена Worker_1».

Менеджер ничего не знает о своих работниках. Единственное, что он знает, это то, что сервис зарегистрирован по определенному пути в ZooKeeper. И это так просто, независимо от того, есть ли у нас несколько рабочих, запущенных локально или распределенных по разным серверам в разных странах.

Балансировка нагрузки из коробки с ZooKeeper

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

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

1
2
3
4
5
6
7
java -jar simple-worker/build/libs/simple-worker-1.0-shadow.jar Worker_1 18005 &
           
java -jar simple-worker/build/libs/simple-worker-1.0-shadow.jar Worker_2 18006 &
 
java -jar simple-worker/build/libs/simple-worker-1.0-shadow.jar Worker_3 18007 &
           
java -jar simple-worker/build/libs/simple-worker-1.0-shadow.jar Worker_4 18008 &

Хитрость заключается в том, что все работники зарегистрированы в ZooKeeper по одному и тому же пути, поэтому, когда мы перечисляем узлы в / load-balancing-example / worker, мы увидим четыре экземпляра:

1
2
[zk: localhost:2181(CONNECTED) 0] ls /load-balancing-example/worker  <enter>
[d5bc4eb9-8ebb-4b7c-813e-966a25fdd843, 13de9196-bfeb-4c1a-b632-b8b9969b9c0b, 85cd1387-2be8-4c08-977a-0798017379b1, 9e07bd1d-c615-430c-8dcb-bf228e9b56fc]

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

Так что теперь, когда мы откроем http: // localhost: 18000 / Delegate и нажмем кнопку «Обновить» несколько раз, мы увидим:

1
2
3
4
5
6
7
Work done by Worker_1
Work done by Worker_2
Work done by Worker_3
Work done by Worker_4
Work done by Worker_1
Work done by Worker_2
Work done by Worker_3

Как это реализовано под капотом? По умолчанию ServiceProvider использует реализацию Round-robin ProviderStrategy, которая вращает экземпляры, доступные по заданному пути, так что каждый получает определенную работу. Конечно, мы можем реализовать нашу собственную стратегию, если механизм по умолчанию не соответствует нашим потребностям.

Резюме

Это все на сегодня. Как вы можете видеть, используя Apache ZooKeeper и Curator, мы можем жить без отдельных балансировщиков нагрузки, которые необходимо развертывать, отслеживать и управлять ими. Инфраструктура в архитектуре микросервисов довольно сложна даже без них.

Ссылка: ZooKeeper, куратор и как работает балансировка нагрузки микросервисов от нашего партнера по JCG Томаша Дзюрко в блоге Code Hard Go Pro .