Go kit是一个流行的 Go 微服务框架。我发现它非常有趣,但缺乏学习者可以遵循的清晰和详细的示例。当我尝试实现stringsvc3时,官方的 hello-world 教程stringsvc真的让我很困惑。经过一番挣扎,我意识到这个例子想要做的只是“模拟”一个API 网关,这在现实世界中有点不切实际。因此,我将stringsvc3与apigateway结合起来,打造了一个更实用的微服务应用。如果你想用 Go kit 写一个有用的演示,我希望这篇文章能对你有所帮助。
基于服务发现的API网关在本文中,我将重点介绍 API 网关的实现,而不是 Go kit 中的端点、传输或服务等基本概念。
服务发现是微服务架构的重要组成部分。简而言之,我们不需要费心选择使用哪个服务实例,因为服务发现系统会在后台执行此操作。
在这个例子中,我将使用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))
在此处阅读完整的源代码。