ZooKeeper

2020-10-22  本文已影响0人  1519f8ccc7b0

一、宏观

image.png

1.1 是什么

zookeeper是用于分布式中一致性处理的框架,简单的说 zookeeper = 文件系统 (保证集群内数据一致性)+监听通知机。

1.2 重要概念

1.3 架构图

image.png

1.4 有哪些应用场景

分布式应用程序可以基于 ZooKeeper 实现一下功能:

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为了最大化保证数据强一致性,牺牲了可用性(选主期间不可用)。

image.png

二、中观

2.1 数据模型

ZK的数据结构与linux文件系统很类似,和文件系统不同的是,zk的数据存储是是结构化存储,没有文件和目录的概念,文件和目录被抽象成了节点(node),也叫znode。节点有永久和临时两种类型,永久节点不会随着该节点的session的结束而被删除,除非显示delete才会被删除。临时节点在session结束后,会被自动删除。

每个znode由三个部分组成:

image.png

在 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协议有两种模式,恢复模式和广播模式。

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启动,给自己投票,然后发投票信息,由于其它机器还没有启动所以它收不到反馈信息,服务器1的状态一直属于Looking。
  2. 服务器2启动,给自己投票,同时与之前启动的服务器1交换结果,由于服务器2的编号大所以服务器2胜出,但此时投票数没有大于半数,所以两个服务器的状态依然是LOOKING。
  3. 服务器3启动,给自己投票,同时与之前启动的服务器1,2交换信息,由于服务器3的编号最大所以服务器3胜出,此时投票数正好大于半数,所以服务器3成为领导者,服务器1,2成为小弟。
  4. 服务器4启动,给自己投票,同时与之前启动的服务器1,2,3交换信息,尽管服务器4的编号大,但之前服务器3已经胜出,所以服务器4只能成为小弟。

(2) 非全新集群选举(有数据)
对于运行正常的zookeeper集群,中途有机器down掉,需要重新选举时,选举过程就需要加入数据ID、服务器ID、和逻辑时钟。

  1. 逻辑时钟小的选举结果被忽略,重新投票;(除去选举次数不完整的服务器)
  2. 统一逻辑时钟后,数据id大的胜出;(选出数据最新的服务器)
  3. 数据id相同的情况下,服务器id大的胜出。(数据相同的情况下, 选择服务器id最大,即权重最大的服务器)

3.3 应用场景

3.3.1 分布式锁

锁的定义

ZooKeeper 上的一个 节点(node)可以表示一个锁。


image.png
排它锁

如果事务 T1 对数据对象 O1 加上了排他锁,那么加锁期间,只允许事务 T1 对 O1 进行读取和更新操作。核心是保证当前有且仅有一个事务获得锁,并且锁释放后,所有正在等待获取锁的事务都能够被通知到。

共享锁

如果事务 T1 对数据对象 O1 加上了共享锁,那么当前事务 T1 只能对 O1 进行读取操作,其他事务也只能对这个数据对象加共享锁,直到数据对象上的所有共享锁都被释放。

/
├── /host1-R-000000001
├── /host2-R-000000002
├── /host3-W-000000003
├── /host4-R-000000004
├── /host5-R-000000005
├── /host6-R-000000006
└── /host7-W-000000007

3.3.3 负载均衡

负载均衡是一种手段,用来把对某种资源的访问分摊给不同的设备,从而减轻单点的压力。
实现的思路:

image.png

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](下文我们以“主机节点”代表这个节点),如下图所示。

image

当 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

创建完节点后,根据以下步骤确定执行顺序:

  1. 通过 get_children() 接口获取 /queue_fifo 节点下所有子节点
  2. 通过自己的节点序号在所有子节点中的顺序
  3. 如果不是最小的子节点,那么进入等待,同时向比自己序号小的最后一个子节点注册 Watcher 监听
  4. 接收到 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 节点的数量是否为所有的线程数,如果不是,则等待,如果是,则开始执行。具体的步骤如下:

  1. getData() 获取 /queue_barrier 节点的数据内容
  2. getChildren() 获取 /queue_barrier 节点下的所有子节点,同时注册对子节点列表变更的 Watche 监听。
  3. 统计子节点的个数
  4. 如果子节点个数不足10,那么进入等待
  5. 接收 Watcher 通知后,重复2

如何等待所有线程结束? 所有线程在执行完毕后,都检查 /queue/barrier 下所有 children 节点数量是否为0,若不为0,则继续等待。

用什么类型的节点? 根节点使用持久节点,子节点使用临时节点,根节点为什么要用持久节点?首先因为临时节点不能有子节点,所以根节点要用持久节点,并且在程序中要判断根节点是否存在。 子节点为什么要用临时节点?临时节点随着连接的断开而消失,在程序中,虽然会删除临时节点,但可能会出现程序在节点被删除之前就 crash了,如果是持久节点,节点不会被删除。

三、参考

https://zhuanlan.zhihu.com/p/75161633
https://zhuanlan.zhihu.com/p/59669985

上一篇下一篇

猜你喜欢

热点阅读