台州国强建设网站,网站建设属于移动互联网,爱情动做网站推荐,数据服务网站开发目录
系统背景
集群结构
多个节点之间的角色
节点的状态
为什么引入 Observer
存储结构
ZNode 节点结构
ZNode 创建类型
内存数据存储
数据持久化
zookeeper 的容量大小
数据同步
消息广播
崩溃恢复
如何保证顺序一致性
核心流程
Leader 选举流程
脑裂问题
…目录
系统背景
集群结构
多个节点之间的角色
节点的状态
为什么引入 Observer
存储结构
ZNode 节点结构
ZNode 创建类型
内存数据存储
数据持久化
zookeeper 的容量大小
数据同步
消息广播
崩溃恢复
如何保证顺序一致性
核心流程
Leader 选举流程
脑裂问题
session 会话
临时节点的实现
顺序节点的实现
节点 watch 机制
应用场景
数据发布与订阅 / 分布式服务协调
分布式锁
分布式队列 系统背景
zookeeper 为了解决什么问题 分布式协调服务用于管理分布式系统使一个系统中的节点知道其他节点的状态。
实现方式 通过维护和监控存储的数据变化达到基于数据的集群一致性管理。 简单说zookeeper 文件系统 监控通知机制
应用场景 配置维护、域名维护、分布式同步、服务发现等
zookeeper中的 CAP
C保证「最终一致性」并不保证强一致性在十几秒可以Sync到各个节点如果保证强一致写性能会很差 如果想保证取得是数据一定是最新的需要手工调用Sync()
A保证了可用性数据总是可用的没有锁 但是在集群选主期间服务不可用 C 一致性 A 可用性 zookeeper C 最终一致性写操作有10s左右的同步过程 选主期间30~120s不可用 Eureka A 对等复制期间数据不同步 心跳机制对差异数据补偿最终一致 有本地缓存挂机后仍然可用 单机 Redis C 单 worker 集群 Redis A 主从数据同步过程中可能不一致 哨兵增加 sentinel 节点监控主从状态仍不能保证主从数据的一致 集群对数据进行分片如果一个槽只有一个节点数据是同步的 但是为增加可用性对每个节点增加主从仍存在数据不一致的风险 为了增加可用性增加了 主从、哨兵、集群模式
集群结构 在分布式系统中zookeeper 作为一个协调组件更需要保证其可用性所以一定是多节点部署的。 多个节点之间的角色 Leader 领导者提供读、写服务负责投票的发起、决议 Follower 跟随者提供读服务参与事务投票参与 leader 选举投票参加 leader 选举 Observer 观察者提供读服务不参与事务投票不参与 leader 选举投票不参加 leader 选举
节点的状态 Looking竞选状态 Following跟随者状态 observing观察者状态 leadering领导者状态
为什么引入 Observer Observer 是 zk-3.3 之后引入的新角色其与 follower 的区别就是不参与任何投票 可以在不影响「写」性能的前提下提高「读」性能写操作在超过半数节点投票之后才成功参与投票的节点多投票过程会成为性能瓶颈 Observer 的数据没有事务化到硬盘只加载在内存中。
存储结构 简单讲 zookeeper 文件系统 通知机制
ZNode 节点结构 在zk中可以说所有的数据都是由znode组成的并且以 k/v 的形式存储。类似于 linux中文件目录的 树形结构根目录以 / 开头。 cZxid 创建节点时的事务ID ctime 创建节点时的时间 mZxid 最后修改节点时的事务ID mtime 最后修改节点时的时间 pZxid 表示该节点的子节点列表最后一次修改的事务ID添加子节点或删除子节点就会影响子节点列表但是修改子节点的数据内容则不影响该ID注意只有子节点列表变更了才会变更pzxid子节点内容变更不会影响pzxid cversion 子节点版本号子节点每次修改版本号加1 dataversion 数据版本号数据每次修改该版本号加1 aclversion 权限版本号权限每次修改该版本号加1 ephemeralOwner 创建该临时节点的会话的sessionID。如果该节点是持久节点那么这个属性值为0 dataLength 该节点的数据长度 numChildren 该节点拥有子节点的数量只统计直接子节点的数量 ZNode 创建类型 Persistent 持久化节点 Persistent_sequential 顺序自动编号持久化节点根据当前已存在的节点数1 Ephemeral 临时节点客户端session超时此节点自动删除 Ephemeral_sequential 临时自动编号节点 内存数据存储 zookeeper 的数据组织形式为一个类似文件系统的数据结构这些数据都是存储在内存中的。 但是其存储格式并不是一个真正的数据而是一个对象DataTree代表了内存中的全部数据
public class DataTree {private final ConcurrentHashMapString, DataNode nodes new ConcurrentHashMapString, DataNode();private final WatchManager dataWatches new WatchManager();private final WatchManager childWatches new WatchManager();
}
其中 DataNode 是数据存储的最小单位
public class DataNode implements Record {byte data[]; // 数据的字节数组Long acl; // 访问权限 public StatPersisted stat; // 持久化统计信息private SetString children null; // 子节点路径的集合
} 数据持久化
为了避免宕机或断电情况下的数据丢失zookeeper 也制定了一些数据持久化方式 事务日志 log 事务快照 snapshot
事务日志 log 文件 对于客户端的每一次事务写操作zk除了会将数据变更到内存中还会记录到事务日志中 目录格式log.{zxid}zxid创建该文件时的最大 zxid
事务快照 snapshot 文件 用于记录某一个时刻 zk 服务器上的全量数据 目录格式snapshot.{zxid}zxid创建该文件时的最大 zxid
事务恢复过程
先通过快照数据快速恢复再通过增量事务日志恢复 先从 snapshot 目录中找到 zxid 最大的文件根据它的内容恢复 去 log 目录中找 略小于 zxid、所有比 zxid 大的文件依次读取并执行请求 zookeeper 的容量大小 每个 znode 中数据的限制为 1MB一般 znode 的数量 10wsnapshot 大小 800M zookeeper 应该被作为一个分布式协调服务而不是存储服务所以不应该被存储过多的数据。
数据同步 节点间的数据一致性指的是事务操作改变的数据即写操作 在 zookeeper 中主要通过 ZAB协议Zookeeper Atomic Broadcast 「zk原子广播协议」 来实现分布式数据一致性。ZAB 协议分为两部分 消息广播使用一个单一的leader来处理所有的写请求并且将请求以事务投票的形式广播到所有的follower中 崩溃恢复在主机断电、宕机的情况下集群恢复之后依然正常工作数据一致
消息广播 其过程类似于2PC不同的是主节点不需要等待所有节点的ack而是过半的 follower 就可以。Observer 不参与投票 leader 收到消息请求后将会生成一个 zxid leader 与 follower 之间通过 tcp 协议 FIFO 队列 通信将带有 zxid 的消息发送给 follower zxid —— zk中的事务id全局单调递增唯一id 高32位 epoch 纪元 代表 leader 周期每进行一次 leader 选举1并将 counter 清零 低32位 counter 计数器针对客户端的每个事务请求都1 崩溃恢复
什么时候算是崩溃 leader 服务器出现故障或者由于网络问题leader 服务器与过半的 follower 失去通信。
什么叫做恢复 选举出一个新的 leader
在这个过程中可能出现两种情况
被丢弃的消息不能再出现 原 leader 发出写请求后崩溃其他服务器并没有收到此消息选取新 leader后此消息被跳过 但原 leader 重启后成为 follower其保留了此消息需要被删除 新的 leader 会以自己的最后一个事务消息为准明确其他节点需要丢弃的消息 已经被处理的消息不能被丢弃 原 leader 在本地执行 commmit并且发出 commit 后崩溃则剩下的服务器并没有提交此事务 选举出的新 leader 是zxid最大的节点必然包括此事务 新的 leader 会将自己事务日志中已 proposal 但没有 commit 的事务提交掉然后给其他的 follower 发消息。 被丢弃的消息不能再出现 原 leader 发出写请求后崩溃其他服务器并没有收到此消息选取新 leader后此消息被跳过 但原 leader 重启后成为 follower其保留了此消息需要被删除
新的 leader 会以自己的最后一个事务消息为准明确其他节点需要丢弃的消息
已经被处理的消息不能被丢弃 原 leader 在本地执行 commmit并且发出 commit 后崩溃则剩下的服务器并没有提交此事务
选举出的新 leader 是zxid最大的节点必然包括此事务
新的 leader 会将自己事务日志中已 proposal 但没有 commit 的事务提交掉然后给其他的 follower 发消息。 如何保证顺序一致性
从客户端的角度来看 zookeeper 保证了同一个客户端请求的有序性但是不同的客户端之间的事务请求不保证。 因为 zk 中通过一个 Map 去保存每一个客户端的请求每个客户端的key 不同而value 为一个 FIFO 先进先出队列。
从服务端Leader 节点的角度来看 一致性 对于ZooKeeper来讲ZooKeeper的写请求由Leader处理Leader能够保证并发写入的有序性即同一时刻只有一个写操作被批准然后对该写操作进行全局编号最后进行原子广播写入所以ZooKeeper的并发写请求是顺序处理的而底层又是用了ConcurrentHashMap理所当然写请求是线程安全的。 有序性 zookeeper 的 leader 节点需要保证事务按照 zxid 的顺序来生效 但是 leader 节点在执行一个事务时需要发起提议、收到超半数的 follower 的投票后才生效这两个步骤的时间并不是完全按照发起的顺序。 Leader 为了保证提案按 ZXID 顺序生效使用了一个 ConcurrentHashMap 记录所有未提交的提案 命名为 outstandingProposals 每发起一个提案将提案的 zxid 与内容记录到 outstandingProposals 即待提交提案 收到 follower 的 ack 后根据 ack 中的 zxid 从 outstandingProposals 中找到对应的提案对 ack 计数 尝试将提案提交 判断提案是否收到了超过半数的 ack达到半数可以提交 判断当前 zxid 之前是否还有提案未提交如果有当前提案暂时不能被提交 —— 判断 outstandingProposals 里当前 zxid 的前一个 zxid zxid-1是否还在 核心流程
Leader 选举流程 发生 leader 选举的时机有两个集群启动时、或是原 leader 节点宕机。 几个选举时非常重要的参数 myid服务器id集群中节点的编号1/2/3...编号越大权重越高 zxid事务id值越大说明此节点上的数据越新 ephoch与zxid中的ephoch不同投票选举轮数同一轮投票所有的 ephoch是相同的
服务器启动时leader选举 集群启动时每个节点都是 looking 竞选状态 每个节点将自己的ephoch、zxid、myid作为投票发送给其他节点 节点处理投票优先级为ephoch、zxid、myid 对方节点 自己更新投票为对方节点的信息 自己 对方节点投票信息不变 进入下一轮投票ephoch1 其中每一轮投票都进行一次统计是否有超过半数的节点投给了某个节点如果超过一半则已选出leader 此节点更新自身状态为 leadering进入数据同步、消息广播阶段。
运行过程中leader选举 与启动时类似
脑裂问题
什么是脑裂在什么场景下会发生 在分布式系统中如 zookeeper 集群会有一个统一的“大脑”也就是 leader 节点。但由于网络问题等集群中一部分节点不能与 leader 通信此时集群将分裂成不同小集群各自选出自己的 leader。
zookeeper 如何避免 zookeeper 在选举 leader 时采用「投票过半」机制只有获得超过半数节点的投票才能选举成功。因此要么选举出唯一 leader要么选举失败。
如果其余节点选举出新 leader旧的 leader 又复活了呢 zookeeper 维护了一个 ephoch 纪元变量每个 leader 产生ephoch1follower 会拒绝所有 ephoch 现任 leader ephoch 的所有请求
zookeeper 之外所有脑裂问题的解决方式 超半数投票添加心跳线采用多种通信方式磁盘锁集群中只有一个 leader 可以获取锁
session 会话 所谓 session就是在客户端与服务器之间创建的一个「TCP长链接」。有了链接之后后续的请求发送、回应、心跳等都是基于会话实现的。 zk 中一个客户端只会与一个服务器建立 tcp 长链接除非与当前服务器连接失败
session 的管理机制——分桶
session 的数据结构 SessionId会话的唯一id由zk管理分配由时间戳机器id生成 TimeOut超时时间相对时间 ExpirationTime绝对过期时间是 ExpirationIntervalzk定时检查session过期频率默认 2000ms 的倍数。所以 expiration time 是一个近似值。 zookeeper 服务端会把各个 session 分配到不同的「桶」里面进行管理区分的维度即Expiration Time。 然后 zk 会以 ExpirationInterval 为频率定期扫描下一个过期的桶将其内的 session 置为过期。
session 续约 客户端与服务器建立连接之后expiration time 并不是永远不变的客户端会发生读写请求或者心跳来保持会话的有效性。 过期时间的更新也意味着 session 需要在 桶 之间进行迁移。 客户端发送 读、写 请求都会触发一次激活 客户端没有请求在 timeout 的1/3 时间会发送一次 ping 来续约 客户端断开则 timeout 过期之后 zk 扫描到此 session 过期并清理
临时节点的实现
临时节点的过期时间 客户端建立连接时的 session 过期后其创建的所有临时节点将被删除。
zookeeper 怎么知道哪些节点由此客户端创建的 znode 中的 ephemeralOwner 字段保存了客户端 session Id
顺序节点的实现
sequence 自增原理? 由父节点的 cVersion 控制在此父节点下创建 顺序子节点子节点的 path cVersion然后 cVersion 自增
如何进行并发控制 由于创建节点属于 事务操作因此 follower 节点会转发给 leader 节点进行操作在 leader 节点中 create Sequence node 之前会给其父节点加 synchronized
节点 watch 机制
watcher 的特性 一次性watcher 是一次性的一旦被触发就会移除再次使用时需要重新注册 需要监听一个节点的更新事件需要不断注册 是的。zookeeper curator 框架对此做了封装实现了自动注册。 顺序性watcher 回调是顺序串行化执行的一个 watcher 回调逻辑不应该太多以免影响其他 watcher 的执行 watch 的通知事件是从服务器异步发送给客户端不同的客户端收到的 watch 的时间可能不同。但是 zooKeeper 有保证客户端先收到 watch 事件再看到结点数据的变化的。例如A3此时在上面设置了一次 watch如果 A 突然变成 4 了那么客户端会先收到 watch 事件的通知然后才会看到 A4。 不同的客户端监听的事件可能在不同时刻但是 同一个客户端是保证顺序 的客户端 watcher1 监听的事件优先于 watcher2 监听的事件发生可以保证在客户端 watcher1 先于 watcher2 被触发。 轻量级watchEvent 是最小通信单元结构上包括事件类型、节点路径但不包括内容的前后变化 时效性watcher 在当前 session 完全失效时才失效如果客户端在 session 有效时间内重连成功watcher 依然有效
watchEvent 内容 KeeperState EventType 触发条件 SyncConnected None 客户端与服务器成功建立连接 NodeCreated 监听节点被创建 NodeDeleted 监听节点被删除 NodeDataChanged 监听节点内容变更 NodeChildrenChanged 监听节点的子节点列表发生变更 DisConnected None 客户端与服务断开连接 Expired None 会话超时 AuthFails None 权限错误
zookeeper 的 watcher 机制与「观察者模式」类似一共分为四个步骤 客户端注册 watcher 客户端可以在任意的 读命令 上注册 watcher如 getData、exists、getChildren 接口 服务端处理 watcher 服务端触发 watcher 事件 客户端回调 watcher 客户端首先将 watcher 注册到服务端同时将 watcher 对象保存到客户端的 watch 管理器中。当 ZooKeeper 服务端监听的数据状态发生变化时服务端会主动通知客户端接着客户端的 watch 管理器会触发相关 watcher 来回调相应处理逻辑从而完成整体的数据发布/订阅流程。
watcher管理器 数据结构
客户端 —— ZKWatchManager key数据节点路径 value注册在当前节点的 watcher 集合
// 代表节点上内容数据、状态信息变更相关监听
private final MapString, SetWatcher dataWatches new HashMapString, SetWatcher();// 代表节点变更相关监听
private final MapString, SetWatcher existWatches new HashMapString, SetWatcher();// 代表节点子列表变更相关监听
private final MapString, SetWatcher childWatches new HashMapString, SetWatcher();
服务端 —— WatchManager
// key代表数据节点路径value代表客户端连接的集合
// 该map作用为通过一个指定znode路径可找到其映射的所有客户端当znode发生变更时
// 可快速通知所有注册了当前Watcher的客户端
private final HashMapString, HashSetWatcher watchTable new HashMapString, HashSetWatcher();//key代表一个客户端与服务端的连接value代表当前客户端监听的所有数据节点路径
//该map作用为当一个连接彻底断开时可快速找到当前连接对应的所有
//注册了监听的节点以便移除当前客户端对节点的Watcher
private final HashMapWatcher, HashSetString watch2Paths new HashMapWatcher, HashSetString(); 应用场景
数据发布与订阅 / 分布式服务协调 多个订阅者同时监听同一个主题对象主题对象状态变化时通知所有订阅者此方式可以让发布方与订阅方独立封装互相解耦
在分布式系统中的应用有 配置中心 服务发现 集群机器存活监控检测系统和被检测系统不直接联系通过 zk 上的节点关联解耦 通过客户端注册 EPHEMERAL 节点监控端进行监听一旦会话结束或过期该节点就会消失。 在此方式之前一般是监控者通过某种手段比如 ping定时监控每台机器或者是客户端定时向监控者汇报我还活着 系统调度管理者在控制台进行一些操作实际是修改了某些 zk 的节点对应的客户端将会收到节点变化通知作出相应任务
分布式锁
排它锁 定义锁
/exclusive_lock/lock 所有的客户端通过调用 create() 接口在 /exclusive_lock 节点下创建 临时子节点 /exclusive_lock/lock最终只有一个客户端能创建成功调用成功的客户端代表成功获取锁 其余的客户端在 /exclusive_lock 节点上注册一个 子节点变更的 watcher 监听事件以便重新争取获得锁
羊群效应 所有的客户端均监控 /exclusive_lock 一个节点如果当前锁释放所有客户端均收到通知但是只能有一个客户端会重新获取锁这对资源是一种浪费。 解决客户端创建 临时顺序节点每个客户端监控其前一个序号的节点。 优点每次锁释放只有一个客户端被通知另外还实现了公平锁先到先得。
共享锁读写锁 定义锁
/shared_lock/[hostname]-请求类型W/R-序号 客户端调用 create 方法创建类似定义锁方式的 临时顺序节点 客户端调用 getChildren 接口来获取所有已创建的子节点列表判断是否获得锁 对于读请求如果自己是序号最小的节点、或者所有比自己小的子节点都是读请求表明已经成功获取共享锁否则监听比自己序号小的最后一个写请求节点 对于写请求如果自己不是序号最小的子节点那么就进入等待否则监听序号比自己序号小的节点
应用场景 ZooKeeper分布式锁如InterProcessMutex能有效的解决分布式锁问题但是性能并不高。 因为每次在创建锁和释放锁的过程中都要动态创建、销毁瞬时节点来实现锁功能。大家知道ZK中创建和删除节点只能通过Leader服务器来执行然后Leader服务器还需要将数据同不到所有的Follower机器上这样频繁的网络通信性能的短板是非常突出的。 在高性能高并发的场景下不建议使用ZooKeeper的分布式锁可以使用Redis的分布式锁。而由于ZooKeeper的高可用特性所以在并发量不是太高的场景推荐使用ZooKeeper的分布式锁。
分布式队列
实现方式 定义队列节点
/queue_fifo 生产者写入消息即在队列节点下创建 持久性顺序节点 消费者调用 getChildren 获取 /queue_fifo 下所有子节点
为空调用 getChildren 进行子节点监控消费者进入等待状态不为空则消费、删除节点
应用场景 不推荐使用。因为 ZooKeeper 有 1MB 的传输限制实践中 ZNode 必须相对较小而队列包含成千上万的消息可能非常的大 当某个 ZNode 很大的时候会很难清理同时调用这个节点的 getChildren() 方法会失败 当出现大量的包含成千上万的子节点的 ZNode 时ZooKeeper 的性能会急剧下降 ZooKeeper 的数据完全存放在内存中如果有大量的队列消息会占用很多的内存空间。