Мы широко используем микросервисную архитектуру, хоть и не считаем ее панацеей, и чуть больше 2 лет назад начали переходить на язык Go. Он сравнительно прост и, на мой взгляд, очень хорошо подходит для создания простых, небольших и быстрых микросервисов. Эта простота имеет и обратную сторону: из-за неё возникает множество способов решить одну и ту же задачу.  Казалось бы, насколько сильно может отличаться один микросервис, который ходит в базу данных, от другого микросервиса, который ходит в соседнюю базу данных? Например, одна команда использует Go 1.9, glide, стандартный database/sql и одну структуру проекта, а в это же время другая команда использует Go 1.13, modules, sqlx и, конечно же, другую структуру проекта.  Когда один микросервис в компании отличается от другого, а он, в свою очередь, отличается от третьего — это замедляет разработку. А медленная разработка — это убытки повод для оптимизации.  Меня зовут Алексей Партилов, я техлид команды web-разработки в компании Lamoda. В этой статье я расскажу, как мы справляемся с разношерстностью около 40 наших микросервисов на Go. Статья будет полезна разработчикам, которые только вливаются в Go и не знают, с чего начать более сложный проект, чем “helloworld”.  Изначально Lamoda работала на одном коробочном монолите на PHP. Потом часть функций начала перемещаться в новые Python и PHP сервисы. Сейчас у нас есть монолиты на PHP с большой и сложной бизнес-логикой. Эти приложения занимаются автоматизацией нашей операционной деятельности: доставки, фотостудии, партнерского сервиса. Также у нас есть монолит на Java, который автоматизирует множество сложных процессов на складе.  В этом случае мы считаем использование монолитов оправданным, поскольку каждый из них разрабатывается своей командой, и у нас нет необходимости масштабировать определенные участки логики.  В e-commerce платформе мы уже достаточно давно перешли от монолитов на Python в пользу микросервисов на Golang. Впрочем, несколько больших приложений со сложным UI мы оставили на Python+Django. В распиле монолита на микросервисы участвовали несколько команд, и каждая имела свой неповторимый стиль создания микросервисов. Это в дальнейшем создало некоторые трудности.  Микросервисы сильно различались между собой, что усложняло вхождение в проект новым разработчикам или создание новой фичи. Время backend-разработчика уходило на то, чтобы разобраться в работе сервиса, подстроиться под его стиль, научиться использовать набор библиотек и инструментарий, который используется в проекте. Мы решили минимизировать время на эту работу и начали приводить наши сервисы к единому виду. Большинство наших команд уже давно соблюдают правило, которое гласит: прежде чем написать новый микросервис или ручку в уже существующий микросервис, нужно сделать спецификацию по стандарту OpenAPI. Это правило приносит много плюсов: В качестве генератора кода по OpenAPI-спецификации мы используем форк проекта go-swagger (и ласково зовем его gogi). Мы не выкладывали его в opensource, поскольку ищем ему альтернативу, но принципы работы с этим генератором кода подойдут и к другим генераторам. Можно запускать генератор из бинарного файла, но мы используем docker-образ. Дело в том, что на разных проектах может потребоваться та или иная версия генератора. Для версионирования удобно использовать именно docker. Генерация кода из спецификации запускается командой в Makefile:  Большинство разночтений у нас случается именно в организации кода в проекте. У каждой команды есть своя устоявшаяся структура проекта, а бывает, что и не одна. Чтобы сделать проекты разных команд максимально похожими друг на друга, мы выделили эталонный проект-пустышку. Его главная задача — продемонстрировать, как организовать код внутри своего проекта: Любая команда может взять этот проект за основу своего нового микросервиса или скорректировать структуру уже существующего проекта. Пустышка не делает ничего полезного, зато имеет подключения к базе данных (мы в основном используем Postgres), к одному из наших production-сервисов, Kafka и несколько ручек в API. Почему именно такая конфигурация? Всё просто. Потому что большинство наших сервисов используют именно такие источники данных. Для новых сервисов мы сделали динамический шаблон проекта на cookiecutter. Благодаря этому мы не копируем структуру проекта вручную и не вычищаем все плейсхолдеры. Подробнее про шаблон можно прочитать в девятом пункте про динамический шаблон проекта. Пока речь пойдет про другие улучшения, которые попали в этот шаблон. Начиная с версии 1.11 сообщество языка Go перешло на новую систему управления зависимостями под названием go modules. С версии 1.14 модули объявлены как production-ready. Раньше у нас долгое время был glide, но мы достаточно быстро и почти безболезненно перешли на go modules, как, впрочем, и большая часть Go-сообщества. Go modules хранят все зависимости проектов в одном месте $GOPATH/pkg/mod и позволяют не размещать сами проекты в GOPATH.  Для поддержки модулей в самом Go были внесены изменения во многие команды (build, get, test). Например, build теперь загрузит недостающие зависимости и только потом начнет сборку проекта. Подробнее можно почитать в официальной документации. В официальном блоге golang есть обширная статья о том, как лучше мигрировать на модули, если в проекте есть другие системы управления зависимостями. Мы же дадим свой короткий чеклист с нюансами, касающимся приватных репозиториев.  1. Активируем go modules. Если речь идет о проекте с Go до версии 1.13, который находится в $GOPATH, то советую форсированно активировать go mod 2. Добавляем RSA-ключ для приватных репозиториев. Если для работы с репозиторием используется RSA-ключ c паролем, то лучше проследить, чтобы он был подгружен в ssh-agent. Это позволит не вводить пароль на каждую загрузку модуля. 3. Добавляем исключение для Go proxy. Начиная с версии Go 1.13, пакеты по умолчанию загружаются из публичного прокси-репозитория proxy.golang.org, который не знает про приватные пакеты. Поэтому мы инструктируем модули, чтобы пакеты брались из нашего репозитория напрямую. 4. Создаем go.mod-файл. Для поддержки модулей мы сделали файл go.mod и наполнили его зависимостями. Если в проекте уже используется другой пакетный менеджер (например, glide), при создании mod-файла зависимости будут подтянуты из lock-файла. В новом проекте будет создан go.mod — файл, не имеющий зависимостей внутри себя. Под полным именем понимается название проекта с адресом репозитория, где он живет, например, github.com/spf13/cobra. 5. Запускаем тесты. Даже если их еще нет в проекте. Запуск тестов провоцирует загрузку зависимостей, компиляцию и, собственно, запуск тестов. Если все три стадии пройдены, то внедрение модулей завершено. В Lamoda для доставки кода на прод мы используем свой Kubernetes-кластер, поэтому нам необходимы Docker-образы для каждого проекта. В принципе, для сборки Go-проектов можно использовать официальный Docker-образ golang. При сборке запускаем линтеры и тесты c построением отчетов по покрытию прямо внутри Docker-контейнера. Поэтому мы взяли за основу официальный Docker-образ и дополнили его необходимыми пакетами, сертификатами и ключами. Все наши сборки golang-проектов основываются на этом базовом образе. Но не советую использовать образ golang как конечный для production-среды. Дело в том, что такие образы далеко не легковесны (они занимают порядка сотен мегабайт) и содержат в себе массу ненужного для прода: базовый образ debian или alpine, компилятор go, вспомогательные библиотеки. Нашим решением было использовать разные Docker-образы для сборки и релиза, а также Docker multistage.  Вкратце, Docker multistage — это способ сборки в Docker, благодаря которому проект можно собрать в одном образе, а потом перенести все необходимые артефакты в другой образ, где нет установленных зависимостей для сборки, временных файлов и т.д.  Вот пример сборки одного из наших микросервисов:  Во многих наших микросервисах используются реляционные базы данных, в основном, Postgres. Чем дольше живет сервис, тем больше изменений вносится в базу. Конечно, их можно вносить и вручную или руками DBA, если он есть. Но такой подход не позволит быстро развернуть тестовую БД. Или невозможно будет вспомнить, как в проекте появилось то или иное поле. У нас разработчики пишут SQL-скрипты для миграции и потом под контролем DBA и DevOps накатывают их в production среду.  Чтобы миграции не терялись где-нибудь в Jira, они лежат в коде проекта и накатываются при помощи библиотеки migrate. Наш выбор пал именно на эту библиотеку, потому что она сейчас хорошо поддерживается. Также, что немаловажно, migrate может запускаться в Docker-образе — это снимает необходимость вносить ее в зависимости проекта.  Принцип работы библиотеки достаточно прост — в папке migrations нужно создать файлы вида <№ миграции>_<название>.[up|down].sql up — миграция вверх, т.е. накатывающая изменения,
down — миграция вниз, т.е. откатывающая изменения.  Содержимое файлов по сути является обычным SQL-файлом, который поддерживает БД. Пример миграции «вверх»: Пример миграции «вниз»: Запустить миграции можно, указав базу данных и папку с миграциями: Создать новые миграции можно следующим образом: Мы вручную задаем содержимое миграций и, соответственно, тестируем. Поэтому хорошей идеей будет связать между собой оба процесса: запуск тестов проекта и накатывание миграций. О том, как это сделать, речь пойдет в следующем разделе. Подробнее о работе с migrate написано здесь. В Lamoda есть сервисы, которые могут использовать БД, очереди или другие внешние источники данных. При тестировании и/или разработке часто нужно развернуть микросервис с его внешними источниками данных, сконфигурировать и провести их подготовку. Например, сервис использует БД Postgres и определенную схему внутри этой базы. Поэтому перед запуском сервиса нужно развернуть базу Postgres и накатить на нее миграции. Эти действия придется выполнять при каждом развертывании/тестировании проекта в development окружении, поэтому мы их автоматизировали с помощью Docker Compose. Compose управляет порядком запуска контейнеров для построения development окружения сервиса. Например, в одном нашем микросервисе контейнеры при полном развертывании запускаются в следующем порядке: Необязательно соблюдать именно эту последовательность. Например, можно запустить только контейнер с базой и миграции. Последнее обычно бывает полезно, когда хочется запустить тесты локально, но держать базу в контейнере.  Пример конфигурации Docker-compose для одного из наших микросервисов: Для удобного использования Docker-compose в Makefile я рекомендую добавить следующие команды: Для языка Go есть утилита gofmt, которая может работать в режиме линтера, и другие инструменты для форматирования и линтинга: goimports, go-critic, gocyclo и т.д. Установка множества линтеров по отдельности, их интеграция в IDE и CI может стать очень тяжелой работой. Вместо этого я рекомендую использовать golangci-lint, который является агрегатором большинства известных в мире Go линтеров. Он позволяет запускать их параллельно, управлять запуском в зависимости от среды и конфигурировать всё из одного конфиг-файла.  Можно ускорить процесс и указать коммит, начиная с которого ошибки считаются «новыми». Так мы сразу внедряем линтер в проект, чтобы не допустить новых ошибок, и понемногу исправляем ошибки из «старого» кода, который был в проекте до его появления. Установка выглядит так: Пример конфигурации файла: Запуск: Или же можно запускать golangci-lint при помощи утилиты pre-commit, о которой написано ниже. Часто перед созданием Pull Request код должен пройти ряд автоматических проверок: например, проверки линтеров и/или тесты, которые можно запустить локально. Обычно для этих целей мы пишем bash-скрипты и устанавливаем в качестве git pre-commit хуков. В таком случае скрипты достаточно сложно поддерживать, поскольку каждый разработчик пишет их по-своему. Есть и другая трудность: их приходится хранить в репозитории и устанавливать на каждую новую машину вручную.  Вместо этого советую использовать python утилиту pre-commit: или любым другим способом, описанным в официальном руководстве. Теперь линтер golangci-lint будет запускаться при каждом коммите и проверять измененные или добавленные файлы. Либо можно запускать hook'и вручную. Опция all-files запускает проверки на всех файлах проекта, а не только на файлах в staging'e: Так можно генерировать hook'и для большого ассортимента линтеров утилит и test runner'ов. Подробнее можно прочитать на странице проекта https://pre-commit.com Gonkey — это инструмент тестирования микросервисов, разработанный в Lamoda. Как правило, он используется для написания функциональных тестов. Подробности о том, зачем появился Gonkey, и простые примеры его использования можно найти в нашей статье на habr.ru “Gonkey — инструмент тестирования микросервисов”.  Gonkey умеет: Полная документация есть на GitHub. Все вышесказанное собиралось далеко не один день и пережило другие, более «костыльные» решения. Оставалась только одна проблема: новые разработчики, да и некоторые старые, не будем лукавить, не сильно вчитывались в эту статью.  Согласитесь, не каждый готов осилить несколько страниц текста в Confluence в пятницу вечером. Хочется быстро создать сервис, в котором все вышесказанное будет «из коробки». Мы немного подумали и сделали динамический шаблон микросервиса на базе cookiecutter. Если коротко, cookiecutter — это python-утилита, которая из jinja-шаблонов генерирует исходный код на любом необходимом языке. Нам остается только определить переменные в этих jinja-шаблонах и сделать инструкцию.  Для генерации новых микросервисов за пару минут теперь нужно лишь вызвать команду: и заполнить несколько переменных, например, название проекта, его описание и т.д. На выходе получается готовый работоспособный код на основе всех вышесказанных возможностей. Наш шаблон для cookiecutter выпущен совсем недавно, поэтому наши команды только начинают его использовать. Сейчас Lamoda продолжает унифицировать свои сервисы. Все еще устаканиваются споры по некоторым вопросам. В то же время наши команды перенимают практики, про которые я рассказал, и разности между проектами постепенно сглаживаются. В случае с микросервисами быть похожим на других — это хорошо. Одинаковость снижает наши трудозатраты на то, чтобы стартовать новый микросервис (потому что не надо писать всё с нуля), и ускоряет адаптацию инженера (потому что новый сервис будет похож на все остальные).  А, как мы знаем, если разработчик не занят рутиной, то он занят творчеством разработкой полезного функционала, который будет радовать наших пользователей.  Поэтому рекомендую всем, кто переезжает на/использует микросервисную архитектуру, как можно раньше задуматься над тем, чтобы сервисы были похожими друг на друга. Backend разработчик