open atlas
↑ К треку
CI/CD-пайплайны CICD · 06 · 04

Инфраструктура раннеров и стоимость CI на масштабе

Счёт за CI — лишь половина стоимости: вторая — время очереди, помноженное на инженеро-часы. Запускай ephemeral-раннеры, автоскейль под очередь, ставь на spot ради экономии 60-90%, right-size и следи за p95 времени очереди.

CICD Senior ◷ 18 min
Уровень
ОсновыJuniorMiddleSenior

Распоряжение пришло после того, как флапающий сторонний раннер слил токен: «перенести всё на большие hosted-раннеры, для надёжности». Сработало. И заодно сделало две вещи, которые никто не просчитал. Месячный счёт за CI вырос в 10 раз — самая большая машина на каждой задаче, даже на той, что линтит README. А когда финансы в ответ зажали concurrency, чтобы отыграть счёт назад, PR-ы начали стоять в очереди по 20+ минут на пике. Дашборд, на который все смотрели, показывал длительность задачи стабильной — выглядело нормально. Чего он не показывал — время очереди: сорок инженеров, каждый ждёт двадцать минут свободного раннера, по несколько раз в день. Счёт за раннеры, из-за которого они воевали, был погрешностью рядом с инженеро-часами, которые они сжигали в очереди. Этот урок — о флоте раннеров под твоими пайплайнами: hosted против self-hosted, ephemeral против persistent, автоскейлинг, spot и кривая стоимости, у которой две оси, а не одна.

Hosted против self-hosted: кто владеет машиной

Прежде чем погружаться: вопрос не только в том, какие раннеры дешевле поминутно. Когда учитываешь ожидание инженеров, ответ часто переворачивается — именно поэтому этот урок начинается с модели стоимости, а не со сравнения фич.

GitHub-hosted раннеры дают тебе чистую ephemeral-VM (одноразовую, уничтожаемую после задачи) на каждую задачу с нулевыми операциями на твоей стороне — ты платишь поминутно и никогда ничего не патчишь. Это верный дефолт, пока не кусает одно из трёх: счёт на большом объёме, потолок спецификации (нужны большие машины, GPU или конкретное железо, которого нет в hosted-каталоге) или сетевой доступ (задача должна достучаться до сервисов внутри твоего VPC). Self-hosted раннеры закрывают все три — твои машины, дешевле на объёме, любая спецификация, доступ к VPC, — но ты наследуешь патчинг, скейлинг и безопасность.

Эта оговорка про безопасность — не сноска. Self-hosted раннер, привязанный к публичному репозиторию, опасен: pull request от незнакомца запускает его код на твоей инфраструктуре. Держи self-hosted раннеры подальше от публичных репозиториев или закрывай их обязательным одобрением для контрибьюторов-новичков.

# Джоба, запиненная на твой self-hosted флот по лейблу.
jobs:
  build:
    runs-on: [self-hosted, linux, x64, ephemeral]  # лейблы выбирают флот
    steps:
      - uses: actions/checkout@v4
      - run: ./ci/build.sh

Ephemeral-раннеры: чистая комната на каждую задачу

Именно эта практика делает self-hosted безопасным. Ephemeral-раннер исполняет ровно одну задачу, после чего уничтожается и пересоздаётся с нуля. Persistent-раннер остаётся поднятым и берёт задачу за задачей — и накапливает между ними состояние: оставшиеся файлы в рабочей директории, отравленный кеш зависимостей, фоновый процесс, порождённый предыдущей (возможно вредоносной) задачей. Этот осадок — канал заражения. Поздняя задача может прочитать оставленные секреты, попасть на подменённый бинарник или унаследовать испорченный кеш — и ничто не воспроизводимо, потому что каждая задача стартует оттуда, где предыдущая оставила машину.

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

# Зарегистрировать self-hosted раннер, который завершается после одной джобы и не переиспользуется.
./config.sh --url https://github.com/acme/payments \
  --token "$REG_TOKEN" \
  --labels linux,x64 \
  --ephemeral            # <- уничтожить после одной джобы; состояние не выживает
./run.sh
Почему это работает

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

Автоскейлинг: следуй за очередью, а не за догадкой

Фиксированный флот из N раннеров имеет два режима отказа, и константой из обоих не вырулить. Слишком мало раннеров — и задачи встают в очередь: разработчики ждут, и настоящая цена — это простой инженера, а не простой машины. Слишком много — и ты платишь за простой раннеров, что сидят тёплыми без дела. Какое бы N ты ни выбрал, оно неверно в какой-то момент дня, потому что спрос рваный: тихо в 3 ночи и громовой табун, когда все пушат перед стендапом.

Автоскейлинг заставляет N следовать за спросом. Actions Runner Controller (ARC) запускает ephemeral-раннеры как поды Kubernetes, которые скейлятся под очередь задач: когда очередь растёт, он поднимает больше подов-раннеров; когда она пустеет, он скейлит до нуля, так что простаивающие часы не стоят ничего. Ты получаешь лучшее из обоих — мощность при табуне, ноль счёта, когда его нет, — и, что важно, раннеры всё ещё ephemeral, так что автоскейлинг и изоляция-чистая-комната складываются, а не дерутся.

# ARC: автоскейлинговый набор ephemeral-раннеров (скейлится под очередь, вплоть до нуля).
apiVersion: actions.github.com/v1alpha1
kind: AutoscalingRunnerSet
spec:
  githubConfigUrl: https://github.com/acme/payments
  minRunners: 0          # скейл до нуля, когда очередь пуста
  maxRunners: 200        # потолок, чтобы табун не разорил
  template:
    spec:
      containers:
        - name: runner
          image: ghcr.io/actions/actions-runner:latest

У кривой стоимости две оси

Вот сеньорный пересмотр, который команда из военной истории упустила. Спроси себя: когда ваша команда спорила о счёте за раннеры, кто-нибудь мерял, сколько инженеро-часов сгорало в очереди? Стоимость CI — не одно число. Это $ за раннеры плюс $ инженерного времени всех, кто ждёт CI. Оптимизируй только счёт — меньше раннеров, меньше машины, зажатый concurrency — и ты раздуешь время очереди и wall-clock, что умножается на каждого ждущего инженера. Инженеро-часы организации затмевают её счёт за CI, так что истинный оптимум — обычно больше параллелизма, а не меньше: добавляй раннеры, пока wall-clock не перестанет улучшаться, потому что дальше этой точки ты платишь за машины, которые не сокращают ожидание, но до неё каждая срезанная минута умножается на команду.

Два рычага делают «больше параллелизма» дешёвым. Первый — spot / preemptible инстансы (облачные машины по рыночной цене, которые провайдер может прервать в любой момент, но в среднем дешевле в 3–10 раз): CI взаимозаменяем и безопасен к ретраю — вытесненная задача просто перезапускается, — так что это идеальная нагрузка для spot, срезающая счёт за раннеры на 60-90%. Второй — right-sizing: большинству задач нужен маленький раннер; резервируй большие машины для тех немногих задач, что реально ускоряются на них. Линт README на 64-ядерном раннере — чистая растрата.

Выбери лучший вариант

Твой счёт за CI высок И PR-ы стоят в очереди по 20 минут на пике. Инженеры простаивают в очереди по несколько раз в день. Какой дизайн флота чинит и то, и другое разом?

Викторина

Ты выбираешь между GitHub-hosted и self-hosted раннерами. Какое утверждение схватывает суть трейда, который ты на самом деле делаешь?

Викторина

Почему автоскейлинговый флот ephemeral-раннеров бьёт фиксированный флот persistent-раннеров для нагруженной организации с рваным спросом?

Вспомните перед уходом
  1. 01
    Объясни, почему ephemeral-раннеры стоят своей платы за запуск на задачу против быстрого persistent-флота, и как автоскейлинг (ARC) складывается с ними.
  2. 02
    Разложи двухосевую модель стоимости CI и вытекающие из неё решения по флоту: hosted против self-hosted, spot, right-sizing и метрику, за которой следить.
Итог

Флот раннеров под твоими пайплайнами решает и счёт за CI, и скрытую стоимость ожидания инженеров. GitHub-hosted раннеры — дефолт с нулём операций — чистая ephemeral-VM на задачу, поминутно, — пока счёт на большом объёме, потолок спецификации (большие машины, GPU) или доступ к VPC не вынудят тебя на self-hosted, где ты получаешь контроль над стоимостью и спецификацией, но владеешь патчингом, скейлингом и безопасностью и никогда не должен привязывать self-hosted раннеры к публичным репозиториям, потому что PR незнакомца запустится на твоей инфраструктуре. Практика, делающая self-hosting безопасным, — ephemeral-раннеры: каждый раннер исполняет ровно одну задачу, затем уничтожается, давая чистую комнату на задачу, тогда как persistent-раннер накапливает межзадачное состояние (оставшиеся файлы, отравленный кеш, бродячий процесс), которое утекает между задачами и невоспроизводимо; плата за запуск на задачу смягчается тёплыми пулами и быстрыми образами, но изоляцию иначе не получить. Фиксированный флот отказывает обоими способами — слишком мало раннеров встают в очередь и тратят время инженеров, слишком много платят за простой, — так что автоскейлинг через Actions Runner Controller запускает ephemeral-раннеры как поды Kubernetes, скейлящиеся под очередь и до нуля на простое. Наконец, у стоимости CI две оси: счёт за раннеры плюс ожидание инженеров, и раз инженеро-часы затмевают счёт, оптимум обычно больше параллелизма, а не меньше — ставь безопасную к ретраю нагрузку CC на spot/preemptible инстансы ради среза счёта на 60-90%, делай right-sizing, чтобы большие машины обслуживали лишь те задачи, что выигрывают, и следи за p95 времени очереди, а не за длительностью задачи, ведь время очереди невидимо в логе прогона, но прямо умножает ожидание разработчиков. Теперь, когда слышишь «CI медленный» — первый вопрос не о длительности задачи, а о том, каково p95 времени очереди.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 5 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.