Golang 微服务实战 - 2. Consul
第二部分需要提到微服务架构中必不可少的功能:服务发现。我在先前做的 Spring Cloud 项目中使用过 Eureka 作为服务发现框架。但现在 Eureka 已经停更一年多,且使用 Go 编写的 Consul 更适合在 Go 环境微服务分布式系统上使用。
以下列出几篇文章,这几篇文章说明了有关服务发现的相关概念,并对当下热门服务发现框架进行了一些对比:《什么是服务发现》,《服务发现框架Consul的使用》,深入了解服务注册与发现
Consul
什么是 Consul ?官方网站的解释如下:
HashiCorp Consul 是一个网络服务解决方案,使团队能够管理服务之间、本地环境、多云环境以及运行时之间的安全网络连接。Consul 提供服务发现、服务网格、流量管理和网络基础设施设备的自动更新。您可以在单个 Consul 部署实例中单独或一起使用这些功能。
安装 Consul
在官方网站下载二进制文件,随后解压至你所指定的文件目录。解压完成后,仅会得到一个二进制文件consul(Windows 下则是consul.exe)。将该二进制文件所在目录添加进系统环境变量,并在控制台输入指令consul以检查安装是否完成。
Consul 常用命令
全部 Consul 控制台命令详情请使用
consul -h查看
本项目最常用的指令是consul agent,其主要功能为运行一个 consul 代理。consul agent最常使用的功能如下:
-h查看consul agent的所有额外命令-bind 0.0.0.0指定 consul 所在机器的 IP 地址。默认值:0.0.0.0。注意,bind 后没有等号。-http-port=8500consul 自带一个 web 访问的默认端口:8500-client=127.0.0.1表明哪些机器可以访问 consul ,默认为本机。0.0.0.0 则表示 所有其它机器均可访问。-config-dir=path该选项用于指定service的配置文件和检查定义所在的位置。通常会指定为某一个路径/consul.d(通常情况下,.d表示一系列配置文件存放的目录)-config-file指定一个要装载的配置文件。该选项可以配置多次,进而配置多个配置文件。-data-dir=path该选项用于指定 agent 储存状态的数据目录,这是所有 agent 都必须的,对于 server 尤其重要,因为他们必须持久化集群的状态。-dev开发者模式,该选项用于创建一个开发环境下的server节点,该参数配置下,不会有任何持久化操作,即不会有任何数据写入到磁盘。dev模式仅仅是在开发和测试环境中使用,不能用于生产环境。-bootstrap-expect该选项用于通知 consul server 类型节点,指定集群的 server 节点个数,该参数是为了延迟选举启动,直到全部的节点启动完毕以后再进行启动。-node=hostname该 node 选项用于指定节点在集群中的名称,该名称在集群中需要是唯一的,推荐直接使用机器的 IP。-rejoinconsul 启动的时候,设置其所加入到的 consul 集群-server以服务方式开启 consul ,允许其他的 consul 连接到开启的 consul上 (形成集群)。如果不加 -server, 表示以 “客户端” 的方式开启。不能被连接。每个数据中心(DC)的 server 数量推荐3到5个。所有的 server 节点加入到集群后要经过选举,采用 raft 一致性算法来确保数据操作的一致性。-client该参数用于指定 consul 界定为 client 节点类型。-ui可以使用 web 页面(进入该页面的 IP 地址与-bind命令有关,端口与-http-port命令有关)来查看服务发现的详情-dcdc 是 datacenter 的简称,该选项用于指定节点加入的 dc 实例。
其它 consul 常用指令:
consul members查看集群中的成员。consul info查看当前 consul 的 IP 等其它信息。consul join该命令的作用是将 agent 加入到 consul 的集群当中。当新启动一个 agent 节点后,往往需要指定节点需要加入到特定的 consul 集群中,此时使用 join 命令进行指定。consul reload重启 consulconsul leave优雅的关闭 consul不优雅指 ctrl + c。
使用 Consul 注册服务
运行以下命令启动
1 | consul agent -server -bootstrap-expect 1 -data-dir=d:/code/Consul/consul_data -node=n1 -bind 127.0.0.1 -ui -rejoin -config-dir=d:/code/Consul/consul.d/ -client 0.0.0.0 |
按以下步骤操作
进入配置文件路径
D:\code\Consul\consul_cfg创建 json 文件
web.json在该文件中,填写服务信息。
1
2
3
4
5
6
7
8
9{
"service": {
"name": "Faceid",
"tags": [
"rails"
],
"port": 9000
}
}Json 配置文件详细配置选项参见官方文档
重新启动 consul
1
consul agent -server -bootstrap-expect 1 -data-dir=d:/code/Consul/consul_data -node=n1 -bind 127.0.0.1 -ui -rejoin -config-dir=d:/code/Consul/consul.d/ -client 0.0.0.0
查询服务
浏览器查看:
直接在浏览器输入
http://127.0.0.1:8500/,进入 consul 仪表板查看终端命令查看:
输入如下指令:
1
curl -o d:/code/Consul/test.json 127.0.0.1:8500/v1/catalog/service/Faceid
在
d:/code/Consul/test.json文件中读取数据即可1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40[
{
"ID": "57bee4ce-9f8d-b68c-3fa3-5d21f98e65aa",
"Node": "n1",
"Address": "127.0.0.1",
"Datacenter": "dc1",
"TaggedAddresses": {
"lan": "127.0.0.1",
"lan_ipv4": "127.0.0.1",
"wan": "127.0.0.1",
"wan_ipv4": "127.0.0.1"
},
"NodeMeta": {
"consul-network-segment": ""
},
"ServiceKind": "",
"ServiceID": "Faceid",
"ServiceName": "Faceid",
"ServiceTags": [
"rails"
],
"ServiceAddress": "",
"ServiceWeights": {
"Passing": 1,
"Warning": 1
},
"ServiceMeta": {},
"ServicePort": 9000,
"ServiceSocketPath": "",
"ServiceEnableTagOverride": false,
"ServiceProxy": {
"Mode": "",
"MeshGateway": {},
"Expose": {}
},
"ServiceConnect": {},
"CreateIndex": 40,
"ModifyIndex": 40
}
]
Consul 健康检查
在/consul.d路径下新建health.json,该配置会每隔5秒检查名为Faceid的 consul 服务在端口9000上的 http 请求是否有响应,如果超过1秒没有响应,则会判断该健康检查不通过。
1 | { |
然后通过以下命令启动 consul :
1 | consul agent -server -bootstrap-expect 1 -data-dir=d:/code/Consul/consul_data -node=n1 -bind 127.0.0.1 -ui -rejoin -config-dir=d:/code/Consul/consul.d/ -config-file d:/code/Consul/consul.d/health.json -client 0.0.0.0 |
在http://127.0.0.1:8500/下的仪表板能看到服务健康状况:
因为我们只是在测试 consul 的功能,并没有把微服务注册上去,因此这里的HTTP API on port 9000健康检查一定是不通过的,因为我们没有实现任何一个微服务实例能对该 consul 服务器每5秒发送一个 http 请求。同时,Faceid 服务的自检查则是能够通过 consul 健康检查的。
除了上述例子中 http 实现健康检查外,还可以使用 脚本、tcp、ttl 等方式进行健康检查。
Consul 结合 gRPC
基本 gRPC 远程调用
Protocol Buffer
GoLand 项目中新建文件夹,创建 ConsulClient.go 、 ConsulServer.go 以及文件夹 pb。在文件夹 pb 中创建 protobuf 文件,取名为 pb.person.proto 。内容编写如下:
1 | syntax = "proto3"; |
使用语句 protoc --go-grpc_out=./ *.proto --go_out=./ *.proto 进行编译,生成两个文件:
pb.person.pb.gopb.person_grpc.pb.go
服务端
在 ConsulServer.go 中编写如下代码
1 | package main |
注意到在新版 gRPC 实现接口的类中,除了需要 protobuf 中定义的接口,还需要匿名嵌入(更通俗的方法讲就是继承) protobuf 生成的结构体(类)
Unimplemented****Server,其中 **** 是服务名。官方文档中对此的解释是保证程序的向前兼容( forward compatible ),即保证程序能在未来的版本迭代中保持健壮性。通俗一点讲,当版本迭代或业务需求改变,protobuf 的内容或是提供的 gRPC 服务发生改变的时候,原本该服务的 gRPC 接口实现将全部报错;但如果所有接口实现都继承了
Unimplemented****Server类,那么程序不会报错(因为父类Unimplemented****Server一定实现了该接口),而是会根据Unimplemented****Server类执行一定的错误处理和报告,保证了程序的健壮性。
客户端
在 ConsulClient.go 中编写如下代码
1 | package main |
先运行服务端,再运行客户端,看到客户端输出 name:"hello Kevin" age:22 则说明运行成功。
将 gRPC 服务注册到 Consul 上
修改服务端代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78package main
import (
"context"
"fmt"
"github.com/hashicorp/consul/api"
"google.golang.org/grpc"
"net"
"src/src/Consul/GRPC/pb"
)
type Children struct {
*pb.UnimplementedHelloServer // 向前兼容,即需要兼容未来的软件
}
func (c *Children) SayHello(_ context.Context, p *pb.Person) (*pb.Person, error) {
p.Name = "hello " + p.Name
return p, nil
}
func main() {
fmt.Println("hello")
// 1. 创建 Consul 对象
consulObj, err := api.NewClient(api.DefaultConfig())
if err != nil {
fmt.Println(err)
return
}
// 2. 告诉consul, 即将注册的服务的配置信息
reg := api.AgentServiceRegistration{
// 服务节点 id,用于区分提供同一服务的不同服务器
ID: "Faceid",
Tags: []string{"grpc", "consul"},
// 服务名
Name: "grpc And Consul",
Address: "127.0.0.1",
Port: 8800,
Check: &api.AgentServiceCheck{
// 该服务节点 id下,服务自健康检查的 ID
CheckID: "consul grpc test",
TCP: "127.0.0.1:8800",
Timeout: "1s",
Interval: "5s",
},
}
// 3. 把 gRPC 服务注册到 Consul 上
err = consulObj.Agent().ServiceRegister(®)
if err != nil {
return
}
// gRPC 操作
grpcServer := grpc.NewServer()
pb.RegisterHelloServer(grpcServer, new(Children))
listener, err := net.Listen("tcp", ":8800")
if err != nil {
fmt.Println(err)
return
}
defer func(listener net.Listener) {
err := listener.Close()
if err != nil {
fmt.Println(err)
}
}(listener)
fmt.Println("服务启动...")
err = grpcServer.Serve(listener)
if err != nil {
fmt.Println(err)
return
}
}修改客户端代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61package main
import (
"context"
"fmt"
"github.com/hashicorp/consul/api"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"src/src/Consul/GRPC/pb"
"strconv"
)
func main() {
// 1. 创建 Consul 对象
consulObj, err := api.NewClient(api.DefaultConfig())
if err != nil {
fmt.Println(err)
return
}
// 2. 从 consul 上获取健康的服务
/*
func (h *Health) Service(service, tag string, passingOnly bool, q *QueryOptions) ([]*ServiceEntry, *QueryMeta, error)
- 参数:
service: 服务名。 -- 注册服务时,指定该string
tag:标签名/别名。 如果有多个, 任选一个
passingOnly:是否通过健康检查。 true
q:额外查询参数。 通常传 nil
- 返回值:
ServiceEntry: 存储服务的切片。
QueryMeta:额外查询返回值。 nil
error: 错误信息
*/
services, _, err := consulObj.Health().Service("gRPC And Consul", "grpc", true, nil)
if err != nil {
fmt.Println(err)
return
}
// 可在此引入负载均衡算法
ipAddr := services[0].Service.Address + ":" + strconv.Itoa(services[0].Service.Port)
dial, err := grpc.Dial(ipAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
fmt.Println(err)
return
}
grpcClient := pb.NewHelloClient(dial)
person := pb.Person{
Name: "Kevin",
Age: 22,
}
newPerson, err := grpcClient.SayHello(context.TODO(), &person)
if err != nil {
return
}
fmt.Println(newPerson)
}通过指令
consul agent -dev直接以 dev 模式启动 Consul ,然后先后启动服务端与客户端。在客户端看到输出:name:"hello Kevin" age:22,并且在 Consul 控制台中看到输出:1
2022-10-15T15:19:18.385+0800 [DEBUG] agent.http: Request finished: method=GET url=/v1/health/service/gRPC%20And%20Consul?passing=1&tag=grpc from=127.0.0.1:65497 latency=2.0771ms
以上,说明我们成功将 gRPC 服务注册到 Consul 上。在 127.0.0.1:8800 的仪表板上也能看到名字为 gRPC And Consul 的服务。
服务注销
1 | package main |







