一篇文章了解Consul服务发现实现原理

从 2016 年起就开始接触 Consul,使用的主要目的就是做服务发现,后来逐步应用于生产环境,并总结了少许使用经验。

最开始使用 Consul 的人不多,这两年微服务越来越火,使用 Consul 的人也越来越多。

经常有人会问一些问题,比如:

服务注册到节点后,其他节点为什么没有同步?

Client 是干什么的?(Client 有什么作用?)

能不能直接注册到 Server?(是否只有 Server 节点就够了?)

服务信息是保存在哪里的?

如果节点挂了,健康检查能不能转移到别的节点?

有些人可能对服务注册和发现还没有概念,有些人可能使用过其他服务发现的工具,比如 ZooKeeper,etcd,会有一些先入为主的经验。

这篇文章将结合 Consul 的官方文档和自己的实际经验,谈一下 Consul 做服务发现的方式,文中尽量不依赖具体的框架和开发语言,从原理上进行说明,希望能够讲清楚上边的几个问题。

为什么使用服务发现

防止硬编码、容灾、水平扩缩容、提高运维效率等等,只要你想使用服务发现总能找到合适的理由。

一般的说法是因为使用微服务架构。传统的单体架构不够灵活不能很好的适应变化,从而向微服务架构进行转换。

而伴随着大量服务的出现,管理运维十分不便,于是开始搞一些自动化的策略,服务发现应运而生。所以如果需要使用服务发现,你应该有一些对服务治理的痛点。

但是引入服务发现就可能引入一些技术栈,增加系统总体的复杂度,如果你只有很少的几个服务,比如 10 个以下,并且业务不怎么变化,吞吐量预计也很稳定,可能就没有必要使用服务发现。

Consul 内部原理

下面这张图来源于 Consul 官网,很好的解释了 Consul 的工作原理,先大致看一下:

首先 Consul 支持多数据中心,在上图中有两个 DataCenter,他们通过 Internet 互联,同时请注意为了提高通信效率,只有 Server 节点才加入跨数据中心的通信。

在单个数据中心中,Consul 分为 Client 和 Server 两种节点(所有的节点也被称为 Agent),Server 节点保存数据,Client 负责健康检查及转发数据请求到 Server。

Server 节点有一个 Leader 和多个 Follower,Leader 节点会将数据同步到 Follower,Server 的数量推荐是 3 个或者 5 个,在 Leader 挂掉的时候会启动选举机制产生一个新的 Leader。

集群内的 Consul 节点通过 gossip 协议(流言协议)维护成员关系,也就是说某个节点了解集群内现在还有哪些节点,这些节点是 Client 还是 Server。

单个数据中心的流言协议同时使用 TCP 和 UDP 通信,并且都使用 8301 端口。跨数据中心的流言协议也同时使用 TCP 和 UDP 通信,端口使用 8302。

集群内数据的读写请求既可以直接发到 Server,也可以通过 Client 使用 RPC 转发到 Server,请求最终会到达 Leader 节点。

在允许数据轻微陈旧的情况下,读请求也可以在普通的 Server 节点完成,集群内数据的读写和复制都是通过 TCP 的 8300 端口完成。

Consul 服务发现原理

下面这张图是自己画的,基本描述了服务发现的完整流程,先大致看一下:

首先需要有一个正常的 Consul 集群,有 Server,有 Leader。这里在服务器 Server1、Server2、Server3 上分别部署了 Consul Server。

假设他们选举了 Server2 上的 Consul Server 节点为 Leader。这些服务器上最好只部署 Consul 程序,以尽量维护 Consul Server 的稳定。

然后在服务器 Server4 和 Server5 上通过 Consul Client 分别注册 Service A、B、C,这里每个 Service 分别部署在了两个服务器上,这样可以避免 Service 的单点问题。

服务注册到 Consul 可以通过 HTTP API(8500 端口)的方式,也可以通过 Consul 配置文件的方式。

Consul Client 可以认为是无状态的,它将注册信息通过 RPC 转发到 Consul Server,服务信息保存在 Server 的各个节点中,并且通过 Raft 实现了强一致性。

最后在服务器 Server6 中 Program D 需要访问 Service B,这时候 Program D 首先访问本机 Consul Client 提供的 HTTP API,本机 Client 会将请求转发到 Consul Server。

Consul Server 查询到 Service B 当前的信息返回,最终 Program D 拿到了 Service B 的所有部署的 IP 和端口,然后就可以选择 Service B 的其中一个部署并向其发起请求了。

如果服务发现采用的是 DNS 方式,则 Program D 中直接使用 Service B 的服务发现域名,域名解析请求首先到达本机 DNS 代理,然后转发到本机 Consul Client,本机 Client 会将请求转发到 Consul Server。

Consul Server 查询到 Service B 当前的信息返回,最终 Program D 拿到了 Service B 的某个部署的 IP 和端口。

图中描述的部署架构笔者认为是最普适最简单的方案,从某些默认配置或设计上看也是官方希望使用者采用的方案,比如 8500 端口默认监听 127.0.0.1,当然有些同学不赞同,后边会提到其他方案。

Consul 实际使用

为了更快的熟悉 Consul 的原理及其使用方式,最好还是自己实际测试下。

Consul 安装十分简单,但是在一台机器上不方便搭建集群进行测试,使用虚拟机比较重,所以这里选择了 Docker。

这里用了 Windows 10,需要是专业版,因为 Windows 上的 Docker 依赖 Hyper-V,而这个需要专业版才能支持。

这里对于 Docker 的使用不会做过多的描述,如果遇到相关问题请搜索一下。

安装 Docker

通过这个地址下载安装:

https://store.docker.com/editions/community/docker-ce-desktop-windows

安装完成后打开 Windows PowerShell,输入 docker –version,如果正常输出 Docker 版本就可以了。

启动 Consul 集群

在 Windows PowerShell 中执行命令拉取最新版本的 Consul 镜像:

docker pull consul

然后就可以启动集群了,这里启动 4 个 Consul Agent,3 个 Server(会选举出一个 Leader),1 个 Client。

#启动第1个Server节点,集群要求要有3个Server,将容器8500端口映射到主机8900端口,同时开启管理界面docker run -d --name=consul1 -p 8900:8500 -e CONSUL_BIND_INTERFACE=eth0 consul agent --server=true --bootstrap-expect=3 --client=0.0.0.0 -ui#启动第2个Server节点,并加入集群docker run -d --name=consul2 -e CONSUL_BIND_INTERFACE=eth0 consul agent --server=true --client=0.0.0.0 --join 172.17.0.2#启动第3个Server节点,并加入集群docker run -d --name=consul3 -e CONSUL_BIND_INTERFACE=eth0 consul agent --server=true --client=0.0.0.0 --join 172.17.0.2#启动第4个Client节点,并加入集群docker run -d --name=consul4 -e CONSUL_BIND_INTERFACE=eth0 consul agent --server=false --client=0.0.0.0 --join 172.17.0.2

第 1 个启动容器的 IP 一般是 172.17.0.2,后边启动的几个容器 IP 会排着来:172.17.0.3、172.17.0.4、172.17.0.5。

这些 Consul 节点在 Docker 的容器内是互通的,他们通过桥接的模式通信。但是如果主机要访问容器内的网络,需要做端口映射。

在启动第一个容器时,将 Consul 的 8500 端口映射到了主机的 8900 端口,这样就可以方便的通过主机的浏览器查看集群信息。

进入容器 consul1:

docker exec -it consul1 /bin/sh#执行ls后可以看到consul就在根目录ls

输入 exit 可以跳出容器。服务注册自己写一个 Web 服务,用最熟悉的开发语言就好了,不过需要在容器中能够跑起来,可能需要安装运行环境。

比如 Python、Java、.net core等的 sdk 及 Web 服务器,需要注意的是 Consul 的 Docker 镜像基于 alpine 系统,具体运行环境的安装请搜索一下。

这里写了一个 hello 服务,通过配置文件的方式注册到 Consul,服务的相关信息如下:

name:hello,服务名称,需要能够区分不同的业务服务,可以部署多份并使用相同的 name 注册。 

id:hello1,服务 id,在每个节点上需要唯一,如果有重复会被覆盖。

address:172.17.0.5,服务所在机器的地址。 

port:5000,服务的端口。 

健康检查地址::5000/,如果返回 HTTP 状态码为 200 就代表服务健康,每 10 秒 Consul 请求一次,请求超时时间为 1 秒。 

请将下面的内容保存成文件 services.json,并上传到容器的 /consul/config 目录中:

{  "services": [    {      "id": "hello1",      "name": "hello",      "tags": [        "primary"      ],      "address": "172.17.0.5",      "port": 5000,      "checks": [        {        "http": ":5000/",        "tls_skip_verify": false,        "method": "Get",        "interval": "10s",        "timeout": "1s"        }      ]    }  ]}

复制到 consul config 目录:

docker cp {这里请替换成services.json的本地路径} consul4:/consul/config

重新加载 consul 配置:

consul reload

然后这个服务就注册成功了。可以将这个服务部署到多个节点,比如部署到 consul1 和 consul4,并同时运行。

服务发现

服务注册成功以后,调用方获取相应服务地址的过程就是服务发现。Consul  提供了多种方式。

HTTP API 方式

curl :8500/v1/health/service/hello?passing=true

返回的信息包括注册的 Consul 节点信息、服务信息及服务的健康检查信息。

这里用了一个参数 passing=false,会自动过滤掉不健康的服务,包括本身不健康的服务和不健康的 Consul 节点上的服务,从这个设计上可以看出 Consul 将服务的状态绑定到了节点的状态。

如果服务有多个部署,会返回服务的多条信息,调用方需要决定使用哪个部署,常见的可以随机或者轮询。

为了提高服务吞吐量,以及减轻 Consul 的压力,还可以缓存获取到的服务节点信息,不过要做好容错的方案,因为缓存服务部署可能会变得不可用。具体是否缓存需要结合自己的访问量及容错规则来确定。

上边的参数 passing 默认为 false,也就是说不健康的节点也会返回,结合获取节点全部服务的方法,这里可以做到获取全部服务的实时健康状态,并对不健康的服务进行报警处理。

DNS 方式

hello 服务的域名是:hello.service.dc1.consul,后边的 service 代表服务,固定;dc1 是数据中心的名字,可以配置;最后的 consul 也可以配置。

官方在介绍 DNS 方式时经常使用 dig 命令进行测试,但是 alpine 系统中没有 dig 命令,也没有相关的包可以安装,但是有人实现了,下载下来解压到 bin 目录就可以了。

curl -L https://github.com/sequenceiq/docker-alpine-dig/releases/download/v9.10.2/dig.tgz|tar -xzv -C /usr/local/bin

然后执行 dig 命令:

dig @127.0.0.1 -p 8600 hello.service.dc1.consul. ANY

如果报错:parse of /etc/resolv.conf failed ,请将 resolv.conf 中的 search 那行删掉。

正常的话可以看到返回了服务部署的 IP 信息,如果有多个部署会看到多个,如果某个部署不健康了会自动剔除(包括部署所在节点不健康的情况)。需要注意这种方式不会返回服务的端口信息。

使用 DNS 的方式可以在程序中集成一个 DNS 解析库,也可以自定义本地的 DNS Server。

自定义本地 DNS Server 是指将 .consul 域的请求全部转发到 Consul Agent,Windows 上有 DNS Agent,Linux 上有 Dnsmasq。

对于非 Consul 提供的服务则继续请求原 DNS;使用 DNS Server 时 Consul 会随机返回具体服务的多个部署中的一个,仅能提供简单的负载均衡。

DNS 缓存问题:DNS 缓存一般存在于应用程序的网络库、本地 DNS 客户端或者代理,Consul Sever 本身可以认为是没有缓存的(为了提高集群 DNS 吞吐量,可以设置使用普通 Server 上的陈旧数据,但影响一般不大)。

DNS 缓存可以减轻 Consul Server 的访问压力,但是也会导致访问到不可用的服务。使用时需要根据实际访问量和容错能力确定 DNS 缓存方案。

Consul Template

Consul Template 是 Consul 官方提供的一个工具,严格的来说不是标准的服务发现方式。

这个工具会通过 Consul 监听数据变化然后替换模板中使用的标签,并发布替换后的文件到指定的目录。在 Nginx 等 Web 服务器做反向代理和负载均衡时特别有用。

Consul 的 Docker 镜像中没有集成这个工具,需要自己安装,比较简单:

curl -L |tar -xzv -C /usr/local/bin

然后创建一个文件 in.tpl,内容为:

{{ range service "hello" }}server {{ .Name }}{{ .Address }}:{{ .Port }}{{ end }}

这个标签会遍历 hello 服务的所有部署,并按照指定的格式输出。在此文件目录下执行:

nohup consul-template -template "in.tpl:out.txt" &

现在你可以 cat out.txt 查看根据模板生产的内容,新增或者关闭服务,文件内容会自动更新。

此工具我没有用在生产环境,详细使用请访问:

https://github.com/hashicorp/consul-template

节点和服务注销

节点和服务的注销可以使用 HTTP API:

注销任意节点和服务:/catalog/deregister 

注销当前节点的服务:/agent/service/deregister/:service_id 

注意:如果注销的服务还在运行,则会再次同步到 catalog 中,因此应该只在 Agent 不可用时才使用 catalog 的注销 API。

节点在宕机时状态会变为 failed,默认情况下 72 小时后会被从集群移除。

如果某个节点不继续使用了,也可以在本机使用 consul leave 命令,或者在其他节点使用 consul force-leave 节点 id,则节点上的服务和健康检查全部注销。

Consul 的健康检查

Consul 做服务发现是专业的,健康检查是其中一项必不可少的功能,其提供 Script/TCP/HTTP+Interval,以及 TTL 等多种方式。

服务的健康检查由服务注册到的 Agent 来处理,这个 Agent 既可以是 Client 也可以是 Server。

很多同学都使用 ZooKeeper 或者 etcd 做服务发现,使用 Consul 时发现节点挂掉后服务的状态变为不可用了,所以有同学问服务为什么不在各个节点之间同步?这个根本原因是服务发现的实现原理不同。

Consul 与 ZooKeeper、etcd 的区别

后边这两个工具是通过键值存储来实现服务的注册与发现:

ZooKeeper 利用临时节点的机制,业务服务启动时创建临时节点,节点在服务就在,节点不存在服务就不存在。 

etcd 利用 TTL 机制,业务服务启动时创建键值对,定时更新 TTL,TTL 过期则服务不可用。 

ZooKeeper 和 etcd 的键值存储都是强一致性的,也就是说键值对会自动同步到多个节点,只要在某个节点上存在就可以认为对应的业务服务是可用的。

Consul 的数据同步也是强一致性的,服务的注册信息会在 Server 节点之间同步,相比 ZK、etcd,服务的信息还是持久化保存的,即使服务部署不可用了,仍旧可以查询到这个服务部署。

但是业务服务的可用状态是由注册到的 Agent 来维护的,Agent 如果不能正常工作了,则无法确定服务的真实状态。

并且 Consul 是相当稳定了,Agent 挂掉的情况下大概率服务器的状态也可能是不好的,此时屏蔽掉此节点上的服务是合理的。

Consul 也确实是这样设计的,DNS 接口会自动屏蔽挂掉节点上的服务,HTTP API 也认为挂掉节点上的服务不是 passing 的。

鉴于 Consul 健康检查的这种机制,同时避免单点故障,所有的业务服务应该部署多份,并注册到不同的 Consul 节点。

部署多份可能会给你的设计带来一些挑战,因为调用方同时访问多个服务实例可能会由于会话不共享导致状态不一致,这个有许多成熟的解决方案,可以去查询,这里不做说明。

健康检查能不能支持故障转移?

上边提到健康检查是由服务注册到的 Agent 来处理的,那么如果这个 Agent 挂掉了,会不会有别的 Agent 来接管健康检查呢?答案是否定的。

从问题产生的原因来看,在应用于生产环境之前,肯定需要对各种场景进行测试,没有问题才会上线,所以显而易见的问题可以屏蔽掉。

如果是新版本 Consul 的 Bug 导致的,此时需要降级;如果这个 Bug 是偶发的,那么只需要将 Consul 重新拉起来就可以了,这样比较简单。

如果是硬件、网络或者操作系统故障,那么节点上服务的可用性也很难保障,不需要别的 Agent 接管健康检查。

从实现上看,选择哪个节点是个问题,这需要实时或准实时同步各个节点的负载状态。

而且由于业务服务运行状态多变,即使当时选择出了负载比较轻松的节点,无法保证某个时段任务又变得繁重,可能造成新的更大范围的崩溃。

如果原来的节点还要启动起来,那么接管的健康检查是否还要撤销,如果要,需要记录服务们最初注册的节点,然后有一个监听机制来触发。

如果不要,通过服务发现就会获取到很多冗余的信息,并且随着时间推移,这种数据会越来越多,系统变的无序。

从实际应用看,节点上的服务可能既要被发现,又要发现别的服务,如果节点挂掉了,仅提供被发现的功能实际上服务还是不可用的。

当然发现别的服务也可以不使用本机节点,可以通过访问一个 Nginx 实现的若干 Consul 节点的负载均衡来实现,这无疑又引入了新的技术栈。

如果不是上边提到的问题,或者你可以通过一些方式解决这些问题,健康检查接管的实现也必然是比较复杂的,因为分布式系统的状态同步是比较复杂的。

同时不要忘了服务部署了多份,挂掉一个不应该影响系统的快速恢复,所以没必要去做这个接管。

Consul 的其他部署架构

如果你实在不想在每个主机部署 Consul Client,还有一个多路注册的方案可供选择,这是交流群中获得的思路。

如图所示,在专门的服务器上部署 Consul Client,然后每个服务都注册到多个 Client。

这里为了避免服务单点问题还是每个服务部署多份,需要服务发现时,程序向一个提供负载均衡的程序发起请求,该程序将请求转发到某个 Consul Client。

这种方案需要注意将 Consul 的 8500 端口绑定到私网 IP 上,默认只有 127.0.0.1。

这个架构的优势:

Consul 节点服务器与应用服务器隔离,互相干扰少。

不用每台主机都部署 Consul,方便 Consul 的集中管理。

某个 Consul Client 挂掉的情况下,注册到其上的服务仍有机会被访问到。 

但也需要注意其缺点:

引入更多技术栈:负载均衡的实现,不仅要考虑 Consul Client 的负载均衡,还要考虑负载均衡本身的单点问题。 

Client 的节点数量:单个 Client 如果注册的服务太多,负载较重,需要有个算法(比如 hash 一致)合理分配每个 Client 上的服务数量,以及确定 Client 的总体数量。 

服务发现要过滤掉重复的注册:因为注册到了多个节点会认为是多个部署(DNS 接口不会有这个问题)。 

这个方案其实还可以优化,服务发现使用的负载均衡可以直接代理 Server 节点,因为相关请求还是会转发到 Server 节点,不如直接就发到 Server。

是否可以只有 Server?

这个问题的答案还是有关服务数量的问题,首先 Server 的节点数量不是越多越好,3 个或者 5 个是推荐的数量,数量越多数据同步的处理越慢(强一致性)。

然后每个节点可以注册的服务数量是有上限的,这个受限于软硬件的处理能力。

所以如果你的服务只有 10 个左右,只有 Server 问题是不大的,但是这时候有没有必要使用 Consul 呢?

因此正常使用 Consul 的时候还是要有 Client 才好,这也符合 Consul 的反熵设计。

大家可以将这个部署架构与前文提到的普世架构对比下,看看哪个更适合自己,或者你有更好的方案欢迎分享出来。

作者:波斯码

简介:北漂,IT 行业,十年余,邮箱:[email protected]

编辑:陶家龙、孙淑娟

出处:

精彩文章推荐:

Kafka是如何处理Netflix每天2万亿条消息的?

新手也能看懂,Kubernetes其实很简单

没想到,我们的分布式缓存竟这样把注册中心搞垮!