Монорепо: Жизнь До И После

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

Руководитель направления b2b развития Юлы Валентин Дубровский рассказал на конференции Golang Live 2020 о проблемах, с которыми столкнулась компания, и о тех, которые удалось решить с помощью этого хода.



Монорепо: жизнь до и после

В этой статье мы поговорим о:

  1. Проблемы, которые решает монорепо;
  2. Недостатки монорепозитория;
  3. GO в команде Yuly B2B;
  4. Как там внедряли монорепозиторий.

Внимание! Я не буду говорить о монолите.

У многих с ним ассоциируется любое «моно».



Монорепо: жизнь до и после

Речь пойдет о монорепозитории.



Какие проблемы решает монорепо?

Десять лет назад фронтенд и PHP-код хранились в Yula в одном репозитории объемом 10 Гб (GitLab/GitHub).

Когда наступила эра микросервисов, ситуация изменилась: один микросервис — один репозиторий.

Это логично, ведь одна команда мониторит несколько микросервисов и вносит вклад только в них, не ломает ничьи процессы и не интегрируется в код-ревью других команд. Наверное, все, кто начинал с микросервисов, делали их в отдельных репозиториях.

Сегодня мы попробуем сделать шаг назад и поговорить о монорепозитории.

Монорепозиторий — это когда все микросервисы хранятся в одном репозитории, а не в отдельных для каждого сервиса.

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

Для чего это? Почему бы и дальше не жить по стандартам один сервис — один репозиторий или одна внутренняя библиотека — один репозиторий? С какими проблемами нам поможет справиться монорепо?

Ориентация на местоположение

Начнем с истории.

Разработчик получает задание: ему необходимо создать новый сервис.

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

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

И тут приходит его коллега и спрашивает: «Вы сделали сервис, который загружает картинку пользователя.

Можете ли вы сказать мне, где его найтиЭ» Он не может сориентироваться, поискав GitLab/GitHub в устаревшей сервисной документации.

Поэтому он обращается напрямую к разработчику сервиса.

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

Например, вы следите за новыми обновлениями.

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

Но это проще сделать в условном монорепозитории.

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



Частные репозитории

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

Давайте рассмотрим сценарий.

У базы данных есть адаптер: Redis, MongoDB — обертка над общей публичной библиотекой.

Мы делаем для него отдельный репозиторий.

И проводим определенную процедуру: добавляем CI, который запускает тесты, добавляем теги при выпуске мастера.

Дальше начинаются танцы с бубнами.

Как мы можем добавить эту библиотеку в наш репозиторий? Ведь через Go.mod это сделать не так-то просто.

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

Это лишние движения, которых можно избежать.

И монорепо позволяет это сделать.



Одна задача — один запрос на включение

Еще одно интересное преимущество монорепозитория связано с выполнением самой задачи.

Разработчик берет на себя задачу, которая затрагивает несколько сервисов.

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

Имеет два алгоритма.

Первый.

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

Их необходимо отправлять на рассмотрение по одному.

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

Второй вариант решения этой проблемы — декомпозиция задачи на сервисы.

Там есть общая родительская задача.

Рецензии проводятся по одному, вне контекста общей задачи.

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

Ведь изменилось примерно 15-20-30 строк кода.

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

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

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

Команда делает сервис и доводит его до производства.

Продукт и бизнес довольны.

Разработчики идут на кухню, разговаривают с другой командой, и они говорят, что такой сервис у них уже есть.



Улучшение командного процесса

Эта проблема позволяет понять, насколько проще организовать процесс перекрестного рецензирования и обмена знаниями в монорепозитории.

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

Но в монорепозитории это происходит само собой.

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

Например, вы используете один и тот же адаптер — например для Redis — в двух сервисах.

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

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

Это не требует дополнительных затрат.

Среда

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

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

Существует инструмент docker-compose, который идеально подходит для локальной разработки.

Он настраивает сборку и запуск службы.

И это отличный вариант, не требующий лишних действий.

Очень легко настроить базовые вещи в локальной среде.

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

git скрипт, который будет запускаться по обработчику предварительной фиксации.

Это позволяет отлаживать простой код локально, а не в CI.

Минусы монорепозитория

Любой процесс, технология, решение имеют свои сложности.

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

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

Например, в одном репозитории драйвер для Mongo версии 6, а в другом такой же драйвер версии 5. И проблем с этим не будет. В монорепозитории это не сработает, и вам придется следить за критическими изменениями.

Мы не можем использовать разные основные версии в разных репозиториях.

Кроме того, нужно тщательно следить за внутренними библиотеками.

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

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

При использовании монопредставительства процесс CI/CD усложняется.

Если мы живем в парадигме один сервис — один репозиторий, мы выполняем 4 работы: тестирование, сборка, развертывание и что-то еще.

Слили мастер, собрали, развернули — ок.

В монорепозитории это сложнее, потому что когда мы мержим мастер, нам нужно собрать только то, что изменилось.

Монорепозиторий будет медленно расти.

Он может иметь 30, 40, 50, 100 сервисов.

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



GO в команде Yuly B2B



Монорепо: жизнь до и после

У Юлы на GO несколько десятков микросервисов.

GRPC и Kafka используются для связи между микросервисами, GRPC для синхронного, Kafka для асинхронного.

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

GraphQL используется для мобильных и веб-клиентов.

Gitlab CI используется для создания образов Docker и загрузки на тестовые стенды.

Юля использует Gitlab CI для всех CI-процессов; они пишут сами, без DevOps. Инженеры DevOps помогают обеспечить конвейер, который движется для выпуска службы в тестовую среду.

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

Это не стандартный макет пакета Golang, а его собственная версия.

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

Внутренние библиотеки хранились в отдельной группе.

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

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

Каждая служба должна реализовывать CI/CD. В шаблоны Gitlab CI были добавлены и включены базовые вещи.

Если вы знакомы с Gitlab CI, вы понимаете, о чем я — назовем это повторным использованием шаблонов.

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

Я уже упоминал, что с приватными репозиториями не все так просто, поэтому Юла сделала маленький ход конем: подняли Афины.

В нем можно указать путь к GitLab и учетные данные к нему, это позволяет собирать приватные репозитории через GOPROXY. Для каждого разработчика, для каждой CI-машины это выглядит так: делаешь пустой приватный пакет, указываешь Go-прокси к Афинам, и это упрощает жизнь.

Кроме того, мы создали отдельный репозиторий для docker-compose, где указали последнюю версию реестра каждого сервиса.



Как в Юле появилось монорепо

Сначала нам нужно было объединить репозитории.

Это сложный процесс, разбитый на этапы:

  • Приведите все версии внешних библиотек к одному знаменателю.

    Это сложный процесс, поскольку он требует дополнительного тестирования;

  • Выберите стратегию, как это сделать.

Первая стратегия выглядит так: мы все замираем.

Для этого готовим инфраструктуру в монорепозитории.

Это самая простая схема действий.

Но компания пошла по другому пути.

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

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

Например, GraphQL Getaway. В этот момент нам пришлось задействовать командные действия.

На этом этапе всё было перенесено в монорепозиторий менее чем за сутки.

Далее мы переходим к локальной среде.

Как я уже говорил, для этого идеально подходит docker-compose. Лучшая стратегия: отправлять путь к локальному каталогу модулей go через том в каждую сборку.

Затем, если вы что-то go.mod, оно попадает в GOPATH/pkg/mod. И это ускорит сборку.

И, конечно же, добавьте вкусняшек.

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

Затем мы добавили остальные прокси для Кафки.

Он позволяет вам производить и потреблять из Kafka через обычный Postman. Теперь поговорим о сборках.

В Yula пошли по такому сценарию: для каждого сервиса использовали директиву Gitlab CI onlychanges. Записаны только изменения Dockerfile, go.mod, внутренних библиотек и все зависимости от внешних сервисов:

Монорепо: жизнь до и после

Все это прекрасно работает, пока не изменится что-то общее, например, go.mod. Когда сервис, к которому добавлена новая библиотека, объединяется, CI начинает все строить.

В этом случае помогает автоматизация.

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

Что еще сделала компания? Раньше были пухлые CI-файлы Gitlab. Но в монорепо вы можете получить доступ к локальным скриптам.

А в Юле мы выделили папку со скриптами для CI. А тяжелые вещи, необходимые в CI, написаны на коде Go. Например, выбор рецензента.

Выбор рецензента приобрел интересный формат. Поначалу они просто закидывали в чат созданные пул-реквесты.

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

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

С монорепо мы решили перенести это в CI. Когда прикрепляется запрос на включение, запускается скрипт Go и выбираются два случайных рецензента.

Первый основан на модифицированных файлах: среди лучших кандидатов случайным образом выбирается тот, кто затронул больше всего.

Второй — из всего проекта монорепо.



Монорепо: жизнь до и после

Это работает очень круто.

Естественно, такие вещи, как уведомление при открытии PR, или уведомление, если PR долго не просматриваются, в монорепозитории пошли как по маслу.

Каждые 2-3 часа напоминают: «Ой, не забудь про отзыв!»

Монорепо: жизнь до и после

Это увеличивает конверсию от пул-реквестов к релизам, и работает коллективная ответственность.

В Юле в продакшене нет Continuous Delivery, и релиз происходит так:

Монорепо: жизнь до и после

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



выводы

Monorepo имеет множество преимуществ, которые значительно облегчают вашу работу.

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

А в монорепозитории гораздо проще организовать процесс перекрестного рецензирования и обмена знаниями.

Для Юлы переход на монорепозиторий стал успешной практикой, потому что:

  • Рассмотрение ускорилось.

  • Задания стали доставляться быстрее и с меньшим количеством блокировщиков.

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

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

У Юлы есть несколько отделов.

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

На данном этапе мне кажется, что не стоит всех переводить в одно монорепо.

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

Хотите больше контента о разработке GO? У вас есть возможность купить видео с Golang Live 2020. В этом году на конференции сделали акцент на Go в разработке продуктов и раскрыли эту тему с трех сторон: как бороться с проблемами Go и какие инструменты доступны, почему все же стоит выбрать Go и как поддерживать продукт после этого.

Теги: #ИТ-компании #ИТ-компании #Конференции #Go #golang #конференции Олега Бунина #монорепозиторий
Вместе с данным постом часто просматривают: