在Zookeeper 官网上是这样介绍它的:ZooKeeper is a centralized service for maintaining configuration information, naming, providing distributed synchronization, and providing group services。
大概的意思就是说 Zookeeper 主要是一个分布式服务协调框架,实现同步服务,配置维护和命名维护等分布式应用,是一个高性能的分布式数据一致性解决方案。
ZooKeeper主要服务于分布式系统,可以用ZooKeeper来做:统一配置管理、统一命名服务、分布式锁、集群管理。为什么能做这些后面再说。
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是一定要达到的。
ZooKeeper是个CP(一致性+分区容错性)的,即任何时刻对ZooKeeper的访问请求能得到一致的数据结果,同时系统对网络分割具备容错性;但是它不能保证每次服务请求的可用性。也就是在极端环境下,ZooKeeper可能会丢弃一些请求,消费者程序需要重新请求才能获得结果。
ZooKeeper是分布式协调服务,它的职责是保证数据在其管辖下的所有服务之间保持同步、一致;所以就不难理解为什么ZooKeeper被设计成CP而不是AP特性的了。而且, 作为ZooKeeper的核心实现算法Zab(后述解释),就是解决了分布式系统下数据如何在多个服务之间保持同步问题的。
Zookeeper提供基于类似于文件系统的目录节点树方式的数据存储,但是Zookeeper并不是用来专门存储数据的,它的作用主要是用来维护和监控你存储的数据的状态变化。通过监控这些数据状态的变化,从而可以达到基于数据的集群管理。
与Linux文件系统不同的是,Linux文件系统有目录和文件的区别,而Zookeeper的数据节点称为ZNode,ZNode是Zookeeper中数据的最小单元,每个ZNode都可以保存数据,同时还可以挂载子节点,因此构成了一个层次化的命名空间,称为树。
Znode 主要有以下几种类型:
注:Zookeeper 主要是为读多写少的场景设计,Znode 并不是用来存储大规模业务数据的,而是用来春初少量的状态和配置信息的,每个节点的数据一般最大不超过 1MB。
常见的有:
以上的 exists、getData、getChildren 属于读操作。Zookeeper 客户端在请求读操作时,可以选择是否设置 Watch。
Watch 机制是它功能强大的强大辅助。我们可以理解为是注册在Znode 上的触发器。当这个Znode 发生改变时,也就是调用了create、delete等操作时,将会触发Znode 上注册的对应事件,请求 watch 的客户端会接收到异步通知。
常见的监听场景有以下两项:
以节点的删除举例:
Zookeeper内部存储了分布式系统运行时状态的元数据,这些元数据会直接影响基于Zookeeper进行构造的分布式系统的运行状态,如何保障系统中数据的安全,从而避免因误操作而带来的数据随意变更而导致的数据库异常十分重要,Zookeeper提供了一套完善的ACL权限控制机制来保障数据的安全。
我们可以从三个方面来理解ACL机制:权限模式 Scheme、授权对象 ID、权限 Permission,通常使用"scheme:id:permission"
来标识一个有效的ACL信息。
Zookeeper Service 集群采取的是一主多从结构。
我们主要学习 Leader、Follower。
在更新数据的时候,首先更新到主节点(这里的节点是指服务器,而不是Znode),再同步到从节点。写操作时,从节点接收到请求,就会把请求转发给主节点。读取数据时,直接读取任意从节点。
为了保证主从节点的数据一致性,Zookeeper 采用 ZAB协议。
最大ZXID 也就是节点本地的最新事务编号,包含 epoch 和计数部分。epoch 是纪元的意思。
如果 Zookeeper 主节点挂掉了,宕机了,集群就会进行奔溃恢复,ZAB 的崩溃恢复分为三个阶段:
此时集群处于 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, 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 越大机会越大。
发现阶段,这个阶段用于在从节点中发现最新的ZXID 和事务日志。这是为了防止某些意外情况,比如因网络原因在上一阶段产生多个Leader 的情况。
所以这一阶段,Leader集思广益,接收所有Follower发来各自的最新epoch值。Leader从中选出最大的epoch,基于此值加1,生成新的epoch分发给各个Follower。
各个Follower收到全新的epoch后,返回ACK给Leader,带上各自最大的ZXID和历史事务日志。Leader选出最大的ZXID,并更新自身历史日志。
把Leader 刚才收集得到的最新历史事务日志,同步给集群中所有的Follower。只有当半数Follower同步成功,这个准Leader才能成为正式的Leader。
Zookeeper常规情况下更新数据的时候,由Leader广播到所有的Follower。其过程如下:
客户端发出写入数据请求给任意Follower。
Follower把写入数据请求转发给Leader。
Leader采用二阶段提交方式,先发送Propose广播给Follower。
Follower接到Propose消息,写入日志成功后,返回ACK消息给Leader。
Leader接到半数以上ACK消息,返回成功给客户端,并且广播Commit请求给Follower。
1. 分布式锁
2. 服务注册和发现
利用Znode和Watcher,可以实现分布式服务的注册和发现。最著名的应用就是阿里的分布式RPC框架Dubbo。
3. 共享配置和状态信息
摘抄至:https://www.hollischuang.com/archives/1580
二阶段提交协议主要分为来个阶段:准备阶段和提交阶段。
在日常生活中其实是有很多事都是这种二阶段提交的,比如西方婚礼中就经常出现这种场景:
牧师:”你愿意娶这个女人吗?爱她、忠诚于她,无论她贫困、患病或者残疾,直至死亡。Doyou(你愿意吗)?”
新郎:”Ido(我愿意)!”
牧师:”你愿意嫁给这个男人吗?爱他、忠诚于他,无论他贫困、患病或者残疾,直至死亡。Doyou(你愿意吗)?”
新娘:”Ido(我愿意)!”
牧师:现在请你们面向对方,握住对方的双手,作为妻子和丈夫向对方宣告誓言。
新郎:我——某某某,全心全意娶你做我的妻子,无论是顺境或逆境,富裕或贫穷,健康或疾病,快乐或忧愁,我都将毫无保留地爱你,我将努力去理解你,完完全全信任你。我们将成为一个整体,互为彼此的一部分,我们将一起面对人生的一切,去分享我们的梦想,作为平等的忠实伴侣,度过今后的一生。
新娘:我全心全意嫁给你作为你的妻子,无论是顺境或逆境,富裕或贫穷,健康或疾病,快乐或忧愁,我都将毫无保留的爱你,我将努力去理解你,完完全全信任你,我们将成为一个整体,互为彼此的一部分,我们将一起面对人生的一切,去分享我们的梦想,作为平等的忠实伴侣,度过今后的一生。
上面这个比较经典的桥段就是一个典型的二阶段提交过程。
首先协调者(牧师)会询问两个参与者(二位新人)是否能执行事务提交操作(愿意结婚)。如果两个参与者能够执行事务的提交,先执行事务操作,然后返回YES,如果没有成功执行事务操作,就返回NO。
当协调者接收到所有的参与者的反馈之后,开始进入事务提交阶段。如果所有参与者都返回YES,那就发送COMMIT请求,如果有一个人返回NO,那就返送roolback请求。
值得注意的是,二阶段提交协议的第一阶段准备阶段不仅仅是回答YES or NO,还是要执行事务操作的,只是执行完事务操作,并没有进行commit还是roolback。和上面的结婚例子不太一样。如果非要举例的话可以理解为男女双方交换定情信物的过程。信物一旦交给对方了,这个信物就不能挪作他用了。也就是说,一旦事务执行之后,在没有执行commit或者roolback之前,资源是被锁定的。这会造成阻塞。
下面我们来分析下2PC存在的问题。
这里暂且不谈2PC存在的同步阻塞、单点问题、脑裂等问题(上篇文章中有具体介绍),我们只讨论下数据一致性问题。作为一个分布式的一致性协议,我们主要关注他可能带来的一致性问题的。
2PC在执行过程中可能发生协调者或者参与者突然宕机的情况,在不同时期宕机可能有不同的现象。
这种情况其实比较好解决,只要找一个协调者的替代者。当他成为新的协调者的时候,询问所有参与者的最后那条事务的执行情况,他就可以知道是应该做什么样的操作了。所以,这种情况不会导致数据不一致。
这种情况其实也比较好解决。如果协调者挂了。那么之后的事情有两种情况:
这种情况比较复杂,我们分情况讨论。
所以,2PC协议中,如果出现协调者和参与者都挂了的情况,有可能导致数据不一致。
为了解决这个问题,衍生除了3PC。我们接下来看看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操作,反馈状态)
直接分析协调者和参与者都挂的情况。
简单概括一下就是,如果挂掉的那台机器已经执行了commit,那么协调者可以从所有未挂掉的参与者的状态中分析出来,并执行commit。如果挂掉的那个参与者执行了rollback,那么协调者和其他的参与者执行的肯定也是rollback操作。
所以,再多引入一个阶段之后,3PC解决了2PC中存在的那种由于协调者和参与者同时挂掉有可能导致的数据一致性问题。
在doCommit阶段,如果参与者无法及时接收到来自协调者的doCommit或者rebort请求时,会在等待超时之后,会继续进行事务的提交。
所以,由于网络原因,协调者发送的abort响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。这样就和其他接到abort命令并执行回滚的参与者之间存在数据不一致的情况。
Zookeeper就是典型的2PC。