当前位置: 首页 > 科技观察

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

时间:2023-03-12 06:24:56 科技观察

我从2016年开始接触Consul,主要用它来做服务发现,后来逐渐应用到生产环境,一点心得在使用中进行了总结。一开始用Consul的人不多。近两年,微服务越来越流行,使用Consul的人也越来越多。人们经常会问一些问题,比如:服务注册到节点后,为什么其他节点不同步?客户做什么?(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。Servers的推荐数量为3或5。当Leader死亡时,将启动选举机制产生新的Leader。集群中的Consul节点通过八卦协议(gossipprotocol)维护成员关系,也就是说,一个节点知道集群中还有哪些其他节点,以及这些节点是Client还是Server。单个数据中心内的gossip协议同时使用TCP和UDP通信,均使用8301端口。跨数据中心的gossip协议也同时使用TCP和UDP通信,使用8302端口。集群内对数据的读写请求可以直接发送给服务器,或者通过客户端使用RPC转发给服务器,请求最终到达领导节点。在允许稍微陈旧数据的情况下,读请求也可以在普通的Server节点上完成,集群中的数据读写和复制都是通过TCP8300端口完成的。Consul服务发现原理下图是自己画的,基本上描述了服务发现的完整过程。我们先来看一下:首先需要有一个普通的Consul集群,有Server和Leader。这里ConsulServer分别部署在服务器Server1、Server2、Server3上。假设他们选举Server2上的ConsulServer节点为Leader。最好只在这些服务器上部署Consul程序,尽可能保持ConsulServer的稳定性。然后在服务器Server4和Server5上通过ConsulClient注册ServiceA、B、C。这里每个Service分别部署在两台服务器上,可以避免Service的单点问题。可以通过HTTPAPI(端口8500)或通过Consul配置文件向Consul注册服务。ConsulClient可以认为是无状态的,它通过RPC将注册信息转发给ConsulServer,服务信息存储在Server的各个节点中,通过Raft实现强一致性。最后,在服务器Server6中,程序D需要访问ServiceB,此时程序D首先访问原生ConsulClient提供的HTTPAPI,由原生Client将请求转发给ConsulServer。ConsulServer查询ServiceB的当前信息并返回。最后,程序D获取到ServiceB的所有deployment的IP和端口,然后可以选择其中一个ServiceB的deployment向其发起请求。如果使用DNS方式进行服务发现,在程序D中直接使用ServiceB的服务发现域名,域名解析请求先到达本地的DNS代理,再转发给本地的ConsulClient,由ConsulClient转发向领事服务器请求。ConsulServer查询ServiceB的当前信息并返回,最后程序D获取到ServiceB某个部署的IP和端口。图中描述的部署架构是笔者认为最通用、最简单的方案。从一些默认的配置或设计来看,也是官方希望用户采用的解决方案。例如8500端口默认监听127.0.0.1。当然,也有同学不同意。其他选项将在后面提到。Consul的实际使用为了更快的熟悉Consul的原理以及如何使用它,最好自己实际测试一下。Consul的安装非常简单,但是不方便在一台机器上搭建集群测试,而且使用虚拟机比较重,所以这里选择了Docker。这里使用Windows10,需要专业版,因为Windows上的Docker依赖Hyper-V,这个需要专业版支持。Docker的使用这里就不过多描述了,遇到相关问题请自行搜索。安装Docker通过这个地址下载安装:https://store.docker.com/editions/community/docker-ce-desktop-windows安装完成后,打开WindowsPowerShell,输入docker–version,如果是Docker版本正常输出,就没事了。启动Consul集群在WindowsPowerShell中执行命令拉取最新版本的Consul镜像:dockerpullconsul然后就可以启动集群了,这里启动4个ConsulAgent,3个Server(会选举出一个Leader),1个Client。#启动第一个Server节点,集群需要3台Server,将容器端口8500映射到宿主机端口8900,打开管理界面dockerrun-d--name=consul1-p8900:8500-eCONSUL_BIND_INTERFACE=eth0consulagent--server=true--bootstrap-expect=3--client=0.0.0.0-ui#启动第二个Server节点并加入集群dockerrun-d--name=consul2-eCONSUL_BIND_INTERFACE=eth0consulagent--server=true--client=0.0.0.0--join172.17.0.2#启动第三个Server节点,加入集群dockerrun-d--name=consul3-eCONSUL_BIND_INTERFACE=eth0consulagent--server=true--client=0.0.0.0--join172.17.0。2#启动第四个Client节点,加入集群dockerrun-d--name=consul4-eCONSUL_BIND_INTERFACE=eth0consulagent--server=false--client=0.0.0.0--join172.17.0.2第一个启动容器的IP一般,是172.17.0.2,后面启动的几个容器的IP会排成一行:172.17.0.3、172.17.0.4、172.17.0.5。这些Consul节点在Docker容器中相互通信,它们之间通过桥接方式进行通信。但是如果宿主机要访问容器内部的网络,就需要进行端口映射。在启动第一个容器时,将Consul的8500端口映射到宿主机的8900端口,这样可以方便的通过宿主机的浏览器查看集群信息。进入consul1容器:dockerexec-itconsul1/bin/sh#执行ls后可以看到consul在根目录ls,输入exit跳出容器。服务注册自己写一个web服务,使用最熟悉的开发语言即可,但是需要能在容器中运行,可能还需要安装运行环境。例如,Python、Java和.netcore等SDK和Web服务器。需要注意的是,Consul的Docker镜像是基于alpine系统的。具体运行环境请搜索安装。这里写了一个hello服务,通过配置文件注册到Consul。服务的相关信息如下:name:hello,服务名称,需要能够区分不同的业务服务。可以使用相同名称部署和注册多个副本。id:hello1,serviceid,需要在每个节点上唯一,重复覆盖。address:172.17.0.5,服务所在机器地址。端口:5000,服务的端口。健康检查地址:http://localhost:5000/。如果返回的HTTP状态码为200,则表示服务是健康的。Consul每10秒请求一次,请求超时时间为1秒。请将以下内容保存为文件services.json,上传到容器的/consul/config目录下:{"services":[{"id":"hello1","name":"hello","tags":["primary"],"address":"172.17.0.5","port":5000,"checks":[{"http":"http://localhost:5000/","tls_skip_verify":false,"method":"Get","interval":"10s","timeout":"1s"}]}]}复制到consulconfig目录:dockercp{这里替换成services.json的本地路径}consul4:/consul/config重新加载consul配置:consulreload然后服务注册成功。这个服务可以部署到多个节点,比如consul1和consul4,同时运行。服务发现服务注册成功后,调用者获取对应服务地址的过程就是服务发现。Consul提供了多种方式。HTTPAPI方法curlhttp://127.0.0.1:8500/v1/health/service/hello?passing=true返回的信息包括注册的Consul节点信息、服务信息、服务健康检查信息。这里使用了一个参数passing=false,它会自动过滤掉不健康的服务,包括不健康的服务和不健康的Consul节点上的服务。从这个设计中我们可以看出,Consul将服务的状态与节点的状态进行了绑定。如果服务有多个部署,会返回多条关于服务的信息,调用者需要决定使用哪个部署。通常,它可以是随机的或轮询的。为了提高服务吞吐量,减轻Consul的压力,也可以缓存获取到的服务节点信息,但是必须准备容错方案,因为缓存服务部署可能会变得不可用。是否缓存需要结合自己的流量和容错规则来决定。上面传递的参数默认是false,也就是说不健康的节点也会被返回。结合获取节点所有服务的方法,可以在此处获取所有服务的实时健康状态,并对不健康的服务进行告警处理。DNS方式下hello服务的域名为:hello.service.dc1.consul,后面的service代表服务,是固定的;dc1为数据中心名称,可配置;最后一个领事也可以配置。官方在介绍DNS方法的时候经常使用dig命令来测试,但是alpine系统中并没有dig命令,也没有相关的包可以安装,但是有人实现了,下载下来解压到bin目录下。curl-Lhttps://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-p8600hello.service.dc1.consul.ANY如果报错:parseof/etc/resolv.conffailed,请删除resolv.conf中的搜索行。如果正常,可以看到返回了服务部署的IP信息。如果有多个部署,您将看到多个。如果部署不健康,它将被自动删除(包括部署不健康的节点)。需要注意的是,该方法不会返回服务的端口信息。要使用DNS,可以在程序中集成一个DNS解析库,也可以自定义本地DNSServer。自定义本地DNSServer是指将.consul域的所有请求转发给ConsulAgent。Windows上有DNS代理,Linux上有Dnsmasq。对于非Consul提供的服务,继续请求原来的DNS;使用DNSServer时,Consul会随机返回特定服务的多个部署中的一个,只能提供简单的负载均衡。DNS缓存问题:DNS缓存一般存在于应用程序的网络库、本地DNS客户端或代理中,而ConsulSever本身可以认为是没有缓存的(为了提高集群DNS吞吐量,可以设置为在普通服务器上使用陈旧数据)服务器,但通常影响不大)。DNS缓存可以减轻ConsulServer的访问压力,但也会导致访问不可用的服务。使用时需要根据实际流量和容错能力确定DNS缓存方案。ConsulTemplateConsulTemplate是Consul官方提供的一个工具,严格来说并不是标准的服务发现方式。该工具会通过Consul监听数据变化并替换模板中使用的标签,并将替换后的文件发布到指定目录。它对于Nginx等Web服务器的反向代理和负载平衡特别有用。这个工具没有集成在Consul的Docker镜像中,需要自己安装,比较简单:curl-Lhttps://releases.hashicorp.com/consul-template/0.19.5/consul-template_0.19.5_linux_amd64.tgz|tar-xzv-C/usr/local/bin然后创建一个文件in.tpl,内容为:{{rangeservice"hello"}}server{{.Name}}{{.Address}}:{{.Port}}{{end}}这个标签会遍历hello服务的所有部署,并按照指定的格式输出。在这个文件目录下执行:nohupconsul-template-template"in.tpl:out.txt"&现在可以catout.txt查看根据模板生成的内容,添加或关闭服务,文件内容会自动更新。我没有在生产环境中使用过这个工具。详细使用方法请访问:https://github.com/hashicorp/consul-template节点和服务注销节点和服务注销可以使用HTTPAPI:注销任意节点和服务:/catalog/deregister来注销服务当前节点:/agent/service/deregister/:service_id注意:如果注销的服务还在运行,会重新同步到目录,所以目录的注销接口只能在Agent不可用的时候使用。节点宕机状态会变为failed,默认72小时后从集群中移除。如果某个节点不再继续使用,也可以在本地使用consulleave命令,或者在其他节点上使用consulforce-leavenodeid,该节点上的所有服务和健康检查都会被注销。Consul的健康检查Consul在服务发现方面是专业的,健康检查是必不可少的功能之一。它提供了Script/TCP/HTTP+Interval,以及TTL等多种方式。服务的健康检查由服务注册的Agent处理,它可以是Client也可以是Server。很多同学使用ZooKeeper或者etcd来做服务发现。在使用Consul时,他们发现节点挂掉后服务状态变为不可用。那么有同学问为什么节点之间服务不同步呢?其根本原因在于服务发现的实现原理不同。Consul与ZooKeeper和etcd的区别后两个工具通过key-value存储实现服务的注册和发现:ZooKeeper利用临时节点机制,在业务服务启动时创建临时节点,节点在服务中,节点不存在不存在。etcd使用TTL机制在业务服务启动时创建键值对,并定时更新TTL。当TTL过期时,服务不可用。ZooKeeper和etcd的key-value存储都是强一致的,也就是说key-value对会自动同步到多个节点,只要在某个节点上存在,就认为对应的业务服务可用。Consul的数据同步也是强一致的。服务的注册信息会在服务器节点之间同步。与ZK、etcd相比,服务信息仍然是持久化存储的。即使服务部署不可用,仍然可以查询服务部署。但是,业务服务的可用状态由注册的代理维护。如果Agent无法正常工作,则无法确定服务的真实状态。而且,Consul相当稳定。如果Agent挂了,很大概率可能是服务器状态不好。这个时候在这个节点上屏蔽服务是合理的。Consul确实是这样设计的。DNS接口会自动屏蔽挂起节点上的服务,HTTPAPI也认为挂起节点上的服务不通。针对Consul健康检查的这种机制,在避免单点故障的同时,所有的业务服务都应该多副本部署,注册到不同的Consul节点上。部署多个副本可能会给你的设计带来一些挑战,因为调用者同时访问多个服务实例可能会因为未共享会话而导致状态不一致。对此有很多成熟的解决方案,可以查询,这里不再赘述。健康检查能否支持故障转移?上面说了健康检查是由注册到服务的Agent来处理的,那么如果这个Agent挂掉了,会不会有另一个Agent接手健康检查呢?答案是否定的。从问题的原因来看,在应用到生产环境之前,必须在各种场景下进行测试,没有问题才会上线,这样可以屏蔽明显的问题。如果是新版Consul的bug导致,此时需要降级;如果是偶发的bug,只需要重新把Consul拉上来,比较简单。如果是硬件、网络或操作系统故障,那么节点上服务的可用性就很难保证,不需要其他Agent来接管健康检查。从实现的角度来看,选择哪个节点是一个问题,需要实时或准实时同步各个节点的负载状态。而且,由于业务服务的运行状态是多变的,即使当时选择了一个负载比较轻的节点,也不能保证在某个时间段任务会再次变重,这可能会导致新的和更大规模的崩溃。如果原节点还需要启动,接管的健康检查是否应该撤销?如果是这样,就需要记录服务最初注册的节点,然后有一个监控机制来触发它。否则,通过服务发现会得到很多冗余信息,随着时间的推移,这种数据会越来越多,系统就会变得混乱。从实际应用的角度来看,节点上的服务可能不仅仅被发现,其他的服务也必须被发现。如果节点挂了,只提供发现的功能,实际上服务不可用。当然,发现其他服务不需要使用本地节点,可以通过接入Nginx实现的几个Consul节点的负载均衡来实现,这无疑引入了一个新的技术栈。如果不是上面提到的问题,或者你可以通过一些方式解决这些问题,那么健康检查接管的实现肯定更复杂,因为分布式系统的状态同步更复杂。同时,不要忘记已经部署了服务的多个副本。挂一个应该不会影响系统的快速恢复,所以没必要做这个接管。Consul的其他部署架构如果实在不想在每台主机上都部署ConsulClient,也有多路注册的方案可以选择。这是在交流群里得到的想法。如图所示,ConsulClient部署在专用服务器上,然后每个服务注册到多个Client。这里为了避免服务单点的问题,每个服务都部署了多个副本。当需要服务发现时,程序向提供负载均衡的程序发起请求,程序将请求转发给某个ConsulClient。该方案需要注意将Consul的8500端口绑定到私网IP,默认只有127.0.0.1。这种架构的优点:Consul节点服务器与应用服务器隔离,相互干扰小。不需要在每台主机上都部署Consul,方便了Consul的集中管理。当一个ConsulClient挂了,注册到它的服务还有机会被访问。但是我们也需要注意它的缺点:引入更多的技术栈:负载均衡的实现不仅要考虑ConsulClient的负载均衡,还要考虑负载均衡本身的单点。Client节点数量:如果单个Client注册的服务过多,负载过大,则需要通过算法(如hash一致性)合理分配每个Client上的服务数量,确定Client的总数。服务发现应该过滤掉重复注册:因为注册到多个节点会被认为是多次部署(DNS接口不会有这个问题)。事实上,这个解决方案是可以优化的。用于服务发现的负载均衡可以直接代理Server节点,因为相关的请求还是会转发给Server节点,所以直接发给Server比较好。是否可以只有服务器?这个问题的答案还是跟服务的数量有关。首先,Server节点的数量并不是越多越好。3或5个节点是推荐的数量。数字越大,数据同步过程越慢(强一致性)。那么每个节点可以注册的服务数量是有上限的,受限于软硬件的处理能力。那么如果你只有10个左右的服务,只有Server问题不大,但是这个时候有必要用Consul吗?所以正常使用Consul的时候最好有一个Client,这也符合Consul的反熵设计。大家可以把这个部署架构和上面说的通用架构对比一下,看看哪个更适合你,或者如果你有更好的方案欢迎分享。