company_banner

Разработка в монорепозитории. Доклад Яндекса

    Моё имя Азат Разетдинов, я в Яндексе уже 12 лет, руковожу службой разработки интерфейсов в Я.Недвижимости. Сегодня я хотел бы поговорить про монорепозиторий. Если у вас всего один репозиторий в работе — поздравляю, вы уже живете в монорепозитории. Теперь о том, зачем он нужен другим.



    Как сказала руководитель службы разработки API Яндекс.Карт Марина Перескокова — посадил дед монорепу, выросла монорепа большая-пребольшая.

    — Мы в Яндексе пробовали разные способы работы с несколькими сервисами и заметили — как только у тебя появляется больше одного сервиса, неизбежно начинают возникать общие части: модели, утилиты, инструменты, куски кода, шаблоны, компоненты. Встает вопрос: куда все это девать? Конечно, можно копипастить, мы это умеем, но хочется же красиво.

    Мы пробовали даже такую сущность, как SVN externals, для тех, кто помнит. Мы пробовали git-сабмодули. Мы пробовали npm-пакеты, когда они появились. Но все это было как-то долго, что ли. Ты поддерживаешь какой-нибудь пакет, находишь ошибку, вносишь исправления. Затем тебе нужно выпустить новую версию, пройтись по сервисам, обновиться на эту версию, проверить, что все работает, запустить тесты, обнаружить ошибку, вернуться обратно в репозиторий с библиотекой, поправить ошибку, выпустить новую версию, пройтись по сервисам, обновиться и так по кругу. Это просто превращалось в боль.



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

    Лично для меня самое важное — атомарность коммитов, то, что я как разработчик могу одним коммитом поправить библиотеку, обойти все сервисы, внести изменения, запустить тесты, проверить, что все работает, запушить в мастер, и все это одним изменением. Не нужно ничего пересобирать, публиковать, обновлять.

    Но если все так хорошо, почему в монорепозиторий еще не переехали все? Конечно, в нем есть и минусы.



    Как сказала руководитель службы разработки API Яндекс.Карт Марина Перескокова — посадил дед монорепу, выросла монорепа большая-пребольшая. Это факт, не шутка. Если вы собираете много сервисов в одном монорепозитории, он неизбежно разрастается. А если мы говорим про git, который вытягивает все файлы плюс всю их историю за все время существования вашего кода, это довольно большой дисковый объем.

    Вторая проблема — вливание в мастер. Ты подготовил пул-реквест, прошел ревью, уже готов его сливать. И выясняется, что кто-то успел вперед тебя и тебе нужно разрешать конфликты. Ты разрешил конфликты, опять готов вливать, и ты опять не успел. Эта задача решается, есть системы merge queue, когда специальный робот автоматизирует эту работу, выстраивает пул-реквесты в очередь, пытается разрешить конфликты, если может. Если не может — призывает автора. Тем не менее, такая проблема существует. Есть решения, которые ее нивелируют, но нужно иметь ее в виду.

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

    Интересно, а кто еще кроме Яндекса и других игроков использует монорепозиторий? Довольно много кто. Это React, Jest, Babel, Ember, Meteor, Angular. Люди понимают — проще, дешевле, быстрее разрабатывать и публиковать npm-пакеты из монорепозитория, чем из нескольких маленьких репозиториев. Самое интересное, что вместе с этим процессом начали развиваться инструменты работы с монорепозиторием. Как раз о них и хочу поговорить.

    Все начинается с создания монорепозитория. Самый известный во фронтенд-мире инструмент для этого называется lerna.



    Достаточно открыть ваш репозиторий, запустить npx lerna init, он задаст вам несколько наводящих вопросов и добавит несколько сущностей в вашу рабочую копию. Первая сущность — это конфиг lerna.json, в котором указываются как минимум два поля: сквозная версия всех ваших пакетов и расположение ваших пакетов в файловой системе. По умолчанию все пакеты складываются в папку packages, но это вы можете настроить как угодно, можете даже в корень складывать, lerna это тоже умеет подхватывать.

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

    Чего нам хочется добиться? Скорее всего, у вас уже есть какие-то репозитории, в данном случае А и В.



    Это два сервиса, каждый в своем репозитории, и мы хотим их перенести в новый монорепозиторий в папку packages, желательно с сохранением истории коммитов, чтобы можно было сделать git blame, git log и так далее.



    Для этого есть инструмент lerna import. Вы просто указываете расположение вашего репозитория, и lerna переносит его в вашу монорепу. При этом она, во-первых, берет список всех коммитов, модифицирует каждый коммит, меняя путь к файлам с корня на packages/название_пакета, и применяет их друг за другом, накладывает их в ваш монорепозиторий. Фактически препарирует каждый коммит, изменяя пути файлов в нем. По сути, lerna занимается git-магией за вас. Если вы почитаете исходный код, там просто команды git исполняются в определенной последовательности.

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



    Для более плавного перехода в монорепу есть такой инструмент как git subtree. Это более навороченная штука, но при этом нативная для git, которая позволяет не только импортировать отдельные репозитории в монорепозиторий по какому-то префиксу, но и обмениваться изменениями туда и обратно. То есть команда, которая делает сервис, может спокойно разрабатываться дальше в своем отдельном репозитории, при этом вы можете подтягивать их изменения через git subtree pull, вносить свои правки и пушить их обратно через git subtree push. И жить так в переходном периоде сколь угодно долго.

    А когда вы все настроили, проверили, что все тесты запускаются, деплой работает, весь CI/CD настроен, вы можете сказать, что пора переходить. Для переходного периода отличное решение, рекомендую.

    Хорошо, мы перевезли наши репозитории в один монорепозиторий, но где магия-то? Мы же хотим выделять общие части и как-то их использовать. И для этого есть механизм «связывание зависимостей». Что такое связывание зависимостей? Есть инструмент lerna bootstrap, это команда, которая похожа на npm install, просто запускает npm install во всех ваших пакетах.



    Но это не всё. Кроме того она ищет внутренние зависимости. Вы можете внутри своего репозитория в одном пакете использовать другой. Например, если у вас есть пакет А, который зависит в данном случае от Jest, есть пакет В, который зависит от Jest и от пакета A. Если пакет А — это общий инструмент, общий компонент, то пакет В — это сервис, который его использует.

    Lerna определяет такие внутренние зависимости и заменяет физически на файловой системе эту зависимость на символическую ссылку.





    После того, как вы запускаете lerna bootstrap, прямо внутри папки node_modules вместо физической папки А появляется символическая ссылка, которая ведет на папку с пакетом А. Это очень удобно, потому что вы можете править код внутри пакета А и тут же проверять результат в пакете В, запускать тесты, интеграционные, юниты, что хотите. Сильно упрощается разработка, вам не нужно больше пересобирать пакет А, публиковать, подключать пакет В. Просто здесь поправили, там проверили.

    Обратите внимание, если посмотреть на папки node_modules, и там, и там есть jest, у нас дублируется установленный модуль. И вообще это довольно долго, когда вы запускаете lerna bootstrap, ждете, пока все остановится, из-за того, что много всякой повторной работы, в каждом пакете получаются задублированные зависимости.

    Чтобы ускорить установку зависимостей, используется механизм подъема зависимостей. Идея очень простая: можно взять и общие зависимости унести в корневой node_modules.



    Если указать опцию --hoist (это подъем с английского), то почти все зависимости просто переедут в корневой node_modules. Причем это работает почти всегда. Нода так устроена, что если она не нашла зависимостей на своем уровне, она начинает искать на уровень выше, если там нет — еще на уровень выше и так далее. Практически ничего не меняется. А по сути, мы взяли и дедуплицировали наши зависимости, перенесли зависимости в корень.

    При этом lerna достаточно умная. Если есть какой-то конфликт, например, если бы пакет А использовал Jest версии 1, а пакет В — версии 2, то один из них всплыл бы наверх, а второй остался бы на своем уровне. Это примерно то, чем на самом деле занимаются npm внутри обычной папки node_modules, он тоже пытается дедуплицировать зависимости и по максимуму нести их в корень.

    К сожалению, эта магия работает не всегда, особенно с инструментами, с Babel, с Jest. Часто бывает, что он начинает, поскольку в Jest есть своя система резолвинга модулей, Нода начинает лагать, бросать ошибку. Специально для таких случаев, когда инструмент не справляется с зависимостями, которые уехали в корень, есть опция nohoist, который позволяет точечно сказать, что эти пакеты в корень не переноси, оставляй их на месте.



    Если указать --nohoist=jest, то все зависимости кроме jest уедут в корень, а jest останется на уровень пакетов. Не зря я такой пример привел — именно у jest есть проблемы с таким поведением, и nohoist в этом помогает.

    Еще один плюс подъема зависимостей:



    Если у вас до этого были отдельные package-lock.json на каждый сервис, на каждый пакет, то при хойстинге у вас все переезжает наверх, и остается единственный package-lock.json. Это удобно с точки зрения вливания в мастер, разрешения конфликтов. Один раз все зарезолвили, и всё.

    Но каким образом lerna это достигает? Она довольно агрессивно химичит с npm. Когда вы указываете hoist, она берет ваш package.json в корне, бэкапит его, вместо него подставляет другой, агрегирует в него все ваши зависимости, запускает npm install, почти все ставится в корень. Затем этот временный package.json убирает, восстанавливает ваш. Если вы после этого запустите любую команду с npm, например, npm remove, npm не поймет, что произошло, почему вдруг все зависимости оказались в корне. Lerna нарушает уровень абстракции, она залезает в инструмент, который находится ниже ее по уровню.

    Первыми эту проблему заметили ребята из Yarn и сказали: чего мы мучаетесь, давайте мы вам все сделаем нативно, чтобы все из коробки работало.



    Yarn уже сейчас может из коробки делать все то же самое: связывать зависимости, если видит, что пакет В зависит от пакет А, он сделает за вас симлинку, даром. Он умеет поднимать зависимости, делает это по умолчанию, все складывает в корень. Как и lerna умеет оставлять единственный yarn.lock в корне репозитория. Все остальные yarn.lock больше вам не нужны.



    Настраивается он похожим образом. К сожалению, yarn предполагает, что все настройки добавляются в package.json, я знаю, есть люди, которые стараются все настройки инструментов оттуда уносить, оставить только минимум. К сожалению, yarn еще не научился указывать это в другом файле, только package.json. Там появляется две новые опции, одна новая и одна обязательная. Поскольку предполагается, что корневой репозиторий никогда не будет публиковать, yarn требует, чтобы там был указано private=true.

    А вот настройки workspaces хранятся в одноименном ключе. Настройка очень похожа на настройки lerna, есть поле packages, где вы указываете расположение ваших пакетов, и есть опция nohoist, очень похожая на опцию nohoist в lerna. Просто указываете эти настройки и получаете аналогичную структуру, как и в lerna. Все общие зависимости уехали в корень, а те, которые указаны в ключе nohoist, остались на своем уровне.



    Самое приятное, что lerna умеет работать с yarn и подхватывать его настройки. Достаточно указать в lerna.json два поля, lerna тут же поймет, что вы используете yarn, зайдет в package.json, достанет оттуда все настройки и будет работать с ними. Эти два инструмента уже знают друг про друга и работают в паре.



    А почему в npm до сих пор не сделали поддержку, если столько больших компаний использует монорепозиторий?


    Ссылка со слайда

    Говорят, что все будет, но в седьмой версии. Базовая поддержка в седьмой, расширенная — в восьмой. Месяц назад вышел этот пост, но при этом до сих пор даже неизвестна дата, когда выйдет седьмой npm. Ждем, когда он наконец догонит yarn.

    Когда у вас несколько сервисов в одном монорепозитории, неизбежно встает вопрос, как ими управлять, чтобы не ходить в каждую папку, не запускать команды? Для этого есть массовые операции.





    У yarn есть команда yarn workspace, потом название пакета и название команды. Поскольку yarn из коробки, в отличие от npm, умеет все три вещи: запускать собственные команды, добавить зависимость от jest, запускать скрипты из package.json, как test, а также умеет запускать исполняемые файлы из папки node_modules/.bin. Приучем он сам за вас с помощью эвристики поймет, что вы хотите. Очень удобно использовать yarn workspace для точечных операций над одним пакетом.

    Есть похожая команда, которая позволяет выполнить команду над всеми пакетами, что у вас есть.



    Указываете просто ваши команды со всеми аргументами.



    Из плюсов, очень удобно запускать разные команды. Из минусов, например, невозможно запускать shell-команды. Допустим, я хочу удалить все папки node modules, я не могу запустить yarn workspaces run rm.
    Невозможно указать список пакетов, например, хочу только в двух пакетах удалить зависимость, только по очереди или по отдельности.

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



    У lerna куда более интересный инструментарий, там есть две отдельных команды run и exec. Run умеет исполнять скрипты из package.json, при этом в отличие от yarn она умеет фильтровать все по пакетам, можно указать --scope, можно использовать звездочки, глобы, все довольно универсально. Можно запускать эти операции параллельно, можно игнорировать ошибки через ключ --no-bail.



    Exec очень похож. В отличие от yarn, он позволяет не только запускать исполняемые файлы из node_modules.bin, а исполнять любые произвольные shell-команды. Например, можно удалить node_modules или запустить какой-нибудь make, все что хотите. И поддерживается та же самая опция.



    Очень удобный инструментарий, одни плюсы. Это тот случай, когда lerna рвет yarn, находится на нужном уровне абстракции. Именно за этим lerna и нужна: упрощать работу с несколькими пакетами в монорепе.

    С монорепами есть еще один минус. Когда у вас налажен CI/CD, вы его никак не оптимизируете. Чем больше у вас сервисов, тем дольше это все происходит. Допустим, вы запускаете тестирование всех сервисов на каждый пул-реквест, и чем больше их становится, тем дольше идет работа. Для оптимизации этого процесса можно использовать селективные операции. Я назову три разных способа. Первые два из них можно использовать не только в монорепе, но и в ваших проектах, если вы эти способы почему-то не используете.

    Первый — lint-stages, позволяющий запускать линтеры, тесты, все что хотите, только на те файлы, которые изменились или будут закоммичены в данном коммите. Запускать весь lint не на весь ваш проект, а только на те файлы, которые поменялись.





    Настройка очень простая. Ставите lint-staged, husky, прекоммит-hooks и говорите, что при изменении любого js-файла нужно запустить eslint. Таким образом прекоммит-проверка сильно ускоряется. Особенно если у вас много сервисов, очень большой монорепозиторий. Тогда запускать eslint на все файлы слишком дорого, и можно таким способом оптимизировать прекоммит-hooks на lint.



    Если вы пишите тесты на Jest, у него тоже есть инструменты для селективного запуска тестов.



    Эта опция позволяет передать ему список исходных файлов и найти все тесты, которые так или иначе затрагивают эти файлы. Что можно использовать в связке с lint-staged? Обратите внимание, здесь я указываю не все js-файлы, а только исходники. Мы исключаем сами js-файлы с тестами внутри, смотрим только исходники. Запускаем findRelatedTests и сильно ускоряем прогон юнитов на прекоммит или на препуш, кому как удобно.

    И третий способ, связанный именно с монорепозиториями. Это lerna, которая умеет определять, какие пакеты поменялись в сравнении с базовым коммитом. Тут речь скорее не про хуки, а про ваш CI/CD: Travis или другой сервис, который вы используете.





    У команд run и exec есть опция since, позволяющая прогонять любую команду только в тех пакетах, которые поменялись с какого-то коммита. В простых случаях можно указать мастер, если вы все выливаете в него. Если хотите более точно, то лучше через ваш инструмент CI/CD указать базовый коммит вашего пул-реквеста, тогда это будет более честное тестирование.

    Поскольку lerna знает все зависимости внутри пакетов, она умеет определять и косвенные зависимости. Если вы поменяли библиотеку А, которая используется в библиотеке В, которая используется в сервисе С, lerna это поймет. Предположим, вы меняете код в библиотеке А. Тогда она транзитивно определит, что пакет C нужно перетестировать — например, с помощью написанного вами интеграционного теста. И lerna запустит эту команду на пакет С.

    Вот несколько ссылок, которые могут вам пригодиться: cайт lerna, рекомендация по yarn workspaces и теоретическое описание плюсов и минусов монорепозитория в принципе.

    Есть люди, которые любят монорепозитории. Есть люди, которые любят мультирепозитории. Здесь всегда вопрос компромисса. Что проще? Я для себя определил, что работающие по отдельности команды склонны работать независимо, потому что чем больше у них независимости, тем они счастливее. Но предположим, мы поднимаемся на уровень руководителя отдела или небольшой компании, где все по отдельности независимые. Тогда компания начинает терять, потому что какие-то вещи каждая команда делает по отдельности. Допустим, нужно перейти на новую версию Babel. Каждая команда по отдельности разбирается, что там поменялось, что нужно поменять в коде. И предприниматель или руководитель отдела тратит ресурсы компании на одну и ту же деятельность в пяти разных местах. Когда мы приходим в монорепозиторий, то можем эту общую деятельность вынести за скобки и сэкономить ресурсы.

    Хочу сказать спасибо своим коллегам: Мише mishanga Трошеву и Гоше Беседину. Они потратили довольно много времени на изучение инструментов, которые мы сегодня рассмотрели, и поделились опытом и знаниями. На этом всё, спасибо.
    Яндекс
    334,04
    Как мы делаем Яндекс
    Поделиться публикацией

    Комментарии 39

      –3
      Как сказала руководитель службы разработки API Яндекс.Карт Марина Перескокова — посадил дед монорепу, выросла монорепа большая-пребольшая.

      Не подмажешь — не поедешь

        +2
        Минусы: Размытие ответственности

        Хотелось бы понять, какая зона ответственности у конкретного разработчика? Например, один разработчик доработал или что-то исправил в общей библиотеке, и начал прогон тестов. И тут выяснилось, что его изменения что-то сломали в смежных проектах других команд. Кто будет это чинить? Если ответственность на создателе коммита, получается, ему теперь нужно худо бедно разбираться во всех проектах, заехавших в монорепу? Вместо 2kk строк кода поддерживать 20kk?

        Допустим, нужно перейти на новую версию Babel. Каждая команда по отдельности разбирается, что там поменялось, что нужно поменять в коде.

        Вопрос того же плана. Вот разработчик перенес проект своей команды на новый Babel. А в остальных 9 смежных проектах сборка сломалась. Теперь ему нужно будет тратить еще N времени на миграцию смежных проектов и проталкивании своих пул реквестов. А в это время смежным проектам может быть вообще не до обновлений. В таких условиях переход на новые инструменты может оказаться большой проблемой?
          –2
          Он может везде починить сам, в сложных случаях — обратиться за помощью к разработчикам этих сервисов. Монорепозиторий подталкивает к унификации всех проектов, поэтому задача разбираться в них всех не выглядит такой уж сложной. Опыт показывает, что суммарное время, которое один разработчик потратит на обновление всех проектов, меньше, чем если бы каждый из них обновлялся по отдельности.
            +4
            Боюсь, вы не учитываете психологию разработчиков. Пусть, суммарное время, которое один разработчик потратит на обновление всех проектов, и меньше (с чем еще можно поспорить). Но никто не хочет поддерживать кроме своего проекта еще и хотя бы один чужой. У каждого разработчика есть свои задачи, свои KPI, если хотите, и экспертиза в своем проекте. А разбираться в чужом не хочет никто — за это не похвалят.
            Так же несостоятелен и вариант обратиться за помощью к разработчикам сервисов: у каждой команды есть свой бэклог, свой ритм и свои проблемы которые требуют решения. У них нет мотивации удовлетворять запросы других разработчиков. Особенно, если их проект не получит никаких бонусов. В лучшем случае можно надеяться, что они возьмут задачу в работу через пару-тройку недель. А если таких команд несколько, то придется ждать их всех.
              –1
              Какую-то не дружелюбную среду вы описываете. Не в каждой фирме такие жесткие KPI, что нельзя помочь коллегам из соседней команды, или обновить общую инфраструктурную библиотеку в нескольких проектах сразу.
                +1
                тогда такому разработчику лучше не работать в такой компании. а вообще стоит расти как профессионал и забывать, что ответственность это только твой код написанный тобой.
                0
                Он может везде починить сам, в сложных случаях — обратиться за помощью к разработчикам этих сервисов. Монорепозиторий подталкивает к унификации всех проектов, поэтому задача разбираться в них всех не выглядит такой уж сложной. Опыт показывает, что суммарное время, которое один разработчик потратит на обновление всех проектов, меньше, чем если бы каждый из них обновлялся по отдельности.

                Вы в статье справдливо минусом монорепозиториев указываете размывание ответственности, и тут же в комментариях пишете что один разработчик может править код всех проектов.
                Беспрепятственное впиливание сторонним разработчиком нового функционала в любые сервисы, вместо каких-то универсальных точек расширения и обратной совместимости приведёт лишь к тому что всё связано со всем, на уровне приложения и на уровне команд.
                +1

                В паре компаний испытывали такие проблемы, решили разделением на разные проекты, из-за того, что это был монолит, было сложно, но оно того стоило.
                Если проекты просто в одном месте лежат — однозначно надо пытаться делить.

                  0

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

                  +1
                  И как всегда в статье про монорепу нет ни слова про Python/PyPI.
                    0
                    Доклад из фронтенд-секции конференции.
                      +1
                      Умею готовить Python/PyPI в монорепе, задавайте свои вопросы.
                        0

                        Вам удалось сделать монорепу, совместимую с PyPI?

                          0

                          Совместимость с PyPI — это возможность бесшовно вкорячивать пакеты с PyPI внутрь репозитория? Если да, то способ сделать такую монорепу есть.


                          Для bazel имеется набор правил https://github.com/bazelbuild/rules_python, в нем есть правило pip_import импорта внешних пакетов с PyPI. Они скачиваются и становятся доступными как объекты сборки наравне с собственными библиотеками монорепы. После первоначальной настройки вся работа заключается в том, что в корне репы поддерживается файл requirements.txt, а все бинарники в своих правилах сборки получают возможность указать необходимые зависимости из PyPI. Код импортирует эти зависимости как обычно.


                          При таком подходе герметичность может страдать, поскольку в bazel пока нельзя задавать контрольные суммы скаченных библиотек, а что там может случиться на pypi.org никто не знает. Эту проблему можно решить поддержкой собственного зеркала PyPI. Даже если отбросить разгерметизацию сборки, зеркало полезно, когда сборка начинает запускаться на десятках машин/сотни раз в день.


                          Правила сборки py_binary/par_binary из того же bazel позволяют запаковать код в нечто, похожее на монобинарь, вместе со всеми его зависимостями.


                          Ну и еще я знаю, что некоторые кладут в их монорепу сам код зависимостей. Про удобство рассуждать не могу, но такое тоже работает.

                            0

                            Ваши ответы вызывают у меня смутные подозрения по поводу размеров вашей монорепы. Если не секрет, сколько строк (или гигабайт) кода в ней? Один requirements.txt на всю репу означает, что никакие части вашей кодовой базы не имеет конфликта версий, что практически невозможно при достаточно большой кодовой базе в компании от тысячи сотрудников и выше.


                            Все идеологи монорепы, с которыми мне доводилось общаться, предлагали ее (и статическую линковку интерпретатора питона в один бинарь со всеми скриптами — для герметичности) в качестве лекарства от dependency hell, который неизбежно возникает при разрастании кодовой базы. При наличии лазеек вроде установки пакетов со внешнего PyPI нарушается герметичность, но это не самое страшное — по-хорошему эти пакеты еще должны проходить security audit (иначе вместе с новой версией можно притащить себе уязвимость). В итоге у вас один из трёх сценариев:


                            • засасывание в монорепу всего внешнего PyPI с несколькими версиями для каждого пакета
                            • свой third_party с утащенными PyPI пакетами, но давно протухшими (потому что security audit никто не отменял, а пакетов много)
                            • отдельный штат специально обученых обновлятелей зеркала PyPI, обновляющих все зависимости сначала во внешнем мире (иначе разъедется), а потом затягивающих обновления к себе.

                            Линковка всего в монобинарь сильно отличается от подхода virtualenv + pip install -r requirements.txt, поэтому зачастую вопрос выглядит именно как "PyPI или монорепа". Писать на интерпретируемом языке, а потом ждать сборки бинаря для меня до сих пор выглядит диковато.

                              0
                              Если не секрет, сколько строк (или гигабайт) кода в ней?

                              Не секрет, если взять только *.py файлы и выкинуть контрибы — 1.1M LOC, порядка сотни сервисов. Самих контрибов из PyPI больше 10M LOC. Кроме этого, в репо живет столько же JS кода и на порядок меньше С++/Go. И это, кстати, было гораздо более весомым аргументом в пользу Bazel, чем менеджмент Python и его зависимостей.


                              Один requirements.txt на всю репу означает, что никакие части вашей кодовой базы не имеет конфликта версий

                              Это так. Монорепа использовалась с самого начала (2017 год), количество зависимостей в requirements.txt (полный список, через pip freeze) — 318.


                              Три описанных сценария комментировать не буду, потому что ни один из них в полной мере не подходит. Редкие пакеты действительно могут быть старыми. Наугад потыкал сейчас в наиболее известные пакеты (numpy, flask, scipy, sqlalchemy) — все версии в пределах последних 2-4 месяцев. Обновление пакетов правда занимает больше времени, чем в случае мультирепы из-за необходимости фиксить тесты во всех местах. Из хорошего — в GitLabе есть инструменты для коллаборативных PR, так что в особо запущенные тесты можно позвать более близких к коду людей. Из-за конфликта версий можно подвиснуть и иногда даже приходится лезть во внешние гитхабы для рассылки фиксов. Это тоже проблема, но она еще ни разу не становилась полным блокером.


                              по-хорошему эти пакеты еще должны проходить security audit (иначе вместе с новой версией можно притащить себе уязвимость)

                              Здесь риски были приняты, потому что аудит кода — далеко не единственный контур защиты. Откровенная упячка не пройдет ревью, менее откровенная упячка фиксится коллективным разумом, подписанным на security digestы. В общем и целом же компромис сдвинут в сторону более быстрой разработки.


                              Писать на интерпретируемом языке, а потом ждать сборки бинаря для меня до сих пор выглядит диковато.

                              Понимаю о чем вы, мне поначалу тоже казалось странным. Со временем перестаешь обращать внимание. Ну либо у меня уже развился стокгольмский синдром:)

                      +4

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

                        0
                        это решается правильной билд системой, которая пожет понять, какие проекты необходимо прогнать.
                          +1

                          Какие билд системы уже так умеют?


                          Допустим, что конкретно для JS есть вот эта lerna, описываемая в статье. И которая требует, чтобы структура репозитория была перекроена под нее. А что с бэкэндом делать? Отдельный репозиторий? Но ведь это не монорепозиторий тогда.

                            0

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

                              0
                              lerna не требует перекраивать структуру репозитория. Можно хоть местоположение каждого сервиса указать отдельно в lerna.json.
                            0
                            Есть такая хорошая штука как импакт анализ.
                            Основная идея проста — гоняем только те тесты, которые есть смысл прогонять для данного изменения.
                            Так что значимое разбухание очереди тестов на CI будет только если каждый коммит меняет все модули всех проектов или общую зависимость всех проектов.
                            Как это сделано во фронтенде Яндекса — понятия не имею.
                            В одном из докладов Авито про автоматизацию Android приложения как раз рассказывалось как гонять тесты чаще но при этом быстрее. Вполне сойдет за идею для реализации в другой прикладной сфере.
                              +1
                              Извините, а вы последнюю часть про селективные операции читали?
                              +5

                              Если бы мы объединили свои проекты в монорепозиторий, получилось бы чудовище на пару десятков терабайт. Спасибо, нет :) Геймдев, если что.

                                0
                                А так орда монстриков?
                                +5

                                Какие-то плюсы сомнительные. Ну если для ребят работает, то и флаг им в руки.
                                Но чем плохи в таком случае сабрепозитории — хоть git, хоть hg, хоть любая другая система — мне осталось не ясным из статьи. Казалось бы: хочешь общие куски кода — подключи их как сабрепозитории и контролируй на какой версии этих кусков сидеть, когда обновляться и т.п.

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

                                    Так и слава богу что они не похожи на моно. Если весь вопрос в том что не будет новая версия общего кода сразу при пуше попадать во все репозитории которые ее используют, тем самым ломая их код и принуждая фиксить это прямо здесь и сейчас, то такое можно организовать скриптами и это будет намного гуманнее. К примеру автоматически фиксировать новую версию общего кода только когда и если все автотесты прошли успешно для этого конкретного репозитория, а иначе уведомлять разработчиков "ребята, фиксите свой код и сливайтесь с новым общим".
                                    А неконсистентность может быть только на период пока код в репозитории ломается от новой версии общего кода, но это и монорепозиторий не решает.

                                      0
                                      К примеру автоматически фиксировать новую версию общего кода только когда и если все автотесты прошли успешно для этого конкретного репозитория, а иначе уведомлять разработчиков «ребята, фиксите свой код и сливайтесь с новым общим».


                                      В правильно построенном CI в монорепозитории так и происходит: на каждый PR запускается набор необходимых тестов и зеленый свет на влитие включается только после их успешного прохождения.

                                      А неконсистентность может быть только на период пока код в репозитории ломается от новой версии общего кода, но это и монорепозиторий не решает.

                                      Ровно эту проблему монорепа и решает! Если поддерживать тесты в зеленом состоянии в каждом влитом пулл-реквесте, то в любой момент времени HEAD версия репозитория валидна и готова к бою.

                                      Ну а принуждение к исправлению проблем «здесь и сейчас» (т.е. перед влитием в мастер/транк) автором изменения вместо «потом и неизвестно кем», когда отработает некоторая механика — это и есть один из столпов монорепозиториев. Где-то такой подход считается более удачным. Но давайте не будем об этом спорить:)
                                  0

                                  Недавно в очередной раз исследовал вопрос монорепы в плане улучшения инструментов и обнаружил, с помощью гугла, что в основном инструментарий заточен под js экосистему. А для многоязычных проектов такое ощущение, что за три года ничего не появилось и лучшим вариантом остаются gut submodule с обвязкой из баша для таких вещей как детектировпние изменений для инкрементальных билдов и тестов.

                                    0
                                    а зачем submodule? это уже не монорепа получается.

                                    bazel/pants как билд система

                                    vfsforgit.org для оптимизации операций с git, если разрослись до больших маштабов.
                                      0

                                      Выглядит как монорепа, ведёт себя как монорепа...


                                      Я о том, что не могу найти нормального тулинга для настоящей монорепы, в которой живут исходники приложений и сервисов на разных языках и платформах, с разными системами управления зависимостей/пакетирования ( yarn/comooser/go mod вот прямо сейчас надо) и т. п., с поддержкой как независимого, так и синхронного бампа версий.


                                      На уровне центральной билд-CI-системы более-менее можно разрулить как раз, а вот для локальной разработки…

                                        0
                                        под билд системой, я скорее имел ввиду систему сборки, bazel/pants это как раз про управление зависимостями, версиями, и сборкой всего этого добра.

                                        go: www.pantsbuild.org/go_readme.html
                                        node: www.pantsbuild.org/node_readme.html
                                        jvm/python там же.

                                        для bazel там еще больше всего должно быть.
                                    +1
                                    >>Затем тебе нужно выпустить новую версию, пройтись по сервисам, обновиться на эту версию, проверить, что все работает, запустить тесты, обнаружить ошибку, вернуться обратно в репозиторий с библиотекой, поправить ошибку, выпустить новую версию, пройтись по сервисам, обновиться и так по кругу. Это просто превращалось в боль.
                                    Не в боль, а в рутину. Мне кажется тут поможет автоматизация тех процессов, которые вы описали.
                                      +1

                                      Занятно, что тому же make и инкрементальной компиляции — лет 50. Всякие .net и java умеют разбивать проект на подпроекты с пеленок.


                                      А в ноде надо отдельную тулу, которая лазит в гит чтобы понять что менялось, и делает симлинки — чтобы хакнуть историю с node_modules.

                                        0

                                        Если про фронт я ещё могу понять идею моно репа, то как вы относитесь к монорепу, где лежит и бжу и фронт?

                                          0

                                          Я вот сейчас в очередной раз (каждые 2-3 года приходится) рисерчу плюсы и минусы монорепы для всей системы.


                                          Явный плюс — гораздо легче вносить одновременно согласованные изменения во фронт и бэк, по крайней мере в общие ветки типа develop и master. Например, изменилось поле в джсон схеме ендпоинта бэка: при раздельных репах для фронта и бэка и наличии e2e тестов в пайплайнах мы заведомо идём на то что для кого-то из них тесты упадут: или бэк уже будет ожидать новую схему, а фронт ещё слать старую, или наоборот. Настройка процесса так, чтобы билд бэкенда, не прошедший тесты потом помечался как успешно прошедший во вребя билда фронта, не тривиальна. Да и от ложноотрицательных оповещениях не спасает. Так же гораздо проще разработчику или тестировщику равзвернуть систему в акутальном состоянии локально.


                                          Явный минус — для бэкендеров появляются "мусорные" коммиты, ветки, прочие вещи от фронтендеров и наоборот. Ну и сама репа тяжелее становится, но нам ещё далеко, до ситуации когда это может начать влиять. Мне пока кажется, что этим можно пренебречь, но продолжаю исследования в сторону поиска каких-то "эмуляторов" монорепы, начиная с git submodules/


                                          В целом склоняюсь к мнению, что монорепа больше проявляет свои плюсы, когда изменения если не всех, то большинство компонентов системы должны разворачиваться одновременно, а плюсы полиреп сильнее проявляются когда циклы разворачивания этих компонентов независимы, ресурсы в версионность и обратную совместимость их API вкладываются by design, а не для того, чтобы обойти минусы организации..

                                            0

                                            Меня больше всего волнует мусорность коммитов и, как было описано выше, коллизии с пулреквестами. Так же сталкивались со временем сборки, которая в лучшем варианте была полчаса. Что касается зависимости бэка и фронта, то в данном случае придётся тестировать сразу 2 ПР. Мы мокали заранее оговоренный сваггер и пушили в свои ветки. Потому это все поднималось на дев среде в контейнерах и тестилось е2е. Потому все поднималось на проде. П.С. У нас был ньюанс. Было с десяток фронтовых микроаппов и около 20-ти микроаппов бэка, что не так много.

                                          0

                                          del, misscliked

                                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                          Самое читаемое