✏️ Написано Майклом Соломоном и Йони Голдберг

На что мы смотрим

Рынок Монорепо горяч, как огонь. Как ни странно, сейчас, когда спрос на Monoreps резко вырос, одна из ведущих библиотек — Лерна — только что ушла на пенсию. При внимательном рассмотрении это может быть не просто совпадением — с таким количеством прорывных и блестящих функций, представленных новыми поставщиками, Lerna не успевала за темпами и оставалась актуальной. Этот расцвет новых инструментов многих сбивает с толку — какой правильный выбор для моего следующего проекта? На что следует обратить внимание при выборе инструмента Monorepo? Этот пост посвящен обработке этой информационной перегрузки, освещению новых инструментов, подчеркиванию важного и, наконец, некоторым рекомендациям. Если вы пришли сюда за инструментами и функциями, вы попали в нужное место, хотя вы можете оказаться на пути самоанализа к желаемому рабочему процессу разработки.

Этот пост касается только бэкенда и Node.js. Он также касался типичных бизнес-решений. Если вы разработчик Google/FB, который столкнулся с 8000 пакетов — извините, вам нужно специальное снаряжение. Следовательно, инструменты монстра Monorepo, такие как Bazel, не учитываются. Здесь мы рассмотрим некоторые из самых популярных инструментов Monorepo, включая Turborepo, Nx, PNPM, рабочее пространство Yarn/npm и Lerna (хотя на самом деле он больше не поддерживается — это хорошая база для сравнения).

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

Уровень 1: Старые простые папки, чтобы оставаться в курсе вашего кода

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

Этот слой часто упускается из виду. Если ваша кодовая база невелика, а компоненты сильно развязаны (подробнее об этом позже) — это может быть все, что вам нужно. Мы видели несколько успешных решений Monorepo без каких-либо специальных инструментов.

С учетом сказанного, некоторые из новых инструментов дополняют этот опыт интересными функциями:

  • И Turborepo, и Nx, и Lerna обеспечивают визуальное представление зависимостей пакетов.
  • Nx допускает «правила видимости, которые касаются соблюдения того, кто что может использовать. Учтите, что к библиотеке checkout должен обращаться только микросервис заказа — отклонение от этого приведет к сбою во время разработки (не принудительному выполнению во время выполнения).

  • Генератор рабочего пространства Nx позволяет собирать компоненты. Всякий раз, когда члену команды нужно создать новый контроллер/библиотеку/класс/микрослужбу, он просто вызывает команду CLI, код продукта которой основан на шаблоне сообщества или организации. Это обеспечивает согласованность и обмен передовым опытом.

Уровень 2: задачи и конвейер для эффективного создания кода.

Даже в мире автономных компонентов есть задачи управления, которые необходимо выполнять в пакетном режиме, например, применение исправления безопасности через обновление npm, запуск тестов нескольких компонентов, на которые повлияло изменение, публикация 3 связанные библиотеки, чтобы назвать несколько примеров. Все инструменты Monorepo поддерживают эту базовую функциональность вызова некоторой команды для группы пакетов. Например, Lerna, Nx и Turborepo.

В некоторых проектах достаточно вызвать каскадную команду. В основном, если каждый пакет имеет автономный жизненный цикл, а процесс сборки охватывает один пакет (подробнее об этом позже). В некоторых других типах проектов, где рабочий процесс требует одновременного тестирования/запуска и публикации/развертывания множества пакетов — это закончится ужасно медленно. Рассмотрим решение с сотнями пакетов, которые транспилированы и объединены в пакеты — запуск широкого теста может занять несколько минут. Хотя полагаться на широкие/E2E-тесты не всегда хорошая практика, в дикой природе это довольно распространено. Именно здесь проявляется новая волна инструментов Monorepo — глубоко оптимизация процесса сборки. Я должен сказать это вслух: эти инструменты обеспечивают красивую и инновационную оптимизацию сборки:

  • Распараллеливание. Если две команды или пакеты ортогональны друг другу, команды будут выполняться в двух разных потоках или процессах. Обычно ваш контроль качества включает в себя тестирование, выравнивание, проверку лицензий, проверку CVE — почему бы не распараллелить?
  • Разумный план выполнения. Помимо распараллеливания, оптимизированный порядок выполнения задач определяется на основе многих факторов. Рассмотрим сборку, включающую A, B, C, где A, C зависят от B — наивно система сборки будет ждать сборки B и только затем запускать A и C. Это можно оптимизировать, если мы запустим A и C. изолированные модульные тесты во времясборки Б, а не после. Запуская задачу параллельно как можно раньше, общее время выполнения улучшается — это оказывает заметное влияние в основном при размещении большого количества компонентов. См. ниже пример визуализации улучшения конвейера.

  • Определять, на кого влияет изменение. Даже в системе с высокой степенью связанности между пакетами обычно нет необходимости запускать все пакеты, а только те, которые затронуты изменением. . Что именно «пострадало»? Пакеты/микросервисы, которые зависят от другого измененного пакета. Некоторые инструменты могут игнорировать незначительные изменения, которые вряд ли сломают другие. Это не сильно повышает производительность, но также является отличной функцией тестирования — разработчики могут быстро получить обратную связь о том, были ли какие-либо из их клиентов неисправны. И Nx, и Turborepo поддерживают эту функцию. Лерна может сказать только, какой из пакетов Monorepo изменился
  • Подсистемы (т. е. проекты) –по аналогии с «затронутыми» выше, современные инструменты могут реализовывать части графа, которые взаимосвязаны (проект или приложение), в то время как другие недоступны для компонента. в контексте (другой проект), чтобы они знали, что нужно использовать только пакеты соответствующей группы
  • Кэширование — это серьезный ускоритель: Nx и Turborepo кэшируют результат/вывод задач и избегают их повторного запуска в последующих сборках, если в этом нет необходимости. Например, рассмотрите длительные тесты микрослужбы, когда при команде на пересборку этой микрослужбы инструментарий может понять, что ничего не изменилось, и тест будет пропущен. Это достигается созданием хэш-карты всех зависимых ресурсов — если какой-либо из этих ресурсов не изменился, то хэш-карта будет такой же, и задача будет пропущена. Они даже кешируют стандартный вывод команды, поэтому когда вы запускаете кешированную версию, она действует как настоящая — подумайте о запуске 200 тестов, просмотре всех операторов журнала тестов, получении результатов через терминал за 200 мс, все работает как ' настоящее тестирование, а на самом деле тесты вообще не запускались, а кеш!
  • Удаленное кэширование — аналогично кэшированию, только путем размещения хэш-карт и результатов задачи на глобальном сервере, поэтому при дальнейшем выполнении на компьютерах других членов команды также будут пропускаться ненужные задачи. В огромных проектах Monorepo, которые полагаются на тесты E2E и должны собирать все пакеты для разработки, это может сэкономить много времени.

Уровень 3: Поднимите свои зависимости, чтобы ускорить установку npm

Оптимизация скорости, описанная выше, не поможет, если узким местом является большой бык грязи, который называется npm install (не ругать, просто он сложный по своей природе). Возьмем в качестве примера типичный сценарий, учитывая десятки компонентов, которые необходимо собрать, они могут легко инициировать установку тысяч подзависимостей. Хотя они используют очень похожие зависимости (например, один и тот же регистратор, один и тот же ORM), если версия зависимости не одинакова, то npm будет дублировать (проблема двойников NPM) установку этих пакетов, что может привести к длительному процессу.

Именно здесь появляется линейка инструментов рабочей области (например, рабочая область Yarn, рабочие области npm, PNPM) и вводится некоторая оптимизация — вместо установки зависимостей внутри папки «NODE_MODULES» каждого компонента создается одна централизованная папка и связываются все зависимости поверх нее. там. Это может показать огромное увеличение времени установки для крупных проектов. С другой стороны, если вы всегда сосредотачиваетесь на одном компоненте за раз, установка пакетов одного микросервиса/библиотеки не должна вызывать беспокойства.

И Nx, и Turborepo могут полагаться на диспетчер пакетов/рабочее пространство для обеспечения этого уровня оптимизации. Другими словами, Nx и Turborepo — это слой над менеджером пакетов, который заботится об установке оптимизированных зависимостей.

Кроме того, Nx вводит еще один нестандартный, может быть, даже спорный прием: в корневой папке всего монорепозитория может быть только ОДИН package.json. По умолчанию при создании компонентов с помощью Nx у них не будет собственного package.json! Вместо этого все будут совместно использовать корневой файл package.json. Таким образом, все микросервисы/библиотеки совместно используют свои зависимости, а время установки сокращается. Примечание. Можно создавать «публикуемые» компоненты, у которых есть package.json, просто он не используется по умолчанию.

Я беспокоюсь здесь. Совместное использование зависимостей между пакетами увеличивает связанность. Что, если Microservice1 хочет увеличить версию dependency1, а Microservice2 не может этого сделать в данный момент? Кроме того, package.json является частью среды выполнения Node.js, и при исключении его из корня компонента теряются важные функции, такие как основное поле package.json или экспорт ESM (сообщение клиентам, какие файлы доступны). На прошлой неделе я запустил POC с Nx и обнаружил, что меня заблокировали — библиотека B была заполнена, я попытался импортировать ее из библиотеки A, но не смог получить оператор «импорт», чтобы указать правильное имя пакета. Естественным действием было открыть package.json B и проверить имя, но Package.json нет… Как мне определить его имя? Документы Nx великолепны, наконец-то я нашел ответ, но мне пришлось потратить время на изучение нового «фреймворка».

Остановитесь на секунду: все дело в вашем рабочем процессе

Мы имеем дело с инструментами и функциями, но на самом деле бессмысленно оценивать эти параметры, прежде чем определить, является ли предпочтительный рабочий процесс синхронизированным или независимым (мы обсудим это через несколько секунд). Это предварительное фундаментальное решение изменит почти все.

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

Вариант А — синхронизированный рабочий процесс.При таком стиле разработки все три компонента будут разработаны и развернуты в одном блоке вместе. На практике разработчик будет кодировать изменения в Library1, тестировать libray1, а также запускать широкие интеграционные/e2e-тесты, включающие Microservice1 и Microservice2. Когда они будут готовы, версии всех компонентов будут изменены. Наконец, они будут развернуты вместе.

При таком подходе у разработчика есть возможность увидеть весь поток с точки зрения клиента (Microservice1 и 2), тесты охватывают не только библиотеку, но и глазами клиентов, которые ее фактически используют. С другой стороны, это требует обновления всех зависимых компонентов (их может быть несколько десятков), что увеличивает радиус взрыва риска, поскольку затрагивается больше юнитов, и это следует учитывать перед развертыванием. Кроме того, работа над большой единицей работы требует создания и тестирования большего количества вещей, что замедляет сборку.

Вариант Б — независимый рабочий процесс. Этот стиль заключается в работе с единицей за единицей, по кусочку за раз, и независимом развертывании каждого компонента на основе его личных деловых соображений и приоритетов. Вот как это происходит: Разработчик вносит изменения в Библиотеку1, они должны быть тщательно протестированы в рамках Библиотеки1. Как только она готова, SemVer переводится на новый мажор, и библиотека публикуется в реестре менеджера пакетов (например, npm). Как насчет клиентских микросервисов? Что ж, команда Microservice2 сейчас очень занята другими приоритетами и пока пропустит это обновление (так же, как мы все откладываем многие наши обновления npm). Тем не менее, Microservice1 очень заинтересован в этом изменении — команда должна активно обновлять эту зависимость и получать последние изменения, запускать тесты и, когда они будут готовы, сегодня или на следующей неделе — развертывать их.

Переходя к независимому рабочему процессу, автор библиотеки может двигаться намного быстрее, потому что ему не нужно учитывать 2 или 30 других компонентов — некоторые из них написаны разными командами. Этот рабочий процесс также вынуждает ее писать эффективные тесты для библиотеки — это ее единственная система безопасности, которая, скорее всего, закончится автономными компонентами, слабо связанными с другими. С другой стороны, изолированное тестирование без точки зрения клиента теряет некоторое измерение реализма. Кроме того, если одному разработчику нужно обновить 5 модулей — публикация каждого в реестре по отдельности, а затем обновление во всех зависимостях может быть немного утомительной.

Об иллюзии синхронности

В распределенных системах невозможно достичь 100% синхронности — вера в обратное может привести к ошибкам проектирования. Рассмотрим критическое изменение в Microservice1, теперь его клиент Microservice2 адаптируется и готов к изменению. Эти две микрослужбы развертываются вместе, но из-за природы микрослужб и распределенной среды выполнения (например, Kubernetes) развертывание только микрослужбы 1 не удается. Теперь код Microservice2 не соответствует производственной версии Microservice1, и мы столкнулись с производственной ошибкой. Эта линия сбоев может быть в некоторой степени обработана также с помощью синхронизированного рабочего процесса — развертывание должно организовать развертывание каждого модуля, чтобы каждый из них развертывался одновременно. Хотя этот подход выполним, он увеличивает вероятность крупномасштабного отката и усиливает опасения при развертывании.

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

Уровень 4: Свяжите свои пакеты для немедленной обратной связи

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

Вариант 1. Использование npm.Каждая библиотека представляет собой стандартный пакет npm, и ее клиент устанавливает ее с помощью стандартных команд npm. Учитывая Microservice1 и Library1, это закончится двумя копиями Library1: одна внутри Microservices1/NODE_MODULES (т. е. локальная копия потребляющей Microservice), а вторая — папка разработки, в которой команда кодирует Library1.

Вариант 2. Просто обычная папка. В этом случае Library1 представляет собой не что иное, как логический модуль внутри папки, которую Microservice1,2,3 просто локально импортирует. NPM здесь не при чем, это просто код в отдельной папке. Например, так представлены модули Nest.js.

При варианте 1 команды получают все преимущества менеджера пакетов — SemVer(!), инструментарий, стандарты и т. д. Однако если кто-то обновит Library1, изменения не отразятся в Microservice1, поскольку он берет свою копию из реестр npm и изменения еще не были опубликованы. Это фундаментальная проблема с Monorepo и менеджерами пакетов — нельзя просто кодировать несколько пакетов и тестировать/выполнять изменения.

При варианте 2 команды теряют все преимущества менеджера пакетов: каждое изменение немедленно распространяется на всех потребителей.

Как нам принести пользу из обоих миров (предположительно)? Использование связывания. Lerna, Nx, различные рабочие пространства менеджера пакетов (Yarn, npm и т. д.) позволяют использовать библиотеки npm и в то же время связывать клиентов (например, Microservice1) и библиотеку. Под капотом они создали символическую ссылку. В режиме разработки изменения распространяются сразу, во время развертывания — копия берется из реестра.

Если вы выполняете синхронизированный рабочий процесс, все готово. Только теперь любое рискованное изменение, вносимое библиотекой 3, должно СЕЙЧАС обрабатываться 10 микросервисами, которые его потребляют.

Если вы предпочитаете независимый рабочий процесс, это, конечно, вызывает большую озабоченность. Некоторые могут назвать этот стиль прямых ссылок «монолитным монорепозиторием» или, может быть, «монолитом». Однако без связывания сложнее устранить небольшую проблему между микросервисом и библиотекой npm. Что я обычно делаю, так это временно связываю (со ссылкой npm) между пакетами, отлаживаю, кодирую, а затем, наконец, удаляю связь.

Nx использует немного более разрушительный подход — он использует пути TypeScript для связывания между компонентами. Когда Microservice1 импортирует Library1, чтобы избежать полного локального пути, он создает сопоставление TypeScript между именем библиотеки и полным путем. Но подождите минутку, в продакшене нет TypeScript, так как же он может работать? Что ж, во время обслуживания/сборки он упаковывает и сшивает компоненты вместе. Не очень стандартный способ работы с Node.js.

Закрытие: что следует использовать?

Все дело в вашем рабочем процессе и архитектуре — огромный невидимый перекресток стоит перед решением об инструментах Monorepo.

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

Например, если ваша микрослужба должна поддерживать одну и ту же версию, или если команда очень маленькая и одни и те же люди обновляют все компоненты, или если ваша модульность основана не на диспетчере пакетов, а на модулях, принадлежащих фреймворку (например, Nest. js), если вы работаете с интерфейсом, в котором компоненты по своей сути публикуются вместе, или если ваша стратегия тестирования в основном опирается на E2E — для всех этих и других случаев Nx — это инструмент, созданный для улучшения опыта кодирования многих относительно связанные компоненты. Это отличный сахар для систем, которые неизбежно являются большими и связанными.

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

Сценарий B.Если вы используете независимый рабочий процесс, в котором каждый пакет разрабатывается, тестируется и развертывается (почти) независимо друг от друга, то, по сути, вам не нужны причудливые инструменты для организовать сотни пакетов. Большую часть времени в фокусе только один пакет. Это требует выбора более компактного и простого инструмента — Turborepo. Идя по этому пути, Monorepo — это не то, что влияет на вашу архитектуру, а скорее инструмент с ограниченной областью действия для более быстрого выполнения сборки. Одним из конкретных инструментов, поощряющих независимый рабочий процесс, является Bilt Гила Тайара, он еще не приобрел достаточной популярности, но вскоре может стать популярным и является отличным источником, чтобы узнать больше об этой философии работы.

В любом случае рассмотрите рабочие пространства. Если вы столкнулись с проблемами производительности, вызванными установкой пакета, различные инструменты для рабочих пространств Yarn/npm/PNPM могут значительно минимизировать эти накладные расходы при малой занимаемой площади. Тем не менее, если вы работаете в автономном рабочем процессе, шансы столкнуться с такими проблемами меньше. Не используйте только инструменты, если нет боли.

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

Бонус: сравнительная таблица

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

Только превью, полную таблицу можно найти здесь