
Салют, Хабр!
Go Kit предоставляет стандартизированный способ создания сервисов, с ее помощью можно легко реализовать совместимость сервисов. С его помощью можно легко интегрировать различные транспортные протоколы, такие как HTTP, RPC, gRPC, и многое другое, а также реализовывать общие паттерны: логирование, метрики, трассировка. В общем, Go Kit хорошо подходит для разработки микросервисов на go.
Мотивацию создания этой либы разработчики описали так:
Go стал языком сервера, но он по-прежнему недостаточно представлен в так называемых «современных корпоративных» компаниях, таких как Facebook, Twitter, Netflix и SoundCloud. Многие из этих организаций обратились к стекам на основе JVM для создания своей бизнес-логики, во многом благодаря библиотекам и экосистемам, которые напрямую поддерживают их микросервисные архитектуры.
Чтобы достичь следующего уровня успеха, Go нужно нечто большее, чем простые примитивы и идиомы. Ему нужен всеобъемлющий набор инструментов для последовательного распределенного программирования в целом. Go Kit — это набор пакетов и лучших практик, которые обеспечивают комплексный, надежный и надежный способ создания микросервисов для организаций любого размера.
Также стоит упомянуть, что разработчики не ставят цель реализовать следующие функции:
-
Поддержка шаблонов обмена сообщениями, отличных от RPC (на данный момент), например MPI, pub/sub, CQRS и т. д.
-
Повторная реализация функциональности, которую можно обеспечить путем адаптации существующего программного обеспечения.
Установка Go Kit:
go get -u github.com/go-kit/kit
Go Kit требует Go версии 1.13 или выше.
Компоненты Go Kit
Сервисы — это база микросервисной архитектуры. Каждый сервис представляет собой отдельный компонент, который выполняет определенную функцию или набор функций. В Go Kit сервисы разрабатываются как наборы интерфейсов и реализаций, которые разделяют бизнес-логику от остальной части системы.
Транспортный слой является мостом между вашими сервисами и внешним миром. Он отвечает за прием запросов от клиентов, их обработку и передачу данных обратно клиентам. Go Kit предлагает систему транспортных слоев, поддерживающих множество протоколов, включая HTTP, gRPC, Thrift и т.д.
Endpoints представляют собой конечные точки, к которым обращаются клиенты для выполнения определенных операций. Endpoints отвечают за обработку входящих запросов, выполнение соответствующих вызовов сервисов и возврат результатов обратно клиентам.
Основные функции
Сервисный слой
Сервисный слой начинается с определения интерфейса. Интерфейс сервиса описывает операции или действия, которые можно выполнить в рамках данного сервиса. Это абстракция, которая скрывает детали реализации, позволяя фокусироваться на том, что делает сервис, а не как он это делает.
Допустим нужен микросервис для управления пользователями. На уровне интерфейса это может выглядеть так:
package userservice // userService определяет интерфейс для нашего сервиса управления пользователями. type UserService interface { CreateUser(name string, email string) (User, error) GetUser(id string) (User, error) }
UserService предоставляет две операции: CreateUser для создания нового пользователя и GetUser для получения информации о пользователе по его идентификатору. Возвращаемые значения и ошибки указывают на результат выполнения каждой операции.
После определения интерфейса следующим шагом будет реализация этого интерфейса. Реализация — это конкретный код, который выполняет логику, описанную интерфейсом:
package userservice import "errors" // userService представляет реализацию нашего UserService. type userService struct { // здесь различные зависимости, ссылки на бд и т.п } // NewUserService создает и возвращает новый экземпляр userService. func NewUserService() UserService { return &userService{} } // CreateUser реализует логику создания пользователя. func (s *userService) CreateUser(name string, email string) (User, error) { // логика создания нового пользователя. // проверка валидности данных и запись пользователя в базу данных return User{Name: name, Email: email}, nil } // GetUser реализует логику получения пользователя по ID. func (s *userService) GetUser(id string) (User, error) { // логика поиска пользователя по его ID в базе данных. // если пользователь не найден, возвращается ошибка. return User{}, errors.New("user not found") } // ser представляет модель пользователя в нашей системе. type User struct { ID string Name string Email string }
userService является приватной структурой, которая реализует интерфейс UserService. ФункцияNewUserServiceнужно чтобы скрыть детали создания экземпляра сервиса и возвращаем интерфейс, а не конкретный тип.
Endpoint слой
Допустим, у нас есть сервис UserService с методом GetUser, который мы хотим экспонировать через HTTP. Сначала определим endpoint:
import ( "context" "github.com/go-kit/kit/endpoint" ) // GetUserRequest определяет структуру запроса к endpoint. type GetUserRequest struct { UserID string } // GetUserResponse определяет структуру ответа от endpoint. type GetUserResponse struct { User User `json:"user,omitempty"` Err string `json:"err,omitempty"` // ошибки не сериализуются по JSON напрямую. } // MakeGetUserEndpoint создает endpoint для метода GetUser сервиса UserService. func MakeGetUserEndpoint(svc UserService) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { req := request.(GetUserRequest) user, err := svc.GetUser(req.UserID) if err != nil { return GetUserResponse{User: user, Err: err.Error()}, nil } return GetUserResponse{User: user, Err: ""}, nil } }
Крейтнули endpoint, который принимает запрос GetUserRequest, извлекает UserID и использует его для вызова метода GetUser нашего сервиса UserService. Ответ от сервиса затем оборачивается в GetUserResponse.
Middleware позволяет добавлять перехватывающую логику в обработку запросов, например, для логирования, мониторинга, проверки аутентификации и т.д., не изменяя логику самих endpoints.
Проще говоря, middleware представляет собой функцию, которая принимает endpoint и возвращает другой endpoint:
import ( "context" "github.com/go-kit/kit/endpoint" "github.com/go-kit/kit/log" ) // LoggingMiddleware возвращает Middleware, которое логирует запросы к сервису. func LoggingMiddleware(logger log.Logger) endpoint.Middleware { return func(next endpoint.Endpoint) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (response interface{}, err error) { logger.Log("msg", "calling endpoint") response, err = next(ctx, request) logger.Log("msg", "called endpoint") return } } }
Здесь middleware логирует сообщения до и после вызова оригинального endpoint. Можно применить это middleware к любому endpoint сервиса, передав его через MakeGetUserEndpoint, например, или к любому другому endpoint.
Endpoints можно группировать. Группировка endpoints достигается через создание набора endpoints, который представляет собой агрегацию всех endpoints, связанных с определенным сервисом.
Определим несколько базовых endpoints для нашего примера сервиса, который будем группировать. К примеру есть ProfileService, предоставляющий функционал для управления профилями пользователей:
type ProfileService interface { CreateProfile(ctx context.Context, profile Profile) (string, error) GetProfile(ctx context.Context, id string) (Profile, error) }
Для каждого метода интерфейса сервиса определим соответствующий endpoint.
func makeCreateProfileEndpoint(svc ProfileService) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { req := request.(createProfileRequest) id, err := svc.CreateProfile(ctx, req.Profile) return createProfileResponse{ID: id, Err: err}, nil } } func makeGetProfileEndpoint(svc ProfileService) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { req := request.(getProfileRequest) profile, err := svc.GetProfile(ctx, req.ID) return getProfileResponse{Profile: profile, Err: err}, nil } }
Теперь есть несколько endpoints, их можно группировать вместе. Это обычно делается путем создания структуры, которая содержит все эти endpoints как поля:
type Endpoints struct { CreateProfile endpoint.Endpoint GetProfile endpoint.Endpoint } func MakeEndpoints(svc ProfileService) Endpoints { return Endpoints{ CreateProfile: makeCreateProfileEndpoint(svc), GetProfile: makeGetProfileEndpoint(svc), } }
Структура Endpoints теперь агрегирует все endpoints, связанные с ProfileService, вроде — удобно.
После группировки endpoints их можно использовать в транспортном слое (про транспортные слое чуть ниже). Например, при создании HTTP сервера, можно ссылаться на эти endpoints напрямую из структуры Endpoints:
func NewHTTPHandler(endpoints Endpoints) http.Handler { r := mux.NewRouter() r.Methods("POST").Path("/profiles").Handler(httptransport.NewServer( endpoints.CreateProfile, decodeHTTPCreateProfileRequest, encodeHTTPGenericResponse, )) r.Methods("GET").Path("/profiles/{id}").Handler(httptransport.NewServer( endpoints.GetProfile, decodeHTTPGetProfileRequest, encodeHTTPGenericResponse, )) return r }
Транспортный слой
Для создания HTTP сервера определяются транспортные функции, которые преобразуют HTTP запросы в вызовы вашего сервиса и ответы сервиса обратно в HTTP ответы:
package main import ( "context" "encoding/json" "net/http" "github.com/go-kit/kit/endpoint" "github.com/go-kit/kit/transport/http" ) // сервис type MyService interface { Add(a, b int) int } type myService struct{} func (myService) Add(a, b int) int { return a + b } // endpoint func makeAddEndpoint(svc MyService) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { req := request.(addRequest) v := svc.Add(req.A, req.B) return addResponse{V: v}, nil } } type addRequest struct { A int `json:"a"` B int `json:"b"` } type addResponse struct { V int `json:"v"` } // decode и encode функции func decodeAddRequest(_ context.Context, r *http.Request) (interface{}, error) { var request addRequest if err := json.NewDecoder(r.Body).Decode(&request); err != nil { return nil, err } return request, nil } func encodeResponse(_ context.Context, w http.ResponseWriter, response interface{}) error { return json.NewEncoder(w).Encode(response) } func main() { svc := myService{} addEndpoint := makeAddEndpoint(svc) addHandler := http.NewServer(addEndpoint, decodeAddRequest, encodeResponse) http.Handle("/add", addHandler) http.ListenAndServe(":8080", nil) }
Создаем простой сервис MyService с методом Add, который складывает два числа. Затем создается endpoint, который обрабатывает логику преобразования запросов и ответов. Для обработки HTTP запросов и ответов используем http.NewServer.
Для создания HTTP клиента используется аналогичная абстракция:
package main import ( "context" "encoding/json" "net/http" "github.com/go-kit/kit/endpoint" "github.com/go-kit/kit/transport/http" ) func makeHTTPClient(baseURL string) MyService { var addEndpoint endpoint.Endpoint addEndpoint = http.NewClient( "POST", mustParseURL(baseURL+"/add"), encodeHTTPRequest, decodeHTTPResponse, ).Endpoint() return Endpoints{AddEndpoint: addEndpoint} } func encodeHTTPRequest(_ context.Context, r *http.Request, request interface{}) error { // код для кодирования запроса } func decodeHTTPResponse(_ context.Context, resp *http.Response) (interface{}, error) { var response addResponse if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { return nil, err } return response, nil }
Go Kit предлагает встроенную поддержку для gRPC:
Определим интерфейс сервиса и структуры данных, которые он использует, в .proto файле. fe:
syntax = "proto3"; package example; service StringService { rpc Uppercase (UppercaseRequest) returns (UppercaseResponse) {} rpc Count (CountRequest) returns (CountResponse) {} } message UppercaseRequest { string str = 1; } message UppercaseResponse { string str = 1; string err = 2; } message CountRequest { string str = 1; } message CountResponse { int32 count = 1; }
Используя protoc компилятор с плагином для Go, можно сгенерировать Go код, который будет использоваться для создания gRPC сервера:
protoc --go_out=. --go-grpc_out=. path/to/your_service.proto
Далее реализуем сервис в Go, используя интерфейсы, сгенерированные из .proto файла:
package main import ( "context" "strings" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" pb "path/to/your_service_package" ) type stringService struct { pb.UnimplementedStringServiceServer } func (s *stringService) Uppercase(ctx context.Context, req *pb.UppercaseRequest) (*pb.UppercaseResponse, error) { if req.Str == "" { return nil, status.Errorf(codes.InvalidArgument, "Empty string") } return &pb.UppercaseResponse{Str: strings.ToUpper(req.Str)}, nil } func (s *stringService) Count(ctx context.Context, req *pb.CountRequest) (*pb.CountResponse, error) { return &pb.CountResponse{Count: int32(len(req.Str))}, nil }
run:
package main import ( "log" "net" "google.golang.org/grpc" pb "path/to/your_service_package" ) func main() { lis, err := net.Listen("tcp", ":50051") if err != nil { log.Fatalf("failed to listen: %v", err) } var opts []grpc.ServerOption grpcServer := grpc.NewServer(opts...) pb.RegisterStringServiceServer(grpcServer, newStringService()) grpcServer.Serve(lis) }
Здесь так же как и в эндпоинтах есть Middleware, который в транспортном слое позволяет встраивать дополнительную логику обрабтки для входящих и исходящих запросов/ответов:
package main import ( "context" "fmt" "github.com/go-kit/kit/endpoint" "github.com/go-kit/log" ) // LoggingMiddleware возвращает Middleware, которое логирует детали запроса func LoggingMiddleware(logger log.Logger) endpoint.Middleware { return func(next endpoint.Endpoint) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (response interface{}, err error) { logger.Log("msg", "calling endpoint") defer func() { logger.Log("msg", "called endpoint", "err", err) }() return next(ctx, request) } } }
Прочие возможности
В го кит есть абстракция логирования, которая позволяет легко интегрировать любую систему логирования с вашими сервисами. Например, мой любимыйlog.Logger, который отличается своей минималистичностью:
import ( "github.com/go-kit/log" "github.com/sirupsen/logrus" ) type logrusLogger struct { *logrus.Logger } func (l logrusLogger) Log(keyvals ...interface{}) error { // здесь может быть реализация адаптация аргументов keyvals // для logrus или другой логики адаптации. l.Logger.WithFields(logrus.Fields{"keyvals": keyvals}).Info() return nil } // экземпляр Logger Go Kit, используя logrus logger := logrusLogger{logrus.New()}
Можно интегрироваться с системами метрик, к примеру с Prometheus:
import ( "github.com/go-kit/kit/metrics/prometheus" stdprometheus "github.com/prometheus/client_golang/prometheus" ) var requestCount = prometheus.NewCounterFrom(stdprometheus.CounterOpts{ Namespace: "my_namespace", Subsystem: "my_subsystem", Name: "request_count", Help: "Number of requests received.", }, []string{"method"})
Можно интегрироваться с системами трассировки, к примеру Jaeger:
import ( "github.com/go-kit/kit/tracing/opentracing" "github.com/opentracing/opentracing-go" "github.com/uber/jaeger-client-go/config" ) // сеттинги Jaeger cfg, _ := config.FromEnv() tracer, _, _ := cfg.NewTracer() // трассировка в endpoint tracedEndpoint := opentracing.TraceServer(tracer, "my_endpoint")(myEndpoint)
В примерах ранее уже реализовывали обработку ошибок, но думаю, в этом разделе стоит эту функцию включить, к примеру обработку польз.ошибки:
import ( "errors" "net/http" "github.com/go-kit/kit/transport/http" ) var ErrInvalidArgument = errors.New("invalid argument") // прнбразование ошибки в HTTP статус errorEncoder := func(ctx context.Context, err error, w http.ResponseWriter) { code := http.StatusInternalServerError if err == ErrInvalidArgument { code = http.StatusBadRequest } w.WriteHeader(code) json.NewEncoder(w).Encode(map[string]interface{}{ "error": err.Error(), }) }
Таким образом с go kit можно решить типичные проблемы в распределенных системах и архитектуре приложений. Go Kit на гитхабе, сайт Go Kit
Статья подготовлена в преддверии старта курса «Microservice Architecture» от OTUS.
ссылка на оригинал статьи https://habr.com/ru/articles/793888/
Добавить комментарий