一 Zookeeper是什么
ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,它是集群的管理者,监视着集群中各个节点的状态根据节点提交的反馈进行下一步合理操作。最终,将简单易用的接口和性能高效、功能稳定的系统提供给用户,主要为了解决分布式架构下数据一致性问题,典型的应用场景有分布式配置中心、分布式注册中心、分布式锁、分布式队列、集群选举、分布式屏障、发布/订阅等场景。简单说zookeeper=文件系统+通知机制。
1,Zk的文件系统
每个子目录项如 NameService 都被称作为znode,和文件系统一样,我们能够自由的增加、删除znode,在一个znode下增加、删除子znode,唯一的不同在于znode是可以存储数据的。
- 持久化目录节点:客户端与zookeeper断开连接后,该节点依旧存在,只要不手动删除该节点,他将永远存在。
- 持久化顺序编号目录节点:客户端与zookeeper断开连接后,该节点依旧存在,只是zookeeper给该节点名称进行顺序编号。
- 临时目录节点:客户端与zookeeper断开连接后,该节点被删除。
- 临时顺序编号目录节点:客户端与zookeeper断开连接后,该节点被删除,只是zookeeper给该节点名称进行顺序编号。
2,zk的通知机制
客户端注册监听它关心的目录节点,当目录节点发生变化(数据改变、被删除、子目录节点增加删除)时,zookeeper会通知客户端。监听事件如下:
1、None:连接建立事件
2、NodeCreated:节点创建
3、NodeDeleted:节点删除
4、NodeDataChanged:节点数据变化
5、NodeChildrenChanged:子节点列表变化
6、DataWatchRemoved:节点监听被移除
7、ChildWatchRemoved:子节点监听被移除
3,zk的角色
- 领导者(leader),负责进行投票的发起和决议,更新系统状态
- 学习者(learner),包括跟随者(follower)和观察者(observer),follower用于接受客户端请求并想客户端返回结果,在选主过程中参与投票
- Observer可以接受客户端连接,将写请求转发给leader,但observer不参加投票过程,只同步leader的状态,observer的目的是为了扩展系统,提高读取速度。zoo.cfg中添加peerType=Observer,这表示这个server以observer角色运行,即不参与投票。再在所有 server的配置文件中,修改server.X配置项,在那些observer的节点上加上:observer后缀。例如,server.1对应的server要作为observer:Server.1=IP:2181:3181:observer
这样配置后,ZooKeeper集群中的所有服务器节点都知道哪些节点扮演的是observer角色。 - 客户端(client),请求发起方
Leader 服务器会和每一个 Follower/Observer 服务器都建立 TCP 连接,同时为每个Follower/Observer 都创建一个叫做 LearnerHandler 的实体。LearnerHandler 主要负责 Leader 和Follower/Observer 之间的网络通讯,包括数据同步,请求转发和 proposal 提议的投票等。Leader 服务器保存了所有 Follower/Observer 的 LearnerHandler 。
4,CAP理论
zookeeper:满足数据的最终一致性,但是在leader选举过程中不可用。
Eureka:在任何时间都可用,但是可能有数据不一致的情况.
- C是一致性,Consistency,就是各个节点保存的数据都是最新的。
- A是Availability, 可用性,每次请求都可以获得响应,但是没法保证是最新的响应。
- P是分区容错性(Partition tolerance),就是每个节点可能都存在于不同的子网络,也就是不同的分区,不同分区之间的通信总是有可能发生故障的,或者总是有可能存在一些节点挂了的情况。
可以认为P总是成立的,C和A是无法同时做到。所以一般对于数据一致性要求特别高的业务,例如支付,交易相关的业务,就是会优先保证一致性C和分区容错性P,就是保证数据一致性,例如让所有子节点都收到更新后才算提交成功,就像MySQL主从同步中的全同步模式一样。普通的业务是优先保证可用性A和分区容错性P,比如在MySQL主从同步时,默认就是异步的方式,我们执行一条更新SQL,只需要主节点更新成功就行就对事务进行提交,不需要等待从节点更新数据成功,主节点会异步把SQL发送给从节点。
二 zk的工作流程
Zookeeper的核心是原子广播,这个机制保证了各个Server之间的同步。实现这个机制的协议叫做Zab协议。Zab协议有两种模式,它们分别是恢复模式(选主)和广播模式(同步)。当服务启动或者在领导者崩溃后,Zab就进入了恢复模式,当领导者被选举出来,且大多数Server完成了和leader的状态同步以后 ,恢复模式就结束了。状态同步保证了leader和Server具有相同的系统状态。
为了保证事务的顺序一致性,zookeeper采用了递增的事务id号(zxid) 来标识事务。所有的提议(proposal)都在被提出的时候加上了zxid。实现中zxid是一个64位的数字,它高32位是epoch用来标识leader关系是否改变,每次一个leader被选出来,它都会有一个新的epoch,标识当前属于那个leader的统治时期。低32位用于递增计数。
每个Server在工作过程中有三种状态:
- LOOKING:当前Server不知道leader是谁,正在搜寻
- LEADING:当前Server即为选举出来的leader
- FOLLOWING:leader已经选举出来,当前Server与之同步
1,zk的选主流程
当leader崩溃或者leader失去大多数的follower,这时候zk进入恢复模式,恢复模式需要重新选举出一个新的leader,让所有的 Server都恢复到一个正确的状态。Zk的选举算法有两种:一种是基于basic paxos实现的,另外一种是基于fast paxos算法实现的。系统默认的选举算法为fast paxos。
1)basic paxos流程:
- 选举线程由当前Server发起,其主要功能是对投票结果进行统计,并推选出Leader
- 选举线程首先向所有的server发次一次询问(包括自己)
- 选举线程收到回复后,验证是否是自己发起的询问(验证zxid),然后获取对方的id(myId),并存储到当前询问对象列表中,最后获取对方提议的leader相关信息(zxid和myid),并将这些信息存储到当次选举的投票记录中。
- 收到所有的server回复后,就计算出zxid最大的server,并将这个Server相关的信息设置成下次要投票成leader的server。
- 线程将当前最大zxid的server设置成推荐leader时,如果获得n/2+1的server同意,则zxid最大的server称为leader,然后将根据获胜的server相关的信息来设置自己的状态,否则继续当前流程,直到leader选出。
通过流程分析我们可以得出:要使Leader获得多数Server的支持,则Server总数必须是奇数2n+1,且存活的Server的数目不得少于n+1。
每个Server启动后都会重复以上流程。在恢复模式下,如果是刚从崩溃状态恢复的或者刚启动的server还会从磁盘快照中恢复数据和会话信息,zk会记录事务日志并定期进行快照,方便在恢复时进行状态恢复。选主的具体流程图如下所示:
2)fast paxos流程
是在选举过程中,某Server首先向所有Server提议自己要成为leader,当其它Server收到提议以后,解决epoch和 zxid的冲突,并接受对方的提议,然后向对方发送接受提议完成的消息,重复这个流程,最后一定能选举出Leader。其流程图如下所示:
如果还不明白可以参考:
https://blog.csdn.net/qq_35128600/article/details/104798113
2,zk的同步流程
选完leader以后,zk就进入状态同步过程。
- leader等待server连接;
- Follower连接leader,将最大的zxid发送给leader;
- Leader根据follower的zxid确定同步点;
- 完成同步后通知follower 已经成为uptodate状态;
- Follower收到uptodate消息后,又可以重新接受client的请求进行服务了。
流程图如下所示:
3,zk的工作流程
ZooKeeper集群中的每个server都能为客户端提供读、写服务。对于客户端的读请求,server会直接从它本地的内存数据库中取出数据返回给客户端,这个过程不涉及其它任何操作,也不会联系leader。对于客户端的写请求,因为写操作会修改znode的数据、状态,所以必须要在ZooKeeper集群中进行协调。处理过程如下:
如图所示:
- 收到写请求的那个server,首先将写请求发送给leader。
- leader收到来自follower(或observer)的写请求后,首先计算这次写操作之后的状态,然后将这个写请求转换成带有各种状态的事务(如版本号、zxid等等)。
- leader将这个事务以提议的方式广播出去(即发送proposal)。
- 所有follower收到proposal后,对这个提议进行投票,投票完成后返回ack给leader。follower的投票只有两种方式:(1)确认这次提议表示同意;(2)丢弃这次提议表示不同意。
- leader收集投票结果,只要投票数量达到了大多数的要求(例如,5个节点的集群,3个或3个以上的节点才算大多数),这次提议就通过。
- 提议通过后,leader向所有server发送一个提交通知。
- 所有节点将这次事务写入事务日志,并进行提交。
- 提交后,收到写请求的那个server向客户端返回成功信息。
上面只是简单的介绍了下zk的写流程,实际过程中,leader和foller还有好多其他的功能,具体如下。
1.Leader的工作流程
Leader的主要功能是:恢复数据和维持与foller的心跳,并接收foller的请求消息类型。leader接收的消息类型主要有
- PING消息:指的是foller的心跳信息。
- REQUEST消息:foller发送的提议信息,包括写请求和同步数据请求
- ACK消息:foller对于提议信息的恢复,超过半数的foller同意,则该commit该提议。
- REVALIDATE消息:是用来延长session的有效时间。
Leader的工作流程简图如下所示,在实际实现中,流程要比下图复杂得多,启动了三个线程来实现功能。
2.foller的工作流程
Follower主要有四个功能:
- 向Leader发送请求(PING消息、REQUEST消息、ACK消息、REVALIDATE消息)
- 接收Leader消息并进行处理;
- 接收Client的请求,如果为写请求,发送给Leader进行投票;
- 返回Client结果。
Follower的消息循环处理如下几种来自Leader的消息:
- PING消息: 心跳消息;
- PROPOSAL消息:Leader发起的提案,要求Follower投票;
- COMMIT消息:服务器端最新一次提案的信息;
- UPTODATE消息:表明同步完成;
- REVALIDATE消息:根据Leader的REVALIDATE结果,关闭待revalidate的session还是允许其接受消息;
- SYNC消息:返回SYNC结果到客户端,这个消息最初由客户端发起,用来强制得到最新的更新。
Follower的工作流程简图如下所示,在实际实现中,Follower是通过5个线程来实现功能的
三 zk的常见应用场景
1,zk的命名服务
命名服务是指通过指定的名字来获取资源或者服务的地址,提供者的信息。利用Zookeeper很容易创建一个全局的路径,而这个路径就可以作为一个名字,它可以指向集群中的集群,提供的服务的地址,远程对象等。简单来说使用Zookeeper做命名服务就是用路径作为名字,路径上的数据就是其名字指向的实体。
例如:Dubbo中使用ZooKeeper来作为其命名服务,维护全局的服务地址列表。在Dubbo实现中:
- 服务提供者在启动的时候,向ZK上的指定节点/dubbo/${serviceName}/providers目录下写入自己的URL地址,这个操作就完成了服务的发布。
- 服务消费者启动的时候,订阅/dubbo/{serviceName}/providers目录下的提供者URL地址, 并向/dubbo/{serviceName} /consumers目录下写入自己的URL地址。
2,zk的配置管理
程序总是需要配置的,如果程序分散部署在多台机器上,要逐个改变配置就变得困难。现在把这些配置全部放到zookeeper上去,保存在 Zookeeper 的某个目录节点中,然后所有相关应用程序对这个目录节点进行监听,一旦配置信息发生变化,每个应用程序就会收到 Zookeeper 的通知,然后从 Zookeeper 获取新的配置信息应用到系统中就好
3,zk的注册中心
所谓集群管理无在乎两点:是否有机器退出和加入、选举master。
- 对于第一点,所有机器约定在父目录GroupMembers下创建临时目录节点,然后监听父目录节点的子节点变化消息。一旦有机器挂掉,该机器与 zookeeper的连接断开,其所创建的临时目录节点被删除,所有其他机器都收到通知:某个兄弟目录被删除,于是,所有人都知道:它上船了。新机器加入也是类似,所有机器收到通知:新兄弟目录加入,highcount又有了
- 对于第二点,我们稍微改变一下,所有机器创建临时顺序编号目录节点,每次选取编号最小的机器作为master就好。
4,zk的分布式锁
有了zookeeper的一致性文件系统,锁的问题变得容易。锁服务可以分为两类,一个是保持独占,另一个是控制时序。
- 对于第一类,我们将zookeeper上的一个znode看作是一把锁,通过createznode的方式来实现。所有客户端都去创建 /distribute_lock 节点,最终成功创建的那个客户端也即拥有了这把锁。用完删除掉自己创建的distribute_lock 节点就释放出锁。
- 对于第二类, /distribute_lock 已经预先存在,所有客户端在它下面创建临时顺序编号目录节点,和选master一样,编号最小的获得锁,用完删除,依次方便。
5,zk的队列管理
两种类型的队列:
- 同步队列,当一个队列的成员都聚齐时,这个队列才可用,否则一直等待所有成员到达。在约定目录下创建临时目录节点,监听节点数目是否是我们要求的数目。
- 队列按照 FIFO 方式进行入队和出队操作。和分布式锁服务中的控制时序场景基本原理一致,入列有编号,出列按编号。
四 zk的常见问题
1,zk的节点信息
通过stat命令来查看主要信息,其中信息有:
1、cZxid:创建znode的事务id(Zxid的值)。
2、mZxid:最后修改znode的事务id。
3、pZxid:最后添加或删除子节点的事务id(子节点列表发生变化才会发生改变)。
4、ctime:znode创建时间。
5、mtime:znode最近修改时间。
6、dataVersion:znode的当前数据版本。
7、cversion:znode的子节点结果集版本(一个节点的子节点增加、删除都会影响这个版本)。
8、aclVersion:表示对此znode的acl版本。
9、ephemeralOwner:znode是临时znode时,表示znode所有者的sessionid,如果znode不是临时节点,则该字段设置为零。
10、dataLength:znode数据字段的长度。
2,zookeeper创建节点
1、创建持久节点:create /节点名称
2、创建持久顺序节点:create -s /节点名称
3、创建临时节点:create -e /节点名称
4、创建临时顺序节点:create -e -s /节点名称
5、监听节点:get -w /节点名称
3,zookeeper的权限控制
zookeeper有ACL( Access Control List )权限控制,可以控制节点的读写操作,保证数据的安全性。
ACL(Access Control List),分别由权限模式,授权对象和权限信息组成
1)权限模式:
- 范围验证:zookeeper可以通过对一个ip或者一段ip进行授权。
- 口令验证:用户名密码方式验证
2)授权对象: 就是把权限授予给谁,如果是范围验证方式,那么授权对象就是ip地址,如果是口令验证,授权对象就是用户名。
3)权限信息:
- 数据节点(c:create)创建权限,授予权限的对象可以在数据节点下创建子节点。
- 数据节点(w:wirte)更新权限,授予权限的对象可以更新该数据节点
- 数据节点(r:read)读取权限,授予权限的对象可以读取该节点的内容以及子节点的列表信息。
- 数据节点(d:delete)删除权限,授予权限的对象可以删除该数据节点的子节点。
- 数据节点(a:admin)管理者权限,授予权限的对象可以对该数据节点体进行ACL权限设置。
可以通过getAcl来获取某个节点的权限信息,通过setAcl来设置某个节点的权限信息。
4,zookeeper的数据持久化
zookeeper和redis很像,数据都是在内存中的,持久化也是两种方式,一种是记录事务日志,一种是快照方式。
记录事务日志磁盘会进行IO操作,事务日志的不断增多会触发磁盘为文件开辟新的磁盘块,所以为了提升磁盘的效率,可以在创建文件的时候
就向操作系统申请一块大一点的磁盘块,通过参数zookeeper.preAllocSize配置。事务日志的存放地址通过zoo.cfg配置文件中的dataDir来指定。
5,Zookeeper 的通知机制是什么?
Zookeeper 允许客户端向服务端的某个 znode 注册一个 Watcher 监听,当服务端的一些指定事件,触发了这个 Watcher ,服务端会向指定客户端发送一个事件通知来实现分布式的通知功能,然后客户端根据 Watcher 通知状态和事件类型做出业务上的改变。大致分为三个步骤:
1,客户端注册 Watcher
- 调用 getData、getChildren、exist 三个 API ,传入Watcher 对象。
- 标记请求request ,封装 Watcher 到 WatchRegistration 。
- 封装成 Packet 对象,发服务端发送request 。
- 收到服务端响应后,将 Watcher 注册到 ZKWatcherManager 中进行管理。
- 请求返回,完成注册。
2,服务端处理 Watcher
- 服务端接收 Watcher 并存储。
- Watcher 触发
- 调用 process 方法来触发 Watcher 。
3,客户端回调 Watcher
- 客户端 SendThread 线程接收事件通知,交由 EventThread 线程回调Watcher 。
- 客户端的 Watcher 机制同样是一次性的,一旦被触发后,该 Watcher 就失效了。
client 端会对某个 znode 建立一个 watcher 事件,当该 znode 发生变化时,这些 client 会收到 zk 的通知,然后 client 可以根据 znode 变化来做出业务上的改变等。
Zookeeper 对节点的 watch 监听通知是永久的吗?
不是,一次性的。无论是服务端还是客户端,一旦一个 Watcher 被触发,Zookeeper都会将其从相应的存储中移除。这样的设计有效的减轻了服务端的压力,不然对于更新非常频繁的节点,服务端会不断的向客户端发送事件通知,无论对于网络还是服务端的压力都非常大。
6,说说Zookeeper中的脑裂?
1.脑裂是什么?
简单点来说,脑裂(Split-Brain) 就是比如当你的 cluster 里面有两个节点,它们都知道在这个cluster 里需要选举出一个 master。那么当它们两个之间的通信完全没有问题的时候,就会达成共识,选出其中一个作为 master。但是如果它们之间的通信出了问题,那么两个结点都会觉得现在没有 master,所以每个都把自己选举成 master,于是 cluster 里面就会有两个 master。对于Zookeeper来说有一个很重要的问题,就是到底是根据一个什么样的情况来判断一个节点死亡down掉了?在分布式系统中这些都是有监控者来判断的,但是监控者也很难判定其他的节点的状态,唯一一个可靠的途径就是心跳,Zookeeper也是使用心跳来判断客户端是否仍然活着。使用ZooKeeper来做Leader HA基本都是同样的方式:每个节点都尝试注册一个象征leader的临时节点,其他没有注册成功的则成为follower,并且通过watch机制监控着leader所创建的临时节点,Zookeeper通过内部心跳机制来确定leader的状态,一旦leader出现意外Zookeeper能很快获悉并且通知其他的follower,其他flower在之后作出相关反应,这样就完成了一个切换,这种模式也是比较通用的模式,基本大部分都是这样实现的。但是这里面有个很严重的问题,如果注意不到会导致短暂的时间内系统出现脑裂,因为心跳出现超时可能是leader挂了,但是也可能是zookeeper节点之间网络出现了问题,导致leader假死的情况,leader其实并未死掉,但是与ZooKeeper之间的网络出现问题导致Zookeeper认为其挂掉了然后通知其他节点进行切换,这样follower中就有一个成为了leader,但是原本的leader并未死掉,这时候client也获得leader切换的消息,但是仍然会有一些延时,zookeeper需要通讯需要一个一个通知,这时候整个系统就很混乱可能有一部分client已经通知到了连接到新的leader上去了,有的client仍然连接在老的leader上,如果同时有两个client需要对leader的同一个数据更新,并且刚好这两个client此刻分别连接在新老的leader上,就会出现很严重问题。
总结: 假死:由于心跳超时(网络原因导致的)认为leader死了,但其实leader还存活着。 脑裂:由于假死会发起新的leader选举,选举出一个新的leader,但旧的leader网络又通了,导致出现了两个leader ,有的客户端连接到老的leader,而有的客户端则连接到新的leader。
2.如何解决脑裂问题的?
要解决Split-Brain脑裂的问题,一般有下面几种种方法:
- Quorums (法定人数) 方式: 比如3个节点的集群,Quorums = 2, 也就是说集群可以容忍1个节点失效,这时候还能选举出1个lead,集群还可用。比如4个节点的集群,它的Quorums = 3,Quorums要超过3,相当于集群的容忍度还是1,如果2个节点失效,那么整个集群还是无效的。这是zookeeper防止"脑裂"默认采用的方法。
- Redundant communications (冗余通信)方式:集群中采用多种通信方式,防止一种通信方式失效导致集群中的节点无法通信。
- Fencing (共享资源) 方式:比如能看到共享资源就表示在集群中,能够获得共享资源的锁的就是Leader,看不到共享资源的,就不在集群中。要想避免zookeeper"脑裂"情况其实也很简单,follower节点切换的时候不在检查到老的leader节点出现问题后马上切换,而是在休眠一段足够的时间,确保老的leader已经获知变更并且做了相关的shutdown清理工作了然后再注册成为master就能避免这类问题了,这个休眠时间一般定义为与zookeeper定义的超时时间就够了,但是这段时间内系统可能是不可用的,但是相对于数据不一致的后果来说还是值得的。
zooKeeper默认采用了Quorums这种方式来防止"脑裂"现象。即只有集群中超过半数节点投票才能选举出Leader。这样的方式可以确保leader的唯一性,要么选出唯一的一个leader,要么选举失败。在zookeeper中Quorums作用如下:*集群中最少的节点数用来选举leader保证集群可用。通知客户端数据已经安全保存前集群中最少数量的节点数已经保存了该数据。*一旦这些节点保存了该数据,客户端将被通知已经安全保存了,可以继续其他任务。而集群中剩余的节点将会最终也保存了该数据。
假设某个leader假死,其余的followers选举出了一个新的leader。这时,旧的leader复活并且仍然认为自己是leader,这个时候它向其他followers发出写请求也是会被拒绝的。因为每当新leader产生时,会生成一个epoch标号(标识当前属于那个leader的统治时期),这个epoch是递增的,followers如果确认了新的leader存在,知道其epoch,就会拒绝epoch小于现任leader epoch的所有请求。那有没有follower不知道新的leader存在呢,有可能,但肯定不是大多数,否则新leader无法产生。Zookeeper的写也遵循quorum机制,因此,得不到大多数支持的写是无效的,旧leader即使各种认为自己是leader,依然没有什么作用。zookeeper除了可以采用上面默认的Quorums方式来避免出现"脑裂",还可以可采用下面的预防措施:
- 添加冗余的心跳线,例如双线条线,尽量减少“裂脑”发生机会。
- 启用磁盘锁。正在服务一方锁住共享磁盘,“裂脑"发生时,让对方完全"抢不走"共享磁盘资源。但使用锁磁盘也会有一个不小的问题,如果占用共享盘的一方不主动"解锁”,另一方就永远得不到共享磁盘。现实中假如服务节点突然死机或崩溃,就不可能执行解锁命令。后备节点也就接管不了共享资源和应用服务。于是有人在HA中设计了"智能"锁。即正在服务的一方只在发现心跳线全部断开(察觉不到对端)时才启用磁盘锁。平时就不上锁了。
- 设置仲裁机制。例如设置参考IP(如网关IP),当心跳线完全断开时,2个节点都各自ping一下 参考IP,不通则表明断点就出在本端,不仅"心跳"、还兼对外"服务"的本端网络链路断了,即使启动(或继续)应用服务也没有用了,那就主动放弃竞争,让能够ping通参考IP的一端去起服务。更保险一些,ping不通参考IP的一方干脆就自我重启,以彻底释放有可能还占用着的那些共享资源。
7,Zookeeper选举中投票信息的五元组是什么?
Leader:被选举的 Leader 的 SID
Zxid:被选举的 Leader 的事务 ID
Sid:当前服务器的 SID
electionEpoch:当前投票的轮次
peerEpoch:当前服务器的 Epoch
Epoch > Zxid > Sid
Epoch,Zxid 都可能一致,但是 Sid 一定不一样,这样两张选票一定会 PK 出结果。
8,Zookeeper集群支持动态添加机器吗?
其实就是水平扩容了,Zookeeper 在这方面不太好。两种方式:
全部重启:关闭所有 Zookeeper 服务,修改配置之后启动。不影响之前客户端的会话。
逐个重启:在过半存活即可用的原则下,一台机器重启不影响整个集群对外提供服务。这是比较常用的方式。
3.5版本后支持动态扩容。
9,什么场景下会导致ZooKeeper发生延迟通知
watch事件延迟:节点被修改后,会有事件通知发往观察者,直到接收到watch事件,观察者才会知道节点被修改了;当管擦着接到watch事件的那一刻,该节点又被其他修改者修改了,而 近的watch事件还没有通知到观察者,就会造成延迟通知。
参考文献:
https://blog.csdn.net/u014374173/article/details/105173043
https://blog.csdn.net/weixin_38612401/article/details/125216821
leader选举:
https://blog.csdn.net/qq_35128600/article/details/104798113