Русский
Русский
English
Статистика
Реклама

Go-swagger как основа взаимодействия микросервисов



Здравствуй, NickName! Если ты программист и работаешь с микросервисной архитектурой, то представь, что тебе нужно настроить взаимодействие твоего сервиса А с каким-то новым и ещё неизвестным тебе сервисом Б. Что ты будешь делать в первую очередь?

Если задать такой вопрос 100 программистам из разных компаний, скорее всего, мы получим 100 разных ответов. Кто-то описывает контракты в swagger, кто-то в gRPC просто делает клиенты к своим сервисам без описания контракта. А кто-то и вовсе хранит JSON в гуглодоке :D. В большинстве компаний складывается свой подход к межсервисному взаимодействию на основании каких-либо исторических факторов, компетенций, стека технологий и прочего. Я хочу рассказать, как сервисы в Delivery Club общаются друг с другом и почему мы сделали именно такой выбор. И главное как мы обеспечиваем актуальность документации с течением времени. Будет много кода!

Ещё раз привет! Меня зовут Сергей Попов, я тим-лид команды, отвечающей за поисковую выдачу ресторанов в приложениях и на сайте Delivery Club, а также активный участник нашей внутренней гильдии разработки на Go (возможно, мы об этом ещё расскажем, но не сейчас).

Сразу оговорюсь, речь пойдет, в основном, про сервисы, написанные на Go. Генерирование кода для PHP-сервисов мы ещё не реализовали, хотя достигаем там единообразия в подходах другим способом.

К чему, в итоге, мы хотели прийти:

  1. Обеспечить актуальность контрактов сервисов. Это должно ускорить внедрение новых сервисов и упростить коммуникацию между командами.
  2. Прийти к единому способу взаимодействия по HTTP между сервисами (пока не будем рассматривать взаимодействия через очереди и event streaming).
  3. Стандартизировать подход к работе с контрактами сервисов.
  4. Использовать единое хранилище контрактов, чтобы не искать доки по всяким конфлюенсам.
  5. В идеале, генерировать клиенты под разные платформы.

Из всего перечисленного на ум приходит Protobuf как единый способ описания контрактов. Он имеет хороший инструментарий и может генерировать клиенты под разные платформы (наш п.5). Но есть и явные недостатки: для многих gRPC остается чем-то новым и неизведанным, а это сильно усложнило бы его внедрение. Ещё одним важным фактором было то, что в компании давно принят подход specification first, и документация уже существовала на все сервисы в виде swagger или RAML-описания.

Go-swagger


Так совпало, что в то же время мы начали адаптацию Go в компании. Поэтому следующим нашим кандидатом на рассмотрение оказался go-swagger инструмент, который позволяет генерировать клиентов и серверный код из swagger-спецификации. Из очевидных недостатков он генерирует код только для Go. На самом деле, там используется гошное кодогенерирование, и go-swagger позволяет гибко работать с шаблонам, так что, теоретически, его можно использовать для генерирования кода на PHP, но мы ещё не пробовали.

Go-swagger это не только про генерирование транспортного слоя. Фактически он генерирует каркас приложения, и тут я бы хотел немного упомянуть о культуре разработки в DC. У нас есть Inner Source, а это значит, что любой разработчик из любой команды может создать pull request в любой сервис, который у нас есть. Чтобы такая схема работала, мы стараемся стандартизировать подходы в разработке: используем общую терминологию, единый подход к логированию, метрикам, работе с зависимостями и, конечно же, к структуре проекта.

Таким образом, внедряя go-swagger, мы вводим стандарт разработки наших сервисов на Go. Это еще один шаг навстречу нашим целям, на который мы изначально не рассчитывали, но который важен для разработки в целом.

Первые шаги


Итак, go-swagger оказался интересным кандидатом, который, кажется, может покрыть большинство наших хотелок требований.
Примечание: весь дальнейший код актуален для версии 0.24.0, инструкцию по установке можно посмотреть в нашем репозитории с примерами, а на официальном сайте есть инструкция по установке актуальной версии.
Давайте посмотрим, что он умеет. Возьмём swagger-спеку и сгенерируем сервис:

> goswagger generate server \    --with-context -f ./swagger-api/swagger.yml \    --name example1

Получилось у нас следующее:



Makefile и go.mod я уже сделал сам.

Фактически у нас получился сервис, который обрабатывает запросы, описанные в swagger.

> go run cmd/example1-server/main.go2020/02/17 11:04:24 Serving example service at http://127.0.0.1:54586   > curl http://localhost:54586/hello -iHTTP/1.1 501 Not ImplementedContent-Type: application/jsonDate: Sat, 15 Feb 2020 18:14:59 GMTContent-Length: 58Connection: close "operation hello HelloWorld has not yet been implemented"

Второй шаг. Разбираемся с шаблонизацией


Очевидно, что сгенерированный нами код далёк от того, что мы хотим видеть в эксплуатации.

Что мы хотим от структуры нашего приложения:

  • Уметь конфигурировать приложение: передавать настройки подключения к БД, указывать порт HTTP-соединений и прочее.
  • Выделить объект приложения, который будет хранить состояние приложения, подключение к БД и прочее.
  • Сделать хэндлеры функциями нашего приложения, это должно упростить работу с кодом.
  • Инициализировать зависимости в main-файле (в нашем примере этого не будет, но мы всё равно этого хотим.

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



Нам необходимо описать файлы шаблонов (`*.gotmpl`) и файл для конфигурации (`*.yml`) генерирования нашего сервиса.

Далее по порядку разберем те шаблоны, которые сделал я. Глубоко погружаться в работу с ними не буду, потому что документация go-swagger достаточно подробная, например, вот описание файла конфигурации. Отмечу только, что используется Go-шаблонизация, и если у вас уже есть в этом опыт или приходилось описывать HELM-конфигурации, то разобраться не составит труда.

Конфигурирование приложения


config.gotmpl содержит простую структуру с одним параметром портом, который будет слушать приложение для входящих HTTP-запросов. Также я сделал функцию InitConfig, которая будет считывать переменные окружения и заполнять эту структуру. Вызывать буду из main.go, поэтому InitConfig сделал публичной функцией.

package config import (    "github.com/pkg/errors"    "github.com/vrischmann/envconfig") // Config structtype Config struct {    HTTPBindPort int `envconfig:"default=8001"`} // InitConfig funcfunc InitConfig(prefix string) (*Config, error) {    config := &Config{}    if err := envconfig.InitWithPrefix(config, prefix); err != nil {        return nil, errors.Wrap(err, "init config failed")    }     return config, nil}

Чтобы этот шаблон использовался при генерировании кода, его нужно указать в YML-конфиге:

layout:  application:    - name: cfgPackage      source: serverConfig      target: "./internal/config/"      file_name: "config.go"      skip_exists: false

Немного расскажу про параметры:

  • name несёт чисто информативную функцию и на генерирование не влияет.
  • source фактически путь до файла шаблона в camelCase, т.е. serverConfig равносильно ./server/config.gotmpl.
  • target директория, куда будет сохранен сгенерированный код. Здесь можно использовать шаблонизацию для динамического формирования пути (пример).
  • file_name название сгенерированного файла, здесь также можно использовать шаблонизацию.
  • skip_exists признак того, что файл будет сгенерирован только один раз и не будет перезаписывать существующий. Для нас это важно, потому что файл конфига будет меняться по мере роста приложения и не должен зависеть от генерируемого кода.

В конфиге кодогенерирования нужно указывать все файлы, а не только те, которые мы хотим переопределить. Для файлов, которые мы не меняем, в значении source указываем asset:<путь до шаблона>, например, как здесь: asset:serverConfigureapi. Кстати, если интересно посмотреть оригинальные шаблоны, то они здесь.

Объект приложения и хэндлеры


Объект приложения для хранения состояния, подключений БД и прочего я описывать не буду, всё аналогично только что сделанному конфигу. А вот с хэндлерами всё немного интереснее. Наша ключевая цель состоит в том, чтобы при добавлении URL в спецификацию у нас в отдельном файле создалась функция с заглушкой, и самое главное, чтобы наш сервер вызывал эту функцию для обработки запроса.

Опишем шаблон функции и заглушки:

package app import (    api{{ pascalize .Package }} "{{.GenCommon.TargetImportPath}}/{{ .RootPackage }}/operations/{{ .Package }}"    "github.com/go-openapi/runtime/middleware") func (srv *Service){{ pascalize .Name }}Handler(params api{{ pascalize .Package }}.{{ pascalize .Name }}Params{{ if .Authorized }}, principal api{{ .Package }}.{{ if not ( eq .Principal "interface{}" ) }}*{{ end }}{{ .Principal }}{{ end }}) middleware.Responder {    return middleware.NotImplemented("operation {{ .Package }} {{ pascalize .Name }} has not yet been implemented")}

Немного разберём пример:

  • pascalize приводит строку с CamelCase (описание остальных функции здесь).
  • .RootPackage пакет сгенерированного веб-сервера.
  • .Package название пакета в сгенерированном коде, в котором описаны все необходимые структуры для HTTP-запросов и ответов, т.е. структуры. Например, структура для тела запроса или структура ответа.
  • .Name название хэндлера. Оно берётся из operationID в спецификации, если указано. Я рекомендую всегда указывать operationID для более очевидного результата.

Конфиг для хэндлера следующий:

layout:  operations:    - name: handlerFns      source: serverHandler      target: "./internal/app"      file_name: "{{ (snakize (pascalize .Name)) }}.go"      skip_exists: true

Как видите, код хэндлеров не будет перезаписываться (skip_exists: true), а название файла будет генерироваться из названия хэндлера.

Окей, функция с заглушкой есть, но веб-сервер ещё не знает, что эти функции нужно использовать для обработки запросов. Я исправил это в main.go (весь код приводить не буду, полную версию можно найти здесь):

package main {{ $name := .Name }}{{ $operations := .Operations }}import (    "fmt"    "log"     "github.com/delivery-club/go-swagger-example/{{ dasherize .Name }}/internal/generated/restapi"    "github.com/delivery-club/go-swagger-example/{{ dasherize .Name }}/internal/generated/restapi/operations"    {{range $index, $op := .Operations}}        {{ $found := false }}        {{ range $i, $sop := $operations }}            {{ if and (gt $i $index ) (eq $op.Package $sop.Package)}}                {{ $found = true }}            {{end}}        {{end}}        {{ if not $found }}        api{{ pascalize $op.Package }} "{{$op.GenCommon.TargetImportPath}}/{{ $op.RootPackage }}/operations/{{ $op.Package }}"        {{end}}    {{end}}     "github.com/go-openapi/loads"    "github.com/vrischmann/envconfig"     "github.com/delivery-club/go-swagger-example/{{ dasherize .Name }}/internal/app") func main() {    ...    api := operations.New{{ pascalize .Name }}API(swaggerSpec)     {{range .Operations}}    api.{{ pascalize .Package }}{{ pascalize .Name }}Handler = api{{ pascalize .Package }}.{{ pascalize .Name }}HandlerFunc(srv.{{ pascalize .Name }}Handler)    {{- end}}    ...}

Код в импорте выглядит сложным, хотя на самом деле здесь просто Go-шаблонизация и структуры из репозитория go-swagger. А в функции main мы просто присваиваем хэндлерам наши сгенерированные функции.

Осталось сгенерировать код с указанием нашей конфигурации:

> goswagger generate server \        -f ./swagger-api/swagger.yml \        -t ./internal/generated -C ./swagger-templates/default-server.yml \        --template-dir ./swagger-templates/templates \        --name example2

Финальный результат можно посмотреть в нашем репозитории.

Что мы получили:

  • Мы можем использовать свои структуры для приложения, конфигов и всего, что захотим. Самое главное это достаточно просто встраивается в генерируемый код.
  • Мы можем гибко управлять структурой проекта, вплоть до названий отдельных файлов.
  • Go-шаблонизация выглядит сложной и к ней нужно привыкнуть, но в целом это очень мощный инструмент.

Третий шаг. Генерирование клиентов


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

Для проектов на Go принято складывать публичные пакеты в ./pkg, мы сделаем так же: положим клиент для нашего сервиса в pkg, а сам код сгенерируем следующим образом:

> goswagger generate client -f ./swagger-api/swagger.yml -t ./pkg/example3

Пример сгенерированного кода здесь.

Теперь все потребители нашего сервиса могут импортировать себе этот клиент, например, по тэгу (для моего примера тэг будет example3/pkg/example3/v0.0.1).

Шаблоны клиентов можно настраивать, чтобы, например, прокидывать open tracing id из контекста в заголовок.

Выводы


Естественно, наша внутренняя реализация отличается от приведенного здесь кода, в основном, за счёт использования внутренних пакетов и подходов к CI (запуск различных тестов и линтеров). В сгенерированном коде из коробки настроен сбор технических метрик, работа с конфигами и логирование. Мы стандартизировали все общие инструменты. За счёт этого мы упростили разработку в целом и выпуск новых сервисов в частности, обеспечили более быстрое прохождение чек-листа сервиса перед деплоем на прод.

Давайте проверим, получилось ли достигнуть первоначальных целей:

  1. Обеспечить актуальность описанных для сервисов контрактов, это должно ускорить внедрение новых сервисов и упростить коммуникацию между командами Да.
  2. Прийти к единому способу взаимодействия по HTTP между сервисами (пока не будем рассматривать взаимодействия через очереди и event streaming) Да.
  3. Стандартизировать подход к работе с контрактами сервисов, т.к. мы давно пришли к подходу Inner Source в разработке сервисов Да.
  4. Использовать единое хранилище контрактов, чтобы не искать документацию по всяким конфлюенсам Да (фактически Bitbucket).
  5. В идеале, генерировать клиенты под разные платформы Нет (на самом деле, не пробовали, шаблонизация не ограничивает в этом плане).
  6. Внедрить стандартную структуру сервиса на Go Да (дополнительный результат).

Внимательный читатель, наверное, уже задался вопросом: как файлы шаблонов попадают в наш проект? Сейчас мы храним их в каждом нашем проекте. Это упрощает повседневную работу, позволяет что-то настраивать под конкретный проект. Но есть и другая сторона медали: отсутствует механизм централизованного обновления шаблонов и доставки новых фич, в основном, связанных с CI.

P.S. Если этот материал понравится, то в дальнейшем подготовим статью про стандартную архитектуру наших сервисов, расскажем, какими принципами мы пользуемся при разработке сервисов на Go.
Источник: habr.com
К списку статей
Опубликовано: 04.08.2020 14:20:22
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Блог компании mail.ru group

Блог компании delivery club tech

Go

Микросервисы

Golang

Swagger

Delivery club

Dctech

Microservices

Api

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru