Найти тему
Arenadata

Балансируем нагрузку в Jenkins

Оглавление

В этой статье мы рассказываем о том, какими способами решали задачу балансировки нагрузки в Jenkins и что из этого получилось.

В Arenadata мы используем Jenkins для CI. Почему? Как бы банально это ни звучало — так исторически сложилось. Мы хранили код в GitHub, когда там ещё не было Actions, и продолжаем хранить, потому что много работаем с Open Source. За три года работы с Jenkins мы неплохо разобрались в нём, в том числе научились быстро масштабироваться, чтобы удовлетворять запросы разработки. В этой статье хотим поделиться тем, что мы успели понять про разные способы балансировки нагрузки в Jenkins.

Для тех, кто давно эксплуатирует Jenkins и кому проблемы, связанные с его эксплуатацией, набили оскомину, сразу напишем список того, о чём мы не будем рассказывать в этой статье:

  • как быстро обновлять плагины Jenkins и не сломаться;
  • как следить за адом зависимостей плагинов;
  • что делать, если наш плагин перестали поддерживать (да и надо ли это обсуждать, речь ведь про Open Source).

Речь пойдёт о том, какими способами мы решали задачу балансировки нагрузки в Jenkins и что из этого получилось.

Небольшие вводные: Jenkins — это фреймворк автоматизации, написанный на языке Java. Понятно, что для успешного использования любого фреймворка неплохо бы владеть языком, на котором этот фреймворк написан, но где вы видели отделы DevOps, которые умеют писать на Java? Вот и наша DevOps-команда на Java не пишет. Однако пока нам удавалось успешно справляться со всеми вызовами, используя Jenkins.

Как в Jenkins балансируют нагрузку

Для каждого узла указаны:

  • метки (labels) — описывают задачи, которые могут запускаться на узле; название узла тоже является меткой;
  • исполнители (executors) — их количество определяет количество одновременно запущенных задач на узле Jenkins.

Все билды встают в очередь, зная label узла, на которой должны выполниться; как только на узле освобождается executor, запускается билд.

С одной стороны, современный CI — это запустить в контейнере что-нибудь очень легковесное (например, pylint, ansible-lint и так далее) и дать обратную связь, а с другой — развернуть окружение, запустить тесты и прибраться за собой. Так что придётся потрудиться даже узлу, на котором работает один лишь управляющий процесс для тестов, а всё остальное развернуто за его пределами. Например, мы запускаем pytest c плагином xdist. Компиляция и сборка тоже могут быть очень ресурсоёмкими. Поэтому нельзя просто взять десяток узлов, прилепить им метку «docker» и насыпать побольше исполнителей: они будут постоянно конкурировать друг с другом за ресурсы. У вас не получится адекватно и просто позаботиться о том, чтобы запущенные пайплайны не задушили друг друга или узел.

Итак, проблема понятна, давайте её решать.

Исходные условия:

  • Jenkins;
  • инфраструктура в облаке;
  • множество команд разработки, потребности которых постоянно растут;
  • разнообразные требования по мощности к узлам Jenkins;
  • DevOps умеют писать на Python, но не на Java.

Проблемы

  1. Управление состоянием кластера Jenkins.
  2. Очереди на выполнение. «Слишком большие — дайте мощностей!»
  3. Бережное использование ресурсов. «Очереди нет, надо убрать лишнее».
  4. Когда узлов становится больше 10, становится сложно визуально понять, какие проекты используют выделенные мощности, а какие нет. Нужны мониторинг и аналитика.
  5. Нужно постоянно помнить о необходимости убираться на узлах, чтобы они были в работоспособном состоянии.
  6. Конкуренция на узлах за ресурсы.
  7. Labels hell.

Подход первый: Ansible

Начинали мы с самой простой задачи: нам надо было оперативно добавлять и удалять мощности из кластера. Конечно же, пишем ansible-playbook и сразу наслаждаемся бенефитами.

  • Набор софта на узле зафиксирован и версионируется.
  • Чтобы добавить в кластер новый узел, создаём виртуалку с заранее известным SSH-ключом, добавляем в Inventory и запускаем плейбук. Это действие, конечно же, легко автоматизируется, тут кому как будет удобней: Terraform + Ansible и динамический Inventory, а можно и на чистом Ansible. Получаем узел в кластере и данные узла в мониторинге.
  • Можно пытаться оперативно удалять и добавлять узлы, чтобы сэкономить деньги в ручном режиме. На самом деле это не работает, потому что узлы обычно добавляются там, где мало мощностей, а там, где их хватает, никто не жалуется. Поэтому сподвигнуть вас удалить лишние узлы могут только сбор метрик и аналитика.

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

Для балансировки нагрузки мы сделали небольшой пул узлов с меткой «docker», добавили побольше исполнителей и стали там запускать все незатратные процессы. А для проектов и стадий пайплайнов, которые требовали много ресурсов, сделали отдельные узлы с одним исполнителем.

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

Спойлер: мы так не сделали.

Да, управление кластером становится простым и лёгким, а ручные манипуляции сводятся к минимуму.

-2

С одной стороны, мы приблизились к решению проблем из пунктов 1, 2 и 3. Но я ещё ничего не сказал о мастере. Да и если хочется заботиться о ресурсах как следует, то есть ещё большой резерв по совершенствованию решений проблем из пунктов 2 и 3.

Подход второй: Slave Setup Plugin

В первом приближении стало понятно, как администрировать кластер. Правда, следить за метками было всё ещё неудобно, а идея, которая позволит улучшить ситуацию, выглядела сложной в реализации. Мониторинга и аналитики до сих пор не было. Задачи, которые запускались на docker-узлах, периодически «перерастали» их, это тоже было неприятно.

В следующей итерации мы нашли прекрасный Slave setup plugin, который позволяет выполнять произвольный скрипт при запуске slave-узла и в глобальных настройках на master-узле, чтобы включать slave по мере надобности и выключать его спустя какое-то время бездействия.

Пришло время сэкономить немного денег: будем включать/выключать узлы по требованию. Таким образом можно создать узлы с избытком, чтобы в определённых пределах нагрузки не возникло больших очередей. Переплачивать мы будем только за диски. Хотя и это не обязательно, ведь slave-узел можно не включать/выключать, а создавать/удалять, для master никакой разницы нет.

Тут ничего сложного:

  • устанавливаем плагин;
  • переписываем наш Ansible, чтобы он умел распознавать узлы, умеющие выключаться/включаться по требованию;
  • пишем скрипт, который будет усыплять узел, а потом нежно будить его на работу по первому зову из очереди; вооружаем master этим скриптом.

Тут нужно кое-что пояснить. Для управления кластером Jenkins мы используем собственную платформу Arenadata Cluster Manager (ADCM). Она хранит в себе информацию об узлах и умеет их создавать, включать, отключать и удалять. Наш скрипт запрашивает эти операции, а вся логика содержится в ansible-плейбуках ADCM. В общем случае же достаточно, чтобы скрипт дожидался доступности узлов по SSH после их запуска, а после выключения дожидался, пока облако закончит операцию.

Создавать узлы можно напрямую через rest API Jenkins или выполнять groovy-скрипты на мастере, отправляя тело скрипта так же — через rest api. Мы пошли вторым путём.

С полным текстом статьи вы можете ознакомиться в нашем блоге на «Хабре».