本文来自作者投稿, 作者: 林湾村龙猫, Hollis 做了一些修改和补充.
随着互联网技术的发展, 大型网站需要的计算能力和存储能力越来越高. 网站架构逐渐从集中式转变成分布式.
虽然分布式和集中式系统相比有很多优势, 比如能提供更强的计算, 存储能力, 避免单点故障等问题. 但是由于采用分布式部署的方式, 就经常会出现网络故障等问题, 并且如何在分布式系统中保证数据的一致性和可用性也是一个比较关键的问题.
分布式的工作方式有点类似于团队合作. 当有一项任务分配到某个团队之后, 团队内部的成员开始各司其职, 然后把工作结果统一汇总给团队主管, 由团队主管再整理团队的工作成果汇报给公司.
但是, 日常工作中, 如果两个员工或用户对某件事产生了分歧, 通常我们的做法是找上级, 去做数据和信息的同步.
那么对于我们的服务呢, 多个节点之间数据不同步如何处理?
对于分布式集群来说, 这个时候, 我们通常一个能够在各个服务或节点之间进行协调的服务或中间人
架构设计中, 没有一个问题不能通过增加一个抽象层来解决的, 如果有, 那就增加两层.
集群管理
我们可以一起看看, 协调服务中的佼佼者 --ZooKeeper
zookeeper 起源
最初, 在 Hadoop 生态中, 会存在很多的服务或组件(比如 hive,pig 等), 每个服务或组件之间进行协调处理是很麻烦的一件事情, 急需一种高可用高性能数据强一致性的协调框架.
因此雅虎的工程师们创造了这个中间程序, 但中间程序的命名却愁死了开发人员, 突然想到 hadoop 中的大多是动物名字, 似乎缺乏一个管理员, 这个程序的功能有是如此的相似. 因此 zookeeper 诞生.
Zookeeper 是一个开放源码的分布式服务协调组件, 是 Google Chubby 的开源实现. 是一个高性能的分布式数据一致性解决方案. 他将那些复杂的, 容易出错的分布式一致性服务封装起来, 构成一个高效可靠的原语集, 并提供一系列简单易用的接口给用户使用.
zookeeper 提供了哪些特性, 以便于能够很好的完成协调能力的处理呢?
功能与特性
数据存储
zookeeper 提供了类似 Linux 文件系统一样的数据结构. 每一个节点对应一个 Znode 节点, 每一个 Znode 节点都可以存储 1MB(默认)的数据.
客户端对 zk 的操作就是对 Znode 节点的操作.
zookeeper 数据结构
Znode: 包含 ACL 权限控制, 修改 / 访问时间, 最后一次操作的事务 Id(zxid)等等
说有数据存储在内存中, 在内存中维护这么一颗树.
每次对 Znode 节点修改都是保证顺序和原子性的操作. 写操作是原子性操作.
举个例子, 在注册中心中, 可以通过路径 "/fsof / 服务名 1/providers" 找到 "服务 1" 的所有提供者.
每一个 Znode 节点又根据节点的生命周期与类型分为 4 种节点.
生命周期: 当客户端会话结束的时候, 是否清理掉这个会话创建的节点. 持久 - 不清理, 临时 - 清理.
类型: 每一个会话, 创建单独的节点(例子: 正常节点: rudytan, 顺序编号节点: rudytan001,rudytan002 等等)
监听机制
zookeeper 除了提供对 Znode 节点的处理能力, 还提供了对节点的变更进行监听通知的能力.
监听机制的步骤如下:
任何 session(session1,session2)都可以对自己感兴趣的 znode 监听.
当 znode 通过 session1 对节点进行了修改.
session1,session2 都会收到 znode 的变更事件通知.
节点常见的事件通知有:
session 建立成功事件
节点添加
节点删除
节点变更
子节点列表变化
需要特别说明的是:
一次监听事件, 只会被触发一次, 如果想要监听到 znode 的第二次变更, 需要重新注册监听.
到这里, 我们了解到 zookeeper 提供的能力, 那我们在哪些场景可以使用它? 如何使用它呢?
应用场景
zookeeper 用得比较多的地方可能是, 微服务的集群管理与服务注册与发现.
注册中心
依赖于临时节点
消费者启动的时候, 会先去注册中心中全量拉取服务的注册列表.
当某个服务节点有变化的时候, 通过监听机制做数据更新.
zookeeper 挂了, 不影响消费者的服务调用.
目前还有个比较流行的服务 Eureka 也可以做注册中心, 他们有什么优势和劣势呢?
注册中心的对比
通过上面的架构图, 可以发现 Eureka 不同于 zk 中的节点, Eureka 中的节点每一个节点对等. 是个 AP 系统, 而不是 zk 的 CP 系统. 在注册中心的应用场景下, 相对于与强数据一致性, 更加关心可用性.
分布式锁
依赖于临时顺序节点
判断当前 client 的顺序号是否是最小的, 如果是获取到锁.
没有获取到锁的节点监听最小节点的删除事件(比如 lock_key_001)
锁释放, 最小节点删除, 剩余节点重新开始获取锁.
重复步骤二到四.
Redis 和 db 也能创建分布式锁, 那有什么异同呢?
分布式锁的对比
分布式锁可以参考文章: 分布式锁(Redis/MySQL), 分布式锁的几种实现方式
具体差异比较:
从理解的难易程度角度 (从低到高) 数据库> 缓存(Redis)> Zookeeper
从实现的复杂性角度(从低到高)Zookeeper>= 缓存(Redis)> 数据库
从性能角度 (从高到低) 缓存(Redis)> Zookeeper>= 数据库
从可靠性角度(从高到低)Zookeeper> 缓存(Redis)> 数据库
集群管理与 master 选举
依赖于临时节点
zookeeper 保证无法重复创建一个已存在的数据节点, 创建成功的 client 为 master.
非 master, 在已经创建的节点上注册节点删除事件监听.
当 master 挂掉后, 其他集群节点收到节点删除事件, 进行重新选举
重复步骤二到四
当然还有其他应用场景, 不一一列举了.
有人说, zookeeper 可以做分布式配置中心, 分布式消息队列, 看到这里的小伙伴们, 你们觉得合适么?
到这里, 可以基本上满足基于 zk 应用开发的理论知识储备.
高性能高可用强一致性保障
高性能 - 分布式集群
高性能, 我们通常想到的是通过集群部署来突破单机的性能瓶颈. 对于 zk 来说, 就是通过部署多个节点共同对外提供服务, 来提供读的高性能.
Master/Slave 模式.
在 zookeeper 中部署多台节点对外提供服务, 客户端可以连接到任意一个节点.
每个节点的数据都是一样的.
节点根据角色分为 Leader 节点与 Learner 节点(包括 Follower 节点与 Observer 节点).
集群中, 只有一个 Leader 节点, 完成所有的写请求处理.
每次写请求都会生成一个全局的唯一的 64 位整型的事务 ID(可以理解为全局的数据的版本号).
Learner 节点可以有很多, 每个 Leaner 可以独自处理读请求, 转写请求到 Leader 节点.
当 Leader 节点挂掉后, 会从 Follower 节点中通过选举方式选出一个 Leader 提供对外服务.
Follower 节点与 Observer 节点区别在于不参与选举和提议的事务过半处理.
集群通常是按照奇数个节点进行部署(偶然太对容灾没啥影响, 浪费机器).
数据一致性(zab 协议 - 原子广播协议)
通过集群的部署, 根据 CAP 原理, 这样, 可能导致同一个数据在不同节点上的数据不一致. zookeeper 通过 zab 原子广播协议来保证数据在每一个节点上的一致性. 原子广播协议 (类似 2PC 提交协议) 大概分为 3 个步骤.
Leader 包装写请求, 生成唯一 zxid, 发起提议, 广播给所有 Follower.
Follower 收到提议后, 写入本地事务日志, 根据自身情况, 是否同意该事务的提交.
Leader 收到过半的 Follower 同意, 自己先添加事务. 然后对所有的 Learner 节点发送提交事务请求.
需要说明的是, zookeeper 对数据一致性的要求是:
顺序一致性: 严格按照事务发起的顺序执行写操作.
原子性: 所有事务请求的结果在集群中的所有节点上的应用情况是一致的.
单一视图: 客户端访问任何一个节点, 看到的数据模型都是一致的.
实时性: 保证在极小一段时间客户端最终可以从服务读取最新数据状态(如果要实时, 需要客户端调用 syn 方法).
可用性 - leader 选举(zab 协议 - 崩溃恢复协议)
在整个集群中, 写请求都集中在一个 Leader 节点上, 如果 Leader 节点挂了咋办呢?
当集群初始化或 Follower 无法联系上 Leader 节点的时候, 每个 Follower 开始进入选举模式. 选举步骤如下:
Follower 节点第一次投票先投自己, 然后将自己的选票广播给剩余的 Follower 节点.
Follower 节点接收到其他的选票.
选票比较: 比较自己的与接收的选票的投票更有.
如果资金的选票不是最优选票, 变更自己的选票, 投最优选票的节点.
统计自己收到的选票, 如果某个节点获得了过半的节点的投票. 确认该节点为新的 Leader 节点.
确认 Leader 节点后, 每个节点变更自己的角色. 完成投票选举.
选举原则: 谁的数据最新, 谁就有优先被选为 Leader 的资格.
举个例子, 假如现在 zk 集群有 5 个节点, 然后挂掉了 2 个节点. 剩余节点 S3,S4,S6 开始进行选举, 他们的最大事务 ID 分别是 6,2,6. 定义投票结构为(投票的节点 ID, 被投节点 ID, 被投节点最大事务 ID).
初始状态, S3,S4,S5 分别投自己, 并带上自己的最大事务 ID.
S3,S4,S5 分别对自己收到的 2 票与自己的 1 票做比较.
S5 发现自己的是最优投票, 不变更投票, S3,S4 发现 S5 的投票是最优解, 更改投票.
S3,S4 广播自己变更的投票.
最后大家都确认了 S5 是 Leader,S5 节点状态变更为 Leader 节点, S3,S4 变更为 Follower 节点.
到这里, 就是选举的主要过程.
数据的持久化
zookeeper 所有数据都存在内存中.
zookeeper 会定期将内存 dump 到磁盘中, 形成数据快照.
zookeeper 每次的事务请求, 都会先接入到磁盘中, 形成事务日志.
全量数据 = 数据快照 + 事务日志.
Zookeeper 和 CAP 的关系
前面提到了 zk 在可用性, 数据一致性, 性能等方面都表现的很优秀, 也介绍了其中的原理.
但是分布式系统的 CAP 理论告诉我们: 任何软件系统都无法同时满足一致性, 可用性以及分区容错性.
那么, Zookeeper 其实也是一个分布式系统, 那么也就要满足 CAP 理论, 也就是说, 虽然在各个方面, ZK 可以说是做了很多努力, 但是在极端情况下, Zookeeper 也需要在这三者之间有一些权衡, 那么 Zookeeper 在 CAP 中是如何取舍的呢?
ZooKeeper 是个 CP(一致性 + 分区容错性)的, 即任何时刻对 ZooKeeper 的访问请求能得到一致的数据结果, 同时系统对网络分割具备容错性, 但是它不能保证每次服务请求的可用性(注: 也就是在极端环境下, ZooKeeper 可能会丢弃一些请求, 消费者程序需要重新请求才能获得结果).
但是别忘了, ZooKeeper 是分布式协调服务, 它的职责是保证数据 (注: 配置数据, 状态数据) 在其管辖下的所有服务之间保持同步, 一致, 所以就不难理解为什么 ZooKeeper 被设计成 CP 而不是 AP 特性的了.
如果是 AP 的, 那么将会带来恐怖的后果(注: ZooKeeper 就像交叉路口的信号灯一样, 你能想象在交通要道突然信号灯失灵的情况吗?).
而且, 作为 ZooKeeper 的核心实现算法 Zab, 就是解决了分布式系统下数据如何在多个服务之间保持同步问题的.
如果 ZooKeeper 下所有节点都断开了, 或者集群中出现了网络分割的故障(注: 由于交换机故障导致交换机底下的子网间不能互访).
那么 ZooKeeper 会将它们都从自己管理范围中剔除出去, 外界就不能访问到这些节点了, 即便这些节点本身是 "健康" 的, 可以正常提供服务的; 所以导致到达这些节点的服务请求被丢失了.(Zookeeper 介绍(二)--Zookeeper 概述)
那么, 再来深入原理看一下 Zookeeper 是如何在 CAP 之间做权衡的呢?
感悟
最后, 说说在整个学习和使用 zk 过程中的一个感悟吧.
没有银弹, 每一种技术或方案都有其优点和缺点.
做一件事情很简单, 做好一件事件很难.
来源: https://juejin.im/entry/5bf36578f265da612239f4bb