Go 微服务框架对比:Go Micro, Go Kit, Gizmo, Kite

Golang

我最近在墨尔本Golang会议上做了一个关于如何开发微服务和框架的演讲。在这篇文章中,我将与你分享我的知识(另外,这对我也是一个很好的提醒)。

下面是我要比较的框架:

框架简介

Go Micro

我认为最流行的框架之一。有很多博客文章和简单的例子。您可以在medium上关注microhq@MicroHQ以获取Go-Micro中的最新更新。

好吧,什么是 Go Micro?它是一个可插入的RPC框架,用于在Go中编写微服务。开箱即用,您将收到:

  • 服务发现-应用程序自动注册到服务发现系统。
  • 负载平衡-客户端负载平衡,用于平衡服务实例之间的请求。
  • 同步通信-提供请求/响应传输层。
  • 异步通信-内置发布/订阅功能。
  • 消息编码-基于消息的内容类型头的编码/解码。
  • RPC客户机/服务器包-利用上述功能并公开接口来构建微服务。

Go微体系结构可以描述为三层堆栈。

图1. Go Micro 架构

顶层由客户端-服务器模型和服务抽象组成。服务器是用于编写服务的构建块。客户端提供了向服务请求的接口。

底层由以下类型的插件组成:

  • 代理-为异步发布/订阅通信提供消息代理的接口。
  • 编解码器-用于编码/解码消息。支持的格式包括json,bson,protobuf,msgpack等。
  • 注册表-提供服务发现机制(默认为Consul)。
  • 选择器-建立在注册表上的负载平衡抽象。它允许使用诸如随机,轮循,最小康等算法来“选择”服务。
  • 传输-服务之间同步请求/响应通信的接口。
  • Go Micro还提供了Sidecar等功能。这使您可以使用以Go以外的语言编写的服务。 Sidecar提供服务注册,gRPC编码/解码和HTTP处理程序。它支持多种语言。

Go Kit

Go Kit是一个用于在 Go 中构建微服务的编程工具包。与Go Micro不同,它被设计为一个用于导入二进制包的库。

Go Kit遵循简单的规则,例如:

  • 没有全局状态
  • 声明式组合
  • 显式依赖关系
  • 接口即约定
  • 领域驱动设计

在Go Kit中,您可以找到以下的包:

  • 认证 - Basic 认证和JWT认证
  • 传输 - HTTP、Nats、gRPC等等。
  • 日志记录 - 用于结构化服务日志记录的通用接口。
  • 指标 - CloudWatch、Statsd、Graphite 等。
  • 追踪 - Zipkin 和 Opentracing。
  • 服务发现 - Consul、Etcd、Eureka 等等。
  • 断路器 - Hystrix 的 Go 实现。

在 Peter Bourgon 的文章和 幻灯片中,你可以找到关于 Go 工具包最好的描述:

此外,在「Go +微服务」幻灯片中,您将看到一个使用 Go Kit 构建的服务架构示例。为了快速入门,这里有一个服务架构图。

图 2。使用 Go Kit 构建的服务架构示例(原始图片在 「Go + 微服务」 幻灯片)

Gizmo

Gizmo 是《纽约时报》开源的一个微服务工具包。它提供了将服务器和 pubsub组合在一起的包。

功能如下:

  • server -提供两种服务器实现:SimpleServer(over HTTP),RPCServer(在gRPC上)。
  • server/kit - 基于 Go-kit 的包,目前试验阶段。
  • config - 包含配置 JSON 文件的函数,Consul k/v 中的 JSON blob,或环境变量。
  • pubsub - 提供用于发布和使用队列中数据的通用接口。
  • pubsub/pubsubtest - 包含发布者和订阅者接口的测试实现。
  • web - 公开了解析请求查询和有效负载类型的函数。

Pubsub 包提供了与以下驱动:

在我看来,Gizmo 的使用场景在于 Go Micro 和 Go Kit 中间。它不像Go Micro那样是一个完整的『黑匣子』。同时,它也不像 Go Kit 那样原始。它提供更高级的构建组件,如 configpubsub 包。

Kite

Kite 是一个在 Go 微服务框架。它公开了 RPC 客户端和服务器包。创建的服务将自动注册到服务发现系统 Kontrol 中。Kontrol 使用 Kite 构建的,它本身就是一种 Kite 服务。这意味着 Kite 微服务可以在自己的环境中正常工作。如果你需要连接 Kite 微服务到另一个服务发现系统,它将需要定制。这是我不看好此框架的主要原因。

框架对比

我将使用四个类别比较框架:

  • GitHub 统计
  • 文档和示例
  • 用户和社区
  • 代码品质

GitHub statistics

表 1. Go微服务框架统计(2018 年 4 月收集)

文档和代码示例

简单来说,没有框架会提供可靠的文档,通常来说,唯一正式的文档是项目首页的readme。

对 Go Micro 来说很多信息和公告可以在 micro.mumicrohq 看到,还有 @MicroHQ 作为他们的公共媒体。

对 Go Kit来说最好的文档可以在 Peter Bourgon’s blog 找到。最好的示例代码之一可以在 ru-rocker blog 找到。

如果是 Gizmo的话,它的源码提供了最好的文档和代码示例。

综上所述,如果你是NodeJS的忠实用户,期望看到和ExpressJS类似的教程,那你可能要失望了,但是从另一方面来说,这是一个你编写自己教程的好机会。

用户和社区

根据GitHub统计数据,Go Kit是最受欢迎的微服务框架——在这篇文章发布前超过10k颗star。它有很多贡献者(122人)和1000多个 fork。最后,Go Kit得到了DigitalOcean 的支持。

拥有3600多颗star、27个贡献者和385个fork的 Go Micro 位居第二。Go Micro的最大赞助商之一是Sixt

Gizmo 位居第三。超过2200颗star,31个贡献者和137个fork。由《纽约时报》支持和创建。

代码质量

Go Kit在代码质量类别中排名第一。它拥有几乎80%的代码覆盖率和出色的Go评级报告 报告评级。Gizmo 的Go评级报告 也很高。但是它的代码覆盖率只有46%。Go Micro不提供覆盖信息,但它的Go评级报告很高。

微框架示例

好啦,理论的东西讲的差不多了,接下来开始编码。为了更好地理解框架,我创建了三个简单的微服务:

图 3. 实际示例架构

这些是实现一个问候语的服务。当用户将 name 参数传递给服务时,该服务将发送一个问候回应。此外,所有服务都满足以下要求:

  • 服务应在服务发现系统中自动发现
  • 服务应带有运行状况检查的 endpoint
  • 服务应至少支持 HTTP 和 gRPC 传输

对于喜欢阅读源代码的用户。您可以在这里停下来阅读 托管在 GitHub 上的源码

Go 微框架问候器

使用 Go Micro 创建服务首先需要定义 protobuf 描述。接下来,这三个服务都使用了相同的 protobuf 定义。我创建了以下服务描述:

*go-micro-greeter.proto *

syntax = "proto3";

package pb;

service Greeter {
  rpc Greeting(GreetingRequest) returns (GreetingResponse) {}
}

message GreetingRequest {
  string name = 1;
}

message GreetingResponse {
  string greeting = 2;
}

接口定义了一个方法 Greeting。请求中有一个参数 name,响应中有一个参数-greeting

然后我使用修改后的 protoc 从protobuf 生成服务接口。生成器从 Go Micro 中 forked 并进行了修改,以支持框架的某些功能。我在 greeter 服务中把这些连接在一起。此时,服务正在启动并向服务发现系统注册。它只支持 gRPC 传输协议:

*go-micro-greeter-grpc-main.go *

package main

import (
    "log"

    pb "github.com/antklim/go-microservices/go-micro-greeter/pb"
    "github.com/micro/go-micro"
    "golang.org/x/net/context"
)

// greeter 实现问候服务。
type Greeter struct{}

// 问候方法的实现
func (g *Greeter) Greeting(ctx context.Context, in *pb.GreetingRequest, out *pb.GreetingResponse) error {
    out.Greeting = "GO-MICRO Hello " + in.Name
    return nil
}

func main() {
    service := micro.NewService(
        micro.Name("go-micro-srv-greeter"),
        micro.Version("latest"),
    )

    service.Init()

    pb.RegisterGreeterHandler(service.Server(), new(Greeter))

    if err := service.Run(); err != nil {
        log.Fatal(err)
    }
}

要支持 HTTP 传输,我必须添加另一个模块。它将 HTTP 请求映射到 protobuf 定义的请求,并调用 gRPC 服务。然后它将服务响应映射到 HTTP 响应并回复给用户。

go-micro-greeter-http-main.go

package main

import (
    "context"
    "encoding/json"
    "log"
    "net/http"

    proto "github.com/antklim/go-microservices/go-micro-greeter/pb"
    "github.com/micro/go-micro/client"
    web "github.com/micro/go-web"
)

func main() {
    service := web.NewService(
        web.Name("go-micro-web-greeter"),
    )

    service.HandleFunc("/greeting", func(w http.ResponseWriter, r *http.Request) {
        if r.Method == "GET" {
            var name string
            vars := r.URL.Query()
            names, exists := vars["name"]
            if !exists || len(names) != 1 {
                name = ""
            } else {
                name = names[0]
            }

            cl := proto.NewGreeterClient("go-micro-srv-greeter", client.DefaultClient)
            rsp, err := cl.Greeting(context.Background(), &proto.GreetingRequest{Name: name})
            if err != nil {
                http.Error(w, err.Error(), 500)
                return
            }

            js, err := json.Marshal(rsp)
            if err != nil {
                http.Error(w, err.Error(), http.StatusInternalServerError)
                return
            }

            w.Header().Set("Content-Type", "application/json")
            w.Write(js)
            return
        }
    })

    if err := service.Init(); err != nil {
        log.Fatal(err)
    }

    if err := service.Run(); err != nil {
        log.Fatal(err)
    }
}

非常简单和直接。很多事情都是由 Go Micro 在幕后处理的,例如在服务发现系统中注册。如果你自己创建一个纯净的 HTTP 服务器,这些开发起来还是挺费劲的 。

Go Kit greeter

完成Go Micro后,我接着写Go Kit服务的实现。我花了很多时间通读Go Kit 仓库中提供的示例代码。理解端点 Endpoint 的概念花了我很多时间。下一个花了我很多时间的难题是怎么去写服务发现注册器。我找到一个好的例子才把它实现出来了。 example.

最后,我为以下内容创建了四个包:

  • 服务逻辑实现。
  • 与传输无关的服务端点。
  • 与传输特定的端点(gRPC, HTTP)
  • 服务发现注册器

go-kit-greeter-service.go

package greeterservice

// Service 描述了 greetings 这个服务
type Service interface {
    Health() bool
    Greeting(name string) string
}

// GreeterService  是 Service 接口的实现
type GreeterService struct{}

// Service 的 Health 接口实现
func (GreeterService) Health() bool {
    return true
}

// Service 的 Greeting 接口实现
func (GreeterService) Greeting(name string) (greeting string) {
    greeting = "GO-KIT Hello " + name
    return
}

如您所见,代码没有任何依赖关系。它只是实现逻辑。下面的代码片段是端点Endpoint的定义:

go-kit-greeter-endpoints.go

package greeterendpoint

import (
    "context"

    "github.com/go-kit/kit/log"

    "github.com/antklim/go-microservices/go-kit-greeter/pkg/greeterservice"
    "github.com/go-kit/kit/endpoint"
)


type (
    // Endpoints 收集构成 Greeting Service 的所有端点。
    // 它应该被用作助手 struct ,将所有端点收集到一个参数中。
    Endpoints struct {
        // Consul 用这个端点做健康检查(Health Check)
        HealthEndpoint   endpoint.Endpoint
        GreetingEndpoint endpoint.Endpoint
    }
    HealthRequest struct {
    }
    HealthResponse struct {
        Healthy bool  `json:"health,omitempty"`
        Err     error `json:"err,omitempty"`
    }
    GreetingRequest struct {
        Name string `json:"name,omitempty"`
    }
    GreetingResponse struct {
        Greeting string `json:"greeting,omitempty"`
        Err      error  `json:"err,omitempty"`
    }
    // Failer是一个应该由响应类型实现的接口。
    // 响应编码器可以检查响应是否是一个 Failer。
    // 如果响应是一个 Failer, 那么就说明响应已经失败了,根据错误使用单独的写入路径对响应进行编码。
    Failer interface {
        Failed() error
    }
)

// MakeServiceEndpoints 返回服务端点, 将所有已提供的中间件连接起来
func MakeServerEndpoints(s greeterservice.Service, logger log.Logger) Endpoints {
    var healthEndpoint endpoint.Endpoint
    {
        healthEndpoint = MakeHealthEndpoint(s)
        healthEndpoint = LoggingMiddleware(log.With(logger, "method", "Health"))(healthEndpoint)
    }

    var greetingEndpoint endpoint.Endpoint
    {
        greetingEndpoint = MakeGreetingEndpoint(s)
        greetingEndpoint = LoggingMiddleware(log.With(logger, "method", "Greeting"))(greetingEndpoint)
    }

    return Endpoints{
        HealthEndpoint:   healthEndpoint,
        GreetingEndpoint: greetingEndpoint,
    }
}

// MakeHealthEndpoints 构造一个 Health 端点,将服务包装为一个端点
func MakeHealthEndpoint(s greeterservice.Service) endpoint.Endpoint {
    return func(ctx context.Context, request interface{}) (response interface{}, err error) {
        healthy := s.Health()
        return HealthResponse{Healthy: healthy}, nil
    }
}

// MakeGreetingEndpoints 构造一个 Greeter 端点,将 Greeting 服务包装为一个端点
func MakeGreetingEndpoint(s greeterservice.Service) endpoint.Endpoint {
    return func(ctx context.Context, request interface{}) (response interface{}, err error) {
        req := request.(GreetingRequest)
        greeting := s.Greeting(req.Name)
        return GreetingResponse{
            Greeting: greeting,
        }, nil
    }
}

// 实现 Failer.Failed 接口
func (r HealthResponse) Failed() error {
    return r.Err
}

// 实现 Failer.Failed 接口
func (r GreetingResponse) Failed() error {
    return r.Err
}

定义服务和端点后,我们接下来通过不同的传输协议暴露端点。我从HTTP开始:

go-kit-greeter-http.go

package greetertransport

import (
    "context"
    "encoding/json"
    "errors"
    "net/http"

    "github.com/antklim/go-microservices/go-kit-greeter/pkg/greeterendpoint"
    "github.com/go-kit/kit/log"
    httptransport "github.com/go-kit/kit/transport/http"
    "github.com/gorilla/mux"
)

var (
    // 当缺少预期的路径变量时,将返回 ErrBadRouting。
    ErrBadRouting = errors.New("inconsistent mapping between route and handler")
)

// NewHTTPHandler返回一个使一组端点在预定义路径上可用的HTTP处理程序。
func NewHTTPHandler(endpoints greeterendpoint.Endpoints, logger log.Logger) http.Handler {
    m := mux.NewRouter()
    options := []httptransport.ServerOption{
        httptransport.ServerErrorEncoder(encodeError),
        httptransport.ServerErrorLogger(logger),
    }
    // GET /health         获取服务健康信息
    // GET /greeting?name  获取 greeting

    // NewServer 方法需要端点,解码器,编码器作为参数
    m.Methods("GET").Path("/health").Handler(httptransport.NewServer(endpoints.HealthEndpoint, DecodeHTTPHealthRequest, EncodeHTTPGenericResponse, options...))
    m.Methods("GET").Path("/greeting").Handler(httptransport.NewServer(endpoints.GreetingEndpoint, DecodeHTTPGreetingRequest, EncodeHTTPGenericResponse, options...))
    return m
}

func EncodeHTTPGenericResponse(ctx context.Context, writer http.ResponseWriter, response interface{}) error {
    if f, ok := response.(greeterendpoint.Failer); ok && f.Failed() != nil {
        encodeError(ctx, f.Failed(), writer)
        return nil
    }
    writer.Header().Set("Content-Type", "application/json; charset=utf-8")
    return json.NewEncoder(writer).Encode(response)
}

// 解码 Health HTTP 请求的方法
func DecodeHTTPHealthRequest(_ context.Context, _ *http.Request) (interface{}, error) {
    return greeterendpoint.HealthRequest{}, nil
}

// 解码 Greeting HTTP 请求的方法
func DecodeHTTPGreetingRequest(_ context.Context, r *http.Request) (interface{}, error) {
    vars := r.URL.Query()
    names, exists := vars["name"]
    if !exists || len(names) != 1 {
        return nil, ErrBadRouting
    }
    req := greeterendpoint.GreetingRequest{Name: names[0]}
    return req, nil
}

// errorWrapper 将 error 封装为一个 json 结构体方便转换为 json
type errorWrapper struct {
    Error string `json:"error"`
}

// 编码错误的方法
func encodeError(_ context.Context, err error, w http.ResponseWriter) {
    w.WriteHeader(err2code(err))
    json.NewEncoder(w).Encode(errorWrapper{Error: err.Error()})
}

// err2code 函数将 error 转换为对应的 http 状态码
func err2code(err error) int {
    switch err {
    default:
        return http.StatusInternalServerError
    }
}

在我们开始编写 gRPC 端点的实现之前,我不需要 protobuf 定义。我复制了Go Micro服务的 protobuf 定义。但是在Go Kit的例子中,我使用默认的服务生成器来创建服务接口。

go-greeter-gen.sh

#!/usr/bin/env sh

protoc greeter.proto --go_out=plugins=grpc:.

go-kit-grpc.go

package greetertransport

import (
    "context"

    "github.com/antklim/go-microservices/go-kit-greeter/pb"
    "github.com/antklim/go-microservices/go-kit-greeter/pkg/greeterendpoint"
    "github.com/go-kit/kit/log"
    grpctransport "github.com/go-kit/kit/transport/grpc"
    oldcontext "golang.org/x/net/context"
)

type (
    grpcServer struct {
        greeter grpctransport.Handler
    }
)

// NewGRPCServer 使一组端点可用作 gRPC Greeting 服务器。
func NewGRPCServer(endpoints greeterendpoint.Endpoints, logger log.Logger) pb.GreeterServer {
    options := []grpctransport.ServerOption{
        grpctransport.ServerErrorLogger(logger),
    }

    return &grpcServer{greeter: grpctransport.NewServer(endpoints.GreetingEndpoint, decodeGRPCGreetingRequest, encodeGRPCGreetingResponse, options...)}
}

// encodeGRPCGreetingResponse 是一个 transport/grpc.EncodeResponseFunc 将用户域
// 问Greeting响应转换为 gRPC Greeting 响应。
func encodeGRPCGreetingResponse(i context.Context, i2 interface{}) (response interface{}, err error) {
    res := response.(greeterendpoint.GreetingResponse)
    return &pb.GreetingResponse{Greeting: res.Greeting}, nil
}

// decodeGRPCGreetingRequest 是一个 transport/grpc.DecodeRequestFunc 将 gRPC Greeting 请求转换为用户域 Greeting 请求
func decodeGRPCGreetingRequest(context context.Context, grpcReq interface{}) (request interface{}, err error) {
    req := grpcReq.(*pb.GreetingRequest)
    return greeterendpoint.GreetingRequest{Name: req.Name}, nil
}

// 实现 GreeterService.Greeting 接口
func (s *grpcServer) Greeting(ctx context.Context, req *pb.GreetingRequest) (*pb.GreetingResponse, error) {
    _, res, err := s.greeter.ServeGRPC(ctx, req)
    if err != nil {
        return nil, err
    }
    return res.(*pb.GreetingResponse), nil
}

最后,我注册了服务自动发现器:

go-kit-sd.go

package greetersd

import (
    "math/rand"
    "os"
    "strconv"
    "time"

    "github.com/go-kit/kit/log"
    "github.com/go-kit/kit/sd"
    consulsd "github.com/go-kit/kit/sd/consul"
    "github.com/hashicorp/consul/api"
)

// ConsulRegister 方法
func ConsulRegister(consulAddress string,
    consulPort string,
    advertiseAddress string,
    advertisePort string) (registar sd.Registrar) {

    // 日志相关
    var logger log.Logger
    {
        logger = log.NewLogfmtLogger(os.Stderr)
        logger = log.With(logger, "ts", log.DefaultTimestampUTC)
        logger = log.With(logger, "caller", log.DefaultCaller)
    }

    rand.Seed(time.Now().UTC().UnixNano())

    // 服务发现域。在本例中,我们使用 Consul.
    var client consulsd.Client
    {
        consulConfig := api.DefaultConfig()
        consulConfig.Address = consulAddress + ":" + consulPort
        consulClient, err := api.NewClient(consulConfig)
        if err != nil {
            logger.Log("err", err)
            os.Exit(1)
        }
        client = consulsd.NewClient(consulClient)
    }

    check := api.AgentServiceCheck{
        HTTP:     "http://" + advertiseAddress + ":" + advertisePort + "/health",
        Interval: "10s",
        Timeout:  "1s",
        Notes:    "Basic health checks",
    }

    port, _ := strconv.Atoi(advertisePort)
    num := rand.Intn(100) // to make service ID unique
    asr := api.AgentServiceRegistration{
        ID:      "go-kit-srv-greeter-" + strconv.Itoa(num), //unique service ID
        Name:    "go-kit-srv-greeter",
        Address: advertiseAddress,
        Port:    port,
        Tags:    []string{"go-kit", "greeter"},
        Check:   &check,
    }
    registar = consulsd.NewRegistrar(client, &asr, logger)
    return
}

我在服务启动程序中将它们合并在一起:

go-kit-service-starter.go

package main

import (
    "flag"
    "fmt"
    "net"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "text/tabwriter"

    "github.com/antklim/go-microservices/go-kit-greeter/pb"
    "google.golang.org/grpc"

    "github.com/antklim/go-microservices/go-kit-greeter/pkg/greeterendpoint"
    "github.com/antklim/go-microservices/go-kit-greeter/pkg/greetersd"
    "github.com/antklim/go-microservices/go-kit-greeter/pkg/greeterservice"
    "github.com/antklim/go-microservices/go-kit-greeter/pkg/greetertransport"

    "github.com/go-kit/kit/log"
    "github.com/oklog/oklog/pkg/group"
)

func main() {
    fs := flag.NewFlagSet("greetersvc", flag.ExitOnError)
    var (
        debugAddr  = fs.String("debug.addr", ":9100", "Debug and metrics listen address")
        consulAddr = fs.String("consul.addr", "", "Consul Address")
        consulPort = fs.String("consul.port", "8500", "Consul Port")
        httpAddr   = fs.String("http.addr", "", "HTTP Listen Address")
        httpPort   = fs.String("http.port", "9110", "HTTP Listen Port")
        grpcAddr   = fs.String("grpc-addr", ":9120", "gRPC listen address")
    )
    fs.Usage = usageFor(fs, os.Args[0]+" [flags]")
    fs.Parse(os.Args[1:])

    var logger log.Logger
    {
        logger = log.NewLogfmtLogger(os.Stderr)
        logger = log.With(logger, "ts", log.DefaultTimestampUTC)
        logger = log.With(logger, "caller", log.DefaultCaller)
    }

    var service greeterservice.Service
    {
        service = greeterservice.GreeterService{}
        service = greeterservice.LoggingMiddleware(logger)(service)
    }

    var (
        endpoints   = greeterendpoint.MakeServerEndpoints(service, logger)
        httpHandler = greetertransport.NewHTTPHandler(endpoints, logger)
        registar    = greetersd.ConsulRegister(*consulAddr, *consulPort, *httpAddr, *httpPort)
        grpcServer  = greetertransport.NewGRPCServer(endpoints, logger)
    )

    var g group.Group
    {
        // The debug listener mounts the http.DefaultServeMux, and serves up
        // stuff like the Go debug and profiling routes, and so on.
        debugListener, err := net.Listen("tcp", *debugAddr)
        if err != nil {
            logger.Log("transport", "debug/HTTP", "during", "Listen", "err", err)
            os.Exit(1)
        }
        g.Add(func() error {
            logger.Log("transport", "debug/HTTP", "addr", *debugAddr)
            return http.Serve(debugListener, http.DefaultServeMux)
        }, func(error) {
            debugListener.Close()
        })
    }
    {
        // The service discovery registration.
        g.Add(func() error {
            logger.Log("transport", "HTTP", "addr", *httpAddr, "port", *httpPort)
            registar.Register()
            return http.ListenAndServe(":"+*httpPort, httpHandler)
        }, func(error) {
            registar.Deregister()
        })
    }
    {
        // The gRPC listener mounts the Go kit gRPC server we created.
        grpcListener, err := net.Listen("tcp", *grpcAddr)
        if err != nil {
            logger.Log("transport", "gRPC", "during", "Listen", "err", err)
            os.Exit(1)
        }
        g.Add(func() error {
            logger.Log("transport", "gRPC", "addr", *grpcAddr)
            baseServer := grpc.NewServer()
            pb.RegisterGreeterServer(baseServer, grpcServer)
            return baseServer.Serve(grpcListener)
        }, func(error) {
            grpcListener.Close()
        })
    }
    {
        // This function just sits and waits for ctrl-C.
        cancelInterrupt := make(chan struct{})
        g.Add(func() error {
            c := make(chan os.Signal, 1)
            signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
            select {
            case sig := <-c:
                return fmt.Errorf("received signal %s", sig)
            case <-cancelInterrupt:
                return nil
            }
        }, func(error) {
            close(cancelInterrupt)
        })
    }
    logger.Log("exit", g.Run())
}

func usageFor(fs *flag.FlagSet, short string) func() {
    return func() {
        fmt.Fprintf(os.Stderr, "USAGE\n")
        fmt.Fprintf(os.Stderr, "  %s\n", short)
        fmt.Fprintf(os.Stderr, "\n")
        fmt.Fprintf(os.Stderr, "FLAGS\n")
        w := tabwriter.NewWriter(os.Stderr, 0, 2, 2, ' ', 0)
        fs.VisitAll(func(f *flag.Flag) {
            fmt.Fprintf(w, "\t-%s %s\t%s\n", f.Name, f.DefValue, f.Usage)
        })
        w.Flush()
        fmt.Fprintf(os.Stderr, "\n")
    }
}

如你所见,我在几个地方使用了日志中间件。它允许我将日志逻辑与 service/endpoints 工作流区分开。

go-kit-greeter-service-middleware.go

package greeterservice

import (
    "time"

    "github.com/go-kit/kit/log"
)

// ServiceMiddleware 描述服务中间件
type ServiceMiddleware func(Service) Service

// LoggingMiddleware 将 logger 作为依赖项并返回 ServiceMiddleware  
func LoggingMiddleware(logger log.Logger) ServiceMiddleware {
    return func(next Service) Service {
        return loggingMiddleware{next, logger}
    }
}

type loggingMiddleware struct {
    Service
    logger log.Logger
}

func (m loggingMiddleware) Health() (healthy bool) {
    defer func(begin time.Time) {
        m.logger.Log(
            "method", "Health",
            "healthy", healthy,
            "took", time.Since(begin),
        )
    }(time.Now())
    healthy = m.Service.Health()
    return
}

func (m loggingMiddleware) Greeting(name string) (greeting string) {
    defer func(begin time.Time) {
        m.logger.Log(
            "method", "Greeting",
            "name", name,
            "greeting", greeting,
            "took", time.Since(begin),
        )
    }(time.Now())
    greeting = m.Service.Greeting(name)
    return
}

go-kit-greeter-endpoints-middleware.go

package greeterendpoint

import (
    "context"
    "time"

    "github.com/go-kit/kit/endpoint"
    "github.com/go-kit/kit/log"
)

// 日志中间件返回 endpoint 中间件,记录每次调用的持续时间,
// 以及产生的错误(如果有)。
func LoggingMiddleware(logger log.Logger) endpoint.Middleware {
    return func(next endpoint.Endpoint) endpoint.Endpoint {
        return func(ctx context.Context, request interface{}) (response interface{}, err error) {
            defer func(begin time.Time) {
                logger.Log("transport_error", err, "took", time.Since(begin))
            }(time.Now())
            return next(ctx, request)
        }
    }
}

Gizmo greeter

我以类似 Go Kit. 的方式创建了 Gizmo 服务。我为服务、端点、传输和服务发现注册器定义了四个包。

服务实现和发现注册器与 Go-Kit 服务共享相同的代码。但是 endpoints 定义和传输实现必须根据 Gizmo 特性来完成。

gizmo-greeter-endpoints.go

package greeterendpoint

import (
    "net/http"

    ocontext "golang.org/x/net/context"

    "github.com/NYTimes/gizmo/server"
    "github.com/antklim/go-microservices/gizmo-greeter/pkg/greeterservice"
)

// 收集组成 greeter 的所有 endpoints
type Endpoints struct {
    HealthEndpoint   server.JSONContextEndpoint
    GreetingEndpoint server.JSONContextEndpoint
}

// MakeServerEndpoints 返回服务 Endoints
func MakeServerEndpoints(s greeterservice.Service) Endpoints {
    healthEndpoint := MakeHealthEndpoint(s)
    greetingEndpoint := MakeGreetingEndpoint(s)

    return Endpoints{
        HealthEndpoint:   healthEndpoint,
        GreetingEndpoint: greetingEndpoint,
    }
}

// MakeHealthEndpoint 构建一个健康监控 endpoint
func MakeHealthEndpoint(s greeterservice.Service) server.JSONContextEndpoint {
    return func(ctx ocontext.Context, r *http.Request) (int, interface{}, error) {
        healthy := s.Health()
        return http.StatusOK, HealthResponse{Healthy: healthy}, nil
    }
}

// MakeGreetingEndpoint 构建一个问候的 endpoint.
func MakeGreetingEndpoint(s greeterservice.Service) server.JSONContextEndpoint {
    return func(ctx ocontext.Context, r *http.Request) (int, interface{}, error) {
        vars := r.URL.Query()
        names, exists := vars["name"]
        if !exists || len(names) != 1 {
            return http.StatusBadRequest, errorResponse{Error: "query parameter 'name' required"}, nil
        }
        greeting := s.Greeting(names[0])
        return http.StatusOK, GreetingResponse{Greeting: greeting}, nil
    }
}

// HealthRequest 收集 Health 方法的请求参数
type HealthRequest struct{}

// HealthResponse 收集 Health 方法的响应值
type HealthResponse struct {
    Healthy bool `json:"healthy,omitempty"`
}

// GreetingRequest 收集问候语方法的请求参数
type GreetingRequest struct {
    Name string `json:"name,omitempty"`
}

// GreetingResponse 收集问候语方法的响应值
type GreetingResponse struct {
    Greeting string `json:"greeting,omitempty"`
}

type errorResponse struct {
    Error string `json:"error"`
}

如您所见,代码类似于 Go-Kit。主要区别在于应该返回的接口类型:

gizmo-greeter-http.go

package greetertransport

import (
    "context"

    "github.com/NYTimes/gizmo/server"
    "google.golang.org/grpc"

    "errors"
    "net/http"

    "github.com/NYTimes/gziphandler"
    pb "github.com/antklim/go-microservices/gizmo-greeter/pb"
    "github.com/antklim/go-microservices/gizmo-greeter/pkg/greeterendpoint"
    "github.com/sirupsen/logrus"
)

type (
    // TService 将实现 server.RPCService 并处理对服务器的所有请求。
    TService struct {
        Endpoints greeterendpoint.Endpoints
    }

    // Config 包含 JSONService 所需的所有配置的结构。
    Config struct {
        Server *server.Config
    }
)

// NewTService 将用给定配置实例化 RPCService
func NewTService(cfg *Config, endpoints greeterendpoint.Endpoints) *TService {
    return &TService{Endpoints: endpoints}
}

// Prefix 返回此服务中所有端点使用的字符串前缀
func (s *TService) Prefix() string {
    return ""
}

// Service 向 TService 提供要服务的描述和实现
func (s *TService) Service() (*grpc.ServiceDesc, interface{}) {
    return &pb.Greeter_serviceDesc, s
}

// 中间件提供了一个 http.Handler 钩子,它包装了所有请求。
// 在这个实现中,我们使用 GzipHandler 中间件来压缩我们的响应。
func (s *TService) Middleware(h http.Handler) http.Handler {
    return gziphandler.GzipHandler(h)
}

// ContextMiddleware 为所有请求提供一个 server.ContextHAndler 钩子。
// 如果需要修饰请求上下文,这可能很方便。
func (s *TService) ContextMiddleware(h server.ContextHandler) server.ContextHandler {
    return h
}

// JSONMiddleware 为所有请求提供一个 JSONEndpoint 钩子。
//  我们使用它来提供日志记录和检查错误,并提供一般响应。
func (s *TService) JSONMiddleware(j server.JSONContextEndpoint) server.JSONContextEndpoint {
    return func(ctx context.Context, r *http.Request) (int, interface{}, error) {

        status, res, err := j(ctx, r)
        if err != nil {
            server.LogWithFields(r).WithFields(logrus.Fields{
                "error": err,
            }).Error("problems with serving request")
            return http.StatusServiceUnavailable, nil, errors.New("sorry, this service is unavailable")
        }

        server.LogWithFields(r).Info("success!")
        return status, res, nil
    }
}

// ContextEndpoints may be needed if your server has any non-RPC-able
// endpoints. In this case, we have none but still need this method to
// satisfy the server.RPCService interface.
func (s *TService) ContextEndpoints() map[string]map[string]server.ContextHandlerFunc {
    return map[string]map[string]server.ContextHandlerFunc{}
}

// JSONEndpoints is a listing of all endpoints available in the TService.
func (s *TService) JSONEndpoints() map[string]map[string]server.JSONContextEndpoint {
    return map[string]map[string]server.JSONContextEndpoint{
        "/health": map[string]server.JSONContextEndpoint{
            "GET": s.Endpoints.HealthEndpoint,
        },
        "/greeting": map[string]server.JSONContextEndpoint{
            "GET": s.Endpoints.GreetingEndpoint,
        },
    }
}

gizmo-greeter-grpc.go

package greetertransport

import (
    pb "github.com/antklim/go-microservices/gizmo-greeter/pb"
    ocontext "golang.org/x/net/context"
)

// Greeting implementation of the gRPC service.
func (s *TService) Greeting(ctx ocontext.Context, r *pb.GreetingRequest) (*pb.GreetingResponse, error) {
    return &pb.GreetingResponse{Greeting: "Hola Gizmo RPC " + r.Name}, nil
}

Go-Kit 与 Gizmo 只要区别在于传输实现。Gizmo 提供了几种您可以使用的服务类型。我所要做的就是将 HTT P路径映射到端点定义。底层 HTTP 请求/响应处理由 Gizmo 处理。

结论

Go Micro是启动微服务系统的最快方法。 框架提供了许多功能。 因此,您无需重新发明轮子。 但是,这种舒适性和速度会带来牺牲–灵活性。 更改或更新系统部件并不像Go Kit那样容易。 并且将gRPC强制为默认通信类型。

您可能需要一些时间来熟悉Go Kit。 它需要你具备 Go 功能的丰富知识和软件架构方面的经验。 灵活是他的另一个优势,没有框架限制, 所有部件均可独立更改和更新。

Gizmo位于Go Micro和Go Kit之间。 它提供了一些更高级别的抽象,例如Service包。 但是缺少文档和示例,这意味着我不得不通读源代码以了解不同服务类型的工作方式。 使用Gizmo比使用Go Kit容易。 但是它不像Go Micro那样流畅。


今天就这些了。 谢谢阅读。 请查看微服务 code repository如果您对Go和微服务框架有任何经验,请在下面的评论中分享。

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://medium.com/seek-blog/microservic...

译文地址:https://learnku.com/go/t/36973

本帖已被设为精华帖!
本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
讨论数量: 4
yourself

go kit 严格上其实算不上框架,更多是提供了一个思想。

4年前 评论
playmaker

go-zero 呢?

2年前 评论
Abser 2年前

开发微服务可以参考一下这个最佳实践 github.com/janrs-io/Jgrpc 这个把devops也考虑进去了。基于kubernetes跟Istio的。可以用go单独开发,也可以用其他语言开发。

11个月前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!