Как стать автором
Обновить

Сложности и их решения в работе с Jenkins API

Давайте автоматизировать автоматизацию.
Сегодня поговорим о некоторых сложностях и их решениях в работе с Jenkins API.



Пример


Общая идея


Jenkins запускает задачи (Jobs) и следит за их пошаговым исполнением. Мы хотим запустить задачу с параметрами (Build) не из Jenkins GUI а из кода, и следить за ее исполнением.
* Оговорка- нижеприведенная задача это упрощение. Смысл в том, чтобы показать несколько проблем и их решение а не конкретную имплементацию у нас.

Цель


Деплойнуть среду (или очень много таких сред).
Для этого сначала мы поднимаем инфраструктуру.
Потом заливаем в неё новый код.
И вконце запускаем системные тесты и оповещения.

Способ


Параметры среды находятся в Single_Source_Of_Truth.json файле.
Вот как выглядит наш код
Single_Source_Of_Truth.json:
{
backend_server: {count: 4, code_path: Https:github.com/my_backend_code},
mobile_server: {count: 10, code_path: Https:github.com/my_mobile_code},
frontend_server: {count: 5, code_path: Https:github.com/my_frontend_code},
}

list_queue_items = []
for server_type_data in load(Single_Source_Of_Truth.json):
    list_queue_items.extend(jenkins.build_job(server_type_data))

list_builds = []
for q_item in list_queue_items:
    list_builds.extend(jenkins.get_build(q_item)) 
 
time_to_live = time_now + 10 * 60 # 10 min
while time_now  <  time_to_live and list_builds != []:
    for build in list_builds:
        if build.finished:
            list_builds.pop(build)     
  
    sleep(10)

run_system_checks()
send_notification()


Flow со стороны Jenkins


Терминология


(Предполагается, что это знакомо. Просто для уточнения общего языка):
Job — задача, имеет переменные, которые нужно заполнить, чтобы ее запустить.
Executor- Процесс выполняющий задачу (Job а точнее Build).
Agent- Исполнитель задач. Server/Docker Container/AWS Instance и т.д. Управляет Executor-ами.
Jenkins_Node — Дженкинсовый мозг. Управляет задачами и агентами.
Queue_Item — Задача с заполненными параметрами, готовая к запуску. Находится в очереди ожидания.
Queue_Item_Id — Queue_Item идентификатор. Временный номер, может повторяться многократно у разных Queue_Items
Build_Item — Задача направлена к Agent & Executor и начала исполнятся.
Build_Item_Id — Глобальный уникальный идентификатор Build_Item-а.

Flow


(1) Клиент (наш код) триггерит задачу в Jenkins_Node.
(2) Jenkins_Node создаёт из неё Queue_Item и возвращает нам Queue_Item_Id.
(3) Мы периодически посылаем запросы по Queue_Item_Id, чтобы получить Build_Item_Id. Пока
наша задача не получила Agent & Executor она не имеет Build_Item_Id. Ждём.
(4) Jenkins_Node назначает Agent, создаёт Build_Item и возвращает нам Build_Item_Id.
(5) Мы делаем запросы по Build_Item_Id ожидая окончания выполнения задачи.

Сложности и их решения


Мы столкнулись со следующими “задачами”.
(Отсортированы не по значимости а по ходу процесса приведённого выше.)

Задача

(1):
jenkins.build_job(server_team_data)- Обычный HTTP Request. У нас занимал до 1 минуты
При десятках последовательных задач (у нас количество приближается к 100) это было критично.
Решение:
Триггерить параллельно. Каждый запуск- свой thread. Только запуск. После этого они (threads) отмирают. Главный процесс ждет завершения всех пусков и отслеживает статус всех секвентально.

Задача

(3):
Известная и старая проблема.
Случается, что в какой то момент Jenkins_Node перестаёт узнавать Queue_Item_Id (ещё не вернув нам Build_Item_Id). Тупик. Все дальнейшие запросы бессмысленны. На каждый запрос queueitem.get_build вы получаете ошибку “ID не найден”.
Решение:
Создаём свой глобальный уникальный идентификатор- аргумент задачи (Job argument) Global_Random_Id. Он известен нам заранее. Теперь мы можем найти по нему наш Build_Item не зная его Build_Item_Id. Это превращает Queue_Item_Id в рудимент и он отмирает (только если вы не хотите следить за Jenkins Queue. Лично мы не хотим).
Так выглядит наш новый код:
triggered_builds = []
for server_type_data in load(Single_Source_Of_Truth.json):
    server_type_data.global_random_id = random()
    jenkins.build_job(server_type_data)
    Triggered_builds.extend(server_type_data)

started_builds = []
time_to_live = time_now + 10 * 60 # 10 min
    while time_now  <  time_to_live and triggered_builds != []:
        for build in triggered_builds:
            builds_history = jenkins.get_job_buids_history(build.job_name)
            if build in builds_history:
                triggered_builds.pop(build)
                started_builds.extend(build)
        sleep(10)

time_to_live = time_now + 10 * 60 # 10 min
while time_now  <  time_to_live and started_builds != []:
    for build in started_builds:
        if jenkins.build.finished:
            started_builds.pop(build)     
    sleep(10)

run_system_checks()
send_notification()


Задача

(4):
Мы используем AWS Instances как Agents. Дабы не тратить деньги за простой, Jenkins поднимает их по мере надобности. При 100 запущенных задачах и 5 Executors per Agent мы долго ждём пока Jenkins запустит все Agents по мере запуска задач.
Решение:
Перед запуском самих задач мы запускаем нужное количество Agents.
Из за того, что это функционал плагина Jenkins а не самого Jenkins API пришлось в коде симулировать POST Request, который посылает Jenkins GUI, после клика мыши на Provision.
AWS EC2 Provision URL
https://your-jenkins.com/cloud/ec2-Main%20AWS%20account/provision


Задача

(5):
Напоминалка- в (3) мы изменили индивидуальные запросы статуса по Queue_Item_Id на запросы истории билдов по Job_name и в них ищем наш по Global_Random_Id.

В некоторых клиентах каждый запрос по Job_Name посылает всю историю билдов. Когда это доходит до сотен тысяч (у нас 300,000) начинаются аномалии в виде таймаутов и ошибок на прокси по дороге. Да и вообще, зачем?
Решение:
Ограничение количества возвращаемых задач в ответе от Jenkins_Node. Нам для этого пришлось сменить python client.

Задача

(5):
Чтобы получить статус исполняемой задачи нужно раз в какое то время посылать get_build_info запрос на каждый бегущий Build_Item.
Из нашего примера сверху:
time_to_live = time_now + 10 * 60 # 10 min
while time_now  <  time_to_live and started_builds != []:
    for build in started_builds:
        if jenkins.finished(build):
            started_builds.pop(build)     
    sleep(timeout)


При timeout = 10 и length(list_builds) = 100
Каждые 10 секунд мы делаем 100 запросов.
Так как задачи бегут разное количество времени мы тратим кучу ресурсов в пустую. Если мы решаем изменить timeout=60 возможна ситуация, когда все задачи выполнились но мы спим минуту до того как сообщить об этом. Умножим на 60 программистов в день ожидающих ответ. Получим 1 человеко-час впустую при выполнении одного деплоя в день. А их как минимум три на человека (Testing/Pre-Prod/Prod). Да и зачем Jenkins мурыжить впустую?
Решение:
Динамичный timeout.
Пример реализации
time_seed = 3
while time_now  <  time_to_live and started_builds != []:
    for build in started_builds:
        if jenkins.finished(build):
            started_builds.pop(build)
    sleep(time_seed*length(started_builds))

Считаем плюшки


Первая итерация спит 100*3 = 300 sec.
Если по истечении 5 минут у нас стало length(list_builds) == 50
(100+50)/2 — среднее количество запросов за итерацию.
(300/timeout) — Количество сэкономленных итераций.
Timeout = 10
(100+50)/2 * (300/timeout)= 75*30 = 2250 Requests экономим за первую итерацию нового кода.


Здесь я прошелся только по архитектурным решениям не касаясь мелких ошибок, шероховатостей и оптимизаций. Как результат- нам удалось существенно сократить время затрачиваемое на инфраструктуру. С 40 минут в среднем до 24 минут без предзапуска AWS Agents. После введения последнего, удалось сократить до 19 минут, иногда встречаются и 16- редко правда, но это даёт стимул дальше работать в этом направлении )
Теги:
Хабы:
Данная статья не подлежит комментированию, поскольку её автор ещё не является полноправным участником сообщества. Вы сможете связаться с автором только после того, как он получит приглашение от кого-либо из участников сообщества. До этого момента его username будет скрыт псевдонимом.