Go kit是一个流行的 Go 微服务框架。我发现它非常有趣,但缺乏学习者可以遵循的清晰和详细的示例。当我尝试实现stringsvc3时,官方的 hello-world 教程stringsvc真的让我很困惑。经过一番挣扎,我意识到这个例子想要做的只是“模拟”一个API 网关,这在现实世界中有点不切实际。因此,我将stringsvc3与apigateway结合起来,打造了一个更实用的微服务应用。如果你想用 Go kit 写一个有用的演示,我希望这篇文章能对你有所帮助。

在本文中,我将重点介绍 API 网关的实现,而不是 Go kit 中的端点、传输或服务等基本概念。

基于服务发现的API网关

服务发现是微服务架构的重要组成部分。简而言之,我们不需要费心选择使用哪个服务实例,因为服务发现系统会在后台执行此操作。

在这个例子中,我将使用Consul来实现一个非常简单的客户端服务发现系统。

服务注册
proxying.gomain.go
// We don't need this anymore.
// svc = proxyingMiddleware(context.Background(), *proxy, logger)(svc)

然后,我们可以使用 Go kit 的 sd 包注册 stringsvc。

package main

import (
    ....

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

func main() {
    ...

    // Build consul client and register services.
    // Specify the information of an instance.
    asr := api.AgentServiceRegistration{
        // Every service instance must have an unique ID.
        ID:      fmt.Sprintf("%v%v/%v", host, listen, prefix),
        Name:    serviceName,
        // These two values are the location of an instance.
        Address: host,
        Port:    port,
    }
    consulConfig := api.DefaultConfig()
    // We can get the address of consul server from environment variale or a config file.
    if len(consulServer) > 0 {
        consulConfig.Address = consulServer
    }
    consulClient, err := api.NewClient(consulConfig)
    if err != nil {
        logger.Log("err", err)
        os.Exit(1)
    }
    sdClient := consul.NewClient(consulClient)
    registar := consul.NewRegistrar(sdClient, &asr, logger)
    registar.Register()
    // According to the official doc of Go kit, 
    // it's important to call registar.Deregister() before the program exits.
    defer registar.Deregister()

    ...
}

在服务注册表 Consul 中注册服务非常简单。你可以做一些额外的配置,比如服务的一些标签。进一步阅读:Go consul API 的 Godoc。

稍后我将使用 Docker Compose 部署此演示,所以现在我想构建一个 stringsvc 的 Docker 映像(我已将此应用程序转换为 Go 模块项目,以便更方便地管理依赖项)。

FROM golang:latest as builder

ENV GO111MODULE=on

ENV GOPROXY=https://goproxy.cn,direct

RUN mkdir /app

WORKDIR /app

COPY . .

RUN CGO_ENABLED=0 GOOS=linux go build -o out

FROM alpine:latest

RUN mkdir /app

WORKDIR /app

COPY --from=builder /app/out .

CMD ["./out"]
服务发现客户端 - API 网关
stringclientmain.go
    // Build instancer.
    consulConfig := api.DefaultConfig()
    if len(consulServer) > 0 {
        consulConfig.Address = consulServer
    }
    consulClient, err := api.NewClient(consulConfig)
    if err != nil {
        logger.Log("err", err)
        os.Exit(1)
    }
    client := consul.NewClient(consulClient)
    instancer := consul.NewInstancer(client, logger, serviceName, []string{}, true)
consul.NewInstancertagspassingOnlytrue

最后,我们只需要为 stringsvc 创建一些端点并运行客户端。

    // uppercase endpoint
    // Create an endpointer that subscribes to the instancer.
    uppercaseEndpointer := sd.NewEndpointer(instancer, serviceFactoryBuilder(uppercasePath, "POST", encodeRequest, decodeResponseFuncBuilder(uppercaseResponse{})), logger)
    // Use round-robin load balancing.
    // Set retry policy.
    uppercaseEndpoint := lb.Retry(3, 3*time.Second, lb.NewRoundRobin(uppercaseEndpointer))
    http.Handle(uppercasePath, httptransport.NewServer(
        uppercaseEndpoint,
        decodeRequestFuncBuilder(uppercaseRequest{}),
        encodeResponse,
    ))
    // count endpoint
    countEndPointer := sd.NewEndpointer(instancer, serviceFactoryBuilder(countPath, "POST", encodeRequest, decodeResponseFuncBuilder(countResponse{})), logger)
    countEndPoint := lb.Retry(3, 3*time.Second, lb.NewRoundRobin(countEndPointer))
    http.Handle(countPath, httptransport.NewServer(
        countEndPoint,
        decodeRequestFuncBuilder(countRequest{}),
        encodeResponse,
    ))

    logger.Log("err", http.ListenAndServe(":8080", nil))
sd.NewEndpointer
func serviceFactoryBuilder(path string, method string, enc httptransport.EncodeRequestFunc, dec httptransport.DecodeResponseFunc) sd.Factory {
    // instance (host:port) is the location of an instance.
    return func(instance string) (e endpoint.Endpoint, closer io.Closer, err error) {
        httpPrefix := "http://"
        if !strings.HasPrefix(instance, httpPrefix) {
            instance = httpPrefix + instance
        }

        tgt, err := url.Parse(instance)
        if err != nil {
            return nil, nil, err
        }
        tgt.Path = path

        return httptransport.NewClient(method, tgt, enc, dec).Endpoint(), nil, nil
    }
}

当然,使用上一节中相同的 Dockerfile 将客户端程序构建到 Docker 映像中。

部署和测试

就像我之前说的,我会使用 Docker Compose 来部署这个演示。

version: "3.7"

services:
  consul:
    image: consul
    command: agent -server -bootstrap -ui -client=0.0.0.0
    ports:
      - 8500:8500
      - 8600:8600/udp
    networks: 
      - gokit
  stringsvc1:
    image: stringsvc
    depends_on: 
      - consul
    ports:
      - 8001
    networks:
      - gokit
  stringsvc2:
    image: stringsvc
    depends_on: 
      - consul
    ports:
      - 8002
    networks:
      - gokit
  stringsvc3:
    image: stringsvc
    depends_on: 
      - consul
    ports:
      - 8003
    networks:
      - gokit
  stringclient:
    image: stringclient
    depends_on: 
      - consul
    ports:
      - 8080:8080
    networks:
      - gokit

networks:
  gokit:
stringsvc1stringsvc2stringsvc3
$ curl -d '{"s": "foo"}' http://localhost:8080/stringsvc/uppercase
{"v":"FOO"}

$ curl -d '{"s": "foo"}' http://localhost:8080/stringsvc/count
{"v":"3"}

伟大的!有用!在结束本文之前,我还想向您展示一件事:

$ for s in foo bar baz ; do curl -d"{\"s\":\"$s\"}" localhost:8080/stringsvc/uppercase ; done
{"v":"FOO"}
{"v":"BAR"}
{"v":"BAZ"}

如果我们查看日志,我们会发现如下内容:

stringsvc2_1    | listen=:8002 caller=logging.go:22 method=uppercase input=foo output=FOO err=null took=629ns
stringsvc3_1    | listen=:8003 caller=logging.go:22 method=uppercase input=bar output=BAR err=null took=967ns
stringsvc1_1    | listen=:8001 caller=logging.go:22 method=uppercase input=baz output=BAZ err=null took=646ns

相同服务的 3 个实例被一个一个调用,这是因为我们在创建端点时使用了循环负载平衡策略。

uppercaseEndpoint := lb.Retry(3, 3*time.Second, lb.NewRoundRobin(uppercaseEndpointer))

在此处阅读完整的源代码。