Overview
ZooKeeper(简称ZK)是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,它是集群的管理者,监视着集群中各个节点的状态根据节点提交的反馈进行下一步合理操作。最终,将简单易用的接口和性能高效、功能稳定的系统提供给用户
Zookeeper主要服务于分布式系统,应用场景包括统一配置管理、统一命名服务、分布式锁和集群管理,使用分布式系统就无法避免对节点管理的问题(感知节点的状态、对节点的管理等),而由于这些问题处理起来相对比较麻烦且加深了系统的复杂度,Zookeeper正是解决这些问题的通用中间件
特性
- 最终一致性,为客户端展示同一视图
- 可靠性,如果一条消息被一台服务器接收成功,那么它将被所有服务器接收
- 实时性,ZK不能保证两个客户端同时收到刚更新的数据,如果需要更新数据,在读数据之前需要调用Sync接口
- 等待无关(wait-free),慢的或者失效的客户端不干预快速的客户端请求
- 原子性,更新操作要么成功,要么失败,没有中间状态
- 顺序性,对于所有Server,同一消息发布顺序一致
基本概念
Zookeeper提供基于类似于Unix文件系统的目录节点树方式的数据存储,这是一个共享的内存中的树型结构。每个节点叫做ZNode,每一个节点可以通过路径来标识,结构图如下
/
├── /Apps
|
│ ├── /App1
|
│ └── /App2
|
│ ├── /SubApp1
│ └── /SubApp2
│
├── /Configuration
|
│ ├── /Mysql1
│ ├── /Mysql2
│ └── /Mysql3
│
├── /GroupMembers
|
│ ├── /Member1
│ └── /Member2
│
└── /NameService
|
├── /Server1
└── /Server2
Zk的这种数据结构具有如下特点
- 每个子目录项如NameService都被称为ZNode,会保存自己的数据内容和属性信息,有Sequential(顺序)属性,ZNode通过路径进行唯一标识,如Member1的标识为/GroupMemeber/Memeber1
- ZNode可以有子节点目录,并且每个ZNode可以存储数据,注意临时节点(Ephemeral)类型的目录节点不能有子节点目录
- ZNode包含版本,每个ZNode中存储的数据可以有多个版本
- ZNode的目录名可以自动编号,如App1已经存在,再创建的话,将会自动命名为App2
层次命名空间
每个节点(ZNode)都维护着一个stat结构,由版本号、ACL(操作控制列表)、时间戳和数据长度组成,每个ZNode最多可以存储1MB数据
Znode类型
- 持久节点(Persistent),客户端与ZK断开连接后,该节点(ZNode)依旧存在
- 临时节点(Ephemeral),客户端与ZK断开连接后,该节点(ZNode)被删除
- 顺序节点(Sequential),可以是持久的活临时的具有各自的特性,只是ZK给该节点(ZNode)名称进行顺序编号
Session 会话
客户端启动会与ZK建立一个TCP长连接,通过该连接可以发生请求并接收响应,以及响应ZK Watcher事件通知
Watches 监视
客户端注册监听它关心的目录节点,当目录节点(ZNode)发生变化(数据改变、被删除、子目录节点增加删除)时,ZK会通知客户端。
典型应用场景
Zookeeper 从设计模式角度来看,是一个基于观察者模式设计的分布式服务管理框架,它负责存储和管理大家都关心的数据,然后接受观察者的注册,一旦这些数据的状态发生变化,Zookeeper 就将负责通知已经在 Zookeeper 上注册的那些观察者做出相应的反应,从而实现集群中类似 Master/Slave 管理模式
下面详细介绍这些典型的应用场景,也就是 Zookeeper 到底能帮我们解决那些问题
Name service 统一命名服务
命名服务就是提供名称的服务,ZK的命名服务有两个方面的应用
- 提供类JNDI功能,可以把系统中各种服务的名称、地址以及目录信息存放在ZK,需要的时候去ZK中获取
- 制作分布式序列号生成器,利用ZK顺序节点的特性,可以生成有顺序的容易理解的同时支持分布式环境的编号
Configuration Management 配置管理
利用ZK本身的发布/订阅架构特性可以很容易的实现配置中心,发布者把数据发布到ZK的一个或一系列节点上,供订阅者进行数据订阅,ZK采用推拉结合的方式
- 推,ZK会推给注册了监控节点的客户端Watcher事件通知
- 拉,客户端获取到通知后,可以主动到ZK拉取最新数据
Group Membership 集群管理
集群管理主要指集群监控和集群控制两个方面,前者侧重于集群运行状态的收集,后者则是对集群进行操作与控制,分布式集群管理体系中有一种传统的基于Agent的方式,就是在集群每个机器部署Agent来收集机器的CPU、内存等指标,但是如果需要深入到业务状态进行监控,比如一个分布式消息中间件中,希望监控每个消费者对消息的消费状态,或者一个分布式任务调度系统中,需要对每个机器删的任务执行情况进行监控。对于这些业务紧密耦合的监控需求,统一的 Agent 是不太合适的。
利用ZK的实现思路:
在管理机器上线/下线的场景中,为了实现自动化的线上运维,我们必须对机器的上/下线情况有一个全局的监控。通常在新增机器的时候,需要首先将指定的 Agent 部署到这些机器上去。Agent 部署启动之后,会首先向 ZooKeeper 的指定节点进行注册,具体的做法就是在机器列表节点下面创建一个临时子节点,例如 /machine/[Hostname]
当 Agent 在 ZooKeeper 上创建完这个临时子节点后,对 /machines 节点关注的监控中心就会接收到“子节点变更”事件,即上线通知,于是就可以对这个新加入的机器开启相应的后台管理逻辑。另一方面,监控中心同样可以获取到机器下线的通知,这样便实现了对机器上/下线的检测,同时能够很容易的获取到在线的机器列表,对于大规模的扩容和容量评估都有很大的帮助。
Locks 分布式锁
分布式锁是控制分布式系统之间同步访问共享资源的一种方式,如果不同系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,一般需要通过一些互斥的手段来防止彼此之间的干扰,以保证一致性
排它锁
如果事务T1对数据对象O1加上了排他锁,在加锁期间,只允许事务T1对O1进行读取和更新操作,核心是保证当前有且仅有一个事务获得锁,并且锁释放后,所有正在等待获取锁的事务都能被通知到
实现思路:
通过ZK的ZNode可以表示一个锁,如/x_lock/lock
- 获取锁,多有客户端通过调用create接口尝试在/x_lock创建临时节点/x_lock/lock,最终只有一个客户端创建成功,那么该客户端就获取到了锁,同时其他没有获取到锁的客户端将注册监听该节点的变更
- 释放锁,获取锁的客户端在完成任务或者宕机后,会把临时节点删除,此时其他客户端监听到变化就可以开始新一轮锁的获取
共享锁
如果事务T1对数据对象O1加上了共享锁,则当前事务T1只能对O1进行读操作,其他事务也只能对该对象O1加共享锁,直到该数据对象上的所有共享锁都被释放。
实现思路:
通过ZK的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临时节点
- 判断读写顺序,共享锁规定,不同的事务可以同时对同一个数据进行读取操作,而更新操作必须在当前没有任何写操作的情况下进行,因此对于读请求,没有比自己序号小的节点或所有比自己序号小的节点都是读请求,对于写请求,则是要求没有比自己序号小的节点
- 释放锁,获取锁的客户端完成任务或者宕机后,会把练市节点删除,此时其他客户端可以开始新一轮锁的获取
羊群效应
上面共享锁过程中在判断读写顺序的时候会出现一个问题,在整个共享锁的竞争过程中,大量的“Watcher”通知和“子节点列表获取”两个操作重复运行,客户端无端地接收到过多和自己并不相关的事件通知,集群规模比较大时,不仅会影响性能,如果同一时间有多个节点对应的客户端完成事务或者是事务中断引起节点消失,ZK服务器会在短时间内向其他客户端发送大量的事件通知 - 这就是“羊群效应”
结合上述“羊群效应”,可以提出一个优化方法,对于读请求,只需要关心比自己序号小的最后一个写请求节点;对于写请求,只需要关系比自己序号小的最后一个节点
Master 选举
分布式系统中 Master 是用来协调集群中其他系统单元,具有对分布式系统状态更改的决定权。比如一些读写分离的应用场景,客户端写请求往往是 Master 来处理的。
利用常见关系型数据库中的主键特性来实现也是可以的,集群中所有机器都向数据库中插入一条相同主键 ID 的记录,数据库会帮助我们自动进行主键冲突检查,可以保证只有一台机器能够成功。
但是有一个问题,如果插入成功的和护短机器成为 Master 后挂了的话,如何通知集群重新选举 Master?
利用 ZooKeeper 创建节点 API 接口,提供了强一致性,能够很好保证在分布式高并发情况下节点的创建一定是全局唯一性。
集群机器都尝试创建节点,创建成功的客户端机器就会成为 Master,失败的客户端机器就在该节点上注册一个 Watcher 用于监控当前 Master 机器是否存活,一旦发现 Master 挂了,其余客户端就可以进行选举了。
大型分布式系统应用
Hadoop
Hadoop 利用 ZooKeeper 实现了高可用的功能,包括 HDFS 的 NameNode 和 YARN 的 ResourceManager。此外 YARN 的运行状态是利用 ZooKeeper 来存储的,主要利用了ZK的统一配置管理、分布式锁和集群管理
ResourceManager HA
- 多个 RM 在 /yarn-leader-election/pseudo-yarn-rm-cluster 竞争创建锁节点
- 注册 Watcher,创建锁成功的 RM 为 Active 节点,创建锁不成功的 RM 监听此节点,并成为 Stanby 状态
- 当 Active RM 挂了,通知 Standby RM,开始新一轮竞争
Fencing
Fencing 一般指解决脑裂这样的问题,YARN 引入了 Fencing 机制,利用的是 ZooKeeper 数据节点的 ACL 权限控制。
如果 RM1 假死,此时 RM2 成为 Active 状态,当 RM1 恢复之后,会试图去更新 ZooKeeper 里的数据,但此时会发现写上了 ACL 权限的节点无法修改,这样就可以避免了脑裂。
Kafka
Kafka 中大部分组件都应用了 ZooKeeper,主要利用了ZK的统一配置管理
- Broker 注册 /broker/ids/[0...N] 记录了 Broker 服务器列表记录,这个临时节点的节点数据是 ip 端口之类的信息。
- Topic 注册 /broker/topcs 记录了 Topic 的分区信息和 Broker 的对应关系
- 生产者负载均衡 生产者需要将消息发送到对应的 Broker 上,生产者通过 Broker 和 Topic 注册的信息,以及 Broker 和 Topic 的对应关系和变化注册事件 Watcher 监听,从而实现一种动态的负载均衡机制。
- 消息消费进度 Offset 记录 消费者对指定消息分区进行消息消费的过程中,需要定时将分区消息的消费进度 Offset 记录到 ZooKeeper 上,以便消费者进行重启或者其他消费者重新阶段该消息分区的消息消费后,能够从之前的进度开始继续系消费
Dubbo
Dubbo 基于 ZooKeeper 实现了服务注册中心。哪一个服务由哪一个机器来提供必需让调用者知道,简单来说就是 ip 地址和服务名称的对应关系。ZooKeeper 通过心跳机制可以检测挂掉的机器并将挂掉机器的 ip 和服务对应关系从列表中删除。主要利用了ZK的统一命名服务和统一配置管理
至于支持高并发,简单来说就是横向扩展,在不更改代码的情况通过添加机器来提高运算能力。通过添加新的机器向 ZooKeeper 注册服务,服务的提供者多了能服务的客户就多了。
网友评论