服务发现的定义:
服务发现是伴随着微服务体系而产生的。在微服务诞生以前,又或者我们的网络应用没有那么大的并发请求量,单体应用完全可以满足网络服务的设计要求。在微服务体系中,单体应用被按照功能模块拆分成了许多的个体应用,这些应用之间由原本的进程内通讯变成了进程间甚至是网络间通讯,如何彼此的相互感知就成了一个技术问题,服务发现由此而生。
服务发现通常包含三个角色:
服务中介—— 服务中介是联系服务提供者与服务消费者的桥梁,为服务消费者指供服务提供者的感知服务。
服务提供者—— 向服务中介注册自己的地址和身份信息,供服务消费者发现和访问。
服务消费者—— 从服务中介查询服务提供者地址和身份信息,根据策略选择其中一个进行访问。
引用网络上的描述图:
最简单的服务发现:
DNS应该算是最经典的服务发现应用之一了。
上世纪60年末代,美国国防部高级研究计划署(ARPA,后来的DARPA)资助试验性广域计算机网络,称为ARPAnet,其初衷是将电脑主机连接起来,共享计算机资源。70年代时,ARPAnet只是一个拥有几百台主机的小网络,仅需要一个HOSTS文件就可以容纳所需要主机信息,HOSTS提供的是主机名和IP地址的映射关系,也就是说可以用主机名进行网络信息的共享,而不需要记住IP地址。HOSTS.TXT文件是由SRI的网络信息中心(Network Information Center,简称NIC)负责维护,并且从一台主机SRI-NIC上分发到整个网络。ARPAnet的管理员通常是通过电子邮件通知NIC,同时定期FTP到SRI-NIC上获得最新的HOSTS.TXT文件。但是随着ARPAnet的增长,这种方法行不通了。每台主机的变更都会导致HOSTS.TXT 的变化,导致所有主机需要到SRI-NIC上获得更新文件。当ARPAnet 采用TCP/IP协议后,网络上的主机爆炸性的增长,出现了许多问题,其中最主要的是频繁更新导致的维护工作量和一致性问题。
这时候的HOSTS文件其本质就是一个服务中介,只不这个服务中介的性能实在是太低了。于是1983年在美国南加州大学工作的保罗·莫卡派乔斯发明和设计了DNS系统。DNS是一个分布式的域名数据库,如果把它当成一个整体来看的话,它相当于一个域名系统的中介,一方面它对外提供域名的注册服务,另一方面又对外提供域名到IP的查询服务。
服务发现系统的条件:
现代服务发现系统首先需要满足分布式CAP理论。
CAP理论阐述了在上图的三个特性中,一个正常工作的分布式系统最多只能同时满足其中两个。而在大部分的分布式系统中,设计者都考虑牺牲一致性,来换取整个系统的可用性和分区容错性,尽可能地使系统高可用。所以,一个合格的现代服务发现系统应该是可以多点布署的,并且在发生网络或节点故障的时候能够自动进行故障隔离。
现代服务发现系统其次需要具备友好的开发接口。现代开发语言呈现五花八门,在很多公司的微服务团队中,采用不同语言开发微服务单元也是所见不鲜。现代服务发现系统需要为开发者着想,提供语种比较全面的开发者接口。除此之外,开发者接口内部还应屏蔽服务发现实现过程中复杂的技术细节,确保较高的性能和较低的系统资源占用,以及较强的数据一致性。
现代服务发现系统还应支持对服务提供者进行健康检测。服务中介不能简单地依赖服务提供者进行启动时注册和退出时注销,因为当服务提供者发生故障时,注销操作将没法进行,这时需要服务中介具备健康检测能力,淘汰故障的服务提供者。
此外,现代服务发现系统大多还支持分布式KV存储,KV存储被广泛地应用于分布式服务配置场景。虽然KV存储不是服务发现的本职,但是从抽象角度来看,服务发现的分发特性非常适合网络服务配置,其内部工作原理与分发服务提供者是相似的。
服务发现系统的选择:
如果条件允许,我们可以选择自研服务发现系统,使用这种方法的收益和风险都是极端的。不少中大型互联网公司,由于多方面的原因,并没有选择开源的服务发现系统,但是可以想到的是开发运维的过程中肯定也没有少填坑。所以,建议不想趟水的朋友们,还是尽量选择现成稳定的解决方案。
目前领域内比较知名的有eureka、zookeeper、etcd、consul,使用java技术栈的朋友都可以选,使用c++和golang的朋友建议选用后二者,可以省去不少java运行环境的搭建和运维时间。
这里重点提一下etcd和consul,这俩位做为服务发现的新秀,都基于golang开发,列举两点差异:
- etcd是Apache许可,consul是MPL许可,虽然两者都可以用于商用,但etcd更宽松。
- etcd偏向于分布式KV,拿来做服务发现的话需要进行二次开发。consul则直接面向服务发现,而且功能十分强大,真正的开箱即用。
如何选择,请读者自行取舍。下面简单地讲一讲Consul的基本用法。
Consul的安装:
下载地址:https://www.consul.io/downloads.html
官网提供各个系统平台的二进制压缩包,下载后解压可直接使用,建议配置好PATH环境变量。
Consul的工作原理:
官方提供的架构图如下:
上图显示了两个数据中心,分别标注为DATACENTER1和DATACENTER2,Consul对多数据中心有非常友好的支持。在两个数据中心分别布署有许多SERVER和CLIENT节点,节点之间通过GOSSIP流行病协议通信,每个节点有两个GOSSIP池(LAN池和WAN池),LAN池用于数据中心内部通讯,WAN池则对所有数据中心共享,用于跨数据中心通讯。
关于SERVER和CLIENT节点,Consul规定了所有的节点一律称为Agent,SERVER和CLIENT分别是Agent的两个模式。在CLIENT模式下,Agent不保存数据,主要面向服务提供者和服务消费者, 提供服务注册、服务查询、健康检测等功能。在SERVER模式下,Agent除了支持CLIENT的所有功能外,还负责节点主从选举、数据存储,并维护数据一致性。通常在单个数据中心,SERVER的布署数量在3~5个,而CLIENT节点,要求在除布署SERVER的机器外,每台都布署CLIENT。
关于多数据中心,通常情况下,不同的数据中心之间是不会同步数据的,但是当对另一个数据中心的资源进行请求时,本地SERVER会将该资源的RPC请求转发给SERVER,并返回结果。
Consul命令说明:
consul命令输出,格式如下:
选取常用子命令及相应选项,描述如下:
agent—— 启动代理实例,常用子命令及选项包括:
-dev
以开发者模式运行,这主要用于快速布署开发,此时不支持任何持久化存储。
-server
未指明本选项的情况下,节点以CLIENT模式运行,此时仅支持服务注册、健康检测、查询和临时状态存储。指明本选项后,节点以SERVER模式运行,此时除支持CLIENT模式的所有功能外,节点还负责参与主从选举、数据存储。
-ui
启动自带的UI管理界面。
-datacenter=
指定本代理所属的数据中心名称,默认为dc1。
-node=
指定本代理的节点名称,若不指定则为主机名。
-node-id=
指定本代理的节点ID,若不指定则自动生成。
-advertise=
建议设置用于集群通讯时的绑定地址,包括raft通讯地址、LAN和WAN的gossip通讯地址。
-advertise-wan=
额外建议设置用于集群通讯时WAN口的gossip通讯地址。
-bind=
明确设置用于集群通讯时的绑定地址,包括raft通讯地址、LAN和WAN的gossip通讯地址。
-server-port=
设置raft工作端口,默认为8300。
-allow-write-http-from=
指定允许HTTP写操作的许可地址,格式为192.168.1.0/24,可多次指定不同的地址,若不指定表示不限制。
-serf-lan-bind=
明确设置gossip LAN地址,默认与advertise/bind指定的相同。
-serf-lan-port=
设置gossip LAN工作端口,默认为8301。
-serf-lan-allowed-cidrs=
指定gossip LAN接入许可地址,格式为192.168.1.0/24,可多次指定不同的地址,若不指定表示不限制。
-serf-wan-bind=
明确设置gossip WAN地址,默认与advertise/bind指定的相同。
-serf-wan-port=
设置gossip WAN工作端口,默认为8302。
-serf-wan-allowed-cidrs=
指定gossip WAN接入许可地址,格式为192.168.1.0/24,可多次指定不同的地址,若不指定表示不限制。
-bootstrap
设置为启动模式,此时节点可以选举自己为Leader,建议一个数据中心只启动一个此模式的节点。
-bootstrap-expect=
设置当前数据中心实际布署的SERVER节点数,选举时达成多数派需要。
-client=
设置用于查询的绑定地址,包括HTTP和DNS,默认为127.0.0.1。
-http-port=
另行设置HTTP服务端口,默认为8500。
-dns-port=
另行设置DNS服务端口,默认8600。
-config-file=
指定配置文件路径。
-config-dir=
指定配置文件目录,在此目录下的*.json文件将在启动时被加载。
-data-dir=
指定数据存储目录,用于节点内部的数据状态存储。
-enable-script-checks
允许脚本类型的健康检测,出于安全考虑默认禁止。
-join=
指定集群的其它节点地址,申请加入集群,可多次设置指定多个地址。
-join-wan=
额外指定集群的WAN节点地址,申请加入集群,可多次设置指定多个地址。
-rejoin
设置节点支持断连后重新加入集群。
-retry-interval=
设置加入集群失败时的重试时间间隔,默认30S。
-retry-interval-wan=
额外设置加入集群WAN口失败时的重试时间间隔,默认30S。
-retry-max=
设置加入集群失败时的重试次数,默认0表示无限次。
-retry-max-wan=
设置加入集群WAN口失败时的重试次数,默认0表示无限次。
-log-file=
指定日志文件的前缀,默认日志不输出到文件。
-log-rotate-bytes=
指定日志回滚的大小限制。
-log-rotate-duration=
指定日志回滚的时间限制。
-log-rotate-max-files=
指定保留的日志文件数。
-max-query-time=
指定查询请求的最大超时时间。
-encrypt=
指定加密通讯的KEY,KEY由consul keygen命令产生。
info—— 输出节点的版本和状态信息,常用子命令及选项包括:
-http-addr=
指定http的请求地址,默认值为http://127.0.0.1:8500。
members—— 输出已知的集群成员信息,常用子命令及选项包括:
-http-addr=
指定http的请求地址,默认值为http://127.0.0.1:8500。
-detailed
输出详细信息。
-wan
明确指定已知的来自WAN池的成员信息。
catalog—— 输出目录服务信息,常用子命令及选项包括:
-http-addr=
指定http的请求地址,默认值为http://127.0.0.1:8500。
datacenters
列出已知的数据中心名称。
nodes
列出本数据中心已知的节点信息。
services
列出本数据中心已知的服务名称。
services—— 向节点注册/注销服务实例,常用子命令及选项包括:
-http-addr=
指定http的请求地址,默认值为[http://127.0.0.1:8500]
register
注册服务实例。
deregister
注销服务实例。
reload—— 重新加载配置,常用子命令及选项包括:
-http-addr=
指定http的请求地址,默认值为[http://127.0.0.1:8500]
join—— 控制节点加入指定地址所在的集群,常用子命令及选项包括:
-http-addr=
指定http的请求地址,默认值为[http://127.0.0.1:8500]
-wan
指定该地址为WAN口地址,默认为LAN口。
leave—— 控制节点优雅地退出当前集群,常用子命令及选项包括:
-http-addr=
指定http的请求地址,默认值为[http://127.0.0.1:8500]
force-leave—— 控制指定故障节点强制从集群中移除,常用子命令及选项包括:
-http-addr=
指定http的请求地址,默认值为[http://127.0.0.1:8500]
-wan
指定该地址为WAN口地址,默认为LAN口。
maint—— 控制节点/服务实例进入/结束维护状态,常用子命令及选项包括:
-http-addr=
指定http的请求地址,默认值为[http://127.0.0.1:8500]
-disable
结束维护状态。
-enable
进入维护状态。
-reason=
指定维护操作的描述说明。
-service=
指定维护的是服务实例而非代理节点,明确服务ID。
kv—— 分布式KV操作,常用子命令及选项包括:
-http-addr=
指定http的请求地址,默认值为[http://127.0.0.1:8500]
put
写入/更新指定KV。
get
读取指定KV。
delete
删除指定KV。
monitor—— 监听节点的工作日志,常用子命令及选项包括:
-http-addr=
指定http的请求地址,默认值为[http://127.0.0.1:8500]
snapshot—— 控制节点执行快照操作,常用子命令及选项包括:
-http-addr=
指定http的请求地址,默认值为[http://127.0.0.1:8500]
save
保存快照。
restore
恢复快照。
operator—— 提供集群级的操作工具,常用子命令及选项包括:
-http-addr=
指定http的请求地址,默认值为[http://127.0.0.1:8500]
raft list-peers
显示raft节点列表及工作状态。
开发模式运行:
执行命令:
./consul agent -dev
输出:
==> Starting Consul agent...
Version: '1.11.1'
Node ID: '3689ee75-2f1d-4c63-2bf9-548ffcdbfd72'
Node name: 'localhost.localdomain'
Datacenter: 'dc1' (Segment: '
Server: true (Bootstrap: false)
Client Addr: [127.0.0.1] (HTTP: 8500, HTTPS: -1, gRPC: 8502, DNS: 8600)
Cluster Addr: 127.0.0.1 (LAN: 8301, WAN: 8302)
Encrypt: Gossip: false, TLS-Outgoing: false, TLS-Incoming: false, Auto-Encrypt-TLS: false
可以看到数据中心名称、节点名称、节点ID、各工作端口均为默认或自动生成。
查看成员,执行命令:
./consul members -detailed
输出:
Node Address Status Tags
localhost.localdomain 127.0.0.1:8301 alive acls=0,ap=default,build=1.11.1:2c56447e,dc=dc1,ft_fs=1,ft_si=1,id=f4498061-1866-8061-3255-0bfcaf00e32a,port=8300,raft_vsn=3,role=consul,segment=
查看raft工作状态,执行命令:
./consul operator raft list-peers
输出:
Node ID Address State Voter RaftProtocol
localhost.localdomain f4498061-1866-8061-3255-0bfcaf00e32a 127.0.0.1:8300 leader true 3
集群模式运行:
集群模式下,建议使用3~5台机器来做为SERVER节点,由于手头没有那么多机器,暂时用单个实例来模拟,执行命令:
./consul agent -server -datacenter=dc1 -node=192.168.56.101 -bootstrap -bootstrap-expect=1 -bind=192.168.56.101 -config-dir=./conf -data-dir=./data -enable-script-checks=true
查看成员,执行命令:
./consul members -detailed
输出:
Node Address Status Tags
192.168.56.101 192.168.56.101:8301 alive acls=0,ap=default,bootstrap=1,build=1.11.1:2c56447e,dc=dc1,ft_fs=1,ft_si=1,id=ec3c13cf-6c38-7643-3305-44fe559fea3c,port=8300,raft_vsn=3,role=consul,segment=
查看raft工作状态,执行命令:
./consul operator raft list-peers
输出:
Node ID Address State Voter RaftProtocol
192.168.56.101 ec3c13cf-6c38-7643-3305-44fe559fea3c 192.168.56.101:8300 leader true 3
与其他节点连接,组成集群,执行命令:
./consul join 其它节点IP
操作成功后,可继续查看成员和raft状态。
停止代理:
如果agent是以终端前台方式运行,可以操作CTRL+C,此时程序会安全退出。
否则,可以执行命令:
./consul leave
效果是一样的。
如果某个代理发生了长时间故障,集群中的其他节点会一直重试与之恢复,为了避免资源长时间消耗,此时建议从集群中移除故障节点,并添加新的节点进行代替。
移除节点的命令:
./consul force-leave 故障节点名称
注册服务:
可以使用配置文件和HTTP调用两种方法来注册服务。
配置文件方法:
在conf目录下添加web.json,输入内容:
{
"service": {
"name": "web",
"id":"web1",
"tags": ["rails"],
"port": 80
}
}
然后执行命令重新加载:
./consul reload
HTTP调用方法:
这种方法又包括手动调用和程序自动调用,两者的本质是一样的。程序自动调用可以更有效地控制服务实例的生存周期,跟随程序的启动和退出自动发起注册和注销操作。
调用格式:
curl \
--request PUT \
--data @payload.json \
http://127.0.0.1:8500/v1/agent/service/register?replace-existing-checks=true
payload.json的文件内容如下:
{
"name": "web",
"id":"web1",
"tags": ["rails"],
"port": 80
}
对应的命令为:
./consul services register web.json
注意:这里payload.json、web.json两个文件的格式是不一样的,不留意的话很容易弄混。
注销服务:
同样可以使用配置文件和HTTP调用两种方法来注销服务。
配置文件方法:
删除对应的配置文件,然后执行命令重新加载:
./consul reload
HTTP调用方法:
调用格式:
curl \
--request PUT \
http://127.0.0.1:8500/v1/agent/service/deregister/服务实例ID
对应的命令为:
./consul services register web.json
对于程序发起的HTTP调用,需要注意的是,如果程序发生异常退出,并且没有相应的恢复机制,为避免人工干预的不及时性,建议为每一个注册的服务实例增加健康检测。
健康检测:
支持系统级和应用级健康检测的管理。
如果健康检测与服务相关联,则认为它是应用级,否则为系统级。系统级健康检测主要关注节点的健康状态,而应用级则主要关注服务实例的健康状态。
健康检测可以在配置文件中定义,也可以在运行时通过HTTP接口添加。通过HTTP接口创建的健康检测也会在该结点进行持久化。
支持多种健康检测类型,包括:
1. 定时脚本检测
调用外部应用程序来执行健康状况检测,以适当的退出代码退出,并可能产生一些输出。输出限制为4KB,大于4kb的输出将被截断。
默认情况下,脚本检查将配置30秒的超时时间,超时后Consul将尝试强制终止检测脚本和它产生的任何子进程。
在Consul 0.9.0和更高版本中,代理必须配置-enable-script-checks=true以启用脚本检测。
2. 定时HTTP检测
调用HTTP向指定的URL发送请求,服务的状态取决于HTTP响应代码:任何2xx代码都被视为检测通过,429是太多请求的警告,其他任何状态码都代表检测失败。
默认情况下,HTTP检查是GET请求,除非method字段指定了不同的方法。可以通过header字段来设置其它header字段信息,以字符串列表映射的形式设置。
同样的,输出限制为4KB,大于4kb的输出将被截断。HTTP检查也支持SSL。默认要求有效的SSL证书,可以通过设置TLSSkipVerify为true关闭证书检查。
3. 定时TCP检测
调用TCP连接对指定的IP:PORT地址进行检测,服务的状态取决于连接尝试是否成功。
4. 定时TTL检测
TTL检测会在TTL时间内保留上次的检测状态,检测状态必须通过HTTP接口定期更新,如果外部系统无法在给定的TTL内更新状态,则检查设置为失败状态。
这个检测机制依靠应用程序直接报告其健康状况。例如,健康的应用程序可以定期向HTTP端点发送状态更新,如果应用程序失败,TTL将过期,健康检查进入危险状态。
系统级和应用级健康检测注册方法与服务注册相似,也包括配置文件和HTTP调用两种方法。这里主要讲应用级健康检测的使用,系统级的与之相似,主要差异在于配置文件格式和调用接口有所区别。
应用级配置文件方法:
修改web.json,加入check部分,这里加入了HTTP检测:
{
"service": {
"name": "web",
"id":"web1",
"tags": ["rails"],
"port": 80,
"check": {
"HTTP":"http://localhost:8080",
"Interval": "10s",
"Timeout": "5s"
}
}
}
然后执行命令重载配置:
./consul reload
应用级HTTP调用方法:
修改payload.json,加入check部分,内容如下:
{
"name": "web",
"id":"web1",
"tags": ["rails"],
"port": 80,
"check": {
"HTTP":"http://localhost:8080",
"Interval": "10s",
"Timeout": "5s"
}
}
然后发起调用:
curl \
--request PUT \
--data @payload.json \
http://127.0.0.1:8500/v1/agent/service/register?replace-existing-checks=true
查询服务:
支持HTTP和DNS查询两种方法,对于DNS方法,域名以 TAG.NAME.service.consul 格式给定的。
HTTP方法:
例如查询上面注册的web服务实例,执行命令:
curl \
http://127.0.0.1:8500/v1/health/service/web?passing \
| python -m json.tool
输出:
[
{
"Checks": [
{
"CheckID": "serfHealth",
"CreateIndex": 92440,
"Definition": {},
"ExposedPort": 0,
"Interval": "",
"ModifyIndex": 92440,
"Name": "Serf Health Status",
"Node": "192.168.56.101",
"Notes": "",
"Output": "Agent alive and reachable",
"ServiceID": "",
"ServiceName": "",
"ServiceTags": [],
"Status": "passing",
"Timeout": "",
"Type": ""
}
],
"Node": {
"Address": "192.168.56.101",
"CreateIndex": 5,
"Datacenter": "dc1",
"ID": "ec3c13cf-6c38-7643-3305-44fe559fea3c",
"Meta": {
"consul-network-segment": ""
},
"ModifyIndex": 92466,
"Node": "192.168.56.101",
"TaggedAddresses": {
"lan": "192.168.56.101",
"lan_ipv4": "192.168.56.101",
"wan": "192.168.56.101",
"wan_ipv4": "192.168.56.101"
}
},
"Service": {
"Address": "",
"Connect": {},
"CreateIndex": 96810,
"EnableTagOverride": false,
"ID": "web1",
"Meta": null,
"ModifyIndex": 96810,
"Port": 80,
"Proxy": {
"Expose": {},
"MeshGateway": {},
"Mode": ""
},
"Service": "web",
"Tags": [
"rails"
],
"Weights": {
"Passing": 1,
"Warning": 1
}
}
}
]
DNS方法:
例如,查询上面注册的web服务,执行命令:
dig @127.0.0.1 -p 8600 web.service.consul
输出:
; <<>> DiG 9.9.4-RedHat-9.9.4-61.el7 <<>> @127.0.0.1 -p 8600 web.service.consul
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 39228
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;web.service.consul. IN A
;; ANSWER SECTION:
web.service.consul. 0 IN A 192.168.56.101
;; Query time: 0 msec
;; SERVER: 127.0.0.1#8600(127.0.0.1)
;; WHEN: 六 12月 25 20:45:18 CST 2021
;; MSG SIZE rcvd: 63
也可以进一步查询指定标签的服务实例,执行命令:
dig @127.0.0.1 -p 8600 rails.web.service.consul
Web UI:
在启动命令中加入-ui选项即可开启Consul的自带Web管理界面,执行命令:
./consul agent -server -datacenter=dc1 -node=192.168.56.101 -bootstrap -bootstrap-expect=1 -bind=192.168.56.101 -config-dir=./conf -data-dir=./data -enable-script-checks=true -ui
在网页中输入网址:http://127.0.0.1:8500/ui
由于Consul HTTP默认只在127.0.0.1上提供服务,不方便远程查询,可以使用-client参数修改,如:
./consul agent -server -datacenter=dc1 -node=192.168.56.101 -bootstrap -bootstrap-expect=1 -bind=192.168.56.101 -config-dir=./conf -data-dir=./data -enable-script-checks=true -ui -client=0.0.0.0