本文参考 <从Paxos到Zookeeper分布式一致性原理与实践 > <ZooKeeper分布式过程协同技术详解>
为什么说ZK是一个CP系统?
- 在选主期间整个集群不可用
- 在选主后的数据同步完成之前整个集群不可用
- 每次写请求,保证大于半数的节点写成功(一致性保证)
ZK适合用来做注册中心吗?
- 不太合适. ZK是一个CP系统,在选主及数据同步期间整个集群不可用.作为一个注册中心,可以容忍短暂的数据不一致(如服务列表),但要保证高可用
ZK集群为什么推荐节点的个数为奇数?
- 这其实并不是必须的,只不过使用偶数会使得系统更加脆弱.
- 假如现在有4个节点,根据过半原则,需要存活的节点数为3,即只有一个节点可以崩溃; 假如集群中只有3个节点,根据过半原则,存活的节点数需要为2,也是只允许1个节点可以崩溃, 4保3 和 3 保2,哪个更难?
ZK有什么缺点?
- 太复杂
- 选主过程很慢,大概要30-120s,整个选主期间集群不可用.ZK对网络非常敏感,一旦出现网络隔离就要发起选举流程,很容易将短暂的网络问题时间放大(选主)
- 无法跨机房,因为只有一个master节点,所以高可用上存在不足
- ZK集群伸缩伸缩性不太灵活,集群中所有机器ip及port都是事先配置在每个服务的zoo.cfg 文件里,如果要往集群增加一个follower节点,首先需要更改所有机器的zoo.cfg,然后逐个重启
什么是脑裂?
- 因为一些特殊原因,例如主节点负载很高,导致消息任意延迟,然后备份节点接管主节点的工作,成为第二个主要主节点
- 如果一些从节点无法与主节点通信,如由于网络分区错误导致,这些从节点可能会停止与主要主节点通信,而与第二个主要主节点接力主从关系
- 即: 系统中两个或者多个部分开始独立工作,导致整体行为不一致性
主从结构问题
- 客户端向主节点派发任务,主节点将有效的任务派发到从节点. 从节点接收到任务,执行完这些任务后向主节点报告执行状态,然后主节点将执行结果通知给客户端
- 主节点崩溃: 系统将无法分配新的任务或重新分配已失败的任务
- 从节点崩溃: 已分配的任务将无法完成
- 通信故障: 如果主节点和从节点之间无法进行信息交换,从节点将无法得知新任务分配给它
- 主节点选举 崩溃检测 组成员关系管理 元数据管理
应用场景
- 数据发布/订阅
- 负载均衡
- 命名服务
- 分布式协调/通知
- 集群管理
- Master选举
- 分布式锁
- 分布式队列
- 集群内的主节点选举,保存集群的元数据,用于跟踪可用的服务器 Hadoop
- 实现Topic的发现,检测崩溃,保存主题的生产和消费装填 Kafka
数据发布/订阅
即所谓的配置中心.发布订阅一般有两种设计模式,分别为: Push模式和Pull模式. ZK采用推拉模式相结合的方式: 客户端向服务端注册自己需要监听的节点,一旦该节点数据发生变更,服务端向客户端发送Watcher事件通知,客户端收到通知之后主动向服务端获取最新数据.
基于ZK配置中心的配置信息有如下特点:
- 数据量通常比较小
- 数据内容在系统运行时一般会发生动态变化
- 集群中各节点共享,配置一致
负载均衡
负载均衡分为硬件负载均衡和软件负载均衡,可以利用ZK是实现一些软负载均衡的效果
- 基于ZK的自动化动态DNS方案
命名服务
在分布式系统中,被命名的实体通常可以是集群中的机器 提供的服务地址或远程对象,我们都可以称它们为Name. 如RPC中的服务地址列表,通过使用命名服务,客户端能够根据指定名字来获取资源的实体 服务地址 提供者信息
- 注册中心
- 利用顺序节点生成全局唯一ID
分布式协调/通知
- MySQL数据复制
复制任务注册节点; 任务执行节点注册节点(任务热备份 任务切换);
集群管理
- 集群监控: 对集群运行状态的收集
- 集群控制: 对集群的操作
可能会有以下需求:
- 集群中目前有多少节点在工作
- 对集群中每个节点运行状态的收集
- 对集群中的机器进行上下限操作
方案:
- 部署独立Agent方式: 大规模升级困难 编程语言多样性
- ZK: Watcher机制 和 临时节点特性
场景:
- 分布式日志收集系统
- 在线云主机管理
Master选举
- ZK可以保证客户端无法创建一个重复的数据节点
- 利用Watcher和临时特点特性,在Master宕机之后,其它节点将重新选主
分布式锁
基于ZK可以实现分布式独占锁和共享锁
独占锁
- 基于Watcher机制和临时节点特性
- 创建临时节点成功,代表获取锁; 释放锁的时候删除临时节点
- 没有获取锁的机器监听该临时节点的删除事件,当该临时节点删除之后,重新参与竞争(即创建临时节点)
共享锁
- 基于Watcher机制和临时顺序节点特性
- 读请求的时候判断比自小的序号节点中是否有写请求节点
- 写请求的时候判断自己是不是序号最小的节点
有什么问题?
- 羊群效应
当整个集群中机器规模比较大的时候(大于10),每次只有一个节点能获得锁,却要对所有的机器发送Watcher通知,这会对ZK服务器造成巨大的性能影响和网络冲击
改进: 只对比自己序号小的最后那个节点进行监听
分布式队列
FIFO队列
- 基于临时顺序节点和Watcher机制
- 如果自己不是最小节点,则等待,同时向自己序号小的最后一个节点注册Watcher监听
ZNode
- 持久
- 持久有序
每个父节点都会为它的第一级子节点维护一份顺序,用于记录每个子节点创建的先后顺序. 基于这个特性,可以设置这个标志,那么在创建节点过程中,zk会自动为给定节点加上一个后缀,作为一个新的 完整的节点名
- 临时
客户端会话失效,节点会被自动清理,不能基于临时节点来创建子节点,临时节点只能作为叶子节点
- 临时顺序
Stat属性
Stat数据结构里面维护了一些节点的详细信息,Stat中记录了这个ZNode的三个数据版本,分别是: dataVersion(当前ZNode的版本) cversion(当前ZNode子节点的版本) aclVersion(当前ZNode的ACL版本)
czxid:引起这个znode创建的zxid,创建节点的事务的zxid(ZK Transaction Id);
ctime:znode被创建的毫秒数(从1970年开始);
mzxid:znode最后更新的zxid;
mtime:znode最后修改的毫秒数(从1970年开始);
pZxid:znode最后更新的子节点zxid;
cversion:znode子节点变化号,znode子节点修改次数;
dataversion:znode数据变化号;
aclVersion:znode访问控制列表的变化号;
ephemeralOwner:如果是临时节点,这个是znode拥有者的session id。如果不是临时节点则是0;
dataLength:znode的数据长度;
numChildren:znode子节点数量;
ACL
ZK采用ACL(AccessControlLists)策略来进行权限控制,类似于 UNIX 文件系统的权限控制。使用场景:开发/测试环境分离,开发者无权操作测试库的节点,只能查看;生产环境上控制指定IP的服务可以访问相关节点,防止混乱。
ACL命令
- getAcl: 获取某个节点的ACL权限信息
- setAcl: 设置某个节点的ACL权限信息
- addauth: 输入认证信息,注册时输入明文注册,ZK中对密码时加密的
ACL构成
通过[scheme:id:permissions]来构成权限列表,和我们平时系统种的权限管理很像
- scheme: 采用的某种权限机制
- id: 访问的用户
- permissions: 权限列表
scheme
- world: 匿名访问. world形式下只有一个用户: anyone
- auth: 密码是明文
- digest: 密码是密文
- ip: 通过限制IP访问
- supper: 超级管理员工,需要修改配置文件并重启ZK
permissions
ZK中定义了如下5种权限(crwda),其中尤其需要注意的是: CREATE和DELETE这两种权限都是针对子节点的权限控制
- CREATE:创建子节点的权限
- READ:获取节点数据和子节点列表的权限
- WRITE:更新节点数据的权限
- DELETE:删除子节点的权限
- ADMIN:创设置子节点ACL的权限
ACL相关操作
getAcl /test
setAcl /test world:anyone:rda
addauth digest immoc:immoc
setAcl /test auth:immoc:immoc:rdwa
addauth digest immoc:immoc
setAcl /test digest:immoc:密码加密后的密文:rdwa
setAcl /test ip:192.168.11.223:rdwa
super
修改zkServer.sh配置文件,然后重启ZK
基本命令
ls /test
stat /test
ls2 /test
create /test test
create -e /test/temp test-temp
create -s /test/seq seqqqq
delete /test
set /test new-test
set /test new-test 0
stat /test watcher
get /test watcher
ls /test watcher
getAcl /test
setAcl /test world:anyone:rda
addauth digest immoc:immoc
setAcl /test auth:immoc:immoc:rdwa
addauth digest immoc:immoc
setAcl /test digest:immoc:密码加密后的密文:rdwa
setAcl /test ip:192.168.11.223:rdwa
四字命令
一些与服务器交互的简写命令
echo mntr | nc localhost 2181
Watcher
ZK允许用户在指定节点上注册一些Watcher,并且在一些特定事件触发的时候,ZK服务端会将事件通知到感兴趣的客户端上去,该机制是ZK实现分布式协调服务的重要特性,可用于统一资源配置
- 针对每个节点的操作,都会有一个watcher
- 当监控的某个节点发生变化,则触发watcher事件
- ZK中的watcher是一次性的,出发后立即销毁
- 父节点子节点都可以触发watcher事件
- 针对不同的操作类型,触发watcher的事件也不同: 增 删 改
Watcher事件
stat /test watcher
get /test watcher
- 创建父节点触发:NodeCreated;
- 修改父节点触发:NoeDataChanged;
- 删除父节点触发:NodeDeleed;
ls /test watcher
- ls为父节点设置watcher,创建子节点触发:NodeChildrenChanged;
- ls为父节点设置watcher,删除子节点触发:NodeChildrenChanged;
会话
简单来讲,ZK的连接与会话就是客户端通过实例化Zookeeper
对象来实现客户端与服务端创建并保持TCP连接的过程.
会话状态
- NOT_CONNECTED
- CONNECTING: 一旦客户端开始创建
Zookeeper
对象,客户端状态就变成CONNECTING - CONNECTED: 成功连接上服务端,客户端状态就变成CONNECTED
- CLOSED: 会话超时 权限检查失败 客户端主动退出,客户端状态就变成CLOSED
- 客户端初始连接到集群中某一个服务器或一个独立的服务器. TCP长连接
- 当会话无法与当前连接的服务器继续通信时,会话就可能转移到另一个服务器上
- 会话提供了顺序保障,这就意味着同一个会话中的请求会以FIFO(先进先出)顺序执行
- 会话ID: 0x13b6fe376cd0000
- 会话状态: NOT_CONNECTED CONNECTING CONNECTED CLOSED
- ZK服务器会在本地处理只读请求,写请求会转发给leader
- 所有的变更处理需要以原子方式执行
- 在每个服务器中启动一个单独的线程来处理事务,通过单独的线程来保障事务之间的顺序执行互不干扰
- 每个事务对应一个zxid
- 群首将每一个写请求转换为一个事务,将这些事务发送给追随者,确保集群按照群首确定的顺序接受并处理这些事务
- 在接收到一个写请求操作后,追随者会将请求转发给群首,群首将探索性地执行该请求,并将执行结果以事务的方式对状态更新进行广播
核心概念
- sessionID: 会话ID. 用来唯一标识一个会话,每次客户端创建新的会话时,ZK都会位其分配一个全局唯一的sessionID
- timeOut: 会话超时时间.
注意,连接断开并不代表会话失效,只要在sessionTimeOut时间内重连成功该会话还是有效
客户端的两种异常
- CONNECTION_LOSS:
有时因为网络闪断导致客户端与服务器断开连接,或是因为客户端当前连接的服务器出现问题导致连接断开,我们称这种现象为
客户端与服务端断开连接
现象. 这种情况客户端会自动从地址列表中重新逐个选取新的地址并尝试进行重新连接,直到最终成功连接到服务器. - SESSION_EXPIRED
Leader选举
- ZK服务器有3种角色: Leader Follower Observer. Observer不参与选举过程,只接收读请求,提高集群吞吐量
- 每个Server启动后进入
LOOKING
状态,开始选举一个新的Leader或查找已经存在的Leader. 如果Leader已经存在,其他Server就会通知这个新启动的Server谁是Leader,然后与Leader建立连接,以确保自己的状态与Leader一致 - 如果集群中所有的Server均处于
LOOKING
状态,这些Server之间就会进行通信来选举一个Leader. 在本次选举过程中胜出的Server将进入LEADING
状态,而集群中其他Server将会进入FOLLOWING
状态 - 选举期间,整个集群不可用
三中角色的作用如下:
Leader
- 事务请求(写请求)的唯一调度和处理者,保证集群处理的顺序性;
- 集群内部各服务器的调度者
Follower
- 处理客户端非事务请求(读请求),转发事务请求给Leader
- 参与事务请求Proposal的投票
- 参与Leader选举投票
Observer
- 处理客户端非事务请求(读请求),转发事务请求给Leader
- 不参与投票,不参与选举
初始启动选举模式:
- 每个Server发出一个投票,投票内容为: Server的
myid
和其对应的ZXID
,默认Server就是它自己 - 接收来自各个服务器的投票,每个服务器都会接收来自其他服务器的投票,会做一些前置校验: 检查是否是本轮投票 是否来自
LOOKING状态
的服务器 - 处理投票: 收到其他Server的投票后,需要将别人的投票和自己的投票进行PK. 本质上就是: 先比较
ZXID
, 然后比较myid
. 小的那一方需要将大的数据更新为自己的状态. 最后各个Server再次将投发出 - 统计投票: 每次投票后,服务器会统计所有的投票,判断是否已经有过半的机器接收到相同的投票信息
- 改变Server状态: 一旦确定了Leader,就需要改变Server状态: Leading Following
Leader恢复选举模式, 即Leader运行中突然宕机,然后需要重新选举Leader
- 更新状态: Leader宕机之后,余下的非Observer节点将改变自己的状态为
LOOKING
- 每个Server发出一个投票: 需要注意的是在运行期间,每个服务器的
ZXID
可能不同,各个Server根据自己的ZXID
和myid
生成投票信息发给其他服务器 - 相互接收来自各个Server的投票
- 处理投票并统计投票
- 改变Server状态
数据与存储
- 初始化 => Leader选举 => 数据同步
- 分为内存数据存储与磁盘数据存储
内存数据
- ZK的数据模型是一棵树,类似于内存数据库,在该内存数据库中,存储了整棵树的内容. ZK会定时将这个数据存储到磁盘上
- DataTree
- ZKDatabase
事务日志
- 每个事务日志文件的大小相同,都为64MB
- 每个事务日志文件名为该文件第一条事务记录的ZXID
- 事务日志文件会采取磁盘空间预分配策略,在文件创建之初就向操作系统分配一个很大的磁盘块,默认是64MB,一旦分配的文件空间不足4KB,将会再次
预分配
.目的是避免磁盘Seek的频率,提高磁盘I/O效率 - 事务消息包括事务头和事务体,写文件之前需要分别对事务头和事务体序列化,然后根据序列化之后的字节数组计算该消息的
CheckSum
- 写入事务日志文件流
- 刷入磁盘
数据快照
- 数据快照用于记录ZK服务器上某一时刻的全量内存数据内容,并将其写入到磁盘文件中
- 文件名代表本次数据快照开始时刻的服务器最新ZXID
- 没有采用预分配机制
- ZK会在进行若干次事务日志记录后,将内存数据库中的数据全量Dump到本地文件,这个过程就是数据快照.具体多少次通过
SnapCount
配置 - 在数据快照之前,先切换事务日志文件,就是标识当前事务日志文件已经下
写满
,创建一个新的事务日志文件 - 创建数据快照异步线程,不能影响ZK主流程,需要创建一个单独的异步线程执行数据快照
- 获取权限数据和会话信息,从ZKdatabase中获取DataTree和会话信息
- 生成数据快照文件名,根据当前已提交的最大ZXID来创建数据快照文件名
- 序列化之后写入数据快照文件
启动初始化
- 初始化的目的在于将存储在磁盘上的数据加载到ZK服务器内存中
- 初始化ZKDatabase
- 获取最新的100个数据快照文件,然后
逐个
解析. 逐个的意思是前一个解析失败,就会接着解析下一个,如果前一个解析成功,后面的就不管了 - 根据数据快照获取最新的ZXID
- 根据事务日志文件处理增量的数据,即从事务日志文件中获取
最新ZXID之后提交的事务
- 再将从事务日志文件中解析出的事务应用到内存中
- 再次获取最新的ZXID,这个值也就代表上次服务器正常运行时提交的最大事务ID
数据同步
- 同步过程就是Leader服务器将那些没有在Learner服务器上提交过事务请求同步给Learner服务器
- Learner向Leader注册的最后阶段,Learner会发送给Leader一个ACKEPOCH数据包,Leader会从这个数据包中解析出currentEpoch和lastZxid
- Leader从内存中提取出事务请求对应的
提议缓存队列
: proposals. 同时完成以下三个ZXID的初始化
3.1 peerLastZxid: 该Learner最后处理的ZXID
3.2 minCommitedLog: Leader从提议缓存队列
获取到的最小ZXID
3.3 maxCommitedLog: Leader从提议缓存队列
获取到的最大ZXID - 根据 peerLastZxid minCommitedLog maxCommitedLog 的值来决定采用那种同步方式
4.1 peerLastZxid 介于minCommitedLog和maxCommitedLog中间: 直接差异化同步
4.2 peerLastZxid 介于minCommitedLog和maxCommitedLog中间的特殊情况(前一个Leader写入了事务日志但是还没将Proposal发送给Follower进行投票的时候挂机): 先回滚再差异化同步
4.3 peerLastZxid大于maxCommitedLog: 回滚同步
4.4 peerLastZxid小于minCommitedLog: 全量同步
ZAB协议
- 选主 数据同步
- 消息广播