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

Ursprünglicher Autor: Shiju Varghese
  • Übersetzung

In diesem Artikel werde ich die Verwendung des Go-Kits, einer Reihe von Tools und Bibliotheken zum Erstellen von Microservices für Go, beschreiben. Dieser Artikel ist eine Einführung in das Go-Kit. Der erste Teil meines Blogs, der Quellcode der 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, benötigen Sie möglicherweise Unterstützung für verschiedene spezifische Funktionen in Ihren Diensten, z. B. verschiedene Transportprotokolle ( z. B. HTTP-Spur, gRPC usw. ) und Nachrichtenverschlüsselungsformate für sie, RPC-Zuverlässigkeit, Protokollierung Verfolgung, Metriken und Profilierung, Abfragen unterbrechen, Anzahl der Abfragen begrenzen, in die Infrastruktur integrieren und sogar die Architektur beschreiben. Go ist aufgrund seiner Einfachheit und seiner „magischen“ Herangehensweise eine beliebte Sprache. Go-Pakete, zum Beispiel die Standardbibliothek, eignen sich daher bereits mehr für die Entwicklung verteilter Systeme als die Verwendung eines vollwertigen Frameworks mit viel „Magie unter der Haube“. Ich persönlichca. pro. Shiju Varghese ] unterstützt nicht die Verwendung vollständiger Frameworks. Ich bevorzuge Bibliotheken, die dem Entwickler mehr Freiheit geben. Go Kit füllte die Lücke im Go-Ökosystem und ermöglichte die Verwendung einer Reihe von Bibliotheken und Paketen bei der Erstellung von Microservices, die wiederum die Verwendung guter Prinzipien für die Gestaltung einzelner Dienste in verteilten Systemen ermöglichen.


Bild


Einführung zum Go-Kit


Go Kit ist ein Set von Go-Paketen, die die Erstellung zuverlässiger und unterstützter Mikrodienste ermöglichen. Go Kit stellt Bibliotheken für die Implementierung verschiedener Komponenten einer transparenten und zuverlässigen Anwendungsarchitektur bereit. Dabei werden folgende Schichten verwendet: Protokollierung, Metriken, Ablaufverfolgung, Einschränkung und Unterbrechung von Anforderungen, die zum Ausführen von Mikrodiensten auf dem Server erforderlich sind. Go Kit ist gut, weil es gut implementierte Tools für die Interaktion mit verschiedenen Infrastrukturen, Nachrichtenverschlüsselungsformaten und verschiedenen Transportebenen ist.


Neben einer Reihe von Bibliotheken für die Entwicklung von Microservices bietet und fördert es die Verwendung guter Designprinzipien für die Architektur Ihrer Services. Das Go-Kit hilft dabei, sich an die Prinzipien von SOLID, den subjektorientierten Ansatz (DDD) und die von Alistair Cockburn vorgeschlagene hexagonale Architektur oder andere Ansätze aus den Architekturprinzipien von Jeffrey Palermo und der reinen Architektur von Robert C. Martin zu halten . Obwohl das Go-Kit als Paket für die Entwicklung von Microservices entwickelt wurde, eignet es sich auch für die Entwicklung eleganter Monolithen.


Go Kit-Architektur


Die drei wichtigsten Ebenen der Architektur der Anwendung, die mit dem Go-Kit entwickelt wurde, sind:


  • Transportebene
  • Endpunktebene
  • Service-Level

Transportebene


Wenn Sie Mikrodienste für verteilte Systeme erstellen, müssen die Dienste in diesen häufig über verschiedene Transportprotokolle wie HTTP oder gRPC miteinander kommunizieren oder Pub / Sub-Systeme wie NATS verwenden. Die Transportebene im Go-Kit ist an ein bestimmtes Transportprotokoll gebunden (im Folgenden als Transport bezeichnet). Das Go-Kit unterstützt verschiedene Transportvorgänge für Ihren Dienst, z. B. HTTP, gRPC, NATS, AMQP und Thirft ( Sie können auch ein eigenes Transportprotokoll für Ihr Protokoll entwickeln).). Mit dem Go-Kit geschriebene Dienste betonen daher häufig die Implementierung einer bestimmten Geschäftslogik, die nichts über den verwendeten Transport weiß. Sie können also verschiedene Transporte für denselben Dienst verwenden. Zum Beispiel kann ein auf dem Go-Kit geschriebener Dienst gleichzeitig über HTTP und gRPC darauf zugreifen.


Endpunkte


Der Endpunkt oder Endpunkt ist ein grundlegender Baustein für Services und Kunden. Im Go-Kit ist das Hauptkommunikationsmuster RPC. Der Endpunkt wird als separate RPC-Methode dargestellt. Jede Servicemethode im Go-Kit wird in einen Endpunkt konvertiert, sodass die Kommunikation zwischen Server und Client im RCP-Stil möglich ist. Jeder Endpunkt macht die Servicemethode über die Transportschicht verfügbar, die wiederum verschiedene Transportprotokolle wie HTTP oder gRPC verwendet. Ein separater Endpunkt kann mit Hilfe mehrerer Transporte gleichzeitig außerhalb des Diensts platziert werden ( ca. HTTP und gRPC an verschiedenen Ports ).


Dienstleistungen


Geschäftslogik ist in der Service-Schicht implementiert. Mit dem Go-Kit geschriebene Dienste sind als Schnittstellen konzipiert. Die Geschäftslogik in der Serviceschicht enthält den Hauptgeschäftslogikkern, der nichts über die verwendeten Endpunkte oder ein bestimmtes Transportprotokoll wie HTTP oder gRPC oder das Kodieren oder Dekodieren von Anforderungen und Antworten verschiedener Nachrichtentypen wissen muss. Auf diese Weise können Sie sich bei den mit dem Go-Kit geschriebenen Diensten an die reine Architektur halten. Jede Servicemethode wird mithilfe eines Adapters in einen Endpunkt konvertiert und mithilfe eines bestimmten Transports angezeigt. Dank der reinen Architektur kann eine separate Methode mit mehreren Transporten gleichzeitig belichtet werden.


Beispiele


Betrachten wir nun die oben beschriebenen Ebenen am Beispiel einer einfachen Anwendung.


Geschäftslogik im Dienst


Die Geschäftslogik im Service wird mithilfe von Schnittstellen entworfen. Wir betrachten ein Beispiel für einen Auftrag im 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
}

Die Bestellservice-Schnittstelle funktioniert mit der Entität der Subjektdomäne.


// Order represents an ordertype 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 ordertype 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 modeltype 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 Bestellservice-Schnittstelle:


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 Servicetype service struct {
   repository ordersvc.Repository
   logger     log.Logger
}
// NewService creates and returns a new Order service instancefuncNewService(rep ordersvc.Repository, logger log.Logger)ordersvc.Service {
   return &service{
      repository: rep,
      logger:     logger,
   }
}
// Create makes an orderfunc(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 idfunc(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 orderfunc(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
   }
   returnnil
}

RPC-Endpunktabfragen und -antworten


Servicemethoden werden als RPC-Endpunkte bereitgestellt. Wir müssen also die Nachrichtentypen bestimmen ( Anmerkung. DTO - Datentransferobjekt ), die zum Senden und Empfangen von Nachrichten über RPC-Endpunkte verwendet werden. Definieren Sie nun die Struktur für die Arten von Anforderungen und Antworten für RPC-Endpunkte im Bestelldienst:


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

Endpoint Go-Kit für Servicemethoden wie RPC-Endpunkte


Der Kern unserer Geschäftslogik wird vom Rest des Codes getrennt und in die Service-Schicht gerendert, die mithilfe von RPC-Endpunkten verfügbar gemacht wird, die die so genannte Go-Kit-Abstraktion verwenden Endpoint.


So sieht der Endpunkt des Go-Kits aus:


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

Wie oben erwähnt, ist der Endpunkt eine separate RPC-Methode. Jede Servicemethode wird in endpoint.EndpointAdapter konvertiert . Lassen Sie uns die Go-Endpunkte für die Bestellservice-Methoden 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.funcMakeEndpoints(s order.Service)Endpoints {
   return Endpoints{
      Create:       makeCreateEndpoint(s),
      GetByID:      makeGetByIDEndpoint(s),
      ChangeStatus: makeChangeStatusEndpoint(s),
   }
}
funcmakeCreateEndpoint(s order.Service)endpoint.Endpoint {
   returnfunc(ctx context.Context, request interface{})(interface{}, error) {
      req := request.(CreateRequest)
      id, err := s.Create(ctx, req.Order)
      return CreateResponse{ID: id, Err: err}, nil
   }
}
funcmakeGetByIDEndpoint(s order.Service)endpoint.Endpoint {
   returnfunc(ctx context.Context, request interface{})(interface{}, error) {
      req := request.(GetByIDRequest)
      orderRes, err := s.GetByID(ctx, req.ID)
      return GetByIDResponse{Order: orderRes, Err: err}, nil
   }
}
funcmakeChangeStatusEndpoint(s order.Service)endpoint.Endpoint {
   returnfunc(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 eine Schnittstelle als Parameter für die Eingabe und konvertiert sie in eine Go-Kit-Abstraktion, endpoint.Enpointwodurch jede einzelne Servicemethode zu einem Endpunkt wird. Diese Adapterfunktion führt den Vergleich und die Typkonvertierung für Anforderungen durch, ruft die Servicemethode auf und gibt eine Antwortnachricht zurück.


funcmakeCreateEndpoint(s order.Service)endpoint.Endpoint {
   returnfunc(ctx context.Context, request interface{})(interface{}, error) {
      req := request.(CreateRequest)
      id, err := s.Create(ctx, req.Order)
      return CreateResponse{ID: id, Err: err}, nil
   }
}

Den Dienst über HTTP ausschalten


Wir haben unseren Service entwickelt und RPC-Endpunkte beschrieben, um unsere Service-Methoden nach außen zu zeigen. Jetzt müssen wir unseren Dienst nach außen veröffentlichen, damit andere Dienste RCP-Endpunkte anrufen können. Um unseren Dienst außerhalb zu platzieren, müssen wir das Transportprotokoll für unseren Dienst festlegen, nach dem er Anfragen erhält. Das Go-Kit unterstützt verschiedene Transporte, z. B. HTTP, gRPC, NATS, AMQP und Thrift.


Beispielsweise verwenden wir HTTP-Transport für unseren Service. Das Go-Kit-Paket github.com/go-kit/kit/transport/http bietet die Möglichkeit, HTTP-Anforderungen zu bedienen. Die Funktion NewServeraus dem Paket transport/httperstellt einen neuen http-Server, der http.Handlerdie bereitgestellten Endpunkte implementiert und umschließt.


Nachfolgend finden Sie den Code, der die Endpunkte des Go-Kits 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.funcNewService(
   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
}
funcdecodeCreateRequest(_ context.Context, r *http.Request)(request interface{}, err error) {
   var req transport.CreateRequest
   if e := json.NewDecoder(r.Body).Decode(&req.Order); e != nil {
      returnnil, e
   }
   return req, nil
}
funcdecodeGetByIDRequest(_ context.Context, r *http.Request)(request interface{}, err error) {
   vars := mux.Vars(r)
   id, ok := vars["id"]
   if !ok {
      returnnil, ErrBadRouting
   }
   return transport.GetByIDRequest{ID: id}, nil
}
funcdecodeChangeStausRequest(_ context.Context, r *http.Request)(request interface{}, err error) {
   var req transport.ChangeStatusRequest
   if e := json.NewDecoder(r.Body).Decode(&req); e != nil {
      returnnil, e
   }
   return req, nil
}
funcencodeResponse(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)
      returnnil
   }
   w.Header().Set("Content-Type", "application/json; charset=utf-8")
   return json.NewEncoder(w).Encode(response)
}

Wir erstellen http.Handlermit einer Funktion NewServeraus einem Paket transport/http, die uns Endpunkte und Abfrage-Decodierungsfunktionen (Rückgabewert type DecodeRequestFunc func) und Codierungsantworten (zum Beispiel type EncodeReponseFunc func) zur Verfügung stellt.


Folgendes sind Beispiele DecodeRequestFuncund EncodeResponseFunc:


// For decoding request type DecodeRequestFunc func(context.Context, *http.Request)(request interface{}, err error)

// For encoding responsetype EncodeResponseFunc func(context.Context, http.ResponseWriter, interface{})error

Starten Sie den HTTP-Server


Schließlich können wir unseren HTTP-Server zum Verarbeiten von Anforderungen ausführen. Die NewServiceoben beschriebene Funktion implementiert eine Schnittstelle http.Handler, die es uns ermöglicht, sie als HTTP-Server auszuführen:


funcmain() {
   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 Servicevar 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)
   gofunc() {
      c := make(chan os.Signal)
      signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
      errs <- fmt.Errorf("%s", <-c)
   }()
   gofunc() {
      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 läuft unser Dienst und verwendet auf Transportebene das HTTP-Protokoll. Derselbe Dienst kann mit einem anderen Transport gestartet werden, beispielsweise kann der Dienst mithilfe von gRPC oder Apache Thrift nach außen angezeigt werden.


Für den einleitenden Artikel haben wir die Go-Kit-Grundelemente bereits ausreichend verwendet, sie bieten jedoch auch mehr Funktionen zum Erstellen von Systemen mit transparenten, zuverlässigen Mustern, Erkennen von Diensten, Lastverteilung usw. Wir werden diese und andere Dinge in den folgenden Artikeln im Go-Kit besprechen.


Quellcode


Der gesamte Quellcode der Beispiele kann hier auf GitHub eingesehen werden.


Middlewares in Go-Kit


Go-Kit ist für die Verwendung guter Prinzipien des Systemdesigns wie die Trennung in Schichten geeignet. Das Isolieren von Servicekomponenten und Endpunkten ist durch die Verwendung von Middlewares ( ca. Lane Lane Mediator ) möglich. Middlewares im Go-Kit bieten einen leistungsstarken Mechanismus, mit dem Sie Services und Endpunkte umschließen und Funktionen (isolierte Komponenten) hinzufügen können, z. B. Protokollierung, Unterbrechen von Anforderungen, Beschränken der Anzahl von Anforderungen, Lastverteilung oder verteilte Ablaufverfolgung.


Unten ist ein Bild von der Go-Kit- Website , die als typische "Zwiebelarchitektur" mit Middlewares im Go-Kit dargestellt ist:
Bild


Vorsicht vor Spring Boot Mikroservices


Wie beim Go-Kit ist Spring Boot ein Toolkit für die Erstellung von Microservices in der Java-Welt. Im Gegensatz zum Go-Kit ist Spring Boot ein recht ausgereiftes Framework. Viele Java-Entwickler verwenden Spring Boot zum Erstellen von Miroservices mithilfe von Java-Stack mit positiver Rückmeldung von der Verwendung. Einige von ihnen glauben, dass es bei Mikrodienstleistungen nur um die Verwendung von Spring Boot geht. Ich sehe viele Entwicklungsteams, die die Verwendung von Microservices falsch interpretieren, dass sie nur mit Hilfe von Spring Boot und OSS Netflix entwickelt werden können und Mikroservices nicht als Vorlage bei der Entwicklung verteilter Systeme wahrnehmen.


Denken Sie also daran, dass Sie mit einem Toolkit wie einem Go-Kit oder einer Art Rahmen Ihre Entwicklung auf Micro-Sevris als Designmuster ausrichten. Microservices lösen zwar viele Probleme mit der Skalierung sowie mit Befehlen und Systemen, erzeugen aber auch viele Probleme, da die Daten in auf Microservices basierenden Systemen über verschiedene Datenbanken verteilt sind, was manchmal beim Erstellen von Transaktions- oder Datenabfragen zu Problemen führt. Es hängt alles von dem Problem der Domäne und dem Kontext Ihres Systems ab. Das Coole ist, dass das Go-Kit, das als Werkzeug zur Erstellung von Mikrodienstleistungen konzipiert wurde, auch für die Erstellung eleganter Monolithen geeignet ist, die mit einem guten Design der Architektur Ihrer Systeme erstellt werden.


Einige Go-Kit-Funktionen, z. B. Unterbrechung und Einschränkung von Anforderungen, sind auch auf Service-Mesh-Plattformen verfügbar, beispielsweise Istio. Wenn Sie also beispielsweise Istio zum Starten des Mikrosecures verwenden, benötigen Sie möglicherweise nicht einige Dinge aus dem Go-Kit, aber nicht jeder verfügt über ausreichend Kanalbreite, um das Service-Mesh für die Kommunikation zwischen den Diensten zu nutzen eine Ebene und zusätzliche Komplexität.


PS


Der Autor der Übersetzung darf die Meinung des Autors des Originaltextes nicht teilen. Dieser Artikel wurde nur zu Bildungszwecken für die russischsprachige Community Go übersetzt.


UPD Dies ist
auch der erste Artikel im Übersetzungsbereich, und ich würde mich über Feedback zur Übersetzung freuen.

Nur registrierte Benutzer können an der Umfrage teilnehmen. Bitte melden Sie sich an.

Und was benutzt du beim Entwickeln?


Jetzt auch beliebt: