Microservices on Go avec le kit Go: Introduction

Dans cet article, je décrirai l'utilisation du kit Go, un ensemble d'outils et de bibliothèques pour créer des micro-services sur Go. Cet article est une introduction au kit Go. La première partie de mon blog, le code source des exemples est disponible ici .


Go est de plus en plus choisi pour le développement de systèmes distribués modernes. Lorsque vous développez un système distribué basé sur le cloud, vous devrez peut-être prendre en charge diverses fonctionnalités spécifiques dans vos services, telles que: divers protocoles de transport ( etc. trad. HTTP, gRPC, etc. ) et formats de codage des messages pour eux, fiabilité RPC, journalisation , traçage, métriques et profilage, interruption des requêtes, limitation du nombre de requêtes, intégration dans l'infrastructure, voire description de l'architecture. Go est un langage populaire en raison de sa simplicité et de ses approches «sans magie», donc les packages Go, par exemple, une bibliothèque standard, sont déjà plus adaptés pour développer des systèmes distribués que d'utiliser un cadre à part entière avec beaucoup de «magie sous le capot». Personnellement, je [ env. trans. Shiju Varghese ] Je ne supporte pas l'utilisation de frameworks à part entière, je préfère utiliser des bibliothèques qui donnent plus de liberté au développeur. Le kit Go a comblé une lacune dans l'écosystème Go, permettant d'utiliser un ensemble de bibliothèques et de packages lors de la création de microservices, qui à leur tour permettent l'utilisation de bons principes pour la conception de services individuels dans des systèmes distribués.


image


Kit d'introduction à Go


Le kit Go est un ensemble de packages Go qui facilitent la création de microservices fiables et pris en charge. Le kit Go fournit des bibliothèques pour implémenter divers composants d'une architecture d'application transparente et fiable, en utilisant des couches telles que: la journalisation, les métriques, le traçage, la limitation et l'interruption des requêtes qui sont nécessaires pour exécuter des microservices sur le prod. Le kit Go est bon car il dispose d'outils bien mis en œuvre pour interagir avec diverses infrastructures, formats d'encodage de messages et différentes couches de transport.


En plus de l'ensemble des bibliothèques pour les services du monde en développement, il fournit et encourage l'utilisation de bons principes pour concevoir l'architecture de vos services. Le kit Go vous aide à adhérer aux principes SOLID, à l'approche orientée sujet (DDD) et à l' architecture hexagonale proposés par Alistair Cockburn ou à toute autre approche issue des principes architecturaux connus sous le nom de « architecture oignon » par Jeffrey Palermo et « architecture propre » par Robert C. Martin . Bien que le kit Go ait été conçu comme un ensemble de packages pour développer des microservices, il convient également pour développer des monolithes élégants.


Kit Architecture Go


Les trois niveaux principaux de l'architecture des applications développées à l'aide du kit Go sont:


  • niveau de transport
  • niveau de point final
  • niveau de service

Niveau de transport


Lorsque vous écrivez des microservices pour des systèmes distribués, les services qu'ils contiennent doivent souvent communiquer entre eux à l'aide de divers protocoles de transport, tels que: HTTP ou gRPC, ou utiliser des systèmes pub / sub, tels que NATS. La couche de transport dans le kit Go est liée à un protocole de transport spécifique (ci-après transport). Le kit Go prend en charge divers transports pour votre service, tels que: HTTP, gRPC, NATS, AMQP et Thirft ( environ. Vous pouvez également développer votre propre transport pour votre protocole ). Par conséquent, les services écrits à l'aide du kit Go se concentrent souvent sur la mise en œuvre d'une logique métier spécifique qui ne sait rien du transport utilisé, vous êtes libre d'utiliser différents transports pour le même service. Par exemple, un service écrit dans le kit Go peut simultanément y accéder via HTTP et gRPC.


Points de terminaison


Un point de terminaison ou un point de terminaison est la pierre angulaire fondamentale des services et des clients. Dans le kit Go, le modèle de communication principal est RPC. Le point final est présenté comme une méthode RPC distincte. Chaque méthode de service du kit Go est convertie en point de terminaison, vous permettant de communiquer entre le serveur et le client dans le style RCP. Chaque point de terminaison expose une méthode de service utilisant la couche Transport, qui à son tour utilise divers protocoles de transport, tels que HTTP ou gRPC. Un point de terminaison séparé peut être exposé en dehors du service simultanément à l'aide de plusieurs transports ( environ HTTP et gRPC selon différents ports ).


Les services


La logique métier est implémentée dans la couche service. Les services écrits avec le kit Go sont conçus comme des interfaces. La logique métier dans la couche service contient le noyau principal de la logique métier, qui n'a besoin de rien savoir sur les points de terminaison utilisés ou un protocole de transport spécifique, comme HTTP ou gRPC, ou sur l'encodage ou le décodage des demandes et des réponses de divers types de messages. Cela vous permettra d'adhérer à une architecture propre dans les services écrits à l'aide du kit Go. Chaque méthode de service est convertie en point de terminaison à l'aide d'un adaptateur et exposée à l'extérieur à l'aide d'un transport spécifique. Grâce à l'utilisation d'une architecture propre, une seule méthode peut être définie à l'aide de plusieurs transports en même temps.


Des exemples


Et maintenant, regardons les couches décrites ci-dessus en utilisant un exemple d'application simple.


Logique métier au service


La logique métier du service est conçue à l'aide d'interfaces. Nous considérerons l'exemple d'une commande en e-commerce:


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

L'interface du service de commande fonctionne avec l'entité de domaine de commande:


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

Ici, nous implémentons l'interface du service 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 } 

Demandes et réponses pour les points de terminaison RPC


Les méthodes de service sont exposées en tant que points de terminaison RPC. Nous devons donc déterminer les types de messages ( environ Per. DTO - objet de transfert de données ) qui seront utilisés pour envoyer et recevoir des messages via des points de terminaison RPC. Définissons maintenant les structures des types de demande et de réponse pour les points de terminaison RPC dans le service 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"` } 

Aller aux points de terminaison du kit pour les méthodes de service comme les points de terminaison RPC


Le cœur de notre logique métier est séparé du reste du code et placé dans la couche de service, qui est exposée à l'aide de points de terminaison RPC, qui utilisent l'abstraction du kit Go appelée Endpoint .


Voici à quoi ressemble le point final du kit Go:


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

Comme nous l'avons dit ci-dessus, le point final représente une méthode RPC distincte. Chaque méthode de service est convertie en endpoint.Endpoint aide d'adaptateurs. Faisons les kits d'extrémité du kit Go pour les méthodes de service de commande:


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

L'adaptateur de point de terminaison prend l'interface comme argument à l'entrée et la convertit en endpoint.Enpoint kit d'abstraction Go. endpoint.Enpoint faisant de chaque méthode de service individuelle un point de terminaison. Cette fonction d'adaptateur effectue des conversions de comparaison et de type pour les demandes, appelle une méthode de service et renvoie un message de réponse.


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

Exposer un service à l'aide de HTTP


Nous avons créé notre service et décrit les points de terminaison RPC pour exposer nos méthodes de service. Nous devons maintenant publier notre service à l'extérieur afin que d'autres services puissent appeler des points de terminaison RCP. Pour exposer notre service, nous devons déterminer le protocole de transport pour notre service, selon lequel il acceptera les demandes. Le kit Go prend en charge divers transports, tels que HTTP, gRPC, NATS, AMQP et Thrift prêts à l'emploi.


Par exemple, nous utilisons le transport HTTP pour notre service. Le package go kit github.com/go-kit/kit/transport/http offre la possibilité de servir les requêtes HTTP. Et la fonction NewServer du NewServer transport/http créera un nouveau serveur http qui implémentera http.Handler et http.Handler les points de terminaison fournis.


Vous trouverez ci-dessous le code qui convertit les points de terminaison du kit Go en un transport HTTP qui sert les requêtes 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) } 

Nous créons http.Handler à l'aide de la fonction NewServer du NewServer transport/http , qui nous fournit des points de terminaison et demande des fonctions de décodage (renvoie la valeur de type DecodeRequestFunc func ) et un codage de réponse (par exemple, type EncodeReponseFunc func ).


Voici des exemples de DecodeRequestFunc et 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 

Démarrage du serveur HTTP


Enfin, nous pouvons exécuter notre serveur HTTP pour traiter les demandes. La fonction NewService décrite ci-dessus implémente l'interface http.Handler qui nous permet de l'exécuter en tant que serveur 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) } 

Maintenant, notre service est lancé et utilise le protocole HTTP au niveau du transport. Le même service peut être démarré à l'aide d'un autre transport. Par exemple, un service peut être exposé à l'aide de gRPC ou d'Apache Thrift.


Pour l'article d'introduction, nous avons déjà suffisamment utilisé les primitives du kit Go, mais il fournit également plus de fonctionnalités pour créer des systèmes de modèles transparents et fiables, la découverte de services, l'équilibrage de charge, etc. Nous en discuterons et d'autres dans le kit Go dans les articles suivants.


Code source


Le code source complet pour les exemples peut être consulté sur GitHub ici.


Kit Middlewares in Go


Le kit Go prédispose à l'utilisation de bons principes de conception de système, tels que la superposition. L'isolement des composants de service et des points d'extrémité est possible à l'aide de middlewares ( modèle de médiateur de voie environ ). Les middlewares du kit Go fournissent un mécanisme puissant par lequel vous pouvez encapsuler des services et des points de terminaison et ajouter des fonctionnalités (composants isolés), telles que la journalisation, l'interruption des demandes, la limitation du nombre de demandes, l'équilibrage de charge ou le traçage distribué.


Ci-dessous, une image du site Web du kit Go , qui est décrite comme une «architecture d'oignon» typique utilisant des middlewares dans le kit Go:
image


Méfiez-vous du syndrome des microservices Spring Boot


Comme le kit Go, Spring Boot est une boîte à outils de microservices dans le monde Java. Mais, contrairement au kit Go, Spring Boot est un framework très mature. En outre, de nombreux développeurs Java utilisent Spring Boot pour créer des services mondiaux à l'aide de la pile Java avec des commentaires positifs sur l'utilisation, certains d'entre eux pensent que les microservices ne concernent que l'utilisation de Spring Boot. Je vois de nombreuses équipes de développement qui interprètent mal l'utilisation des microservices, qui ne peuvent être développées qu'en utilisant Spring Boot et OSS Netflix et ne perçoivent pas les microservices comme un modèle lors du développement de systèmes distribués.


Gardez donc à l'esprit qu'avec un ensemble d'outils, comme un kit Go ou une sorte de cadre, vous orientez votre développement vers les microseurises, comme modèle de conception. Bien que les microservices résolvent de nombreux problèmes de mise à l'échelle des commandes et des systèmes, cela crée également de nombreux problèmes car les données des systèmes basés sur les microservices sont dispersées dans diverses bases de données, ce qui crée parfois de nombreux problèmes lors de la création de requêtes transactionnelles ou de données. Tout dépend du problème du domaine et du contexte de votre système. Ce qui est cool, c'est que le kit Go, conçu comme un outil pour créer des microservices, était également adapté pour créer des monolithes élégants qui sont créés avec une bonne architecture pour vos systèmes.


Et certaines fonctionnalités du kit Go, telles que l'interruption et la restriction des demandes, sont également disponibles sur les plates-formes de maillage de service, comme Istio. Donc, si vous utilisez quelque chose comme Istio pour lancer vos microseurises, vous n'aurez peut-être pas besoin de certaines choses du kit Go, mais tout le monde n'aura pas assez de largeur de canal pour utiliser le maillage de service pour créer une communication interservices, car cela ajoute plus un niveau et une complexité supplémentaire.


PS


L'auteur de la traduction peut ne pas partager l'avis de l'auteur du texte original , cet article a été traduit à des fins éducatives uniquement pour la communauté de langue russe Go.


UPD
Ceci est également le premier article de la section traduction et je vous serais reconnaissant de tout commentaire sur la traduction.

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


All Articles