整合网上资料,有不足之处请指正。
目录
Zookeeper
zookeeper是一个分布式协调服务,可以用于元数据管理、分布式锁、分布式协调、发布订阅、服务命名等。
**简短介绍zk:**它是个数据库,文件存储系统,并有监听通知机制(观察者模式)。
分布式数据一致性
附上链接:https://zhuanlan.zhihu.com/p/35616810
分布式事务
保证在多台物理机的数据库上的操作具有原子性,要么都提交,要么都回滚。
两阶段提交(2PC)
既是算法,也是协议。参与者将操作结果通知协调者,再由协调者根据所有参与者的结果决定是否提交操作或者终止操作。
- 第一阶段:投票阶段------事务协调者给每个参与者发送prepare消息,每个参与者需要返回结果,失败则直接返回,成功则需在本地执行事务,写redo和undo日志,但不提交,响应后,等待协调者的通知。
- 第二阶段:提交阶段------协调者收到参与者的失败消息或者超时,直接给每个参与者发送回滚消息,否则返回提交消息。参与者根据协调者指令执行对应操作,释放所有事务处理过程中使用的锁资源,并反馈事务执行结果。
缺点:
-
同步阻塞
所有参与节点都是事务阻塞型。
-
单点故障
协调者挂掉,参与者会一致阻塞下去,即使选举出新的协调者,也无法解决因为宕机导致的参与者处于阻塞的问题。
-
数据不一致
由于网络或者协调者自身的问题,导致在提交阶段,只有部分参与者收到了commit消息,导致整体数据不一致。
-
存在无法解决的问题
协调者发出commit消息后宕机,恰巧参与者此时也宕机了,即使有新的协调者被选举出,也无法知晓这条事务是否已经提交。
三阶段提交(3PC)
阶段分为:
- Can-Commit:询问参与者是否具备执行能力。
- Pre-Commit:如果参与者都具备执行能力,则协调者发送该请求,参与者写入redo和undo,但不提交,并响应,等待协调者后续操作。如果有一个参与者不具备执行能力或者超时,则执行事务中断。
- Do-Commit:与2PC差不多。
在2PC的基础上有两个改动点:
- 引入超时机制。同时在协调者和参与者都引入超时机制。
- 将2PC的第一阶段拆分为两个阶段(canCommit,preCommit),保证在最后提交阶段之前各参与节点状态的一致。
为什么要拆分第一阶段?
如果所有参与者中有一个参与者不能执行提交,而2PC在第一阶段时其余参与者已经写了undo和redo,协调者最后因为这一个参与者执行回滚,消耗很大。所以执行pre的前提是所有参与者都具备可执行条件,减少资源消耗。
2PC和3PC的区别:
与2PC相比,3PC主要解决了单点故障,并减少阻塞。参与者一旦接收不到协调者的信息,会默认执行commit,而不会一直持有事务。
但是要是协调者发出的是中断请求,但由于网络原因,导致部分参与者没有接受到,那么它们会默认执行commit,所以还是会有数据不一致的情况出现,接下来就引入了Paxos算法。
Paxos算法
待研究…
zookeeper使用哪种呢?
zookeeper使用2PC来保证节点之间的数据一致性,但是由于leader需要和所有的follower交互,这样一来通信的开销会变得较大,zookeeper的性能就会下降。所以为了提升性能,采取ACK过半即可。
顺序一致性
zookeeper集群是读写分离的,当leader节点接收到消息之后,会按照请求的严格顺序一一进行处理,它会保证消息的顺序一致性。
如果消息A比消息B先到达,那么在所有的zookeeper节点中,消息A都会先于消息B到达,zookeeper保证了消息的全局顺序。
zookeeper如何保证消息的顺序?
那就是zxid
节点之间会通过发送proposal(提案)来进行通信、数据同步,proposal中就会带上zxid和具体的数据(Message)。
而zxid由两部分组成:
- epoch(高32位):leader的迭代版本,每个leader的epoch都不一样
- counter(低32位):计数器,来一条消息就会自增
这是唯一zxid生成算法的底层实现,由于每个leader所使用的epoch都是唯一的,而不同的消息在相同的epoch中,counter值是不同的,所以所有的提案在zookeeper集群中都有唯一的zxid。
ZAB协议
参考链接https://www.jianshu.com/p/b82aaed5389c
即原子广播协议,支持崩溃恢复的原子广播协议,主要用于实现数据一致性。
zookeeper可以在恢复和广播模式之间切换,当leader正常时,进入广播模式,不正常时,进入恢复模式。
三种角色
- follower
- leader
- Observer
本质上follower和observer的功能是一样的,都为zookeeper提供了横向扩展的能力,使其能够抗住更多的并发,但区别在于observer不参与投票选举。而且只有leader处理写请求,follower和observer不会处理,当写请求到达follower或者observer时,会将写请求转发给leader
observer到底是干什么的?有什么作用呢?
observer的设计是希望能动态扩展zookeeper集群又不会降低写性能。随着投票的成员增多,写入性能会有所下降,因为投票需要集群中至少一半的节点投票达成一致,因此投票成本会增加。而observer只会听取投票结果,不参与投票。由于这一点,我们可以增加任意数量的observer,同时不会影响我们集群的性能。也就说他不会影响集群的可用性,对用户来说,相比follower,可以用observer更能通过不可靠的网络连接。
启动流程
- 统一由QuorumPeerMain作为启动类,加载解析zoo.cfg配置文件
- 初始化核心类:ServerCnxnFactory(IO操作)、FileTxnSnapLog(事务日志及快照文件操作)、QuorumPeer实例(代表zk集群中的一台机器)、ZKDatabase(内存数据库)等
- 加载本地快照文件及事务日志,恢复内存数据
- 完成leader选举,节点间通过一系列投票,选举产生最合适的机器成为leader,同时其余机器成为follower或是observer。关于选举算法,就是集群中哪个机器处理的数据越新(通过ZXID来比较,ZXID越大,数据越新),其越有可能被选中
- 完成leader与learner间的数据同步:集群中节点角色确定后,leader会重新加载本地快照及日志文件,以此作为基准数据,再结合各个learner的本地提交数据,leader再确定需要给具体learner回滚哪些数据及同步哪些数据
- 当leader收到过半的learner(不太准确,确切地说是follower,observer只会同步leader的数据)完成数据同步的ACK,集群开始正常工作,可以接收并处理客户端请求,在此之前集群不可用
工作原理
- 客户端首先向zookeeper集群的任意节点发起写请求(事务)
- 如果节点是follower/observer类型,就将请求转发给leader节点。
- leader节点接收消息之后对消息进行处理
- 生成递增的全局唯一的zxid(日志)
- 将带有zxid的消息包装成一个提案,并转发给所有follower节点
- follower将这个提案用本地事务进行持久化后,给leader反馈ack
- leader统计ack数量
- 如果有过半的ack,则视为成功,即广播commit消息。(在源码中仅仅是循环遍历follower节点,然后通过消息队列发送)
- 否则,返回失败,即follower抛弃leader服务器。注意分布式事务才有回滚
- 最后响应客户端
崩溃恢复
触发恢复的两种情况:
- leader挂掉
- 集群过半的follower节点挂掉
选主原则:
- zxid越大越好(主要)
- myid越大越好(次要)
当其中有超过一半的选票选同一台服务器,即任命其为leader,即在最终一致的前提下,尽量保证强一致性。
一致性原则:
-
已经被处理的消息不能丢弃(commit后)
因为每次提交的事务都有一个zxid(全局唯一,递增),因此我们只需要找出所有机器内zxid最大的事务(既该事务是最后一个被提交的事务)并且把存放该zxid的机器选举为leader即可。
-
已经被丢弃的消息不能再次出现(commit前)
之前宕机的leader节点重新启动之后若再次被选为Leader,要把之前没有commit的事务重新commit,而当前的epoch大于该事务的epoch所以事务会被丢弃而不会被重新加载。也就是只有当事务zxid的epoch和当前的epoch相同时,事务才会被提交。
新选举的leader后,epoch会自增,并且将counter重置为0。
leader选举完成后,服务依然是不可用的,因为还没有做数据同步。
此后,leader会等待其余的follower来连接,然后通过proposal向所有的follower发送其缺失的数据。
至于缺失哪些数据?
proposal本身是要记录日志,通过proposal中的zxid的低32位的counter值,就可以做一个diff
这里还有一个优化,如果缺失的数据太多,一条一条地发送proposal效率太低。所以leader发现缺失的数据太多时会将当前的数据打个快照,直接打包发送给follower。
leader会发送一个NEW_LEADER的proposal给follower,当且仅当该proposal被过半的follower返回ACK后,leader才会commit这个提案(磁盘到内存),之后集群才会正常进行工作。
为什么集群通常是奇数的?
根据过半机制来说,只要有超过一半的节点能够成功投票,这个集群就是可用的。
假设有两个集群分别有3个和4个节点,此时各挂掉一个节点,两个集群仍然可用,效果是一样的(注意不要钻牛角尖,过半针对的整个集群的,不是当前存活的,节点是根据zoo.conf来读取整个集群的节点的)。所以为了节省集群的资源使用,采用奇数个。
内部运行机制
ZNode
数据存储的最小单元,ZNode中维护了一个数据结构,用于记录ZNode中数据更改的版本号以及ACL(Access Control List)的变更。
有了这些数据的版本号以及更新的时间戳,zookeeper就可以验证客户端请求的缓存是否合法,并协调更新。
当客户端执行更新或删除操作时,都要带上对应的版本号。如果版本号对不上,则不执行。
机制
zookeeper的文件系统中每个文件都是节点,因层级关系,所以会形成一个文件数。
zookeeper的znode也有几种类型:
-
持久节点
create /node_name
除非主动删除,否则一直存在。
-
持久顺序节点
create -s /node_name
除了持久节点的特性,还会保证子节点的先后顺序,并会自动地为节点加上10位自增序列号作为节点名,以此来保证节点名的唯一性。
-
临时节点
create -e /node_name
生命周期和client连接相关,一旦连接断开(session过期也算),节点就会被删除。并且无法创建子节点。
-
临时顺序节点
create -e -s /node_name
除了临时节点的特性之外,还会在后缀加上自增数字。
ACL
ZooKeeper提供了一套完善的ACL权限控制机制保障数据安全性。
- 对于身份认证,提供了以下几种方式。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qaFsgBiL-1631860207873)(C:\Users\11985\AppData\Roaming\Typora\typora-user-images\image-20210713174647027.png)]
- 对于znode权限,提供了以下5种操作权限。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xkqVglsC-1631860207875)(C:\Users\11985\AppData\Roaming\Typora\typora-user-images\image-20210713174611282.png)]
用途
元数据管理
Kafka在运行时会依赖一个zookeeper集群,并通过zookeeper来管理集群相关的元数据以及leader的选举。
Zookeeper分布式锁
本质: 通过创建临时顺序节点来实现分布式锁。
**原理:**当客户端加锁成功后,实际上是在zookeeper上创建了临时顺序节点。当客户端成功创建节点之后,还会获取同级的所有节点。此时客户端会根据自增号判断自己当前节点是否是最小的节点,如果是最小的,则获取到了分布式锁。如果不是最小的,则会等待锁的释放,即客户端会对锁对象注册一个监听器,对该节点的任意更新都会触发对应操作。当被监听的节点被删除时,就会唤醒客户端并再次判断自己的节点是否最小。
但是这样会因为同时监听一个节点,导致,每次变动都会通知所有服务器,而只有一个能够成功加锁,所以会加重带宽和资源消耗。
优化就是将监听最小的节点,改为监听前一个节点,当前一个节点删除时,即释放监听,再去到下一个循环,判断自己是否是序号最小的节点,如果是,则加锁成功,否则继续监听自己的新的前一个节点。
分布式协调
之前提到的数据一致性,是commit还是rollback。
发布订阅
利用监听器(Watch)功能,一旦发布消息,可以立即动态更新。
命名服务
- 利用zookeeper的文件系统特性,存储结构化文件。
- 利用文件特性和顺序节点特性,来生成全局的唯一标识。
前者可以用于在系统之间共享某种业务上的特定资源,后者则可以用于实现分布式锁。
Session
当zookeeper的客户端和服务器建立连接(长连接)之后,客户端会拿到一个64位的SessionID和密码。
SessionID可以理解,那为什么会有密码呢?
zookeeper可以部署多个实例,如果客户端断开连接又和另外的zookeeper服务器建立连接,那么在建立连接时会带上这个密码。该密码是zookeeper的一种安全措施,所有zookeeper节点都可以对其进行验证,所以即使连上了别的zookeeper节点,session一样有效。
Session过期
session过期有两种情况:
-
过了指定的失效时间
过期时间的范围只能在2倍的tickTime到20倍的tickTime之间
-
指定时间内客户端没有发送心跳
zookeeper中的心跳是通过ping请求来实现的,每隔一段时间,客户端都会发送ping请求到服务器,这就是心跳的本质。心跳不仅可以让服务端感知客户端还存在,同样让客户端感知与服务端的连接还是有效的。
tickTime是zookeeper的配置项,用于指定客户端向服务器发送心跳的时间。默认是2秒。
Watch
基础
给某个节点注册监听器,该节点一旦发生变更(更新或删除),监听者就会收到一个Watch Event。
watch的类型:
-
一次性watch
被触发后,watch就会移除。
-
永久性watch
被触发后,仍然保留,可以继续监听ZNode上的变更,是zookeeper3.6.0版本新增的功能
一次性watch的问题
在watch触发事件到达客户端、再到客户端设立新的Watch时,是有一个时间间隔的,并且这个时段,客户端无法感知。
缺点
- 非高可用:极端情况下zk会丢弃一些请求,机房之间连接出现故障。
- Zookeeper master只能照顾一个机房,其他机房运行的业务模块由于没有master都只能停掉,对网络抖动非常敏感。
- 选举过程很慢而且此时无法对外提供服务。
- zk的性能有限,承受不住巨大的流量,必须使用缓存来缓解。
- zk本身的权限控制非常薄弱
- 羊群效应:所有客户端尝试对一个临时节点加锁,当该节点变更时,会通知所有客户端,并进行新一轮的加锁,这样会极大的消耗网络带宽。详情见Zookeeper分布式锁
- zk的读取操作,读取到的可能是过期的旧数据。它只能保证最终一致性,尽量去靠拢强一致性。
脑裂
由于假死会导致新的一轮选举,此时旧的leader网络通了,导致出现了两个leader,有的客户端连接到老的leader,而有的客户端连接到新的leader。
旧leader想要向其他follower发送写请求是会被拒绝的,因为epoch已经自增,当然也有别的follower不知道新leader的产生,但是仍然占少数,因为多数的话,新leader不会产生。并且因为过半机制,得不到多数的支持,写无效。