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

Про Jenkins

Jenkins — это инструмент для автоматизации CI/CD-процессов:

  • Он представлен в виде монолитного Java-приложения, в котором нет персистентного хранилища или какой-либо базы данных. 

  • Jenkins хранит всю нужную ему для работы информацию в виде файлов в файловой системе: журналы сборок, конфигурации конвейеров, артефакты сборок и другое. 

  • Есть встроенная песочница, позволяющая выполнять произвольные Groovy-скрипты, которые невозможно контролировать. 

  • Из коробки в Jenkins нет резервирования. 

  • Основная функциональность поставляется в виде open source-плагинов, которые создаются разными разными людьми, из-за чего невозможно контролировать качество. 

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

В CI-контуре Сбера сейчас 55 различных инсталляций Jenkins, примерно 420 тысяч конвейеров и примерно 1 миллион запусков в сутки. 

Типовая инсталляция Jenkins

Есть виртуальная машина, на которой запущен Jenkins-мастер. Перед ней веб-сервер с настройками TLS, он является точкой входа для клиентов. К Jenkins-мастеру подключаются агенты, с помощью которых пользователи создают свои сборки.

Если инсталляция важная, то необходимо обеспечить ей резервное копирование. Как правило, для этого поднимают вторую виртуалку, обычно в другом ЦОДе. Туда устанавливают Jenkins, и состояние файловой системы между мастером и резервной копией синхронизируется каким-либо инструментом, например, rsync. 

В случае отказа мастера администраторам нужно выполнить некие действия, чтобы восстановить предоставление услуги: запустить новый мастер на резервной реплике и подождать, пока она прочитает всю нужную информацию из файловой системы. Для больших инсталляций Jenkins это может занимать длительное время. И у нас они обычно большие — десятки тысяч конвейеров и история сборок за несколько лет, потому что требуется хранить их определённый срок. Затем администраторам нужно переподключить агенты к новому Jenkins Master, переписать маршруты на веб-сервере. И пока администраторы всё это делают, пользователи ждут и не имеют доступа к своим сборкам и конвейерам.

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

Вместо этого мы хотели:

  • обеспечить динамическую маршрутизацию и бесшовные переключения трафика;

  • заставить Jenkins работать в кластере и наращивать его производительность, добавляя новые шарды и обеспечивая горизонтальное масштабирование;

  • каждому Jenkins-мастеру обеспечить резервирование;

  • дать администраторам возможность централизованного управления;

  • автоматизировать жизненный цикл. Что это значит? Кроме инцидентов с недоступностью Jenkins-мастера у нас есть рутинные периодические операции, например обновление операционных систем. Из-за этого часто приходится перезапускать виртуалки. То есть мы хотим, чтобы администраторы могли проводить такие работы без влияния на пользователей, максимально просто, без лишних зависимостей и накладных расходов, безопасно и гибко.

С преферансом и танцовщицами

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

Решили сделать несколько узлов с Jenkins-мастером, которые стоят за единой точкой входа. Всех пользователей будут по разным признакам распределять по мастерам в кластере. Вся информация о состоянии узла о маршрутах должна храниться в какой-то персистентной базе данных. А значит, к ней можно подключить интерфейс или API и предоставить доступ администраторам, чтобы они могли управлять состоянием всех машин. 

Хранилище метаинформации

Основная задача — это хранение метаинформации о кластере. Для этого выбрали ETCD: оно простое в использовании и с древовидной структурой хранения ключей, похожей на файловую систему. А у нас как раз есть требования по репликации каждого узла, поэтому необходимо знать, то какие файлы лежат на каждом мастере. 

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

Буферное хранилище файлов

Вторым инфраструктурным хранилищем стало S3. Оно работает буферным хранилищем для передачи файлов, которые нужно перекидывать между Jenkins-мастерами. S3 легко масштабируется. Есть быстрый поиск файлов внутри хранилища, можно хранить данные в похожем на файловую систему виде. И поскольку мы решили делать обвязку самостоятельно, нам понравилось, что во всех популярных языках программирования есть готовые клиенты для S3. 

Технологии

В качестве основного языка выбрали Python. У нас все сервисы и обвязки асинхронные, поэтому активно используем Asyncio. Основной веб-фреймворк — FastAPI. Для написания асинхронных клиентов к системам хранения, сервисам и Jenkins выбрали AioHTTP. Наконец, библиотека Asyncinotify позволяет из Python подписываться на события изменения файловой системы через identify-драйвер Linux. Это ключевая библиотека для понимания механизма репликации, так как мы устанавливаем наши сервисы на машины и отслеживаем изменения файлов. 

Маршрутизация и репликация

Определившись с технологиями, мы выбрали атомарную единицу для репликации и маршрутизации трафика — «проектная область». В нашей терминологии это конвейер типа folder, который лежит в корне Jenkins-мастера. 

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

Рассмотрим подробнее механизм маршрутизации. Мы реализовали сервис HAProxy Router, управляющий экземплярами HAProxy. Он периодически считывает из ETCD информацию об узлах кластера, какие проектные области и на каком мастере активны. В зависимости от этого сервис переписывает правила маршрутизации на HAProxy и управляет экземпляром. То есть проверяет, что HAProxy жив и конфигурация актуальна и соответствует данным в ETCD. 

Теперь рассмотрим механизм шардирования. Он представлен двумя сервисами: load-controller и resharder. Так как Jenkins работает в нашей обвязке как кластер, для нормального горизонтального масштабирования необходимо равномерно нагружать все шарды кластера и все узлы внутри каждого шарда. Для этого нужно рассчитать удельную нагрузку на каждый мастер, чем занимается контролёр нагрузки. Эту информацию он периодически обновляет и сохраняет в ETCD. На её основании решают, размещать ли новую проектную область в каком-либо шарде и мастере. 

Решардер отвечает за перенос проектных областей между шардами. 

Механизм бесшовности мы реализовали с помощью сервиса Healthcheker — он отслеживает состояние Jenkins‑мастеров: собирает их метрики и метрики Java‑машин, на которых работают мастеры, и решает, нужно ли выводить какие‑либо узлы из балансировки. Если нужно вывести какой‑то узел, сервис обращается к управляющему модулю API‑сервера с просьбой перевести трафик с выключаемого узла на оставшиеся синхронизированные реплики в текущем шарде. API‑сервер меняет нужные статусы в ETCD. При этом сервис HAProxy Router видит изменения статусов проектных областей по разным нодам и переписывает маршруты к кластерам балансировщиков, на которые приходят клиенты.

Механизм репликации основан на сервисе syncer. Он ставится на каждую виртуальную машину и работает напрямую с файловой системой. Причём работает одновременно в двух режимах: для активных проектных областей на текущем мастере и для пассивных проектных областей там же. Сервис создаёт identify watcher, который отслеживает изменения файловой системы. Обнаружив изменение или создание какого‑либо файла для активной проектной области, syncer загружает последнюю версию файла в S3 и через ETCD сообщает таким же сервисам на пассивных репликах, что им нужно забрать себе последнюю версию. Те идут в S3, скачивают обновление и по API подгружают в свои Jenkins‑мастеры. Так у нас получается набор Jenkins‑мастеров с синхронизированным состоянием, а для каждой проектной области есть набор горячих реплик, которые могут принять на себя клиентский трафик в случае необходимости.

Механизм управления кластером

Мы на FastAPI написали API‑сервер. Он предоставляет администраторам интерфейс в виде Swagger для управления всей нашей обвязкой. Можно просматривать состояние узлов и управлять ими, управлять агентами. Сервер позволяет управлять нагрузкой, пересобирать кластер и перебалансировать.

Механизм управления агентами

Для управления агентами используется сервис сервис agent‑explorer. Он работает в двух режимах. Первый — для так называемых коммунальных агентов. Коммунальные агенты — это агенты, которые сопровождает команда сопровождения самого Jenkins. Они общие. При таком режиме agent‑explorer подключает агенты к каждому узлу в своём шарде и следит за тем, чтобы они находились в актуальном состоянии и всегда были подняты.

Второй режим — пользовательский. На вход agent‑explorer принимает проектные области, и если они переезжают на другой мастер, то сервис переподключает к нему агент и переносит туда настройки, которые были внесены пользователями.

Вспомогательные сервисы

У нас есть сервис резервного копирования, создающий копии конфигураций конвейеров и дерева ETCD, дерево ключей. Также он очищает хранилище S3 от устаревших данных.

Для удобства администраторов мы написали консольный клиент к API‑серверу. Ещё один важный сервис — cluster‑exporter. Он собирает данные из ETCD, считает статистику использования кластера и предоставлять это в виде Prometheus‑метрик. На их основе мы делаем инфопанели, по которым администраторы управляют и отслеживают состояние кластера и нашей обвязки.

Если всё, о чём я рассказал, разместить на одной схеме, то получится вот такая красота:

Пять основных слоёв:

  1. Клиентский слой. Клиентами являются либо пользователи, либо другие системы, которые использует Jenkins. Они обращаются к единой точке входа и попадают на второй слой.

  2. Кластеры балансировщиков. На каждом из балансировщиков стоит наш сервис HAProxy Router, который читает данные из ETCD и прописывает маршруты к конкретным проектным областям.

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

  4. Сопровождение. В качестве буферного хранилища используем S3.

  5. Управление. Cбоку к ETCD подключён API‑сервер, через который администраторы управляют кластером.

Заключение

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

Сделали бесшовное переключение с помощью отслеживания состояния различных Jenkins‑мастеров и автоматизировав принятие решения о том, нужно ли какой‑либо мастер выводить из балансировки.

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

Сделали механизм резервирования: каждый узел резервирован в рамках своего шарда.

Предоставили администраторам единую точку входа для управления кластером.

Благодаря всему этому длительность восстановления услуги сократилась в 30 раз. У нас всегда есть горячие реплики, которые готовы принять трафик. Автоматизирован процесс принятия решения о выводе из балансировки, управление агентами и трафиком. Администраторы больше не «чинят здесь и сейчас», у них есть время спокойно разобраться в проблеме или обслужить узлы без простоя пользователей. Появилась единая точка управления: всего один человек может управлять маршрутами, трафиком и всем кластерам через наш API‑сервер.

Комментарии (3)


  1. Thomas_Hanniball
    16.09.2024 13:18

    То, что вы сделали - это круто. Было бы неплохо сделать всё это на Java и предложить разработчиками Jenkins добавить этот код в свой продукт.

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


  1. dburmistrov
    16.09.2024 13:18

    Если вы держите резервные инстансы контроллеров под парами, как решили проблему с таймерными джобами и гит-поллингом? Или у вас всё на веб-хуках?


  1. ashkraba
    16.09.2024 13:18
    +1

    А почему вы просто не захотели поднять jenkins в k8s кластере + jcsac? Мы такой вариант уже много лет используем - полет нормальный. Но у нас нагрузки конечно не такие космические как у вас. Но тут тоже вопрос - а не проще ли иметь несколько разных jenkinsов? Ведь когда в одной системе 100500 пользователей и ярды джоб это ну такое себе.