导语:什么是服务注册中心?为什么需要服务注册中心?本文从服务发现的必要性入手,并对几款应用比较广泛的服务发现组件进行学习总结,分析每个组件使用的协议算法即原理,最后总结如果我们自己搭建一个服务发现组件需要实现什么基本功能?并且在实战项目中如何选择合适的服务发现组件?
单服务架构向微服务架构转变时,服务间的互相发现是一个必要环节。早期微服务拆分时,可以将服务所在的ip写死在配置文件中来进行服务调用,但随着节点的增多,维护ip配置文件会很耗精力,并且当某台机器挂掉后,不能及时将ip剔除,这个时候服务发现的必要性就体现出来了,他能够自动的发现所服务所在的ip,同时也能及时剔除不可用的节点。
目前服务发现有很多优秀的开源项目可用,例如zookeeper,Eureka,Consul,Etcd等。
一个服务注册中心,以下基本功能要满足:
zookeeper是一个分布式应用程序协调服务,是hadoop下的一个子项目,很多分布式服务都采用zookeeper作为组件。zookeeper可以干这些事情:配置管理、名字服务、分布式同步以及集群管理。
本文将针对zookeeper的名字服务进行讲解。
在了解zookeeper的基本原理前,一些基本概念需要清楚。
znode是zookeeper的基本数据结构,使用类似文件系统的树状结构进行znode管理,其中根路径以 / 开头,如下图1:
根节点 / 包含了两个字节点 /module1,/module2,而节点 /module1 又包含了三个字节点 /module1/app1,/module1/app2,/module1/app3。在zk中,节点以绝对路径表示,不存在相对路径,且路径最后不能以 / 结尾(根节点除外)。zk中每新来一个任务(相当于注册),在相应目录下增加一个节点即可,节点中存储着数据信息。
znode共有四种类型:
持久的保存znode,哪怕znode的创建者已不在,例如zk持久化znode保存从节点的任务分配情况。
与创建者的状态相关,以下两种情况,znode会被删除:1. 创建znode的客户端因回话超市或主动关闭而终止;2. 某个客户端主动删除该节点(不一定是创建者);
另,临时节点不允许有子节点。
创建一个有序节点后,znode节点会被分配一个唯一递增的整数,这个整数就是序号,追加到节点末尾,例如tesk/tesk-1,可以通过这种方式直观的查看znode的创建顺序。
3.1节了解了zookeeper是什么以及一些基本概念,下面看zk是如何作为服务注册中心工作的。
服务提供方启动服务并在zk注册。本质就是建立跟zk的回话,并在zk下创建一个节点,节点下保存自己的url地址,供调用者使用。例如/ucc/test_server/providers下注册一个节点,并写上自己的url,表示test_server这个server提供的一个服务。
消费者启动后,会向zk订阅自己需要的服务,其实就是解析上一步提供者的url地址作为服务地址列表。同时,消费者会创建一个临时节点,节点内写入自己的url,表示自己是消费者。
基于从zk中取出来的服务提供者url,基于负载均衡算法,选择其中一个地址进行调用。
如果在providers下建立新的节点,并通知该服务的相关消费者。providers建立的节点也都是临时节点,如果剔除提供者,也要通知相关的消费者。
zk宕机后,服务的提供者有变化,则不能通知到相关消费者,但是消费者可以用之前已获取的服务地址。
通过以上的基本注册流程,我们会有以下疑问:
zk通常是以远程服务的方式提供访问,如果频繁跟zk交互获取节点下的内容,就会造成很多不必要的消耗,如下图:
客户端C2在前两次轮询中没有找到自己想要的节点,轮询三次才找到自己要的,因此zk里用的是通知机制。
zk的通知机制允许客户端向关心的目录节点进行监听,一旦节点发生改变,zk就会通知客户端。监听过程如下:
客户端在注册完watcher的同时,会在本地的watchmanager保存watcher对象,当zk进行回调时,会触发watchmanager里的watcher事件进行相应操作。事件如下:
watch具有以下特性:
watchedEvent是zk中watcher通知的最小单元,该数据结构中只包含三部分:通知状态,事件类型和节点路径。他只会告诉客户端发生了事件,但不会告诉事件的具体内容。
zk的通知机制是不可靠的,因为回调过程中有可能失败,且失败后该回调消息并不会重写,具体原因可参考下面这篇文章。
https://cloud.tencent.com/developer/article/1158972
总结:zk不会提供单独的api给你让你获取服务地址,而是采用了监听机制,服务在启动时需要告诉zk我需要监听哪些服务的地址,当被监听的服务有变动时,zk进行通知。
在介绍zk的一致性之前,需要先介绍一下zk的灵魂算法Paxos。
zk的一致性是有zab协议做的,zab协议是基于paxos算法的。
Paxos算法Lamport是1998年提出,用于解决分布式中消息传递的一致性,并拿了2013年的图领奖。该算法一经问世持续性垄断分布式中的一致性算法。
三个角色:
Proposer: 提出提案 (Proposal)。Proposal信息包括提案编号 (Proposal ID) 和提议的值 (Value)。
Acceptor:参与决策,回应Proposers的提案。收到Proposal后可以接受提案,若Proposal获得多数Acceptors的接受,则称该Proposal被批准。
Learner:不参与决策,从Proposers/Acceptors学习最新达成一致的提案(Value)。
该算法通过一个决议需要两个阶段:
Accept阶段做承诺时,遵循两个原则:
请求量小的时候没什么问题,超半数同意则接受提案,但请求量上来时改算法可能会形成活锁,导致没有结果,如图:
S1提出P3.1,3是时间戳,1是服务器id,在还没决策之前,S发出提议P3.5,S3拒绝了P3.1,同时S4又发出了P4.1,于是S3拒绝了P3.5,此时S5又发出了P5.5,依次往后,永远选不出最终的提议。因此mutil-paxos算法出现
原始算法在高并发下有两点问题:
mutil-paxos算法改进:在所有proposer中选一个leader,由leader唯一的提交proposal给accpetor进行表决,这样没有了proposer的竞争,解决活锁的问题。
zk的默认算法是mutil-paxos的变形fast-paxos。
开发环境一般是一个zookeeper机器,但实际项目中通常是一个zk集群,既然是一个集群,那必然涉及到集群leader的选举以及一致性的问题,本节将zk的leader选举算法。
zookeeper的角色:
zk中的事务标识符:群首会为每一个事务分配一个标识符,称之为会话ID——zxid,zxid是64位的整数,分为两部分,高32位是时间戳(epoch),每次一个leader被选出来,它都会有一个新的epoch,低32位是一个递增计数器。
服务器状态:
以上为一些基本术语。下面开始介绍zk的群首选举。
当集群中所有服务器的状态都是LOOKING状态,那么这些服务器之间就会进行通信选举leader,每一个选举的通知消息就是一张选票,选票包括服务器的标识和最近执行事务的zxid信息,例如一张vote信息为(1,5),标识该服务器的sid为1,最近执行的事务zxid为5(投票的zxid只有一个数字,不是64位)。选举过程如下:
最终,只有最新的服务器才会赢得选举。如图:
在选举过程中,也有可能出现消息延迟,如下:
虽然S2因为消息延迟导致选错了leader,但不影响最终的leader选举,因为leader选举已经达到了法定的仲裁数量,S2认为S3是leader,因此在后续过程中发消息给S3,但S3是错误的leader,所以不会返回,达到超时时间后,S2会重新找寻leader。
注:一组服务器达到仲裁法定数量是必须条件,如果过多服务器退出,无法得到仲裁法定数量,zk也就启动不起来。
为了解决延时问题,zk会延长选举时间,例如让s2进行群首选举的时候多等待一会,那就会选出正确的leader,默认延长时间200ms,这个时间已经比预计的消息延迟时间(1ms或几ms)要长得多,相比因为选错leader最后进行重新选举,消息延迟是值得的。
收到一个写请求后,follower会将请求发给leader,由leader执行该事务,接下来一个服务器如何确认一个事务是否已经提交,由此引出了Zab:zookeeper原子广播协议。
通过该协议提交一个事务非常简单:
第二步的follower应答过程前,需要做一些检查操作,检查消息是否来自群首?确认群首广播的笑嘻嘻是按照顺序执行。
zab保障了以下几个重要属性:
zk的群首有可能崩溃,因此需要选举新的群首,其中zxid的高32位epoch时间错代表了管理权的变化时间,每个时间戳代表每个群首统治的时间,因此可以很容易根据epoch整理出事务的顺序,这样就算群首崩溃可能很快恢复。
当群首崩溃后,为了保证所有的事务能够继续提交,zab有以下保证:
为了保证第一点,老群首崩溃后,选出的新群首不会马上处于活动状态,而是先确认仲裁数量的服务器认可当前这个群首的时间戳,即新群首的事务时间戳一定是最新的。
在群首选举的时候,我们会选zxid最大的作为群首,因此不用出现follower将提议发送给leader,而是leader将提议发送给follower。
时间戳更新后,有两种方式同步follower之后的提议:
在CAP(C-数据一致性;A-服务可用性;P-服务对网络分区故障的容错性,这三个特性在任何分布式系统中不能同时满足,最多同时满足两个)定理中,zk是cp的,但是他不一定能保证每次服务都是可用的,例如上面提到的监听回调失败问题,另外zk不是为高可用设计的,现在很多项目都是多地部署,或者在云上有好几个分区,但zk只能有一个leader,在一致性上会变慢,另外如果遇到网络抖动,会话会丢失,leader选举过程也很慢,30~120s左右,期间zk不可用。即没有保证服务的可用性。
基于zk满足不了高可用场景,因此很多团队用了其他的服务注册组件。
Eureka是netfix开源的,主要用于服务注册和服务发现,由两部分组成:Eureka服务器&Eureka客户端。特点如下:
客户端向服务器注册时,需要提供自身源数据,例如ip,port,运行状况等
客户端30秒发送一次心跳保活,正常情况下,Eureka服务器90s内没有收到客户端的消息,就会从注册表中剔除
client向server获取注册表信息,并缓存到本地
client在程序关闭时会发送取消请求,Eureka服务器收到后从注册表中剔除
eureka没有leader,他采用p2p这种无中心网络架构,各点之间互相连接,互相通知,如下:
处理流程:
针对多地部署,每个分区都会至少有一个Eureka server,各个server之间进行数据同步
问题:
Eureka采用对等通信(p2p),所以没有master/slave之分。Eureka是弱一致性的,即CAP中,Eureka采用了AP。
Eureka的p2p模式,任何server都可以接受数据并进行写操作,然后点对点进行数据互相更新。
节点中相互复制和更新,如何保证数据不被写乱呢?先看一下注册表中服务实例的信息:
@JsonProperty("instanceId") String instanceId, @JsonProperty("app") String appName, @JsonProperty("appGroupName") String appGroupName, @JsonProperty("ipAddr") String ipAddr, @JsonProperty("sid") String sid, @JsonProperty("port") PortWrapper port, @JsonProperty("securePort") PortWrapper securePort, @JsonProperty("homePageUrl") String homePageUrl, @JsonProperty("statusPageUrl") String statusPageUrl, @JsonProperty("healthCheckUrl") String healthCheckUrl, @JsonProperty("secureHealthCheckUrl") String secureHealthCheckUrl, @JsonProperty("vipAddress") String vipAddress, @JsonProperty("secureVipAddress") String secureVipAddress, @JsonProperty("countryId") int countryId, @JsonProperty("dataCenterInfo") DataCenterInfo dataCenterInfo, @JsonProperty("hostName") String hostName, @JsonProperty("status") InstanceStatus status, @JsonProperty("overriddenstatus") InstanceStatus overriddenStatus, @JsonProperty("overriddenStatus") InstanceStatus overriddenStatusAlt, @JsonProperty("leaseInfo") LeaseInfo leaseInfo, @JsonProperty("isCoordinatingDiscoveryServer") Boolean isCoordinatingDiscoveryServer, @JsonProperty("metadata") HashMap<String, String> metadata, @JsonProperty("lastUpdatedTimestamp") Long lastUpdatedTimestamp, @JsonProperty("lastDirtyTimestamp") Long lastDirtyTimestamp, @JsonProperty("actionType") ActionType actionType, @JsonProperty("asgName") String asgName) |
---|
其中,lastDirtyTimestamp表示该server数据最近的更新时间
但server1和server2中的脏数据如何解决?
心跳保活,Eureka会每30秒发送一次心跳,发送心跳的时候就会知道是否要增加节点,没心跳90s内也会剔除掉无用节点,所以Eureka的数据一致性无法做到强一致。
Eureka使用Ribbon进行负载均衡,Ribbon 是一个服务调用的组件,并且是一个客户端实现负载均衡处理的组件。
策略 | 说明 |
---|---|
RoundRobinRule 轮询策略 | 默认值,启动的服务被循环访问 |
RandomRule 随机选择 | 随机从服务器列表中选择一个访问 |
BestAvailableRule 最大可用策略 | 先过滤出故障服务器,再选择一个当前并发请求数最小的服务 |
WeightedResponseTimeRule 带有加权的轮询策略 | 对各个服务器响应时间进行加权处理,再采用轮询的方式获取相应的服务器 |
AvailabilityFilteringRule 可用过滤策略 | 先过滤出故障的或并发请求大于阈值的服务实例,再以线性轮询的方式从过滤后的实例清单中选出一个 |
ZoneAvoidanceRule 区域感知策略 | 先使用主过滤条件(区域负载器,选择最优区域)对所有实例过滤并返回过滤后的实例清单,依次使用次过滤条件列表中的过滤条件对主过滤条件的结果进行过滤,判断最小过滤数(默认1)和最小过滤百分比(默认0),最后对满足条件的服务器则使用RoundRobinRule(轮询方式)选择一个服务器实例 |
策略均比较简单,源码在ribbon的ribbon-loadbalancer下,可以自行查看。具体使用哪个策略可以在配置文件中进行配置。
etcd是CoreOS2013年6月的开源项目,使用go语言编写,是一个高度一致的key-value存储,本质就是一个key-value存储组件。其应用都是针对键值存储的功能展开,应用场景较多的是服务注册和发现。
特点如下:
能力:
类似zk,服务器也有状态:
状态流转如下:
投票过程:
如果follower节点没有收到任何通知消息,则该节点会投自己一票,让自己变为candidate。
每个follower节点在某个任期内只能向candidate投出一张选票,当该candidate获得过半选票则成为leader。一个candidate节点在等待选举的过程中可能出现以下情况:
针对candidate的第三种情况,处理办法如下:
raft设置了一个选举随机超时区间,比如说150ms~300ms,当follower成为candidate后,在该区间内随机设置一个超时时间,该时间段内未赢得选举,则切换成为follower状态重新开始选举,这样就避免了选票被瓜分的情况,并且在实际应用中,该算法也能够快速的选举出一个leader。
该随机算法的可用性官方是有做保障的。起初该团队设计的方案是为每个candidate设置一个唯一的排名值,以便在竞争候选人时进行抉择。如果一个候选人发现另一个候选人的排名比自己高,那么该候选人就变为follower,但在实验过程中,这种方法的可用性不好,怎么调整都有问题,所以最后用了这种随机算法。
日志复制中的名词解释:
leader选举出来后,整个系统开始进入工作状态,客户端的写请求都会给到leader,由leader进行请求的写入和请求的提交,这里的请求即是日志,流程如下:
以上其实就是数据同步的过程,当然第6步的应用有可能失败,所以raft并不是强一致性,但他会保证最终一致性。因为leader会持续的发送心跳包让follower同步自己的状态机。从这里就能看到etcd的心跳是整个系统的基石。
另外再说下log replication中的安全性,首先要知道raft中commit和apply的区别,commit是上述第4步之前,apply才是将条目真正的写入日志,raft确保在apply后,日志不会回滚,这是该协议所保障的安全性,为了这个安全性,raft有以下保证:
第一个特性基于以下进行保证:leader在每个任期内,保证一个log index只创建一个log entry,可以理解为联合主键
第二个特性由每次收到心跳包后的一致性检查所保证,leader将新的条目附加在日志中发给follower,如果follower该term下index的条目,即不一致,就会拒绝leader新添加的这个entry。在不出现异常的情况下,一致性会很容易保障,我们看下异常的情况。
异常产生:leader crash或者follower crash,都会造成日志的不同步,不一致会加速一系列的崩溃,如下图:
a~表示了follower6中可能的状态,a和b是少了条目,c和d是多了条目,e和f出现了条目跟leader不一致,e和f可能是因为以下情况出现:server是一个leader,在commit前宕机,重启后又重新选为leader,加了些条目,commit前又crash。
针对不一致的情况,leader会强行让follower跟自己一致,即follower的数据会被覆盖。如何强行一致呢?leader会使用一个nextIndex来跟follower维持同步,举个栗子:
以上是保证最终一致性的过程,raft演示动画可以更好的帮助理解http://www.kailing.pub/raft/index.html#home
,不过我还是建议直接读raft的论文,这样理解的更清楚。
介绍完三个组件,总结下如果我们设计一个服务注册中心,该如何设计?
1. 该系统须实现基本的服务注册、服务下线、心跳保活、服务剔除,服务信息修改功能;
2. 需要选择合适的一致性算法,保证集群内部数据的一致性;
3. 需要选择一个负载均衡算法来将请求均衡到服务注册中心的集群上。
再升华一下,对比几个服务注册组件,服务发现与注册的流程其实就是如何协调一个分布式系统,在分布式集群中如何保持数据的一致性,如何在分布式集群中做负载均衡,至于服务注册组件的选型,主要还是看业务方的需求,例如:
跳出服务注册功能来看这三个组件,其实就是分布式系统中,如何做数据的一致性,如何节点保活等。一致性主要还是看用什么协议,目前分布式中主流的一致性协议无非就是paxos的一系列变种和raft,如果想要一致性强的,那就用paxos,想要可用性搞高的就选raft。
参考:
zookeeper:
《zookeeper分布式过程协同技术详解》
https://www.jianshu.com/p/c68b6b241943
https://juejin.cn/post/6844903684610981895
https://cloud.tencent.com/developer/article/1158972
https://zhuanlan.zhihu.com/p/31780743
Eureka:
整体架构:https://www.infoq.cn/article/emmw80joe8l0v4qaaizt
一致性:https://cloud.tencent.com/developer/article/1083131
负载均衡:https://www.jianshu.com/p/c15e80deb5e8
etcd:
etcd系列:https://cloud.tencent.com/developer/article/1630711
raft协议论文:https://raft.github.io/raft.pdf
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。