Microsserviços on Go com o kit Go: Introdução

Neste artigo, descreverei o uso do kit Go, um conjunto de ferramentas e bibliotecas para criar microsserviços no Go. Este artigo é uma introdução ao kit Go. A primeira parte do meu blog, o código fonte dos exemplos, está disponível aqui .


Go é cada vez mais escolhido para o desenvolvimento de sistemas distribuídos modernos. Ao desenvolver um sistema distribuído baseado em nuvem, pode ser necessário oferecer suporte a várias funcionalidades específicas em seus serviços, como: vários protocolos de transporte ( etc. trad. HTTP, gRPC, etc. ) e formatos de codificação de mensagens para eles, confiabilidade de RPC, registro em log , rastreamento, métricas e criação de perfil, interrompendo solicitações, limitando o número de solicitações, integração à infraestrutura e até descrevendo a arquitetura. O Go é uma linguagem popular devido à sua simplicidade e abordagens “sem mágica”, portanto, os pacotes Go, por exemplo, uma biblioteca padrão, já são mais adequados para o desenvolvimento de sistemas distribuídos do que o uso de uma estrutura completa com muita “mágica escondida”. Pessoalmente, eu [ aprox. trans. Shiju Varghese ] Eu não apoio o uso de estruturas completas, eu prefiro usar bibliotecas que dão mais liberdade ao desenvolvedor. O kit Go preencheu uma lacuna no ecossistema Go, possibilitando o uso de um conjunto de bibliotecas e pacotes ao criar microsserviços, o que, por sua vez, permite o uso de bons princípios para o design de serviços individuais em sistemas distribuídos.


imagem


Introdução ao kit Go


O kit Go é um conjunto de pacotes Go que facilitam a criação de microsserviços confiáveis ​​e compatíveis. O kit Go fornece bibliotecas para implementar vários componentes de uma arquitetura de aplicativo transparente e confiável, usando camadas como: registro, métricas, rastreamento, limitação e interrupção de solicitações necessárias para executar microsserviços no produto. O kit Go é bom porque possui ferramentas bem implementadas para interagir com várias infra-estruturas, formatos de codificação de mensagens e várias camadas de transporte.


Além do conjunto de bibliotecas para o desenvolvimento de serviços mundiais, ele fornece e incentiva o uso de bons princípios para projetar a arquitetura de seus serviços. O kit Go ajuda a respeitar os princípios do SOLID, a abordagem orientada ao assunto (DDD) e a arquitetura hexagonal proposta por Alistair Cockburn ou qualquer outra abordagem de arquitetura conhecida como “ arquitetura de cebola ” por Jeffrey Palermo e “ arquitetura limpa ” por Robert C. Martin . Embora o kit Go tenha sido projetado como um conjunto de pacotes para o desenvolvimento de microsserviços, ele também é adequado para o desenvolvimento de monólitos elegantes.


Kit de arquitetura Go


Os três níveis principais na arquitetura de aplicativos desenvolvidos usando o kit Go são:


  • nível de transporte
  • nível do ponto final
  • nível de serviço

Nível de transporte


Quando você escreve microsserviços para sistemas distribuídos, os serviços neles geralmente precisam se comunicar usando vários protocolos de transporte, como: HTTP ou gRPC, ou usam sistemas pub / subs, como NATS. A camada de transporte no kit Go está vinculada a um protocolo de transporte específico (a seguir designado transporte). O kit Go suporta vários transportes para o seu serviço, como: HTTP, gRPC, NATS, AMQP e Thirft ( aprox. Também é possível desenvolver seu próprio transporte para o seu protocolo ). Portanto, os serviços escritos usando o kit Go geralmente se concentram na implementação de uma lógica comercial específica que não sabe nada sobre o transporte usado; você pode usar transportes diferentes para o mesmo serviço. Como exemplo, um serviço escrito no kit Go pode fornecer acesso a ele simultaneamente via HTTP e gRPC.


Pontos finais


Um ponto final ou ponto final é o alicerce fundamental para serviços e clientes. No kit Go, o principal padrão de comunicação é o RPC. O terminal é apresentado como um método RPC separado. Cada método de serviço no kit Go é convertido em um terminal, permitindo a comunicação entre o servidor e o cliente no estilo RCP. Cada nó de extremidade expõe um método de serviço usando a camada Transporte, que por sua vez usa vários protocolos de transporte, como HTTP ou gRPC. Um terminal separado pode ser exposto fora do serviço simultaneamente usando vários transportes ( aprox. Por HTTP e gRPC em portas diferentes ).


Serviços


A lógica de negócios é implementada na camada de serviço. Os serviços escritos com o kit Go são projetados como interfaces. A lógica de negócios na camada de serviço contém o principal núcleo da lógica de negócios, que não precisa saber nada sobre os terminais usados ​​ou um protocolo de transporte específico, como HTTP ou gRPC, ou sobre solicitações e respostas de codificação ou decodificação de vários tipos de mensagens. Isso permitirá que você adote uma arquitetura limpa nos serviços escritos usando o kit Go. Cada método de serviço é convertido no terminal usando um adaptador e exposto fora usando um transporte específico. Através do uso de arquitetura limpa, um único método pode ser definido usando vários transportes ao mesmo tempo.


Exemplos


E agora vamos ver as camadas descritas acima usando um exemplo de um aplicativo simples.


Lógica de negócios no serviço


A lógica de negócios no serviço é projetada usando interfaces. Vamos considerar o exemplo de um pedido no comércio eletrônico:


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

A interface do serviço de pedidos funciona com a entidade do domínio do pedido:


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

Aqui implementamos a interface do serviço de pedidos:


 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 } 

Solicitações e respostas para terminais RPC


Os métodos de serviço são expostos como pontos de extremidade RPC. Portanto, precisamos determinar os tipos de mensagens ( aprox. Por DTO - objeto de transferência de dados ) que serão usadas para enviar e receber mensagens através dos pontos de extremidade RPC. Vamos agora definir estruturas para tipos de solicitação e resposta para terminais RPC no serviço de pedidos:


 // 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"` } 

Terminais do kit Go para métodos de serviço como terminais RPC


O núcleo da nossa lógica de negócios é separado do restante do código e colocado na camada de serviço, que é exposta usando pontos de extremidade RPC, que usam a abstração do kit Go chamada Endpoint .


É assim que o ponto de extremidade do kit Go se parece:


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

Como dissemos acima, o terminal representa um método RPC separado. Cada método de serviço é convertido em endpoint.Endpoint usando adaptadores. Vamos criar pontos de extremidade do kit Go para métodos de serviço 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 } } 

O adaptador de nó de extremidade aceita a interface como um parâmetro para a entrada e a converte em uma abstração do endpoint.Enpoint kit Go. Faça com que cada método de serviço individual seja um nó de extremidade. Essa função do adaptador faz comparação e digita conversões para solicitações, chama um método de serviço e retorna uma mensagem de resposta.


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

Expondo um serviço usando HTTP


Criamos nosso serviço e descrevemos os pontos de extremidade de RPC para expor nossos métodos de serviço. Agora precisamos publicar nosso serviço fora para que outros serviços possam chamar pontos de extremidade RCP. Para expor nosso serviço, precisamos determinar o protocolo de transporte para o nosso serviço, de acordo com o qual ele aceitará solicitações. O kit Go suporta vários transportes, como HTTP, gRPC, NATS, AMQP e Thrift, prontos para uso.


Por exemplo, usamos o transporte HTTP para o nosso serviço. O pacote go kit github.com/go-kit/kit/transport/http fornece a capacidade de atender solicitações HTTP. E a função NewServer do pacote transport/http criará um novo servidor http que implementará o http.Handler e http.Handler os pontos de extremidade fornecidos.


Abaixo está o código que converte os pontos de extremidade do kit Go em um transporte HTTP que atende a solicitações 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) } 

Criamos http.Handler usando a função http.Handler do pacote transport/http , que fornece pontos de extremidade e funções de decodificação de solicitação (retorna o valor do type DecodeRequestFunc func ) e codificação de resposta (por exemplo, type EncodeReponseFunc func ).


A seguir, exemplos de DecodeRequestFunc e 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 

Iniciando servidor HTTP


Finalmente, podemos executar nosso servidor HTTP para processar solicitações. A função NewService descrita acima implementa a interface http.Handler que nos permite executá-la como um servidor 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) } 

Agora, nosso serviço é iniciado e usa o protocolo HTTP no nível de transporte. O mesmo serviço pode ser iniciado usando outro transporte, por exemplo, um serviço pode ser exposto usando gRPC ou Apache Thrift.


Para o artigo introdutório, já usamos o suficiente as primitivas do kit Go, mas também fornece mais funcionalidade para criar sistemas de padrões transparentes e confiáveis, descoberta de serviços, balanceamento de carga etc. Discutiremos essas e outras coisas no kit Go nos seguintes artigos.


Código fonte


O código fonte completo para os exemplos pode ser visualizado no GitHub aqui.


Middlewares no kit Go


O kit Go predispõe ao uso de bons princípios de design de sistema, como camadas. O isolamento de componentes de serviço e pontos de extremidade é possível usando Middlewares ( aprox. Padrão do mediador de faixa ). Os middlewares no kit Go fornecem um mecanismo poderoso pelo qual você pode agrupar serviços e terminais e adicionar funcionalidades (componentes isolados), como registro em log, interrupção de solicitação, limitação do número de solicitações, balanceamento de carga ou rastreamento distribuído.


Abaixo está uma foto do site do kit Go , que é representada como uma “arquitetura de cebola” típica usando Middlewares no kit Go:
imagem


Cuidado com a síndrome de microsserviços de inicialização por mola


Como o kit Go, o Spring Boot é um kit de ferramentas de microsserviço no mundo Java. Mas, diferentemente do kit Go, o Spring Boot é uma estrutura muito madura. Além disso, muitos desenvolvedores Java usam o Spring Boot para criar serviços mundiais usando a pilha Java com feedback positivo do uso, alguns deles acreditam que os microsserviços são apenas sobre o uso do Spring Boot. Vejo muitas equipes de desenvolvimento que interpretam mal o uso de microsserviços, que eles só podem ser desenvolvidos usando o Spring Boot e o Netflix OSS e não percebem os microsserviços como um padrão no desenvolvimento de sistemas distribuídos.


Portanto, lembre-se de que, com um conjunto de ferramentas, como um kit Go ou algum tipo de estrutura, você direciona seu desenvolvimento para microssensibilidades, como um padrão de design. Embora os microsserviços resolvam muitos problemas de dimensionamento de comandos e sistemas, eles também criam muitos problemas porque os dados nos sistemas de microsserviço estão espalhados por vários bancos de dados, o que às vezes cria muitos problemas ao criar consultas transacionais ou de dados. Tudo depende do problema da área de assunto e do contexto do seu sistema. O legal é que o kit Go, projetado como uma ferramenta para criar microsserviços, também foi adequado para criar monólitos elegantes criados com um bom design de arquitetura para seus sistemas.


E alguns recursos do kit Go, como interromper e restringir solicitações, também estão disponíveis em plataformas de malha de serviço, como o Istio. Portanto, se você usar algo como o Istio para iniciar suas microsseguranças, talvez não precise de algumas coisas do kit Go, mas nem todos terão largura de canal suficiente para usar a malha de serviço para criar comunicação entre serviços, pois isso adiciona mais um nível e complexidade extra.


PS


O autor da tradução não pode compartilhar a opinião do autor do texto original . Este artigo foi traduzido para fins educacionais apenas para a comunidade de idiomas russo Go.


UPD
Este também é o primeiro artigo na seção de tradução e eu ficaria grato por qualquer feedback sobre a tradução.

Source: https://habr.com/ru/post/pt430300/


All Articles