Итак, наш блог будет состоять из 5 микросервисов, написанных на golang:
- API Gateway (api-gw) – отвечает за маршрутизацию, аутентификацию, логирование и трасировку запросов
- Пользователи (user) – регистрация/аутентификация пользователей, логирование, трасировка запросов
- Статьи (post) – создание/чтение/изменение/удаление статей (CRUD), логирование, трасировка и авторизация запросов
- Комментарии (comment) – создание/чтение/изменение/удаление комментариев (CRUD), логирование, трасировка и авторизация запросов
- Категории (category) – создание/чтение/изменение/удаление категорий (CRUD), логирование, трасировка и авторизация запросов
Клиентское приложение (web/frontend) будет реализован на vue.js и будет взаимодействовать с микросервисами через REST API, а сами микросервисы будут взаимодействовать друг с другом по gRPC.
В качестве хранилища мы будем использовать MongoDB.
Отдельной вишенкой на торте покажем, как с минимальными трудозатратами поддерживать документацию API (в формате swagger) в актуальном состоянии в активно развивающемся проекте.
Компонентная схема блога
Каждый микросервис будет реализован в отдельном Docker контейнере, а запуск проекта будет осуществляться с помощью docker-compose.
Сразу оговорюсь в примере, для упрощения процесса разработки, буду использовать два допущения, которые не следует использовать в продакшене.
- База данных развернута в Docker контейнере. Такой подход снижает надежность хранилища (за исключением схемы, о которой говорилось на HighLoad 2018).
- Весь проект размещен в одном git-репозитории. Этот подход противоречит одному из основных принципов микросервисной архитектуры — изолированность, и увеличивает вероятность появления межкомпонентной связанности.
Демо проекта можно посмотреть здесь, а исходный код здесь.
Структура проекта
Как будет построен процесса разработки
Как я уже ранее говорил, взаимодействие между микросервисами будет построено на основе gRPC. В двух словах gRPC это высокопроизводительный фреймворк, разработанный компанией Google, для вызова удаленных процедур (RPC) — работает поверх HTTP/2. В основе gRPC лежит так называемый протофайл (см. пример ниже), основная задача которого в компактной форме задекларировать две вещи:
- дать полный перечень интерфейсов сервиса (аналог API интерфейсов);
- описать что подается на вход каждого интерфейса и что получаем на выходе.
Ниже, в качестве примера, приведен протофайла сервис Category.
syntax = "proto3";
package protobuf;
import "google/api/annotations.proto";
//Описание интерфейсов сервиса Category
service CategoryService {
//Создание записи
rpc Create (CreateCategoryRequest) returns (CreateCategoryResponse) {
option (google.api.http) = {
post: "/api/v1/category"
};
}
//Обновление записи
rpc Update (UpdateCategoryRequest) returns (UpdateCategoryResponse) {
option (google.api.http) = {
post: "/api/v1/category/{Slug}"
};
}
//Удаление записи
rpc Delete (DeleteCategoryRequest) returns (DeleteCategoryResponse) {
option (google.api.http) = {
delete: "/api/v1/category/{Slug}"
};
}
//Возвращает запись по SLUG
rpc Get (GetCategoryRequest) returns (GetCategoryResponse) {
option (google.api.http) = {
get: "/api/v1/category/{Slug}"
};
}
//Поиск
rpc Find (FindCategoryRequest) returns (FindCategoryResponse) {
option (google.api.http) = {
get: "/api/v1/category"
};
}
}
//------------------------------------------
// CREATE
//------------------------------------------
message CreateCategoryRequest {
string ParentId = 1;
string Name = 2;
string Path = 3;
}
message CreateCategoryResponse {
Category Category = 1;
}
//------------------------------------------
// UPDATE
//------------------------------------------
message UpdateCategoryRequest {
string Slug = 1;
string ParentId = 2;
string Name = 4;
string Path = 5;
int32 Status = 6;
}
message UpdateCategoryResponse {
int32 Status =1;
}
//------------------------------------------
// DELETE
//------------------------------------------
message DeleteCategoryRequest {
string Slug = 1;
}
message DeleteCategoryResponse {
int32 Status =1;
}
//------------------------------------------
// GET
//------------------------------------------
message GetCategoryRequest {
string Slug = 1;
}
message GetCategoryResponse {
Category Category = 1;
}
//------------------------------------------
// FIND
//------------------------------------------
message FindCategoryRequest {
string Slug = 1;
}
message FindCategoryResponse {
repeated Category Categories = 1;
}
//------------------------------------------
// CATEGORY
//------------------------------------------
message Category {
string Slug = 1;
string ParentId = 2;
string Path = 3;
string Name = 4;
int32 Status = 5;
}
Теперь, когда мы в общих чертах разобрались зачем нужен протофайл, посмотрим как будет выглядеть процесс разработки наших микросервисов:
- Описываем структуру сервис в протофайле;
- Запускаем генератор кода (./bin/protogen.sh), он сгенерит нам основную часть серверного кода + создаст клиентский код, например, для API Gateway + создаст актуальную документацию в формате swagger;
- Все что нам останется сделать своими руками, это написать код реализацию интерфейсов в специальном файле /protobuf/functions.go.
Далее, если мы захотим внести изменения в один из наших микросервисов, действуем по вышеописанному алгоритму: правим протофайл, запускаем protogen, правим реализацию в functions.go, а в документацию и к клиентам изменения “уедут” автоматически.
Продолжение в статье «Пишем блог на микросервисах часть 2 API Gateway».
Комментарии (8)
karl93rus
29.10.2019 08:19А ещё я никак не могу понять смысл gRPC. Зачем ещё какая-то прослойка, если можно просто поднять микросервис и по урлу его дёргать? Объясните пожалуйста.
xkondorx
29.10.2019 08:32Я тоже не могу найти объективных причин усложнения архитектуры, объемы передаваемой информации не оправдывают его применение. Ну и раз уж речь зашла о gRPC то не плохо было бы сюда прикрутить что нибудь вроде егеря для трассировки. Чтобы решить те задачи которые в данном случае решает gRPC, достаточно реализовать REST Client для каждого микросервиса в виде пакета.
Sly_tom_cat
29.10.2019 16:49Ну если относиться к проекту как к демонстратору технологий, то решения — вполне уместные, разве что вот Mongo немного смущает…
xkondorx
29.10.2019 16:53Вроде как все сущности укладываются в рамки «документа», транзакционно зависимых CRUD операций на ум не приходит. Почему бы и не mongo, по сути на его месте может быть любая БД.
time2rfc
29.10.2019 12:23При написании связанных сервисов на GO столкнулся с тем что много кода дублируют друг дружку, особенно части отвечающие за транспорт и бизнесс-область, я правильно понимаю что по этой причине у вас все в одном моно-репозитории?
Сам думал о моно-репе или модных-новых-классных модуляхPtimofeev Автор
30.10.2019 12:03Мое личное мнение, моно-репа это наследие мышления в системе координат монолитых решений. Избыточность кода, это цена за независимость и децентрализованность. Представьте что каждый отдельный микросервис «пилит» отдельная команда, которая ничего не знает о коде других команд. В этом смысле избыточность выглядет уже иначе )
В примере моно-репа использовалась для упрощения процесса запуска проекта и понимания структуры проекта в целом.
DmitriyTitov
Каковы на ваш взгляд основные причины для того чтобы разделить сайт с журналами на отдельные микрослужбы? Какие проблемы вы решаете таким образом?
Ещё интересуют причины выбора Mongo вместо традиционных реляционных БД. В чём преимущество Mongo с вашей точки зрения?
Ptimofeev Автор
Из основных причин, пожалуй это высокая нагрузка, но из моего опыта у систем такого класса это достаточно редкое явление ) Также использование микросервисной архитектуры позволяет сделать разные части проекта независимыми, т.е. например, отказ в работе комментариев не отразится на работоспособности других сервисов. В целом, цель статьи показать вариант реализации взаимодействия между микросервисами на простом и понятном примере. MongoDB выбрана с целью децентрализации данных. Использование единой релеационной БД вносит ненужную связанность между микросервисами, а использование отдельных релеационных БД под каждый микросервис, на мой взгляд, избыточно.