
Сложно организовать совместную работу большой команды, тем более над общей кодовой базой, такой как Shopify. Наш монолит меняется по 40 раз на дню. Мы отслеживаем разработку в trunk-based рабочем процессе и ежедневно вливаем в мастер по 400 коммитов. У нас три правила безопасного деплоя, но с ростом масштаба разработки их становилось всё труднее соблюдать. Небольшие конфликты ломали основную ветку, медленные развёртывания увеличивали разрыв между ней и продакшном, а скорость деплоя критических изменений замедлилась из-за отставания пул-реквестов. Чтобы решить эти проблемы, мы обновили Merge Queue (наш инструмент для автоматизации и управления скоростью мержей в основную ветку). Теперь он интегрирован с GitHub, запускает непрерывную интеграцию (CI) перед слиянием с основной веткой, удаляет запросы, которые не вошли в CI, и увеличивает скорость развёртывания.
Наши три основных правила безопасного деплоя и обслуживания основной ветки (мастера):
- Мастер всегда должен быть зелёным (через CI), чтобы была возможность деплоить из него в любое время. Зелёный мастер — это значит, что основная ветка всегда успешно компилируется и проходит все этапы сборки. В противном случае разработчики не могут влить изменения в ветку, тормозя процесс во всей компании.
- Мастер должен быть близок к продакшну. Уход слишком далеко вперёд увеличивает риски.
- Экстренные слияния должны быть быстрыми. В случае ЧП мы должны быть в состоянии быстро внести исправления.
Merge Queue v1
Два года назад мы выкатили первую итерацию очереди в нашем опенсорсном инструменте непрерывного развёртывания Shipit. Наша цель состояла в том, чтобы не дать основной ветке уйти слишком далеко от продакшна. Вместо непосредственного слияния с мастером разработчики добавляют пул-реквесты в очередь Merge Queue от их имени.

Merge Queue v1
Пул-реквесты не сливаются с основной веткой сразу, а накапливаются в очереди. Merge Queue v1 контролировала размер очереди и предотвращала слияние, когда в мастере было слишком много изменений, которые ещё не выкатили в продакшн. Это уменьшило риск сбоев и возможного отставания продакшна. Во время инцидентов мы блокировали очередь, предоставляя место для аварийных исправлений.

Браузерное расширение Merge Queue v1
Через браузерное расширение Merge Queue v1 разработчики отправляли пул-реквесты в очередь слияния в интерфейсе GitHub. Оно также позволяло быстро накатывать исправления во время чрезвычайных ситуаций, минуя очередь.
Проблемы с Merge Queue v1
Merge Queue v1 отслеживала пул-реквесты, но система CI не работала на пул-реквестах, которые находятся в очереди. В некоторые неудачные дни — когда из-за инцидентов приходилось приостанавливать деплои — в очереди на слияние скапливалось более 50 пул-реквестов. Объединение и развёртывание очереди такого размера может занять несколько часов. Также никакой гарантии, что пул-реквест в очереди пройдёт через CI после слияния с основной веткой, поскольку между запросами в очереди могут быть мягкие конфликты (два независимых пул-реквеста проходят CI по отдельности, но не вместе).
Основной головной болью стало браузерное расширение. Новые разработчики иногда забывали его установить, иногда выполняли слияние напрямую в основную ветку вместо отправки пул-реквеста в очередь. Это грозило разрушительными последствиями, если деплой уже сильно отстал или очередь поставлена на паузу из-за инцидента.
Merge Queue v2
В этом году мы выпустили вторую версию очереди — Merge Queue v2. Мы сосредоточились на оптимизации пропускной способности за счёт сокращения времени простоя очереди и улучшения UI, заменив браузерное расширение более интегрированным интерфейсом. Мы также хотели решить проблемы, которые не могли решить с прежней версией системы: держать мастер зелёным и быстрее накатывать аварийные исправления. Кроме того, наше решение должно было противостоять ненадёжным тестам, которые завершаются с непредсказуемым результатом.
Отказ от браузерного расширения
В Merge Queue v2 реализовали новый интерфейс. Хотелось, чтобы он был интуитивно понятен разработчикам, знакомым с GitHub. Мы черпали вдохновение из системы Atlantis, которую уже использовали в своей установке Terraform, и сделали интерфейс на основе комментариев.

Merge Queue v2 с интерфейсом на основе комментариев
На каждый пул-реквест выдаётся приветственное сообщение с инструкциями по использованию очереди слияния. Каждое слияние теперь начинается с комментария
/shipit
. Он отправляет веб-хук в нашу систему, сообщая о новом пул-реквесте. Мы проверяем, что пул-реквест прошёл CI и одобрен рецензентом, прежде чем добавить его в очередь. В случае успеха на этот комментарий выдаётся ответ с положительным эмодзи через addReaction из GitHub GraphQL.addReaction(input: {
subjectId: $comment_id
content: thumbs_up
})
Другие комментарии к пул-реквесту сообщают об ошибках, таких как недопустимая ветвь или отсутствующие отзывы.
addComment(input: {
subjectId: $pr_id
body: $error_message
})
Слияние напрямую в мастер минуя очередь снижает общую пропускную способность, поэтому мы программно отключили возможность слияния непосредственно в мастер с помощью функции защиты ветвей GitHub, которая является частью процесса интеграции очереди.
createBranchProtectionRule(input: {
repositoryId: $repository_id
pattern: 'master'
# This is how we disable to merge pull request button for non-admins.
restrictsPushes: true
# Admins should be able to use the merge button in case merge queue is broken
# The app also depends on this to merge directly in emergencies
isAdminEnforced: false
})
Тем не менее, нам все ещё нужна возможность прямых слияний в обход очереди, когда происходит чрезвычайная ситуация. Для этих случаев мы добавили отдельную команду
/shipit --emergency
, которая блокирует любые проверки, и код вливается непосредственно в мастер. Это помогает донести до разработчиков, что прямые слияния зарезервированы только для чрезвычайных ситуаций, и у нас есть возможность проверки каждого такого пул-реквеста.Сохранять основную ветку зелёной
Чтобы сохранять основную ветку зелёной, мы ещё раз посмотрели, как и когда вносим в неё изменения. Если перед слиянием в мастер мы запускаем CI, то гарантируем слияние только зелёных изменений. Это улучшает качество локальной разработки, устраняя число обращений к сломанному мастеру и ускоряя деплой, не беспокоясь о задержках из-за неудачной сборки.
Здесь мы решили создать так называемую «прогнозную ветвь» (predictive branch), где объединяются пул-реквесты и запускается CI. Это возможная будущая версия мастера, однако этой веткой по-прежнему можно свободно манипулировать. Мы избегаем локального чекаута, чтобы не поддерживать stateful-систему и не рисковать синхронизацией, и вместо этого взаимодействуем с данной веткой через GraphQL GitHub API.
Для гарантии, что прогнозная ветвь на GitHub согласуется с нашим желаемым состоянием, мы используем шаблон, похожий на Virtual DOM в React. Система создаёт в памяти представление желаемого состояния и запускает разработанный нами алгоритм согласования, который выполняет необходимые мутации в состояние на GitHub. Алгоритм согласования синхронизирует наше желаемое состояние с GitHub в два шага. Первый шаг — отбросить устаревшие коммиты слияния. Это созданные в прошлом коммиты, которые больше не нужны для желаемого состояния дерева. Второй шаг — создать недостающие коммиты на слияние. Как только они созданы, инициируется соответствующий запуск CI.
Такая схема позволяет свободно изменять желаемое состояние при изменении очереди и устойчива к десинхронизации.

Merge Queue v2 запускает CI на очереди коммитов
Чтобы сохранять основную ветку в состоянии готовности (зелёной), нужно ещё удалить из очереди пул-реквесты, которые не проходят CI, чтобы предотвратить каскадные сбои для последующих пул-реквестов. Однако наш основной монолит Shopify, как и многие другие большие кодовые базы, страдает от ненадёжных тестов. Из-за это нам не хватает уверенности, удалять или не удалять пул-реквест из очереди. Хотя мы продолжаем дорабатывать тесты, но ситуация такая, какая есть, и система должна справляться с ней.
Мы добавили порог отказоустойчивости и удаляем пул-реквесты только в том случае, если число последовательных отказов превышает этот порог. Идея в том, что реальные сбои сохранятся при последующих запусках, а ложная тревога не подтвердится. Высокий порог увеличит точность, но требует больше времени. Чтобы найти компромисс, можно проанализировать данные с результатами ненадёжных тестов. Предположим, что вероятность ложного срабатывания составляет 25%. Посчитаем вероятность нескольких последовательных ложных срабатываний.
Порог отказоустойчивости | Вероятность |
---|---|
0 | 25% |
1 | 6,25% |
2 | 1,5% |
3 | 0,39% |
4 | 0,097% |
Из этих цифр ясно, что с повышением порога вероятность значительно снижается. Она никогда не снизится ровно до нуля, но в уже порог 3 достаточно близко приближает нас к этому. Это означает, что при четвёртом последовательном сбое мы удалим из очереди пул-реквест, который не проходит CI.
Увеличение пропускной способности
Ещё одна важная задача Merge Queue v2 — увеличить пропускную способность. Развёртывание должно идти непрерывно, при этом надо следить, что каждый деплой содержит максимальное количество пул-реквестов, которое прошли проверку.
Чтобы гарантировать постоянный поток готовых пул-реквестов, Merge Queue v2 сразу запускает CI для всех пул-реквестов, которые добавляются в очередь. Такая предусмотрительность весьма кстати во время инцидентов, когда очередь блокируется. Поскольку CI выполняется до слияния с основной веткой, то ещё до разрешения инцидента и разблокировки очереди у нас уже есть готовые к развёртыванию пул-реквесты. На следующем графике видно, что количество пул-реквестов в очереди увеличивается во время блокировки очереди, а затем уменьшается по мере её разблокировки и немедленного слияния готовых пул-реквестов.

Чтобы оптимизировать количество пул-реквестов для каждого деплоя, мы разделяем их в очереди на пакеты. Под пакетом понимается максимальное количество пул-реквестов, которое можно обработать за один деплой. Теоретически, большие пакеты повышают пропускную способность очереди, но также повышают риск. На практике слишком большое повыше��ие риска снижает пропускную способность, вызывая сбои, которые труднее изолировать, и увеличивает число откатов. Для своего приложения мы выбрали размер пакета в 8 пул-реквестов. Это своеобразный баланс между пропускной способностью и рисками.
В каждый момент времени у нас CI работает на трёх пул-реквестах из очереди. Наличие ограниченного количества пакетов гарантирует, что ресурсы CI расходуются только на то, что скоро понадобится, а не весь набор пул-реквестов. Это помогает снизить затраты и использование ресурсов.
Выводы
За счёт внедрения Merge Queue v2 мы повысили удобство, улучшили безопасность и пропускную способность деплоя в продакшн. Хотя для текущего масштаба все цели достигнуты, по мере дальнейшего роста придётся пересмотреть наши модели и предположения. Мы сосредоточим следующие шаги на удобстве и обеспечении разработчикам контекста для принятия решений на каждом этапе. Очередь Merge Queue v2 дала нам гибкость для продолжения разработки, и это только начало наших планов по масштабированию деплоя.