Microservices on Go mit dem Go-Kit: Einführung

In diesem Artikel werde ich die Verwendung des Go-Kits beschreiben, einer Reihe von Tools und Bibliotheken zum Erstellen von Microservices unter Go. Dieser Artikel ist eine Einführung in das Go-Kit. Der erste Teil meines Blogs, der Quellcode für die Beispiele, ist hier verfügbar.


Go wird zunehmend für die Entwicklung moderner verteilter Systeme ausgewählt. Wenn Sie ein Cloud-basiertes verteiltes System entwickeln, müssen Sie möglicherweise verschiedene spezifische Funktionen in Ihren Diensten unterstützen, z . B .: Verschiedene Transportprotokolle ( usw., übersetzt HTTP, gRPC usw. ) und Nachrichtenkodierungsformate für diese, RPC-Zuverlässigkeit, Protokollierung , Ablaufverfolgung, Metriken und Profilerstellung, Unterbrechen von Anforderungen, Begrenzen der Anzahl von Anforderungen, Integration in die Infrastruktur und sogar Beschreiben der Architektur. Go ist aufgrund seiner Einfachheit und seiner „No Magic“ -Ansätze eine beliebte Sprache. Daher eignen sich Go-Pakete, beispielsweise eine Standardbibliothek, bereits besser für die Entwicklung verteilter Systeme als die Verwendung eines vollwertigen Frameworks mit viel „Magic under the Hood“. Persönlich habe ich [ ca. trans. Shiju Varghese ] Ich unterstütze die Verwendung vollwertiger Frameworks nicht, ich bevorzuge Bibliotheken, die dem Entwickler mehr Freiheit geben. Das Go-Kit füllte die Lücke im Go-Ökosystem und ermöglichte die Verwendung einer Reihe von Bibliotheken und Paketen beim Erstellen von Microservices, die wiederum die Verwendung guter Prinzipien für das Entwerfen einzelner Dienste in verteilten Systemen ermöglichen.


Bild


Einführung in das Go-Kit


Das Go-Kit besteht aus einer Reihe von Go-Paketen, mit denen sich auf einfache Weise zuverlässige und unterstützte Microservices erstellen lassen. Das Go-Kit bietet Bibliotheken zum Implementieren verschiedener Komponenten einer transparenten und zuverlässigen Anwendungsarchitektur unter Verwendung von Ebenen wie Protokollierung, Metriken, Ablaufverfolgung, Begrenzung und Unterbrechung von Anforderungen, die zum Ausführen von Microservices auf dem Produkt erforderlich sind. Das Go-Kit ist gut, da es gut implementierte Tools für die Interaktion mit verschiedenen Infrastrukturen, Nachrichtenkodierungsformaten und verschiedenen Transportschichten enthält.


Zusätzlich zu den Bibliotheken für Dienste in Entwicklungsländern bietet und fördert es die Verwendung guter Prinzipien für die Gestaltung der Architektur Ihrer Dienste. Mit dem Go-Kit können Sie die SOLID-Prinzipien, den von Alistair Cockburn vorgeschlagenen themenorientierten Ansatz (DDD) und die hexagonale Architektur oder einen anderen Ansatz aus den von Jeffrey Palermo als „ Zwiebelarchitektur “ und von Robert C. Martin als „ saubere Architektur “ bekannten Architekturprinzipien einhalten. Obwohl das Go-Kit als Paket für die Entwicklung von Microservices konzipiert wurde, eignet es sich auch für die Entwicklung eleganter Monolithen.


Architektur Go Kit


Die drei Hauptebenen in der Architektur von Anwendungen, die mit dem Go-Kit entwickelt wurden, sind:


  • Transportniveau
  • Endpunktebene
  • Service Level

Transportniveau


Wenn Sie Microservices für verteilte Systeme schreiben, müssen die darin enthaltenen Dienste häufig über verschiedene Transportprotokolle wie HTTP oder gRPC miteinander kommunizieren oder Pub / Sub-Systeme wie NATS verwenden. Die Transportschicht im Go-Kit ist an ein bestimmtes Transportprotokoll gebunden (im Folgenden: Transport). Das Go-Kit unterstützt verschiedene Transporte für Ihren Dienst, z. B. HTTP, gRPC, NATS, AMQP und Thirft ( ca. Sie können auch Ihren eigenen Transport für Ihr Protokoll entwickeln ). Daher konzentrieren sich Services, die mit dem Go-Kit geschrieben wurden, häufig auf die Implementierung einer bestimmten Geschäftslogik, die nichts über den verwendeten Transport weiß. Sie können verschiedene Transporte für denselben Service verwenden. Beispielsweise kann ein in Go Kit geschriebener Dienst gleichzeitig über HTTP und gRPC Zugriff darauf gewähren.


Endpunkte


Ein Endpunkt oder Endpunkt ist der grundlegende Baustein für Services und Kunden. Im Go-Kit ist das Hauptkommunikationsmuster RPC. Der Endpunkt wird als separate RPC-Methode dargestellt. Jede Dienstmethode im Go-Kit wird in einen Endpunkt konvertiert, sodass Sie im RCP-Stil zwischen Server und Client kommunizieren können. Jeder Endpunkt stellt eine Dienstmethode unter Verwendung der Transportschicht bereit, die wiederum verschiedene Transportprotokolle wie HTTP oder gRPC verwendet. Ein separater Endpunkt kann über mehrere Transporte gleichzeitig aus dem Dienst entfernt werden ( ca. Lane HTTP und gRPC an verschiedenen Ports ).


Dienstleistungen


Geschäftslogik ist in der Serviceschicht implementiert. Mit dem Go-Kit geschriebene Dienste sind als Schnittstellen konzipiert. Die Geschäftslogik in der Serviceschicht enthält den Hauptkern der Geschäftslogik, die nichts über die verwendeten Endpunkte oder ein bestimmtes Transportprotokoll wie HTTP oder gRPC oder über das Codieren oder Decodieren von Anforderungen und Antworten verschiedener Nachrichtentypen wissen muss. Auf diese Weise können Sie bei Diensten, die mit dem Go-Kit geschrieben wurden, eine saubere Architektur einhalten. Jede Servicemethode wird mithilfe eines Adapters in einen Endpunkt konvertiert und mithilfe eines bestimmten Transports außerhalb verfügbar gemacht. Durch die Verwendung einer sauberen Architektur kann eine einzelne Methode mit mehreren Transporten gleichzeitig festgelegt werden.


Beispiele


Schauen wir uns nun die oben beschriebenen Ebenen anhand eines Beispiels einer einfachen Anwendung an.


Geschäftslogik im Service


Die Geschäftslogik im Service wird mithilfe von Schnittstellen entworfen. Wir werden uns ein Beispiel für eine Bestellung im E-Commerce ansehen:


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

Die Order Service-Schnittstelle funktioniert mit der Order Domain Entity:


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

Hier implementieren wir die Schnittstelle des Bestellservices:


 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 } 

Anfragen und Antworten für RPC-Endpunkte


Dienstmethoden werden als RPC-Endpunkte verfügbar gemacht. Daher müssen wir die Nachrichtentypen ( ca. Per. DTO - Datenübertragungsobjekt ) bestimmen, die zum Senden und Empfangen von Nachrichten über RPC-Endpunkte verwendet werden. Definieren wir nun Strukturen für Anforderungs- und Antworttypen für RPC-Endpunkte im Bestellservice:


 // 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-Endpunkte für Servicemethoden wie RPC-Endpunkte


Der Kern unserer Geschäftslogik wird vom Rest des Codes getrennt und in die Serviceschicht eingefügt, die mithilfe von RPC-Endpunkten verfügbar gemacht wird, die die Go-Kit-Abstraktion namens Endpoint .


So sieht der Endpunkt aus dem Go-Kit aus:


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

Wie oben erwähnt, stellt der Endpunkt eine separate RPC-Methode dar. Jede Servicemethode wird mithilfe von Adaptern in endpoint.Endpoint konvertiert. Lassen Sie uns Go-Kit-Endpunkte für Bestelldienstmethoden erstellen:


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

Der Endpunktadapter akzeptiert die Schnittstelle als Argument für die Eingabe und konvertiert sie in eine Abstraktion des Go-Kit- endpoint.Enpoint macht jede einzelne Dienstmethode zu einem Endpunkt. Diese Adapterfunktion führt Vergleichs- und Typkonvertierungen für Anforderungen durch, ruft eine Dienstmethode auf und gibt eine Antwortnachricht zurück.


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

Bereitstellen eines Dienstes über HTTP


Wir haben unseren Service erstellt und RPC-Endpunkte für die Bereitstellung unserer Servicemethoden beschrieben. Jetzt müssen wir unseren Service außerhalb veröffentlichen, damit andere Services RCP-Endpunkte aufrufen können. Um unseren Service verfügbar zu machen, müssen wir das Transportprotokoll für unseren Service festlegen, nach dem Anfragen angenommen werden. Das Go-Kit unterstützt verschiedene Transporte wie HTTP, gRPC, NATS, AMQP und Thrift.


Zum Beispiel verwenden wir den HTTP-Transport für unseren Service. Das Go-Kit-Paket github.com/go-kit/kit/transport/http bietet die Möglichkeit, HTTP-Anforderungen zu bearbeiten. Die NewServer Funktion aus dem transport/http Paket erstellt einen neuen http-Server, der http.Handler implementiert und die bereitgestellten Endpunkte http.Handler .


Im Folgenden finden Sie den Code, der Go-Kit-Endpunkte in einen HTTP-Transport konvertiert, der HTTP-Anforderungen bedient:


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

Wir erstellen http.Handler mit der NewServer Funktion aus dem transport/http Paket, das Endpunkte und Anforderungsdecodierungsfunktionen (gibt den Wert vom type DecodeRequestFunc func ) und Antwortcodierung (z. B. type EncodeReponseFunc func ) type EncodeReponseFunc func .


Das Folgende sind Beispiele für DecodeRequestFunc und 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-Server starten


Schließlich können wir unseren HTTP-Server ausführen, um Anforderungen zu verarbeiten. Die NewService beschriebene NewService Funktion implementiert die http.Handler Schnittstelle http.Handler der wir sie als HTTP-Server ausführen können:


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

Jetzt wird unser Dienst gestartet und verwendet das HTTP-Protokoll auf Transportebene. Derselbe Dienst kann mit einem anderen Transport gestartet werden. Beispielsweise kann ein Dienst mit gRPC oder Apache Thrift verfügbar gemacht werden.


Für den Einführungsartikel haben wir bereits genug Go-Kit-Grundelemente verwendet, aber es bietet auch mehr Funktionen zum Erstellen von Systemen mit transparenten, zuverlässigen Mustern, Serviceerkennung, Lastausgleich usw. Wir werden diese und andere Dinge im Go-Kit in den folgenden Artikeln diskutieren.


Quellcode


Der gesamte Quellcode für die Beispiele kann hier auf GitHub eingesehen werden.


Middleware im Go-Kit


Das Go-Kit ist prädisponiert für die Verwendung guter Prinzipien des Systemdesigns, wie z. B. Layering. Die Isolierung von Servicekomponenten und Endpunkten ist mit Middlewares möglich ( ca. Lane Mediator Pattern ). Middlewares im Go-Kit bietet einen leistungsstarken Mechanismus, mit dem Sie Dienste und Endpunkte umschließen und Funktionen (isolierte Komponenten) hinzufügen können, z. B. Protokollierung, Unterbrechung von Anforderungen, Begrenzung der Anzahl von Anforderungen, Lastausgleich oder verteilte Ablaufverfolgung.


Unten sehen Sie ein Bild von der Go-Kit- Website, das als typische „Zwiebelarchitektur“ mit Middlewares im Go-Kit dargestellt wird:
Bild


Vorsicht vor dem Spring Boot Microservices Syndrom


Wie das Go-Kit ist Spring Boot ein Microservice-Toolkit in der Java-Welt. Im Gegensatz zum Go-Kit ist Spring Boot jedoch ein sehr ausgereiftes Framework. Viele Java-Entwickler verwenden Spring Boot, um mithilfe des Java-Stacks World Services mit positivem Feedback aus der Verwendung zu erstellen. Einige von ihnen glauben, dass es bei Microservices nur um die Verwendung von Spring Boot geht. Ich sehe viele Entwicklungsteams, die die Verwendung von Microservices falsch interpretieren, dass sie nur mit Spring Boot und OSS Netflix entwickelt werden können und Microservices bei der Entwicklung verteilter Systeme nicht als Muster wahrnehmen.


Denken Sie also daran, dass Sie mit einer Reihe von Tools, wie einem Go-Kit oder einem Framework, Ihre Entwicklung auf Mikroseurises als Entwurfsmuster ausrichten. Obwohl Microservices viele Skalierungsprobleme sowohl von Befehlen als auch von Systemen lösen, entstehen auch viele Probleme, da die Daten in Microservice-Systemen auf verschiedene Datenbanken verteilt sind, was manchmal viele Probleme beim Erstellen von Transaktions- oder Datenabfragen verursacht. Es hängt alles vom Problem des Themenbereichs und dem Kontext Ihres Systems ab. Das Coole ist, dass das Go-Kit, das als Tool zum Erstellen von Microservices entwickelt wurde, auch zum Erstellen eleganter Monolithen geeignet ist, die mit einem guten Architekturdesign für Ihre Systeme erstellt wurden.


Einige Funktionen des Go-Kits, z. B. das Unterbrechen und Einschränken von Anforderungen, sind auch auf Service-Mesh-Plattformen wie Istio verfügbar. Wenn Sie also etwas wie Istio verwenden, um Ihre Mikroseurises zu starten, benötigen Sie möglicherweise nicht einige Dinge aus dem Go-Kit, aber nicht jeder hat genug Kanalbreite, um das Service-Mesh zum Erstellen einer dienstübergreifenden Kommunikation zu verwenden, da dies mehr hinzufügt eine Ebene und zusätzliche Komplexität.


PS


Der Autor der Übersetzung teilt möglicherweise nicht die Meinung des Autors des Originaltextes . Dieser Artikel wurde nur zu Bildungszwecken für die russische Sprachgemeinschaft Go übersetzt.


UPD
Dies ist auch der erste Artikel im Übersetzungsbereich und ich wäre für jedes Feedback zur Übersetzung dankbar.

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


All Articles