Pull to refresh

Как Shopify организовала совместную работу более 1000 разработчиков

Reading time7 min
Views3.4K
Original author: Jack Li


Сложно организовать совместную работу большой команды, тем более над общей кодовой базой, такой как 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 дала нам гибкость для продолжения разработки, и это только начало наших планов по масштабированию деплоя.
Tags:
Hubs:
Total votes 8: ↑7 and ↓1+6
Comments4

Articles