Как стать автором
Обновить
408.41
Рейтинг
Конференции Олега Бунина (Онтико)
Профессиональные конференции для IT-разработчиков

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

Блог компании Конференции Олега Бунина (Онтико) Go *Конференции IT-компании
Это история о том, почему в одном из направлений «Юлы» отказались от практики отдельных репозиториев на микросервисы и внутренние библиотеки, перейдя на монорепозиторий, и что из этого вышло. О проблемах, с которыми столкнулись в компании, и тех, которые получилось решить при помощи этого переезда, рассказал на конференции Golang Live 2020 руководитель b2b-разработки «Юлы» Валентин Дубровский.



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

  1. Проблемах, которые решает монорепо;
  2. Минусах монорепозитория;
  3. GO в команде B2B «Юлы»;
  4. Том, как там вводили монорепозиторий.

Внимание! Я буду говорить не про монолит. У многих любое «моно-» ассоциируется именно с ним.



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

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


Десять лет назад фронтенд и PHP-код хранились в «Юле» в одном репозитории по 10 Gb (GitLab/GitHub).

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

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

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

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

Ориентирование на местности


Начнем с истории. Разработчик получает задачу: ему нужно создать новый сервис. Первым делом он создает репозиторий, настраивает CI и далее совершает некий порядок действий. В зависимости от компании и процессов, этот порядок будет различаться.

А потом приходит его коллега и спрашивает: «Ты делал сервис, который загружает картинку пользователя. Расскажешь, где его найти?». Он не может сам сориентироваться по поиску через GitLab/GitHub в устаревшей документации сервиса. Поэтому обращается к разработчику сервиса напрямую.

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

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

Приватные репозитории


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

Рассмотрим сценарий. В базе данных есть адаптер: Redis, MongoDB — обертка над общей публичной библиотекой. Мы делаем для нее отдельный репозиторий. И осуществляем некий порядок действий: добавляем CI, который прогоняет тесты, навешиваем тэги при релизе master. Дальше начинаются танцы с бубнами. Как притянуть эту библиотеку в наш репозиторий? Ведь через Go.mod это не так просто сделать.

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

Одна задача — один Pull request


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

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

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

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

Второй вариант решения этой проблемы – декомпозиция задачи по сервисам. Там есть общая родительская задача. Ревью проходят по одному, вне контекста общей задачи. Или же можно попробовать сымитировать контекст. Но это все дополнительные действия, которые нужно предпринять, несмотря на то, что задача маленькая. Ведь изменилось условно 15-20-30 строчек кода.

Если задача небольшая, монорепо позволяет сделать все в рамках одного pull request. Ревью сможет заняться один разработчик, у которого будет полный контекст по задаче. Это удобно и ускоряет поставку задач на production из-за того, что мы избавляемся от лишнего оверхеда.

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

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

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


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

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

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

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

Окружение


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

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

Базовые штуки в локальном окружении настроить очень просто. Например, запуск линтера на pre-commit hook. Мы должны закинуть в .git скрипт, который будет запускаться на pre-commit hook. Это позволяет отлаживать простой код на локальном уровне, а не в CI.

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


В любом процессе, технологии, решении есть свои сложности. Самый большой минус монорепозиториев – это проблема зависимостей.

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

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

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

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

Если мы живем в парадигме один сервис – один репозиторий, мы находимся в 4 джобах: test, build, deploy и что-нибудь еще. Слили master, сбилдили, задеплоили – ОК.

В монорепозитории это сложнее, потому что при сливе master, нам нужно билдить только то, что изменилось. Монорепозиторий будет потихоньку разрастаться. В нем может быть 30, 40, 50, 100 сервисов. И если мы будем билдить все, ни к чему хорошему это не приведет.

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

GO в команде B2B «Юлы»




В «Юле» несколько десятков микросервисов на GO.

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

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

Сервисы имеют однотипную структуру, которую определили на раннем этапе.
Это не стандартный Golang package layout, а своя версия.

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

В каждом сервисе должен быть реализован CI/CD. Базовые штуки вносили в Gitlab CI шаблоны, и инклюдили. Если вы знакомы с Gitlab CI, понимаете, о чем я – назовем это переиспользованием шаблонов.

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

Я уже упоминал, что с приватными репозиториями не все так просто, поэтому в «Юле» сделали небольшой ход конем: подняли Athens. В ней можно указать путь до GitLab и креды до него, это позволяет стягивать приватные репозитории через GOPROXY. Для каждого разработчика, для каждой CI машины это выглядит так: ты делаешь пустой приватный пакет, указываешь Go proxy до Athens, и она упрощает жизнь.

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

Как в «Юле» вводили монорепо


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

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

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

Первая стратегия выглядит следующим образом: мы все фризим. Для этого подготавливаем инфраструктуру в монорепо. Это самый простой паттерн действия.

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

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

Дальше мы переходим к локальному окружению.

Как я уже говорил, docker-compose идеально для этого подходит. Лучшая стратегия: отправить через volume в каждый билд путь до до локальной директории go modules. Тогда, если вы что-то делаете go.mod, оно попадает в GOPATH/pkg/mod. И это ускорит билд.

И конечно же добавить какие-нибудь вкусняшки. В этом случае речь о UI для GRPC. На каждый сервис сделали свой UI, вывели порт наружу. То есть можно было просто собирать GRPC-запросы в формочки.

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

Теперь поговорим о билдах.

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



Это все здорово работает до тех пор, пока не меняется что-то общее, например, go.mod. Когда сливается сервис, в который добавилась новая библиотека, CI начинает все билдить.

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

Что в компании сделали еще? Раньше там были пухлые Gitlab CI файлы. Но в монорепо можно обращаться к локальным скриптам. И в «Юле» выделили папку script на CI. И тяжелые вещи, которые нужны в CI, пишутся на Go-коде. Например, выбор ревьювера.

Выбор ревьювера эволюционировал в интересном формате. Сначала просто скидывали в чат созданные pull requests. Но ревью проходили долго потому, что все надеялись друг на друга и не приступали к задаче.

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

С монорепо решили перенести это в CI. Когда аттачится pull request, запускают Go-скрипт и выбирают двух случайных ревьюверов. Первого на основе измененных файлов: кто больше всего трогал, тот и выбирается рандомно среди топ-кандидатов. Второго — из всего проекта монорепо.



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

Естественно, в монорепозитории как по маслу пошли такие вещи, как нотификация при открытии PR, или нотификация, если PR долго не ревьювятся. Каждые 2-3 часа напоминают: «Эй, не забудь про ревью!».



Это повышает конверсию из pull requests в релиз, работает коллективная ответственность.

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



Сейчас в компании почти continuous delivery в production (кстати, он есть на тестовые стенды).

Выводы


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

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

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


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

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

Хотите больше материалов по go-разработке? У вас есть возможность купить видео с Golang Live 2020.

В этом году на конференции сделали акцент на Go в продуктовой разработке и раскрыли эту тему с трех сторон: как справиться с проблемами Go и какие есть тулзы, почему все-таки стоит выбрать именно Go, и как после этого поддерживать продукт.
Теги:
Хабы:
Всего голосов 20: ↑17 и ↓3 +14
Просмотры 9.2K
Комментарии Комментарии 14

Информация

Дата основания
Местоположение
Россия
Сайт
www.ontico.ru
Численность
31–50 человек
Дата регистрации
Представитель