ZooKeeper
一、宏观
image.png1.1 是什么
zookeeper是用于分布式中一致性处理的框架,简单的说 zookeeper = 文件系统 (保证集群内数据一致性)+监听通知机。
1.2 重要概念
- ZooKeeper 本身就是一个分布式程序(只要半数以上节点存活,ZooKeeper 就能正常服务)。
- 为了保证高可用,最好是以集群形态来部署 ZooKeeper,这样只要集群中大部分机器是可用的(能够容忍一定的机器故障),那么 ZooKeeper 本身仍然是可用的。
- ZK 将数据保存在内存中,这也就保证了 高吞吐量和低延迟(但是内存限制了能够存储的容量不太大,此限制也是保持 Znode 中存储的数据量较小的进一步原因)。
- ZK 的读是高性能的。在“读”多于“写”的应用程序中尤其地高性能,因为“写”会导致所有的服务器间同步状态。(“读”多于“写”是协调服务的典型场景。)
- ZK 有临时节点的概念。当创建临时节点的客户端会话一直保持活动,瞬时节点就一直存在。
而当会话终结时,瞬时节点被删除。持久节点是指一旦这个 ZNode 被创建了,除非主动进行 ZNode 的移除操作,否则这个 ZNode 将一直保存在 Zookeeper 上。 - ZooKeeper 底层其实只提供了两个功能:
①管理(存储、读取)用户程序提交的数据;
②为用户程序提交数据节点监听服务。
1.3 架构图
image.png1.4 有哪些应用场景
分布式应用程序可以基于 ZooKeeper 实现一下功能:
- 数据发布/订阅
- 负载均衡
- 命名服务
- 分布式协调/通知
- 集群管理
- Master 选举
- 分布式锁
- 分布式队列
1.5 有哪些缺点/限制
1. ZK不是为高可用性设计的
由于zookeeper对于网络隔离的极度敏感,导致zookeeper对于网络的任何风吹草动都会做出激烈反应。这使得zookeeper的‘不可用’时间比较多
,我们不能让zookeeper的‘不可用’,变成系统的不可用。
2. ZK的选举过程速度很慢
而zookeeper对那种情况非常敏感。一旦出现网络隔离,zookeeper就要发起选举流程。zookeeper的选举流程通常耗时30到120秒,期间zookeeper由于没有master,都是不可用的
。对于网络里面偶尔出现的,比如半秒一秒的网络隔离,zookeeper会由于选举过程,而把不可用时间放大几十倍。
3. ZK的性能有限
典型的zookeeper的tps大概是一万多
,无法覆盖系统内部每天动辄几十亿次的调用。因此每次请求都去zookeeper获取业务系统master信息是不可能的。因此zookeeper的client必须自己缓存业务系统的master地址。
1.6 ZK与Eureka的对比
从CAP原则来看,ZooKeeper选择了CP,Eureka选择了AP
Eureka为了最大化保证可用性,牺牲了数据的强一致性,仅保证弱一致性。
ZooKeeper为了最大化保证数据强一致性,牺牲了可用性(选主期间不可用)。
二、中观
2.1 数据模型
ZK的数据结构与linux文件系统很类似,和文件系统不同的是,zk的数据存储是是结构化存储,没有文件和目录的概念,文件和目录被抽象成了节点(node),也叫znode。节点有永久和临时两种类型,永久节点不会随着该节点的session的结束而被删除,除非显示delete才会被删除。临时节点在session结束后,会被自动删除。
每个znode由三个部分组成:
- stat, 描述该znode的版本权限等信息。
- data,与该znode关联的数据。
- children, 该znode下的子节点。
在 ZK 中,“节点"分为两类:
第一类同样是指构成集群的机器,我们称之为机器节点。
第二类则是指数据模型中的数据单元,我们称之为数据节点一ZNode
2.2 通知机制
ZooKeeper允许用户在指定节点上注册一些 Watcher,并在特定事件触发的时候,将事件通知到感兴趣的客户端上去。客户端接收到这个消息通知后,需要主动到服务端获取最新的数据。
image.png
2.3 保证机制
ZK提供的保证机制:
- 顺序一致性-来自客户端的更新将按照发送的顺序应用。
- 原子性-更新成功或失败。 没有部分结果。
- 统一映像-无论客户端连接到哪个服务器,客户端都将看到相同的服务视图。 即,即使客户端故障转移到具有相同会话的其他服务器,客户端也永远不会看到系统的较旧视图。
- 可靠性-应用更新后,此更新将一直持续到客户端覆盖更新为止。
- 及时性-确保系统的客户视图在特定时间范围内是最新的。
2.4 数据一致性
集群中的角色
Leader(只能有1个): 服务器为客户端提供读和写服务。
Follower和Observer:
- 共同点: 只提供读服务
- 区别: Observer不参与leader选举,和写操作的『过半写成功』策略。因此它可在不影响写性能的情况下提升集群的读性能。
image.png
如何保证数据一致性
ZooKeeper通过ZAB协议来保证数据一致性。
只有当服务端的ZK存在多台时,才会出现数据一致性的问题, 服务端存在多台服务器,他们被划分成了不同的角色,只有一台Leader,多台Follower和多台Observer。他们中的任意一台都能响应客户端的读请求,任意一台也都能接收写请求, 不同的是,Follower和Observer接收到客户端的写请求后不能直接处理这个请求而是将这个请求转发给Leader,由Leader发起原子广播完成数据一致性
.
理论上ZK集群中的每一个节点的作用都是相同的,他们应该和单机时一样,各个节点存放的数据保持一致才行
Leader接收到Follower转发过来的写请求后发起提议,要求每一个Follower都对这次写请求进行投票Observer不参加投票,继续响应client的读请求),Follower收到请求后,如果认为可以执行写操作,就发送给leader确认ack, 这里存在一个过半机制,就是说,在Leader发起的这次请求中如果存在一半以上的Follower响应了ack,Leader就认为这次的写操作通过了决议,向Follower发送commit,让它们把最新的操作写进自己的文件系统。
三、微观
3.1 ZAB协议
Zookeeper的核心是原子广播,保证各server的同步,实现该机制的协议是Zab协议。Zab协议有两种模式,恢复模式和广播模式。
- 恢复模式
leader崩溃后,Zab进入恢复模式,开始选举leader,选好后且大多数server完成了和leader的同步,恢复模式结束。 - 广播模式
一旦leader和多数的follower状态同步后,就进入广播状态。Zookeeper会维持在Broadcast状态,直到leader崩溃了或者失去了大部分的followers支持。
3.2 选举机制
Leader选取规则
1. 优先检查zxid(递增的事务id),zxid大的作为leader服务器。
2. zxid相同就比较server id,server id大的作为leader服务器(越晚启动serverid越大)。
3. 只有获取过半server的支持才能成为leader。
注意: zxid是一个64位的数字,它高32位是epoch用来标识leader关系是否改变,每次一个leader被选出来,它都会有一个新的epoch,低32位是个递增计数。
选举算法
Zk的选举算法有两种:一种基于basic paxos实现,另外一种基于fast paxos算法实现。系统默认的选举算法为fast paxos。
fast paxos算法
fast paxos过程,每个Server会向所有其他Server提议自己要成为leader, 选举过程如下。
(1) 全新集群选举(无数据)
有5台服务器均无数据,按编号1,2,3,4,5,依次启动:
- 服务器1启动,给自己投票,然后发投票信息,由于其它机器还没有启动所以它收不到反馈信息,服务器1的状态一直属于Looking。
- 服务器2启动,给自己投票,同时与之前启动的服务器1交换结果,由于服务器2的编号大所以服务器2胜出,但此时投票数没有大于半数,所以两个服务器的状态依然是LOOKING。
- 服务器3启动,给自己投票,同时与之前启动的服务器1,2交换信息,由于服务器3的编号最大所以服务器3胜出,此时投票数正好大于半数,所以服务器3成为领导者,服务器1,2成为小弟。
- 服务器4启动,给自己投票,同时与之前启动的服务器1,2,3交换信息,尽管服务器4的编号大,但之前服务器3已经胜出,所以服务器4只能成为小弟。
(2) 非全新集群选举(有数据)
对于运行正常的zookeeper集群,中途有机器down掉,需要重新选举时,选举过程就需要加入数据ID、服务器ID、和逻辑时钟。
- 逻辑时钟小的选举结果被忽略,重新投票;(除去选举次数不完整的服务器)
- 统一逻辑时钟后,数据id大的胜出;(选出数据最新的服务器)
- 数据id相同的情况下,服务器id大的胜出。(数据相同的情况下, 选择服务器id最大,即权重最大的服务器)
3.3 应用场景
3.3.1 分布式锁
锁的定义
ZooKeeper 上的一个 节点(node)可以表示一个锁。
image.png
排它锁
如果事务 T1 对数据对象 O1 加上了排他锁,那么加锁期间,只允许事务 T1 对 O1 进行读取和更新操作。核心是保证当前有且仅有一个事务获得锁,并且锁释放后,所有正在等待获取锁的事务都能够被通知到。
-
定义锁
通过 ZooKeeper 上的 Znode 可以表示一个锁,/x_lock/lock。 -
获取锁
所有客户端都会通过调用 create() 接口尝试在 /x_lock 创建临时子节点 /x_lock/lock。最终只有一个客户端创建成功,那么该客户端就获取了锁。同时没有获取到锁的其他客户端,注册一个子节点变更的 Watcher 监听。 -
释放锁
获取锁的客户端发生宕机或者正常完成业务逻辑后,就会把临时节点删除。临时子节点删除后,其他客户端又开始新的一轮获取锁的过程。
共享锁
如果事务 T1 对数据对象 O1 加上了共享锁,那么当前事务 T1 只能对 O1 进行读取操作,其他事务也只能对这个数据对象加共享锁,直到数据对象上的所有共享锁都被释放。
-
定义锁
通过 ZooKeeper 上的 Znode 表示一个锁,/s_lock/[HOSTNAME]-请求类型-序号。
/
├── /host1-R-000000001
├── /host2-R-000000002
├── /host3-W-000000003
├── /host4-R-000000004
├── /host5-R-000000005
├── /host6-R-000000006
└── /host7-W-000000007
-
获取锁
需要获得共享锁的客户端都会在 s_lock 这个节点下面创建一个临时顺序节点,如果当前是读请求,就创建类型为 R 的临时节点,如果是写请求,就创建类型为 W 的临时节点。
判断读写顺序 共享锁下不同事务可以同时对同一个数据对象进行读取操作,而更新操作必须在当前没有任何事务进行读写操作的情况下进行。- 2.1 创建完节点后,获取 s_lock 的所有子节点,并对该节点注册子节点变更的 Watcher 监听
- 2.2 然后确定自己的节点序号在所有的子节点中的顺序
- 2.3 对于读请求,如果没有比自己小的子节点,那么表名自己已经成功获取到了共享锁,同时开始执行读取逻辑,如果有比自己序号小的写请求,那么就需要进行等待。对于写请求,如果有比自己小的子节点,就需要进行等待。
- 2.4 接收到 Watcher 通知后重复 2.1
-
释放锁
获取锁的客户端发生宕机或者正常完成业务逻辑后,就会把临时节点删除。临时子节点删除后,其他客户端又开始新的一轮获取锁的过程。
3.3.3 负载均衡
负载均衡是一种手段,用来把对某种资源的访问分摊给不同的设备,从而减轻单点的压力。
实现的思路:
- 首先建立 Servers 节点,并建立监听器监视 Servers 子节点的状态(用于在服务器增添时及时同步当前集群中服务器列表)。
- 在每个服务器启动时,在 Servers 节点下建立临时子节点 Worker Server,并在对应的字节点下存入服务器的相关信息,包括服务的地址,IP,端口等等。
- 可以自定义一个负载均衡算法,在每个请求过来时从 ZooKeeper 服务器中获取当前集群服务器列表,根据算法选出其中一个服务器来处理请求。
3.3.3 命名服务
命名服务就是提供名称的服务。ZooKeeper 的命名服务有两个应用方面。
提供类 JNDI 功能,可以把系统中各种服务的名称、地址以及目录信息存放在 ZooKeeper,需要的时候去 ZooKeeper 中读取
制作分布式的序列号生成器
利用 ZooKeeper 顺序节点的特性,制作分布式的序列号生成器,或者叫 id 生成器。(分布式环境下使用作为数据库 id,另外一种是 UUID(缺点:没有规律)),ZooKeeper 可以生成有顺序的容易理解的同时支持分布式环境的编号。
在创建节点时,如果设置节点是有序的,则 ZooKeeper 会自动在你的节点名后面加上序号,上面说容易理解,是比如说这样,你要获得订单的 id,你可以在创建节点时指定节点名为 order_[日期]_xxxxxx,这样一看就大概知道是什么时候的订单。
/
└── /order
├── /order-date1-000000000000001
├── /order-date2-000000000000002
├── /order-date3-000000000000003
├── /order-date4-000000000000004
└── /order-date5-000000000000005
3.3.4 分布式协调/通知
一种典型的分布式系统机器间的通信方式是心跳。好处就是检测系统和被检系统不需要直接相关联,而是通过 ZooKeeper 节点来关联,大大减少系统的耦合。
心跳检测是指分布式环境中,不同机器之间需要检测彼此是否正常运行。传统的方法是通过主机之间相互 PING 来实现,又或者是建立长连接,通过 TCP 连接固有的心跳检测机制来实现上层机器的心跳检测。
如果使用 ZooKeeper,可以基于其临时节点的特性,不同机器在 ZooKeeper 的一个指定节点下创建临时子节点,不同机器之间可以根据这个临时节点来判断客户端机器是否存活。
3.3.5 集群管理
利用 ZooKeeper 实现集群管理监控
在管理机器上线/下线的场景中,为了实现自动化的线上运维,我们必须对机器的上/下线情况有一个全局的监控。通常在新增机器的时候,需要首先将指定的 Agent 部署到这些机器上去。Agent 部署启动之后,会首先向 ZooKeeper 的指定节点进行注册,具体的做法就是在机器列表节点下面创建一个临时子节点,例如 /machine/[Hostname]
(下文我们以“主机节点”代表这个节点),如下图所示。
当 Agent 在 ZooKeeper 上创建完这个临时子节点后,对 /machines
节点关注的监控中心就会接收到“子节点变更”事件,即上线通知,于是就可以对这个新加入的机器开启相应的后台管理逻辑。另一方面,监控中心同样可以获取到机器下线的通知,这样便实现了对机器上/下线的检测,同时能够很容易的获取到在线的机器列表,对于大规模的扩容和容量评估都有很大的帮助。
3.3.6 Master 选举
分布式系统中 Master 是用来协调集群中其他系统单元,具有对分布式系统状态更改的决定权。比如一些读写分离的应用场景,客户端写请求往往是 Master 来处理的。
利用常见关系型数据库中的主键特性来实现也是可以的,集群中所有机器都向数据库中插入一条相同主键 ID 的记录,数据库会帮助我们自动进行主键冲突检查,可以保证只有一台机器能够成功。
但是有一个问题,如果插入成功的和护短机器成为 Master 后挂了的话,如何通知集群重新选举 Master?
利用 ZooKeeper 创建节点 API 接口,提供了强一致性,能够很好保证在分布式高并发情况下节点的创建一定是全局唯一性。
集群机器都尝试创建节点,创建成功的客户端机器就会成为 Master,失败的客户端机器就在该节点上注册一个 Watcher 用于监控当前 Master 机器是否存活,一旦发现 Master 挂了,其余客户端就可以进行选举了。
3.3.7 分布式队列
FIFO
使用 ZooKeeper 实现 FIFO 队列,入队操作就是在 queue_fifo
下创建自增序的子节点,并把数据(队列大小)放入节点内。出队操作就是先找到 queue_fifo
下序号最下的那个节点,取出数据,然后删除此节点。
/queue_fifo
|
├── /host1-000000001
├── /host2-000000002
├── /host3-000000003
└── /host4-000000004
创建完节点后,根据以下步骤确定执行顺序:
- 通过
get_children()
接口获取/queue_fifo
节点下所有子节点 - 通过自己的节点序号在所有子节点中的顺序
- 如果不是最小的子节点,那么进入等待,同时向比自己序号小的最后一个子节点注册 Watcher 监听
- 接收到 Watcher 通知后重复 1
Barrier
Barrier就是栅栏或者屏障,适用于这样的业务场景:当有些操作需要并行执行,但后续操作又需要串行执行,此时必须等待所有并行执行的线程全部结束,才开始串行,于是就需要一个屏障,来控制所有线程同时开始,并等待所有线程全部结束。
image利用 ZooKeeper 的实现,开始时 queue_barrier
节点是一个已经存在的默认节点,并且将其节点的数据内容赋值为一个数字 n 来代表 Barrier 值,比如 n=10 代表只有当 /queue_barrier
节点下的子节点个数达到10才会打开 Barrier。之后所有客户端都会在 queue_barrier
节点下创建一个临时节点,如 queue_barrier/host1
。
如何控制所有线程同时开始? 所有的线程启动时在 ZooKeeper 节点 /queue_barrier
下插入顺序临时节点,然后检查 /queue/barrier
下所有 children 节点的数量是否为所有的线程数,如果不是,则等待,如果是,则开始执行。具体的步骤如下:
-
getData()
获取/queue_barrier
节点的数据内容 -
getChildren()
获取/queue_barrier
节点下的所有子节点,同时注册对子节点列表变更的 Watche 监听。 - 统计子节点的个数
- 如果子节点个数不足10,那么进入等待
- 接收 Watcher 通知后,重复2
如何等待所有线程结束? 所有线程在执行完毕后,都检查 /queue/barrier
下所有 children 节点数量是否为0,若不为0,则继续等待。
用什么类型的节点? 根节点使用持久节点,子节点使用临时节点,根节点为什么要用持久节点?首先因为临时节点不能有子节点,所以根节点要用持久节点,并且在程序中要判断根节点是否存在。 子节点为什么要用临时节点?临时节点随着连接的断开而消失,在程序中,虽然会删除临时节点,但可能会出现程序在节点被删除之前就 crash了,如果是持久节点,节点不会被删除。
三、参考
https://zhuanlan.zhihu.com/p/75161633
https://zhuanlan.zhihu.com/p/59669985