ZooKeeper学习笔记
概述
官方文档上这么解释zookeeper,它是一个分布式服务框架,是Apache Hadoop 的一个子项目,它主要是用来解决分布式应用中经常遇到的一些数据管理问题,如:统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等。
上面的解释有点抽象,简单来说zookeeper=文件系统+监听通知机制。
1. 文件系统
Zookeeper维护一个类似文件系统的数据结构:
每个子目录项如 NameService 都被称作为 znode(目录节点),和文件系统一样,我们能够自由的增加、删除znode,在一个znode下增加、删除子znode,唯一的不同在于znode是可以存储数据的。
有四种类型的znode:
-
PERSISTENT-持久化节点
客户端与服务端断开连接后,该节点依旧存在
-
PERSISTENT_SEQUENTIAL-持久化有序节点
客户端与服务端断开连接后,该节点依旧存在,只是Zookeeper给该节点名称进行顺序编号
-
EPHEMERAL-临时节点
客户端与服务端断开连接后,该节点被删除
- EPHEMERAL_SEQUENTIAL-临时有序节点
客户端与服务端断开连接后,该节点被删除,只是Zookeeper给该节点名称进行顺序编号
ZooKeeper 将所有数据存储在内存中,数据模型是一棵树(Znode Tree),由斜杠(/)的进行分割的路径,就是一个 Znode,例如/foo/path1。每个上都会保存自己的数据内容,同时还会保存一系列属性信息。
在 Zookeeper 中,Node 可以分为持久节点 和 临时节点两类。
所谓持久节点是指一旦这个 ZNode 被创建了,除非主动进行 ZNode 的移除操作,否则这个 ZNode 将一直保存在 ZooKeeper 上。
而临时节点就不一样了,它的生命周期和客户端会话绑定,一旦客户端会话失效,那么这个客户端创建的所有临时节点都会被移除。(客户端与服务器端断开连接时)
另外,ZooKeeper 还允许用户为每个节点添加一个特殊的属性:SEQUENTIAL。一旦节点被标记上这个属性,那么在这个节点被创建的时候,ZooKeeper 会自动在其节点名后面追加上一个整型数字,这个整型数字是一个由父节点维护的自增数字。如:sNode0000000002,sNode0000000003
节点特性:
-
同一级节点 key 名称是唯一的
-
创建节点时,必须要带上全路径
-
session 关闭,临时节点清除
- 可监听节点变化
2. 监听通知机制
客户端注册监听它关心的目录节点,当目录节点发生变化(数据改变、被删除、子目录节点增加删除)时,zookeeper会通知客户端。
Zookeeper能做什么
- 统一命名服务
- 状态同步服务
- 集群管理
- 分布式应用配置项
- 数据发布/订阅
- 负载均衡
- 分布式协调/通知
- Master选举
- 分布式锁
- RPC的注册中心
- 配置中心
。。。等
特点
-
最终一致性:无论客户端连到哪一个 ZooKeeper 服务器上,其看到的服务端数据都是一致的。
-
顺序一致性:从同一客户端发起的事务请求,最终将会严格地按照顺序被应用到 ZooKeeper 中去。
-
原子性:所有事务请求的处理结果在整个集群中所有机器上的应用情况是一致的,也就是说,要么整个集群中所有的机器都成功应用了某一个事务,要么都没有应用。
-
可靠性:一旦一次更改请求被应用,更改的结果就会被持久化,直到被下一次更改覆盖。
-
等待无关(wait-free):慢的或者失效的client不得干预快速的client的请求,使得每个client都能有效的等待。
-
实时性:Zookeeper保证客户端将在一个时间间隔范围内获得服务器的更新信息,或者服务器失效的信息。但由于网络延时等原因,Zookeeper不能保证两个客户端能同时得到刚更新的数据,如果需要最新数据,应该在读数据之前调用sync()接口。
安装&运行
为了测试zookeeper的选举机制,起3台zookeeper服务器:
使用docker-compose 启动zookeeper集群,创建docker-compose.yml,在该文件所在目录下执行。
# version 对应关系 https://docs.docker.com/compose/compose-file/ version: '3.8' services: zoo1: image: zookeeper restart: always container_name: zoo1 ports: - "2181:2181" volumes: - /Users/dong320/dockerTest/zk/zoo1/data:/data - /Users/dong320/dockerTest/zk/zoo1/datalog:/datalog environment: ZOO_MY_ID: 1 ZOO_SERVERS: server.1=zoo1:2888:3888;2181 server.2=zoo2:2888:3888;2181 server.3=zoo3:2888:3888;2181 zoo2: image: zookeeper restart: always container_name: zoo2 ports: - "2182:2181" volumes: - /Users/dong320/dockerTest/zk/zoo2/data:/data - /Users/dong320/dockerTest/zk/zoo2/datalog:/datalog environment: ZOO_MY_ID: 2 ZOO_SERVERS: server.1=zoo1:2888:3888;2181 server.2=zoo2:2888:3888;2181 server.3=zoo3:2888:3888;2181 zoo3: image: zookeeper restart: always container_name: zoo3 ports: - "2183:2181" volumes: - /Users/dong320/dockerTest/zk/zoo3/data:/data - /Users/dong320/dockerTest/zk/zoo3/datalog:/datalog environment: ZOO_MY_ID: 3 ZOO_SERVERS: server.1=zoo1:2888:3888;2181 server.2=zoo2:2888:3888;2181 server.3=zoo3:2888:3888;2181
# ;2181 还不知道什么意思,不加的话bin/zkServer.sh status会报错
docker命令:
docker-compose up -d # 后台运行 docker-compose config # 查看配置 docker exec -it 容器id /bin/bash bin/zkServer.sh status # 查看zookeeper集群状态(follower/leader) bin/zkCli.sh # 启动客户端
客户端命令:
help # 查看帮助,可以看到多数的命令 create /demo "test" # 创建持久节点 create -e /demo/eNode "eNodTest" # 创建临时节点,只能创建一个 create -s /demo/sNode "sNodeTest" # 创建临时有序节点,可以创建多个,如sNode0000000002,sNode0000000003 set /demo "test2" # 修改节点数据 addWatch /demo # 监听节点,接收到监听后可以执行其他命令,如:get /demo delete /demo/eNode # 删除节点 ls -R /demo # 列出节点 quit # 退出客户端,退出后,临时节点,临时有序节点都将自动删除
zk可视化客户端:
https://issues.apache.org/jira/secure/attachment/12436620/ZooInspector.zip
解压,进入目录ZooInspector\build
运行或mac下双击zookeeper-dev-ZooInspector.jar:
java -jar zookeeper-dev-ZooInspector.jar
点击左上角连接按钮,输入Zookeeper服务地址:ip:2181
点击OK,就可以查看Zookeeper节点信息了。
zoo.cfg配置说明:
-
tickTime=2000:这个时间是作为 Zookeeper 服务器之间或客户端与服务器之间维持心跳的时间间隔,也就是每个 tickTime 时间就会发送一个心跳。
-
initLimit=10:这个配置项是用来配置 Zookeeper 接受客户端(这里所说的客户端不是用户连接 Zookeeper 服务器的客户端,而是 Zookeeper 服务器集群中连接到 Leader 的 Follower 服务器)初始化连接时最长能忍受多少个心跳时间间隔数。当已经超过 10个心跳的时间(也就是 tickTime)长度后 Zookeeper 服务器还没有收到客户端的返回信息,那么表明这个客户端连接失败。总的时间长度就是 10*2000=20 秒
-
syncLimit=5:这个配置项标识 Leader 与 Follower 之间发送消息,请求和应答时间长度,最长不能超过多少个 tickTime 的时间长度,总的时间长度就是 5*2000=10秒
-
dataDir:数据文件目录+数据持久化路径,主要用于保存 Zookeeper 中的数据。
- clientPort=2181:这个端口就是客户端连接 Zookeeper 服务器的端口,Zookeeper 会监听这个端口,接受客户端的访问请求。
-
server.A=B:C:D:
- 其中 A 是一个数字,表示这个是第几号服务器;
- B 是这个服务器的 ip 地址;
- C 表示的是这个服务器与集群中的 Leader 服务器交换信息的端口;
- D 表示的是万一集群中的 Leader 服务器挂了,需要一个端口来重新进行选举,选出一个新的 Leader,而这个端口就是用来执行选举时服务器相互通信的端口。如果是伪集群的配置方式,由于 B 都是一样,所以不同的 Zookeeper 实例通信端口号不能一样,所以要给它们分配不同的端口号。
Zookeeper工作原理&介绍
Zookeeper 的核心是原子广播,这个机制保证了各个Server之间的同步。实现这个机制的协议叫做Zab协议。Zab协议有两种模式,它们分别是恢复模式(选主)和广播模式(同步)。当服务启动或者在领导者崩溃后,Zab就进入了恢复模式,当领导者被选举出来,且大多数Server完成了和 leader的状态同步以后,恢复模式就结束了。状态同步保证了leader和Server具有相同的系统状态。
为了保证事务的顺序一致性,zookeeper采用了递增的事务id号(zxid)来标识事务。所有的提议(proposal)都在被提出的时候加上了zxid。实现中zxid是一个64位的数字,它高32位是epoch用来标识leader关系是否改变,每次一个leader被选出来,它都会有一个新的epoch,标识当前属于那个leader的统治时期。低32位用于递增计数。
Zookeeper 下 Server工作状态,每个Server在工作过程中有三种状态:
- LOOKING:当前Server不知道leader是谁,正在搜寻
-
LEADING:当前Server即为选举出来的leader
-
FOLLOWING:leader已经选举出来,当前Server与之同步
Stat结构体
在前面我们已经提到,Zookeeper 的每个 ZNode 上都会存储数据,对应于每个 ZNode,Zookeeper 都会为其维护一个叫作 Stat 的数据结构。
- czxid - 创建节点的事务 zxid,每次修改 ZooKeeper 状态都会收到一个 zxid 形式的时间戳,也就是 ZooKeeper事务ID。事务ID 是 ZooKeeper中所有修改总的次序。每个修改都有唯一的 zxid,如果 zxid1 小于 zxid2,那么 zxid1 在 zxid2 之前发生。
- 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 子节点数量
Watcher监听机制
Watcher(事件监听器),是 ZooKeeper 中的一个很重要的特性。
ZooKeeper 允许用户在指定节点上注册一些 Watcher,并且在一些特定事件触发的时候,ZooKeeper 服务端会将事件通知到感兴趣的客户端上去,该机制是 ZooKeeper 实现分布式协调服务的重要特性。
zookeeper 的 watcher 机制,可以分为四个过程:
- 客户端注册 watcher。
- 服务端处理 watcher。
- 服务端触发 watcher 事件。
- 客户端回调 watcher。
其中客户端注册 watcher 有三种方式,调用客户端 API 可以分别通过 getData、exists、getChildren 实现。
客户端发送请求给服务端是通过 TCP 长连接建立网络通道,底层默认是通过 java 的 NIO 方式,也可以配置 netty 实现方式。
ACL
ZooKeeper 采用 ACL(AccessControlLists)策略来进行权限控制,类似于 UNIX 文件系统的权限控制。
ZooKeeper 定义了 5 种权限:
- CREATE: 创建子节点权限
- READ: 获取节点数据和子字节点列表的权限
- WRITE: 更新节点数据的权限
- DELETE: 删除子节点的权限
- ADMIN: 设置节点ACL的权限
其中尤其需要注意的是,CREATE 和 DELETE 这两种权限都是针对子节点的权限控制
ZooKeeper 集群角色介绍
最典型集群模式:Master/Slave 模式(主备模式)。在这种模式中,通常 Master 服务器作为主服务器提供写服务,其他的 Slave 服务器从服务器通过异步复制的方式获取 Master 服务器最新的数据提供读服务。
但是,在 ZooKeeper 中没有选择传统的 Master/Slave 概念,而是引入了Leader、Follower 和 Observer 三种角色。如下图所示:
ZooKeeper 集群中的所有机器通过一个 Leader 选举过程来选定一台称为 “Leader” 的机器。
Leader 既可以为客户端提供写服务又能提供读服务。除了 Leader 外,Follower 和 Observer 都只能提供读服务。
Follower 和 Observer 唯一的区别在于 Observer 机器不参与 Leader 的选举过程,也不参与写操作的“过半写成功”策略,因此 Observer 机器可以在不影响写性能的情况下提升集群的读性能
角色 | 描述 | |
领导者(Leader) | 领导者负责进行投票的发起和决议,更新系统状态和数据 | |
学习者(Learner) | 跟随者 (Follower) |
Follwer用户号接受客户请求并向客户端返回结果,在选主过程中参与投票 |
观察者 |
Observer可以接口客户端连接,将写请求转发给leader节点。 但Observer不参与投票过程,只同步leader的状态。 Observer的目的是为了扩展系统,提高读取速度 |
|
客户端(Client) | 请求发起方 |
Zookeeper选举流程
选举机制中的概念
Zookeeper为了保证各节点的协同工作,在工作时需要一个Leader 角色,而Zookeeper默认采用FastLeaderElection算法,且投票数大于平数则胜出的机制,
在介绍选举机制前,首先了解选举涉及的相关概念。
1. 服务器ID (myid)
这是在配置集群时设置的myid参数文件,且参数分别表示为服务器1、服务器2、服务器3,编号越大在FastLeaderElection算法中的权重越大。
2. 选举状态
在选举过程中,Zookeeper 服务器有四种状态,它们分别为竞选状态(LOOKING)、随从状态(FOLLOWING,同步leader状态,参与投票)、观察状态(OBSERVING,同步leader状态,不参与投票)、领导者状态(LEADING)。
3. 数据ID (zxid)
是服务器中存放的最新数据版本号,该值越大说明数据越新,在选举过程中数据越新权重越大。
4. 逻辑时钟 (epoch-logicalclock)
通俗的讲,逻辑时钟被称为投票次数,同一轮投票过程中的逻辑时钟值是相同的,逻辑时钟起始值为0,每投完一次票,这个数据就会增加。然后,与接收到其它服务器返回的投票信息中的数值相比较,根据不同的值做出不同的判断。如果某台机器宕机,那么这台机器不会参与投票,因此逻辑时钟也会比其他的低。
选举机制的类型
Zookeeper选举机制有两种类型,分别为全新集群选举和非全新集群选举,下面分别对两种类型进行详细讲解。
1. 全新集群选举
全新集群选举是新搭建起来的,没有数据ID和逻辑时钟的数据影响集群的选举。
假设,目前有5台服务器,它们的编号分别是1-5,按编号依次启动Zookeeper服务。
下面来讲解全新集群选举的过程:
- 步骤1: 服务器1启动,首先,会给自己投票;其次,发投票信息,由于其它机器还没有启动所以它无法接收到投票的反馈信息,因此服务器1的状态一直属于LOOKING状态。
- 步骤2: 服务器2启动,首先,会给自己投票;其次,在集群中启动Zookeeper服务的机器发起投票对比,这时它会与服务器1交换结果,由于服务器2的编号大,所以服务器2胜出,此时服务器1会将票投给服务器2,但此时服务器2的投票数并没有大于集群半数(2<5/2),所以两个服务器的状态依然是LOOKING状态。
- 步骤3: 服务器3启动,首先会给自己投票;其次,与之前启动的服务器1.2交换信息,由于服务器3的编号最大所以服务器3胜出,那么服务器1、2会将票投给服务器3,此时投票数正好大于半数(3>5/2),所以服务器3成为领导者状态,服务器1. 2成为追随者状态。
- 步骤4: 服务器4启动,首先,给自己投票;其次,与之前启动的服务器1、2、3交换信息,尽管服务器4的编号大,但是服务器3已经胜出。所以服务器4只能成为追随者状态.
- 步骤5: 服务器5启动,同服务器4一样,均成为追随者状态。
2. 非全新集群选举
对于正常运行的Zookeeper集群,一旦中途有服务器宕机,则需要重新选举时,选举的过程中就需要引入服务器ID、数据ID和逻辑时钟。这是由于Zookeeper集群已经运行过一-段时间,那么服务器中就会存在运行的数据。
下面来讲解非全新集群选举的过程。
- 步骤1: 首先,统计逻辑时钟是否相同,逻辑时钟小,则说明途中可能存在宕机问题,因此数据不完整,那么该选举结果被忽略,重新投票选举;
- 步骤2: 其次,统一逻辑时钟后,对比数据ID值,数据ID反应数据的新旧程度,因此数据ID大的胜出。
- 步骤3: 如果逻辑时钟和数据ID都相同的情况下,那么比较服务器ID (编号),值大则胜出;
简单的讲,非全新集群选举时是优中选优,保证Leader是Zookeeper集群中数据最完整、最可靠的一台服务器。
Zookeeper同步流程
在 Zookeeper 中,主要依赖 ZAB 协议来实现分布式数据一致性。
ZAB 协议分为两部分:
- 消息广播
- 崩溃恢复
消息广播
Zookeeper 使用单一的主进程 Leader 来接收和处理客户端所有事务请求,并采用 ZAB协议的原子广播协议,将事务请求以 Proposal 提议广播到所有 Follower 节点,当集群中有过半的Follower 服务器进行正确的 ACK 反馈,那么Leader就会再次向所有的 Follower 服务器发送commit 消息,将此次提案进行提交。这个过程可以简称为 2pc 事务提交,整个流程可以参考下图,注意 Observer 节点只负责同步 Leader 数据,不参与 2PC 数据同步过程。
崩溃恢复
在正常情况消息广播情况下能运行良好,但是一旦 Leader 服务器出现崩溃,或者由于网络原理导致 Leader 服务器失去了与过半 Follower 的通信,那么就会进入崩溃恢复模式,需要选举出一个新的 Leader 服务器。在这个过程中可能会出现两种数据不一致性的隐患,需要 ZAB 协议的特性进行避免。
- Leader 服务器将消息 commit 发出后,立即崩溃
- Leader 服务器刚提出 proposal 后,立即崩溃
ZAB 协议的恢复模式使用了以下策略:
- 选举 zxid 最大的节点作为新的 leader
- 新 leader 将事务日志中尚未提交的消息进行处理
参考: