使用Consul
进行服务注册与服务发现的学习
1 服务注册与服务发现原理
首先我们直到微服务项目主要由服务端和客户端组成。那么微服务和普通web
项目的区别就在于可以将一个项目中的功能进行拆分,然后再进行部署。但是有些功能模块的访问量比较大,有些模块的访问量比较小,这时为了能给客户端及时的返回响应就需要部署不同数量的微服务功能模块。由于每一个部署的微服务模块的ip
肯定是不一样的,那么就需要有一个功能模块来管理这些微服务。简单点说,我们的手机的通讯录管理了所有家人朋友的手机号,我们不需要记住所有人的手机号只需要去通讯录中找到家人朋友的名字就可以进行打电话。所以对于微服务项目来讲,客户端也不需要记住所有服务端的ip
地址,只需要去一个类似于通讯录的模块中找到功能名称即可。目前常见的可用于服务注册和服务发现的模块有counsul
,etcd
等。
(1)服务注册:服务端将服务的id
,ip
,tag
等一系列信息注册到consul
中。
(2)服务发现:客户端带着consul
的地址,以及要查询的服务的信息去consul
中发现服务。
2 Raft
算法
Raft
是一种共识算法,它主要解决的是在分布式系统中多个节点如何达成一致的问题。在consul
和etcd
中为了保证数据的一致性,所使用的都是Raft
算法。
(1)首先当只有一个节点和一个客户端时,客户端向节点发送信息,则容易达成一致性。因为要么节点收到客户端的数据,要么就没有收到。
(2)当我们有多个节点和一个客户端时,这时怎么保证多个节点可以达成一致性呢?这就要使用到Raft
算法了。
1)在Raft
算法中一共有三种状态,分别为,领导人(leader
),候选人(candidater
),追随者(follower
)。
2)如下图所示,当三个节点没有leader
时,其三个节点都会进行一个倒计时,该倒计时是在150毫秒到300毫秒以内随机的时间,谁先倒计时完谁就会成为candidater
。然后candidater
向其他的节点发送信号,让其他节点跟随他。最后当大多数其他节点给candidater
投票,该节点就成为了leader
。然后每隔5秒,follower
就会向leader
发送信息,来确保leader
还存活着。一旦leader
挂了后,其他节点就会开始重新选举leader
。
当2
个节点同时倒计时完毕成为候选人时,他们收到的follower
的投票数是一样的时,就会重新进入倒计时,确保只有一个leader
存在。
3)此时有一个客户端,要将节点值设置为5
,就会把设置信息发送给leader
。然后leader
第一次不会直接设置值,因为他要确定他是否能连通大多数follower
,所以会将set 5
发送给follower
。然后follower
再给leader
回一个收到的信号,此时如果收到大多数follower
的回复,则进行set 5
操作。最后leader
再给follower
回复可以进行set 5
的操作了,follower
才将值设置为5。
4)当出现网络问题将节点进行分割时,被分开的follower
就收不到原本leader
的回复了,此时就开始了重新选举。然后选出新的leader
。然后任期(Term
),从1设置为2。
5)此时如果出现2个客户端,分别连接到了nodeC
和nodeB
,一个要求把值设置为8,另外一个要求吧值设置为3,因此nodeB
和nodeC
分别向follower
发送设置信息,此时,nodeB
发现收到的follower
的回复少于了大多数。所以就不进行设置,而NodeC
则收到了大多数的回复,就进行设置。
6)此时网络恢复了,NodeB
发现了比自己更高Term
的nodeC
,此时NodeB
就下台,成为follower
。并会将set 8
操作传给新加入的follower
。
3 Consul
环境的搭建
(1)先从github
上获取Consul
的配置和资源。并进入服务发现的包中。
git clone https://github.com/hashicorp/learn-consul-docker.git
cd datacenter-deploy-service-discovery
(2)使用docker-compose
启动容器。
docker-compose up -d
(3)这样就搭建好了Consul
的环境。使用浏览器进入http:127.0.0.1:8500
,就可以看到Consul
的管理界面了。
4 使用go
进行服务注册和服务发现
1 连接consul
与服务注册
(1)本例使用的是grpc
上篇中的add
服务,并将其注册到Consul
中,其代码在grpc
上中写过。以下代码进行了对Consul
的连接,以及服务注册。
func main() {
l, err := net.Listen("tcp", ":8082")
if err != nil{
fmt.Printf("failed to listen, err:%v\n",err)
return
}
//起一个grpc服务
s := grpc.NewServer()
//注册grpc服务
pb.RegisterAddServer(s,&service{})
//--------------------------以下部分代码为服务注册以及consul的连接------------------------------//
//连接至consul服务返回一个consul对象
cc, err1 := api.NewClient(api.DefaultConfig())
if err1 != nil{
fmt.Printf("api.NewClient failed err%v\n",err)
return
}
//设置add的配置信息
srv := &api.AgentServiceRegistration{
ID: fmt.Sprintf("%s-%s-%d", "add", "127.0.0.1", 8082),
Name: "add",
Tags: []string{"hc","add"},
Address: "127.0.0.1",
Port: 8082,
}
//将grpc服务注册到consul
err = cc.Agent().ServiceRegister(srv)
if err != nil{
fmt.Printf("cc.Agent().ServiceRegister failed %v\n",err)
return
}
//-----------------------------------------------------------------------------------------//
//启动服务
err = s.Serve(l)
if err != nil{
fmt.Printf("failed to server, err:%v\n",err)
return
}
}
启动以上代码后,会发现在consul
的网页上已经出现了8082
端口add
服务的信息。
2 健康检查
(1)如果不设置健康检查,那么将无法识别该服务是否还在正常运行。由于我们是将Consul
以docker
方式启动的,所以需要将地址设置为本机的出口地址,不然Consul
是无法进行检查服务是否还存活的。这里在配置健康检查配置时,配置了注销服务DeregisterCriticalServiceAfter
,当服务停止后,会在1分钟后对服务进行注销。
func main() {
l, err := net.Listen("tcp", ":8082")
if err != nil{
fmt.Printf("failed to listen, err:%v\n",err)
return
}
//起一个grpc服务
s := grpc.NewServer()
//注册grpc服务
pb.RegisterAddServer(s,&service{})
//-------------------------以下为健康检查的代码------------------------------------------//
//注册健康检查服务
healthCheck := health.NewServer()
healthpb.RegisterHealthServer(s,healthCheck)
//查看本机的出口ip
ipinfo, err := GetOutboundIP()
if err != nil{
fmt.Printf("get out bound ip failed %v\n",err)
return
}
//设置健康检查配置
check := &api.AgentServiceCheck{
GRPC: fmt.Sprintf("%s:%d", ipinfo.String(), 8081),
Timeout: "10s", // 超时时间
Interval: "10s", 运行检查的频率
// 指定时间后自动注销不健康的服务节点
// 最小超时时间为1分钟,收获不健康服务的进程每30秒运行一次,因此触发注销的时间可能略长于配置的超时时间。
DeregisterCriticalServiceAfter: "1m",
}
//---------------------------------------------------------------------------------------//
//连接至consul服务返回一个consul对象
cc, err1 := api.NewClient(api.DefaultConfig())
if err1 != nil{
fmt.Printf("api.NewClient failed err%v\n",err)
return
}
//将grpc服务注册到consul
srv := &api.AgentServiceRegistration{
ID: fmt.Sprintf("%s-%s-%d", "add", ipinfo.String(), 8082),
Name: "add",
Tags: []string{"hc","add"},
Address: ipinfo.String(),
Port: 8082,
Check: check,
}
err = cc.Agent().ServiceRegister(srv)
if err != nil{
fmt.Printf("cc.Agent().ServiceRegister failed %v\n",err)
return
}
//启动服务
err = s.Serve(l)
if err != nil{
fmt.Printf("failed to server, err:%v\n",err)
return
}
}
// GetOutboundIP 获取本机的出口IP
func GetOutboundIP() (net.IP, error) {
conn, err := net.Dial("udp", "8.8.8.8:80")
if err != nil {
return nil, err
}
defer conn.Close()
localAddr := conn.LocalAddr().(*net.UDPAddr)
return localAddr.IP, nil
}
以下为含有健康检查的服务,会发现All service checks passing
绿了,说明consul
在不断向add
服务发起检查。
3 服务发现
(1)服务发现需要在客户端进行设置。只需要将之前的连接grpc
服务端的地址,改为consul
的就可以了。
func main() {
//通过consul,地址为127.0.0.1:8500,去查找healthy=true的add服务
conn, err := grpc.Dial("consul://127.0.0.1:8500/add?healthy=true",
grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil{
log.Fatalf("grpc dial failed err:%v\n",err)
return
}
defer conn.Close()
c := pb.NewAddClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
defer cancel()
resp, err := c.TwoNumAdd(ctx, &pb.AddRequest{A: 2222, B: 2})
if err != nil{
log.Printf("c.TowNumAdd failed,err:%v\n",err)
return
}
// 拿到rpc执行结果
log.Println(resp.GetReply())
}
4 负载均衡
(1)使用add
服务,分别在4个端口起了服务,如下图所示。
(2)然后在客户端进行以下配置。使用轮询策略,然后访问对add
服务发起16次请求。
func main() {
conn, err := grpc.Dial("consul://127.0.0.1:8500/add?healthy=true",
grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy": "round_robin"}`),
grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil{
log.Fatalf("grpc dial failed err:%v\n",err)
return
}
defer conn.Close()
c := pb.NewAddClient(conn)
for i := 0; i < 16; i++{
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
defer cancel()
resp, err := c.TwoNumAdd(ctx, &pb.AddRequest{A: 1, B: 1})
if err != nil{
log.Printf("c.TowNumAdd failed,err:%v\n",err)
return
}
// 拿到rpc执行结果
// 服务端将端口地址和客户端传入的数字进行相加然后返回
// 这里为了展示清除服务端口,所以固定传入1+1,然后用服务端传入的数-2就得到地址。
log.Println(resp.GetReply()-2,":",2)
}
}
进行16次请求后,可以通过下图发现确实是轮询调用的。