使用Consul做服务发现的若干姿势
从2016年起就开始接触consul,使用的主要目的就是做服务发现,后来逐步应用于生产环境,并总结了少许使用经验。最开始使用consul的人不多,为了方便交流创建了一个qq群,这两年微服务越来越火,使用consul的人也越来越多,目前群里已有400多人,经常有人问一些问题,比如:
- 服务注册到节点后,其他节点为什么没有同步?
- 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
通过这个地址下载安装:
安装完成后打开 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,服务的端口。
- 健康检查地址:http://localhost: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": "http://localhost: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 http://127.0.0.1:8500/v1/health/service/hello?passing=false
返回的信息包括注册的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 https://releases.hashicorp.com/consul-template/0.19.5/consul-template_0.19.5_linux_amd64.tgz|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查看根据模板生产的内容,新增或者关闭服务,文件内容会自动更新。
此工具我没有用在生产环境,详细使用请访问:
节点和服务注销
节点和服务的注销可以使用http api:
- 注销任意节点和服务:/catalog/deregister
- 注销当前节点的服务:/agent/service/deregister/:service_id
如果某个节点不继续使用了,也可以在本机使用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的反熵设计。
大家可以将这个部署架构与前文提到的普世架构对比下,看看哪个更适合自己,或者你有更好的方案欢迎分享出来。
后记
在编写这篇文章的时候,发现很多地方还不了解,很多框架也没有使用过,增长了不少见识,同时确认了一些之前似是而非的细节,但是文中仍可能有些理解错误,或者说的不是很清楚的地方,比如如何结合容器在生产环境使用,欢迎大家加群交流(群号: 234939415)!
我的独立博客: