Почему мы переехали в монорепозиторий?
Ранее «Лаборатория Касперского» выбрала модель разработки со множеством репозиториев. У каждой команды был собственный репозиторий. Многие репозитории имели различные системы контроля версий (например, Perforce, Git, TFS и другие). Чтобы внести изменения в код соседней команды, разработчику приходилось подстраиваться под непривычную систему контроля версий и иные конвенции, что вызывало у него трудности.
Помимо того, что команды жили в своих репозиториях с различными системами контроля версий, многие из них поддерживали собственную сборочную и тестовую инфраструктуры. На это тратились ресурсы каждой команды, а также разработчику, вносящему изменения в чужой репозиторий, приходилось приспосабливаться к другой инфраструктуре.
Количество связей между командами постоянно росло, увеличивалось число взаимных коммитов между репозиториями и одновременных коммитов в несколько репозиториев. Чтобы сократить расходы на поддержку таких связей, мы решили перенести все проекты в общий git-репозиторий и выбрали модель разработки trunk-based development.
Помимо переезда в общий репозиторий, мы решили, что для работы с ним и ради сокращения накладных расходов на поддержку собственной инфраструктуры в командах мы будем делать общую инфраструктуру в проекте Monorepo.
Что из себя представляет Monorepo?
Размер репозитория — около 50 Гб. В него ежедневно делается около 350 пулл-реквестов. А число уникальных контрибьюторов в день — около 150. Эти показатели постоянно увеличиваются, потому что все больше команд переезжает в Monorepo.
Инфраструктура Monorepo — это:
общий сборочный конвейер Asgard, который компилирует около 75 млн единиц трансляции в день на 7000 процессорных ядер в составе 250 серверов;
система для запуска тестов Hive, производящая около 13 млн тестовых результатов в день на 8000 виртуальных машин;
распределенное хранилище артефактов AIR;
системы для автоматического и ручного анализа результатов тестов Dashboard.
Кроме этого, у нас используется система управления репозиторием и пайплайнами Azure DevOps и есть единая команда поддержки всей этой инфраструктуры.
Также наши разработчики адаптируют сборочную систему bazel для описания сборок.
Как мы организовали разработку в монорепозитории?
При внесении изменений в код разработчик создает пулл-реквест, в нем запускается валидационный билд для проверки его изменений. В этом билде по зависимостям сначала собирается код общих базовых компонентов под различные платформы, а потом под них собираются и тестируются основные продукты компании. При внесении изменений в конкретный продукт система зависимостей не запустит пересборку кода базовых компонентов. Валидационный билд с максимальным путем занимает сейчас приблизительно 2,5 часа.
В нашем представлении сборка или запуск тестов под одну платформу — это одна уникальная задача. Таких задач в валидационном билде — около 150.
С какими проблемами сталкивается разработчик?
Так как многие наши инфраструктурные подсистемы находятся в процессе разработки, в них всплывают различные проблемы, которые вызывают нестабильность валидационного билда. Помимо этого, проблемы возникают и в пользовательском коде — в сборочных скриптах и тестах. Каждая из подсистем инфраструктуры и пользовательский код могут привносить в билд небольшой процент нестабильности, однако суммарно для разработчика это может превратиться в большую нестабильность билда в рамках пулл-реквеста и, как следствие, затруднения при внесении изменений в код.
Как понять, почему упал билд?
Мы хотим, чтобы разработчик решал минимум проблем, которые не связаны с его изменениями. Поэтому мы начали думать о том, чтобы выявлять эти проблемы как можно раньше.
Мы начали смотреть на валидационные билды, которые запускаются в пулл-реквесте. При большом количестве красных точек, как на графике выше, сложно определить, привнесена ли проблема изменениями, сделанными в пулл-реквесте, не посмотрев логи и не разобравшись в проблеме.
Чтобы отделить проблемы, привнесенные в пулл-реквестах, от ломающих и нестабильных проблем валидационного билда, мы решили запускать валидационный билд пулл-реквеста каждый час по ветке master. В идеальном случае, если нет никаких ломающих и нестабильных проблем, такой билд должен быть всегда зеленым. Однако оказалось, что это не всегда так.
Таким образом мы поняли, что билд по расписанию — это «показатель здоровья» Monorepo. Во-первых, он помогает предотвратить проблемы на ранней стадии: мы видим проблему не в пулл-реквестах разработчиков, а на синтетической метрике, заводим инцидент и разбираемся с ним. Во-вторых, такой билд легко мониторить, потому что не нужно разбираться, связано ли это с изменениями в пулл-реквесте разработчика, поэтому по ошибке данного билда легко найти ответственных. Кроме того, видны глобальные проблемы на билде по расписанию: если падает несколько билдов с одной и той же проблемой — вероятнее всего, возникла глобальная проблема. Наконец, для каждой ошибки такого валидационного билда у нас есть инцидент — баг.
Как мы систематизируем проблемы?
Валидационные билды по master'у у нас запускаются каждый час. Если билд покраснел, мы заводим инцидент. Далее мы разбираемся, почему упал тот или иной билд, и за каждый красный билд пулл-реквеста по master'у мы находим ответственного — подсистему, из-за которой возникло падение.
Благодаря тому, что мы научились искать ответственных за падение валидационных билдов пулл-реквеста по расписанию, мы смогли строить различные статистики. Например, мы можем увидеть, кто больше виноват в падениях валидационного билда и над какими подсистемами сейчас нужно работать.
В итоге с момента введения этой метрики в октябре 2019 года мы добились того, что к маю 2021 года количество проходящих билдов по расписанию увеличилось с 55.31% до 81.1%. А значит, увеличилась и стабильность валидационного билда в пулл-реквесте.
Как разработчик узнает, из-за чего упал билд?
Помимо того, что мы научились мониторить проблемы на синтетическом билде, нам необходимо уведомить разработчика о существовании проблемы, обозначить статус решения проблемы, ускорить перезапуск билда. Для этих целей нами был создан робот Thor — хранитель пулл-реквеста.
Thor умеет отслеживать глобальные проблемы — те, которые не решатся даже при перезапуске валидационного билда (например, если в репозитории появились несовместимые коммиты или проблема возникла из-за обновления инфраструктуры). Когда Thor видит глобальные проблемы, он ставит на паузу валидационный билд, сообщает в каждом пулл-реквесте разработчиков, что билд находится на паузе, и прилагает ссылку на инцидент. В результате каждый разработчик может посмотреть, кто разбирается с проблемой, и увидеть прогноз по срокам решения. Когда проблема решена, инцидент закрывается, билд снимается с паузы и Thor планомерно перезапускает упавшие билды.
Thor также умеет обрабатывать нестабильности — проблемы, которые решаются перезапуском валидационного билда (например, гонки в сборочной процедуре, флакающие тесты, нестабильности в инфраструктуре). Если одна и та же проблема воспроизводится на нескольких независимых пулл-реквестах разработчиков, то мы заводим на это инцидент и начинаем решать проблему. При каждом падении валидационного билда в пулл-реквесте разработчика Thor проверяет, заведен ли инцидент по этой проблеме, и дает под текстом ошибки валидационного билда в пулл-реквесте разработчика ссылку на инцидент, сообщая, когда он перезапустит валидационный билд. Благодаря этой функции разработчик может не тратить свое время на анализ этих проблем и перезапуск билда. Таким образом, около 69% билдов, упавших по причине нестабильности, мы перезапускаем автоматически.
Кроме того, Thor еще и бот. Его можно призвать в пулл-реквест на помощь, запустить расширенные сборки и тесты. Разработчики могут воспользоваться еще рядом мелких возможностей.
Помимо обработки ошибок, Thor помогает разработчикам разбираться с проблемами, которые они привнесли, делая изменения в своем пулл-реквесте. При падении валидационного билда робот сообщает непосредственно в пулл-реквест, какая произошла ошибка, и дает прямую ссылку на артефакты. Кликнув на эту ссылку, разработчик может сразу начать разбираться с проблемой и не тратить время на ее поиск.
Мелочь, а приятно!
Для наших разработчиков мы стали автоматически перезапускать expired-билды. Сейчас в нашем репозитории время экспирации билда составляет 24 часа: даже если билд был успешным, но разработчик не успел закомплитить пулл-реквест за 24 часа, то билд экспайрится. То есть если разработчик делает пулл-реквеста в пятницу, то в понедельник его придется перезапускать и ждать, пока билд пройдет. Мы автоматизировали этот процесс и стали перезапускать протухшие билды разработчиков в выходные.
Как мы повышаем стабильность валидационного билда
Мы боремся с нестабильностью тестов в пулл-реквестах, тем самым повышая стабильность валидационного билда. Под нестабильностью тестов мы понимаем различные результаты теста при запуске по одной сборке.
У нас есть механизм flaky-тестов. Любой тест можно пометить как flaky, при этом к тесту линкуется инцидент, в рамках которого проблему с тестом будут решать. Если при запуске билда в нем упадет flaky-тест, то билд все равно будет считаться успешным. После решения инцидента, в рамках которого разбирались с нестабильностью этого теста, flaky снимается, и тест переходит в блокирующий режим.
Пометку flaky можно сделать как вручную, так и автоматически. Если в различных пулл-реквестах разработчиков тест падает по одной и той же проблеме, то этот тест мы считаем нестабильным, и он автоматически помечается как flaky. Благодаря тому, что часть тестов помечена как flaky, около 4% от всех билдов проходят успешно.
Таймлайн для удобства разработчиков
Чтобы было удобно отследить весь жизненный цикл пулл-реквеста, мы сделали таймлайн. В нем видны перезапуски валидационных билдов, причины падения, какие были паузы за время, пока длился пулл-реквест, сколько длилось код-ревью. Каждый разработчик может посмотреть и разобраться, почему его пулл-реквест долго заходил и какие проблемы с ним возникали.
Как измерить удовлетворенность разработчиков?
Мы все измеряем и принимаем решения на основе данных. В том числе измеряем и удовлетворенность разработчика. Мы считаем количество перезапусков валидационных билдов после последнего коммита в ветку пулл-реквеста до его завершения.
В идеальном случае, когда разработчик делает последний апдейт в своем пулл-реквесте, у него запускается валидационный билд, который должен стать зеленым с первого раза. Однако если после последнего апдейта валидационный билд сначала был красным, а после какого-то количества ретраев стал зеленым без изменения кода, то в подобном случае с большой вероятностью красные валидационные билды упали из-за нестабильности. В статистике отражены процент пулл-реквестов, завершенных без ретраев, с одним ретраем, с двумя и более после последнего апдейта. Мы считаем, что чем больше пулл-реквестов, зашедших без ретраев, тем больше удовлетворены разработчики.
Мы продолжим развивать наши инструменты и инфраструктуру, а значит, улучшать удовлетворенность разработчиков и сохранять их спокойствие.
Некоторое время назад «Лаборатория Касперского» решила перенести свои проекты по разработке в монорепозиторий с общей инфраструктурой. Мы решили поделиться опытом и рассказать, с какими проблемами сталкиваются разработчики в выбранном подходе, и как мы научились их решать.
AlexSpaizNet
Вы же бинарники компилируете? Не веб-сервисы?
У нас раз в 1000 меньше кода, разработчиков и пул реквестов. Но мы деплоим в веб. И мы ушли от монорепо. Еще не до конца, ибо распиливаем. Но новые сервисы в новых рипо по доменам. Не суть.
Одна из главных причин ухода от монорепо это ожидание своей очереди мержа в мастер и последующего деплоя.
То есть:
Ты создал пиар
Запустились тесты и если все хороше можешь мержить
Но пока твои билды бежали кто то уже замержил свой бранч в мастер, и там соответственно опять пробегаются тесты, билд и т.д.
Делаешь апдейт мастера в свой бранч и ждешь опять пока все тесты пробегут
Пытаешь опять замержить в мастер - а там опять кто то успел свой бранч запихнуть.
Надоело... Почему я должен ждать всех если изменения у меня в моем небольшом сервисе?
Понятно что можно автоматизировать. Но все равно придется ждать всех. А мы стартап. И так много проблем а тут еще и деплой ждать чтобы потом проверить все ли там норм.
С отдельными репо, небольшими, ты не зависишь от других команд и разработчиков.
Сколько у вас разработчик ждет от нажатия кнопки "деплой" (не уверен что это слово подходит в вашем случае) до нотификации - готово?
Я так понимаю в ваших масштабах билды как в интеле могут бежать от нескольких часов до суток и ожидание каких-то пару часов мержа вообще не проблема?
shark14
Обычно в монорепозиториях при мерже в мастер триггерятся билды и тесты не всего репозитория, а только какого-то его подмножества, которое потенциально могло быть затронуто.
Соответственно, разработчики, которые разрабатывают код за пределами этого подмножества, могут все делать как обычно, как будто вы ничего не мержили.
Gugic
> Но пока твои билды бежали кто то уже замержил свой бранч в мастер, и там соответственно опять пробегаются тесты, билд и т.д.
Так а какая в сущности разница? Билды по конкретному замерженному коммиту добегут, протестируются и автоматизация подхватит его и пушнет на тестовый сервер. Все будут точно знать что вот этот конкретный коммит прошел все тесты и билдится нормально и с него можно разливать в staging и прод впоследствии.
А то что где-то там параллельно другие билды бегут — так и хорошо, и пусть бегут.
ApeCoder
Один разработчик переименовал метод, другой использовал старое имя в новом месте. Параллельно оба билда проходят но при методе пулл реквестовт мастер перестал собираться
Gugic
Это ведь проблема не спецефичная для монореп.
netch80
Но в малой репе сильно легче отличить коммиты, которые наверняка не конфликтуют — они будут в разных репах :)
Gugic
В правильно приготовленной монорепе обычно очень чистый и понятный граф зависимостей, и отличить неконфликтующие коммиты также довольно просто - если графы не пересекаются, конфликта точно нет. Плюс в монорепе вы не сможете даже закоммитать ломающий чейндж в какую-нибудь либу, которая в случае не монорепы будет с некоторой долей вероятности лежать в отдельном репозитории. В монорепе просто сломается билд. В немонорепе (при условии отдельного хранения либы, скажем, над ней работает отдельная команда) проблема вылезет только на этапе интеграционных тестов.
netch80
Уже говорилось, что для этого нужно одно маленькое, но очень важное условие: чтобы при каждом коммите (или голове цепочки коммитов если так делают в конкретном подходе) проверялось всё, что зависит от этой библиотеки. А в большой репе это значит или всё перепроверять на каждый коммит (очень дорого), или каким-то образом находить компоненты, что зависят от этой библиотеки. Но если кто-то умеет искать такие компоненты, то что ему мешает точно так же искать это и в разных репах?
Что вы называете тут "интеграционными тестами" и почему оно не применяется по-вашему в случае монорепы? Проверка зависящих от этой библиотеки — это интеграционные тесты или нет? Почему это определение может зависеть от того, одна репа или несколько?
В правильном сетапе множества реп — точно так же. А в неправильном — и в монорепе чёрт-те что. Один коллега рассказывал, как он участвовал в написании кода телевизора одного великого корейского концерна. Из полсотни гиг вполне себе монорепы половина тупо не использовалась, была артефактом каких-то древних состояний, но выкинуть их было категорически запрещено (несмотря на то, что история бы сохранилась). И почему я должен предполагать, что монорепа будет использована как положено, а не как свалка, в которой уже всем пофиг, что за пределами его каталога?
vkni
На этом этапе можно ставить «automerge». То есть, PR, его рецензия, дальше прогоняются тесты и как только они прошли, идёт merge автоматом. То есть, если вы ушли пить чай, то вы не пропустили окно.
В идеале, что monorepo, что много разных должны быть одинаковы в этом аспекте. Ведь, по-факту, тесты должны проверять только то, что вы меняли (и зависящие от ваших изменений компоненты). То есть, когда появляются два независимых PR, при правильно настроенной системе тестирования они будут совершенно независимы — никакого merge conflict, никаких пересечений по тестам. И, соответственно, прекрасно сольются.
В микрорепозитариях, на первый взгляд нет конфликтов. Но только на первый взгляд. Если в с коллегой модифицируете две работающие в синергии библиотеки libA и libB из разных репозитариев, то при правильно настроенной системе тестирования вы должны получить конфликт, если ваши изменения несогласованы. Значит, ваши тесты вашего PR в репозитарии libA должны перезапускаться из-за слияния PR в репозитарии libB! То есть, в чистом виде ситуация монорепо!
Таким образом, при правильно настроенной системе тестирования компонент различие между monorepo и большим кол-вом мелких репозитариев несущественна. Но, блин, очень сложно эту систему правильно настроить и поддерживать.
AlexSpaizNet
Понятно что с микро-репозиториями свои проблемы и сложности. Но там они возникают в конкретных случая и как бы выше на уровне шеред стафф и интерфейсов взаимодействия.
Да, если возможно в монорепо сделать так что реально тестируется только код который изменился и только сервисы использующие эти тесты то по идее нужно будет только ждать тесты при создании пиара (при условии что другие команды реально коммитят только в свои файлики и они не затронут тебя и не затриггерят твои тесты). Тогда придется ждать только мерж который в принципе быстрый. Вот только это сложно сделать в монорепо с монолитом + отдельные сервисы + shared dependencies и т.д. Ну и nodejs в придачу =))
Рально не знаешь где выстрелит. Поэтому приходится прогонять все тесты и билдить все и вся внутри :/
Решили что отдельные репы будет быстрее организовать. Хотя да, не очень удобно для разработчика. Когда все стянул с одного репо и у тебя все в одной папочке, удобнее. Но вроде привыкли уже. Но самое главное что боль уходит и теперь свои фичеры можно задеплоить за 5 минут.
Еще отдельная боль как результат того что билдится все и вся в монорепо - со временем появились тесты которые падают рендомно и никто не знает как их чинить. А те кто их писал уже не работают тут. Бывает задеплоить одну строчку кода берет пол дня потому что оно падает а ответственного за этот код хрен найдешь.
vkni
Кмк, тут разница в том, что в монорепо вы делаете «интеграционные тесты» по-умолчанию, а в отдельных репо вы их по-умолчанию не делаете. И то, и то плохо.
Они и в другом случае появятся. То есть, я не вижу причины, почему они не должны появиться. Просто там тесты прогоняются реже, поэтому эта проблема скрыта. Но не является ли она индикатором нарушения каких-то контрактов кода? Может быть они бы в каком-то мелком репо давно падают, просто вы их уже пол года не запускали?
vkni
Есть чудесная статья blog.ffwll.ch/2017/08/github-why-cant-host-the-kernel.html
В ней рассказывается, что наши инструменты (git/GitHub) недостаточны для управления огромными проектами вроде ядра Linux. Ещё аналогичное, видимо, можно написать по системам сборки. Кмк, в этом и ключ проблемы монорепо/мульти-репо.
RoadTrain
Насколько я понял, у вас валидационная политика "протухает" при каждом коммите в мастер.
В идеале, конечно, так и нужно, но в реальности в крупных репозиториях возникают проблемы с толпой разработчиков, пытающихся протолкнуть свой PR в мастер.
Рабочий trade-off в данном случае -- более умная экспирация политики. TFS, например, из коробки умеет в покоммитное протухание (как у вас было) и в экпирацию по времени. В статье указано, что сейчас экспирация политики составляет 24 часа, что позволяет разработчикам планово закатывать свои изменения в мастер. Минусом является возможность внесения ломающих изменений в мастер, но она довольно тривиально решается автоматическим мониторингом и ревертом ломающих коммитов.
AlinaVlasova Автор
Да, в целом все правильно. У нас действительно валидационные билды в пулл-реквестах запускаются параллельно и имеют время экспирации 24 часа. Поэтому в общем случае никого ждать не приходится. И описанная проблема с параллельными коммитами нам не чужда, но за счет мониторингов описанных в данной статье, мы научились достаточно быстро их замечать и откатывать.