带有Go套件的Go on Microservices:简介

在本文中,我将介绍Go工具包的使用,Go工具包是在Go上创建微服务的一组工具和库。 本文是Go工具包的简介。 在我的博客的第一部分中,示例的源代码可在此处获得


Go越来越多地用于开发现代分布式系统。 在开发基于云的分布式系统时,可能需要在服务中支持各种特定功能,例如:各种传输协议( 例如,transl。HTTP,gRPC等 )以及它们的消息编码格式,RPC可靠性,日志记录,跟踪,指标和分析,中断请求,限制请求数量,集成到基础架构中,甚至描述架构。 Go由于其简单性和“无魔术”的方法而成为一种流行的语言,因此,相比于使用具有很多“幕后魔术”的成熟框架,Go包(例如,标准库)已经更适合开发分布式系统。 我个人[ 反式 Shiju Varghese ]我不支持使用成熟的框架,我更喜欢使用为开发人员提供更多自由的库。 Go工具包填补了Go生态系统中的空白,使创建微服务时可以使用一组库和软件包,从而允许使用良好的原则来设计分布式系统中的单个服务。


图片


Go kit简介


Go kit是一组Go软件包,可轻松构建可靠且受支持的微服务。 Go kit提供了一些库,用于实现透明和可靠的应用程序体系结构的各个组件,并使用诸如日志记录,指标,跟踪,限制和中断在产品上运行微服务所需的请求之类的层。 Go kit很好,因为它具有用于与各种基础结构,消息编码格式和各种传输层进行交互的功能完备的工具。


除了用于开发世界服务的库集之外,它还提供并鼓励使用良好的原则来设计服务的体系结构。 Go工具包可帮助您遵守SOLID原则, Alistair Cockburn提出的面向主题的方法(DDD)和六角形体系结构 ,或Jeffrey Palermo称为“ 洋葱体系结构 ”和Robert C. Martin称为“ 清洁体系结构 ”的任何其他体系结构方法。 尽管Go工具包被设计为一组用于开发微服务的软件包,但它也适用于开发优雅的整体。


建筑围棋套件


使用Go套件开发的应用程序体系结构的三个主要级别是:


  • 运输水平
  • 终点水平
  • 服务水平

运输等级


当您为分布式系统编写微服务时,它们中的服务通常必须使用各种传输协议(例如HTTP或gRPC)或使用pub / sub系统(例如NATS)相互通信。 Go工具包中的传输层与特定的传输协议(以下称为传输)绑定在一起。 Go kit支持各种服务传输,例如:HTTP,gRPC,NATS,AMQP和Thirft( 大约。您也可以为协议开发自己的传输 )。 因此,使用Go工具包编写的服务通常专注于特定业务逻辑的实现,而该逻辑对所使用的传输一无所知,您可以自由地对同一服务使用不同的传输。 例如,使用Go工具包编写的一项服务可以同时通过HTTP和gRPC提供对其的访问。


终点


端点是服务和客户的基本构建块。 在Go kit中,主要的通信模式是RPC。 端点作为一种单独的RPC方法呈现。 Go工具包中的每个服务方法都转换为一个端点,使您可以RCP样式在服务器和客户端之间进行通信。 每个端点都使用传输层公开服务方法,该传输层又使用各种传输协议,例如HTTP或gRPC。 可以使用几种传输方式( 在不同端口上分别使用HTTP和gRPC )同时将一个单独的终结点暴露在服务之外。


服务项目


业务逻辑在服务层中实现。 使用Go套件编写的服务被设计为接口。 服务层中的业务逻辑包含业务逻辑的主要核心,该核心不应对所使用的端点或特定的传输协议(例如HTTP或gRPC)或有关各种消息的请求和响应的编码或解码一无所知。 这将使您在使用Go工具包编写的服务中坚持干净的体系结构。 每种服务方法都使用适配器转换为端点,并使用特定的传输方式暴露在外部。 通过使用干净的体系结构,可以同时使用多个传输设置单个方法。


例子


现在,让我们使用一个简单应用程序的示例查看上述层。


服务中的业务逻辑


服务中的业务逻辑是使用接口设计的。 我们将看一个电子商务订单的例子:


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

订单服务接口可与订单域实体一起使用:


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

在这里,我们实现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 } 

RPC端点的请求和答案


服务方法作为RPC端点公开。 因此,我们需要确定将用于通过RPC端点发送和接收消息的消息类型( 大约每DTO-数据传输对象 )。 现在,让我们为Order服务中的RPC端点定义请求和响应类型的结构:


 // 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工具包端点,用于RPC端点之类的服务方法


我们业务逻辑的核心与其余代码分开,放入服务层,该服务层使用RPC端点公开,该端点使用Go工具包抽象(称为Endpoint )公开。


Go工具包中的端点如下所示:


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

如上所述,端点表示一种单独的RPC方法。 每个服务方法都使用适配器转换为endpoint.Endpoint 。 让我们为Order服务方法创建Go kit端点:


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

端点适配器接受接口作为输入的参数,并将其转换为Go kit endpoint.Enpoint的抽象。使每个单独的服务方法成为端点。 该适配器功能对请求进行比较和类型转换,调用服务方法并返回响应消息。


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

使用HTTP公开服务


我们创建了服务,并描述了RPC端点以公开我们的服务方法。 现在,我们需要在外部发布服务,以便其他服务可以调用RCP端点。 要公开我们的服务,我们需要确定服务的传输协议,根据该协议它将接受请求。 Go kit支持各种传输方式,例如现成的HTTP,gRPC,NATS,AMQP和Thrift。


例如,我们为服务使用HTTP传输。 go kit软件包github.com/go-kit/kit/transport/http提供了处理HTTP请求的功能。 transport/http包中的NewServer函数将创建一个新的http服务器,该服务器将实现http.Handler并包装提供的端点。


以下是将Go kit端点转换为可服务HTTP请求的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) } 

我们使用transport/http包中的NewServer函数创建http.Handler ,该函数提供端点和请求解码功能(返回type DecodeRequestFunc func的值)和响应编码(例如type EncodeReponseFunc func )。


以下是DecodeRequestFuncEncodeResponseFunc示例:


 // 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服务器


最后,我们可以运行HTTP服务器来处理请求。 上面描述的NewService函数实现了http.Handler接口http.Handler该接口允许我们将其作为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) } 

现在,我们的服务已启动,并在传输级别使用HTTP协议。 可以使用其他传输方式启动相同的服务,例如,可以使用gRPC或Apache Thrift公开服务。


对于介绍性文章,我们已经足够使用Go kit原语,但是它还提供了更多功能来创建透明,可靠的模式,服务发现,负载平衡等系统。 在以下文章中,我们将在Go工具包中讨论这些以及其他内容。


源代码


示例的完整源代码可以在GitHub上查看


Go kit中的中间件


Go kit倾向于使用系统设计的良好原则,例如分层。 可以使用中间件( 大约Lane中介器模式 )隔离服务组件和端点。 Go工具包中的中间件提供了一种强大的机制,您可以通过该机制包装服务和端点并添加功能(隔离的组件),例如日志记录,请求中断,限制请求数,负载平衡或分布式跟踪。


下面是Go kit网站上的图片,使用Go kit中的中间件将其描绘为典型的“洋葱架构”:
图片


谨防Spring Boot微服务综合症


像Go工具包一样,Spring Boot是Java世界中的微服务工具箱。 但是,与Go工具包不同,Spring Boot是一个非常成熟的框架。 另外,许多Java开发人员使用Spring Boot在Java堆栈上创建世界服务,并得到使用方面的积极反馈,其中一些人认为微服务仅与使用Spring Boot有关。 我看到许多开发团队误解了微服务的使用,它们只能使用Spring Boot和OSS Netflix进行开发,并且在开发分布式系统时不会将微服务视为一种模式。


因此,请记住,使用一套工具(例如Go工具包或某种框架),您可以将开发方向作为设计模式引导至微型抗体。 尽管微服务解决了命令和系统的许多伸缩性问题,但它也带来了许多问题,因为基于微服务的系统中的数据分散在各种数据库中,这有时在创建事务或数据查询时会产生许多问题。 这完全取决于主题领域和系统环境的问题。 很棒的事情是,Go工具包(设计为用于创建微服务的工具)也适用于创建优雅的整体结构,这些整体结构具有针对您的系统的良好架构设计。


在服务网格平台(例如Istio)上,还可以使用某些Go工具包功能,例如中断和限制请求。 因此,如果您使用Istio之类的工具来启动您的微型服务,则可能不需要Go工具包中的某些工具,但是并不是每个人都有足够的通道宽度来使用服务网格来创建服务间通信,因为这会增加更多一级和额外的复杂性。


聚苯乙烯


翻译的作者可能不会与原始作者有共同的看法,本文仅出于俄语学习目的而被翻译为Go。


UPD
这也是翻译部分的第一篇文章,对于翻译的任何反馈,我将不胜感激。

Source: https://habr.com/ru/post/zh-CN430300/


All Articles