Microservicios en Go con el kit Go: Introducción

En este artículo describiré el uso del kit Go, un conjunto de herramientas y bibliotecas para crear microservicios en Go. Este artículo es una introducción al kit Go. La primera parte de mi blog, el código fuente de los ejemplos está disponible aquí .


Go es cada vez más elegido para el desarrollo de sistemas distribuidos modernos. Cuando desarrolle un sistema distribuido basado en la nube, es posible que deba admitir varias funciones específicas en sus servicios, tales como: varios protocolos de transporte ( etc.transl. HTTP, gRPC, etc. ) y formatos de codificación de mensajes para ellos, confiabilidad RPC, registro , seguimiento, métricas y perfiles, interrupción de solicitudes, limitación del número de solicitudes, integración en la infraestructura e incluso descripción de la arquitectura. Go es un lenguaje popular debido a su simplicidad y enfoques "sin magia", por lo tanto, los paquetes de Go, por ejemplo, una biblioteca estándar, ya son más adecuados para desarrollar sistemas distribuidos que usar un marco completo con mucha "magia bajo el capó". Personalmente, yo [ aprox. trans. Shiju Varghese ] No apoyo el uso de frameworks completos, prefiero usar bibliotecas que den más libertad al desarrollador. El kit Go llenó el vacío en el ecosistema Go, haciendo posible el uso de un conjunto de bibliotecas y paquetes al crear microservicios, que a su vez permiten el uso de buenos principios para diseñar servicios individuales en sistemas distribuidos.


imagen


Introducción al kit Go


El kit Go es un conjunto de paquetes Go que facilitan la creación de microservicios confiables y compatibles. Go Kit proporciona bibliotecas para implementar varios componentes de una arquitectura de aplicación transparente y confiable, utilizando capas como: registro, métricas, rastreo, limitación e interrupción de solicitudes que son necesarias para ejecutar microservicios en el producto. Go kit es bueno porque tiene herramientas bien implementadas para interactuar con varias infraestructuras, formatos de codificación de mensajes y varias capas de transporte.


Además del conjunto de bibliotecas para servicios mundiales en desarrollo, proporciona y fomenta el uso de buenos principios para diseñar la arquitectura de sus servicios. El kit Go lo ayuda a adherirse a los principios SÓLIDOS, el enfoque orientado a temas (DDD) y la arquitectura hexagonal propuesta por Alistair Cockburn o cualquier otro enfoque de arquitectura conocido como " arquitectura de cebolla " por Jeffrey Palermo y " arquitectura limpia " por Robert C. Martin . Aunque el kit Go fue diseñado como un conjunto de paquetes para desarrollar microservicios, también es adecuado para desarrollar monolitos elegantes.


Kit de arquitectura Go


Los tres niveles principales en la arquitectura de las aplicaciones desarrolladas con el kit Go son:


  • nivel de transporte
  • nivel de punto final
  • nivel de servicio

Nivel de transporte


Cuando escribe microservicios para sistemas distribuidos, los servicios en ellos a menudo tienen que comunicarse entre sí mediante varios protocolos de transporte, como: HTTP o gRPC, o utilizar sistemas pub / sub, como NATS. La capa de transporte en el kit Go está vinculada a un protocolo de transporte específico (en adelante, transporte). El kit Go admite varios transportes para su servicio, tales como: HTTP, gRPC, NATS, AMQP y Thirft ( aprox. También puede desarrollar su propio transporte para su protocolo ). Por lo tanto, los servicios escritos utilizando el kit Go a menudo se centran en la implementación de una lógica comercial específica que no sabe nada sobre el transporte utilizado, usted es libre de utilizar diferentes transportes para el mismo servicio. Como ejemplo, un servicio escrito en el kit Go puede proporcionarle acceso simultáneamente a través de HTTP y gRPC.


Puntos finales


Un punto final o punto final es el bloque de construcción fundamental para los servicios y los clientes. En el kit Go, el patrón de comunicación principal es RPC. El punto final se presenta como un método RPC separado. Cada método de servicio en el kit Go se convierte en un punto final, lo que le permite comunicarse entre el servidor y el cliente en el estilo RCP. Cada punto final expone un método de servicio utilizando la capa de transporte, que a su vez utiliza varios protocolos de transporte, como HTTP o gRPC. Un punto final separado se puede exponer fuera del servicio simultáneamente usando varios transportes ( aprox. Por HTTP y gRPC en diferentes puertos ).


Servicios


La lógica empresarial se implementa en la capa de servicio. Los servicios escritos con el kit Go están diseñados como interfaces. La lógica empresarial en la capa de servicio contiene el núcleo principal de la lógica empresarial, que no necesita saber nada sobre los puntos finales utilizados o un protocolo de transporte específico, como HTTP o gRPC, o sobre la codificación o decodificación de solicitudes y respuestas de varios tipos de mensajes. Esto le permitirá adherirse a una arquitectura limpia en los servicios escritos con el kit Go. Cada método de servicio se convierte en punto final mediante un adaptador y se expone al exterior mediante un transporte específico. Mediante el uso de una arquitectura limpia, se puede establecer un único método utilizando múltiples transportes al mismo tiempo.


Ejemplos


Y ahora veamos las capas descritas anteriormente usando un ejemplo de una aplicación simple.


Lógica empresarial en el servicio.


La lógica de negocios en el servicio está diseñada usando interfaces. Veremos un ejemplo de un pedido en el comercio electró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 } 

La interfaz del servicio de pedidos funciona con la entidad de dominio de pedidos:


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

Aquí implementamos la interfaz del servicio 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 } 

Solicitudes y respuestas para puntos finales RPC


Los métodos de servicio están expuestos como puntos finales RPC. Por lo tanto, debemos determinar los tipos de mensajes ( aprox. Por DTO - objeto de transferencia de datos ) que se utilizarán para enviar y recibir mensajes a través de puntos finales RPC. Ahora definamos estructuras para los tipos de solicitud y respuesta para puntos finales RPC en el servicio 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"` } 

Ir a los puntos finales del kit para métodos de servicio como puntos finales RPC


El núcleo de nuestra lógica de negocios se separa del resto del código y se coloca en la capa de servicio, que se expone usando puntos finales RPC, que usan la abstracción del kit Go llamada Endpoint .


Así es como se ve el punto final del kit Go:


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

Como dijimos anteriormente, el punto final representa un método RPC separado. Cada método de servicio se convierte en endpoint.Endpoint . Punto endpoint.Endpoint mediante adaptadores. Hagamos los puntos finales del kit Go para los métodos de servicio de pedidos:


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

El adaptador de punto final acepta la interfaz como un parámetro para la entrada y la convierte en una abstracción del endpoint.Enpoint kit Go. endpoint.Enpoint convierte cada método de servicio individual en un punto final. Esta función de adaptador realiza conversiones de comparación y tipo para solicitudes, llama a un método de servicio y devuelve un mensaje de respuesta.


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

Exponer un servicio usando HTTP


Creamos nuestro servicio y describimos los puntos finales RPC para exponer nuestros métodos de servicio. Ahora necesitamos publicar nuestro servicio en el exterior para que otros servicios puedan llamar a los puntos finales RCP. Para exponer nuestro servicio, necesitamos determinar el protocolo de transporte para nuestro servicio, según el cual aceptará solicitudes. El kit Go admite varios transportes, como HTTP, gRPC, NATS, AMQP y Thrift listos para usar.


Por ejemplo, utilizamos el transporte HTTP para nuestro servicio. El paquete go kit github.com/go-kit/kit/transport/http proporciona la capacidad de atender solicitudes HTTP. Y la función NewServer del paquete transport/http creará un nuevo servidor http que implementará http.Handler y envolverá los puntos finales proporcionados.


A continuación se muestra el código que convierte los puntos finales del kit Go en un transporte HTTP que atiende solicitudes 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) } 

Creamos http.Handler utilizando la función NewServer del NewServer de transport/http , que proporciona puntos finales y funciones de decodificación de solicitud (devuelve el valor del type DecodeRequestFunc func ) y la codificación de respuesta (por ejemplo, type EncodeReponseFunc func ).


Los siguientes son ejemplos de DecodeRequestFunc y 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 el servidor HTTP


Finalmente, podemos ejecutar nuestro servidor HTTP para procesar solicitudes. La función NewService descrita anteriormente implementa la interfaz http.Handler que nos permite ejecutarla como un 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) } 

Ahora se lanza nuestro servicio y utiliza el protocolo HTTP a nivel de transporte. El mismo servicio se puede iniciar usando otro transporte, por ejemplo, un servicio se puede exponer usando gRPC o Apache Thrift.


Para el artículo introductorio, ya hemos utilizado las primitivas del kit Go, pero también proporciona más funcionalidad para crear sistemas de patrones transparentes y confiables, descubrimiento de servicios, equilibrio de carga, etc. Discutiremos estas y otras cosas en el kit Go en los siguientes artículos.


Código fuente


El código fuente completo de los ejemplos se puede ver en GitHub aquí.


Middlewares en el kit Go


Go kit predispone al uso de buenos principios de diseño de sistemas, como la estratificación. El aislamiento de los componentes de servicio y puntos finales es posible utilizando Middlewares ( patrón de mediador de carril aprox. ). El kit Middlewares in the Go proporciona un poderoso mecanismo mediante el cual puede ajustar servicios y puntos finales y agregar funcionalidades (componentes aislados), como el registro, la interrupción de solicitudes, la limitación del número de solicitudes, el equilibrio de carga o el seguimiento distribuido.


A continuación se muestra una imagen del sitio web del kit Go , que se representa como una "arquitectura de cebolla" típica usando Middlewares en el kit Go:
imagen


Cuidado con el síndrome de microservicios de arranque de primavera


Al igual que el kit Go, Spring Boot es un kit de herramientas de microservicio en el mundo de Java. Pero, a diferencia del kit Go, Spring Boot es un marco muy maduro. Además, muchos desarrolladores de Java usan Spring Boot para crear servicios mundiales utilizando la pila de Java con comentarios positivos del uso, algunos de ellos creen que los microservicios solo se tratan de usar Spring Boot. Veo muchos equipos de desarrollo que malinterpretan el uso de microservicios, que solo se pueden desarrollar usando Spring Boot y OSS Netflix y no perciben los microservicios como un patrón cuando se desarrollan sistemas distribuidos.


Tenga en cuenta que con un conjunto de herramientas, como un kit Go o algún tipo de marco, dirige su desarrollo hacia microseurises, como un patrón de diseño. Aunque los microservicios resuelven muchos problemas de escalado tanto de comandos como de sistemas, también crea muchos problemas porque los datos en los sistemas de microservicios están dispersos en varias bases de datos, lo que a veces crea muchos problemas al crear consultas transaccionales o de datos. Todo depende del problema del área temática y del contexto de su sistema. Lo bueno es que el kit Go, diseñado como una herramienta para crear microservicios, también fue adecuado para crear monolitos elegantes que se crean con un buen diseño de arquitectura para sus sistemas.


Y algunas características del kit Go, como interrumpir y restringir solicitudes, también están disponibles en plataformas de malla de servicio, como Istio. Entonces, si usa algo como Istio para lanzar sus microseurises, es posible que no necesite algunas cosas del kit Go, pero no todos tendrán suficiente ancho de canal para usar la malla de servicio para crear comunicación entre servicios, ya que esto agrega más Un nivel y complejidad extra.


PS


El autor de la traducción puede no compartir la opinión del autor del texto original , este artículo ha sido traducido con fines educativos solo para la comunidad de idioma ruso Go.


UPD
Este es también el primer artículo en la sección de traducción y agradecería cualquier comentario sobre la traducción.

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


All Articles