type
status
date
slug
summary
tags
category
icon
password
1、概述
ZooKeeper是一个典型的分布式数据一致性的解决方案,分布式应用程序可以基于它实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master选举、分布式锁和分布式队列等功能。ZooKeeper 可以保证如下分布式一致性特性。
- 顺序一致性:从同一个客户端发起的事务请求,最终将会严格地按照其发起顺序被应用到 ZooKeeper 中去。
- 原子性:所有事务请求的处理结果在整个集群中所有机器上的应用情况是一致的,也就是说,要么整个集群所有机器都成功应用了某一个事务,要么都没有应用,一定不会出现集群中部分机器应用了该事务,而另外一部分没有应用的情况。
- 单一视图:无论客户端连接的是哪个 ZooKeeper 服务器,其看到的服务端数据模型都是一致的。
- 可靠性:一旦服务端成功地应用了一个事务,并完成对客户端的响应,那么该事务所引起的服务端状态变更将会被一直保留下来,除非有另一个事务又对其进行了变更。
- 实时性:通常人们看到实时性的第一反应是,一旦一个事务被成功应用,那么客户端能够立即从服务端上读取到这个事务变更后的最新数据状态。这里需要注意的是,ZooKeeper 仅仅保证在一定的时间段内,客户端最终一定能够从服务端上读取到最新的数据状态。
ZooKeeper 的设计目标是将那些复杂且容易出错的分布式一致性服务封装起来,构成一个高效可靠的原语集,并以一系列简单易用的接口提供给用户使用。
2、数据结构
2.1 ZNode结构
ZooKeeper 使得分布式程序能够通过一个共享的、树型结构的名字空间来进行相互协调。这里所说的树型结构的名字空间和标准的 Unix 文件系统非常类似,但没有引入传统文件系统中目录和文件等相关概念,而是使用了其特有的“数据节点”概念,我们称之为 ZNode。
ZNode 是 ZooKeeper 中数据的最小单元,ZNode 的节点路径标识方式和 Unix 文件系统路径非常相似,都是由一系列使用斜杠
/
进行分割的路径表示,例如 /foo/path1
。开发人员可以向这个节点中写入数据,也可以在节点下面创建子节点。所有 ZNode 按层次化结构进行组织,形成一棵树状结构。如下图所示:ZNode 按生命周期共分为四种类型:
- 持久节点(PERSISTENT):持久节点是 ZooKeeper 中最常见的一种节点类型。所谓持久节点,是指该数据节点被创建后,就会一直存在于ZooKeeper服务器上,直到有删除操作来主动清除这个节点。
- 持久顺序节点(PERSISTENT_SEQUENTIAL):持久顺序节点的基本特性和持久节点是一致的,额外的特性表现在顺序性上。在 ZooKeeper 中,每个父节点都会为它的第一级子节点维护一份顺序,用于记录下每个子节点创建的先后顺序。基于这个顺序特性,在创建子节点的时候,可以设置这个标记,那么在创建节点过程中,ZooKeeper 会自动为给定节点名加上一个 10 位的数字后缀,作为一个新的、完整的节点名。另外需要注意的是,这个数字后缀的上限是整型的最大值。
- 临时节点(EPHEMERAL):和持久节点不同的是,临时节点的生命周期和客户端的会话绑定在一起,也就是说,如果客户端会话失效,那么这个节点就会被自动清理掉。注意,这里提到的是客户端会话失效,而非TCP连接断开。另外,ZooKeeper 规定了不能基于临时节点来创建子节点,即临时节点只能作为叶子节点。
- 临时顺序节点(EPHEMERAL_SEQUENTIAL):临时顺序节点的基本特性和临时节点也是一致的,同样是在临时节点的基础上,添加了顺序的特性。
2.2 ZNode状态信息
我们先来了解一下ZXID(事务ID)的概念。
在 ZooKeeper 中,事务是指能够改变 ZooKeeper 服务器状态的操作,我们也称之为事务操作或更新操作,一般包括数据节点创建与删除、数据节点内容更新和客户端会话创建与失效等操作。对于每一个事务请求,ZooKeeper 都会为其分配一个全局唯一的事务 ID,用 ZXID 来表示,通常是一个64位的数字。每一个 ZXID 对应一次更新操作,从这些 ZXID 中可以间接地识别出 ZooKeeper 处理这些更新操作请求的全局顺序。
每个 ZNode 数据节点除了存储了数据内容之外,还存储了数据节点本身的一些状态信息,状态信息里面本身就包含了 ZXID 的相关信息。我们可以通过
get
命令来获取 ZNode 的状态信息。返回结果中第一行是当前数据节点的数据内容,从第二行开始就是节点的状态信息了,这其实就是数据节点的
Stat
对象的格式化输出,图7-3展示了 ZooKeeper 中 Stat
类的数据结构。Stat 类中包含了 ZooKeeper 上一个数据节点的所有状态信息,包括事务ID、版本信息和子节点个数等,下面是具体属性说明
对于ZooKeeper来说,无论数据内容是否变更,一旦有客户端调用了数据更新的接口,且更新成功,就会更新 dataVersion 值。
3、Watch机制
ZooKeeper 提供了分布式数据的发布/订阅功能。一个典型的发布/订阅模型系统定义了一种一对多的订阅关系,能够让多个订阅者同时监听某一个主题对象,当这个主题对象自身状态变化时,会通知所有订阅者,使它们能够做出相应的处理。在 ZooKeeper 中,引入了 Watcher 机制来实现这种分布式的通知功能。ZooKeeper 允许客户端向服务端注册一个 Watcher 监听,当服务端的一些指定事件触发了这个 Watcher,那么就会向指定客户端发送一个事件通知来实现分布式的通知功能。整个 Watcher 注册与通知过程如图所示。
我们可以看到,ZooKeeper 的 Watcher 机制由三部分组成:
- 客户端线程
- 客户端 WatchManager
- ZooKeeper 服务器
在具体工作流程上,简单地讲,客户端在向 ZooKeeper 服务器注册 Watcher 的同时,会将 Watcher 对象存储在客户端的 WatchManager 中。当 ZooKeeper 服务器端触发 Watcher 事件后,会向客户端发送通知,客户端线程从 WatchManager 中取出对应的 Watcher 对象来执行回调逻辑。
在 ZooKeeper 中,接口类Watcher用于表示一个标准的事件处理器,其定义了事件通知相关的逻辑,包含 KeeperState 和 EventType 两个枚举类,分别代表了通知状态和事件类型,同时定义了事件的回调方法:
process(WatchedEvent event)
。KeeperState 和 EventType 的类型如下图所示:
Watcher 机制的特点:
- 一次性:无论是服务端还是客户端,一旦一个 Watcher 被触发,ZooKeeper 都会将其从相应的存储中移除。因此,开发人员在 Watcher 的使用上要记住的一点是需要反复注册。这样的设计有效地减轻了服务端的压力。
- 客户端串行执行:客户端 Watcher 回调的过程是一个串行同步的过程,这为我们保证了顺序。
- 轻量:Watcher 通知非常简单,只会告诉客户端发生了事件,而不会说明事件的具体内容。例如针对 NodeDataChanged 事件,ZooKeeper 的 Watcher 只会通知客户端指定数据节点的数据内容发生了变更,而对于原始数据以及变更后的新数据都无法从这个事件中直接获取到,而是需要客户端主动重新去获取数据。
- 子节点触发父节点:子节点的增删改都能够触发父节点的 Watcher。
4、ACL权限控制
ZooKeeper 作为一个分布式协调框架,其内部存储的都是一些关乎分布式系统运行时状态的元数据,尤其是一些涉及分布式锁、Master 选举和分布式协调等应用场景的数据,会直接影响基于 ZooKeeper 进行构建的分布式系统的运行状态。因此,如何有效地保障 ZooKeeper 中数据的安全,从而避免因误操作而带来的数据随意变更导致的分布式系统异常就显得格外重要了。所幸的是,ZooKeeper 提供了一套完善的 ACL(Access Control List) 权限控制机制来保障数据的安全。
ZooKeeper 的 ACL 机制包含三个方面,分别是:权限模式(Scheme)、授权对象(ID)和权限(Permission),通常使用
scheme:id:permission
来标识一个有效的 ACL 信息。4.1 Scheme、ID和Permission
4.1.1 Schema
权限模式用来确定权限验证过程中使用的检验策略。在 ZooKeeper 中,开发人员使用最多的就是以下四种权限模式:
- IP:IP模式通过IP地址粒度来进行权限控制,例如配置了
ip:192.168.0.110
,即表示权限控制都是针对这个 IP 地址的。同时,IP 模式也支持按照网段的方式进行配置,例如ip:192.168.0.1/24
表示针对 192.168.0.* 这个IP段进行权限控制。
- Digest:Digest 是最常用的权限控制模式,也更符合我们对于权限控制的认识,其以类似于
username:password
形式的权限标识来进行权限配置,便于区分不同应用来进行权限控制。当我们通过username:password
形式配置了权限标识后,我们可以先调用 ZooKeeper 的接口对其先后进行两次编码处理,分别是 SHA-1 算法加密和 BASE64 编码,具体实现是DigestAuthenticationProvider.generateDigest(String idPassword)
函数。
- World:World 是一种最开放的权限控制模式,从其名字中也可以看出,事实上这种权限控制方式几乎没有任何作用,数据节点的访问权限对所有用户开放,即所有用户都可以在不进行任何权限校验的情况下操作 ZooKeeper 上的数据。另外,World 模式也可以看作是一种特殊的 Digest 模式,它只有一个权限标识,即
world:anyone
。
- Super:Super模式,顾名思义就是超级用户的意思,也是一种特殊的 Digest 模式。在 Super 模式下,超级用户可以对任意 ZooKeeper 上的数据节点进行任何操作。
4.1.2 ID
授权对象指的是权限赋予的用户或一个指定实体,例如 IP 地址或是机器等。在不同的权限模式下,授权对象是不同的,下面列出了各个权限模式和授权对象之间的对应关系。
4.1.3 Permission
权限就是指那些通过权限检查后可以被允许执行的操作。在 ZooKeeper 中,所有对数据的操作权限分为以下五大类:
- CREATE(C):数据节点的创建权限,允许授权对象在该数据节点下创建子节点。
- DELETE(D):子节点的删除权限,允许授权对象删除该数据节点的子节点。
- READ(R):数据节点的读取权限,允许授权对象访问该数据节点并读取其数据内容或子节点列表等。
- WRITE(W):数据节点的更新权限,允许授权对象对该数据节点进行更新操作。
- ADMIN(A):数据节点的管理权限,允许授权对象对该数据节点进行ACL相关的设置操作。
4.2 ACL相关命令
通过两种方式进行 ACL 的设置。
- 使用
create
命令在创建数据节点的同时设置 ACL 权限。
- 使用
setAcl
命令单独对已经存在的数据节点设置 ACL 权限。
4.2.1 create
设置ACL权限
命令格式如下:
假如我们要设置创建节点
/mcb/acl1
,并为用户 root:12345
设置 cdrwa
权限,首先调用 DigestAuthenticationProvider.generateDigest(String idPassword)
函数进行加密编码处理,如下所示:输出如下所示:
执行下面命令,在创建 ZNode 的时候设置 ACL 权限。
这时候我们如果直接查看节点
/mcb/acl1
信息,会报无权限错误可以先使用下面命令为当前会话添加用户
root:12345
再查看节点
/mcb/acl1
信息,执行成功4.2.2 setAcl
命令设置ACL权限
使用
setAcl
命令可以单独对已经存在的数据节点进行 ACL 设置,命令如下所示4.2.3 常用命令总结
ACL相关命令
命令 | 描述 | 语法 |
getAcl | 获取某个节点的acl权限信息 | getAcl <path> |
setAcl | 设置某个节点的acl权限信息 | setAcl <path> scheme:id:permissions |
addauth | 添加认证用户,注册时输入明文密码,登陆时也是使用明文密码,但zk存储时会对其先后进行SHA-1和BASE64两次编码处理,密码以加密的形式存储。 | addauth <scheme> user:password |
Schema示例
schema | id | 示例 |
world | 只有一个ID:anyone | setAcl <path> world:anyone:crdwa |
auth | id为username:passwd(需要提前创建) | setAcl <path> auth:username:password:crdwa |
digest | id为username:passwd(任意自定义),例如username:BASE64(SHA-1(password)) | setAcl <path> digest:username:1g4T1B5w+se9ntA6Ckp90uPaJ30=:crdwa |
ip | 通常是一个IP地址或是IP段,例如 192.168.0.110 或 192.168.0.1/24 | setAcl <path> ip:192.168.1.1:crdwa |
super | 与digest模式一致 | setAcl <path> super:username:1g4T1B5w+se9ntA6Ckp90uPaJ30=:crdwa |
5、常见问题
5.1 脑裂问题
ZooKeeper 集群中,各个节点间的网络通信不良时,容易出现脑裂(split-brain)现象。
- 集群中的部分 Follower 节点监听不到 Leader 节点的心跳,就会认为 Leader 节点出了问题;
- 这些监听不到 Leader 节点心跳的 Follower 节点就会选举出新 Leader 节点;
- 新的 Leader 和旧 Leader 节点 和各自的 Fllower 节点组成多个小集群。
一个集群中存在多个 Leader 节点,这就是脑裂现象。
脑裂问题会导致原本应该是统一的一个集群对外提供服务的,现在变成了两个集群同时对外提供服务,如果过了一会,断了的网络突然联通了,那么此时就会出现问题了,两个集群刚刚都对外提供服务了,数据该怎么合并,数据冲突怎么解决等等问题。
ZooKeeper 采用了Quorums 的方式解决脑裂问题。Quorums 是指通过设置法定人数,进而确定集群的容忍度,当集群中存活的节点少于法定人数,集群将不可用。比如:
- 3 个节点的集群中,Quorums = 2:集群可以容忍 (3 - 2 = 1) 个节点失败,这时候还能选举出 leader,集群仍然可用;
- 4 个节点的集群中,Quorums = 3:集群同样可以容忍 1 个节点失败,如果 2 个节点失败,那整个集群就不可用了。
由此可以看出奇数 n 个节点和偶数 n + 1 个节点,可以容忍失败的节点数是一样,所以 ZooKeeper 一般都是部署奇数个节点(3 个及以上),减少资源浪费。
ZooKeeper 规定 Quorums 过半机制:只有获得超过半数节点的投票,才能选举出 Leader。这种方式可以确保要么选出唯一的 Leader,要么选举失败。
为什么 Quorums 过半机制可以解决脑裂问题呢?假如有五台服务器组成一个集群,部署在两个机房,其中一个机房三台机器,另外一个机房两台机器。
当两个机房的网络出现问题后,第一个机房因为有三台机器,三台机器都投票给同一个机器,满足过半机制的节点数(n ≥ 3),所以这个机房可以选举出一个 Leader。另外一个机房只有两台机器,不满足过半机制,所以不能选举出 Leader。整个集群仍然只有一个 Leader 对外提供服务,避免了脑裂问题。
5.2 羊群效应
羊群是一种很散乱的组织,平时在一起也是盲目地左冲右撞,但一旦有一只头羊动起来,其他的羊也会不假思索地一哄而上,全然不顾前面可能有狼或者不远处有更好的草。因此,羊群效应就是比喻人都有一种从众心理,从众心理很容易导致盲从,而盲从往往会陷入骗局或遭到失败。
Zookeeper 的羊群效应是指多个客户端同时 wtach 同一个节点,当该节点变化时所有客户端都会收到通知,但只有一个客户端能最终执行成功,大量多余的请求会造成很大的网络开销,影响 Zookeeper 服务器的稳定性。
解决方案:每个客户端在选择 watch 节点的时候,只需要关注比自己序号小的那个节点。
- 所有客户端都尝试去创建临时有序节点以获取锁
- 序号最小的临时有序节点获得锁
- 未获取到锁的客户端给自己的上一个临时有序节点添加监听
- 获得锁的客户端进行自己的操作,操作完成之后解锁,删除自己的临时有序节点
- 当下一个监听到自己的上一个临时有序节点释放了锁,尝试自己去加锁
- 之后剩下的客户端重复加锁和解锁的操作
- Author:mcbilla
- URL:http://mcbilla.com/article/3751224c-b905-4104-aabb-de9c2d0a0258
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!
Relate Posts