Разработка В Монорепозитории. Отчет Яндекса

Меня зовут Азат Разетдинов, я работаю в Яндексе 12 лет, возглавляю службу разработки интерфейсов в Яндекс.

Недвижимости.

Сегодня я хотел бы поговорить о монорепозитории.

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

Теперь о том, зачем это нужно другим.



Разработка в монорепозитории.
</p><p>
 отчет яндекса

Как рассказала руководитель службы разработки API Яндекс.

Карт Марина Перескокова, мой дедушка посадил монорепу, и монорепка выросла большая и большая.

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

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

Мы даже попробовали такую сущность, как внешние SVN, для тех, кто помнит. Мы попробовали подмодули git. Мы попробовали пакеты npm, когда они вышли.

Но все это заняло много времени, что ли.

Вы поддерживаете пакет, находите ошибку, вносите исправления.

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

Это просто превратилось в боль.



Разработка в монорепозитории.
</p><p>
 отчет яндекса

Потом мы подумали, стоит ли нам объединяться в один репозиторий.

Возьмите все наши сервисы и библиотеки, перенесите их и развивайте в одном репозитории.

Было обнаружено довольно много преимуществ.

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

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

Нет необходимости что-либо пересобирать, публиковать или обновлять.

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



Разработка в монорепозитории.
</p><p>
 отчет яндекса

Как рассказала руководитель службы разработки API Яндекс.

Карт Марина Перескокова, мой дедушка посадил монорепу, и монорепка выросла большая и большая.

Это факт, а не шутка.

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

А если мы говорим о git, который вытаскивает все файлы плюс всю их историю за все время существования вашего кода, то это довольно большой объем дискового пространства.

Вторая проблема - вливание в мастер.

Вы подготовили запрос на включение, прошли проверку и готовы его объединить.

И оказывается, что кто-то вас опередил и вам нужно решать конфликты.

Вы разрешили конфликты, готовы всыпаться снова, и снова не успели.

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

Однако такая проблема существует. Есть решения, которые нивелируют это, но нужно иметь это в виду.

Это технические вопросы, но есть и организационные.

Допустим, у вас есть несколько команд, которые делают несколько разных сервисов.

Когда они переходят в монорепозиторий, их ответственность начинает размываться.

Потому что сделали релиз, закатили в производство — что-то сломалось.

Начинаем разбор полетов.

Оказывается, это разработчик из другой команды что-то зафиксировал в общий код, мы это выдернули, выпустили, не увидели, все сломалось.

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

Интересно, кто еще кроме яндекса и других игроков пользуется монорепозиторием? Довольно много людей.

Это React, Jest, Babel, Ember, Meteor, Angular. Люди понимают, что разрабатывать и публиковать npm-пакеты из монорепозитория проще, дешевле и быстрее, чем из нескольких небольших репозиториев.

Самое интересное, что вместе с этим процессом начали развиваться инструменты для работы с монорепозиторием.

Это именно то, о чем я хочу поговорить.

Все начинается с создания монорепозитория.

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

Разработка в монорепозитории.
</p><p>
 отчет яндекса

Просто откройте свой репозиторий, запустите npx lerna init, он задаст вам несколько наводящих вопросов и добавит несколько объектов в вашу рабочую копию.

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

По умолчанию все пакеты хранятся в папке packages, но вы можете это настроить как угодно, можно даже в корень положить, lerna тоже это может подхватить.

Следующий шаг - как добавить свои репозитории в монорепозиторий, как их перенести? Чего мы хотим достичь? Скорее всего, у вас уже есть несколько репозиториев, в данном случае A и B.

Разработка в монорепозитории.
</p><p>
 отчет яндекса

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



Разработка в монорепозитории.
</p><p>
 отчет яндекса

Для этого есть инструмент под названием lerna import. Вы просто указываете местоположение вашего репозитория, и lerna переносит его в ваш монореп.

При этом он, во-первых, берет список всех коммитов, модифицирует каждый коммит, меняя путь к файлам с root на packages/package_name, и применяет их один за другим, накладывая их в вашем монорепозитории.

Фактически он анализирует каждый коммит, меняя пути к файлам в нем.

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

Это первый способ.

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

Вам нужно будет разобраться, настроить, проверить, что все запускается, протестировать.

Но труд людей того не стоит, они продолжают что-то делать.



Разработка в монорепозитории.
</p><p>
 отчет яндекса

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

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

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

Отличное решение для переходного периода, рекомендую.

Хорошо, мы переместили наши репозитории в один монорепозиторий, но где же магия? Мы хотим выделить общие части и как-то их использовать.

И для этого существует механизм «привязки зависимостей».

Что такое привязка зависимостей? Существует инструмент под названием lerna bootstrap, это команда, похожая на npm install, она просто запускает npm install для всех ваших пакетов.



Разработка в монорепозитории.
</p><p>
 отчет яндекса

Но это не все.

Кроме того, он ищет внутренние зависимости.

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

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



Разработка в монорепозитории.
</p><p>
 отчет яндекса



Разработка в монорепозитории.
</p><p>
 отчет яндекса

После запуска lerna bootstrap прямо внутри папки node_modules вместо физической папки А появляется символическая ссылка, ведущая в папку с пакетом А.

Это очень удобно, потому что вы можете отредактировать код внутри пакета А и сразу проверить результат в пакете B запускайте тесты, интеграционные тесты, модули, что хотите.

Разработка значительно упрощается, больше не нужно пересобирать пакет А, публиковать, подключать пакет Б.

Просто здесь поправили, там проверили.

Обратите внимание, если посмотреть папки node_modules, то и там и там есть jest, у нас есть дубликат установленного модуля.

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

Для ускорения установки зависимостей используется механизм снятия зависимостей.

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

Разработка в монорепозитории.
</p><p>
 отчет яндекса

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

Почти ничего не меняется.

Но на самом деле мы взяли и дедуплицировали наши зависимости, переместили зависимости в корень.

В то же время Лерна довольно умна.

Если возник какой-то конфликт, например, если пакет A использовал Jest версии 1, а пакет B — версию 2, то один из них всплывал бы наверх, а другой оставался бы на своем уровне.

Примерно это и делает npm внутри обычной папки node_modules, он также пытается дедуплицировать зависимости и максимально перенести их в корень.

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

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



Разработка в монорепозитории.
</p><p>
 отчет яндекса

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

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

Разработка в монорепозитории.
</p><p>
 отчет яндекса

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

Однажды все решилось, и все.

Но как Лерна достигает этого? Она довольно агрессивна по отношению к npm. Когда вы указываете hoist, он берет ваш package.json в корень, делает его резервную копию, подставляет на его место другой, агрегирует в него все ваши зависимости, запускает npm install, почти все помещается в корень.

Потом этот временный package.json удаляется, ваш восстанавливается.

Если потом запустить какую-нибудь команду из npm, например, npm удалить, npm не поймет, что произошло, почему вдруг все зависимости оказались в корне.

Лерна нарушает уровень абстракции, она лезет в инструмент, который ниже ее по уровню.

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



Разработка в монорепозитории.
</p><p>
 отчет яндекса

Yarn уже из коробки может делать то же самое: зависимости ссылок; если он увидит, что пакет B зависит от пакета A, он бесплатно создаст для вас символическую ссылку.

Умеет поднимать зависимости, делает это по умолчанию, кладет все в корень.

Как и lerna, он может оставить единственный файл Yarn.lock в корне репозитория.

Вам больше не нужны все остальные пряжи.



Разработка в монорепозитории.
</p><p>
 отчет яндекса

Он настраивается аналогичным образом.

К сожалению, Yarn предполагает, что все настройки добавляются в package.json, я знаю, что есть люди, которые пытаются убрать оттуда все настройки инструмента, оставив только минимальные.

К сожалению, Yarn пока не научилась указывать это в другом файле, только в package.json. Есть две новые опции: одна новая и одна обязательная.

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

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

Все общие зависимости ушли в корень, а те, что указаны в ключе nohoist, остались на своем уровне.



Разработка в монорепозитории.
</p><p>
 отчет яндекса

Самое приятное то, что Лерна умеет работать с пряжей и подбирать ее настройки.

Достаточно указать два поля в lerna.json, lerna сразу поймет, что вы используете Yarn, зайдите в package.json, получите оттуда все настройки и работайте с ними.

Эти два инструмента уже знают друг о друге и работают в паре.



Разработка в монорепозитории.
</p><p>
 отчет яндекса

Почему npm до сих пор не предоставил поддержку, если так много крупных компаний используют монорепозиторий?

Разработка в монорепозитории.
</p><p>
 отчет яндекса



Ссылка со слайда
Говорят, что всё будет, но в седьмой версии.

Базовая поддержка в седьмом, расширенная поддержка в восьмом.

Этот пост был опубликован месяц назад, но дата выхода седьмого npm пока неизвестна.

Ждём, когда он наконец догонит пряжу.

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



Разработка в монорепозитории.
</p><p>
 отчет яндекса



Разработка в монорепозитории.
</p><p>
 отчет яндекса

Yarn содержит команду рабочей области Yarn, затем имя пакета и имя команды.

Потому что Yarn из коробки, в отличие от npm, может делать все три вещи: запускать собственные команды, добавлять зависимость от jest, запускать скрипты из package.json, например test, а также может запускать исполняемые файлы из папки node_modules/.

bin. Давайте научим его понимать за вас, чего вы хотите, используя эвристику.

Очень удобно использовать рабочую область пряжи для точечных операций над одним пакетом.

Существует аналогичная команда, которая позволяет вам запускать команду для всех имеющихся у вас пакетов.



Разработка в монорепозитории.
</p><p>
 отчет яндекса

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



Разработка в монорепозитории.
</p><p>
 отчет яндекса

Из плюсов — очень удобно запускать разные команды.

Из недостатков, например, невозможность запуска команд оболочки.

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

Ну и вылетает при первой же ошибке.

Если я захочу удалить зависимость из всех пакетов - а на самом деле она есть только в двух из них, но я не хочу думать о том, где она находится, а просто хочу ее удалить - то Yarn мне не позволит сделайте это, произойдет сбой в первой ситуации, когда этот пакет отсутствует в зависимостях.

Это не очень удобно; иногда вам хочется игнорировать ошибки и просмотреть все пакеты.



Разработка в монорепозитории.
</p><p>
 отчет яндекса

У Лерны гораздо более интересные инструменты; у него есть две отдельные команды: run и exec. Run умеет выполнять скрипты из package.json и в отличии от Yarn умеет фильтровать всё по пакетам, можно указать --scope, можно использовать звёздочки, глобусы, всё достаточно универсально.

Вы можете запускать эти операции параллельно, а можете игнорировать ошибки, используя ключ --no-bail.

Разработка в монорепозитории.
</p><p>
 отчет яндекса

Exec очень похож.

В отличие от пряжи, он позволяет не только запускать исполняемые файлы из node_modules.bin, но и выполнять любые произвольные команды оболочки.

Например, вы можете удалить node_modules или запустить какой-нибудь make, как хотите.

И этот же вариант поддерживается.



Разработка в монорепозитории.
</p><p>
 отчет яндекса

Очень удобные инструменты, есть некоторые преимущества.

Это тот случай, когда Лерна рвет нить, находится на нужном уровне абстракции.

Лерна именно для этого и нужна: чтобы упростить работу с несколькими пакетами в монорепе.

У одноповторных тренировок есть еще один недостаток.

Когда у вас настроен CI/CD, вы его никак не оптимизируете.

Чем больше у вас услуг, тем больше времени это все занимает. Допустим, вы начинаете тестировать все сервисы по каждому пул-реквесту, и чем их больше, тем больше времени занимает работа.

Для оптимизации этого процесса можно использовать выборочные операции.

Я назову три разных способа.

Первые два из них можно использовать не только в монорепе, но и в ваших проектах, если вы по каким-то причинам не используете эти методы.

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

Запускайте весь линт не на весь ваш проект, а только на те файлы, которые изменились.



Разработка в монорепозитории.
</p><p>
 отчет яндекса



Разработка в монорепозитории.
</p><p>
 отчет яндекса

Настройка очень проста.

Установите lint-staged, хаски, precommit-хуки и скажите, что при изменении любого js-файла нужно запускать eslint. Это значительно ускоряет проверку перед фиксацией.

Особенно если у вас много сервисов, очень большой монорепозиторий.

Тогда запуск eslint для всех файлов будет слишком затратным, и таким образом вы можете оптимизировать перехватчики precommit для lint.

Разработка в монорепозитории.
</p><p>
 отчет яндекса

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



Разработка в монорепозитории.
</p><p>
 отчет яндекса

Эта опция позволяет передать ему список исходных файлов и найти все тесты, которые каким-либо образом влияют на эти файлы.

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

Сами js файлы с тестами внутри исключаем, смотрим только исходники.

Мы запускаем findRelatedTests и значительно ускоряем работу модулей для pre-commit или pre-push, в зависимости от того, что вам удобно.

И третий способ относится именно к монорепозиториям.

Это lerna, которая может определить, какие пакеты изменились с момента базовой фиксации.

Здесь речь идет не о хуках, а о вашем CI/CD: Travis или другом сервисе, которым вы пользуетесь.



Разработка в монорепозитории.
</p><p>
 отчет яндекса



Разработка в монорепозитории.
</p><p>
 отчет яндекса

Команды run и exec имеют опцию с момента, которая позволяет запускать любую команду только в тех пакетах, которые изменились с момента фиксации.

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

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

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

Если вы изменили библиотеку А, которая используется в библиотеке Б, которая используется в сервисе С, lerna это поймет. Допустим, вы меняете код в библиотеке А.

Тогда она транзитивно определит, что пакет С необходимо повторно протестировать — например, с помощью написанного вами интеграционного теста.

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

Есть люди, которые любят монорепозитории.

Есть люди, которые любят мультирепозитории.

Это всегда вопрос компромисса.

Что проще? Я определил, что команды, работающие раздельно, обычно работают независимо, потому что чем больше у них независимости, тем они счастливее.

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

Тогда компания начинает проигрывать, потому что каждая команда какие-то дела делает отдельно.

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

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

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

Хочу сказать спасибо моим коллегам: Миша Мишанга Трошев и Гоша Беседин.

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

Это все, спасибо.

Теги: #git #Управление разработкой #JavaScript #интерфейсы #монорепозиторий #jest #Bootstrap #Сборка систем #lerna

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.