从2016年开始接触微服务的时候就使用consul,当初只知道其特别方便,是一款不错的服务注册与发现工具。至于其部署架构,实现原理都没有深入去了解过,就如同年少读书不求甚解。最近,在着手搞微服务治理,服务治理与发现这块正好选型consul,这才详细的琢磨了下其代码,也对其原理有了一定的认识。下面就听我就徐徐道来……
架构介绍
下面是consul官方给出的一张架构图,我们先来理解一下。
首先,从架构上,图片被两个datacenter分成了上下两部分;但这两部分又并不是完全隔离的,他们之间通过WAN GOSSIP在Internet上交互报文。因此,我们了解到consul是可以支持多个数据中心之间基于WAN来做同步的。
再看单个datacenter内部,节点被划分为两种颜色,其中红色为server,紫色为client。它们之间通过GRPC通信(主要用于业务数据)。除此之外,server和client之间,还有一条LAN GOSSIP通信,这是用于当LAN内部发生了拓扑变化时,存活的节点们能够及时感知,比如server节点down掉后,client就会触发将对应server节点从可用列表中剥离出去。
当然,server与server之间,client与client之间,client与server之间,在同一个datacenter中的所有consul agent会组成一个LAN网络(当然它们之间也可以按照区域划分segment),当LAN网中有任何角色变动,或者有用户自定义的event产生的时候,其他节点就会感知到,并触发对应的预置操作。
所有的server节点共同组成了一个集群,他们之间运行raft协议,通过共识仲裁选举出leader。所有的业务数据都通过leader写入到集群中做持久化,当有半数以上的节点存储了该数据后,server集群才会返回ACK,从而保障了数据的强一致性。当然,server数量大了之后,也会影响写数据的效率。所有的follower会跟随leader的脚步,保障其有最新的数据副本。
同一个consul agent程序,通过启动的时候指定不同的参数来运行server或client模式。这两种模式下,各自所负责的事务具体如下。
Server节点
- 参与共识仲裁(raft)
- 存储群集状态(日志存储)
- 处理查询
- 维护与周边(LAN/WAN)各节点关系
Agent节点
- 负责通过该节点注册到consul的微服务的健康检查
- 将客户端注册请求以及查询转化为对server的RPC请求
- 维护与周边(LAN/WAN)各节点关系
服务端口
端口 | 作用 |
---|---|
8300 | RPC exchanges |
8301 | LAN GOSSIP |
8302 | WAN GOSSIP |
8400 | RPC exchanges by the CLI |
8500 | Used for HTTP API and web interface |
8600 | Used for DNS server |
实现原理
纵观consul的实现,其核心在于两点:
- 集群内节点间信息的高效同步机制,其保障了拓扑变动以及控制信号的及时传递;
- server集群内日志存储的强一致性。
它们主要基于以下两个协议来实现:
- 使用gossip协议在集群内传播信息
- 使用raft协议来保障日志的一致性
Serf
serf是hashicorp基于GOSSIP协议来实现的一个用于分布式集群成员管理,失败检测以及编排的工具,当前最新版本为v0.8.1。有兴趣的朋友可以到这个链接具体了解hashicorp serf,下面我来简单介绍一下其功能。
集群管理
这台机器上有两个IP地址,一个是172.20.20.10,另一个为172.20.20.10。我准备启动两个serf agent进程,分别绑定到不同的两个IP地址上,各自叫做agent-one和agent-two。
由于它们启动之后,相互之间是不知道彼此的,我通过执行serf join
来把它们组成一个LAN serf。这样它们就可以彼此检测到彼此,通过查看serf members可以看到所有的节点以及其健康状况。
1 | $ serf agent -node=agent-one -bind=172.20.20.10 |
事件响应
在前面的步骤中,我们将两个serf进程加入到了同一个LAN中,接下来我们将进行一些更加激动人心的实践。接下来,我们创建了一个脚本(handler.sh),大致内容为:当脚本被调用的时候,会打印出一些具体的信息。然后,我们在启动serf agent的时候,通过参数将该脚本传递给serf agent。这样当收该serf节点收到event时,就会调用用户指定的handler(即执行脚本)。
1 | $ cat handler.sh |
发送自定义event
1 | $ serf event hello-there |
Event类型
serf指定了下面这些类型的event,各自的作用如下所示:
1 | member-join One or more members have joined the cluster. |
Raft
由于介绍raft协议的文章已经比较多,我这里就不在详述。这里重点分析一下在consul中,raft协议运作的一些实践和日志。
节点状态变更
- 在节点数达到bootstrap-expect的数时,开始启用raft选举
- 在节点数超过bootstrap-expect数时,其他节点为follower
- 在leader被干掉后,raft如果判断到节点数依然大于等于bootstrap-expect时,重新选举
- 逐一干掉节点,当节点数少于bootstrap-expect时,raft协议不再选举,将维持先前的状态。
Raft选举日志分析
1 | # 选举日志信息 (bootstrap) |
源码架构
先来看Consul内部是如何做服务注册与发现的流程,下图是consul客户端向agent注册以及发现目标服务的时序图。
通过上图,我们大概知道了在consul agent中,功能分为了consul server和consul agent(client)。在前面架构介绍中我们已经阐述了server和client各自的职责。
consul源码中,server和client都是在一套代码中,通过指定启动参数的形势来运行consul server。这里我们先来重点讲解一下consul client的内部架构。
Consul Client架构
上图简要描述了consul client中的各重要服务,以及它们之间的关系。
lan serf
主要职责是维护节点之间的关系,当有节点加入或者离开的时候,所有节点都会接收到对应的event,这里的lan serf就是指对这些event做处理的handler的go routine服务。state sync
在consul启动的时候,会启动该服务,它监听一个channel,当其他服务有向consul server同步配置的需求的时候,就会像channel中写入event信息;然后就会触发该服务向consul server同步配置信息。这里的同步又分为全同步和部分同步,主要是为了降低网路的负担。gRPC router
这是对连接到consul server的gRPC连接的维护和负载均衡机制。在该服务中心,一方面会基于lan serf对consul server节点的拓扑变更事件来维护server列表,另一方面也会对到存活server的connection做定期的ping来维护连接列表;除此之外,还能够对server连接做客户端负载均衡。local state
是一个本地的内存数据库,一般执行sync就是从server将数据同步过来保存到该db中;平时做一些配置更改也会对应更新该db。api
consul是提供了HTTP和CLI两种对外访问方式的,这里所谓的API并不是想说接口的细节,而指的是consul所提供对外API对应controller逻辑实现。比如下一节要讲到的服务注册的API,后面都做了什么业务逻辑,这是很重要的一部分,对于复杂的逻辑一般包括了:更新本地local state,启动对应的go routine来做事,使用gRPC向server更新数据,向sync channel发消息从而触发sync等操作。
服务注册流程
基于前面一节的介绍,我们大概能够猜测到服务注册大概都需要些什么样的流程,接下来我们就将以下这块的逻辑。
上图是其服务注册API的controller中函数调用的一个简化流程。
- 首先
s.agent.AddService
函数要做的就是将接收到的服务信息做一通校验,然后整理成为local state的数据结构之后保存到本地;但是由于它是一个内存数据库,并不能够持久化,于是再将其保存到本地文件中做持久化。 - 干完这些操作之后,如果该服务没有指定healthcheck操作的话,接下来要做的就是将这个服务注册请求同步到consul server,让raft leader将数据真正持久化到server中,这部分我没有在图上体现出来,但是在代码中确实是这样实现的。
- 对于在注册的时候制定了healthcheck内容的服务,需要继续注册healthcheck。由于consul支持的healthcheck类型较多,这里对其所指定类型做了简单的校验,然后就开始干正事了。启动一个goroutine来专门为这个服务执行定期的健康检查操作,可见,如果该consul agent上注册的服务太多的话,势必消耗很多资源,这就要求我们部署方案要做好规划了。
- 当健康检查的结果与先前的结果不一致的时候,会触发对local state的更新,同时,需要局部同步该服务到consul server上的内容。为什么呢?因为服务的健康状态其实是保存到其check字段下的,而非是service的一个一级属性,这块大家可以下去查阅一下代码。另外,每次状态变更都会触发consul agent通过gRPC调用server的
Catalog.Register
来注册服务,我的理解其实是覆盖先前注册关于该服务的信息。
操作实践
介绍consul agent的配置参数,以及各种使用场景下的命令。
consul agent参数
1 | -advertise 通知展现地址用来改变我们给集群中的其他节点展现的地址,一般情况下-bind地址就是展现地址 |
常用命令
开发模式
最简单,可以用于本地微服务开发的时候,零时做服务注册与发现工具。请注意的是,开发模式下,consul不会做配置的持久化,当consul服务终止时,之前注册的服务和K/V都会随之丢失!
1 | docker run -d --name=dev-consul -e CONSUL_BIND_INTERFACE=eth0 consul |
server模式
1 | docker run -d --net=host -e 'CONSUL_LOCAL_CONFIG={"skip_leave_on_interrupt": true}' consul agent -server -bind=<external ip> -retry-join=<root agent ip> -bootstrap-expect=<number of server agents> |
- 启动server
1 | docker run -d -e 'CONSUL_LOCAL_CONFIG={"skip_leave_on_interrupt": true}' consul agent -server -retry-join=172.17.0.2 -bootstrap-expect=3 |
client模式
- 启动client
1 | docker run -d -e 'CONSUL_LOCAL_CONFIG={"skip_leave_on_interrupt": true}' consul agent -retry-join=172.17.0.2 |
- 暴露dns
1 | docker run -d --net=host -e 'CONSUL_ALLOW_PRIVILEGED_PORTS=' consul -dns-port=53 -recursor=8.8.8.8 |
- 查询
1 | docker exec -t dev-consul consul members |
集群部署实践
下面是部署两个server和一个agent的实例
s1: 10.200.204.104
1
./consul agent -server -bootstrap-expect 1 -data-dir /etc/consul/data -node=s1 -bind=10.200.204.104 -ui -rejoin -config-dir=/etc/consul/conf -client 0.0.0.0
s2: 10.200.204.48
1
./consul agent -server -bootstrap-expect 1 -data-dir /etc/consul/data -node=s2 -bind=10.200.204.48 -ui -rejoin -config-dir=/etc/consul/conf -client 0.0.0.0 -retry-join=10.200.204.104
c1: 10.200.204.133
1
./consul agent -node=c1 -bind=10.200.204.133 -data-dir /etc/consul/data -ui -rejoin -config-dir=/etc/consul/conf -client 0.0.0.0 -retry-join=10.200.204.48
查看raft角色
1 | # consul operator raft list-peers |
注册服务到consul
1 | cat << EOF >> payload.json |
1 | curl --request PUT --data @payload.json http://127.0.0.1:8500/v1/catalog/register |
一般不推荐使用catalog注册,而是使用agent来注册,注册到agent如下:
1 | curl -X PUT -H 'application/json' -d '{"ID": "taobao","Name": "taobao","Tags": ["primary","v1"],"Address": "140.205.94.189","Port": 80,"Meta": {"taobao_version": "4.0"},"EnableTagOverride": false,"Check": {"DeregisterCriticalServiceAfter": "90m","HTTP": "http://140.205.94.189:80/","Interval": "10s"},"Weights": {"Passing": 10,"Warning": 1}}' http://127.0.0.1:8500/v1/agent/service/register |
配置文件注册
直接将以下json文件保存后存放到--config-dir
目录下,重启consul服务
1 | { |
会发现,在哪一个client节点上注册的服务,对应client节点就会负责做healthcheck,也就意味着,这个节点非常重要,如果做不好高可用,所有注册到上面的服务都有被deregisterd的风险。
API 注册
1 | curl -X PUT -d '{"id": "ljchen","name": "ljchen","address": "14.215.177.38","port": 80,"tags": ["dev"],"checks": [{"http": "http://14.215.177.38:80/","interval": "5s"}]}' http://127.0.0.1:8500/v1/agent/service/register |
查询consul中的服务
1 | curl http://127.0.0.1:8500/v1/catalog/service/redis?tag=v1 |
删除node节点
1 | curl -X PUT -H 'application/json' -d '{"Datacenter": "dc1","Node": "node-name"}' http://127.0.0.1:8500/v1/catalog/deregister |