Zookeeper是一个由多个server组成的集群,分布、开源的应用程序协调服务,符合分布式服务ACP原理中的CP。它是集群的管理者,监视着集群中各个节点的状态,根据节点的反馈进行下一步合理操作。主要解决分布式应用经常遇到的数据管理问题,一些RPC框架(如点评pigeon、阿里dubbo的早期版本)使用ZK用来做服务发现和注册,点评的配置管理组件Lion用来做配置数据的获取更新,kafka⽤于检测崩溃,实现主题(topic)的发现,并保持主题的⽣产和 消费状态。zookeeper维护一个类似文件系统的数据结构。如图:
每个子目录项都被称作为 znode,和文件系统一样,自由增加及删除,唯一不同其可存储数据,并提供节点的通知机制:客户端注册监听它关心的目录节点,当目录节点发生变化(数据改变、被删除、子目录节点增加删除)时,zookeeper会通知客户端。Znode分为四种类型
集群中各server的角色分为:
群⾸为集群中的服务器选择出来的⼀个服务器,并会⼀直被集群所认可。设置群⾸的⽬的是为了对客户端所发起的ZooKeeper状态变更请求进⾏排序,包括:create、setData和delete操作。群⾸将每⼀个请求转换为⼀个事务,将这些事务发送给追随者,确保集群按照群⾸确定的顺序接受并处理这些事务。依据解管理权的原理,⼀个服务器必须被仲裁的法定数量的服务器所认可,选举并⽀持⼀个群⾸的集群服务器数量必须⾄少存在⼀个服务器进程的交叉,所以一个群首必须被一半以上的仲裁数量的服务器认可。
每个服务器启动后进⼊LOOKING状态,开始选举⼀个新的群⾸或查 找已经存在的群⾸,如果群⾸已经存在,其他服务器就会通知这个新启动的服务器,告知哪个服务器是群⾸,与此同时,新的服务器会与群⾸建⽴连接,以确保⾃⼰的状态与群⾸⼀致。
如果集群中所有的服务器均处于LOOKING状态,这些服务器之间就会进⾏通信来选举⼀个群⾸,通过信息交换对群⾸选举达成共识的选择。 在本次选举过程中胜出的服务器将进⼊LEADING状态,⽽集群中其他服务器将会进⼊FOLLOWING状态。
对于群⾸选举的消息,我们称之为群⾸选举通知消息(leader election notifications),或简单地称为通知(notifications)。该协议⾮常简单,当⼀个服务器进⼊LOOKING状态,就会发送向集群中每个服务器发送⼀个通知消息,该消息中包括该服务器的投票(vote)信息,投票中包含服务器标识符(sid)和最近执⾏的事务的zxid信息,⽐如,⼀个服务器所发送的投票信息为(1,5),表⽰该服务器的sid为1,最近执⾏的事务的zxid为 5(出于群⾸选举的⽬的,zxid只有⼀个数字,⽽在其他协议中,zxid则有 时间戳epoch和计数器组成)。
当⼀个服务器收到⼀个投票信息,该服务器将会根据以下规则修改⾃⼰的投票信息:
1.将接收的voteId和voteZxid作为⼀个标识符,并获取接收⽅当前的投票中的zxid,⽤myZxid和mySid表⽰接收⽅服务器⾃⼰的值。
2.如果(voteZxid>myZxid)或者(voteZxid=myZxid且 voteId>mySid),保留当前的投票信息。
3.否则,修改⾃⼰的投票信息,将voteZxid赋值给myZxid,将voteId赋值给mySid。
简⽽⾔之,只有最新的服务器将赢得选举,因为其拥有最近⼀次的 zxid。这样做将会简化群⾸崩溃后重新仲裁的流程。如果多个服务器拥有最新的zxid值,其中的sid值最⼤的将赢得选举。
当⼀个服务器接收到仲裁数量的服务器发来的投票都⼀样时,就表⽰群⾸选举成功,如果被选举的群⾸为某个服务器⾃⼰,该服务器将会开始⾏使群⾸⾓⾊,否则就成为⼀个追随者并尝试连接被选举的群⾸服务器。 注意,我们并未保证追随者必然会成功连接上被选举的群⾸服务器,⽐ 如,被选举的群⾸也许此时崩溃了。⼀旦连接成功,追随者和群⾸之间将会进⾏状态同步,在同步完成后,追随者才可以处理新的请求。
通过例⼦模拟这个协议的执⾏过程。图2-1展⽰了三个服务器,这三个服务器分别以不同的初始投票值开始,其投票值取决于该服务器的标识符和其最新的zxid。每个服务器会收到另外两个服务器发送的投票信息,在第⼀轮之后,服务器s2和服务器s3将会改变其投票值为 (1,6),之后服务器服务器s2和服务器s3在改变投票值之后会发送新的通知消息,在接收到这些新的通知消息后,每个服务器收到的仲裁数量的通知消息拥有⼀样的投票值,最后选举出服务器s1为群⾸。
并不是所有执⾏过程都如图2-1中所⽰,在图2-2中,我们展⽰了另⼀种情况的例⼦。服务器s2做出了错误判断,选举了另⼀个服务器s3⽽不是服务器s1,虽然s1的zxid值更⾼,但在从服务器s1向服务器s2传送消息时发⽣了⽹络故障导致长时间延迟,与此同时,服务器s2选择了服务器s3作为群⾸,最终,服务器s1和服务器s3组成了仲裁数量(quorum),并将忽略服务器s2。
虽然服务器s2选择了另⼀个群⾸,但并未导致整个服务发⽣错误,因为服务器s3并不会以群⾸⾓⾊响应服务器s2的请求,最终服务器s2将会在等待被选择的群⾸s3的响应时⽽超时,并开始再次重试。再次尝试,意味着在这段时间内,服务器s2⽆法处理任何客户端的请求,这样做并不可取。
从这个例⼦,我们发现,如果让服务器s2在进⾏群⾸选举时多等待⼀ 会,它就能做出正确的判断。我们通过图9-3展⽰这种情况,我们很难确定服务器需要等待多长时间,在现在的实现中,默认的群⾸选举的实现类为 FastLeaderElection,其中使⽤固定值200ms(常量finalizeWait),这个值⽐在当今数据中⼼所预计的长消息延迟(不到1毫秒到⼏毫秒的时间)要长得多,但与恢复时间相⽐还不够长。万⼀此类延迟(或任何其他延迟)时间并不是很长,⼀个或多个服务器最终将错误选举⼀个群⾸,从⽽导致该群⾸没有⾜够的追随者,那么服务器将不得不再次进⾏群⾸选举。错误地选举⼀个群⾸可能会导致整个恢复时间更长,因为服务器将会进⾏连接以及不必要的同步操作,并需要发送更多消息来进⾏另⼀轮的群⾸选举。
在接收到⼀个请求操作后,如果是读请求直接在本地执行返回给客户端,如果是写请求追随者会将请求转发给群⾸,群⾸将探索性地执⾏该请求,并将执⾏结果以事务的⽅式对状态更新进⾏⼴播。⼀个事务中包含服务器需要执⾏变更的确切操作,当事务提交时,服务器就会将这些变更反馈到数据树上,其中数据树为ZooKeeper⽤于保存状态信息的数据结构。
假设现在有⼀个活动的群⾸服务器,并拥有仲裁数量的追随者⽀持该群⾸的管理权,通过该协议提交⼀个事务类似于⼀个两阶段提交。
1.群⾸向所有追随者发送⼀个PROPOSAL消息p。
2.当⼀个追随者接收到消息p后,在本地将事务信息存储到日志中,响应群⾸⼀个ACK消息,通知群⾸其已接受该提案(proposal)。
3.当收到仲裁数量的服务器发送的确认消息后(该仲裁数包括群⾸⾃⼰),群⾸就会发送消息通知追随者进⾏提交(COMMIT)操作。
Zab保障了以下⼏个重要属性:
1.如果群首按顺序⼴播了事务T1和事务T2,那么每个服务器在提交T2事务前保证事务T1已经提交完成。
2.如果某个服务器按照事务T1、事务T2的顺序提交事务,所有其他服务器也必然会在提交事务T2前提交事务T1。
第⼀个属性保证事务在服务器之间的传送顺序的⼀致,⽽第⼆个属性保证服务器不会跳过任何事务。假设事务为状态变更操作,每个状态变更操作又依赖前⼀个状态变更操作的结果,如果跳过事务就会导致结果的不⼀致性,⽽两阶段提交保证了事务的顺序。Zab在仲裁数量服务器中记录了事务,集群中仲裁数量的服务器需要在群⾸提交事务前对事务达成⼀致,⽽且追随者也会在硬盘中记录事务的确认信息。
1、已经被处理的事务请求(proposal)不能丢(commit的)
1.已经被处理的事务请求(proposal)不能丢(必须commit)。
什么时候会出现事务请求被丢失呢?
当 leader 收到合法数量 follower 的 ACKs 后,就向各个 follower 广播 COMMIT 命令,同时也会在本地执行 COMMIT 并向连接的客户端返回「成功」。但是如果在各个 follower 在收到 COMMIT 命令前 leader 就挂了,导致剩下的服务器并没有执行都这条消息。
如何解决已经被处理的事务请求(proposal)不能丢(commit的)呢?
1、选举协议保证拥有 proposal 最大值(即 zxid 最大) 的节点作为新的 leader:由于所有提案被 COMMIT 之前必须有合法数量的 follower ACK,即必须有合法数量的服务器的事务日志上有该提案的 proposal,因此,zxid最大也就是数据最新的节点保存了所有被 COMMIT 消息的 proposal 状态。
2、新的 leader 将自己事务日志中 proposal 但未 COMMIT 的消息处理。
3、新的 leader 与 follower 建立先进先出的队列, 先将自身有而 follower 没有的 proposal 发送给 follower,再将这些 proposal 的 COMMIT 命令发送给 follower,以保证所有的 follower 都保存了所有的 proposal、所有的 follower 都处理了所有的消息。通过以上策略,能保证已经被处理的消息不会丢。
2、没被处理的事务请求(proposal)不能再次出现
问题2出现的场景是:
当 leader 接收到消息请求生成 proposal 后就挂了,其他 follower 并没有收到此 proposal,因此经过恢复模式重新选了 leader 后,这条消息是被跳过的。 此时,之前挂了的 leader 重新启动并注册成了 follower,他保留了被跳过消息的 proposal 状态,与整个系统的状态是不一致的,需要将其删除。
解决方案是:
Zab 通过巧妙的设计 zxid 来实现这一目的。一个 zxid 是64位,高 32 是纪元(epoch)编号,每经过一次 leader 选举产生一个新的 leader,新 leader 会将 epoch 号 +1。低 32 位是消息计数器,每接收到一条消息这个值 +1,新 leader 选举后这个值重置为 0。这样设计的好处是旧的 leader 挂了后重启,它不会被选举为 leader,因为此时它的 zxid 肯定小于当前的新 leader。当旧的 leader 作为 follower 接入新的 leader 后,新的 leader 会让它将所有的拥有旧的epoch号的未被 COMMIT 的 proposal清除。