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

Наши три основных правила безопасного деплоя и обслуживания основной ветки (мастера):

  1. Мастер всегда должен быть зелёным (через CI), чтобы была возможность деплоить из него в любое время. Зелёный мастер — это значит, что основная ветка всегда успешно компилируется и проходит все этапы сборки. В противном случае разработчики не могут влить изменения в ветку, тормозя процесс во всей компании.
  2. Мастер должен быть близок к продакшну. Уход слишком далеко вперёд увеличивает риски.
  3. Экстренные слияния должны быть быстрыми. В случае ЧП мы должны быть в состоянии быстро внести исправления.

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 дала нам гибкость для продолжения разработки, и это только начало наших планов по масштабированию деплоя.