Хабрахабр

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

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

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

image

Введение в Go kit

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

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

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

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

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

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

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

Эндпоинты

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

Сервисы

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

Примеры

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

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

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

// 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
} // 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)

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

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 }
}

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

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

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

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

Ниже приведен код который преобразовывает эндпоинты 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)
}

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

Ниже приведены примеры 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 сервера

Функция NewService которая приведена выше, реализует интерфейс http. И наконец мы можем запустить наш 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)
}

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

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

Исходный код

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

Middlewares в Go kit

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

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

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

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

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

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

PS

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

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть