Zookeeper
一、什么是 Zookeeper
在Zookeeper 官网上是这样介绍它的:ZooKeeper is a centralized service for maintaining configuration information, naming, providing distributed synchronization, and providing group services。
大概的意思就是说 Zookeeper 主要是一个分布式服务协调框架,实现同步服务,配置维护和命名维护等分布式应用,是一个高性能的分布式数据一致性解决方案。
二、Zookeeper 能做什么
ZooKeeper主要服务于分布式系统,可以用ZooKeeper来做:统一配置管理、统一命名服务、分布式锁、集群管理。为什么能做这些后面再说。
三、Zookeeper 和 CAP
1. CAP
CAP 有三部分:Consistency、Availability、Partition tolerance 这三个,最多只能同时满足其中2个(网络故障下)。
选项 | 描述 |
---|---|
Consistency(一致性) | 指数据在多个副本之间能够保持一致的特性(严格的一致性) |
Availability(可用性) | 指系统提供的服务必须一直处于可用的状态,每次请求都能获取到非错的响应(不保证获取的数据为最新数据) |
Partition tolerance(分区容错性) | 分布式系统在遇到任何网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务,除非整个网络环境都发生了故障 |
什么是分区?
在分布式系统中,不同的节点分布在不同的子网络中,由于一些特殊的原因,这些子节点之间出现了网络不通的状态,但他们的内部子网络是正常的。从而导致了整个系统的环境被切分成了若干个孤立的区域,这就是分区。
CAP 定理表明,在存在网络分区的情况下,一致性和可用性必须二选一。当网络发生分区(不同节点之间的网络发生故障或者延迟较大)时,要么失去一致性(允许不同分区的数据写入),要么失去可用性(识别到网络分区时停止服务)。 而在没有发生网络故障时,即分布式系统正常运行时,一致性和可用性是可以同时被满足的。
-
Consistency(一致性)
这里的一致性是强一致性,强一致性的意思就是例如节点A更新了数据,节点B能同时更新,这样客户端在每次读取获得数据都是最近更新的。但是在定理中是忽略掉我们平日里的网络延迟的。现实情况网络延迟在现在还是无法避免的,所以我们只能实现最终一致性,但是目标还是贴近强一致性,也就是尽力降低延时的时间。
-
Availability(可用性)
可用性指的是非故障的节点需要在合理的时间返回合理的响应。合理的响应的意思也就是不能搞个报错,不能是超时失败。举个例子比如说节点A更新了数据,同时要发布到节点B上,但是中间传输的电缆被挖掘机挖断了,此时用户去访问节点B,此时节点B应该返回老的数据,而不应该报错。这就是可用性。让用户感觉系统还是能用的。
-
Partition tolerance
分区容错性,指的是当网络分区了,系统还能正常的运行和响应。比如节点A和节点B无法通信,你要考虑这个时候系统如何应该解决。虽然网络分区的概率低而且时间短但是这种情况是会发生的。所以理论上是牺牲C或者A,P是一定要达到的。
2. Zookeeper 和 CAP
ZooKeeper是个CP(一致性+分区容错性)的,即任何时刻对ZooKeeper的访问请求能得到一致的数据结果,同时系统对网络分割具备容错性;但是它不能保证每次服务请求的可用性。也就是在极端环境下,ZooKeeper可能会丢弃一些请求,消费者程序需要重新请求才能获得结果。
ZooKeeper是分布式协调服务,它的职责是保证数据在其管辖下的所有服务之间保持同步、一致;所以就不难理解为什么ZooKeeper被设计成CP而不是AP特性的了。而且, 作为ZooKeeper的核心实现算法Zab(后述解释),就是解决了分布式系统下数据如何在多个服务之间保持同步问题的。
四、Zookeeper 节点特性及节点属性分析
Zookeeper提供基于类似于文件系统的目录节点树方式的数据存储,但是Zookeeper并不是用来专门存储数据的,它的作用主要是用来维护和监控你存储的数据的状态变化。通过监控这些数据状态的变化,从而可以达到基于数据的集群管理。
1. 数据模型
与Linux文件系统不同的是,Linux文件系统有目录和文件的区别,而Zookeeper的数据节点称为ZNode,ZNode是Zookeeper中数据的最小单元,每个ZNode都可以保存数据,同时还可以挂载子节点,因此构成了一个层次化的命名空间,称为树。
Znode 主要有以下几种类型:
- PERSISTENT : 持久化节点,一旦创建这个Znode,存储的数据就不会主动消失,除非客户端主动delete;
- EPHEMERAL : 临时节点,当客户端Client 和服务端Server断开连接后,这类型节点就会被删除(一个Session生命周期内);
- PERSISTENT_SEQUENTIAL : 顺序自动编号的ZNode节点,这种znoe节点会根据当前已近存在的ZNode节点编号自动加 1,而且不会随Session断开而消失;
- EPEMERAL_SEQUENTIAL : 临时自动编号节点,ZNode节点编号会自动增加,但是会随Session消失而消失。
2. Znode 中都有啥
- data: Znode 存储的数据信息
- ACL: 记录Znode 的访问权限,即那些人或那些ip能够访问本节点
- stat: 包含Znode 的各种元数据,比如事务ID、版本号、时间戳、大小等等
- child: 当前节点的子节点引用,类似二叉树的左右孩子
注:Zookeeper 主要是为读多写少的场景设计,Znode 并不是用来存储大规模业务数据的,而是用来春初少量的状态和配置信息的,每个节点的数据一般最大不超过 1MB。
五、Zookeeper 的基本操作和事件通知
常见的有:
- create 创建节点
- delete 删除节点
- exists 判断节点是否存在
- getData 获取一个节点的数据
- getChildren 获取节点一下的所有子节点
以上的 exists、getData、getChildren 属于读操作。Zookeeper 客户端在请求读操作时,可以选择是否设置 Watch。
六、Watch 机制
Watch 机制是它功能强大的强大辅助。我们可以理解为是注册在Znode 上的触发器。当这个Znode 发生改变时,也就是调用了create、delete等操作时,将会触发Znode 上注册的对应事件,请求 watch 的客户端会接收到异步通知。
常见的监听场景有以下两项:
- 监听Znode 节点的数据变化;
- 监听子节点的增减变化。
以节点的删除举例:
客户端调用getData ,watch 参数为 true。服务端接到请求,返回节点数据,并且在对应的哈希表里插入被watch 的znode 路径,已经watcher列表
当被watch 的Znode 被删除,服务端会查找哈希表,找到该znode 的所有watcher,异步通知客户端,并且删除哈希表中对应的key-value.
七、ACL
Zookeeper内部存储了分布式系统运行时状态的元数据,这些元数据会直接影响基于Zookeeper进行构造的分布式系统的运行状态,如何保障系统中数据的安全,从而避免因误操作而带来的数据随意变更而导致的数据库异常十分重要,Zookeeper提供了一套完善的ACL权限控制机制来保障数据的安全。
我们可以从三个方面来理解ACL机制:权限模式 Scheme、授权对象 ID、权限 Permission,通常使用"scheme:id:permission"
来标识一个有效的ACL信息。
八、Zookeeper 集群
Zookeeper Service 集群采取的是一主多从结构。
Zookeeper三大角色
- leader: Leader作为整个ZooKeeper集群的主节点,负责响应所有对ZooKeeper状态变更的请求。它会将每个状态更新请求进行排序和编号,以便保证整个集群内部消息处理的有序性(FIFO)。
- follower: 了响应本服务器上的读请求外,follower还要处理leader的提议,并在leader提交该提议时在本地也进行提交。会参与选举.
- Observer: 如果ZooKeeper集群的读取负载很高,或者客户端多到跨机房,可以设置一些observer服务器,以提高读取的吞吐量。不参与选举,不需要将事务持久化到磁盘。
我们主要学习 Leader、Follower。
在更新数据的时候,首先更新到主节点(这里的节点是指服务器,而不是Znode),再同步到从节点。写操作时,从节点接收到请求,就会把请求转发给主节点。读取数据时,直接读取任意从节点。
为了保证主从节点的数据一致性,Zookeeper 采用 ZAB协议。
九、ZAB 协议
1. ZAB 协议定义的三种节点状态
- Looking : 选举状态
- Following : Follower 节点(从节点)所处状态
- Leading: Leader 节点(主节点)所处状态
2. 最大 ZXID 概念
最大ZXID 也就是节点本地的最新事务编号,包含 epoch 和计数部分。epoch 是纪元的意思。
3. 崩溃恢复(选举。。。)
如果 Zookeeper 主节点挂掉了,宕机了,集群就会进行奔溃恢复,ZAB 的崩溃恢复分为三个阶段:
3.1. 选举阶段
此时集群处于 Looking 状态,他们会各自向其它节点发起投票,投标包含自己的服务器id(SID)和最新事务id(ZID),即 (SID,ZID)。
假设我们现在集群由5台机器组成,SID 分别为1、2、3、4、5,ZXID分别为 9、9、9、8、9,并且SID 为2的是 Leader,某时刻,1、2 所处机器出现故障,因此集群开始进行Leader 选举。
-
第一次选举,每台机器都会将自己作为投票对象,于是SID为3,4,5的机器投票情况分别为(3, 9)、(4, 8)、(5, 8)。
-
变更投票。每台机器发出投票后,也会收到其它机器的投票,每台机器根据一定规则来处理收到的其它机器的投票,并以此来决定是否需要变更自己的投票,这个规则就是整个Leader 选举算法的核心:
- vote_sid:接收到的投票中所推举Leader服务器的SID。
- vote_zxid:接收到的投票中所推举Leader服务器的ZXID。
- self_sid:当前服务器自己的SID。
- self_zxid:当前服务器自己的ZXID。
每次对收到的投票进行处理,都是对 (vote_sid, vote_zxid)和(self_sid, self_zxid)对比的过程:
-
规则一:如果vote_zxid大于self_zxid,就认可当前收到的投票,并再次将该投票发送出去。
-
规则二:如果vote_zxid小于self_zxid,那么坚持自己的投票,不做任何变更。
-
规则三:如果vote_zxid等于self_zxid,那么就对比两者的SID,如果vote_sid大于self_sid,那么就认可当前收到的投票,并再次将该投票发送出去。
-
规则四:如果vote_zxid等于self_zxid,并且vote_sid小于self_sid,那么坚持自己的投票,不做任何变更。
图解:
-
确认Leader。 经过第二轮投票后,集群中的每台机器都会再次接收到其他机器的投票,然后开始统计投票。如果一台机器收到了超过半数的相同投票,那么这个投票对应的SID 机器即为Leader。此时我们的例子 Server3 为 Leader。
分析:通常服务器上的数据越新(ZXID会越大),其成为 Leader 的可能性越大,也就越能保证数据的恢复。如果 ZXID相同,则SID 越大机会越大。
3.2. Discovery 发现阶段
发现阶段,这个阶段用于在从节点中发现最新的ZXID 和事务日志。这是为了防止某些意外情况,比如因网络原因在上一阶段产生多个Leader 的情况。
所以这一阶段,Leader集思广益,接收所有Follower发来各自的最新epoch值。Leader从中选出最大的epoch,基于此值加1,生成新的epoch分发给各个Follower。
各个Follower收到全新的epoch后,返回ACK给Leader,带上各自最大的ZXID和历史事务日志。Leader选出最大的ZXID,并更新自身历史日志。
3.3. Synchronization 同步阶段
把Leader 刚才收集得到的最新历史事务日志,同步给集群中所有的Follower。只有当半数Follower同步成功,这个准Leader才能成为正式的Leader。
4. 写入数据-Broadcast
Zookeeper常规情况下更新数据的时候,由Leader广播到所有的Follower。其过程如下:
-
客户端发出写入数据请求给任意Follower。
-
Follower把写入数据请求转发给Leader。
-
Leader采用二阶段提交方式,先发送Propose广播给Follower。
-
Follower接到Propose消息,写入日志成功后,返回ACK消息给Leader。
-
Leader接到半数以上ACK消息,返回成功给客户端,并且广播Commit请求给Follower。
十、Zookeeper 的应用场景
1. 分布式锁
2. 服务注册和发现
利用Znode和Watcher,可以实现分布式服务的注册和发现。最著名的应用就是阿里的分布式RPC框架Dubbo。
3. 共享配置和状态信息
十一、2PC 和 3PC
摘抄至:https://www.hollischuang.com/archives/1580
1. 二阶段提交协议(2PC)
二阶段提交协议主要分为来个阶段:准备阶段和提交阶段。
在日常生活中其实是有很多事都是这种二阶段提交的,比如西方婚礼中就经常出现这种场景:
牧师:”你愿意娶这个女人吗?爱她、忠诚于她,无论她贫困、患病或者残疾,直至死亡。Doyou(你愿意吗)?”
新郎:”Ido(我愿意)!”
牧师:”你愿意嫁给这个男人吗?爱他、忠诚于他,无论他贫困、患病或者残疾,直至死亡。Doyou(你愿意吗)?”
新娘:”Ido(我愿意)!”
牧师:现在请你们面向对方,握住对方的双手,作为妻子和丈夫向对方宣告誓言。
新郎:我——某某某,全心全意娶你做我的妻子,无论是顺境或逆境,富裕或贫穷,健康或疾病,快乐或忧愁,我都将毫无保留地爱你,我将努力去理解你,完完全全信任你。我们将成为一个整体,互为彼此的一部分,我们将一起面对人生的一切,去分享我们的梦想,作为平等的忠实伴侣,度过今后的一生。
新娘:我全心全意嫁给你作为你的妻子,无论是顺境或逆境,富裕或贫穷,健康或疾病,快乐或忧愁,我都将毫无保留的爱你,我将努力去理解你,完完全全信任你,我们将成为一个整体,互为彼此的一部分,我们将一起面对人生的一切,去分享我们的梦想,作为平等的忠实伴侣,度过今后的一生。
上面这个比较经典的桥段就是一个典型的二阶段提交过程。
首先协调者(牧师)会询问两个参与者(二位新人)是否能执行事务提交操作(愿意结婚)。如果两个参与者能够执行事务的提交,先执行事务操作,然后返回YES,如果没有成功执行事务操作,就返回NO。
当协调者接收到所有的参与者的反馈之后,开始进入事务提交阶段。如果所有参与者都返回YES,那就发送COMMIT请求,如果有一个人返回NO,那就返送roolback请求。
值得注意的是,二阶段提交协议的第一阶段准备阶段不仅仅是回答YES or NO,还是要执行事务操作的,只是执行完事务操作,并没有进行commit还是roolback。和上面的结婚例子不太一样。如果非要举例的话可以理解为男女双方交换定情信物的过程。信物一旦交给对方了,这个信物就不能挪作他用了。也就是说,一旦事务执行之后,在没有执行commit或者roolback之前,资源是被锁定的。这会造成阻塞。
1.1. 2PC存在的问题
下面我们来分析下2PC存在的问题。
这里暂且不谈2PC存在的同步阻塞、单点问题、脑裂等问题(上篇文章中有具体介绍),我们只讨论下数据一致性问题。作为一个分布式的一致性协议,我们主要关注他可能带来的一致性问题的。
2PC在执行过程中可能发生协调者或者参与者突然宕机的情况,在不同时期宕机可能有不同的现象。
情况一:协调者挂了,参与者没挂
这种情况其实比较好解决,只要找一个协调者的替代者。当他成为新的协调者的时候,询问所有参与者的最后那条事务的执行情况,他就可以知道是应该做什么样的操作了。所以,这种情况不会导致数据不一致。
情况二:参与者挂了,协调者没挂
这种情况其实也比较好解决。如果协调者挂了。那么之后的事情有两种情况:
- 第一个是挂了就挂了,没有再恢复。那就挂了呗,反正不会导致数据一致性问题。
- 第二个是挂了之后又恢复了,这时如果他有未执行完的事务操作,直接取消掉,然后询问协调者目前我应该怎么做,协调者就会比对自己的事务执行记录和该参与者的事务执行记录,告诉他应该怎么做来保持数据的一致性。
情况三:参与者挂了,协调者也挂了
这种情况比较复杂,我们分情况讨论。
- 协调者和参与者在第一阶段挂了。
- 由于这时还没有执行commit操作,新选出来的协调者可以询问各个参与者的情况,再决定是进行commit还是roolback。因为还没有commit,所以不会导致数据一致性问题。
- 第二阶段协调者和参与者挂了,挂了的这个参与者在挂之前并没有接收到协调者的指令,或者接收到指令之后还没来的及做commit或者roolback操作。
- 这种情况下,当新的协调者被选出来之后,他同样是询问所有的参与者的情况。只要有机器执行了abort(roolback)操作或者第一阶段返回的信息是No的话,那就直接执行roolback操作。如果没有人执行abort操作,但是有机器执行了commit操作,那么就直接执行commit操作。这样,当挂掉的参与者恢复之后,只要按照协调者的指示进行事务的commit还是roolback操作就可以了。因为挂掉的机器并没有做commit或者roolback操作,而没有挂掉的机器们和新的协调者又执行了同样的操作,那么这种情况不会导致数据不一致现象。
- 第二阶段协调者和参与者挂了,挂了的这个参与者在挂之前已经执行了操作。但是由于他挂了,没有人知道他执行了什么操作。
- 这种情况下,新的协调者被选出来之后,如果他想负起协调者的责任的话他就只能按照之前那种情况来执行commit或者roolback操作。这样新的协调者和所有没挂掉的参与者就保持了数据的一致性,我们假定他们执行了commit。但是,这个时候,那个挂掉的参与者恢复了怎么办,因为他之前已经执行完了之前的事务,如果他执行的是commit那还好,和其他的机器保持一致了,万一他执行的是roolback操作那?这不就导致数据的不一致性了么?虽然这个时候可以再通过手段让他和协调者通信,再想办法把数据搞成一致的,但是,这段时间内他的数据状态已经是不一致的了!
所以,2PC协议中,如果出现协调者和参与者都挂了的情况,有可能导致数据不一致。
为了解决这个问题,衍生除了3PC。我们接下来看看3PC是如何解决这个问题的。
2. 三阶段提交协议(3PC)
3PC最关键要解决的就是协调者和参与者同时挂掉的问题,所以3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit
、PreCommit
、DoCommit
三个阶段。在第一阶段,只是询问所有参与者是否可可以执行事务操作,并不在本阶段执行事务操作。当协调者收到所有的参与者都返回YES时,在第二阶段才执行事务操作,然后在第三阶段在执行commit或者rollback。
这里再举一个生活中类似三阶段提交的例子:
班长要组织全班同学聚餐,由于大家毕业多年,所以要逐个打电话敲定时间,时间初定10.1日。然后开始逐个打电话。
班长:小A,我们想定在10.1号聚会,你有时间嘛?有时间你就说YES,没有你就说NO,然后我还会再去问其他人,具体时间地点我会再通知你,这段时间你可先去干你自己的事儿,不用一直等着我。(协调者询问事务是否可以执行,这一步不会锁定资源)
小A:好的,我有时间。(参与者反馈)
班长:小B,我们想定在10.1号聚会……不用一直等我。
班长收集完大家的时间情况了,一看大家都有时间,那么就再次通知大家。(协调者接收到所有YES指令)
班长:小A,我们确定了10.1号聚餐,你要把这一天的时间空出来,这一天你不能再安排其他的事儿了。然后我会逐个通知其他同学,通知完之后我会再来和你确认一下,还有啊,如果我没有特意给你打电话,你就10.1号那天来聚餐就行了。对了,你确定能来是吧?(协调者发送事务执行指令,这一步锁住资源。如果由于网络原因参与者在后面没有收到协调者的命令,他也会执行commit)
小A顺手在自己的日历上把10.1号这一天圈上了,然后跟班长说,我可以去。(参与者执行事务操作,反馈状态)
班长:小B,我们觉得了10.1号聚餐……你就10.1号那天来聚餐就行了。
班长通知完一圈之后。所有同学都跟他说:”我已经把10.1号这天空出来了”。于是,他在10.1号这一天又挨个打了一遍电话告诉他们:嘿,现在你们可以出门拉。。。。(协调者收到所有参与者的ACK响应,通知所有参与者执行事务的commit)
小A,小B:我已经出门拉。(执行commit操作,反馈状态)
3. 3PC为什么比2PC好?
直接分析协调者和参与者都挂的情况。
- 第二阶段协调者和参与者挂了,挂了的这个参与者在挂之前已经执行了操作。但是由于他挂了,没有人知道他执行了什么操作。
- 这种情况下,当新的协调者被选出来之后,他同样是询问所有的参与者的情况来觉得是commit还是roolback。这看上去和二阶段提交一样啊?他是怎么解决一致性问题的呢?
- 看上去和二阶段提交的那种数据不一致的情况的现象是一样的,但仔细分析所有参与者的状态的话就会发现其实并不一样。我们假设挂掉的那台参与者执行的操作是commit。那么其他没挂的操作者的状态应该是什么?他们的状态要么是prepare-commit要么是commit。因为3PC的第三阶段一旦有机器执行了commit,那必然第一阶段大家都是同意commit。所以,这时,新选举出来的协调者一旦发现未挂掉的参与者中有人处于commit状态或者是prepare-commit的话,那就执行commit操作。否则就执行rollback操作。这样挂掉的参与者恢复之后就能和其他机器保持数据一致性了。(为了简单的让大家理解,笔者这里简化了新选举出来的协调者执行操作的具体细节,真实情况比我描述的要复杂)
简单概括一下就是,如果挂掉的那台机器已经执行了commit,那么协调者可以从所有未挂掉的参与者的状态中分析出来,并执行commit。如果挂掉的那个参与者执行了rollback,那么协调者和其他的参与者执行的肯定也是rollback操作。
所以,再多引入一个阶段之后,3PC解决了2PC中存在的那种由于协调者和参与者同时挂掉有可能导致的数据一致性问题。
4. 3PC存在的问题
在doCommit阶段,如果参与者无法及时接收到来自协调者的doCommit或者rebort请求时,会在等待超时之后,会继续进行事务的提交。
所以,由于网络原因,协调者发送的abort响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。这样就和其他接到abort命令并执行回滚的参与者之间存在数据不一致的情况。
Zookeeper就是典型的2PC。