前言
ZooKeeper是Hadoop的开源子项目(**Google Chubby的开源实现**),它是一个针对大型分布式系统的可靠协调系统,提供的功能包括:统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等。Zookeeper的Fast Fail和 Leader选举特性大大增强了分布式集群的稳定和健壮性,并且**解决了Master/Slave模式的单点故障**重大隐患。
1. 分布式架构
分布式系统体系结构从其出现之初就伴随着诸多的难题和挑战,比较典型的问题如下:
问题 | 描述 |
---|---|
通讯异常 | 节点之间通讯需要网络,一般而言单机内存通讯延时在纳秒数量级(10ns左右),网络通讯延迟为毫秒数量级(0.1~1ms左右),网络本身的不可靠,引入了额外的问题,每次网络通讯伴随着网络不可用的风险,网络光纤、路由器、DNS等硬件设备或是系统不可用都会导致最终分布式系统无法顺利完成一次网络通讯。 |
网络分区 | 俗称脑裂,由于网络发生异常,导致分布式系统部分节点之间的网络延时不断增大,最终导致组成分布式系统的所有节点中,只有部分节点之间能够正常通讯的情形。 |
三态 | 每一次分布式系统的请求与响应结果有三种情况:成功、失败与超时。对于超时,有时可能是请求发送的过程中由于网络原因发生消息丢失现象,也可能是请求成功后,响应反馈给请求方时发生丢失。 |
节点故障 | 组成分布式系统的服务器节点出现宕机或僵死现象。 |
为了解决上述问题,在分布式系统事务处理和一致性上引入了事务的概念,事务具有四个特征,分别是原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability),简称ACID特性。
1.1. ACID(本地事务)
ACID | 描述 |
---|---|
原子性 | 整个事务中的所有操作,要么全部完成,要么全部不完成,不可能停滞在中间某个环节。事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像这个事务从来没有执行过一样。 |
一致性 | 数据库的约束 级联和触发机制Trigger都必须满足事务的一致性。也就是说,通过各种途径包括外键约束等任何写入数据库的数据都是有效的,不能发生表与表之间存在外键约束,但是有数据却违背这种约束性。 |
隔离性 | 隔离状态执行事务,使它们好像是系统在给定时间内执行的唯一操作。如果有两个事务,运行在相同的时间内,执行相同的功能,事务的隔离性将确保每一事务在系统中认为只有该事务在使用系统。这种属性有时称为串行化,为了防止事务操作间的混淆,必须串行化或序列化请求,使得在同一时间仅有一个请求用于同一数据。 |
持久性 | 一旦一个事务被提交,它应该持久保存,不会因为和其他操作冲突而取消这个事务。很多人认为这意味着事务是持久在磁盘上,但是规范没有特别定义这点。 |
SQL规范隔离级别:
隔离级别 | 脏读 | 重复读 | 幻读 | 描述 |
---|---|---|---|---|
未授权读 (Read Uncommitted) |
存在 | 不可以 | 存在 | 它充许令外一个事务可以看到这个事务未提交的数据。 |
授权读取 (Read Committed) |
不存在 | 不可以 | 存在 | 保证一个事务修改的数据提交后才能被另外一个事务读取。另外一个事务不能读取该事务未提交的数据。 |
可重复读取 (Repeatable Read) |
不存在 | 可以 | 存在 | 保证在事务处理过程中,多次读取同一个数据时,其值都和事务开始时刻是一致的。 |
串行化 (Serializable) |
不存在 | 可以 | 不存在 | 最严格的事务隔离级别,要求所有事务都被串行执行,即事务只能一个接一个地进行处理,不能并发执行。 |
以下为4种隔离级别示意图:<br />![](https://cdn.nlark.com/yuque/0/2021/png/788484/1630978814584-a65b0c5c-0195-48f5-921a-cda34885a342.png#crop=0&crop=0&crop=1&crop=1&height=308&id=sSezr&originHeight=474&originWidth=898&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=&width=583)<br /> 事务隔离级别越高,就越能保证数据的完整性和一致性,但同时对并发性能的影响也越大。通常,对于绝大多数的应用程序来说,可以优先考虑将数据库系统的隔离级别设置为授权读取,这能够在避免脏读的同时保证较好的并发性能。尽管这种事务隔离级别会导致不可重复读、虚读和第二类丢失更新等并发问题,但较为科学的做法是在可能出现这类问题的个别场合中,由应用程序主动采用悲观锁和乐观锁来进行事务控制。<br /> 主流数据库隔离级别:
数据库 | 默认隔离级别 | 最大隔离级别 |
---|---|---|
Greenplum 4.1 | RC | S |
IBM DB2 10 for z/OS CS | S | |
IBM Informix 11.50 | Depends RR | |
MySQL 5.6 | RR | S |
MS SQL Server 2012 | RC | S |
NuoDB | CR | CR |
Oracle 11g | RC | SI |
Oracle Berkeley DB | S | S |
Oracle Berkeley DB JE | RR | S |
Postgres 9.2.2 | RC | S |
级别说明:
- RC:read committed(未提交读)
- SI:snapshot isolation(快照隔离)
- RR:repeatable read(可重复读)
- CS:cursor stability(游标稳定性-已淘汰)
- S:serializability(可串行化)
- CR:consistent read(一致性读)
1.2. CAP
CAP理论告诉我们,一个分布式系统不可能同时满足Consistency(C:一致性),Availability(A:可用性)和Partition tolerance(P:分区容忍性) 这三个基本需求,最多只能同时满足其中的两项。关系型数据库设计选择了C(一致性)与A(可用性),NoSQL数据库设计则不同。其中,HBase选择了C(一致性)与P(分区可容忍性),Cassandra选择了A(可用性)与P(分区可容忍性)。
CAP定理描述:
CAP | 描述 |
---|---|
一致性 | 数据在多个副本之间是否能够保持一致的特性。在一致性的需求下,当一个系统在数据一致的状态下执行更新操作后,应该保证系统的数据仍然处于一致的状态。 |
可用性 | 系统提供的服务必须一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。 |
分区容错性 | 分布式系统在遇到任何网络分区故障的时候,仍然需要能够保证对外提供满足一致性和可用的服务,除非是整个网络环境都发生了故障。分区容错性是分布式系统最基本的要求。 |
CAP定理应用:
放弃CAP定理 | 说明 |
---|---|
放弃P | 如果希望能够避免系统出现分区容错性问题,一种较为简单的做法是将所有的数据(或者仅仅是那些与事务相关的数据)都放在一个分布式节点上。这样的做法虽然无法100%地保证系统不会出错、但至少不会碰到由于网络分区带来的负面影响。但同时需要注意的是,放弃P的同时也就意味着放弃了系统的可扩展性。 |
放弃A | 相对于放弃分区容错性来说,放弃可用性则正好相反,其做法是一旦系统遇到网络分区或其他故障时,那么受到影响的服务需要等待一定的时间,因此在等待期间系统无法对外提供正常的服务,即不可用。 |
放弃C | 这里所说的放弃一致性,并不是完全不需要数据一致性,如果真是这样的话,那么系统的数据都是没有意义的,整个系统也是没有价值的。事实上,放弃一致性指的是放弃数据的强一致性,而保留数据的最终一致性。这样的系统无法保证数据保持实时的一致性,但是能够承诺的是,数据最终会达到一个一致的状态。这就引入了一个时间窗口的概念,具体多久能够达到数据一致性取决于系统的设计,主要包括数据副本在不同节点之间的复制时间长短。 |
1.3. BASE
BASE是Basically Available(基本可用)、Soft state(软状态)和Eventually consistent(最终一致性)三个短语的简写。BASE是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的总结,是基于CAP定理逐步演化而来的,其核心思想是**即使无法做到强一致性,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性**。
BASE | 描述 |
---|---|
基本可用 | 分布式遇到不可预知故障时,允许部分可用性。例如:1.响应时间上的损失(正常联机查询500ms内响应,机房断电活着断网故障时,响应可能增加至1-2秒);2.功能上的损失(购物网站促购高峰时段,为保障系统稳定性); |
弱状态 | 也称软状态,和硬状态相对,是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。 |
最终一致性 | 最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。 |
在实际工程实践中,最终一致性存在以下五类主要变种:
最终一致性 | 描述 |
---|---|
因果一致性 | 如果进程A在更新完某个数据项后通知了进程B,那么进程B之后对数据项的访问都应该能够或得到进程A更新后的最新值,并且如果进程B要对改数据项进行更新操作的话,务必基于进程A更新后的最新值,即不能发生丢失更新情况。与此同时,与进程A无因果关系的进程C的数据访问则没有这样的显示。 |
读己之所写 | 指进程A更新一个数据项之后,它自己总是能够访问到更新过的最新值,而不会看到旧值。也就是说,对于单个数据获取者来说,其读取到的数据,一定不会比自己上次写入的值旧。因此读己之所写,也是一种特殊的因果一致性。 |
会话一致性 | 会话一致性将对系统数据的访问过程框定在了一个会话中,系统能保证在同一个有效的会话中实现“读己之所写”的一致性,也就是说,执行更新操作之后,客户端能够在一个会话中始终读取到改数据项的最新值。 |
单调读一致性 | 读一致性是指如果一个进程从系统中读取出一个数据项的某个值后,那么系统对于该进程后续的任何数据访问都不应该返回更旧的值。 |
单调写一致性 | 单调写一致性:一个系统需要能够保证来自同一个进程的写操作被顺利的执行。 |
总的来说,BASE理论面向的是大型高可用可扩展的分布式系统,和传统事务的ACID特性是相反的,它完全不同于ACID 的强一致性模型,而是提出通过牺牲强一致性来获得可用性,并允许数据在同一段时间内是不一致的,但最终达到一致的状态。但同时,在实际的分布式场景中,不同业务单元和组件对数据一致性的要求是不同的,因此在具体的分布式系统架构设计过程中,ACID特性与BASE理论往往又会结合在一起使用。<br />应用案例:
- Seata:内置了对AT、XA(2PC、3PC)、TCC、saga的支持。
Apache ShardingSphere(ShardingJDBC) :支持XA(JTA)、集成Seata(柔性事务-BASE)。
2. 一致性协议
为解决分布式一致性问题,在长期的探索研究过程中,涌现出了一大批经典的一致性协议和算法,其中最著名的就是2PC、3PC和Paxos算法。在分布式系统中,每个机器都可以确定自己进行的事务操作是否成功,但是无法直接了解其他机器的操作结果。因此,当一个分布式事务操作需要保持ACID 特性时,就需要一个“协调者”节点调度其他“参与者”节点来进行分布式事务操作。
2.1. 2PC
Two-Phase Commit(两阶段提交)是计算机网络尤其是在数据库领域内,为了使基于分布式系统架构下的所有节点在进行事务处理过程中能够保持原子性和致性而设计的一种算法。通常,二阶段提交协议也被认为是一种一致性协议,用来保证分布式系统数据的一致性。绝大部分的关系型数据库都是采用二阶段提交协议来完成分布式事务处理。
第一阶段:提交事务请求(投票阶段)
- 事务询问。
协调者向所有的参与者发送事务内容,询问是否可以执行事务提交操作,并开始等待各参与者的响应。
- 执行事务。
各参与者节点执行事务操作,并将Undo和Redo信息计入事务日志中。
- 各参与者向协调者反馈事务询问的响应。
如果参与者成功执行了事务操作,那么就反馈给协调者Yes响应,表示事务可以执行;如果参与者没有成功执行事务,那么就反馈给协调者No响应,表示事务不可以执行。
- 第二阶段:执行事务提交(执行阶段)
(1)执行事务提交
如果所有参与者的反馈都是 Yes 响应,那么
- 发送提交请求。
协调者向所有参与者节点发出Commit请求。
- 事务提交。
参与者接收到Commit请求后,会正式执行事务提交操作,并在完成提交之后释放在整个事务执行期间占用的事务资源。
- 反馈事务提交结果。
参与者在完成事务提交之后,向协调者发送ACK信息。
- 完成事务。
协调者接收到所有参与者反馈的ACK消息后,完成事务
(2)中断事务
任何一个参与者反馈了 No 响应,或者在等待超时之后,协调者尚无法接收到所有参与者的反馈响应,那么就会中断事务。
- 发送回滚请求。
协调者向所有参与者节点发出Rollback请求。
- 事务回滚。
参与者接收到rollback请求后,会利用其在阶段一中记录的Undo信息来执行事务回滚操作,并在完成回滚之后释放整个事务执行期间占用的资源。
- 反馈事务回滚结果。
参与者在完成事务回滚之后,向协调者发送ACK信息。
- 中断事务。
协调者接收到所有参与者反馈的ACK信息后,完成事务中断。
2PC优缺点 | 描述 |
---|---|
优点 | 原理简单、实现方便 |
缺点-同步阻塞 | 同步阻塞会极大地限制分布式系统的性能。在2PC的执行过程中,所有参与该事务操作的逻辑都处于阻塞状态,各个参与者在等待其他参与者响应的过程中,将无法进行其他任何操作。 |
缺点-单点问题 | 一旦协调者出现问题,那么整个2PC流程将无法运转,更为严重的是,如果是在阶段二中出现问题,那么其他参与者将会一直处于锁定事务资源的状态中,无法继续完成事务操作。 |
缺点-数据不一致 | 当协调者向所有参与者发送commit请求之后,发生了局部网络异常或协调者在尚未发完commit请求之前自身发生了崩溃,导致最终只有部分参与者接收到了commit请求,于是这部分参与者执行事务提交,而没收到commit请求的参与者则无法进行事务提交,于是整个分布式系统出现了数据不一致性现象。 |
缺点-太过保守 | 如果参与者在与协调者通信期间出现故障,协调者只能靠超时机制来判断是否需要中断事务,这个策略比较保守,需要更为完善的容错机制,任意一个节点的失败都会导致整个事务的失败。 |
2.2. 3PC
Three-Phase Commit(三阶段提交),其将2PC的“提交事务请求”过程一分为二,形成了由 CanCommit、PreCommit和doCommit 三个阶段组成的事务处理协议。<br />![](https://cdn.nlark.com/yuque/0/2021/png/788484/1630978816639-271819d6-5091-4ecd-8a3e-090717607d97.png#crop=0&crop=0&crop=1&crop=1&id=g0mS1&originHeight=444&originWidth=356&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
- 第一阶段: CanCommit
- 事务询问。
协调者向各参与者发送CanCommit的请求,询问是否可以执行事务提交操作,并开始等待各参与者的响应。 - 参与者向协调者反馈询问的响应。
参与者收到CanCommit请求后,正常情况下,如果自身认为可以顺利执行事务,那么会反馈Yes响应,并进入预备状态,否则反馈No。
- 第二阶段: PreCommit
(1)执行事务预提交
如果协调者接收到各参与者反馈都是 Yes,那么执行事务预提交
- 发送预提交请求。
协调者向各参与者发送 preCommit 请求,并进入 prepared 阶段。 - 事务预提交。
参与者接收到 preCommit 请求后,会执行事务操作,并将Undo和Redo信息记录到事务日记中。 - 各参与者向协调者反馈事务执行的响应。
如果各参与者都成功执行了事务操作,那么反馈给协调者 Ack 响应,同时等待最终指令,提交 commit 或者终止 abort。
(2)中断事务
如果任何一个参与者向协调者反馈了No响应,或者在等待超时后,协调者无法接收到所有参与者的反馈,那么就会中断事务。
- 发送中断请求。
协调者向所有参与者发送 abort 请求。 - 中断事务。
无论是收到来自协调者的 abort 请求,还是等待超时,参与者都中断事务。
- 第三阶段: doCommit
(1)执行提交
- 发送提交请求。
假设协调者正常工作,接收到了所有参与者的ack响应,那么它将从预提交阶段进入提交状态,并向所有参与者发送doCommit请求。
- 事务提交。
参与者收到doCommit请求后,正式提交事务,并在完成事务提交后释放占用的资源。 - 反馈事务提交结果。
参与者完成事务提交后,向协调者发送ACK信息。 - 完成事务。
协调者接收到所有参与者ack信息,完成事务。
(2)中断事务
假设协调者正常工作,并且有任一参与者反馈No,或者在等待超时后无法接收所有参与者的反馈,都会中断事务。
- 发送中断请求。
协调者向所有参与者节点发送abort请求。 - 事务回滚。
参与者接收到 abort 请求后,利用 undo 日志执行事务回滚,并在完成事务回滚后释放占用的资源。 - 反馈事务回滚结果。
参与者在完成事务回滚之后,向协调者发送 ack 信息。 - 中断事务。
协调者接收到所有参与者反馈的 ack 信息后,中断事务。 | 3PC优缺点 | 描述 | | —- | —- | | 优点 | 降低了阻塞范围,在等待超时后协调者或参与者会中断事务。避免了协调者单点问题,阶段3中协调者出现问题时,参与者会继续提交事务。 | | 缺点- | 脑裂问题依然存在,即在参与者收到PreCommit请求后等待最终指令,如果此时协调者无法与参与者正常通信,会导致参与者继续提交事务,造成数据不一致。 |
2PC与3PC的主要区别:
区别 | 2PC | 3PC |
---|---|---|
阶段 | 提交事务请求 以及 执行事务提交 | 只有协调者有超时判断。3PC将2PC的提交事务请求分成CanCommit 以及PreCommit |
超时 | 只有协调者有超时判断 | 3PC上参与者和协调者都有超时的判断 |
2.3. Paxos
分布式系统中的节点通信存在两种模型: 共享内存和消息传递。Paxos算法是一种基于消息传递且具有高度容错特性的一致性算法,是目前公认的解决分布式一致性问题的最有效算法之一。基于消息传递通信模型的分布式系统,不可避免会发生进程变慢被杀死,消息延迟、丢失、重复等问题,Paxos算法就是在存在以上异常的情况下仍能保持一致性的协议。<br /> Paxos算法本质上也是个分两阶段提交的选举算法,利用了鸽巢原理,遵循“过半与最新”原则。Paxos 算法目的是让整个集群的结点对某个值的变更达成一致。Paxos 算法(强一致性算法)属于多数派——大多数的决定会成个整个集群的统一决定。任何一个点都可以提出要修改某个数据的提案,是否通过这个提案取决于这个集群中是否有超过半数的结点同意。<br /> Paxos的两个原则:
- 安全原则(保证不能做错的事)
- 只能有一个值被批准,不能出现第二个值把第一个覆盖的情况。
- 每个节点只能学习到已经被批准的值,不能学习没有被批准的值。
- 存活原则(只要有多数服务器存活并且彼此间可以通信最终都要做到的事)
- 最终会批准某个被提议的值。
一个值被批准了,其他服务器最终会学习到这个值。
Paxos的角色:
角色 | 描述 |
---|---|
Proposer -提议者(倡议者) | 提议发起者,倡议者可以提出提议(数值或操作命令等)以供投票表决,处理客户端请求,将客户端的请求发送到集群中,以便决定这个值是否可以被批准。 |
Acceptor -决策者(接受者) | 提议批准者,负责处理接收到的提议,他们的回复就是一次投票,会存储一些状态来决定是否接收一个值。接受者可以对倡议者提出的提议进行投票表决,从众多提议中选出唯一确定的一个; |
Learner -学习者 | 最终决策学习者,学习者无倡议投票权,但是可以从接受者那里获知是哪个提议最终被选中; |
Client -产生议题者 |
Paxos算法过程
第一阶段(prepare)
- 获取一个proposal number, n;
- 提议者向所有节点广播prepare(n)请求;
- 接收者(Acceptors比较善变,如果还没最终认可一个值,它就会不断认同提案号最大的那个方案)比较n和minProposal,如果n>minProposal,表示有更新的提议minProposal=n;如果此时该接受者并没有认可一个最终值,那么认可这个提案,返回OK。如果此时已经有一个accptedValue, 将返回(acceptedProposal,acceptedValue);
- 提议者接收到过半数请求后,如果发现有acceptedValue返回,表示有认可的提议,保存最高acceptedProposal编号的acceptedValue到本地
第二阶段(Accept)
- 广播accept(n,value)到所有节点;
- 接收者比较n和minProposal,如果n>=minProposal,则acceptedProposal=minProposal=n,acceptedValue=value,本地持久化后,返回;否则,拒绝并且返回minProposal。
- 提议者接收到过半数请求后,如果发现有返回值>n,表示有更新的提议,跳转1(重新发起提议);否则value达成一致。
从上述流程可知,并发情况下,可能会出现第4步或者第7步频繁重试的情况,导致性能低下,更严重者可能导致永远都无法达成一致的情况,就是所谓的“活锁”,如下图所示:
- 活锁
当某一proposer提交的proposal被拒绝时,可能是因为acceptor 承诺返回了更大编号的proposal,因此proposer提高编号继续提交。 如果2个proposer都发现自己的编号过低转而提出更高编号的proposal,会导致死循环,这种情况也称为活锁。 比如说当此时的 proposer1提案是3, proposer2提案是4, 但acceptor承诺的编号是5,那么此时proposer1,proposer2 都将提高编号假设分别为6,7,并试图与accceptor连接,假设7被接受了,那么提案5和提案6就要重新编号提交,从而不断死循环。
2.4. Raft
Raft是简化版的Paxos。Raft是一种共识算法,旨在替代Paxos。 它通过逻辑分离比Paxos更容易理解,但它也被正式证明是安全的,并提供了一些额外的功能。Raft提供了一种在计算系统集群中分布状态机的通用方法,确保集群中的每个节点都同意一系列相同的状态转换。Raft是一个通过管理一个副本日志的一致性算法。它提供了跟(multi-)Paxos一样有效的功能,但是它的架构和Paxos不一样;它比Paxos更加容易理解,并且能用于生产环境中。
组件 | 描述 |
---|---|
状态机 | 当我们说一致性的时候,实际就是在说要保证这个状态机的一致性。状态机会从log里面取出所有的命令,然后执行一遍,得到的结果就是我们对外提供的保证了一致性的数据 |
Log | 保存了所有修改记录 |
一致性模块 | 一致性模块算法就是用来保证写入的log的命令的一致性,这也是raft算法核心内容 |
所有一致性算法都会涉及到状态机,而状态机保证系统从一个一致的状态开始,以相同的顺序执行一些列指令最终会达到另一个一致的状态。
Raft协议的每个副本都会处于三种状态之一:Leader、Follower、Candidate。
角色 | 描述 |
---|---|
Leader-领导者 | 所有请求的处理者,Leader副本接受client的更新请求,本地处理后再同步至多个其他副本 |
Follower-追随者 | 请求的被动更新者,从Leader接受更新请求,然后写入本地日志文件 |
Candidate-候选者 | 如果Follower副本在一段时间内没有收到Leader副本的心跳,则判断Leader可能已经故障,此时启动选主过程,此时副本会变成Candidate状态,直到选主结束 |
- 说明
- 所有节点初始状态都是Follower角色。
- 超时时间内没有收到Leader的请求则转换为Candidate进行选举。
- Candidate收到大多数节点的选票则转换为Leader;发现Leader或者收到更高任期的请求则转换为Follower。
- Leader在收到更高任期的请求后转换为Follower。
任期
Raft把时间切割为任意长度的任期,每个任期都有一个任期号,采用连续的整数。每个Term都有一个唯一的数字编号。所有Term的数字编号是从小到大连续排列的。Raft将时间划分为连续的时间段,称为Term。 Term是指从一次Leader选举开始到下一次Leader选举的一段时间。这段时间内只能有一个Leader被选举成功,并负责管理系统或者没有Leader选出。
- 提示
Raft节点是去中心化的架构,不依赖外部的组件,而是做为一个协议簇嵌入到应用中的,即与应用自己是融合为一体的。Kafka在2.8版本中正式废弃了Zookeeper,拥抱Raft。
2.5. Zab
Zab定义
Zab(ZooKeeper Atomic Broadcast )原子消息广播协议是一致性协议,Zookeeper把其作为数据一致性的算法。ZAB是在Paxos算法基础上进行扩展而来的。Zookeeper使用单一主进程Leader用于处理客户端所有事务请求,采用ZAB协议将服务器状态以事务形式广播到所有Follower上,由于事务间可能存在着依赖关系,ZAB协议保证Leader广播的变更序列被顺序的处理,一个状态被处理那么它所依赖的状态也已经提前被处理。
核心思想
保证任意时刻只有一个节点是Leader,所有更新事务由Leader发起去更新所有副本Follower,更新时用的是两段提交协议,只要多数节点prepare成功,就通知他们commit。各个follower要按当初leader让他们prepare的顺序来执行事务。
协议状态
- Looking:系统刚启动时或者Leader崩溃后处于选举状态
- Following:Follower节点所处的状态,Follower与Leader处于数据同步状态
- Leading:Leader所处状态,当前集群中有一个Leader为主进程
状态切换
ZooKeeper启动时所有节点初始状态为Looking,这时集群会尝试选举出一个Leader节点,选举出的Leader节点切换为Leading状态;当节点发现集群中已经选举出Leader则该节点会切换到Following状态,然后和Leader节点保持同步;当Follower节点与Leader失去联系时Follower节点则会切换到Looking状态,开始新一轮选举;在ZooKeeper的整个生命周期中每个节点都会在Looking、Following、Leading状态间不断转换。
ZAB进入原子广播阶段后,这时Leader为和自己同步的每个节点Follower创建一个操作序列,一个时期一个Follower只能和一个Leader保持同步,Leader节点与Follower节点使用心跳检测来感知对方的存在;当Leader节点在超时时间内收到来自Follower的心跳检测那Follower节点会一直与该节点保持连接;若超时时间内Leader没有接收到来自过半Follower节点的心跳检测或TCP连接断开,那Leader会结束当前周期的领导,切换到Looking状态,所有Follower节点也会放弃该Leader节点切换到Looking状态,然后开始新一轮选举。
Zab协议会让ZK集群进入崩溃恢复模式的情况如下:
- 当服务框架在启动过程中。
- 当Leader服务器出现网络中断,崩溃退出或重启等异常情况。
当集群中已经不存在过半的服务器与Leader服务器保持正常通信。
当leader挂掉后,集群无法进行工作,所以需要一个高效且可靠的leader选举算法。zk的实现是FastLeaderElection算法。
Zab协议崩溃恢复要求满足如下2个要求:确保已经被leader提交的proposal必须最终被所有的follower服务器提交。
- 确保丢弃已经被leader出的但是没有被提交的proposal。
阶段
Zab协议定义了选举(election)、发现(discovery)、同步(sync)、广播(Broadcast) 四个阶段;ZAB选举(election)时当Follower存在ZXID(事务ID)时判断所有Follower节点的事务日志,只有lastZXID的节点才有资格成为Leader,这种情况下选举出来的Leader总有最新的事务日志,基于这个原因所以ZooKeeper实现的时候把 发现(discovery)与同步(sync)合并为恢复(recovery) 阶段;
阶段 | 描述 |
---|---|
Election | 在Looking状态中选举出Leader节点,Leader的lastZXID总是最新的; |
Discovery | Follower节点向准Leader推送FOllOWERINFO,该信息中包含了上一周期的epoch,接受准Leader的NEWLEADER指令,检查newEpoch有效性,准Leader要确保Follower的epoch与ZXID小于或等于自身的; |
sync | 将Follower与Leader的数据进行同步,由Leader发起同步指令,最总保持集群数据的一致性; |
Broadcast | Leader广播Proposal与Commit,Follower接受Proposal与Commit; |
Recovery | 在Election阶段选举出Leader后本阶段主要工作就是进行数据的同步,使Leader具有highestZXID,集群保持数据的一致性; |
Phase 0__: Leader election(选举阶段)
节点一开始都处于选举阶段,只要有一个节点得到超半数节点的票数,它就可以当选准leader。这一阶段的目的是就是为了选出一个准leader,然后进入下一个阶段。只有到达 Phase3准leader才会成为真正的leader。协议并没有规定详细的选举算法,后面我们会提到实现中使用的Fast Leader Election。
选举流程:
- 每个Follower都向其他节点发送选自身为Leader的Vote投票请求,等待回复;
- Follower接受到的Vote如果比自身的大(ZXID更新)时则投票,并更新自身的Vote,否则拒绝投票;
每个Follower中维护着一个投票记录表,当某个节点收到过半的投票时,结束投票并把该Follower选为Leader,投票结束;
ZAB协议中使用ZXID作为事务编号,ZXID为64位数字,低32位为一个递增的计数器,每一个客户端的一个事务请求时Leader产生新的事务后该计数器都会加1,高32位为Leader周期epoch编号,当新选举出一个Leader节点时Leader会取出本地日志中最大事务Proposal的ZXID解析出对应的epoch把该值加1作为新的epoch,将低32位从0开始生成新的ZXID;ZAB使用epoch来区分不同的Leader周期;
Phase 1__: Discovery(发现阶段)
在这个阶段,followers跟准leader进行通信,同步followers最近接收的事务提议。这个一阶段的主要目的是发现当前大多数节点接收的最新提议,并且准leader生成新的epoch,让followers接受,更新它们的acceptedEpoch。
一个follower只会连接一个leader,如果有一个节点f认为另一个followerp是leader,f在尝试连接p时会被拒绝,f被拒绝之后,就会进入Phase0。Phase 2__: Synchronization(同步阶段)
同步阶段主要是利用leader前一阶段获得的最新提议历史,同步集群中所有的副本。只有当quorum都同步完成,准leader才会成为真正的leader。follower只会接收zxid比自己的lastZxid大的提议。简单地讲,数据同步过程就是leader服务器将那些没存在follower服务器上提交过的事务请求同步给follower服务器。
获取Learner状态
在注册Learner的最后阶段,Learner服务器会发送给Leader服务器一个ACKEPOCH数据包,Leader会从这个数据包中解析出该Learner的currentEpoch和lastZxid。
数据同步初始化
在开始数据同步之前,Leader服务器会进行数据同步初始化,首先会从ZooKeeper的内存数据库中提取出事务请求对应的提议缓存队列(下面我们用“提议缓存队列”来指代该队列):proposals,同时完成对以下三个ZXID值的初始化。
peerLastZxid:该Learner服务器最后处理的ZXID。
- minCommittedLog: Leader服务器提议缓存队列 committedLog中的最小ZXID。
- maxConmmittedLog: Leader服务器提议缓存队列committedLog中的最大ZXID。
数据同步
ZooKeeper集群数据同步通常分为四类,分别是直接差异化同步(DIFF同步)、先回滚在差异化同步(TRUNC+DIFF同步)、仅回滚同步(TRUNC同步)和全量同步(SNAP 同步)。在初始化阶段,Leader服务器会优先初始化以全量同步方式来同步数据,当然,这并非最终的数据同步方式,在以下步骤中,会根据Leader和Learner服务器之间的数据差异情况来决定最终的数据同步方式。
(1)直接差异化同步(DIFF同步)- 场景
peerLastZxid介于minCommittedLog和maxCommittedLog之间。
对于这种场景,就使用差异化同步(DIFF同步)方式即可。Leader服务器会首先向这个Learner发送一个DIFF指令,用于通知Learner进入差异化数据同步阶段,Leader 服务器即将把一些Proposal同步给自己。在实际Proposal同步过程中,针对每个Proposal, Leader服务器都会通过发送两个数据包来完成,分別是PROPOSAL内容数据包和COMMIT指令数据包,这和ZooKeeper运行时Leader和Follower之间的事务请求的提交过程是一致的。
举个例子来说,假如某个时刻Leader服务器的提议缓存队列对应的ZXID依次是:
0x500000001,0x500000002,0x500000003,0x500000004.0x500000005,Learner服务器最后处理的ZX1D为0x500000003,于是Leader服务器就会依次将0x500000004和0x500000005两个提议同步给Learner服务器,同步过程中的数据包发送顺序如下表所示:
发送顺序 | 数据包类型 | 对应ZXID |
---|---|---|
1 | PROPOSAL | 0x500000004 |
2 | COMMIT | 0x500000004 |
3 | PROPOSAL | 0x500000005 |
4 | COMMIT | 0x500000005 |
(2)先回滚再差异化同步(TRUNC + DIFF 同步)
- 场景
有A、B、C三台机器,假如某一时刻B是Leader服务器,此时的Leader_Epoch为5,同时当前已经被集群中绝大部分机器都提交的ZXID包括:0x500000001和0x500000002。此时,Leader正要处理ZXID:0x50000003,并且已经将该事务写入到了Leader本地的事务日志中去,就在Leader恰好要将该Proposal发送给其他Follower机器进行投票的时候,Leader服务器挂了,Proposal没有被同步出去。此时zooKeeper集群会进行新一轮的Leader选举,假设此次选举产生的新的Loader是A,同时Leader_Epoch变更为6,之后A和C两台服务器继续对外进行服务,又提交了0x600000001和0x600000002两个事务。此时,服务器B再次启动,井开始数据同步。
简单地讲,上面这个场景就是Leader服务器在已经将事务记录到了本地事务日志中,但是没有成功发起Proposal流程的时候就挂了。在这个特殊场景中,我们看到,peerLastzxid、minCommittedLog和maxCommittedLog的值分别是0x500000003、0x500000001和0x600000002.显然,peerLastzxid介于minConlmittcdLog和maxCommittedLog之间。对于这个特殊场景,就使用先回滚再差异化同步(TRUNC+DIFF同步)的方式。当Leader服务器发现某个Learner包含了一条自己没有的事务记录,那么就需要让该Learner进行事务回滚,回滚到Leader服务器上存在的,同时也是最接近于peerLastzxid的ZXID。在上面这个例子中,Leader会需要Learner回滚到ZXID为0x500000002的事务记录。
发送顺序 | 数据包类型 | 对应ZXID |
---|---|---|
1 | TRUNC | 0x500000002 |
2 | PROPOSAL | 0x600000001 |
3 | COMMIT | 0x600000001 |
4 | PROPOSAL | 0x600000002 |
5 | COMMIT | 0x600000002 |
3)仅回滚同步(RUNC同步)
- 场景
peerLastzxid大于maxCommittedLog。这种场景其实就是上述先回滚再差异化同步的简化模式,Leader会要求Learner回滚到ZXID值为maxCommittedLog对应的事务操作,这里不再对该过程详细展开讲解。
4)全量同步(SNAP同步)
- 场景1
peerLastZxid小于minCommittedLog。
- 场景2
Leader服务器上没有提议缓存队列,peerLastZxid不等干lastProcessedZxid(Leader服务器数据恢复后得到的最大ZXID)。上述这两个场景非常类似,在这两种场景下,Leader服务器都无法直接使用提议缓存队列和Learner进行数据同步,因此只能进行全量同步(SNAP同步)。
所谓全量同步就是Leader服务器将本机上的全量内存数据都同步给Learner。Leader服务器首先向Learner发送一个SNAP指令,通知Learner即将进行全量数据同步。随后,Leader会从内存数据库中获取到全量的数据节点和会话超时时间记录器,将它们序列化后传输给Learner。Learner服务器接收到该全量数据后,会对其反序列化后载入到内存数据库中。<br /> 以上就是ZooKeeper集群间机器的数据同步流程了。整个数据同步流程的代码实现主要在LearnerHandler和Learner两个类中。当所有的Learner服务器都成功同步之后,Leader会将这些服务器加入到可用服务器列表中。
Phase 3__: Broadcast(广播阶段)
到了这个阶段,Zookeeper集群才能正式对外提供事务服务,并且leader可以进行消息广播。同时如果有新的节点加入,还需要对新节点进行同步。值得注意的是,ZAB提交事务并不像2PC一样需要全部follower都ACK,只需要得到quorum(法定人数:超过半数的节点)的ACK就可以了。
客户端提交事务请求时Leader节点为每一个请求生成一个事务Proposal,将其发送给集群中所有的Follower节点,收到过半Follower的反馈后开始对事务进行提交,ZAB协议使用了原子广播协议;在ZAB协议中只需要得到过半的Follower节点反馈Ack就可以对事务进行提交,这也导致了Leader节点崩溃后可能会出现数据不一致的情况,ZAB使用了崩溃恢复来处理数据不一致问题。广播消息时Leader节点为每个事务Proposal分配一个全局递增的ZXID(事务ID),ZXID顺序放入到FIFO队列中发送。Follower节点收到事务Proposal后会将该事务以事务日志方式写入到本地磁盘中,成功后反馈Ack消息给Leader节点,Leader在接收到过半Follower节点的Ack反馈后就会进行事务的提交,以此同时向所有的Follower节点广播Commit消息,Follower节点收到Commit后开始对事务进行提交;Recovery Phase (恢复阶段)
这一阶段follower发送它们的lastZixd给leader,leader根据lastZixd决定如何同步数据。这里的实现跟前面Phase2有所不同:Follower收到TRUNC指令会中止L.lastCommittedZxid之后的提议,收到DIFF指令会接收新的提议。
- history:lastCommittedZxid:最近被提交的提议的zxid。
- history:oldThreshold:被认为已经太旧的已提交提议的zxid。
Zab与Paxos的联系和区别
联系:
- 都存在一个类似于Leader进程的角色,由其负责协调多个Follower进程的运行。 - Leader进程都会等待超过半数的Follower做出正确的反馈后,才会将一个提议进行提交。 - 在ZAB协议中,每个Proposal中都包含了一个epoch值,用来代表当前的Leader周期,在Paxos算法中,同样存在这样的一个标识,名字为Ballot。 |
---|
区别:
- Paxos算法中,新选举产生的主进程会进行两个阶段的工作,第一阶段称为读阶段,新的主进程和其他进程通信来收集主进程提出的提议,并将它们提交。第二阶段称为写阶段,当前主进程开始提出自己的提议。 - ZAB协议在Paxos基础上添加了同步阶段,此时,新的Leader会确保存在过半的Follower已经提交了之前的Leader周期中的所有事务Proposal。 - ZAB协议主要用于构建一个高可用的分布式数据主备系统,而Paxos算法则用于构建一个分布式的一致性状态机系统。 |
---|
3. 数据模型
ZooKeeper提供的命名空间与标准的文件系统非常相似。一个名称是由通过斜线分隔开的路径名序列所组成的。ZooKeeper中的每一个节点是都通过路径来识别。下图是Zookeeper中节点的数据模型,这种树形结构的命名空间操作方便且易于理解。<br />![](https://cdn.nlark.com/yuque/0/2021/png/788484/1630978825033-5873341d-4d3f-4458-86ec-aaa5e7dde45d.png#crop=0&crop=0&crop=1&crop=1&id=LeK0D&originHeight=234&originWidth=414&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)<br />ZooKeeper层次命名空间<br /> ZooKeeper的节点是通过像树一样的结构来进行维护的,并且每一个节点通过路径来标示以及访问。除此之外,每一个节点还拥有自身的一些信息,包括:数据、数据长度、创建时间、修改时间等等。ZooKeeper的节点既可以被看做是一个文件,又可以被看做是一个目录,它同时具有二者的特点,zk节点一般称znode。<br /> 具体地说,znode维护着数据、ACL(access control list,访问控制列表)、时间戳等交换版本号等数据结构,它通过对这些数据的管理来让缓存生效并且令协调更新。每当znode中的数据更新后它所维护的版本号将增加,这非常类似于数据库中计数器时间戳的操作方式,类似数据库乐观锁的原理。
- znode与标准文件系统的区别
- 一个znode既可以是文件也可以是目录。
- 每个znode存储的数据有限,不超过1M(zookeeper有内置的检查,可以修改配置项“jute.maxbuffer”来调整)。
- Zookeeper中的文件系统路径是绝对路径,必须以“/”开头,而且路径必须是标准的,即不能包含像unix那样可以表示当前路径或者当前路径上一级目录的符号“.”或“..”。
- 有四种类型的znode | ZNode | 节点类型 | 描述 | | —- | —- | —- | | PERSISTENT | 持久化节点 | 客户端与zookeeper断开连接后,该节点依旧存在 | | PERSISTENT_SEQUENTIAL | 持久化序列节点 | Zookeeper对该节点名称进行顺序编号(10位编号,第一个数编号0000000000,后续依次加一),客户端与zookeeper断开连接后,该节点依旧存在 | | EPHEMERAL | 临时节点 | 客户端与zookeeper断开连接后,该节点被删除 | | EPHEMERAL_SEQUENTIAL | 临时序列节点 | Zookeeper对该节点名称进行顺序编号(10位编号,第一个数编号0000000000,后续依次加一),客户端与zookeeper断开连接后,该节点被删除 |
4. 角色
ZooKeeper角色 | 描述 | |
---|---|---|
领导者(leader) | 负责进行投票的发起和决议,更新系统状态 | |
学习者(learner) | 跟随者(follower) | follower用于接受客户端请求并想客户端返回结果,在选主过程中参与投票 |
观察者(observer) | Observer可以接受客户端连接,将写请求转发给leader,但observer不参加投票过程,只同步leader的状态,observer的目的是为了扩展系统,提高读取速度 | |
客户端(client) | 请求发起方 |
observer场景应用:
- 使用Observer模式的一个主要的理由就是对读请求进行扩展。通过增加更多的Observer,可以接收更多的请求的流量,却不会牺牲写操作的吞吐量。
- 使用Observer的另一个原因是跨数据中心部署。依据Observer的特点。我们能够使用Observer做跨数据中心部署。假设把Leader和Follower分散到多个数据中心的话,由于数据中心之间的网络的延迟。势必会导致集群性能的大幅度下降。使用Observer的话,将Observer跨机房部署。而Leader和Follower部署在单独的数据中心,这样更新操作会在同一个数据中心来处理,并将数据发送的其它数据中心(包括Observer的),然后Client就能够在其它数据中心查询数据了。可是使用了Observer并不是就能全然消除数据中心之间的延迟,由于Observer还得接收Leader的同步结果合Observer有更新请求也必须转发到Leader,所以在网络延迟非常大的情况下还是会有影响的,它的优势就为了本地读请求的高速响应。
5. 选主(FastLeaderElection算法)
名词解释:
- epoch:可以理解为当前集群所处的年代或者周期,每个leader就像皇帝,都有自己的年号,所以每次改朝换代,leader变更之后,都会在前一个年代的基础上加1。这样就算旧的leader崩溃恢复之后,也没有人听他的了,因为follower只听从当前年代的leader的命令。
- Zxid:Zxid是一个64位的数字,其中低32位是一个简单的单调递增的计数器,针对客户端每一个事务请求,计数器加1;而高32位则代表Leader周期epoch的编号,每个当选产生一个新的Leader服务器,就会从这个Leader服务器上取出其本地日志中最大事务的ZXID,并从中读取epoch值,然后加1,以此作为新的epoch,并将低32位从0开始计数。
FLE会选举拥有最新提议历史(lastZixd最大)的节点作为leader,这样就省去了发现最新提议的步骤。这是基于拥有最新提议的节点也有最新提交记录的前提。
成为leader的条件:
- 选epoch最大的。
- epoch相等,选zxid最大的。
- epoch和zxid都相等,选择serverid最大的(就是我们配置zoo.cfg中的myid)。
节点在选举开始都默认投票给自己,当接收其他节点的选票时,会根据上面的条件更改自己的选票并重新发送选票给其他节点,当有一个节点的得票超过半数,该节点会设置自己的状态为leading,其他节点会设置自己的状态为following。
- 每个Follower都向其他节点发送选自身为Leader的Vote投票请求,等待回复;
- Follower接受到的Vote如果比自身的大(ZXID更新)时则投票,并更新自身的Vote,否则拒绝投票;
- 每个Follower中维护着一个投票记录表,当某个节点收到过半的投票时,结束投票并把该Follower选为Leader,投票结束;
ZAB协议中使用ZXID作为事务编号,ZXID为64位数字,低32位为一个递增的计数器,每一个客户端的一个事务请求时Leader产生新的事务后该计数器都会加1,高32位为Leader周期epoch编号,当新选举出一个Leader节点时Leader会取出本地日志中最大事务Proposal的ZXID解析出对应的epoch把该值加1作为新的epoch,将低32位从0开始生成新的ZXID;ZAB使用epoch来区分不同的Leader周期;
6. 工作过程
ZK集群启动后,client连接到其中的一个节点,这个节点可以是leader,也可以是follower。连通后,node分配一个id给client,发送ack信息给client。如果客户端没有收到ack,连接到另一个节点。client周期性发送心跳信息给节点,保证连接不会丢失。
如果client读取数据,发送请求给node,node读取自己数据,返回节点数据给client。如果client存储数据,将路径和数据发送给server,server转发给leader。leader再补发请求给所有follower。只有大多数(超过半数)节点成功响应,则写操作成功。
7. 原生API
7.1. 基本操作
ZooKeeper九种基本操作:
操作 | 描述 |
---|---|
create | 创建一个znode(必须要有父节点,创建时可以设置数据) |
delete | 删除一个znode(该znode不能有任何子节点) |
exists | 测试一个znode是否存在并且查询它的元数据 |
getACL,setACL | 获取/设置一个znode的ACL |
getChildren | 获取一个znode的子节点列表 |
getData,setData | 获取/设置一个znode所保存的数据 |
sync | 将客户端的znode试图与ZooKeeper同步 |
7.2. 监听器(Watcher)
在读操作exists、getChildren和getData上可以设置观察,这些观察可以被写操作create、delete和setData触发。注:ACL的相关操作不参与任何观察。下表是设置观察的操作及其对应的触发器:
设置观察的操作 | 观察触发器 | |||||
---|---|---|---|---|---|---|
create | delete | setData | ||||
znode | 子节点 | znode | 子节点 | znode | 子节点 | |
exists | NodeCreated | NodeDeleted | NodeDataChanged | |||
getData | Error | NodeDeleted | NodeDataChanged | |||
getChildren | Error | NodeChildrenChanged | NodeDeleted | NodeChildrenChanged |
同一个事件类型在不同的通知状态中代表的含义有所不同,下表列举了常见的通知状态和事件类型:
KeeperState | EventType | 触发条件 | 说明 |
---|---|---|---|
SyncConnected(0) | None(-1) | 客户端与服务器端成功建立连接 | 客户端和服务器处于连接状态 |
NodeCreated(1) | Watcher监听的对应数据节点被创建 | ||
NodeDeleted(2) | Watcher监听的对应数据节点被删除 | ||
NodeDataChanged(3) | 监听的对应数据节点的数据内容发生变更 | ||
NodeChildChanged(4) | 监听的对应数据节点的子节点列表发生变更 | ||
Disconnected(0) | Node(-1) | 客户端与服务器端断开连接 | 客户端和服务器处于断开状态 |
Expired(-112) | Node(-1) | 会话超时 | 客户端会话失效,通常同时也会收到SessionExpiredException异常 |
AuthFailed(4) | Node(-1) | 通常有两种情况: 1.使用错误的shema进行权限检查; 2.SASL权限检查失败; |
通常同时也会收到AuthFailedException异常 |
7.3. 回调(AsyncCallback)
Watcher用于监听节点的,比如getData对数据节点a设置了watcher,那么当a的数据内容发生改变时,客户端会收到NodeDataChanged通知,然后进行watcher的回调。AsyncCallback是在以异步方式使用ZooKeeper APi时,用户获取api的处理结果的,这具有本质的不同。Zookeeper 客户端中Watcher和AsyncCallback都是异步回调的方式,但它们回调的时机是不一样的,前者是由服务器发送事件触发客户端回调,后者是在执行了请求后得到响应后客户端主动触发的。
7.4. 访问控制列表(ACL)
ACL(Access Control List),Zookeeper作为一个分布式协调框架,其内部存储的都是一些关于分布式系统运行时状态的元数据,尤其是涉及到一些分布式锁,Master选举和协调等应用场景,我们需要有效地保障Zookeeper中的数据安全。Zookeeper的ACL,可以从三个维度来理解:权限模式(scheme),授权对象(user),权限(permission),通常表示为scheme:id:permissions。<br /> 权限模式:zookeeper提供了如下几种机制。
权限模式 | 描述 |
---|---|
IP | 通过ip地址粒度进行权限控制模式,例如配置了192.168.110.135即表示权限控制都是针对这个ip地址的,同时也支持按网段分配,比如:192.168.110.* |
Digest | 最常用的权限控制模式,也更符合我们对权限控制的认识,其类似于”username:password”形式的权限标识进行权限配置。ZK会对形成的权限标识先后进行两次编码处理,分别是SHA-1加密算法和Base64编码。 |
World | 默认模式,World是一直最开放的权限控制模式。这种模式可以看做为特殊的Digest,他仅仅是一个标识而已,任何人可操作。 |
Super | 超级用户模式,在超级用户模式下可以对ZK任意进行操作。 |
auth | 它不需要id, 只要是通过authentication的user都有权限(zookeeper支持通过kerberos来进行authencation, 也支持username/password形式的authentication) |
权限对象:指的是权限赋予的用户或者是一个指定的实体,例如ip地址或机器等。在不同的模式下,授权对象是不同的。这种模式和权限对象一一对应,详见上述权限模式描述。<br /> 权限:znode被创建时都会带有一个ACL列表,用于决定谁可以对它执行何种操作crwda。
ACL权限 | 运行的操作 |
---|---|
CREATE(c) | create(子节点):创建权限,可以在当前znode下创建子节点 |
READ(r) | getData/getChildren:读权限,可以获取当前znode的数据,或者当前znode所有子节点列表 |
DELETE(d) | delete(子节点):删除权限可以删除(delete命令,不能使用rmr)当前的znode及其子节点 |
WRITE(w) | setData:写权限,可以向当前znode写数据 |
ADMIN(a) | setACL:管理权限,可以设置当前znode的权限 |
注:exists不接受ACL。<br /> ACL毕竟仅仅是访问控制,并非完善的权限管理,通过这种方式做多集群隔离,还有很多局限性:
- ACL并无递归机制,任何一个znode创建后,都需要单独设置ACL,无法继承父节点的ACL设置。
- 除了ip这种scheme,digest和auth的使用对用户都不是透明的,这也给使用带来了很大的成本,很多依赖zookeeper的开源框架也没有加入对ACL的支持,例如hbase,storm。
7.5. 配额(qato)
可以使用setquot来设置一个ZK节点的配额。它有一个设置配额时的选项“-n”和“-b”。ZK的配额被存储ZK本身的“/zookeeper/quota”中。为了禁止其它人改变它的设置,可以使用ACL来确保只有管理员可以读和写这个文件。 ```bash-n:限制子节点个数
-b:限制节点数据长度
val:额度
path:节点
setquota -n|-b val path
显示节点配额信息
bytes表示数据长度,-1表示没有限制。这个值是当前节点及其子节点的数据长度之和。
listquota PATH
删除配额设置
delquota [-n|-b] PATH
**注意:如果我们指定了某个节点只允许创建3个子节点,但是我们创建了四个,也不会报错,只是在日志上记录一个警告而已。**
<a name="YRnQ4"></a>
# 8. 客户端
<a name="icYBS"></a>
## 8.1. zkCli
<a name="HGPen"></a>
## 8.2. ZkClient
<a name="Lx1a0"></a>
## 8.3. Curator(推荐)
<a name="YXlHN"></a>
# 9. 应用场景
- 数据发布 / 订阅
- 负载均衡(Kafka)
- 命名服务(Solr)
- 分布式协调 / 通知
- 集群管理(HBase、Kafka)
- HA(Hadoop、HBase、Strom)
- 注册中心(Dubbo)
- Master选举(Kafka)
- 分布式锁(报表下载)
- 分布式队列(报告下载)
<a name="Yx6jG"></a>
# 10. 监控
1. **ZooKeeper Explorer**
1. **ZooInspector**
1. **Exhibitor**
Exhibitor是一个ZooKeeper的管理工具,Exhibitor提供了ZooKeeper的所有管理需求以及许多其他功能,具体如下:
- **监控**:Exhibitor监控ZooKeeper服务器。 如果由于某些原因ZooKeeper服务器进程崩溃或未运行,Exhibitor将重写配置文件并重新启动服务器进程。
- **日志清理**:Exhibitor可以定期清理ZooKeeper日志。 但是,在3.4.x之后的版本中,ZooKeeper提供了一个自动清除日志文件的选项。
- **备份/恢复**:Exhibitor可以用来备份ZooKeeper事务日志文件。 它还允许对这些日志进行索引,通过这些日志可以搜索任何指定的事务并用于将给定的znode恢复到ZooKeeper实例。
- **群集范围配置**:Exhibitor通过展示单个系统视图,可以将配置更改应用到整个ZooKeeper系统。
- **滚动配置更新**:Exhibitor允许配置的热更新;这使得即使在ZooKeeper ensemble运行时也可以进行配置更新,而无需停机。
- **REST API**:Exhibitor公开了一个REST API,允许开发人员编写程序来执行ZooKeeper管理任务。
- **可视化**:Exhibitor展示了ZooKeeper树的图形可视化。
- **Exhibitor整合**:Exhibitor和Curator可以配置一起工作。 它允许Curator实例更新对ZooKeeper ensemble所做的任何更改。
> - **Exhibitor安装使用**
> 1. github下载源代码
>
[https://github.com/Netflix/exhibitor](https://github.com/Netflix/exhibitor),下载zip包。
> 2. 编译
>
进入目录:exhibitor-master\exhibitor-standalone\src\main\resources\buildscripts\standalone\maven
> 执行命令:mvn clean package
> 把编译好的target下的jar上传到服务器,执行:java -jar exhibitor-1.5.6.jar -c file
> 3. 使用
>
打开浏览器:http://bigdata-aibs-node1:8080/exhibitor/v1/ui/index.html
> 可以重启集群中的Zookeeper、查看节点信息、配置集群中的Zookeeper、查看log等操作。
4. **JMX**
4. **JConsole**
4. **zkui**:[https://www.linuxhub.org/?p=4311](https://www.linuxhub.org/?p=4311)
7. **zkdash**:[https://blog.csdn.net/motrsky/article/details/53896768](https://blog.csdn.net/motrsky/article/details/53896768)
8. **TaoKeeper**
- 机器CPU/MEM/LOAD的监控。
- ZK日志目录所在磁盘空间监控。
- 单机连接数的峰值报警。
- 单机Watcher数的峰值报警。
- 节点自检。
- ZK运行时信息展示。
**ZK监控常用方法如下:**
1. ZK提供一些简单但是功能强大的4字命令,通过对这些4字命令的返回内容进行解析,可以获取不少关于ZK运行时的信息。
1. 用jmx也能够获取一些运行时信息:[http://zookeeper.apache.org/doc/r3.4.3/zookeeperJMX.html](http://zookeeper.apache.org/doc/r3.4.3/zookeeperJMX.html)。
1. 淘宝网已经实现的ZooKeeper监控([TaoKeeper](http://rdc.taobao.com/team/jm/archives/1450))。
| **产品** | **简介** | **节点的可视化操作** | **快照管理** | **节点修改的Diff和Review功能** | **节点操作邮件通知** | **CAS和LDAP登录** | **权限管理** | **级联删除** | **系统状态监控** |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| Shepher | zk管理 | **√** | **√** | **√** | **√** | **√** | **√** | **×** | **×** |
| TaoKeeper | 监控&报表 | **×** | **×** | **×** | **×** | **×** | **×** | **×** | **√** |
| Zkdash | zk管理 | **√** | **√** | **×** | **×** | **×** | **×** | **√** | **×** |
| Disconf | zk管理 | **√** | **√** | **×** | **√** | **×** | **√** | **√** | **√** |
| XDiamond | 配置中心 | **√** | **×** | **×** | **×** | **√** | **√** | **×** | **√** |
<a name="EfO1F"></a>
# 11. 日常运维
<a name="JLT6l"></a>
## 11.1. 清理数据目录
dataDir目录指定了ZK的数据目录,用于存储ZK的快照文件(snapshot)。另外,默认情况下,ZK的事务日志也会存储在这个目录中。在完成若干次事务日志之后(在ZK中,凡是对数据有更新的操作,比如创建节点,删除节点或是对节点数据内容进行更新等,都会记录事务日志),ZK会触发一次快照(snapshot),将当前server上所有节点的状态以快照文件的形式dump到磁盘上去,即snapshot文件。这里的若干次事务日志是可以配置的,默认是100000,具体参看下文中关于配置参数“snapCount”的介绍。<br />考虑到ZK运行环境的差异性,以及对于这些历史文件,不同的管理员可能有自己的用途(例如作为数据备份),因此默认ZK是不会自动清理快照和事务日志,需要交给管理员自己来处理。这里是我们用的清理方法,保留最新的66个文件,将它写到crontab中,每天凌晨2点触发一次:
```bash
#!/bin/bash
# snapshot file dir
dataDir=/home/yinshi.nc/test/zk_data/version-2
# tran log dir
dataLogDir=/home/yinshi.nc/test/zk_log/version-2
# zk log dir
logDir=/home/yinshi.nc/test/logs
# Leave 66 files
count=66
count=$[$count+1]
ls -t $dataLogDir/log.* | tail -n +$count | xargs rm -f
ls -t $dataDir/snapshot.* | tail -n +$count | xargs rm -f
ls -t $logDir/zookeeper.log.* | tail -n +$count | xargs rm -f
# find /home/yinshi.nc/taokeeper/zk_data/version-2 -name "snap*" -mtime +1 | xargs rm -f
# find /home/yinshi.nc/taokeeper/zk_logs/version-2 -name "log*" -mtime +1 | xargs rm -f
# find /home/yinshi.nc/taokeeper/logs/ -name "zookeeper.log.*" -mtime +1 | xargs rm –f
其实,仅管ZK没有自动帮我们清理历史文件,但是它的还是提供了一个叫PurgeTxnLog的工具类,实现了一种简单的历史文件清理策略,可以在这里看一下他的使用方法:
http://zookeeper.apache.org/doc/r3.4.3/api/index.html
简单使用如下:
java -cp zookeeper.jar:lib/slf4j-api-1.6.1.jar:lib/slf4j-log4j12-1.6.1.jar:lib/log4j-1.2.15.jar:conf org.apache.zookeeper.server.PurgeTxnLog<dataDir><snapDir> -n <count>
最后一个参数表示希望保留的历史文件个数,注意,count必须是大于3的整数。可以把这句命令写成一个定时任务,以便每天定时执行清理。
注意: 从3.4.0版本开始, zookeeper提供了自己清理历史文件的功能了,相关的配置参数是“autopurge.snapRetainCount”和“autopurge.purgeInterval”。
11.2. ZK程序日志
ZK默认是没有向ROLLINGFILE文件输出程序运行时日志的,需要我们自己在conf/log4j.properties中配置日志路径。另外,没有特殊要求的话,日志级别设置为INFO或以上,日志级别设置为DEBUG的话,性能影响很大!
11.3. ZK自检恢复
ZK运行过程中,如果出现一些无法处理的异常,会直接退出进程,也就是所谓的快速失败(fail fast)模式。在上文中有提到,“过半存活即可用”的特性使得集群中少数机器down掉后,整个集群还是可以对外正常提供服务的。另外,这些down掉的机器重启之后,能够自动加入到集群中,并且自动和集群中其它机器进行状态同步(主要就是从Leader那里同步最新的数据),从而达到自我恢复的目的。
因此,我们很容易就可以想到,是否可以借助一些工具来自动完成机器的状态检测与重启工作。回答是肯定的,这里推荐两个工具: Daemontools(http://cr.yp.to/daemontools.html) 和 SMF(http://en.wikipedia.org/wiki/Service_Management_Facility),能够帮助你监控ZK进程,一旦进程退出后,能够自动重启进程,从而使down掉的机器能够重新加入到集群中去。
11.4. Unix系统上的安装配置
Zookeeper在Unix操作系统(如:AIX)上需要修改zkServer.sh、zkCli.sh相关内容才能正常运行。
if [ “$JAVA_HOME” = “” ] ; then JAVA_HOME=/home/hadoop/jdk1.7.0_79 fi
BASE_DIR=.. ZOOJAR=$BASE_DIR/zookeeper-3.4.6.jar ZOOCFG=$BASE_DIR/conf/zoo.cfg
. “$BASE_DIR/conf/java.env”
JVMFLAGS=”-Xms256m -Xmx512m -XX:+UseParallelGC $JVMFLAGS”
PIDFILE=${BASE_DIR}/.pidfile JAVA=$JAVA_HOME/bin/java
CLASSPATH=. CLASSPATH=$CLASSPATH:$JAVA_HOME/lib CLASSPATH=$CLASSPATH:$ZOOJAR for i in $BASE_DIR/lib/*.jar do CLASSPATH=”$i:$CLASSPATH” done
case $1 in
start)
nohup $JAVA -Dzookeeper.log.dir=$BASE_DIR \
-Dzookeeper.root.logger=INFO,CONSOLE \
-classpath “$CLASSPATH” $JVMFLAGS \
org.apache.zookeeper.server.quorum.QuorumPeerMain \
$ZOOCFG &
echo $! > $PIDFILE
;;
stop)
pid=cat $PIDFILE
kill -9 ${pid}
rm $PIDFILE
;;
status)
clientPortAddress=`grep "^[[:space:]]*clientPortAddress[^[:alpha:]]" "$ZOOCFG" | sed -e 's/.*=//'`
if ! [ $clientPortAddress ]
then
clientPortAddress="localhost"
fi
clientPort=`grep "^[[:space:]]*clientPort[^[:alpha:]]" "$ZOOCFG" | sed -e 's/.*=//'`
STAT=`"$JAVA" "-Dzookeeper.log.dir=${BASE_DIR}" "-Dzookeeper.root.logger=INFO,CONSOLE" \
-cp "$CLASSPATH" $JVMFLAGS org.apache.zookeeper.client.FourLetterWordMain \
$clientPortAddress $clientPort srvr 2> /dev/null \
| grep Mode`
if [ "x$STAT" = "x" ]
then
echo "Error contacting service. It is probably not running."
exit 1
else
echo $STAT
exit 0
fi
;; *) echo “Usage: $0 {start|start-foreground|stop|restart|status|upgrade|print-cmd}” esac
- **zkCli.sh**
```bash
if [ "$JAVA_HOME" = "" ] ; then
JAVA_HOME=/home/hadoop/jdk1.7.0_79
fi
BASE_DIR=..
ZOOJAR=$BASE_DIR/zookeeper-3.4.6.jar
ZOOCFG=$BASE_DIR/conf/zoo.cfg
ZOOSERVER=127.0.0.1:2181
PIDFILE=${BASE_DIR}/.pidfile
JAVA=$JAVA_HOME/bin/java
CLASSPATH=.
CLASSPATH=$CLASSPATH:$JAVA_HOME/lib
CLASSPATH=$CLASSPATH:$ZOOJAR
for i in $BASE_DIR/lib/*.jar
do
CLASSPATH="$i:$CLASSPATH"
done
$JAVA -Dzookeeper.log.dir=$BASE_DIR \
-Dzookeeper.root.logger=INFO,CONSOLE \
-classpath "$CLASSPATH" \
org.apache.zookeeper.ZooKeeperMain \
-server $ZOOSERVER
12. Q&A
- ZooKeeper是主从架构吗?
Zookeeper集群是一个基于主从架构的高可用集群,角色分为:Leader、Follower(或者是Master、Slave)、Observer。
- ZooKeeper集群节点数量为什么要是奇数个?
- 防止由脑裂造成的集群不可用。
- 在容错能力相同的情况下,奇数台更节省资源。
- ZooKeeper是强一致性的吗?
ZooKeeper通过Zab(2PC + paxos)协议实现了最终一致性,并未实现强一致性(效率问题、网络问题无法避免,实现难度大),客户端可调用sync方法获取最终结果。
附录
附录A. zk常用命令
命令基本语法 | 功能描述 |
---|---|
help | 显示所有操作命令 |
ls path [watch] | 使用 ls 命令来查看当前znode中所包含的内容 |
ls2 path [watch] | 查看当前节点数据并能看到更新次数等数据 |
create | 普通创建 -s 含有序列 -e 临时(重启或者超时消失) |
get path [watch] | 获得节点的值 |
set | 设置节点的具体值 |
stat | 查看节点状态 |
delete | 删除节点 |
rmr | 递归删除节点 |
附录B. Paxos示例
参考
慕课网:彻底讲清楚ZooKeeper分布式锁的实现原理
https://www.imooc.com/article/284956
博客园:Zookeeper客户端的使用(Zookeeper原生API如何进行调用、ZKClient、Curator)和Zookeeper会话
https://www.cnblogs.com/leeSmall/p/9576437.html
CSDN:Zookeeper 原生api zkClient Curator操作
https://blog.csdn.net/eussi/article/details/82909879