赞
踩
ZooKeeper
是一个开源的分布式协调服务框架,为分布式系统提供一致性服务。
那么什么是分布式?什么是协调程序?和集群又有什么区别?
举一个例子来说明,现在有一个网上商城购物系统,并发量太大单机系统承受不住,那我们可以多加几台服务器支持大并发量的访问需求,这个就是所谓的Cluster
集群 。
如果我们将这个网上商城购物系统拆分成多个子系统,比如订单系统、积分系统、购物车系统等等,然后将这些子系统部署在不同的服务器上 ,这个时候就是 Distributed
分布式 。
对于集群来说,多加几台服务器就行(当然还得解决session共享,负载均衡等问题),而对于分布式来说,你首先需要将业务进行拆分,然后再加服务器,同时还要去解决分布式带来的一系列问题。比如各个分布式组件如何协调起来,如何减少各个系统之间的耦合度,如何处理分布式事务,如何去配置整个分布式系统,如何解决各分布式子系统的数据不一致问题等等。ZooKeeper
主要就是解决这些问题的。
开源项目
许多著名的开源项目用到了 ZooKeeper,比如:
ZooKeeper 有三种运行模式:单机模式、伪集群模式和集群模式。
一个分布式系统必然会存在一个问题:因为 分区容忍性(partition tolerance) 的存在,就必定要求我们需要在系统 可用性(availability) 和 数据一致性(consistency) 中做出权衡 。这就是著名的 CAP
定理。
举个例子来说明,假如班级代表整个分布式系统,而学生是整个分布式系统中一个个独立的子系统。这个时候班里的小红小明偷偷谈恋爱被班里的小花发现了,小花欣喜若狂告诉了周围的人,然后小红小明谈恋爱的消息在班级里传播起来了。当在消息的传播(散布)过程中,你问班里一个同学的情况,如果他回答你不知道,那么说明整个班级系统出现了数据不一致的问题(因为小花已经知道这个消息了)。而如果他直接不回答你,因为现在消息还在班级里传播(为了保证一致性,需要所有人都知道才可提供服务),这个时候就出现了系统的可用性问题。
这个例子中前者就是 Eureka
的处理方式,它保证了AP(可用性),后者就 ZooKeeper
的处理方式,它保证了CP(数据一致性)。
CAP理论中,P(分区容忍性)是必然要满足的,因为毕竟是分布式,不能把所有的应用全放到一个服务器里面,这样服务器是吃不消的。所以,只能从AP(可用性)和CP(一致性)中找平衡。
怎么个平衡法呢?在这种环境下出现了BASE理论:即使无法做到强一致性,但分布式系统可以根据自己的业务特点,采用适当的方式来使系统达到最终的一致性。BASE理论由:Basically Avaliable
基本可用、Soft state
软状态、Eventually consistent
最终一致性组成。
一句话概括就是:平时系统要求是基本可用,运行有可容忍的延迟状态,但是,无论如何经过一段时间的延迟后系统最终必须达成数据是一致的。
ACID 是传统数据库常用的设计理念,追求强一致性模型。BASE 支持的是大型分布式系统,通过牺牲强一致性获得高可用性。
其实可能发现不管是CAP理论,还是BASE理论,他们都是理论,这些理论是需要算法来实现的,这些算法有2PC、3PC、Paxos、Raft、ZAB,它们所解决的问题全部都是:在分布式环境下,怎么让系统尽可能的高可用,而且数据能最终能达到一致。
推荐大家先了解其他的一致性算法,如2PC、3PC、Paxos、Raft,可参考大数据中的2PC、3PC、Paxos、Raft、ZAB。
作为一个优秀高效且可靠的分布式协调框架,ZooKeeper 在解决分布式数据一致性问题时并没有直接使用 Paxos ,而是专门定制了一致性协议叫做 ZAB(ZooKeeper Automic Broadcast) 原子广播协议,该协议能够很好地支持 崩溃恢复 。
ZAB 中三个主要的角色,Leader 领导者、Follower跟随者、Observer观察者 。
Leader
: 集群中 唯一的写请求处理者 ,能够发起投票(投票也是为了进行写请求)。Follower
: 能够接收客户端的请求,如果是读请求则可以自己处理,如果是写请求则要转发给 Leader 。在选举过程中会参与投票,有选举权和被选举权 。Observer
: 就是没有选举权和被选举权的 Follower 。在 ZAB
协议中对 zkServer
(即上面我们说的三个角色的总称) 还有两种模式的定义,分别是 消息广播 和 崩溃恢复 。
ZooKeeper 采用全局递增的事务 id 来标识,所有 proposal(提议)在被提出的时候加上了ZooKeeper Transaction Id 。ZXID是64位的Long类型,这是保证事务的顺序一致性的关键。ZXID中高32位表示纪元epoch,低32位表示事务标识xid。你可以认为zxid越大说明存储数据越新,如下图所示:
每个ZooKeeper服务器,都需要在数据文件夹下创建一个名为myid的文件,该文件包含整个ZooKeeper集群唯一的id(整数)。例如,某ZooKeeper集群包含三台服务器,hostname分别为zoo1、zoo2和zoo3,其myid分别为1、2和3,则在配置文件中其id与hostname必须一一对应,如下所示。在该配置文件中,server.
后面的数据即为myid
server.1=zoo1:2888:3888
server.2=zoo2:2888:3888
server.3=zoo3:2888:3888
每一个follower节点都会有一个先进先出(FIFO)的队列用来存放收到的事务请求,保证执行事务的顺序。所以:
1.可靠提交由ZAB的事务一致性协议保证
2.全局有序由TCP协议保证
3.因果有序由follower的历史队列(history queue)保证
ZAB协议两种模式:消息广播模式和崩溃恢复模式。
说白了就是 ZAB
协议是如何处理写请求的,上面我们不是说只有 Leader 能处理写请求嘛?那么我们的 Follower
和 Observer
是不是也需要 同步更新数据 呢?总不能数据只在Leader
中更新了,其他角色都没有得到更新吧。
第一步肯定需要 Leader
将写请求 广播 出去呀,让Leader
问问Followers
是否同意更新,如果超过半数以上的同意那么就进行Follower
和 Observer
的更新(和 Paxos
一样)。消息广播机制是通过如下图流程保证事务的顺序一致性的:
过半写成功策略: Leader节点接收到写请求后,这个Leader会将写请求广播给各个Server,各个Server会将该写请求加入历史队列,并向Leader发送ACK信息,当Leader收到一半以上的ACK消息后,说明该写操作可以执行。Leader会向各个server发送commit消息,各个server收到消息后执行commit操作。
这里要注意以下几点:
Leader并不需要得到Observer的ACK,即Observer无投票权
Leader不需要得到所有Follower的ACK,只要收到过半的ACK即可,同时Leader本身对自己有一个ACK
Observer虽然无投票权,但仍须同步Leader的数据从而在处理读请求时可以返回尽可能新的数据
另外,Follower/Observer也可以接受写请求,此时:
Follower/Observer接受写请求以后,不能直接处理,而需要将写请求转发给Leader处理
除了多了一步请求转发,其它流程与直接写Leader无任何区别
Leader处理写请求是通过上面的消息广播模式,实质上最后所有的zkServer都要执行写操作,这样数据才会一致
而对于读请求,Leader/Follower/Observer都可直接处理读请求,从本地内存中读取数据并返回给客户端即可。由于处理读请求不需要各个服务器之间的交互,因此Follower/Observer越多,整体可处理的读请求量越大,也即读性能越好。
恢复模式大致可以分为四个阶段:选举、发现、同步、广播。
在发现阶段,或许有人会问:既然Leader被选为主节点,已经是集群里数据最新的了,为什么还要从节点中寻找最新事务呢?这是为了防止某些意外情况。所以这一阶段,Leader集思广益,接收所有Follower发来各自的最新epoch值。
这里有两点要注意:
(1)确保已经被Leader提交的提案最终能够被所有的Follower提交
假设 Leader (server2) 发送 commit 请求(忘了请看上面的消息广播模式),他发送给了 server3,然后要发给 server1 的时候突然挂了。这个时候重新选举的时候我们如果把 server1 作为 Leader 的话,那么肯定会产生数据不一致性,因为 server3 肯定会提交刚刚 server2 发送的 commit 请求的提案,而 server1 根本没收到所以会丢弃。
那怎么解决呢?
这个时候 server1 已经不可能成为 Leader 了,因为 server1 和 server3 进行投票选举的时候会比较 ZXID ,而此时 server3 的 ZXID 肯定比 server1 的大了(后面讲到选举机制时就明白了)。同理,只能由server3当Leader,server3当上Leader之后,在同步阶段,会将最新提议历史同步给集群中所有的Follower,这就保证数据一致性了。如果server2在某个时刻又重新恢复了,它作为Follower 的身份进入集群中,再向Leader同步当前最新提议和Zxid即可。
(2)确保跳过那些已经被丢弃的提案
假设 Leader (server2) 此时同意了提案N1,自身提交了这个事务并且要发送给所有 Follower 要 commit 的请求,却在这个时候挂了,此时肯定要重新进行 Leader 的选举,假如此时选 server1 为 Leader (这无所谓,server1和server2都可以当选)。但是过了一会,这个 挂掉的 Leader 又重新恢复了 ,此时它肯定会作为 Follower 的身份进入集群中,需要注意的是刚刚 server2 已经同意提交了提案N1,但其他 server 并没有收到它的 commit 信息,所以其他 server 不可能再提交这个提案N1了,这样就会出现数据不一致性问题了,所以 该提案N1最终需要被抛弃掉 。
脑裂问题:所谓的“脑裂”即“大脑分裂”,也就是本来一个“大脑”被拆分了两个或多个“大脑”。通俗的说,就是比如当你的 cluster 里面有两个节点,它们都知道在这个 cluster 里需要选举出一个 master。那么当它们两之间的通信完全没有问题的时候,就会达成共识,选出其中一个作为 master。但是如果它们之间的通信出了问题,那么两个结点都会觉得现在没有 master,所以每个都把自己选举成 master,于是 cluster 里面就会有两个 master。
ZAB为解决脑裂问题,要求集群内的节点数量为2N+1, 当网络分裂后,始终有一个集群的节点数量过半数,而另一个集群节点数量小于N+1(即小于半数), 因为选主需要过半数节点同意,所以任何情况下集群中都不可能出现大于一个leader的情况。
因此,有了过半机制,对于一个Zookeeper集群,要么没有Leader,要没只有1个Leader,这样就避免了脑裂问题。
Leader
选举可以分为两个不同的阶段,第一个是我们提到的 Leader
宕机需要重新选举,第二则是当 Zookeeper
启动时需要进行系统的 Leader
初始化选举。下面是zkserver的几种状态:
假设我们集群中有3台机器,那也就意味着我们需要2台同意(超过半数)。这里假设服务器1~3的myid分别为1,2,3,初始化Leader选举过程如下:
运行时候如果Leader节点崩溃了会走崩溃恢复模式,新Leader选出前会暂停对外服务,大致可以分为四个阶段:选举、发现、同步、广播(见4.5节),此时Leader选举流程如下:
举个例子来说明,假设集群有三台服务器,Leader (server2)挂掉了,只剩下server1和server3。 server1 给自己投票为(1,99),然后广播给其他 server,server3 首先也会给自己投票(3,95),然后也广播给其他 server。server1 和 server3 此时会收到彼此的投票信息,和一开始选举一样,他们也会比较自己的投票和收到的投票(zxid 大的优先,如果相同那么就 myid 大的优先)。这个时候 server1 收到了 server3 的投票发现没自己的合适故不变,server3 收到 server1 的投票结果后发现比自己的合适于是更改投票为(1,99)然后广播出去,最后 server1 收到了发现自己的投票已经超过半数就把自己设为 Leader,server3 也随之变为 Follower。
ZooKeeper 数据模型(Data model)采用层次化的多叉树形结构,每个节点上都可以存储数据,这些数据可以是数字、字符串或者是二级制序列。并且,每个节点还可以拥有 N 个子节点,最上层是根节点以/
来代表。
每个数据节点在 ZooKeeper 中被称为 znode,它是 ZooKeeper 中数据的最小单元。并且,每个 znode 都一个唯一的路径标识。由于ZooKeeper 主要是用来协调服务的,而不是用来存储业务数据的,这种特性使得 Zookeeper 不能用于存放大量的数据,每个节点的存放数据上限为1M。
和文件系统一样,我们能够自由的增加、删除znode,在一个znode下增加、删除子znode,唯一的不同在于znode是可以存储数据的。默认有四种类型的znode:
在zookeeper客户端使用get
命令可以查看znode的内容和状态信息:
[zk: localhost:2181(CONNECTED) 2] get /zk01
updateed02
cZxid = 0x600000023
ctime = Mon Mar 01 21:20:26 CST 2021
mZxid = 0xb0000000d
mtime = Fri Mar 05 17:15:53 CST 2021
pZxid = 0xb00000018
cversion = 5
dataVersion = 7
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 10
numChildren = 3
下面我们来看一下每个 znode 状态信息究竟代表的是什么吧
znode 状态信息 | 解释 |
---|---|
cZxid | create ZXID,即该数据节点被创建时的事务 id |
ctime | create time,znode 被创建的毫秒数(从1970 年开始) |
mZxid | modified ZXID,znode 最后更新的事务 id |
mtime | modified time,znode 最后修改的毫秒数(从1970 年开始) |
pZxid | znode 最后更新子节点列表的事务 id,只有子节点列表变更才会更新 pZxid,子节点内容变更不会更新 |
cversion | znode 子节点变化号,znode 子节点修改次数,子节点每次变化时值增加 1 |
dataVersion | znode 数据变化号,节点创建时为 0,每更新一次节点内容(不管内容有无变化)该版本号的值增加 1 |
aclVersion | znode 访问控制列表(ACL )版本号,表示该节点 ACL 信息变更次数 |
ephemeralOwner | 如果是临时节点,这个是 znode 拥有者的 sessionid。如果不是临时节,则 ephemeralOwner=0 |
dataLength | znode 的数据长度 |
numChildren | znode 子节点数量 |
Watcher 监听机制是 Zookeeper 中非常重要的特性, 我们基于 Zookeeper上创建的节点,可以对这些节点绑定监听事件,比如可以监听节点数据变更、节点删除、子节点状态变更等事件,通过这个事件机制,可以基于 Zookeeper 实现分布式锁、集群管理等多种功能,它有点类似于订阅的方式,即客户端向服务端 注册 指定的 watcher
,当服务端符合了 watcher
的某些事件或要求则会 向客户端发送事件通知 ,客户端收到通知后找到自己定义的 Watcher
然后 执行相应的回调方法 。
当客户端在Zookeeper上某个节点绑定监听事件后,如果该事件被触发,Zookeeper会通过回调函数的方式通知客户端,但是客户端只会收到一次通知。如果后续这个节点再次发生变化,那么之前设置 Watcher 的客户端不会再次收到消息(Watcher是一次性的操作),可以通过循环监听去达到永久监听效果。
ZooKeeper 的 Watcher 机制,总的来说可以分为三个过程:
监听通知机制的流程如下:
分布式锁是雅虎研究员设计Zookeeper的初衷。利用Zookeeper的临时顺序节点,可以轻松实现分布式锁。
首先,在Zookeeper当中创建一个持久节点ParentLock。当第一个客户端想要获得锁时,需要在ParentLock这个节点下面创建一个临时顺序节点 Lock1。
之后,Client1查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock1是不是顺序最靠前的一个。如果是第一个节点,则成功获得锁。
这时候,如果再有一个客户端 Client2 前来获取锁,则在ParentLock下载再创建一个临时顺序节点Lock2。
Client2查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock2是不是顺序最靠前的一个,结果发现节点Lock2并不是最小的。
于是,Client2向排序仅比它靠前的节点Lock1注册Watcher,用于监听Lock1节点是否存在。这意味着Client2抢锁失败,进入了等待状态。
这时候,如果又有一个客户端Client3前来获取锁,则在ParentLock下载再创建一个临时顺序节点Lock3。
Client3查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock3是不是顺序最靠前的一个,结果同样发现节点Lock3并不是最小的。
于是,Client3向排序仅比它靠前的节点Lock2注册Watcher,用于监听Lock2节点是否存在。这意味着Client3同样抢锁失败,进入了等待状态。
这样一来,Client1得到了锁,Client2监听了Lock1,Client3监听了Lock2。这恰恰形成了一个等待队列,很像是Java当中ReentrantLock所依赖的AQS(AbstractQueuedSynchronizer)。
释放锁分为两种情况:
1.任务完成,客户端显示释放
当任务完成时,Client1会显示调用删除节点Lock1的指令。
2.任务执行过程中,客户端崩溃
获得锁的Client1在任务执行过程中,如果Duang的一声崩溃,则会断开与Zookeeper服务端的链接。根据临时节点的特性,相关联的节点Lock1会随之自动删除。
由于Client2一直监听着Lock1的存在状态,当Lock1节点被删除,Client2会立刻收到通知。这时候Client2会再次查询ParentLock下面的所有节点,确认自己创建的节点Lock2是不是目前最小的节点。如果是最小,则Client2顺理成章获得了锁。
同理,如果Client2也因为任务完成或者节点崩溃而删除了节点Lock2,那么Client3就会接到通知。
最终,Client3成功得到了锁。
下面的表格总结了Zookeeper和Redis分布式锁的优缺点:
有人说Zookeeper实现的分布式锁支持可重入,Redis实现的分布式锁不支持可重入,这是错误的观点。两者都可以在客户端实现可重入逻辑。
什么是 “可重入”,可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁
当某些数据由几个机器共享,且这些信息经常变化数据量还小的时候,这些数据就适合存储到ZK中。
本质上,统一配置管理和数据发布/订阅是一样的。
分布式环境下,配置文件的同步可以由Zookeeper来实现。
可能我们会有这样的需求,我们需要了解整个集群中有多少机器在工作,我们想对及群众的每台机器的运行时状态进行数据采集,对集群中机器进行上下线操作等等。
例如,集群机器监控:这通常用于那种对集群中机器状态,机器在线率有较高要求的场景,能够快速对集群中机器变化作出响应。这样的场景中,往往有一个监控系统,实时检测集群机器是否存活。过去的做法通常是:监控系统通过某种手段(比如ping)定时检测每个机器,或者每个机器自己定时向监控系统汇报“我还活着”。 这种做法可行,但是存在两个比较明显的问题:
利用ZooKeeper有两个特性,就可以实时另一种集群机器存活性监控系统:
如下图所示,监控系统在/manage节点上注册一个Watcher,如果/manage子节点列表有变动,监控系统就能够实时知道集群中机器的增减情况,至于后续处理就是监控系统的业务了。
多个相同的jar包在不同的服务器上开启相同的服务,可以通过nginx在服务端进行负载均衡的配置。也可以通过ZooKeeper在客户端进行负载均衡配置。
ZooKeeper负载均衡和Nginx负载均衡区别:
命名服务是指通过指定的名字来获取资源或者服务的地址,利用 zk 创建一个全局唯一的路径,这个路径就可以作为一个名字,指向集群中某个具体的服务器,提供的服务的地址,或者一个远程的对象等等。
阿里巴巴集团开源的分布式服务框架 Dubbo 中使用 ZooKeeper 来作为其命名服务,维护全局的服务地址列表。在 Dubbo 的实现中:
注意:所有向 ZooKeeper 上注册的地址都是临时节点,这样就能够保证服务提供者和消费者能够自动感应资源的变化。
另外,Dubbo 还有针对服务粒度的监控,方法是订阅/dubbo/${serviceName} 目录下所有提供者和消费者的信息。
另外,分布式锁和选举也是Zookeeper的典型应用场景。
参考资料地址: :https://blog.csdn.net/qq_37555071/article/details/114609145
如有问题,请联系作者修改!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。