ZooKeeper原理探索
ZooKeeper原理探索
0.前言
本文将探索zookeeper的组成、特性、原理、工作流程等问题。本文将持续更新,后续将探索集群的相关问题,并且研究zk在es-job中的实际使用,本文旨在zookeeper的学习与使用,系本人工作之余查阅文章并归纳总结,如有错误之处还请指正。
1.什么是Zookeeper
Zookeeper 是一个分布式协调服务的开源框架。主要作用是为分布式系统提供协调服务,包括但不限于:分布式锁、统一命名服务、配置管理、负载均衡、主控服务器选举以及主从切换等。
ZooKeeper本质上是一个分布式的小文件存储系统。提供类似与文件系统目录树方式的数据存储,并且可以对树中的节点进行有效管理。从而用来维护和监控存储的数据的状态变化。通过监控这些数据状态的变化,实现基于数据的集群管理。
2.Zookeeper Structure
Zookeeper = 文件系统 + 监听通知机制 + ACL
2.1 文件系统
Zookeeper维护一个类似文件系统的数据结构,每一个子目录项都被称作为znode(目录节点),和文件系统一样,我们能够自由的对一个znode进行CRUD,也可以在znode下进行子znode的CRUD,唯一不同的是,znode是可以存储数据的。
节点类型:P持久化、PS持久化顺序、E临时、ES临时顺序
2.2 监听通知机制
简单来说:client 注册监听它所关心的节点,当该节点发生变化(节点被改变、被删除、其子节点有增删改情况)时,zkServer会通过watcher监听机制将消息推送给client。
2.3 ACL(访问控制列表)
zookeeper在分布式系统中承担中间件的作用,它管理的每一个节点上都可能存储着重要的信息,因为应用可以读取到任意节点,这就可能造成安全问题,ACL的作用就是帮助zookeeper实现权限控制。zookeeper的权限控制基于节点,每个znode可以有不同的权限。父子节点之间权限没有关系。
一个 ACL 权限设置通常可以分为 3 部分,分别是:权限模式(Scheme)、授权对象(ID)、权限信息(Permission)。最终组成一条例如“scheme :id :permission”格式的 ACL 请求信息。
3.ZooKeeper Session
client请求和服务端建立连接,服务端会保留和标记当前client的session,包含 session过期时间、sessionId ,然后服务端开始在session过期时间的基础上倒计时,在这段时间内,client需要向server发送心跳包,目的是让server重置session过期时间。
如果因为网络状态不好,client和Server失去联系,client会停留在当前状态,会尝试主动再次连接Zookeeper Server。client不能宣称自己的session expired,session expired是由Zookeeper Server来决定的,client可以选择自己主动关闭session。
session状态变换:
4.Zookeeper Feature
- 最终一致性:client不论连接到哪个Server,展示给它都是同一个视图,这是zookeeper最重要的性能。
- 可靠性:具有简单、健壮、良好的性能,如果消息被一台服务器接受,那么它将被所有的服务器接受。即如果client对某一台server上的数据进行了CUD,那么zookeeper集群会对其他所有的server数据进行同步。
- 实时性:Zookeeper保证客户端将在一个时间间隔范围内获得服务器的更新信息,或者服务器失效的信息。但由于网络延时等原因,Zookeeper不能保证两个客户端能同时得到刚更新的数据,如果需要最新数据,应该在读数据之前调用sync()接口。
- 等待无关(wait-free):慢的或者失效的client不得干预快速的client的请求,使得每个client都能有效的等待。
- 原子性:更新只能成功或者失败,没有中间状态。
- 顺序性:包括全局有序和偏序两种:全局有序是指如果在一台服务器上消息a在消息b前发布,则在所有Server上消息a都将在消息b前被发布;偏序是指如果一个消息b在消息a后被同一个发送者发布,a必将排在b前面。
5.ZooKeeper Data
Zookeeper会维护一个具有层次关系的数据结构,它非常类似于一个标准的文件系统。
Zookeeper这种数据结构有如下这些特点:
1)每个子目录项如NameService都被称作为znode,这个znode是被它所在的路径唯一标识,如Server1这个znode的标识为/NameService/Server1。
2)znode可以有子节点目录,并且每个znode可以存储数据,注意EPHEMERAL(临时的)类型的目录节点不能有子节点目录。
3)znode是有版本的(version),每个znode中存储的数据可以有多个版本,也就是一个访问路径中可以存储多份数据,version号自动增加。
4)znode的类型:
-
Persistent 节点,一旦被创建,便不会意外丢失,即使服务器全部重启也依然存在。每个 Persist 节点即可包含数据,也可包含子节点。
-
Ephemeral 节点,在创建它的客户端与服务器间的 Session 结束时自动被删除。服务器重启会导致 Session 结束,因此 Ephemeral 类型的 znode 此时也会自动删除。
-
Non-sequence 节点,多个客户端同时创建同一个 Non-sequence 节点时,只有一个可创建成功,其它匀失败。并且创建出的节点名称与创建时指定的节点名完全一样。
-
Sequence 节点,创建出的节点名在指定的名称之后带有10位10进制数的序号。多个客户端创建同一名称的节点时,都能创建成功,只是序号不同。
5)znode可以被监控,包括这个目录节点中存储的数据的修改,子节点目录的变化等,一旦变化可以通知设置监控的客户端,这个是Zookeeper的核心特性,Zookeeper的很多功能都是基于这个特性实现的。
6)ZXID:每次对Zookeeper的状态的改变都会产生一个zxid(ZooKeeper Transaction Id),zxid是全局有序的,如果zxid1小于zxid2,则zxid1在zxid2之前发生。
6. ZooKeeper Watch
Zookeeper所有的读操作getData(), getChildren()和 exists()都可以设置监视(watch),监视事件可以理解为一次性的触发器。
6.1 一次性触发
当设置监视的数据发生改变时,该监视事件会被发送到客户端,例如,如果客户端调用了getData("/znode1", true) 并且稍后 /znode1 节点上的数据发生了改变或者被删除了,客户端将会获取到 /znode1 发生变化的监视事件,而如果 /znode1 再一次发生了变化,除非客户端再次对/znode1 设置监视,否则客户端不会收到事件通知。
6.2 发送至客户端
Watch的通知事件是从服务器发送给客户端的,是异步的,这就表明不同的客户端收到的Watch的时间可能不同,但是ZooKeeper有保证:当一个客户端在看到Watch事件之前是不会看到结点数据的变化的。例如:A=3,此时在上面设置了一次Watch,如果A突然变成4了,那么客户端会先收到Watch事件的通知,然后才会看到A=4。
Zookeeper客户端和服务端是通过 socket 进行通信的,由于网络存在故障,所以监视事件很有可能不会成功地到达客户端,监视事件是异步发送至监视者的,Zookeeper 本身提供了顺序保证(ordering guarantee):即客户端只有首先看到了监视事件后,才会感知到它所设置监视的znode发生了变化。
6.3 被设置 watch 的数据
这意味着znode节点本身具有不同的改变方式。你也可以想象 Zookeeper 维护了两条监视链表:数据监视和子节点监视 getData() 和exists()设置数据监视,getChildren()设置子节点监视。或者你也可以想象 Zookeeper 设置的不同监视返回不同的数据,getData() 和 exists() 返回znode节点的相关信息,而getChildren() 返回子节点列表。因此,setData() 会触发设置在某一节点上所设置的数据监视(假定数据设置成功),而一次成功的create() 操作则会出发当前节点上所设置的数据监视以及父节点的子节点监视。一次成功的 delete操作将会触发当前节点的数据监视和子节点监视事件,同时也会触发该节点父节点的child watch。
6.4 watch触发的情况
可以注册watcher的方法:getData、exists、getChildren。
可以触发watcher的方法(操作):create、delete、setData。
New ZooKeeper时注册的watcher叫default watcher,它不是一次性的,只对client的连接状态变化作出反应。
操作会产生事件,事件会触发watcher。
操作产生事件的类型:
事件触发对应的watcher:
7. Zookeeper Characher
在zookeeper的集群中,各个节点共有下面3种角色和4种状态:
- 角色:leader,follower,observer
- 状态:leading,following,observing,looking
Zookeeper的核心是原子广播(Zab),这个机制保证了各个Server之间的同步。实现这个机制的协议叫做Zab协议(ZooKeeper Atomic Broadcast protocol)。Zab协议有两种模式,它们分别是恢复模式(Recovery选主)和广播模式(Broadcast同步)。当服务启动或者在领导者崩溃后,Zab就进入了恢复模式,当领导者被选举出来,且大多数Server完成了和leader的状态同步以后,恢复模式就结束了。状态同步保证了leader和Server具有相同的系统状态。
为了保证事务的顺序一致性,zookeeper采用了递增的事务id号(zxid)来标识事务。所有的提议(proposal)都在被提出的时候加上了zxid。实现中zxid是一个64位的数字,它高32位是epoch用来标识leader关系是否改变,每次一个leader被选出来,它都会有一个新的epoch,标识当前属于那个leader的统治时期。低32位用于递增计数。
Leader
Leader是整个zk集群的核心,负责响应所有对Zookeeper状态变更的请求。它会将每个状态更新请求进行排序和编号,以便保证整个集群内部消息处理的FIFO。
- 处理事务请求(CUD)和非事务请求。Leader是zk中唯一一个有权对事务请求进行调度和处理的角色,能够保证集群事务处理的顺序。
Follower
-
处理client非事务请求,如果client发出的请求是事务请求,会转发给Leader。
-
参与集群对Leader选举投票。
-
参与事务请求 Proposal 的投票(leader发起的提案,要求follower投票,需要半数以上follower节点通过,leader才会commit数据);
在ZooKeeper的实现中,每一个事务请求都需要集群中过半机器投票才能被真正应用到ZooKeeper的内存中去,这个投票与统计过程被称为Proposal流程。
Observer
Observer除了不参与Leader选举投票外,与Follower作用相同。Observer通常用于在不影响集群事务处理能力的前提下,提升集群的非事务处理能力。简单来说,observer可以在不影响集群写(CUD)性能的情况下,提升集群读(R)性能,并且因为它不参与投票,所以他们不属于ZooKeeper集群的关键部位,即使他们failed,或者从集群中断开,也不会影响集群的可用性。
引入观察者的一个主要原因是提高读请求的可扩展性。通过加入多个观察者,我们可以在不牺牲写操作的吞吐率的前提下服务更多的读操作。但是引入观察者也不是完全没有开销,每一个新加入的观察者将对应于每一个已提交事务点引入的一条额外消息。
采用观察者的另外一个原因是进行跨多个数据中心部署。由于数据中心之间的网络链接延时,将服务器分散于多个数据中心将明显地降低系统的速度。引入观察者后,更新请求能够先以高吞吐量和低延迟的方式在一个数据中心内执行,接下来再传播到异地的其他数据中心得到执行。
每个Server在工作过程中有4种状态:
- LOOKING:当前Server不知道leader是谁,正在搜寻。
- LEADING:当前Server即为选举出来的leader。
- FOLLOWING:leader已经选举出来,当前Server与之同步。
- OBSERVING:observer的行为在大多数情况下与follower完全一致,但是他们不参加选举和投票,而仅仅接受(observing)选举和投票的结果。
8. ZooKeeper Election
当leader崩溃或者leader失去大多数的follower,这时候zk进入恢复模式,恢复模式需要重新选举出一个新的leader,让所有的Server都恢复到一个正确的状态。Zk的选举算法有两种:一种是基于basic paxos实现的,另外一种是基于fast paxos算法实现的。系统默认的选举算法为fast paxos。
basic paxos流程:
- 选举线程由当前Server发起选举的线程担任,其主要功能是对投票结果进行统计,并选出推荐的Server;
- 选举线程首先向所有Server发起一次询问(包括自己);
- 选举线程收到回复后,验证是否是自己发起的询问(验证zxid是否一致),然后获取对方的id(myid),并存储到当前询问对象列表中,最后获取对方提议的leader相关信息(id,zxid),并将这些信息存储到当次选举的投票记录表中;
- 收到所有Server回复以后,就计算出zxid最大的那个Server,并将这个Server相关信息设置成下一次要投票的Server;
- 线程将当前zxid最大的Server设置为当前Server要推荐的Leader,如果此时获胜的Server获得n/2 + 1的Server票数,设置当前推荐的leader为获胜的Server,将根据获胜的Server相关信息设置自己的状态,否则,继续这个过程,直到leader被选举出来。
通过流程分析我们可以得出:第一轮发起询问找出最大的zxid,第二轮询问投这个最大的zxid的server。比如:zxid为1到7的7个server参与选举,由5担任,每个server收到5的zxid和sid,各自比较更新投票信息,1-4更新为5,6-7保持自己的zxid不变,5经过统计后得到最大zxid为7,再次发起询问投票7,此时将得到半数以上投票,则选举结束。
每个Server启动后都会重复以上流程。在恢复模式下,如果是刚从崩溃状态恢复的或者刚启动的server还会从磁盘快照中恢复数据和会话信息,zk会记录事务日志并定期进行快照,方便在恢复时进行状态恢复。
fast paxos流程是在选举过程中,某Server首先向所有Server提议自己要成为leader,当其它Server收到提议以后,解决epoch和zxid的冲突,并接受对方的提议,然后向对方发送接受提议完成的消息,重复这个流程,最后一定能选举出Leader。
9. ZooKeeper Work
9.1 Leader工作流程
Leader主要有三个功能:
- 恢复数据;
- 维持与follower的心跳,接收follower请求并判断follower的请求消息类型;
- follower的消息类型主要有PING消息、REQUEST消息、ACK消息、REVALIDATE消息,根据不同的消息类型,进行不同的处理。
- PING消息是指follower的心跳信息;REQUEST消息是follower发送的提议信息,包括写请求及同步请求;
- ACK消息是follower的对提议的回复,超过半数的follower通过,则commit该提议;
- REVALIDATE消息是用来延长SESSION有效时间。
9.2 Follower工作流程
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结果到客户端,这个消息最初由客户端发起,用来强制得到最新的更新。
Zab协议
Zookeeper Server接收到一次request,如果是follower,会转发给leader,Leader执行请求并通过Transaction的形式广播这次执行。Zookeeper集群如何决定一个Transaction是否被commit执行?
通过“两段提交协议”(a two-phase commit):
- Leader给所有的follower发送一个PROPOSAL消息。
- 一个follower接收到这次PROPOSAL消息,写到磁盘,发送给leader一个ACK消息,告知已经收到。
- 当Leader收到法定人数(quorum)的follower的ACK时候,发送commit消息执行。
Zab协议保证:
- 如果leader以T1和T2的顺序广播,那么所有的Server必须先执行T1,再执行T2。
- 如果任意一个Server以T1、T2的顺序commit执行,其他所有的Server也必须以T1、T2的顺序执行。
“两段提交协议”最大的问题是如果Leader发送了PROPOSAL消息后crash或暂时失去连接,会导致整个集群处在一种不确定的状态(follower不知道该放弃这次提交还是执行提交)。Zookeeper这时会选出新的leader,请求处理也会移到新的leader上,不同的leader由不同的epoch标识。切换Leader时,需要解决下面两个问题:
-
不要忘记已发送的信息
Leader在COMMIT投递到任何一台follower之前crash,只有它自己commit了。新Leader必须保证这个事务也必须commit。
-
放弃跳过的消息
Leader产生某个proposal,但是在crash之前,没有follower看到这个proposal。该server恢复时,必须丢弃这个proposal。
Zookeeper会尽量保证不会同时有2个活动的Leader,因为2个不同的Leader会导致集群处在一种不一致的状态,所以Zab协议同时保证:
- 在新的leader广播Transaction之前,先前Leader commit的Transaction都会先执行。
- 在任意时刻,都不会有2个Server同时有法定人数(quorum)的支持者。
这里的quorum是一半以上的Server数目,确切的说是有投票权力的Server(不包括Observer)。
读写流程
1)读流程分析
因为ZooKeeper集群中所有的server节点都拥有相同的数据,所以读的时候可以在任意一台server节点上,客户端连接到集群中某一节点,读请求,然后直接返回。当然因为ZooKeeper协议的原因(一半以上的server节点都成功写入了数据,这次写请求便算是成功),读数据的时候可能会读到数据不是最新的server节点,所以比较推荐使用watch机制,在数据改变时,及时感应到。
2)写流程分析
当一个客户端进行写数据请求时,会指定ZooKeeper集群中的一个server节点,如果该节点为Follower,则该节点会把写请求转发给Leader,Leader通过内部的协议进行原子广播,直到一半以上的server节点都成功写入了数据,这次写请求便算是成功,然后Leader便会通知相应Follower节点写请求成功,该节点向client返回写入成功响应。
并发读写分析
ZooKeeper的数据模型是层次型,类似文件系统,不过ZooKeeper的设计目标定位是简单、高可靠、高吞吐、低延迟的内存型存储系统,因此它的value不像文件系统那样适合保存大的值,官方建议保存的value大小要小于1M,key为路径。
ZooKeeper的层次模型是通过ConcurrentHashMap实现的,key为path,value为DataNode,DataNode保存了znode中的value、children、 stat等信息。
对于ZooKeeper来讲,ZooKeeper的写请求由Leader处理,Leader能够保证并发写入的有序性,即同一时刻,只有一个写操作被批准,然后对该写操作进行全局编号,最后进行原子广播写入,所以ZooKeeper的并发写请求是顺序处理的,而底层又是用了ConcurrentHashMap,理所当然写请求是线程安全的。而对于并发读请求,同理,因为用了ConcurrentHashMap,当然也是线程安全的了。总结来说,ZooKeeper的并发读写是线程安全的。
但是对于ZooKeeper的客户端来讲,如果使用了watch机制,在进行了读请求但是watch znode前这段时间中,如果znode的数据变化了,客户端是无法感知到的,这段时间客户端的数据就有一定的滞后性了,只有当下次数据变化后,客户端才能感知到,所以对于客户端来说,数据是最终一致性。
10. Zookeeper Cluster
为什么是奇数台servers,之前介绍中经常有说到一个关键词(半数以上)。在zookeeper中,半数以上机制比较重要。例如:半数以上服务器存活,该集群才可正常运行;半数以上投票,本次事务请求才可通过等。
配置文件修改:
tickTime=2000 #通信心跳时间,Zookeeper服务器与客户端心跳时间,单位毫秒
initLimit=10 #Leader和Follower初始连接时能容忍的最多心跳数(tickTime的数量),这里表示为10*2s
syncLimit=5 #Leader和Follower之间同步通信的超时时间,这里表示如果超过5*2s,Leader认为Follwer死掉,并从服务器列表中删除Follwer
dataDir=/usr/local/zookeeper-3.5.7/data ●修改,指定保存Zookeeper中的数据的目录,目录需要单独创建
dataLogDir=/usr/local/zookeeper-3.5.7/logs ●添加,指定存放日志的目录,目录需要单独创建
clientPort=2181 #客户端连接端口
#添加集群信息
server.1=192.168.40.10:3188:3288
server.2=192.168.40.20:3188:3288
server.3=192.168.40.30:3188:3288
#集群节点通信时使用端口3188,选举leader时使用的端口3288
-------------------------------------------------------------------------------------
server.A=B:C:D
●A是一个数字,表示这个是第几号服务器。集群模式下需要在zoo.cfg中dataDir指定的目录下创建一个文件myid,这个文件里面有且只有一个数据就是A的值,Zookeeper启动时读取此文件,拿到里面的数据与zoo.cfg里面的配置信息比较从而判断到底是哪个server。
●B是这个服务器的地址。
●C是这个集群中server之间数据同步的端口。
●D是集群中选举新的Leader时服务器相互通信的端口
10.1 ZooKeeper Reconfig
zk动态配置可解决之前zk集群日常扩缩容过程中的如下问题:
-
zk集群短时间内不可用:zk节点滚动重启导致重新选举,选举周期内zk集群对外不可用;
-
依赖zk client端重连:zk节点滚动重启导致已建立的客户端连接被断开,客户端需主动重连其他节点;
-
扩缩容过程繁琐易出错:在静态配置版本下,扩容操作包括:配置新节点、启动新节点、配置老节点、滚动重启老节点。操作繁琐,步骤冗长,依赖人工容易出错。
10.1.1 为什么不能简单的扩容
假设现在有三台机器组成的ZooKeeper集群。但是一两个月后,你会发现使用ZooKeeper的客户端越来越多,并且成为一个关键的服务,因此你想要把服务器扩容到五台,你可以在深夜停止集群,重新配置所有服务器,并在不到一分钟的时间里恢复服务。如果你的应用程序恰当处理了Disconnected事件,用户可能不会感知到服务中断。但事实证明,情况要复杂得多。请看下图中的场景,三个服务器(A、B、C)组成的集群。服务器C因网络拥塞稍稍落后于整个集群,因此服务器C刚刚了解事务到(1,3)(其中1为时间戳,3为对应该时间戳的事务标识)。因为服务器A和B的通信良好,所以服务器C稍稍落后并不会导致整个系统变慢,因此服务器A和B已经提交事务到(1,6)。
现在假设我们将所有zookeeper服务停止,把服务器D和E添加到集群中,当然这两台新服务器并不存在任何状态信息。我们重新配置A、B、C、D和E服务器,使它们成为一个更大的集群,然后重新启动服务。因为我们现在有五台服务器,因此至少需要台服务器组成法定人数。C、D和E三台服务器就能够构成法定人数,因此当这些服务器(下图)构成法定人数并开始同步时会发生什么。这个场景可以简单实现,比如让A和B比其他三个服务器晚启动一些。一旦新的法定人数开始同步,服务器D和E就会与服务器C进行同步,因为法定人数中服务器C的状态为最新状态,法定人数的三个成员服务器会同步到最后的事务(1,3),而不会同步(1,4)、(1,5)和(1,6)这三个事务,因为服务器A和B并不是构成法定人数的成员。
因为C、D和E三台服务器已经构成一个活跃的法定人数,这些服务器可以开始提交新事务。我们假设有两个事务:(2,1)和(2,2)。如下图所示,当服务器A和B启动后连接到服务器C后,服务器C作为leader欢迎它们加入到集群中,并在收到事务(2,1)和(2,2)后,立即告知服务器A和B删除事务(1,4)、(1,5)和(16)
重配置不仅可以让我们改变集群成员配置,还可以修改网络参数配置。因为ZooKeeper中配置信息的变化,需要将重配置参数与静态的配置文件分离,并将其单独保存为一个配置文件(reconfig命令会自动更新该文件)。
10.1.2 如何做zookeeper的重配置
假如现在有一个三台服务器组成的zookeeper集群,每台zkServer的配置如下:
tickTime=2000
#投票选举新leader的初始化时间
initLimit=10
#leader与fo1lower心跳检测最大容忍时间,响应超过syncLimit*tickTime,leader认为follower“死掉”,从服务器列表中删除fo1lowei
syncLimit=5
#ZooKeeper对外服务端口
clientPort=2181
#快照目录
dataDir=/tmp/zookeeper/data
#事务日志目录
dataLogDir=/tmp/zookeeper/log
#ZooKeeper集群中服务器的列表
server.1=10.1.1.11:2888:3888
server.2=10.1.1.12:2888:3888
server.3=10.1.1.13:2888:3888
#允许执行reconfig命令
reconfigEnabled=true
#允许执行四字母命令
4lw.commands.whitelist=*
#禁止leader为客户端提供服务
leaderServes=no
在启动zk服务后,配置文件会被拆分为两个文件:
Zoo.cfg
initLimit=10
syncLimit=5
leaderServes=no
4lw.commands.whitelist=*
clientPort=2181
tickTime=2000
dataDir=/tmp/zookeeper/data
reconfigEnabled=true
dataLogDir=/tmp/zookeeper/log
dynamicConfigFile=/usr/local/apache-zookeeper-3.5.9-bin/conf/zoo.cfg.dynamic.100000000
zoo.cfg.dynamic.100000000
server.1=10.1.1.11:2888:3888:participant
server.2=10.1.1.12:2888:3888:participant
server.3=10.1.1.13:2888:3888:participant
然后修改zoo.cfg文件,并分发到所有zkServer:
initLimit=10
syncLimit=5
leaderServes=no
4lw.commands.whitelist=*
tickTime=2000
dataDir=/tmp/zookeeper/data
reconfigEnabled=true
dataLogDir=/tmp/zookeeper/log
dynamicConfigFile=/usr/local/apache-zookeeper-3.5.9-bin/conf/zoo.cfg.dynamic.100000000
新的zkServer配置,并启动:
server.1=10.1.1.11:2888:3888:participant
server.2=10.1.1.12:2888:3888:participant
server.3=10.1.1.13:2888:3888:participant
server.4=10.1.1.13:2888:3888:participant;0.0.0.0:2181
然后执行:
mkdir -p /tmp/zookeeper/{data,log}
echo'4'>/tmp/zookeeper/data/myid
zkServer.sh start
reconfig -add server.4=10.1.1.14:2888:3888:participant;2181,server.5=10.1.1.15:2888:3888:participant;2181
reconfig -members server.1=10.1.1.11:2888:3888:participant;0.0.0.0:2181,server.2=10.1.1.12:2888:3888:participant;0.0.0.0:2181,server.4=10.1.1.14:2888:3888:participant;0.0.0.0:2181,server.5=10.1.1.15:2888:3888:participant;0.0.0.0:2181
10.2 Zookeeper Brain-Split
ZooKeeper 内部通过心跳机制来确定 leader 的状态,一旦 leader 节点出现问题,集群内部就能立即获悉并迅速通知其他 follower 节点来选出新的 leader。
设想这样一种情况:
-
集群中网络通信不好,导致心跳监测超时 —— follower 认为 leader 节点由于某种原因挂掉了,可其实 leader 节点并未真正挂掉 —— 这就是假死现象。
-
leader 节点假死后,ZooKeeper 通知所有 follower 节点进行新的选举 ==> 某个 follower 节点升级为新的 leader —— 此时集群中存在2个leader节点。
-
如果部分 client 获得了新 leader 节点的信息,而部分没有获得,恰好此时 client 向 ZooKeeper 发起读写请求,ZooKeeper 内部的不一致就会导致:部分 client 连接到了新的 leader 节点,而部分 client 连接到了旧的 leader节点 —— 服务中出现了2个leader,client 不知道听谁好。
10.2.1 如何解决
(1) Quorums(法定人数)
通过设置法定人数,进而确定集群的容忍度,当集群中存活的节点少于法定人数,集群将不可用。比如:
3个节点的集群中,Quorums = 2 —— 集群可以容忍 (3 - 2 = 1) 个节点失败,这时候还能选举出 leader,集群仍然可用;
4个节点的集群中,Quorums = 3 —— 集群同样可以容忍 1 个节点失败,如果2个节点失败,那整个集群就不可用了。
(2) Redundant communications(冗余通信)
集群中采用多种通信方式,防止一种通信方式失效导致集群中的节点无法通信。
(3) Fencing(隔离,即共享资源)
通过隔离的方式,将所有共享资源隔离起来,能对共享资源进行写操作(即加锁)的节点就是 leader 节点。
10.2.2 ZooKeeper的解决方案
ZooKeeper 默认采用了Quorums 的方式:只有获得超过半数节点的投票,才能选举出 leader。
假设leader 发生了假死,followers 选举出了一个新的 leader。
当旧的 leader 复活并认为自己仍然是 leader,它向其他 followers 发出写请求时,会被拒绝。
因为 ZooKeeper 维护了一个叫 epoch 的变量,每当新 leader 产生时,epoch 就会递增,followers 如果确认了新的 leader,同时也会知道其 epoch 的值。它们会拒绝所有 epoch 小于现任 leader 的 epoch 的旧 leader 的请求。
注意:仍然会存在有部分 followers 不知道新 leader 的存在,但肯定不是大多数,否则新 leader 将无法产生。
以上
本博客内容仅供个人学习使用,禁止用于商业用途。转载需注明出处并链接至原文。