В этой статье я опишу использование Go kit, набора инструментов и библиотек для создания микросервисов на Go. Эта статья — введение в Go kit. Первая часть в моем блоге, исходный код примеров доступен здесь.


Go все чаще выбирается для разработки современных распределенных систем. Когда вы разрабатываете облачно-ориентированную распределенную систему, вам может потребоваться поддержка различного специфичного функционала в ваших сервисах, такого как: различные транспортные протоколы (пр. пер. HTTP, gRPC, и др.) и форматы кодирования сообщений для них, надежность RPC, логирование, трассировка, метрики и профилирование, прерывание запросов, ограничение количества запросов, интеграция в инфраструктуру и даже описание архитектуры. Go популярный язык благодаря своей простоте и подходам "без магии", поэтому пакеты Go, например, стандартная библиотека, уже подходят для разработки распределенных систем больше, чем использование полноценного фреймворка с множеством "магии под капотом". Лично я [прим. пер. Shiju Varghese] не поддерживаю использование полноценных фреймворков, предпочитаю использовать библиотеки, которые дают больше свободы разработчику. Go kit заполнил пробел в экосистеме Go, дав возможность использовать набор библиотек и пакетов при создании микросервисов, которые в свою очередь позволяют использовать хорошие принципы проектирования отдельных сервисов в распределенных системах.


image


Введение в Go kit


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


Помимо набора библиотек для разработки миркосервисов он предоставляет и поощряет использование хороших принципов проектирования архитектуры ваших сервисов. Go kit помогает придерживаться принципов SOLID, предметно-ориентированного подхода (DDD) и гексагональной архитектуры предложенной Alistair Cockburn или любых других подходов из архитектурных принципов известных как "луковая архитектура" от Jeffrey Palermo и "чистая архитектура" от Robert C. Martin. Хотя Go kit был разработан как набор пакетов для разработки микросервисов, он также подходит и для разработки элегантных монолитов.


Архитектура Go kit


Три главных уровня в архитектуре приложения разработанных с помощью Go kit это:


  • транспортный уровень
  • уровень эндпоинтов
  • уровень сервиса

Транспортный уровень


Когда вы пишите микросервисы для распределенных систем, сервисам в них часто приходится общаться друг с другом используя различные транспортные протоколы, такие как: HTTP или gRPC, или использовать pub/sub системы, например NATS. Транспортный уровень в Go kit привязывается к конкретному транспортному протоколу (далее транспорт). Go kit поддерживает различный транспорт для работы вашего сервиса, такой как: HTTP, gRPC, NATS, AMQP и Thirft (прим. пер. также вы можете разработать свой транспорт под свой протокол). Поэтому сервисы написанные с помощью Go kit часто акцентируют внимание на реализации конкретной бизнес логики, которая ничего не знает о используемом транспорте, вы свободны использовать различные транспорты для одного и того же сервиса. Как пример, один сервис написанный на Go kit может одновременно предоставлять доступ к нему по HTTP и gRPC.


Эндпоинты


Конечная точка или эндпоинт — это фундаментальный "строительный кирпичик" для сервисов и клиентов. В Go kit основной паттерн общения — это RPC. Эндпоинт представляется как отдельный RPC метод. Каждый метод сервиса в Go kit преобразуется в эндпоинт, позволяющий общаться между сервером и клиентом в RCP стиле. Каждый эндпоинт выставляет наружу сервиса метод, используя Транспортный уровень, который в свою очередь использует различные транспортные протоколы, например HTTP или gRPC. Отдельный эндпоинт может выставляться наружу сервиса одновременно с помощью нескольких транспортов (прим. пер. HTTP и gRPC на разных портах).


Сервисы


Бизнес логика реализуется в сервисном слое. Сервисы, написанные с Go kit, проектируются как интерфейсы. Бизнес логика в сервисном слое содержит основное ядро бизнес логики, которая не должна знать ничего о используемых эндпоинтах или конкретном транспортном протоколе, как HTTP или gRPC, или о кодировании или декодировании запросов и ответов различных типов сообщений. Это позволит вам придерживаться чистой архитектуры в сервисах, написанных с помощью Go kit. Каждый метод сервиса преобразуется в эндпоинт с помощью адаптера и выставляется наружу с помощью конкретного транспорта. Благодаря использованию чистой архитектуры, отдельный метод может быть выставлен с помощью нескольких транспортов одновременно.


Примеры


А теперь давайте посмотрим на описанные выше слои на примере простенького приложения.


Бизнес логика в сервисе


Бизнес логика в сервисе проектируется с помощью интерфейсов. Мы рассмотрим на примере заказа в электронной коммерции:


// Service describes the Order service.
type Service interface {
   Create(ctx context.Context, order Order) (string, error)
   GetByID(ctx context.Context, id string) (Order, error)
   ChangeStatus(ctx context.Context, id string, status string) error
}

Интерфейс сервиса Order работает с сущностью предметной области Order:


// Order represents an order
type Order struct {
   ID           string      `json:"id,omitempty"`
   CustomerID   string      `json:"customer_id"`
   Status       string      `json:"status"`
   CreatedOn    int64       `json:"created_on,omitempty"`
   RestaurantId string      `json:"restaurant_id"`
   OrderItems   []OrderItem `json:"order_items,omitempty"`
}

// OrderItem represents items in an order
type OrderItem struct {
   ProductCode string  `json:"product_code"`
   Name        string  `json:"name"`
   UnitPrice   float32 `json:"unit_price"`
   Quantity    int32   `json:"quantity"`
}

// Repository describes the persistence on order model
type Repository interface {
   CreateOrder(ctx context.Context, order Order) error
   GetOrderByID(ctx context.Context, id string) (Order, error)
   ChangeOrderStatus(ctx context.Context, id string, status string) error
}

Здесь мы реализуем интерфейс сервиса Order:


package implementation

import (
   "context"
   "database/sql"
   "time"

   "github.com/go-kit/kit/log"
   "github.com/go-kit/kit/log/level"
   "github.com/gofrs/uuid"

   ordersvc "github.com/shijuvar/gokit-examples/services/order"
)

// service implements the Order Service
type service struct {
   repository ordersvc.Repository
   logger     log.Logger
}

// NewService creates and returns a new Order service instance
func NewService(rep ordersvc.Repository, logger log.Logger) ordersvc.Service {
   return &service{
      repository: rep,
      logger:     logger,
   }
}

// Create makes an order
func (s *service) Create(ctx context.Context, order ordersvc.Order) (string, error) {
   logger := log.With(s.logger, "method", "Create")
   uuid, _ := uuid.NewV4()
   id := uuid.String()
   order.ID = id
   order.Status = "Pending"
   order.CreatedOn = time.Now().Unix()

   if err := s.repository.CreateOrder(ctx, order); err != nil {
      level.Error(logger).Log("err", err)
      return "", ordersvc.ErrCmdRepository
   }
   return id, nil
}

// GetByID returns an order given by id
func (s *service) GetByID(ctx context.Context, id string) (ordersvc.Order, error) {
   logger := log.With(s.logger, "method", "GetByID")
   order, err := s.repository.GetOrderByID(ctx, id)
   if err != nil {
      level.Error(logger).Log("err", err)
      if err == sql.ErrNoRows {
         return order, ordersvc.ErrOrderNotFound
      }
      return order, ordersvc.ErrQueryRepository
   }
   return order, nil
}

// ChangeStatus changes the status of an order
func (s *service) ChangeStatus(ctx context.Context, id string, status string) error {
   logger := log.With(s.logger, "method", "ChangeStatus")
   if err := s.repository.ChangeOrderStatus(ctx, id, status); err != nil {
      level.Error(logger).Log("err", err)
      return ordersvc.ErrCmdRepository
   }
   return nil
}

Запросы и ответы для RPC эндпоинтов


Методы сервиса выставлены наружу как RPC эндпоинты. Так что нам надо определить типы сообщений (прим. пер. DTO — data transfer object) которые будут использоваться для отправки и получения сообщений через RPC эндпоинты. Давайте теперь определим структуры для типов запросов и ответов для RPC эндпоинтов в сервисе Order:


// CreateRequest holds the request parameters for the Create method.
type CreateRequest struct {
   Order order.Order
}

// CreateResponse holds the response values for the Create method.
type CreateResponse struct {
   ID  string `json:"id"`
   Err error `json:"error,omitempty"`
}

// GetByIDRequest holds the request parameters for the GetByID method.
type GetByIDRequest struct {
   ID  string
}

// GetByIDResponse holds the response values for the GetByID method.
type GetByIDResponse struct {
   Order order.Order `json:"order"`
   Err error `json:"error,omitempty"`
}

// ChangeStatusRequest holds the request parameters for the ChangeStatus method.
type ChangeStatusRequest struct {
   ID  string `json:"id"`
   Status string `json:"status"`
}

// ChangeStatusResponse holds the response values for the ChangeStatus method.
type ChangeStatusResponse struct {
   Err error `json:"error,omitempty"`
}

Эндпоинты Go kit для методов сервиса как RPC эндпоинты


Ядро нашей бизнес логики отделено от остального кода и вынесено в сервисный слой, который выставлен наружу с помощью RPC эндпоинтов, которые используются абстракцию Go kit называемой Endpoint.


Вот так выглядит эндпоинт из Go kit:


type Endpoint func(ctx context.Context, request interface{}) (response interface{}, err error)

Как мы говорили выше, эндпоинт представляет отдельный RPC метод. Каждый метод сервиса преобразовывается в endpoint.Endpoint с помощью адаптеров. Давайте сделаем эндпоинты Go kit для методов сервиса Order:


import (
   "context"

   "github.com/go-kit/kit/endpoint"

   "github.com/shijuvar/gokit-examples/services/order"
)

// Endpoints holds all Go kit endpoints for the Order service.
type Endpoints struct {
   Create       endpoint.Endpoint
   GetByID      endpoint.Endpoint
   ChangeStatus endpoint.Endpoint
}

// MakeEndpoints initializes all Go kit endpoints for the Order service.
func MakeEndpoints(s order.Service) Endpoints {
   return Endpoints{
      Create:       makeCreateEndpoint(s),
      GetByID:      makeGetByIDEndpoint(s),
      ChangeStatus: makeChangeStatusEndpoint(s),
   }
}

func makeCreateEndpoint(s order.Service) endpoint.Endpoint {
   return func(ctx context.Context, request interface{}) (interface{}, error) {
      req := request.(CreateRequest)
      id, err := s.Create(ctx, req.Order)
      return CreateResponse{ID: id, Err: err}, nil
   }
}

func makeGetByIDEndpoint(s order.Service) endpoint.Endpoint {
   return func(ctx context.Context, request interface{}) (interface{}, error) {
      req := request.(GetByIDRequest)
      orderRes, err := s.GetByID(ctx, req.ID)
      return GetByIDResponse{Order: orderRes, Err: err}, nil
   }
}

func makeChangeStatusEndpoint(s order.Service) endpoint.Endpoint {
   return func(ctx context.Context, request interface{}) (interface{}, error) {
      req := request.(ChangeStatusRequest)
      err := s.ChangeStatus(ctx, req.ID, req.Status)
      return ChangeStatusResponse{Err: err}, nil
   }
}

Адаптер эндпоинта принимает на вход интерфейс как параметр и преобразует его в абстракцию Go kit endpoint.Enpoint делая каждый отдельный метод сервиса эндпоинтом. Эта функция адаптер делает сравнение и преобразования типов для запросов, вызывает метод сервиса и возвращает сообщение с ответом.


func makeCreateEndpoint(s order.Service) endpoint.Endpoint {
   return func(ctx context.Context, request interface{}) (interface{}, error) {
      req := request.(CreateRequest)
      id, err := s.Create(ctx, req.Order)
      return CreateResponse{ID: id, Err: err}, nil
   }
}

Выставление сервиса наружу с помощью HTTP


Мы создали наш сервис и описали RPC эндпоинты для выставления наружу методов нашего сервиса. Теперь нам надо опубликовать наш сервис наружу, чтобы другие сервисы могли вызывать RCP эндпоинты. Для выставления наружу нашего сервиса, нам нужно определиться с транспортным протоколом для нашего сервиса, по которому он будет принимать запросы. Go kit поддерживает различные транспорты, например HTTP, gRPC, NATS, AMQP и Thrift из коробки.


Для примера, мы используем HTTP транспорт для нашего сервиса. Go kit пакет github.com/go-kit/kit/transport/http предоставляет возможность обслуживать HTTP запросы. И функция NewServer из пакета transport/http создаст новый http сервер, который будет реализовывать http.Handler и оборачивает предоставленные эндпоинты.


Ниже приведен код который преобразовывает эндпоинты Go kit к HTTP транспорту, который обслуживает HTTP запросы:


package http

import (
   "context"
   "encoding/json"
   "errors"
   "github.com/shijuvar/gokit-examples/services/order"
   "net/http"

   "github.com/go-kit/kit/log"
   kithttp "github.com/go-kit/kit/transport/http"
   "github.com/gorilla/mux"

   "github.com/shijuvar/gokit-examples/services/order/transport"
)

var (
   ErrBadRouting = errors.New("bad routing")
)

// NewService wires Go kit endpoints to the HTTP transport.
func NewService(
   svcEndpoints transport.Endpoints, logger log.Logger,
) http.Handler {
   // set-up router and initialize http endpoints
   r := mux.NewRouter()
   options := []kithttp.ServerOption{
      kithttp.ServerErrorLogger(logger),
      kithttp.ServerErrorEncoder(encodeError),
   }
   // HTTP Post - /orders
   r.Methods("POST").Path("/orders").Handler(kithttp.NewServer(
      svcEndpoints.Create,
      decodeCreateRequest,
      encodeResponse,
      options...,
   ))

   // HTTP Post - /orders/{id}
   r.Methods("GET").Path("/orders/{id}").Handler(kithttp.NewServer(
      svcEndpoints.GetByID,
      decodeGetByIDRequest,
      encodeResponse,
      options...,
   ))

   // HTTP Post - /orders/status
   r.Methods("POST").Path("/orders/status").Handler(kithttp.NewServer(
      svcEndpoints.ChangeStatus,
      decodeChangeStausRequest,
      encodeResponse,
      options...,
   ))
   return r
}

func decodeCreateRequest(_ context.Context, r *http.Request) (request interface{}, err error) {
   var req transport.CreateRequest
   if e := json.NewDecoder(r.Body).Decode(&req.Order); e != nil {
      return nil, e
   }
   return req, nil
}

func decodeGetByIDRequest(_ context.Context, r *http.Request) (request interface{}, err error) {
   vars := mux.Vars(r)
   id, ok := vars["id"]
   if !ok {
      return nil, ErrBadRouting
   }
   return transport.GetByIDRequest{ID: id}, nil
}

func decodeChangeStausRequest(_ context.Context, r *http.Request) (request interface{}, err error) {
   var req transport.ChangeStatusRequest
   if e := json.NewDecoder(r.Body).Decode(&req); e != nil {
      return nil, e
   }
   return req, nil
}

func encodeResponse(ctx context.Context, w http.ResponseWriter, response interface{}) error {
   if e, ok := response.(errorer); ok && e.error() != nil {
      // Not a Go kit transport error, but a business-logic error.
      // Provide those as HTTP errors.
      encodeError(ctx, e.error(), w)
      return nil
   }
   w.Header().Set("Content-Type", "application/json; charset=utf-8")
   return json.NewEncoder(w).Encode(response)
}

Мы создаем http.Handler с помощью функции NewServer из пакета transport/http, который предоставляет нам эндпоинты и функции декодирования запросов (возвращает значение type DecodeRequestFunc func) и кодирования ответов (например type EncodeReponseFunc func).


Ниже приведены примеры DecodeRequestFunc и EncodeResponseFunc:


// For decoding request 
type DecodeRequestFunc func(context.Context, *http.Request) (request interface{}, err error)

// For encoding response
type EncodeResponseFunc func(context.Context, http.ResponseWriter, interface{}) error

Запуск HTTP сервера


И наконец мы можем запустить наш HTTP сервер для обработки запросов. Функция NewService которая приведена выше, реализует интерфейс http.Handler что позволяет нам запустить её как HTTP сервер:


func main() {
   var (
      httpAddr = flag.String("http.addr", ":8080", "HTTP listen address")
   )
   flag.Parse()

   var logger log.Logger
   {
      logger = log.NewLogfmtLogger(os.Stderr)
      logger = log.NewSyncLogger(logger)
      logger = level.NewFilter(logger, level.AllowDebug())
      logger = log.With(logger,
         "svc", "order",
         "ts", log.DefaultTimestampUTC,
         "caller", log.DefaultCaller,
      )
   }

   level.Info(logger).Log("msg", "service started")
   defer level.Info(logger).Log("msg", "service ended")

   var db *sql.DB
   {
      var err error
      // Connect to the "ordersdb" database
      db, err = sql.Open("postgres", 
         "postgresql://shijuvar@localhost:26257/ordersdb?sslmode=disable")
      if err != nil {
         level.Error(logger).Log("exit", err)
         os.Exit(-1)
      }
   }

   // Create Order Service
   var svc order.Service
   {
      repository, err := cockroachdb.New(db, logger)
      if err != nil {
         level.Error(logger).Log("exit", err)
         os.Exit(-1)
      }
      svc = ordersvc.NewService(repository, logger)
   }

   var h http.Handler
   {
      endpoints := transport.MakeEndpoints(svc)
      h = httptransport.NewService(endpoints, logger)
   }

   errs := make(chan error)
   go func() {
      c := make(chan os.Signal)
      signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
      errs <- fmt.Errorf("%s", <-c)
   }()

   go func() {
      level.Info(logger).Log("transport", "HTTP", "addr", *httpAddr)
      server := &http.Server{
         Addr:    *httpAddr,
         Handler: h,
      }
      errs <- server.ListenAndServe()
   }()

   level.Error(logger).Log("exit", <-errs)
}

Теперь наш сервис запущен и использует HTTP протокол на транспортном уровне. Этот же сервис может быть запущен с использованием другого транспорта, Например, сервис может быть выставлен наружу с помощью gRPC или Apache Thrift.


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


Исходный код


Весь исходный код примеров можно посмотреть на GitHub здесь


Middlewares в Go kit


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


Ниже показана картинка с сайта Go kit, которая изображена как типичная "луковая архитектура" с помощью Middlewares в Go kit:
image


Остерегайтесь синдрома Spring Boot Mikroservices


Также как Go kit, Spring Boot — это набор инструментов для создания микросервисов в мире Java. Но, в отличии от Go kit, Spring Boot это вполне зрелый фреймворк. Также множество Java разработчиков используют Spring Boot для создания миркосервисов с помощью Java стэка с положительными отзывами от использования, некоторые из них верят что микросервисы — это только про использование Spring Boot. Я вижу много команд разработки кто неверно истолковывает использование микросервисов, что они могут разрабатываться только с помощью Spring Boot и OSS Netflix и не воспринимают микросервисы как шаблон при разработке распределенных систем.


Так что имейте ввиду, что с помощью набора инструментов, такого как Go kit или какого-то фреймворка, вы направляете свою разработку в сторону микросеврисов, как шаблона проектирования. Хотя микросервисы решают множество проблем с масштабированием и команд и систем, но это также создает множество проблем, потому что данные в системах на основе микросервисов разбросаны по различным базам данных, которые иногда создают множество проблем при создании транзакционности или запросов данных. Это все зависит от проблемы предметной области и контекста вашей системы. Классно то, что Go kit разработанный как инструмент для создания микросервисов, также подходил для создания элегантных монолитов, которые создаются с хорошим дизайном архитектуры ваших систем.


И некоторый функционал Go kit, такие как прерывание и ограничение запросов также доступны в платформах service mesh, например Istio. Так что если вы используете что-то типа Istio, для запуска ваших микросеврисов, у вас может не быть необходимости в некоторых вещах из Go kit, но не у всех будет хватать ширины канала для использования service mesh для создания межсервисного общения так как это добавляет еще один уровень и дополнительную сложность.


PS


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


UPD
Также это первая статья в разделе переводов и буду признателен за любую обратную связь по переводу.

Комментарии (7)


  1. hippoage
    19.11.2018 10:50

    Спасибо, интересная статья.

    Код несколько упрощен (может это и out of scope для gokit, но как на практике без этого в реальном проекте непонятно):
    1. обычно в БД содержатся еще поля, которые не хочется выдавать наружу (по крайней мере всегда). Поэтому интересно посмотреть GetByIDResponse, который содержит только часть ордера. Hint: в Java для этого есть mapstruct.org, интересно как это реализуется в Go / Gokit.
    2. хотелось бы так же посмотреть пример валидации данных запроса и подготовка модели для сохранения (только часть полей модели БД может задаваться в моделе запроса), т.к. это опять же обычно обязательная вещь.

    В репозитарии, наверное, указан некорректный метод ChangeOrderStatus: обычно репозитарий валидирует только возможность сохранения модели (например, что строка по длине помещается в ячейку БД), но не контролирует корректность переходов между состояниями — это функция сервисов. Поэтому, обычно, просто метод сохранения, без ограничения на только изменения статуса. Хотя и так можно, особенно, если реализация такого метода генерируется автоматически.

    Из интересного можно отметить, что в Spring обычно нет отдельного слоя Endpoint: там связка протокола и сервиса в одном классе. Так же довольно многословно получается в Endpoint и Transport: цена за отсутствие «магии».


    1. sah4ez32 Автор
      19.11.2018 11:38

      Ну это перевод статьи, поэтому на некоторые вопросы я наверное не смогу ответить.
      То что есть плата в виде многословности с Endpoint и Transport за отсутствие "магии" — здесь я с вами полностью согласен. Но эту многословность можно решить, например, с помощью кодогенерации.


      1. hippoage
        19.11.2018 12:48

        Понятно, что это перевод. Статью много кто прочитает. Может кто-то знает reference app для микросервисов на Go, чтобы можно было посмотреть на более-менее все реальные аспекты микросервисного приложения.

        Годогенерация = магия, но часто это не плохо.


  1. TonyLorencio
    19.11.2018 11:02

    Остерегайтесь синдрома Spring Boot Mikroservices

    Но завязывание на любой фреймворк, который вносит достаточно много своего — это почти тот же самый синдром, только в профиль


  1. powerman
    19.11.2018 15:12

    Я Go kit пока не использовал — хотелось его пощупать, но случай пока не представился. Возможно, некоторые моменты из статьи не являются недостатками Go kit, а являются особенностями использования Go kit автором.


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

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

    Я очень люблю RPC, но помимо HTTP/1, REST и RPC есть немало других протоколов, которые не вписываются в простую схему запрос/ответ (я вот даже не уверен, что HTTP/2 вписывается, напр. тот же server push). В таких случаях бизнес логика нередко должна знать специфику транспорта, чтобы работать достаточно эффективно. Например, если речь о потоке событий, то бизнес логика зачастую должна быть написана в стиле отдельной горутины, которая отправляет события в канал, и которой можно управлять через сигналы — если обернуть эту бизнес логику в набор RPC методов ради формального требования "бизнес логика не знает о транспорте" то код станет заметно сложнее, а работать будет заметно хуже. Доступные сигналы, которые нужны для управления бизнес логикой, тоже нередко определяются транспортным протоколом. В общем, отрывать бизнес логику от транспорта это хорошая идея, но не везде применимая.


    Как пример, один сервис написанный на Go kit может одновременно предоставлять доступ к нему по HTTP и gRPC.

    Это здорово, но… на практике такая необходимость практически не встречается. Я к тому, что это явно не та фича, которой стоит рекламировать инструмент.


    type Order struct

    Хм. Если бизнес логика ничего не знает о транспортном уровне и типе кодирования данных при передаче по сети — зачем поля структуры описывающей данные домена размечены тегами для кодирования в json?


    func NewService(rep ordersvc.Repository, logger log.Logger) ordersvc.Service {

    Сохранение логгера внутри сервиса обладает серьёзным недостатком, который наглядно демонстрирует следующая строка:


    logger := log.With(s.logger, "method", "Create")

    Логирование, как правило, должно быть контекстно-зависимо от обрабатываемого запроса: нужно выводить с какого IP пришёл запрос, от имени какого юзера/с какими правами выполняется, ну и да, вызванный метод нужно тоже выводить, причём очень хочется чтобы за это отвечала одна строка кода в слое работы с RPC, и не надо было добавлять лишнюю строку в каждый метод бизнес логики (да ещё и вписывать в неё имя метода вручную — это гарантирует наличие опечаток, особенно учитывая что такие строки будут добавляться исключительно копипастом).


    Всё это означает, что логгер необходимо настраивать в соответствии с текущим запросом в слое RPC, и передавать в методы бизнес логики параметром (в крайнем случае — внутри параметра ctx).


    type CreateRequest struct {
    Order order.Order
    }

    Волшебно. Т.е. если у нас меняются данные бизнес логики то это автоматически изменит данные бегающие по RPC. Ну да, совместимость API это для слабаков.


    return CreateResponse{ID: id, Err: err}, nil

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


    Резюмируя: каждый раз, когда я читаю что-то по Go kit, у меня возникает впечатление, что он реализует гибкость немного не в том месте, где она реально нужна. Может дело не в Go kit, а в том как им пользуются, но вообще тенденция немного настораживает.


    1. sah4ez32 Автор
      19.11.2018 15:40

      Весьма интересные замечания и предложения.


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

      С этим пунктом полностью согласен, тут не будет "серебряной пули".


      нужно выводить с какого IP пришёл запрос, от имени какого юзера/с какими правами выполняется, ну и да, вызванный метод нужно тоже выводить, причём очень хочется чтобы за это отвечала одна строка кода в слое работы с RPC,

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


      Волшебно. Т.е. если у нас меняются данные бизнес логики то это автоматически изменит данные бегающие по RPC. Ну да, совместимость API это для слабаков.

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


      Резюмируя: каждый раз, когда я читаю что-то по Go kit, у меня возникает впечатление, что он реализует гибкость немного не в том месте, где она реально нужна. Может дело не в Go kit, а в том как им пользуются, но вообще тенденция немного настораживает.

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


      PS
      powerman если у вас есть опыт разработки микросервисов с помощью какого либо toolkit или framework, а может быть и без, могли бы им поделиться?


      1. powerman
        19.11.2018 16:16
        +1

        решаться с помощью сквозного ID для запроса

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


        Кроме того, следующие по цепочке сервисы не всегда выполняются с правами и в контексте оригинального запроса, плюс по цепочке накапливаются и другие данные — таймауты, стек вызовов, etc. Так что одним ID обойтись обычно не удаётся.


        проще поддерживать активными 2 экземпляра сервисов с разным API и делать роутинг запросов на них.

        И чем это поможет если второй в цепочке сервис постоянно ломает своё API (и, соответственно, ломает этим первый сервис)? Нет, эта проблема решается намного проще, хотя и требует написания некоторого количества относительно тупого кода: структура Order которую возвращает API не должна быть той же структурой Order, с которой работает бизнес логика. Это значит, что при передаче данных между слоями API и бизнес логики требуется постоянно конвертировать данные между этими двумя структурами в обе стороны. Такой код раздражает (кодогенераторы немного помогают), но зато мы имеем стабильное API и одновременно возможность почти как угодно менять данные бизнес логики.


        Я свои сервисы пишу обычно на стандартной библиотеке плюс jsonrpc2 для RPC и structlog для логирования. Middleware обычно нужны при использовании HTTP, и они элементарно пишутся на стандартной библиотеке, либо в виде функций func SomeMiddleware(next http.Handler) http.HandlerFunc либо в виде генератора такий функций type Middleware func(http.Handler) http.Handler; func makeSomeMiddleware(options) Middleware.