Монорепозитории: пожалуйста, не надо (часть 2)

в 14:11, , рубрики: bazel, Git, monorepo, монорепозиторий, системы сборки, Системы управления версиями, управление разработкой

Всем привет!

Итак, новая порция обещанного холивара про монорепозитории. В первой части мы обсуждали перевод статьи уважаемого инженера из Lyft (и ранее Twitter) о том, какие есть недостатки у монорепозиториев и почему они нивелируют почти все достоинства этого подхода. Лично я во многом согласен с доводами, приведенными в оригинальной статье. Но, как и обещал, чтобы поставить точку в этом обсуждении, я бы хотел озвучить еще несколько моментов, на мой взгляд даже более важных и более практических.

Расскажу чуть-чуть о себе — я работал и в маленьких проектах, и в относительно больших, использовал полирепозитории в проекте с более 100 микросервисов (и SLA 99,999%). В данный момент занимаюсь переводом небольшого монорепозитория (на самом деле нет, всего лишь фронт js + бэкенд java) с maven на bazel. Не работал в Google, Facebook, Twitter, т.е. не имел удовольствия использовать правильно настроенный и обвешанный тулингом монорепозиторий.

Итак, для начала, что же такое монорепозиторий? Комментарии к переводу оригинальной статьи показали, что многие считают, что монорепозиторий, это когда все 5 разработчиков компании работают над одним репозиторием и хранят в нем фронтэнд и бэкенд вместе. Конечно же, это не так. Монорепозиторий, это способ хранения всех проектов компании, библиотек, инструментов для сборки, плагинов для IDE, скриптов деплоя и всего прочего в одном большом репозитории. Подробности здесь trunkbaseddevelopment.com.

Как же тогда называется подход, когда компания небольшая, и у нее просто нет такого количества проектов, модулей, компонентов? Это тоже монорепозиторий, только маленький.
Естественно, в оригинальной статье говорится о том, что все описанные проблемы начинают проявляться на определенном масштабе. Поэтому те, кто пишет, что их монорепозиторий на 1,5 землекопа отлично работает, конечно же абсолютно правы.

Итак, первый факт, который хотелось бы зафиксировать: монорепозиторий — это отличное начало для вашего нового проекта. Складывая весь код в одну кучку, на первых порах вы получите только одни преимущества, т.к. поддержка нескольких репозиториев безусловно добавит немного накладных расходов.

В чем же тогда проблема? А проблема, как было отмечено в оригинальной статье, начинается на определенном масштабе. И главное, не упустить момент, когда такой масштаб уже наступил.

Поэтому я склонен утверждать, что по сути проблемы, которые возникают, это не проблемы самого подхода «сложи весь свой код в одну кучку», а это проблемы просто больших репозиториев исходных кодов. Т.е. если предположить, что вы использовали полирепозитории для разных сервисов/компонентов, и один из этих сервисов стал таким большим (насколько большим, обсудим чуть ниже), то вы скорее всего получите ровно те же самые проблемы, только еще и без преимуществ монорепозиториев (если они, конечно, есть).

Итак, насколько же большим должен быть репозиторий, чтобы начать считаться проблемным?
Определенно существует 2 показателя, от которых это зависит — количество кода и количество разработчиков, работающих с этим кодом. Если ваш проект имеет терабайты кода, но при этом с ним работает 1-2 человека, то скорее всего, они почти не заметят проблем (ну или по крайней мере, будет проще ничего не предпринимать, даже если заметят :)

Как же определить, что уже пора задумываться над тем, как оздоровить свой репозиторий? Конечно, это субъективный показатель, скорее всего ваши разработчики начнут жаловаться, что их что-то не устраивает. Но проблема в том, что может быть уже поздно что-то менять. Приведу некоторые цифры от себя лично: если клонирование вашего репозитория занимает больше 10 минут, если сборка проекта занимает более 20-30 минут, если количество разработчиков превышает 50 и так далее.

Интересный факт из личной практики:

я работал над довольно большим монолитом в команде из примерно 50 разработчиков, разделенных на несколько небольших команд. Разработка велась в фича-бранчах, а мердж происходил перед самым фича-фризом. Однажды я потратил на мердж нашей командной ветки 3 дня, после того как передо мной замерджились 6 других команд.

Теперь давайте пройдемся по списку тех проблем, которые возникают в больших репозиториях (часть из них была затронута в оригинальной статье, часть нет).

1) Время скачивания репозитория

С одной стороны, можно сказать, что это разовая операция, которую разработчик выполняет при начальной настройке своей рабочей станции. Лично у меня часто бывают ситуации, когда хочется склонировать проект в соседнюю папку, поковыряться в нем, а затем удалить. Однако, если клонирование занимает больше 10-20 минут, это сделать будет уже не так комфортно.

Но кроме того, не стоит забывать, что перед сборкой проекта на CI-сервере нужно клонировать репозиторий на каждого билд-агента. И вот тут вы начинаете придумывать, как же сэкономить это время, ведь если каждая сборка будет занимать на 10-20 минут дольше, а результат сборки появляться на 10-20 минут позже, это не устроит никого. Так репозиторий начинает появляться в образах виртуальных машин, из которых разворачиваются агенты, появляется дополнительная сложность и дополнительные расходы на поддержку этого решения.

2) Время сборки

Это довольно очевидный пункт, который обсуждался многократно. По сути, если у вас много исходных кодов, то сборка в любом случае будет занимать немалое время. Знакомая ситуация, когда после изменения одной строчки кода приходится ждать по полчаса, пока изменения пересобираются и тестируются. По сути выход здесь только один — использовать систему сборки, построенную вокруг кэширования результатов и инкрементальной сборки.

Вариантов здесь не так уж и много — несмотря на то, что в тот же самый gradle были добавлены возможности кэширования (к сожалению, не использовал на практике), практической пользы они не приносят из-за того, что традиционные системы сборки не обладают повторяемостью результатов (reproducible builds). Т.е. из-за сайд-эффектов предыдущей сборки все равно в какой-то момент необходимо будет вызвать очистку кэшей (стандартный подход maven clean build). Поэтому остается только вариант использовать Bazel/Buck/Pants и иже с ними. Почему это не очень хорошо, обсудим чуть ниже.

3) Индексирование IDE

Мой текущий проект индексируется в Intellij IDEA от 30 до 40 минут. А ваш? Конечно можно открыть только часть проекта или исключить из индексирования все ненужные модули, но… Проблема в том, что переиндексирование происходит при каждом переключении с одной ветки на другую. Вот почему я люблю клонировать проект в соседнюю директорию. Некоторые приходят к тому, что начинают кэшировать кэш IDE :)
<Картинка с Ди-Каприо с прищуренным глазом>

4) Логи сборки

Какой CI-сервер вы используете? Предоставляет ли он удобный интерфейс для просмотра и навигации в логах сборки размером в несколько Гигабайт? К сожалению, мой нет :(

5) Сломанные тесты

Что происходит, если кто-то смог запушить сломанные тесты/некомпилящийся код в мастер? Вы конечно скажете, что ваш CI не позволяет делать такого. А что насчет нестабильных тестов, которые проходят у автора, и ни у кого кроме? А теперь представьте, что этот код растекся на машины 300 разработчиков, и ни один из них не может собрать проект? Что делать в такой ситуации? Ждать, когда автор заметит и исправит? Исправлять за него? Откатывать изменения? Конечно в идеале, стоит коммитить только хороший код, и писать сразу без багов. Тогда такой проблемы возникать не будет.
(для тех, кто в танке и не понял намеков, речь о том, что негативный эффект, если это происходит в репозитории с 10 разработчиками и в репозитории с 300 будет немного разный)

6) Merge bot

Слышали когда-нибудь о такой штуке? Знаете, зачем она нужна? Будете смеяться, но это еще один инструмент, который не должен был бы существовать :) Вот представьте, что время сборки вашего проекта составляет 30 минут. И у вас над проектом работает 100 разработчиков. Предположим, что каждый из них пушит по 1 коммиту в день. Теперь представьте честный CI, который позволяет мерджить изменения в мастер только после того, как они были применены к самому свежему коммиту из мастера (rebase).

Внимание, вопрос: сколько времени должно быть в сутках, чтобы такой честный CI-сервер смог смерджить изменения от всех разработчиков? Правильный ответ, 50. Кто ответил верно, может взять с полки пряник. Ну или представьте, как вы только что отребейзили свой коммит на самый последний коммит в мастер, запустили сборку, а когда она завершилась, мастер уже уехал на 20 коммитов вперед. Все сначала?

Так вот merge bot или merge queue — это такой сервис, который автоматизирует процесс ребейза всех мердж реквестов на свежий мастер, прогон тестов и непосредственно сам мердж, а также может еще и объединять коммиты в батчи и тестировать их вместе. Очень удобная штука. Смотрите mergify.io, k8s test-infra Prow от Google, bors-ng и др. (обещаю написать про это подробнее в будущем)

Теперь к менее техническим проблемам:

7) Использование единого build tool

Честно говоря, для меня до сих пор загадка, зачем собирать весь монорепозиторий с помощью одной общей системы сборки. Почему бы не собирать javascript Yarn'ом, java — gradle'ом, Scala — sbt и т.д.? Если кто-то знает ответ на этот вопрос (не догадывается или предполагает, а именно знает) напишите в комментариях.

Конечно, это кажется очевидным, что использование одной системы сборки лучше, чем нескольких разных. Но все же понимают, что любая универсальная вещь заведомо хуже, чем специализированная, т.к. она скорее всего имеет только подмножество функций всех специализированных. Но что еще хуже, разные языки программирования могут иметь разные парадигмы в плане сборки, управления зависимостями и т.д., которые будет очень трудно завернуть в одну общую обертку. Не хочу вдаваться в детали, приведу один пример про bazel (подробностей ждите в отдельной статье) — мы нашли 5 независимых имплементаций правил сборки javascript для bazel от 5 разных компаний на GitHub, наряду с официальной от Google. Стоит задуматься.

8) Общие подходы

В ответ на оригинальную статью CTO из Chef написал свой ответ Monorepo: please do!. В своем ответе он утверждает, что «главное в монорепо, это то, что он заставляет разговаривать и делает недостатки видимыми». Он имеет в виду, что когда вы захотите изменить свой API, то вы вынуждены будете найти все его использования и обсудить свои изменения с мейнтейнерами этих кусков кода.

Так вот мой опыт ровно противоположный. Понятно, что это очень сильно зависит от инженерной культуры в команде, но я вижу в этом подходе сплошные минусы. Представьте, что у вас используется некий подход, который верой и правдой служил вам на протяжении какого-то времени. И вот вы решили по какой-то причине, решая схожую проблему, использовать немного другой способ, возможно более современный. Какова вероятность того, что добавление нового подхода пройдет ревью?

В своем недавнем прошлом я несколько раз получал комментарии типа «у нас уже есть проверенный путь, используй его» и «если хочешь внедрить новый подход, обнови код во всех 120 местах, где используется старый подход и получи аппрув от всех команд, которые ответственны за эти куски кода». Обычно на этом энтузиазм «инноватора» заканчивается.

А сколько, по-вашему, будет стоить написание нового сервиса на новом языке программирования? В полирепозитории — нисколько. Создаешь новый репозиторий и пишешь, да еще и берешь самую подходящую системы сборки. А теперь то же самое в монорепозитории?

Я прекрасно понимаю, что «стандартизация, переиспользование, совместное владение кодом», но проект должен развиваться. По моему субъективному мнению, монорепозиторий скорее препятствует этому.

9) Open source

Недавно меня спросили: "а есть ли open source инструменты для монорепозиториев?" Я ответил: «Проблема в том, что инструменты для монорепозиториев, как ни странно, разрабатываются внутри самого монорепозитория. Поэтому выложить их в open source оказывается довольно затруднительно!»

Для примера посмотрите на проект на Github с плагином для bazel для Intellij IDEA. Google разрабатывает его в своем внутреннем репозитории, а затем «выплескивает» части от него в Github с потерей истории коммитов, без возможности отправить pull request и так далее. Я не считаю это open source (вот пример моего небольшого PR, который был закрыт, вместо мерджа, а затем изменения появились в следующей версии). Кстати этот факт упомянут в оригинальной статье, что монорепозитории мешают выкладыванию в open-source и созданию коммьюнити вокруг проекта. Думаю, многие не придали особого значения этому аргументу.

Альтернативы

Что ж, если говорить о том, что же делать, чтобы избежать всех этих проблем? Совет ровно один — стремитесь иметь репозиторий как можно меньшего размера.
А при чем же здесь монорепозиторий? А ровно при том, что этот подход лишает вас возможности иметь маленькие, легкие и независимые репозитории.

Какие недостатки есть у подхода полирепозиториев? Я вижу ровно 1: невозможность отслеживать, кто является потребителем твоего API. Особенно это касается подхода в микросервисах «share nothing», при котором код не шарится между микросервисами. (Кстати, как думаете, этот подход кто-нибудь использует в монорепозиториях?) Эту проблему, к сожалению, необходимо решать либо организационными средствами, либо пытаться использовать инструменты для браузинга кода, которые поддерживают независимые репозитории (например, https://sourcegraph.com/).

А что насчет комментариев типа «мы пробовали полирепозитории, но потом нам пришлось постоянно имплементить фичи сразу в нескольких репозиториях, что было утомительно, и мы слили все в один котел»? Ответ на это очень простой: «не надо путать проблемы подхода с неправильной декомпозицией». Никто не утверждает, что в репозитории должен лежать ровно один микросервис и все. В мою бытность использования полирепозиториев мы прекрасно складывали семейство тесно связанных микросервисов в один репозиторий. Тем не менее, с учетом того, что сервисов было более 100, таких репозиториев было больше 20. Самое главное, о чем нужно думать в плане декомпозиции, это то, как эти сервисы будут деплоиться.

А как же аргумент про версии? Ведь монорепозитории позволяют не иметь версий и деплойть все из одного коммита! Во-первых, версионирование — это самая простая из всех озвученных здесь проблем. Даже в такой старой штуке как maven есть maven-version-plugin, который позволяет зарелизить версию всего в один клик. А во-вторых, и в-главных, есть ли у вашей компании мобильные приложения? Если да, то у вас уже есть версии, и вы никуда от этого не денетесь!

Ну есть же еще самый главный аргумент в поддержку монорепозиториев — он позволяет сделать рефакторинг по всей кодовой базе в один коммит! На самом деле, нет. Как было упомянуто в оригинальной статье, из-за ограничений, которые накладывает деплой. Вы должны всегда иметь в виду, что какое-то продолжительное время (продолжительность зависит от того, как у вас выстроен процесс) вы будете иметь 2 версии одного и того же сервиса параллельно. Например, на моем прошлом проекте у нас система находилась в таком состоянии несколько часов при каждом деплое. Это приводит к тому, что нельзя проводить глобальные рефакторинги, затрагивающие интерфейсы взаимодействия, в один коммит даже в монорепозитории.

Вместо заключения:

Итак, тем уважаемым и немногочисленным коллегам, которые работают в Google, Facebook и т.д. и придут сюда защищать свои монорепозитории, хочется сказать: «Не волнуйтесь, вы все делаете правильно, наслаждайтесь своим тулингом, на который было потрачено сотни тысяч или миллионов человекочасов. Они уже потрачены, так что если вы не будете использовать, то и никто не будет».

А всем остальным: «Вы не Google, не используйте монорепозитории!»

П.С. как заметил многоуважаемый Bobuk в подкасте радио-Т при обсуждении оригинальной статьи: «в мире есть ~20 компаний, которые умеют в монорепозиторий. Остальным даже не стоит пытаться».

Автор: kanu

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js