Один под, два раннера и где-то Kaniko: почему это норма, а не дичь¶
Война-стори и смена ментальной модели. Про то, как один pod
gitlab-runnerтащит два зарегистрированных раннера, при этом сам почти ничего не делает, а Kaniko и обычные сборки никогда не сидят в одном поде. Спойлер: manager — это прораб, а не каменщик.
Момент «стоп, что?»¶
Мигрировал я как-то GitLab Runner из docker в Kubernetes. Открываю Argo, смотрю на приложение runner — а там один Deployment, один под. Лезу в GitLab → Admin → Runners — а там два раннера: ci и docker/build. Лезу обратно в кластер — под по-прежнему один.
Первая реакция была примерно как у вас в заголовке: «два раннера в одном поде, да ещё и Kaniko с обычной сборкой вперемешку — это что вообще такое?»
Оказалось — всё правильно. Просто я держал в голове неправильную картинку. Давайте её поменяем, и «дикость» превратится в «а, ну логично же».
Главная подмена понятий¶
В голове сидело: раннер = воркер, который собирает. Поэтому «два раннера в одном поде» звучит как «два грузчика в одном теле».
А на самом деле в Kubernetes-исполнителе:
gitlab-runner (manager pod) ← ПРОРАБ. Не собирает. Опрашивает GitLab, раздаёт работу.
│
├── создаёт job-pod ──► node:22-alpine (сборка фронта) ← КАМЕНЩИК №1
├── создаёт job-pod ──► kaniko:debug (билд образа) ← КАМЕНЩИК №2
└── создаёт job-pod ──► alpine/git (деплой) ← КАМЕНЩИК №3
↑ временные, рождаются и умирают под каждую джобу
Manager-под ничего не собирает. Это процесс gitlab-runner run, который:
- опрашивает GitLab: «есть для меня джобы?»,
- под каждую взятую джобу создаёт отдельный под с образом из
image:этой джобы, - следит за ним и подчищает.
Каменщики (job-поды) — эфемерные. Появились, отработали джобу, исчезли. Kaniko и node-сборка никогда не оказываются в одном поде — это два разных временных пода, под две разные джобы, с двумя разными образами. В manager-поде их нет вообще.
Хотите убедиться — запустите пару пайплайнов и смотрите в namespace раннера:
kubectl -n ci get pods -w
# gitlab-runner-xxxx 1/1 Running ← прораб, висит всегда
# runner-abc123-project-... 0/1 Pending ← каменщик появился под джобу
# runner-abc123-project-... 1/1 Running
# runner-abc123-project-... 0/1 Completed ← отработал и умер
Прораб один и тот же. Каменщики приходят и уходят.
Тогда что такое «два раннера»?¶
Это две регистрации в одном config.toml — два блока [[runners]], каждый со своим токеном и своим набором тегов:
concurrent = 6
[[runners]]
name = "n2-docker-builder"
token = "glrt-…" # теги: docker, build
executor = "kubernetes"
[runners.kubernetes]
namespace = "ci"
[[runners]]
name = "n2-ci"
token = "glrt-…" # тег: ci
executor = "kubernetes"
[runners.kubernetes]
namespace = "ci"
Один процесс (прораб) держит два «удостоверения» и опрашивает GitLab по обоим. Для GitLab это два раннера (две сущности, два токена, разные теги). Для Kubernetes это один Deployment. Обе картинки верны одновременно — просто они про разные слои.
Теги — это не разделение кластера пополам. Тег решает только одно: какую джобу прораб имеет право взять. ci берёт джобы с tags: [ci], docker/build — с tags: [docker]/[build]. Размер пода, лимиты, на какой ноде он окажется — теги на это не влияют вообще.
А ресурсы-то как делятся?¶
Вот это вопрос по делу. Ресурсы живут на двух уровнях, и их легко перепутать.
Уровень 1. Manager-под (тот самый 1 под в Argo)¶
Лёгкий. Он только оркестрирует. Типичные реквесты — копейки:
resources:
requests: { cpu: 100m, memory: 128Mi }
limits: { cpu: 500m, memory: 256Mi }
Оба зарегистрированных раннера делят один и тот же manager-под и один общий пул параллелизма.
Уровень 2. concurrent — главный кран¶
concurrent = 6
Это максимум одновременных джоб суммарно по всем [[runners]]. Не «6 на ci и 6 на build», а 6 на всё. Из коробки квоты «50/50» между регистрациями нет — оба раннера конкурируют за общий concurrent.
Ситуация (concurrent = 4) |
Что будет |
|---|---|
3×ci + 1×docker |
все 4 параллельно |
5×ci |
4 идут, 5-я ждёт в очереди раннера |
2 тяжёлых build + 3 лёгких ci |
лимит 4, без отдельной квоты на тип |
Хотите потолок на конкретную регистрацию — есть limit внутри [[runners]]:
[[runners]]
limit = 2 # этот раннер — не больше 2 джоб одновременно
…но сумма всё равно не перепрыгнет глобальный concurrent. Если concurrent = 3, а limit = 2 у обоих — реальный максимум 3, а не 4.
Уровень 3. Job-поды — где реально горит CPU/RAM¶
Тут и тратятся ресурсы на сборки. Дефолты на все джобы раннера — в config.toml:
[runners.kubernetes]
cpu_request = "500m"
cpu_limit = "2"
memory_request = "1Gi"
memory_limit = "4Gi"
И переопределение на конкретную джобу — прямо в .gitlab-ci.yml:
publish:image:
tags: [docker, build]
variables:
KUBERNETES_CPU_REQUEST: "1"
KUBERNETES_MEMORY_LIMIT: "8Gi"
Поскольку у нас две регистрации, у ci и у docker/build можно задать разные [runners.kubernetes]: лёгкому ci — поменьше, тяжёлому build (Kaniko) — побольше. Это и есть «два раннера с разными аппетитами», хотя прораб у них общий.
Полная картина одной схемой¶
доступные ресурсы нод (общий пул)
▲
│ scheduler кладёт job-поды по requests/limits
┌───────────┴───────────────────────────────┐
│ gitlab-runner pod (прораб) │
│ requests ~100m / 128Mi │
│ concurrent = 6 (общий на ci + build) │
└───────────┬───────────────────────────────┘
┌────────┴────────┐
▼ ▼
┌──────────┐ ┌──────────────┐
│ job pod │ ... │ job pod │ ← каждый со своим образом и лимитами
│ image: │ │ image: │
│ node │ │ kaniko │
│ tag: ci │ │ tag: docker │
└──────────┘ └──────────────┘
- Прораб — фиксированный маленький кусок на одном поде.
- Параллелизм — глобальный
concurrent(+ опциональноlimitна регистрацию). - CPU/RAM сборок — в каждом job-поде, и у
ciсdocker/buildнастройки могут отличаться. - Кластер общий: 10 тяжёлых Kaniko-сборок могут вытеснить лёгкие
ci-джобы вPending— даже при разных тегах. Теги делят очередь, а не ноды.
Две очереди, в которых джоба может зависнуть¶
Когда «не едет», виноват один из двух заторов:
- Очередь раннера — уже занят
concurrent(илиlimit). Джоба в GitLab виситpending / waiting for runner. - Очередь Kubernetes — прораб под создал, но на нодах нет места по
requests→ под вPending, джоба висит.
Коварство: прораб не всегда сверяется со свободной ёмкостью нод заранее. Он может взять джобу «по concurrent», создать под — а тот зависнет в Pending. Поэтому concurrent обычно ставят с запасом относительно реальной ёмкости, а не «впритык».
Если надо жёстко разделить ресурсы¶
Чтобы тяжёлые build не забивали лёгкие ci:
- разные
limitна[[runners]]; - разные
nodeSelector/taints— увестиbuild-поды на отдельные ноды; - разные
[runners.kubernetes]cpu/memory у двух регистраций; - в пределе — второй Deployment раннера (вот тогда будет 2 пода) с привязкой к своим нодам и своим
concurrent.
То есть «2 пода» — это осознанный выбор ради изоляции, а не вариант по умолчанию. По умолчанию один прораб тащит всё, и это нормально.
Что проверять при диагностике¶
concurrentиlimitв каждом[[runners]];cpu_*/memory_*в[runners.kubernetes]по регистрациям;KUBERNETES_*переменные в.gitlab-ci.yml;kubectl top podsи зависшие job-поды вPending;- в GitLab: Runners → сколько джоб одновременно у каждого раннера.
Мораль¶
«Один под — два раннера — и где-то Kaniko» дичью кажется ровно до тех пор, пока держишь в голове «раннер = воркер». Поменяйте картинку на прораб + временные бригады, и всё встаёт на места:
- manager-под = прораб: один, лёгкий, только раздаёт работу;
- два раннера = два удостоверения у прораба (разные теги), а не два тела;
- Kaniko и node-сборка = два разных каменщика, каждый в своём временном поде, никогда не вместе;
- ресурсы = прораб почти ничего не ест; реальная нагрузка — в эфемерных job-подах, а их число режет
concurrent, размер —kubernetes-настройки, а ноды у всех общие.
Самое приятное: когда модель верная, и тюнинг становится очевидным. Надо больше параллелизма — крути concurrent. Надо, чтобы билды не душили тесты — дай им limit или отдельные ноды. Надо железную изоляцию — заведи второй Deployment. Никакой магии, просто прораб и бригада.