Чистка build-агентов Gitlab
Спешим поделиться с вами нашим инструментом для поддержания чистоты и порядка на наших (RNDSOFT) сборочных серверах gitlab-janitor. О том, как мы к нему пришли, и каков первый опыт - далее по тексту.
С каждым годом инфраструктура RNDSOFT, обеспечивающая процессы CI/CD, растёт. Появляются новые проекты, усложняются процессы (pipelines) сборки и тестирования, растет количество сборщиков (build agents), и всё это приносит дополнительные накладные расходы. Сегодня мы рассмотрим конкретную проблему - чистку/освобождение ресурсов на самих сборочных серверах. Мы используем Gitlab и несколько Gitlab runners с докером (docker executor) под капотом. Вот с проблем и начнём.
Проблемы
Вся терминология будет опираться на Gitlab, но всё это применимо и к другим решениям, опирающимся на docker.
Типовой процесс сборки и доставки состоит из 4х больших этапов (stages), по несколько задач (jobs) в каждом:
- сборка docker-образа (обычно одного на проект, но бывает и больше).
- тестирование. Тут мы используем подход, когда тестируем не сам код, а весь контейнер. Конечно, есть юнит-тесты, интеграционные, графические и прочие, но запускаем мы их непосредственно внутри боевого контейнера. А что? У нас ruby - можем себе позволить 💎 :)
- тэгирование (image promoting). Этап в зависимости от результатов тестов и QA перетегируется во что-то вроде
stable
илиrelease
, или как-то еще, в зависимости от конкретного проекта, команды и принятых процессов. - доставка (deploy). Тут всё как обычно - дев/тест стенды, QA-стенды, динамические стенды для MR и всякое разное вроде документации.
Давно хочу написать отдельно про процесс image promotiong, про то, как мы таскаем образы между стадиями, но руки не доходят. Если кого сильно интересует - пишите в комментариях, и мне придётся найти в себе силы :)
В результате описанных выше процессов (особенно тестирования) на сборщиках остаётся много мусора, именно это является проблемой. Далее по порядку.
Подвисшие контейнеры (dangling containers)
Тут речь, конечно, идёт не о повисании ПО внутри контейнера, а о самих "ненужных" контейнерах. Например, для графических тестов поднимается целый стек: Postgres, Chrome, Selenium, само приложение, контейнер, из которого запускаются тесты, Redis и бог еще знает что. Если весь процесс (pipeline) или непосредственно задача (job) будут прерваны, то никто не погасит за нас эти контейнеры, и они продолжат висеть и потреблять ресурсы. Проблема!
Ненужные образы
В процессе сборки имя образа претерпевает примерно следующие изменения: image:$SHA -> image:stable -> image:release. Конечно, это примерно, шагов может больше или именования другие, но сути это не меняет. При последовательных комитах сратыестарые образы остаются лежать мёртвым грузом.
В день мы генерим примерно 75GB образов. ~25GB на каждом из трех сборщиков. И это летом, когда многие в отпуске :)
Ситуация усугубляется тем, что один и тот же образ находится (с максимальной вероятностью) сразу на всех сборщиках, поскольку разные типы тестов запускаются параллельно и попадают на все сборщики и занимают x3 места. Проблема! Дважды проблема, поскольку с образами всё не так просто и очень интересно - об этом ниже.
Безымянные и кеширующие тома (unnamed and cache volumes)
Многие контейнеры при создании аллоцируют в докере временные анонимные тома/диски (мы же всегда про докер говорим, верно?) и оставляют их после себя. Также сам Gitlab, если вы не используете распределённые кеш (shared cache) на базе S3, всё равно создаёт кеш-тома, если ему явно не запретить это делать через disable_cache
. Проблема!
Конечно, эти проблемы не простой "мусор" - старые образы могут ускорить последующую сборку, кеш-тома также теоретически могут ускорять сборку, так что они не совсем бесполезны. Однако, проблемы ускорения и кеширования мы решаем немного иначе - с помощью определённых схем именования образов, используя buildkit, который умеет подтягивать кешированные слои прямо из реестра образов (в нашем случае harbor) и пр.
Что же делать?
Проблемы появляются не сразу. Когда-то у нас был один сборщик, там вообще не было проблем с перетягиванием образов между этапами, а образы чистились через docker rmi
на последнем шаге процесса. Когда сборщиков стало больше, и на них докинули ресурсы, пришлось немного усложнить схему - сначала были bash-скрипты, которые по расписанию удаляли образы по шаблону имени. Затем периодически стали использовать docker system prune
. Но эти решения очень негибкие, плохо поддерживаются и масштабируются и обладают фундаментальной проблемой - частыми кеш-промахами (cache miss), что периодически сильно тормозило процессы (pipelines). И вот однажды терпеть это стало невозможно 😜 :)))
Нам нужно хорошее решение, желательно с баристой и массажисткой! Встречаем - gitlab-janitor!
Gitlab-janitor
- консолидация всех чисток сборщика в одном месте;
- уменьшение кеш-промахов, насколько это возможно;
- простота работы.
Для чистки места на сборщиках уже есть проект gitlab-runner-docker-cleanup, он и явился идейным вдохновителем нашей утилиты, но, к сожалению, не выполнял всех необходимых нам функций.
Написав своё решение, мы решили опубликовать его в открытом виде - и зеркало на github, и образы на docker-hub, и даже документация с примерами. Ну и не мог я удержаться от разнообразных бейджиков :)
Проще всего запускать его в докере на каждом сборщике (для этого мы используем nomad 🥂 ):
docker run --rm \ -v /var/run/docker.sock:/var/run/docker.sock \ -v /persistent/janitor:/store \ -e REMOVE=true \ -e INCLUDE="*integr*, *units*" \ -e EXCLUDE="*gitlab*" \ -e CONTAINER_DEADLINE="1h10m" \ -e VOLUME_DEADLINE="4d" \ -e IMAGE_DEADLINE="4d" \ -e CACHE_SIZE="10G" \ -e IMAGE_STORE="/store/images.txt" \ rnds/gitlab-janitor:latest
Заметили IMAGE_STORE
? С остальными параметрами всё просто - шаблоны для имён образов, томов, сроки хранения, а вот для образов всё гораздо интереснее!
Докер не сохраняет в своём локальном хранилище никакой информации о времени загрузки образа, а только обычную дату создания образа. В результате нет возможности определить, какой образ очень нужен и часто используется (хоть и создан давно), а какой свеженький, но больше никогда и никому не будет нужен.
Для решения этой проблемы в gitlab-janitor сделан простенький механизм - когда образ встречается первый раз, он сохраняется в файл с временной меткой - это считается датой появления. Когда подходит срок - образ удаляется. Однако, если gitlab-janitor увидит созданный контейнер из этого образа - то временная метка в этом файле сбрасывается. Именно для этого и используется параметр IMAGE_STORE
. Данный файл можно монтировать с хост-машины, а можно и нет - в этом случае история образов будет теряться при пересоздании/обновлении контейнера-чистильщика:
selenium/standalone-chrome:latest sha256:c01aea5eb0bf279df5f745e3c277e30d7d9c81f15b9d1d4e829f1075c31ed5b1 1660205645 postgres:10-alpine sha256:d7023df56cb7cbe961f0c402888f7170397c98c099fb76fbe16b5442a236ad51 1660205040 redis:latest sha256:3edbb69f9a493835e66a0f0138bed01075d8f4c2697baedd29111d667e1992b4 1660205645
С остальными параметрами, документацией и примерами можно ознакомиться на гитхабе.
Результаты
Вот и подъехали результаты объективного контроля за работой gitlab-janitor на трех наших сборщиках:
Все чистки с помощью cron+bash
, docker prune
и пр. выключены. Параметры, оптимально подходящие под наши процессы, мы еще подбираем (и будем подбирать), но уже видно, что процесс вошел в стабильную фазу, и наш чистильщик выполняет свою работу!