欢迎关注公众号(通过文章导读关注:【11来了】),及时收到 AI 前沿项目工具及新技术的推送!
在我后台回复 「资料」 可领取
编程高频电子书
!
在我后台回复「面试」可领取硬核面试笔记
!文章导读地址:点击查看文章导读!
感谢你的关注!
一篇文章了解 ZooKeeper 底层运行原理
该文章主要说一下 ZooKeeper 底层运行原理,以及其中比较核心的功能
CAP 是分布式系统中的基本理论,一般只能满足 AP 和 CP,常用的注册中心 ZK 和 Nacos:
- ZooKeeper 符合 CP ,选择了一致性而抛弃了部分可用性 ,因为 ZK 集群中只有 Leader 节点可以写数据,如果 Leader 挂了,需要重新选举 Leader,在这期间 ZK 集群不可用,ZK 只有 Leader 可以写,所以整个写入操作是 中心化 的
- Nacos 符合 AP ,选择了可用性并且满足最终一致性 ,并且 Nacos 的设计是去中心化的
并且由于 ZK 的监听机制,导致在节点数量较多的时候,如果出现大量监听事件的触发,会导致 ZK 瞬时流量过大 ,因此,现在将 Zookeeper 作为服务注册中心的公司在减少,而是转向使用 Nacos 或自研注册中心
首先说一下 ZooKeeper 用来做什么
ZooKeeper 是一个分布式协调服务,提供了一个中心化的服务,用于管理和协调分布式应用中的各种配置、信息和事件
那么一般使用 ZooKeeper 中,较常用的场景就是 注册中心 、 元数据管理
元数据管理 指对多个分布式服务中都需要用到的基础类型的数据进行管理,使用 ZK 管理的好处就是不需要在每一个分布式服务中都进行配置,只要在 ZK 中进行配置即可,并且配置变更之后,所有的分布式服务可以及时收到 数据变更的通知
注册中心 指在那些需要进行 RPC 远程调用时,将 RPC 服务注册到 ZK 中去,之后需要调用的话,可以在 ZK 中查询提供了哪些服务可以进行调用,并且可以进行 负载均衡 处理
如下图,如果提供了一个 HelloServiceImpl 的 RPC 服务,将该服务注册在 ZK 中,节点值为该服务的 IP 地址,这样调用方在需要使用服务的时候,可以拿到该服务的 IP 地址,与该服务建立 TCP 连接即可进行通信(Dubbo 的注册中心可以使用 ZK 也可以使用 Nacos)
ZooKeeper 中的核心内容
ZooKeeper 中的核心内容如下:
- 节点类型
- watcher 监听机制
- 分布式锁
- 集群模式
接下来主要从这几块内容来介绍 ZooKeeper 的运行原理
节点类型
ZK 中的节点类型分为:持久节点、临时节点、顺序节点
持久节点:创建之后一直存在
临时节点:只要客户端断开连接,节点就会被删除
顺序节点:创建节点时,会添加全局递增的序号,经典应用场景是 分布式锁 (顺序节点既可以是持久节点也可以是临时节点)
节点使用应用场景: 在 RPC 调用中,将提供的 RPC 服务作为临时节点注册在 ZK 中,当 RPC 服务提供者与 ZK 断开连接之后,该服务在 ZK 中注册的临时节点就会被删除,那么我们客户端就可以对该节点添加监听器,当发现节点被删除之后,就可以感知到对应的服务下线了(监听器的内容会在后边说道)
ZooKeeper 的监听机制
ZooKeeper 提供了 watcher 监听机制,这是 ZK 中最常用的功能,因为多个分布式节点之间及时感知到 ZK 数据的变化就是通过 监听机制实现的
ZK 中的 watcher 有以下几个特点:
- 一次性: watcher 被触发之后,ZK会将其从客户端的 WatchManager 中删除,也会从服务端删除,重新注册 watcher 才可以继续下一次的监听
- 串行性: 同一 Node 的相同事件类型引发的 watcher 回调方法串行执行(也就是只有执行完w atcher 的回调,才可以重新生成 watcher 对象进行监听,如果回调执行时间太长,可能会导致监听事件的丢失,因此 ZK 也不适合在 watcher 回调中执行耗时的IO操作)
上边说的 watcher 监听是 ZK 中原生的监听机制,我们使用 ZK 都是使用它的客户端工具 Curator,该客户端工具中也封装了一些丰富的功能来提供给我们进行使用,如 分布式锁 、节点监听机制
这里说一下 Curator 提供的节点监听机制 ,提供了有 3 种类型的监听:
- NodeCache :监听指定节点(单个节点监听)
- PathChilrenCache :监听指定节点的子节点
- TreeCache :监听指定节点
那么对于 监听机制的使用 ,这里还是以 RPC 远程服务调用为例,如果每一个 RPC 服务提供者都会将自己的 IP 地址注册到对应的节点下,那么客户端只需要对 com.zqy.service.impl.HelloServiceImpl 这个节点下的子节点进行监听,就可以感知到哪些服务已经下线,就不再对该服务进行远程调用即可,那么这里就可以使用 PathChildrenCache 来进行监听(具体的代码示例这里就不写了,只讲原理)
分布式锁
ZK 作为分布式协调框架,用来作为分布式锁使用从他的设计目的上来讲是比较合适的,那么常用的分布式锁还有 Redis 的分布式锁(Redisson 客户端框架提供)
- 对于这两种锁到底要如何去选择使用呢?
其实使用哪一种都可以,两者提供的分布式锁功能都可以满足日常的使用,并且使用 Redis 集群和使用 ZK 集群都可以保证较好的锁的可用性
但是两者在 极端情况下也会出现问题 ,如在 Redis 集群加锁,写入主节点之后,如果锁的信息未来得及同步到从节点,此时主节点宕机,就会导致这个锁的信息丢失,会出现 重复加锁 的情况;在 ZK 集群中,如果客户端长时间 GC 导致无法与 ZK 维持心跳,ZK 会误认为该客户端下线,将该客户端加锁的临时节点删除,也会出现 重复加锁 的情况
那么可以看到在 功能性 和 可用性 上,两者其实差别都不大,具体选用的话,可以 根据当前项目使用的技术栈 来进行选择,比如当前项目中并没有引入 Redis 依赖,只有 ZK 依赖,那么直接使用 ZK 的分布式锁,完全没有必要去引入 Redis 集群来使用 Redis 的分布式锁,因为多一种技术,就多一份故障的风险,单单为了使用 Redis 分布式锁就去引入 Redis 集群显然小题大做了
- ZK 中的分布式锁实现
ZK 的分布式锁就是依靠 临时顺序节点 实现的,比如说创建一把名为 lock 的锁,那么所有需要加锁的客户端会到该节点下创建 临时顺序节点 ,只有第一个创建成功的客户端可以拿到锁,其他客户端创建的临时顺序节点在后边排序,并且对前一个节点进行 监听 ,当监听到锁释放,自己就拿到了锁(可以了解一下 Curator 对读写锁出现的羊群效应的解决)
ZooKeeper 集群
接下来说一下 ZK 的集群部署,ZK 集群中通过分布式一致性协议 ZAB 来保证数据同步的一致性,以及 Leader 选举、集群的崩溃恢复,接下来会主要 围绕 ZAB 协议 来说一下 ZK 集群相关的核心内容:
ZK 集群中有三种角色:Leader、Follower、Observer
- Leader :执行写操作,并且向 Follower 进行同步
- Follower :从 Leader 同步数据,执行读操作,不可以执行写操作
- Observer :只可以执行读操作
ZK 集群其实是适合 写少读多 场景的,因为整个集群只有 1 个 Leader 可以写,其他节点只可以读,那么你可能有疑问,Follower 节点用来读数据,为什么还需要 Observer 节点呢?
这是因为在集群中 Leader 完成写请求是需要经过半数以上的 Follower 都 Ack 之后,才可以成功写入的,如果集群中 Follower 过多,会大大增加 Leader 节点等待 Follower 节点发送 Ack 的时间,导致 ZK 集群性能很差,因此 ZK 集群部署都是 小集群部署 ,一般都是 3 台或者 5 台机器
Observer 节点Observer 是只读的、不参与 Leader 选举、也不参与 ZAB 协议同步时过半 Ack 的环节,只是单纯的接收数据,同步数据,达到数据顺序一致性的效果,当读并发请求过高时,可以 通过不断添加 Observer 节点来分散读请求的压力
- ZK 的分布式一致性协议 ZAB 保证数据同步的一致性
ZAB 协议中是采用 2PC 两阶段提交思想完成数据写入的:
采用 2PC 两阶段提交思想 的 ZAB 消息广播流程:
每一个消息广播的时候,都是基于 2PC 的思想,先是发起事务提议 Proposal 的广播,各个 Follower 返回 Ack,当过半的 Follower 都返回 Ack 之后,Leader 就发送 Commit 消息到 Follower,让大家提交事务
这里的两阶段指的就是发送 Proposal 和 Commit !
发起一个事务 Proposal 之前,Leader 会分配一个全局唯一递增的事务 id(zxid),以此来严格保证顺序
Leader 会为每个 Follower 创建一个队列,里边存放要发给 Follower 的事务 Proposal,保证了一个同步的顺序性
Follower 收到事务 Proposal 之后,就立即写入本地磁盘日志中,写入成功后数据就不会丢失了,之后返回 Ack 给 Leader,当过半的 Follower 都返回 Ack,Leader 推送 Commit 消息给全部 Follower,让大家进行事务提交,事务提交之后,数据就被写入到了 znode(也就是内存中) ,此时数据就可以被用户感知到了
- ZK 的 ZAB 保证集群的崩溃恢复
下边将会介绍 ZK 集群 启动 再到 崩溃 再到 恢复 整体的流程:
ZK 集启动的时候,进入 恢复模式 ,选举一个 Leader 出来,然后 Leader 等待集群中过半的 Follower 跟他进行数据同步,只要过半的 Follower 完成数据同步,接着就退出恢复模式,可以对外提供服务了
此时,还没完成同步的 Follower 会自己去跟 Leader 进行数据同步的
之后会进入 消息广播模式 ,只有 Leader 可以接受写请求,但是客户端可以任意连接 Leader 或者 Follower,如果客户端连接到 Follower,Follower 就会将写请求转发给 Leader
Leader 收到写请求,就把请求同步给所有的 Follower,当超过半数的 Follower 都返回了 Ack,之后 Leader 先将数据写到自己的 znode 中,再给所有的 Follower 发一个 Commit 消息,让大家提交这个请求事务,Follower 收到 Commit 消息后,就会将磁盘中刚刚写入的数据往内存中的 znode 中写,之后客户端就可以读取到数据了
如果 Leader 宕机了,就会进入 恢复模式 ,重新选举一个 Leader,只要获得了过半的机器的投票,就可以成为 Leader
ZK 集群中可以容忍不超过一半的机器宕机,就比如说一个集群有 3 台机器,那么最多允许 1 台机器宕机,剩下的 2 台选举 Leader,只要 2 台机器都认可其中一台机器当 Leader,也就是超过了集群一半的机器都认可,那么就可以选举这台机器作为 Leader
新的 Leader 等待过半的 Follower 跟他同步,之后重新进入 消息广播模式
以上就是 ZK 集群恢复崩溃的整个流程了,当然我也画了一个流程图,更方便观看,如下:
主要就是分为 3 个阶段:
-
集群启动时:恢复模式,Leader 选举 + 数据同步
-
消息写入时:消息广播模式,Leader 采用 2PC 的过半写机制,给 Follower 进行同步
-
崩溃恢复:恢复模式,Leader/Follower 宕机,只要剩余机器超过一半,就可以选举新的 Leader
-
ZK 集群的性能瓶颈在哪里呢?
瓶颈在于 Leader 的 写性能
,如果 ZK 集群挂掉的话,那么很有可能就是 Leader 的写入压力过大,这对一个公司的技术平台打击是巨大的,因为像 kafka 之类的技术都是强依赖 ZK 的,Dubbo + ZK 去做服务框架的话,当服务实例达到上万甚至几十万时,大量服务的上线、注册、心跳的压力达到了每秒几万甚至十万,单个 Leader 抗几万的请求还行,十几万的话 ZK 单个 Leader 是扛不住这么多的写请求的
想要提升 Leader 的 写性能 ,目前来说也就是提升部署 ZK 的机器性能了,还有一种方式也就是将 dataLogDir 目录挂载的机器上配置 SSD 固态硬盘,以此来提升事务日志 写速度 来提升写性能!
- ZK 集群的 Leader 选举
在 Leader 选举时,需要用到以下几个属性:
SID :服务器 ID,和 myid 一致,作为服务器的标识,不可以重复
ZXID :事务 ID,用来标识一次服务状态的变更,ZXID 全局唯一并且递增,有 64 位,高 32 位时 Leader 的 Epoch,低 32 位是递增计数器
Epoch :每个 Leader 任期的代号(没有 Leader 时,同一轮投票过程中的 Epoch 时相同的,每投完一次票,就会加 1)
当一台机器进入 Leader 选举时,当前集群可能处于以下两种状态:
- 集群中已经有一个 Leader
如果已经有 Leader 的情况,会被告知当前服务器的 Leader 信息,之后进行状态同步即可
- 集群中确实不存在 Leader
假设 zookeeper 有 5 台服务器,SID 分别为 1、2、3、4、5,ZXID 分别为 8、8、8、7、7,并且 SID = 3 的服务器是 Leader
如果某一时刻服务器 3 和 5 出现故障,之后服务器 1、2、4 开始 Leader 选举,规则如下:
1、EPOCH 大的选举为 Leader
2、EPOCH 相同,事务 ID 大的选举为 Leader
3、事务 ID 相同,SID 大的选举为 Leader
那么对于服务器 1、2、4 来说,最终服务器 2 选举为 Leader, 这样的选举策略可以保证有 最新数据的节点 可以竞选