Микросервисы на Go с помощью Go kit: Введение

в 16:58, , рубрики: Go, gokit.io, golang.org, medium.com, микросервисы

В этой статье я опишу использование 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 ki, но он также предоставляет больше функционала для создания систем прозрачных, надежных паттернов, обнаружения сервисов, балансировки нагркузки и т.д. Мы обсудим эти и другие вещи в 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
Также это первая статья в разделе переводов и буду признателен за любую обратную связь по переводу.

Автор: Александр Козленков

Источник

Поделиться

* - обязательные к заполнению поля