Пишем блог на микросервисах – часть 1 «Общее описание»

В этой статье хочу поделится нашими c SergeyMaslov наработками решения типовых задач с использованием микросервисной архитектуры на примере задачи «создание блога» (в надежде, что читатель представляет как устроен блог и это не должно вызывать вопросов по функциональности:)

Итак, наш блог будет состоять из 5 микросервисов, написанных на golang:

  • API Gateway (api-gw) – отвечает за маршрутизацию, аутентификацию, логирование и трасировку запросов
  • Пользователи (user) – регистрация/аутентификация пользователей, логирование, трасировка запросов
  • Статьи (post) – создание/чтение/изменение/удаление статей (CRUD), логирование, трасировка и авторизация запросов
  • Комментарии (comment) – создание/чтение/изменение/удаление комментариев (CRUD), логирование, трасировка и авторизация запросов
  • Категории (category) – создание/чтение/изменение/удаление категорий (CRUD), логирование, трасировка и авторизация запросов

Клиентское приложение (web/frontend) будет реализован на vue.js и будет взаимодействовать с микросервисами через REST API, а сами микросервисы будут взаимодействовать друг с другом по gRPC.

В качестве хранилища мы будем использовать MongoDB.

Отдельной вишенкой на торте покажем, как с минимальными трудозатратами поддерживать документацию API (в формате swagger) в актуальном состоянии в активно развивающемся проекте.

Компонентная схема блога


image

Каждый микросервис будет реализован в отдельном 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;
}

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

  1. Описываем структуру сервис в протофайле;
  2. Запускаем генератор кода (./bin/protogen.sh), он сгенерит нам основную часть серверного кода + создаст клиентский код, например, для API Gateway + создаст актуальную документацию в формате swagger;
  3. Все что нам останется сделать своими руками, это написать код реализацию интерфейсов в специальном файле /protobuf/functions.go.

Далее, если мы захотим внести изменения в один из наших микросервисов, действуем по вышеописанному алгоритму: правим протофайл, запускаем protogen, правим реализацию в functions.go, а в документацию и к клиентам изменения “уедут” автоматически.

Продолжение в статье «Пишем блог на микросервисах часть 2 API Gateway».
X5 Retail Group
Все о цифровой трансформации ритейла

Комментарии 18

    0
    Каковы на ваш взгляд основные причины для того чтобы разделить сайт с журналами на отдельные микрослужбы? Какие проблемы вы решаете таким образом?
    Ещё интересуют причины выбора Mongo вместо традиционных реляционных БД. В чём преимущество Mongo с вашей точки зрения?
      0
      Из основных причин, пожалуй это высокая нагрузка, но из моего опыта у систем такого класса это достаточно редкое явление ) Также использование микросервисной архитектуры позволяет сделать разные части проекта независимыми, т.е. например, отказ в работе комментариев не отразится на работоспособности других сервисов. В целом, цель статьи показать вариант реализации взаимодействия между микросервисами на простом и понятном примере. MongoDB выбрана с целью децентрализации данных. Использование единой релеационной БД вносит ненужную связанность между микросервисами, а использование отдельных релеационных БД под каждый микросервис, на мой взгляд, избыточно.
        0
        1. Каким образом межсервисное сетевое взаимодействие, которое на порядки медленнее in-process, поможет вам в преодолении высоких нагрузок?
        2. При отказе сервиса User все будет продолжать работать? Really?
        3. Каким образом обеспечивается целостность и непротиворечивость данных в трех экземплярах БД? Или этот аспект вносит «ненужную связность»?
        4. В зависимостях между сервисами часть стрелочек (например, комментарии зависят от User) опущена намеренно для «чистоты концепции»?
          0
          Попробую ответить по пунктам
          1.Каким образом межсервисное сетевое взаимодействие ... — имеется ввиду когда под нагрузкой монолит будет «вставать на коленки» микросервис развернутый, например, на K8s за LB будет автоматически скейлится и работать в штатном режиме.
          Безусловно за LB можно поставить весь монолит, вопрос сколько Вам для этого потребуется ресурсов. Прелесть микросервисова в том, что вы масштабируете только узкие места, сохраняя производительность малой кровью.
            +1
            Спасибо, вопросы мои были типовые, ответы, соответственно, тоже «давайте все разделим, а со связями разберемся потом, а с чем не разберемся — закэшируем». Начиная с 1980-х, это уже третья попытка. А ведь практически любая функциональная модификация системы добавляет и новые связи.

            Не совсем понятно одно: монолит — это все что не микросервисы? Если чуть приподняться, то в предметных областях вы найдете сущности слабо и сильносвязанные, т.е. образующие подсистемы. Система из автономных подсистем тоже монолит?
              0
              Простите, не совсем понял Ваш вопрос. Вы хотите чтобы я дал строгое определение монолита и микросервиса? Пожалуй я не готов это сделать, по мне так это очень халиварная тема и мне будет жаль потраченного времени. Но я могу поделиться своим практическим опытом, и продемонстрировать что в моей картине мира есть микросервис, а что монолит.
                0
                Нет, я просто хотел понять, есть в вашей картине мира какие-то другие архитектуры, кроме микросервисной и монолитной. Хотя первая — функциональная, а вторая — компоновочная, т.е. из разных жанров.
                  0
                  Архитектура штука многогранная ) Монолиты и Микросервисы это два основных стрима внутри которых я имел удовольствие реализовывать различные архитектурные паттерны (MVC, listener, Event Sourcing, API GW, ORM и многое другое)
            0
            2. При отказе сервиса User все будет продолжать работать? Really? — why not, все силно зависит от архитектуры решения и способа публикации.
            — На том же K8s можно поднять сервис User в котором гарантировано будет работать несколько подов микросервиса.
            — Сервисы потребители должны быть устойчивы к подобного рода кейсам и не крашиться, а например повторять запросы по затухающей шкале (в gRPC это реализовано из коробки)
            — Если, например, вы используете патерн API Gateway, в этом случае функция аутентификации/авторизации может кэшироваться на API Gateway и в этой части критичность сервиса User снижается
              0
              3. Каким образом обеспечивается целостность и непротиворечивость данных в трех экземплярах БД? — на уровне приложения. Это цена за независимость и распределенность.
                0
                4. В зависимостях между сервисами часть стрелочек (например, комментарии зависят от User) опущена намеренно для «чистоты концепции»? — чистоты концепции
            0
            А ещё я никак не могу понять смысл gRPC. Зачем ещё какая-то прослойка, если можно просто поднять микросервис и по урлу его дёргать? Объясните пожалуйста.
              0
              Я тоже не могу найти объективных причин усложнения архитектуры, объемы передаваемой информации не оправдывают его применение. Ну и раз уж речь зашла о gRPC то не плохо было бы сюда прикрутить что нибудь вроде егеря для трассировки. Чтобы решить те задачи которые в данном случае решает gRPC, достаточно реализовать REST Client для каждого микросервиса в виде пакета.
                0
                Ну если относиться к проекту как к демонстратору технологий, то решения — вполне уместные, разве что вот Mongo немного смущает…
                  0
                  Вроде как все сущности укладываются в рамки «документа», транзакционно зависимых CRUD операций на ум не приходит. Почему бы и не mongo, по сути на его месте может быть любая БД.
              0

              При написании связанных сервисов на GO столкнулся с тем что много кода дублируют друг дружку, особенно части отвечающие за транспорт и бизнесс-область, я правильно понимаю что по этой причине у вас все в одном моно-репозитории?
              Сам думал о моно-репе или модных-новых-классных модулях

                0
                Мое личное мнение, моно-репа это наследие мышления в системе координат монолитых решений. Избыточность кода, это цена за независимость и децентрализованность. Представьте что каждый отдельный микросервис «пилит» отдельная команда, которая ничего не знает о коде других команд. В этом смысле избыточность выглядет уже иначе )
                В примере моно-репа использовалась для упрощения процесса запуска проекта и понимания структуры проекта в целом.
                  0
                  В условиях, когда человеческий ресурс избыточен (избыток программистов-техников при дефиците проектировщиков и аналитиков), вместо горизонтальной организации («системщики» делают платформу для «прикладников») часто используют вертикальную, где каждая команда реализует свои функции на базе собственных велосипедов. С точки зрения управления второй способ выглядит проще, как минимум до того момента, пока не вводится горизонтальная команда «архитектуры», уменьшающая риски одних и тех же повторяющихся ошибок.

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

                Самое читаемое