1.1 zk简介
ZooKeeper 由雅虎研究院开发,后来捐赠给了 Apache。 ZooKeeper 是一个开源的分布式应用程序协调服务器,其为分布式系统提供一致性服务。其一致性是通过基于 Paxos 算法的ZAB 协议完成的。
分布式应用程序可以基于 Zookeeper 实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。
1.2 zk特性
zk 是如何保证分布式系统的一致性的呢?是因为 zk 具有以下几方面的特点:
-
顺序一致性: zk 接收到的 N 多个事务请求(写操作请求),其会被严格按照接收顺序应用到 zk 中。
- 所有的更新都是全局有序的,每个更新都有一个唯一的时间戳,这个时间戳称为zxid(Zookeeper Transaction Id)。
- 而读请求只会相对于更新有序,也就是读请求的返回结果中会带有这个 Zookeeper 最新的 zxid 。
-
原子性: 所有事务请求的结果在 zk 集群中每一台主机上的应用情况都是一致的。要么全部应用成功,要么全部失败。
-
单一视图: 无论 Client 连接的是 zk 集群中的哪个主机,其看到的数据模型都是一致的。
-
可靠性:一旦 zk 成功应用了某事务,那么该事务所引发的 zk 状态变更会被一直保留下来,直到另一个事务将其修改。
-
最终一致性: 一旦一个事务被成功应用, zk 可以保证在一个很短暂时间后, Client 最终能够从 zk 上读取到最新的数据状态。注意,不能保证实时读取到。
1.3 拜占庭将军问题
拜占庭将军问题:在分布式环境下,且信道不可靠的情况下,如何就某个决议达成一致性的问题。
1982年,莱斯利·兰伯特(Leslie Lamport)与另两人共同发表了论文The Byzantine Generals Problem,提出了一种计算机容错理论。在理论描述过程中,为了将所要描述的问题形象的表达出来,Lamport设想出了下面这样一个场景:
拜占庭帝国有许多支军队,不同军队的将军之间必须制订一个统一的行动计划,从而做出进攻或者撤退的决定,同时,各个将军在地理上都是被分隔开来的,只能依靠军队的通讯员来进行通讯。然而,在所有的通讯员中可能会存在叛徒,这些叛徒可以任意篡改消息,从而达到欺骗将军的目的。(无解,在存在消息丢失的不可靠信道上试图通过消息传递的方式达到一致性是不可能的。)
这就是著名的“拜占庭将军问题”。实际上拜占庭将军问题是一个分布式环境下的协议问题,拜占庭帝国军队的将军们必须全体一致的决定是否攻击某一支敌军。
1.4 Paxos 算法
1.4.1 Paxos算法简介
Paxos 算法是莱斯利·兰伯特(Leslie Lamport)1990 年提出的一种基于消息传递的、具有高容错性的一致性算法。Google Chubby 的作者 Mike Burrows 说过,世上只有一种一致性算法,那就是 Paxos,所有其他一致性算法都是 Paxos 算法的不完整版。 Paxos 算法是一种公认的晦涩难懂的算法, 并且工程实现上也具有很大难度。较有名的 Paxos 工程实现有 Google Chubby、ZAB、 微信的 PhxPaxos 等。
Paxos 算法是用于解决什么问题的呢? Paxos 算法要解决的问题是,在分布式系统中如何就某个决议达成一致。
1.4.2 Paxos与拜占庭将军问题
拜占庭将军问题是由 Paxos 算法作者莱斯利·兰伯特提出的点对点通信中的基本问题。该问题要说明的含义是, 在不可靠信道上试图通过消息传递的方式达到一致性是不可能的。 所以, Paxos 算法的前提是不存在拜占庭将军问题,即信道是安全的、可靠的,集群节点间传递的消息是不会被篡改的。
1.4.3 算法描述
三种角色
在 Paxos 算法中有三种角色,分别具有三种不同的行为。但很多时候,一个进程可能同时充当着多种角色。
Proposal,提案
- Proposer:提案者。
- Acceptor:表决者。
- Learner:学习者,同步者。
Paxos 算法的一致性
Paxos 算法的一致性主要体现在以下几点:
-
每个提案者在提出提案时都会首先获取到一个递增的、全局唯一的提案编号 N,然后将该编号赋予其要提出的提案
关于 N 的生成,有两种方式:全局性生成器;提案者自身维护 N。 -
每个表决者在 accept 某提案后,会将该提案的编号 N 记录在本地,这样每个表决者中保存的已经被 accept 的提案中会存在一个编号最大的提案,其编号假设为 maxN。 每个表决者仅会 accept 编号大于自己本地 maxN 的提案。
-
在众多提案中最终只能有一个提案被选定。
-
一旦一个提案被选定,则其它学习者会主动同步(Learn)该提案到本地。
-
没有提案被提出则不会有提案被选定。
Paxos 故事:
有一个叫做 Paxos 的小岛(Island)上面住了一批居民,岛上面所有的事情由一些特殊的人 决定,他们叫做议员(Senator)。
议员的总数(SenatorCount)是确定的,不能更改。
岛上每次环境事务的变更都需要通过一个提议(Proposal),每个提议都有一个编号 (PID),这个编号是一直增长的,不能倒退。
每个提议都需要超过半数((SenatorCount)/2+1)的议员同意才能生效。
每个议员只会同意大于当前编号的提议,包括已生效的和未生效的。
如果议员收到小于等于当前编号的提议,他会拒绝,并告知对方:你的提议已经有 人提过了。这里的当前编号是每个议员在自己记事本上面记录的编号,他不断更新这个编号。 整个议会不能保证所有议员记事本上的编号总是相同的。 现在议会有一个目标:保证所有的议员对于提议都能达成一致的看法。 好,现在议会开始运作,所有议员一开始记事本上面记录的编号都是 0。
有一个议员发了一个提议:
将电费设定为 1 元/度。他首先看了一下记事本,嗯,当前提议编号是 0,那么我的这个 提议的编号就是 1,于是他给所有议员发消息: 1 号提议,设定电费 1 元/度。其他议员收到 消息以后查了一下记事本,哦,当前提议编号是 0,这个提议可接受,于是他记录下这个提 议并回复:我接受你的 1 号提议,同时他在记事本上记录:当前提议编号为 1。发起提议的 议员收到了超过半数的回复,立即给所有人发通知: 1 号提议生效!收到的议员会修改他的 记事本,将 1 好提议由记录改成正式的法令,当有人问他电费为多少时,他会查看法令并告 诉对方: 1 元/度。现在看冲突的解决:
假设总共有三个议员 S1、 S2 与 S3, S1 和 S2 同时发起了一个提议:设定电费,提议编号均为 1。S1 想设为 1 元/度,S2 想设为 2 元/度。此时 S3 先收到了 S1 的提议,于是他一查 记事本,发现提议编号 1 大于当前的编号 0,所以同意了该提议。紧接着他又收到了 S2 的 提议,结果他再查记事本,发现这个提议的编号等于当前记录的编号 1,于是他拒绝了这个提议:对不起,这个提议先前提过了。于是 S2 的提议被拒绝, S1 正式发布了提议: 1 号提 议生效。 S2 向 S1 或者 S3 打听并更新了 1 号法令的内容,然后他可以选择继续发起 2 号提 议。Paxos 对外是如何工作的:
情况一:
屁民甲(Client)到某个议员(ZKServer)那里询问(Get)某条法令的情况(ZNode 的数据),议员毫不犹豫的拿出他的记事(localStorage),查阅法令并告诉他结果。情况二:
屁民乙(Client)到某个议员(ZKServer)那里要求政府归还欠他的一万元钱,议员让他在办公 室等着,自己到议会将这个作为一个提议发给了所有议员。多数议员表示欠屁民的钱一定要 还,于是该访问从国库中拿出一万元还债,国库总资产由 100 万变成 99 万。屁民乙拿到钱 回去了(Client 函数返回)。
1.4.4 2PC 与 3PC
Paxos 对于提案的提交算法有两种方案, 2PC 与 3PC。
- 2PC: Two Phase Commit,即 prepare -> accept。
- 3PC: Three Phase Commit,即 prepare -> accept -> commit。
它们的区别主要就在于 accept 阶段中是否包含 commit 功能。具体看下面的描述。
3PC
Paxos 算法的 3PC 执行过程划分为三个阶段:准备阶段 prepare、接受阶段 accept,与提交阶段 commit。
prepare 阶段
- 提案者(Proposer)准备提交一个编号为 N 的提议,于是其首先向所有表决者(Acceptor)发送 prepare(N)请求,用于试探集群是否支持该编号的提议。
- 每个表决者(Acceptor)中都保存着自己曾经 accept 过的提议中的最大编号 maxN。当一个表决者接收到其它主机发送来的 prepare(N)请求时,其会比较 N 与 maxN 的值。有以下几种情况:
- 若 N 小于 maxN,则说明该提议已过时,当前表决者采取不回应或回应 Error 的方式来拒绝该 prepare 请求;
- 若 N 大于 maxN,则说明该提议是可以接受的,当前表决者会首先将该 N 记录下来,并将其曾经已经 accept 的编号最大的提案 Proposal(myid,maxN,value)反馈给提案者,以向提案者展示自己支持的提案意愿。其中第一个参数 myid 表示该提案的提案者标识 id,第二个参数表示其曾接受的提案的最大编号 maxN,第三个参数表示该提案的真正内容 value。当然,若当前表决者还未曾 accept 过任何提议,则会将Proposal(null,null,null)反馈给提案者。
- 在 prepare 阶段 N 不可能等于 maxN。这是由 N 的生成机制决定的。要获得 N 的值,其必定会在原来数值的基础上采用同步锁方式增一。
accept 阶段
- 当提案者(Proposer)发出 prepare(N)后,若收到了超过半数的表决者(Accepter)的反馈,那么该提案者就会将其真正的提案 Proposal(myid,N,value)发送给所有的表决者。
- 当表决者(Acceptor)接收到提案者发送的 Proposal(myid,N,value)提案后,会再次拿出自己曾经 accept 过的提议中的最大编号 maxN,或曾经记录下的 prepare 的最大编号,让 N与它们进行比较,若 N 大于等于这两个编号,则当前表决者 accept 该提案,并反馈给提案者。若 N 小于这两个编号,则表决者采取不回应或回应 Error 的方式来拒绝该提议。
- 若提案者没有接收到超过半数的表决者的 accept 反馈,则有两种可能的结果产生。一是放弃该提案,不再提出;二是重新进入 prepare 阶段,递增提案号,重新提出 prepare请求
commit 阶段
若提案者接收到的反馈数量超过了半数,则其会向外广播两类信息:
- 向曾 accept 其提案的表决者发送“可执行数据同步信号”,即让它们执行其曾接收到的提案;
- 向未曾向其发送 accept 反馈的表决者发送“提案 + 可执行数据同步信号”,即让它们接受到该提案后马上执行。
2PC
2PC 与 3PC 的区别是,在提案者接收到超过半数的表决者对于 parepare 阶段的反馈后,其会向所有表决者发送真正的提案 proposal。当表决者接受到 proposal 后就直接将其同步到了本地。
那么,为什么不直接使用 2PC,而要使用 3PC 呢?是因为 2PC 中存在着较多的弊端。所以很多 Paxos 工业实现使用的都是 3PC 提交。但 2PC 提交的效率要高于 3PC 提交,所以在保证不出问题的情况下,是可以使用 2PC 提交的。
1.4.5 Paxos活锁问题
前面所述的Paxos算法在实际工程应用过程中,根据不同的实际需求存在诸多不便之处,所以也就出现了很多对于基本 Paxos 算法的优化算法,以对 Paxos 算法进行改进, 例如, Multi Paxos、 Fast Paxos、 EPaxos。
例如, Paxos 算法存在“活锁问题”, Fast Paxos 算法对 Paxos 算法进行了改进:只允许一个进程提交提案,即该进程具有对 N 的唯一操作权。该方式解决了“活锁”问题。
1.5 ZAB协议
1.5.1 ZAB 协议简介
ZAB , Zookeeper Atomic Broadcast, zk 原子消息广播协议,是专为 ZooKeeper 设计的一种支持崩溃恢复的原子广播协议,在 Zookeeper 中,主要依赖 ZAB 协议来实现分布式数据一致性。
Zookeeper 使用一个单一主进程来接收并处理客户端的所有事务请求,即写请求。当服务器数据的状态发生变更后,集群采用 ZAB 原子广播协议, 以事务提案 Proposal 的形式广播到所有的副本进程上。 ZAB 协议能够保证一个全局的变更序列,即可以为每一个事务分配一个全局的递增编号 xid。
当 Zookeeper 客户端连接到 Zookeeper 集群的一个节点后,若客户端提交的是读请求,那么当前节点就直接根据自己保存的数据对其进行响应;如果是写请求且当前节点不是Leader,那么节点就会将该写请求转发给 Leader, Leader 会以提案的方式广播该写操作,只要有超过半数节点同意该写操作,则该写操作请求就会被提交。然后 Leader 会再次广播给所有订阅者,即 Learner,通知它们同步数据。
1.5.2 ZAB 与 Paxos 的关系
ZAB 协议是 Paxos 算法的一种工业实现算法。但两者的设计目标不太一样。 ZAB 协议主要用于构建一个高可用的分布式数据主从系统,即 Follower 是 Leader 的从机,Leader 挂了,马上就可以选举出一个新的 Leader,但平时它们都对外提供服务。而 Fast Paxos 算法则是用于构建一个分布式一致性状态机系统,确保系统中各个节点的状态都是一致的。
另外, ZAB 还使用 Google 的 Chubby 算法作为分布式锁的实现,而 Google 的 Chubby 也是 Paxos 算法的应用。
zk 集群对于事务请求的处理是 Fast Paxos 算法的体现,即只允许 Leader 提出提案。其属于 3PC 提交。
但 Leader 选举是 Paxos 算法的体现,因为 Leader 宕机后,所有 Follower 均可提交提案,它们在最初都是“我选我”。其属于 2PC 提交。
二者有相同的地方:
- 都有一个 Leader,用来协调 N 个 Follower 的运行
- Leader 要等待超半数的 Follower做 出正确反馈之后才进行提案。
- 二者都有一个值来代表 Leader 的周期。ZAB 协议中,每个 Proposal 中都包含一个 epoch 值来代表当前的Leader周期,Paxos中名字为 Ballot 。
1.5.5 三类角色
为了避免 Zookeeper 的单点问题, zk 也是以集群的形式出现的。 zk 集群中的角色主要有以下三类:
- Leader: zk 集群中事务请求的唯一处理者;其也可以处理读请求。
- Follower:处理读请求;将事务请求转发给 Leader;对 Leader 发起的提案进行表决;同步 Leader 的事务处理结果;在 Leader 的选举过程中具有选举权与被选举权。
- Observer:不具有表决权,且在 Leader 选举过程中没有选举权与被选举权的 Follower。
Learner:学习者,同步者。 Learner = Follower + Observer
QuorumPeer = Participant = Leader + Follower
1.5.6 三个数据
在 ZAB 中有三个很重要的数据:
zxid: 64 位长度的 Long 类型,其高 32 位为 epoch,低 32 位为 xid。
epoch:每一个新的 Leader 都会有一个新的 epoch
xid:其为一个流水号
1.5.7 三种模式
ZAB 协议中对 zkServer 的状态描述有三种模式。这三种模式并没有十分明显的界线,它们相互交织在一起。
恢复模式:其包含两个重要阶段: Leader 的选举,与初始化同步
广播模式:其可以分为两类:初始化广播,与更新广播
同步模式:其可以分为两类:初始化同步,与更新同步
1.5.8 四种状态
zk 集群中的每一台主机,在不同的阶段会处于不同的状态。每一台主机具有四种状态。
LOOKING:选举状态
FOLLOWING: Follower 的正常工作状态
OBSERVING: Observer 的正常工作状态
LEADING: Leader 的正常工作状态
1.5.9 同步模式与广播模式
初始化同步
恢复模式具有两个阶段: Leader 选举与初始化同步。当完成 Leader 选举后,此时的 Leader 还是一个准 Leader,其要经过初始化同步后才能变为真正的 Leader。
具体过程如下:
- 为了保证 Leader( Learner = Follower + Observer) 向 Learner 发送提案的有序, Leader 会为每一个 Learner 服务器准备一个队列
- Leader 将那些没有被各个 Learner 同步的事务封装为 Proposal
- Leader 将这些 Proposal 逐条发给各个 Learner,并在每一个 Proposal 后都紧跟一个COMMIT 消息,表示该事务已经被提交, Learner 可以直接接收并执行
- Learner 接收来自于 Leader 的 Proposal,并将其更新到本地
- 当 Learner 更新成功后,会向准 Leader 发送 ACK 信息
- Leader 服务器在收到来自 Learner 的 ACK 后就会将该 Learner 加入到真正可用的 Follower列表或 Observer 列表。没有反馈 ACK,或反馈了但 Leader 没有收到的 Learner, Leader不会将其加入到相应列表。
zookeeper leader和learner的数据同步 https://blog.csdn.net/weixin_36145588/article/details/75043611
消息广播算法
当集群中的 Learner 完成了初始化状态同步,那么整个 zk 集群就进入到了正常工作模式了。
如果集群中的 Learner 节点收到客户端的事务请求,那么这些 Learner 会将请求转发给Leader 服务器。然后再执行如下的具体过程:
- Leader 接收到事务请求后,为事务赋予一个全局唯一的 64 位自增 id,即 zxid,通过zxid 的大小比较即可实现事务的有序性管理,然后将事务封装为一个 Proposal。
- Leader 根据 Follower 列表获取到所有 Follower,然后再将 Proposal 通过这些 Follower 的队列将提案发送给各个 Follower。
- 当 Follower 接收到提案后,会先将提案的 zxid 与本地记录的事务日志中的最大的 zxid进行比较。若当前提案的 zxid 大于最大 zxid,则将当前提案记录到本地事务日志中,并向 Leader 返回一个 ACK。
- 当 Leader 接收到过半的 ACKs 后, Leader 就会向所有 Follower 的队列发送 COMMIT消息,向所有 Observer 的队列发送 Proposal。
- 当 Follower 收到 COMMIT 消息后,就会将事务正式更新到本地。当 Observer 收到Proposal 后,会直接将事务更新到本地。
- 无论是 Follower 还是 Observer,在同步完成后都需要向 Leader 发送成功 ACK。
Observer 的数量问题
Observer 数量并不是越多越好,一般与 Follower 数量相同。 因为 Observer 数量的增多虽不会增加事务操作压力,但其需要从 Leader 同步数据, Observer 同步数据的时间是小于等于 Follower 同步数据的时间的。当 Follower 同步数据完成, Leader 的 Observer 列表中的Observer 主机将结束同步。那些完成同步的 Observer 将会进入到另一个对外提供服务的列表。那么,那些没有同步了数据无法提供服务的 Observer 主机就形成了资源浪费。
所以,对于事务操作发生频繁的系统,不建议使用过多的 Observer。
Leader 中保存的 Observer 列表其实有两个:
- all:包含所有 Observer。
- service:已经完成了从 Leader 同步数据的任务。 service <= all。其是动态的。
Leader 中保存的 Follower 列表其实也有两个:
- all:要求其中必须有过半的 Follower 向 Leader 反馈 ACK
- service:已经完成了从Leader 同步数据的任务。service <= all.其是动态的。
若service <= all/2 ,则说明同步失败,则Leader重新广播,Follower重新同步。
1.5.10 Leader选举
当 Leader 崩溃,或者 Leader 失去大多数的 Follower,这时 Zookeeper 进入恢复模式,恢复模式需要重新选举出一个新的 Leader,让所有的 Server 都恢复到一个正确的状态。
Zookeeper 的选举算法有两种:一种是基于 basic paxos 实现的,另外一种是基于 fast paxos 算法实现的。系统默认的选举算法为 fast paxos 。
-
不同阶段的选举流程
- 服务器启动时期的 Leader 选举。
- 服务器运行时期的 Leader 选举。
-
三种选举算法
Zookeeper源码分析-Zookeeper Leader选举算法:https://juejin.im/post/6844903672824987661
-
LeaderElection :使用 basic paxos 算法。
选择性了解:
- 1、选举线程由当前 Server 发起选举的线程担任,其主要功能是对投票结果进行统计,并选出推荐的 Server 。
- 2、选举线程首先向所有 Server 发起一次询问(包括自己)。
- 3、选举线程收到回复后,验证是否是自己发起的询问(验证 zxid 是否一致),然后获取对方的 id(myid),并存储到当前询问对象列表中,最后获取对方提议的 Leader相关信息(id,zxid),并将这些信息存储到当次选举的投票记录表中。
- 4、收到所有 Server 回复以后,就计算出 zxid 最大的那个 Server ,并将这个 Server 相关信息设置成下一次要投票的 Server 。
- 5、线程将当前 zxid 最大的 Server 设置为当前 Server 要推荐的 Leader ,如果此时获胜的 Server 获得
n/2+1
的 Server 票数,设置当前推荐的 Leader 为获胜的 Server ,将根据获胜的 Server 相关信息设置自己的状态,否则,继续这个过程,直到 Leader 被选举出来。
通过流程分析我们可以得出:要使 Leader 获得多数 Server 的支持,则 Server 总数必须是奇数
2n+1
,且存活的 Server 的数目不得少于n+1
。每个 Server 启动后都会重复以上流程。在恢复模式下,如果是刚从崩溃状态恢复的或者刚启动的 Server 还会从磁盘快照中恢复数据和会话信息,Zookeeper 会记录事务日志并定期进行快照,方便在恢复时进行状态恢复。
-
FastLeaderElection :使用 fast paxos 算法。
由于 LeaderElection 收敛速度较慢,所以 Zookeeper 引入了 FastLeaderElection 选举算法,FastLeaderElection 也成了Zookeeper默认的Leader选举算法。
FastLeaderElection 是标准的 Fast Paxos 的实现。它首先向所有 Server 提议自己要成为 Leader ,当其它 Server 收到提议以后,解决 epoch 和 zxid 的冲突,并接受对方的提议,然后向对方发送接受提议完成的消息。重复这个流程,最后一定能选举出Leader。
FastLeaderElection 算法通过异步的通信方式来收集其它节点的选票,同时在分析选票时又根据投票者的当前状态来作不同的处理,以加快 Leader 的选举进程。
-
AuthFastLeaderElection :在 FastLeaderElection 的基础上,增加认证。
-
最终在 Zookeeper 3.4.0 版本之后,只保留 FastLeaderElection 版本。
-
在集群启动过程中,或 Leader 宕机后,集群就进入了恢复模式。恢复模式中最重要的阶段就是 Leader 选举。
Leader 选举中的基本概念
- serverId:这是 zk 集群中服务器的唯一标识, 也称为 sid,其实质就是 zk 中配置的 myid。例如,有三个 zk 服务器,那么编号分别是 1,2,3。
- 逻辑时钟:Logicalclock,是一个整型数,该概念在选举时称为 logicalclock,而在选举结束后称为 epoch。即 epoch 与 logicalclock 是同一个值,在不同情况下的不同名称。
Leader 选举算法
在集群启动过程中的 Leader 选举过程(算法)与 Leader 断连后的 Leader 选举过程稍微有一些区别,基本相同。
1.集群启动中的 Leader 选举
若进行 Leader 选举,则至少需要两台主机,这里以三台主机组成的集群为例 :
在集群初始化阶段,当第一台服务器 Server1 启动时,其会给自己投票,然后发布自己的投票结果。投票包含所推举的服务器的 myid 和 ZXID,使用(myid, ZXID)来表示,此时 Server1的投票为(1, 0)。 由于其它机器还没有启动所以它收不到反馈信息, Server1 的状态一直属于Looking,即属于非服务状态。
当第二台服务器 Server2 启动时,此时两台机器可以相互通信,每台机器都试图找到Leader,选举过程如下:
- 每个 Server 发出一个投票。此时 Server1 的投票为(1, 0), Server2 的投票为(2, 0),然后各自将这个投票发给集群中其他机器。
- 接受来自各个服务器的投票。集群的每个服务器收到投票后,首先判断该投票的有效性,如检查是否是本轮投票、是否来自 LOOKING 状态的服务器。
- 处理投票。针对每一个投票,服务器都需要将别人的投票和自己的投票进行 PK, PK规则如下:
-
优先检查 ZXID。 ZXID 比较大的服务器优先作为 Leader。
-
如果 ZXID 相同,那么就比较 myid。 myid 较大的服务器作为 Leader 服务器。
对于 Server1 而言,它的投票是(1, 0),接收 Server2 的投票为(2, 0)。其首先会比较两者的 ZXID,均为 0,再比较 myid,此时 Server2 的 myid 最大,于是 Server1 更新自己的投票为(2, 0),然后重新投票。对于 Server2 而言,其无须更新自己的投票,只是再次向集群中所有主机发出上一次投票信息即可 。
- 统计投票。每次投票后,服务器都会统计投票信息,判断是否已经有过半机器接受到 相同的投票信息。对于 Server1、 Server2 而言,都统计出集群中已经有两台主机接受了(2, 0)的投票信息,此时便认为已经选出了新的 Leader,即 Server2。
- 改变服务器状态。一旦确定了 Leader,每个服务器就会更新自己的状态,如果是Follower,那么就变更为 FOLLOWING,如果是 Leader,就变更为 LEADING。
- 添加主机。在新的 Leader 选举出来后 Server3 启动, 其想发出新一轮的选举。但由于当前集群中各个主机的状态并不是 LOOKING,而是各司其职的正常服务,所以其只能是以Follower 的身份加入到集群中。
2.宕机后的 Leader 选举
在 Zookeeper 运行期间, Leader 与非 Leader 服务器各司其职,即便当有非 Leader 服务器宕机或新加入时也不会影响 Leader。但是若 Leader 服务器挂了,那么整个集群将暂停对外服务,进入新一轮的 Leader 选举,其过程和启动时期的 Leader 选举过程基本一致。
假设正在运行的有 Server1、 Server2、 Server3 三台服务器,当前 Leader 是 Server2,若某一时刻 Server2 挂了,此时便开始新一轮的 Leader 选举了。选举过程如下:
- 变更状态。 Leader 挂后,余下的非 Observer 服务器都会将自己的服务器状态由FOLLOWING 变更为 LOOKING,然后开始进入 Leader 选举过程。
- 每个 Server 会发出一个投票,仍然会首先投自己。不过,在运行期间每个服务器上的 ZXID 可能是不同,此时假定 Server1 的 ZXID 为 111, Server3 的 ZXID 为 333;在第一轮投票中, Server1 和 Server3 都会投自己,产生投票(1, 111), (3, 333),然后各自将投票发送给集群中所有机器。
- 接收来自各个服务器的投票。与启动时过程相同。集群的每个服务器收到投票后, 首先判断该投票的有效性,如检查是否是本轮投票、是否来自 LOOKING 状态的服务器。
- 处理投票。与启动时过程相同。针对每一个投票,服务器都需要将别人的投票和自己的投票进行 PK。对于 Server1 而言,它的投票是(1, 111),接收 Server3 的投票为(3, 333)。其首先会比较两者的 ZXID, Server3 投票的 zxid 为 333 大于 Server1 投票的 zxid 的 111,于是Server1 更新自己的投票为(3, 333),然后重新投票。对于 Server3 而言,其无须更新自己的投票,只是再次向集群中所有主机发出上一次投票信息即可。
- 统计投票。与启动时过程相同。对于 Server1、 Server2 而言,都统计出集群中已经有两台主机接受了(3, 333)的投票信息,此时便认为已经选出了新的 Leader,即 Server3。
- 改变服务器的状态。与启动时过程相同。一旦确定了 Leader,每个服务器就会更新自己的状态。 Server1 变更为 FOLLOWING, Server3 变更为 LEADING。
1.5.11 恢复模式的三个原则
当集群正在启动过程中,或 Leader 崩溃后,集群就进入了恢复模式。对于要恢复的数据状态需要遵循三个原则。
Leader 的主动出让原则
若集群中 Leader 收到的 Follower 心跳数量没有过半,此时 Leader 会自认为自己与集群的连接已经出现了问题,其会主动修改自己的状态为 LOOKING,去查找新的 Leader。而其它 Server 由于有过半的主机认为已经丢失了 Leader,所以它们会发起新的 Leader
选举,选出一个新的 Leader。
已被处理的消息不能丢
正常情况下,当 Leader 收到超过半数 Follower 的 ACKs 后,就向各个 Follower 广播COMMIT消息,批准各个 Server执行该写操作事务。当各个 Server 在接收到Leader的 COMMIT消息后就会在本地执行该写操作,然后会向客户端响应写操作成功。但是如果在非全部 Follower 收到 COMMIT 消息之前 Leader 就挂了,这将导致一种后果:部分 Server 已经执行了该事务,而部分 Server 尚未收到COMMIT 消息,所以其并没有执行该事务。当新的 Leader 被选举出,集群经过恢复模式后需要保证所有 Server 上都执行了那些已经被部分 Server 执行过的事务。
被丢弃的消息不能再现
当在 Leader 新事务已经通过,其已经将该事务更新到了本地,但所有 Follower 还都没有收到 COMMIT 之前, Leader 宕机了,此时,所有 Follower 根本就不知道该 Proposal 的存在。当新的 Leader 选举出来,整个集群进入正常服务状态后,之前挂了的 Leader 主机重新启动并注册成为了 Follower。若那个别人根本不知道的 Proposal 还保留在那个主机,那么其数据就会比其它主机多出了内容,导致整个系统状态的不一致。所以,该 Proposa 应该被丢弃。类似这样应该被丢弃的事务,是不能再次出现在集群中的,应该被清除。
1.6 高可用集群的容灾
1.6.1 服务器数量的奇数与偶数
无论是写操作投票,还是 Leader 选举投票,都必须过半才能通过,也就是说若出现超过半数的主机宕机,则投票永远无法通过。基于该理论,由 5 台主机构成的集群,最多只允许 2 台宕机。而由 6 台构成的集群,其最多也只允许 2 台宕机。即, 6 台与5 台的容灾能力是相同的。 基于此容灾能力的原因, 建议使用奇数台主机构成集群,以避免资源浪费。
但从系统吞吐量上说, 6 台主机的性能一定是高于 5 台的。所以使用 6 台主机并不是资源浪费。
1.6.2 容灾设计方案
对于一个高可用的系统,除了要设置多台主机部署为一个集群避免单点问题外,还需要考虑将集群部署在多个机房、多个楼宇。对于多个机房、楼宇中集群也是不能随意部署的,下面就多个机房的部署进行分析。
在多机房部署设计中,要充分考虑“过半原则”,也就是说,尽量要确保 zk 集群中有过半的机器能够正常运行 。
三机房部署
在生产环境下,三机房部署是最常见的、容灾性最好的部署方案。三机房部署中要求每个机房中的主机数量必须少于集群总数的一半。
双机房部署
zk 官方没有给出较好的双机房部署的容灾方案。只能是让其中一个机房占有超过半数的主机,使其做为主机房,而另一机房少于半数。当然,若主机房出现问题,则整个集群会瘫痪。
2.CAP定理
2.1 CAP简介
CAP 定理又称 CAP 原则,指的是在一个分布式系统中, Consistency(一致性)、 Availability(可用性)、 Partition tolerance(分区容错性),三者不可兼得。
-
一致性(C):分布式系统中多个主机之间是否能够保持数据一致的特性。即,当系统数据发生更新操作后,各个主机中的数据仍然处于一致的状态 。
-
可用性(A):系统提供的服务是否一直处于可用的状态,即对于用户的每一个请求,系统是否总是可以在有限的时间内对用户做出响应。
-
分区容错性(P):分布式系统在遇到任何网络分区故障时,仍能够保证对外提供满足一致性和可用性的服务。分区是网络分区。
对于分布式系统,网络环境相对是不可控的,出现网络分区是不可避免的,因此系统必须具备分区容错性。但其并不能同时保证一致性与可用性。 CAP 原则对于一个分布式系统来说,只可能满足两项,即要么 CP,要么 AP。
2.2 BASE 理论
BASE 是 Basically Available(基本可用)、 Soft state(软状态)和 Eventually consistent(最终一致性)三个短语的简写。
BASE 理论的核心思想是:即使无法做到实时一致性,但每个系统都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性。
基本可用:基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性。
软状态:是指允许系统数据存在的中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统主机间进行数据同步的过程存在一定延时。软状态,其实就是一种灰度状态,过渡状态。
最终一致性:强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的一致性。
从达到一致性的时间角度来划分,可以分为:
- 实时一致性:单机情况下可以实现实时一致性
- 最终一致性:经过一段时间后可以达到一致性
单从客户端访问到的内容角度来划分,可以分为:
- 强一致性(严格一致性)
- 弱一致性:允许客户端访问不到部分或全部更新过的数据。
2.3 ZK 与 CP
zk 遵循的是 CP 原则,即保证了一致性,但牺牲了可用性。
当 Leader 宕机后, zk 集群会马上进行新的 Leader 的选举。但选举时长一般在 200 毫秒内,最长不超过 60 秒,整个选举期间 zk 集群是不接受客户端的读写操作的,即 zk 集群是处于瘫痪状态的。所以,其不满足可用性。
2.4 zk 可能会存在脑裂
这里说的 zk 可能会引发脑裂,是指的在多机房部署中,若出现了网络连接问题, 形成多个分区,则可能会出现脑裂问题,可能会导致数据不一致。
情况一:B机房无法对外提供服务,但AC正常。
情况二:①会出现短暂的脑裂,且有可能会出现短暂的数据不一致情况
②很快A机房无法对外提供服务,但B与C会选举出新的Leader并正常运行
情况三:A会对外提供短暂的服务,但很快三个机房均无法对外提供服务。
情况四:B无法对外提供服务但A与C可以正常工作。
情况五:整个集群都可以对外提供服务,不受影响。
3.应用场景
Zookeeper 的功能很强大,应用场景很多,结合我们实际工作中使用 Dubbo 框架的情况,Zookeeper 主要是做注册中心用。
- 基于 Dubbo 框架开发的提供者、消费者都向 Zookeeper 注册自己的 URL ,消费者还能拿到并订阅提供者的注册 URL ,以便在后续程序的执行中去调用提供者。
- 而提供者发生了变动,也会通过 Zookeeper 向订阅的消费者发送通知。
作为服务注册中心,Eureka 比 Zookeeper 好在哪里?
参见 《作为服务注册中心,Eureka 比 Zookeeper 好在哪里》 文章。
比较重要的原因是,注册中心对可用性比一致性有更高的要求,也就是说,能够容忍在异常情况下,读取到几分钟前的数据。
当然,Zookeeper 能提供的不仅仅如此,再例如:
3.1 配置维护
3.1.1 什么是配置维护
分布式系统中,很多服务都是部署在集群中的,即多台服务器中部署着完全相同的应用,起着完全相同的作用。当然,集群中的这些服务器的配置文件是完全相同的。
若集群中服务器的配置文件需要进行修改,那么我们就需要逐台修改这些服务器中的配置文件。如果我们集群服务器比较少,那么这些修改还不是太麻烦,但如果集群服务器特别多,比如某些大型互联网公司的Hadoop集群有数千台服务器,那么纯手工的更改这些配置文件几乎就是一件不可能完成的任务。即使使用大量人力进行修改可行,但过多的人员参与,出错的概率大大提升,对于集群所形成的危险是很大的。
3.1.2 实现原理
zookeeper可以通过“发布/订阅模型”实现对集群配置文件的管理与维护。“发布/订阅模型”分为推模式(Push)与拉模式(Pull)。zookeeper的“发布/订阅模型”采用的是推拉相结合的模式。
3.2 命名服务
3.2.1 什么是命名服务
命名服务是指可以为一定范围内的元素命名一个唯一标识,以与其它元素进行区分。在分布式系统中被命名的实体可以是集群中的主机、服务地址等。
3.2.2 实现原理
通过利用zookeeper中节点路径不可重复的特点来实现命名服务的。当然,也可以配带上顺序节点的有序性来体现唯一标识的顺序性。其可以生成业务相关的唯一标识。
3.3 DNS服务
zookeeper的DNS服务是命名服务的一种特殊用法。其对外表现出的功能主要是防止提供者的单点问题,实现对提供者的负载均衡。
3.3.1 什么是DNS
DNS,Domain Name System,域名系统,即可以将一个名称与特定的主机IP加端口号进行绑定。zookeeper可以充当DNS的作用,完成域名到主机的映射。
3.3.2 基本DNS实现原理
假设应用程序app1与app2分别用于提供service1与service2两种服务,现要将其注册到zookeeper中,具体的实现步骤如下图所示。
3.3.3 具有状态收集功能的DNS实现原理
以上模型存在一个问题,如何获取各个提供者主机的健康状态、运行状态呢?可以为每一个域名节点再添加一个状态子节点,而该状态子节点的数据内容则为开发人员定义好的状态数据。这些状态数据是如何获取到的呢?是通过状态收集器(开发人员自行开发的)定期写入到Zookeeper的该节点中的。
阿里的Dubbo就是使用Zookeeper作为域名服务器的。
3.4 Master选举
3.4.1 什么是Master选举
基于 Zookeeper 实现分布式协调,从而实现主从的选举。这个在 Kafka、Elastic-Job 等等中间件,都有所使用到。
一般情况下,群集中都会存在一个Master,用于协调集群中的其它Slave主机,Master对于Slave的状态具有决定权。那么Master是如何产生的呢?是通过某种机制选举产生的。
3.4.2 广告推荐系统
(1) 需求
系统会根据用户画像,将用户归结为不同的种类。系统会为不同种类的用户推荐不同的广告。每个用户前端需要从广告推荐系统中获取到不同的广告ID。
(2) 分析
这个向用户前端提供服务的广告推荐系统一定是一个集群,这样可以更加快速高效的为前端进行响应。需要注意,推荐系统对于广告ID的计算是一个相对复杂且消耗CPU等资源的过程。如果让集群中每一台主机都可以执行这个计算逻辑的话,那么势必会形成资源浪费,且降低了响应效率。此时,可以只让其中的一台主机去处理计算逻辑,然后将计算的结果写入到某中间存储系统中,并通知集群中的其它主机从该中间存储系统中共享该计算结果。那么,这个运行计算逻辑的主机就是Master,而其它主机则为Slave。
(3) 架构
(4) Master选举
这个广告推荐系统集群中的Master是如何选举出来的呢?使用zk可以完成。使用zk中多个客户端对同一节点创建时,只有一个客户端可以成功的特性实现。
3.5分布式同步
3.5.1 什么是分布式同步
分布式同步,也称为分布式协调,是分布式系统中不可缺少的环节,是将不同的分布式组件有机结合起来的关键。对于一个在多台机器上运行的应用而言,通常需要一个协调者来控制整个系统的运行流程,例如执行的先后顺序,或执行与不执行等。
3.5.2 MySQL数据复制总线
下面以“MySQL数据复制总线”为例来分析zk的分布式同步服务。
(1) 数据复制总线组成
MySQL数据复制总线是一个实时数据复制框架,用于在不同的MySQL数据库实例间进行异步数据复制。其核心部分由三部分组成:生产者、复制管道、消费者。
那么,MySQL数据复制总线系统中哪里需要使用zk的分布式同步功能呢?以上结构中可以显示看到存在的问题:replicator存在单点问题。为了解决这个问题,就需要为其设置多个热备主机。那么,这些热备主机是如何协调工作的呢?这时候就需要使用zk来做协调工作了,即由zk来完成分布式同步工作。
(2) 数据复制总线工作原理
MySQL复制总线的工作步骤,总的来说分为三步:
-
复制任务注册
复制任务注册实际就是指不同的复制任务在zk中创建不同的znode,即将复制任务注册到zk中。 -
replicator热备
复制任务是由replicator主机完成的。为了防止replicator在复制过程中出现故障,replicator采用热备容灾方案,即将同一个复制任务部署到多个不同的replicator主机上,但仅使一个处于RUNNING状态,而其它的主机则处于STANDBY状态。当RUNNING状态的主机出现故障,无法完成复制任务时,使某一个STANDBY状态主机转换为RUNNING状态,继续完成复制任务。 -
主备切换
当RUNNING态的主机出现宕机,则该主机对应的子节点马上就被删除了,然后在当前处于STANDBY状态中的replicator中找到序号最小的子节点,然后将其状态马上修改为RUNNING,完成“主备切换”。
3.6集群管理
3.6.1 需求
对于集群,我们总是希望能够随时获取到以下信息:
- 当前集群中各个主机的运行时状态
- 当前集群中主机的存活状况
3.6.2 基本原理
zk进行集群管理的基本原理如下图所示。
3.6.3 分布式日志收集系统
下面以分布式日志收集系统为例来分析zk对于集群的管理。
(1) 系统组成
首先要清楚,分布式日志收集系统由四部分组成:日志源集群、日志收集器集群,zk集群,及监控系统。
(2) 系统工作原理
分布式日志收集系统的工作步骤有以下几步:
-
收集器的注册:在zk上创建各个收集器对应的节点。
-
任务分配:系统根据收集器的个数,将所有日志源集群主机分组,分别分配给各个收集器。
-
状态收集
这里的状态收集指的是两方面的收集:-
日志源主机状态,例如,日志源主机是否存活,其已经产生多少日志等
-
收集器的运行状态,例如,收集器本身已经收集了多少字节的日志、当前CPU、内存的使用情况等。
收集器/日志源主机会定时/实时将其自身状态写入到对应的节点内容中。
-
-
任务再分配Rebalance
当出现收集器挂掉或扩容,就需要动态地进行日志收集任务再分配了,这个过程称为Rebalance。
3.7分布式锁
分布式锁是控制分布式系统同步访问共享资源的一种方式。Zookeeper可以实现分布式锁功能。根据用户操作类型的不同,可以分为排他锁(写锁)与共享锁(读锁)。
3.7.1 分布式锁的实现
在zk上对于分布式锁的实现,使用的是类似于“/xs_lock/[hostname]-请求类型-序号”的临时顺序节点。
其具体实现过程如下:
Step1:当一个客户端向某资源发出读/写操作请求时,若发现其为第一个请求,则首先会在zk中创建一个根节点。当然,若不是第一个请求,则该根节点已经创建完毕,就无需再创建了。
Step2:根节点已经存在了,客户端会对根节点注册子节点列表变更事件的watcher监听,随时监听子节点的变化情况。
Step3: watcher注册完毕后,其会在根节点下创建一个读写操作的临时顺序节点。读写操作的顺序性就是通过这些子节点的顺序性体现的。每个节点都只关心序号比自己小的节点。因为它们的请求是先于自己提出的,需要先执行。注意,读写操作创建的节点名称是不同的。
Step4:节点创建完后,其就会触发客户端的watcher回调,读取根节点下的所有子节点列表,然后会查看序号比自己小的节点,并根据读写操作的不同,执行不同的逻辑。
Step5:客户端操作完毕后,与zk的连接断开,则zk中该会话对应的节点消失。当然,该操作会引发各个客户端再次执行watcher回调,查看自己是否可以执行操作了。
3.7.2 分布式锁的改进
前面的实现方式存在“羊群效应”,为了解决其所带来的性能下降,可以对前述分布式锁的实现进行改进。
由于一个操作而引发了大量的低效或无用的操作的执行,这种情况称为羊群效应。
当客户端请求发出后,在zk中创建相应的临时顺序节点后马上获取当前的/xs_lock的所有子节点列表,但任何客户端都不向/xs_lock注册用于监听子节点列表变化的watcher。而是改为根据请求类型的不同向“对其有影响的”子节点注册watcher。
3.8分布式队列
说到分布式队列,我们马上可以想到RabbitMQ、Kafka等分布式消息队列中间件产品。zk也可以实现简单的消息队列。
3.8.1 FIFO队列
zk实现FIFO队列的思路是:利用顺序节点的有序性,为每个数据在zk中都创建一个相应的节点。然后为每个节点都注册watcher监听。一个节点被消费,则会引发消费者消费下一个节点,直到消费完毕。
3.8.2 分布式屏障Barrier队列
Barrier,屏障、障碍物。Barrier队列是分布式系统中的一种同步协调器,规定了一个队列中的元素必须全部聚齐后才能继续执行后面的任务,否则一直等待。其常见于大规模分布式并行计算的应用场景中:最终的合并计算需要基于很多并行计算的子结果来进行。
zk对于Barrier的实现原理是,在zk中创建一个/barrier节点,其数据内容设置为屏障打开的阈值,即当其下的子节点数量达到该阈值后,app才可进行最终的计算,否则一直等待。每一个并行运算完成,都会在/barrier下创建一个子节点,直到所有并行运算完成。
4. Zookeeper 的通知机制
Zookeeper 允许客户端向服务端的某个 znode 注册一个 Watcher 监听,当服务端的一些指定事件触发了这个 Watcher ,服务端会向指定客户端发送一个事件通知来实现分布式的通知功能,然后客户端根据 Watcher 通知状态和事件类型做出业务上的改变。
整个流程如下:
- 第一步,客户端注册 Watcher 。
- 第二步,服务端处理 Watcher 。
- 第三步,客户端回调 Watcher 。
Watcher 的特性总结:
-
一次性【最重要】
无论是服务端还是客户端,一旦一个 Watcher 被触发, Zookeeper 都会将其从相应的存储中移除。这样的设计有效的减轻了服务端的压力,不然对于更新非常频繁的节点,服务端会不断的向客户端发送事件通知,无论对于网络还是服务端的压力都非常大。
这个特性可以变成一个面试题「Zookeeper 对节点的 watch 监听通知是永久的吗?」。
如果我们使用 Apache Curator 作为操作 Zookeeper 的客户端,它可以帮我们自动透明的实现持续的 watch 操作,非常方便。
-
客户端串行执行
客户端 Watcher 回调的过程是一个串行同步的过程。
-
轻量级 Watch 机制
Watcher 通知非常简单,只会告诉客户端发生了事件,而不会说明事件的具体内容。
客户端向服务端注册 Watcher 的时候,并不会把客户端真实的 Watcher 对象实体传递到服务端,仅仅是在客户端请求中使用
boolean
类型属性进行了标记。 -
Watcher event 异步发送 Watcher 的通知事件从 Server 发送到Client 是异步的,这就存在一个问题,不同的客户端和服务器之间通过Socket 进行通信,由于网络延迟或其他因素导致客户端在不同的时刻监听到事件,由于 Zookeeper 本身提供了 ordering guarantee ,即客户端监听事件后,才会感知它所监视 znode 发生了变化。所以我们使用 Zookeeper 不能期望能够监控到节点每次的变化。Zookeeper 只能保证最终的一致性,而无法保证强一致性。
-
可以注册 Watcher 的操作:getData、exists、getChildren 。
-
可以触发 Watcher 的操作:create、delete、setData 。
-
当一个 Client 连接到一个新的服务器上时,watch 将会被以任意会话事件触发。当与一个服务器失去连接的时候,是无法接收到 watch 的。而当 Client 重新连接时,如果需要的话,所有先前注册过的watch ,都会被重新注册。通常这是完全透明的。只有在一个特殊情况下,watch 可能会丢失:对于一个未创建的 znode 的 exists watch ,如果在客户端断开连接期间被创建了,并且随后在客户端连接上之前又删除了,这种情况下,这个 watch 事件可能会被丢失。
5. Zookeeper 采用什么权限控制机制
目前,在 Linux/Unix 文件系统中,使用 UGO(User/Group/Others) 权限模型,也是使用最广泛的权限控制方式。是一种粗粒度的文件系统权限控制模式。
一般我们管理后台,采用的 RBAC 居多,和 UGO 比较类似,差别在于一般将权限分配给 Role ,而不是直接给 User 。
对于 Zookeeper ,它采用 ACL(Access Control List)访问控制列表。包括三个方面:
-
权限模式(Scheme)
- IP :从 IP 地址粒度进行权限控制
- 【常用】Digest :最常用,用类似于
username:password
的权限标识来进行权限配置,便于区分不同应用来进行权限控制。 - World :最开放的权限控制方式,是一种特殊的 digest 模式,只有一个权限标识
“world:anyone”
。 - Super :超级用户。
-
授权对象
授权对象指的是权限赋予的用户或一个指定实体,例如 IP 地址或是机器等。
-
权限 Permission
- CREATE :数据节点创建权限,允许授权对象在该 znode 下创建子节点。
- DELETE :子节点删除权限,允许授权对象删除该数据节点的子节点。
- READ :数据节点的读取权限,允许授权对象访问该数据节点并读取其数据内容或子节点列表等。
- WRITE :数据节点更新权限,允许授权对象对该数据节点进行更新操作。
- ADMIN :数据节点管理权限,允许授权对象对该数据节点进行 ACL 相关设置操作。
Chroot 特性是什么?
Zookeeper 3.2.0 版本后,添加了 Chroot 特性。该特性允许每个客户端为自己设置一个命名空间。如果一个客户端设置了 Chroot ,那么该客户端对服务器的任何操作,都将会被限制在其自己的命名空间下。
通过设置 Chroot ,能够将一个客户端应用于 Zookeeper 服务端的一颗子树相对应,在那些多个应用公用一个 Zookeeper 进群的场景下,对实现不同应用间的相互隔离非常有帮助。