大数据之分布式同步服务-ZooKeeper
ZooKeeper基础
概述
ZooKeeper是一个开源的,分布式的,为分布式系统提供协调服务的Apache项目。
分布式系统的出现主要是为了解决单机不可靠和可扩展性的问题。但其自身也存在一些问题,与单机系统相比,分布式系统没有一个全局的主控,协调或控制中心,存在内存地址不一致的问题,且其运行时出现问题的概率要高于单机系统。
分布式系统主要问题有:
- 一致性问题
- 容灾容错
- 执行顺序问题
- 事务问题
因此,基于以上问题,我们就产生了一个需求:需要一个松散耦合的分布式系统中粗粒度锁以及可靠性存储(低容量)的系统。在此背景下,ZooKeeper便应运而生。
ZooKeeper是基于Hadoop系统的,开源的,高效的,分布式同步,组服务,基于Paxo协议的分布式协调架构。(Google内部实现叫Chubby)。
什么是paxos协议?
Paxos用于解决分布式系统中一致性问题。分布式一致性算法(Consensus Algorithm)是一个分布式计算领域的基础性问题,其最基本的功能是为了在多个进程之间对某个(某些)值达成一致(强一致);简单来说就是确定一个值,一旦被写入就不可改变。paxos用来实现多节点写入来完成一件事情,例如mysql主从也是一种方案,但这种方案有个致命的缺陷,如果主库挂了会直接影响业务,导致业务不可写,从而影响整个系统的高可用性。paxos协议是只是一个协议,不是具体的一套解决方案。目的是解决多节点写入问题。
ZooKeeper工作机制
Zookeeper从设计模式角度来理解:是一个基于观察者模式设计的分布式服务管理框架,它负责存储和管理大家都关心的数据,然后接受观察者的注册,一旦这些数据的状态发生变化, Zookeeper就将负责通知已经在Zookeeper上注册的那些观察者做出相应的反应。
ZooKeeper特点
- Zookeeper:一个领导者(Leader),多个跟随者(Follower)组成的集群。
- 集群中只要有半数以上节点存活,Zookeeper集群就能正常服务。
- 全局数据一致:每个Server保存一份相同的数据副本,Client无论连接到哪个Server,数据都是一致的。
- 更新请求顺序进行,来自同一个Client的更新请求按其发送顺序依次执行。
- 数据更新原子性,一次数据更新要么成功,要么失败。
ZooKeeper数据模型和结构
数据模型
- 数据模型
- 命名空间
- 与标准文件系统很相似
- 以 / 为间隔的路径名序列组成
- 只有绝对路径,没有相对路径
- 每个节点自身的信息:
- 数据
- 数据长度
- 创建时间
- 修改时间
- 具有文件、路径的双重特点
数据结构
ZooKeeper数据模型的结构与Unix文件系统很类似,整体上可以看作是一棵树,每个节点称做一个ZNode。每一个ZNode默认能够存储1MB的数据,每个ZNode都可以通过其路径唯一标识。
ZooKeeper应用场景
ZK提供的服务包括:统一命名服务、统一配置管理、统一集群管理、服务器节点动态上下线、软负载均衡等。
统一命名服务
在分布式环境下,经常需要对应用/服务进行统一命名,便于识别(比如hadoop集群的命名)。例如:IP不容易记住,而域名容易记住。
统一配置管理
ZK能够实现全局配置的容错和统一。
分布式环境下,配置文件同步非常常见。
- 一般要求一个集群中,所有节点的配置信息是一致的,比如 Kafka 集群。
- 对配置文件修改后,希望能够快速同步到各个节点上。
配置管理可交由ZooKeeper实现。
- 可将配置信息写入ZooKeeper上的一个Znode。
- 各个客户端服务器监听这个Znode。
- 一旦Znode中的数据被修改,ZooKeeper将通知各个分服务器。
统一集群管理
ZK能够实现集群管理,了解每台服务器的状态,对服务器节点的新增和删除会进行“周知”,能够进行主服务器选举。
分布式环境中,实时掌握每个节点的状态是必要的。
- 可根据节点实时状态做出一些调整。
ZooKeeper可以实现实时监控节点状态变化
- 可将节点信息写入ZooKeeper上的一个ZNode。
- 监听这个ZNode可获取它的实时状态变化。
节点序列最小的是主节点,对外提供服务。
服务器动态上下线
软负载均衡
在Zookeeper中记录每台服务器的访问数,让访问数最少的服务器去处理最新的客户端请求。
ZooKeeper内部原理
ZooKeeper节点类型
持久(Persistent):客户端和服务器端断开连接后,创建的节点不删除
短暂(Ephemeral):客户端和服务器端断开连接后,创建的节点自己删除
Persistent Node(持久化目录节点)
- 永久有效地节点,除非client显式的删除,否则一直存在
Ephemeral Nodes(临时目录节点)
- – 临时节点,仅在创建该节点client保持连接期间有效,一旦连接丢失,zookeeper会自动删除该节点
Sequence Nodes
- 顺序节点,client申请创建该节点时,zk会自动在节点路径末尾添加递增序号,这种类型是实现分布式锁,分布式queue等特殊功能的关键
- 说明:创建znode时设置顺序标识,znode名称后会附加一个值,顺序号是一个单调递增的计数器,由父节点维护
- 注意:在分布式系统中,顺序号可以被用于为所有的事件进行全局排序,这样客户端可以通过顺序号推断事件的顺序
Persistent Sequence Nodes
- 客户端与Zookeeper断开连接后,该节点依旧存在,只是Zookeeper给该节点名称进行顺序编号
Ephemeral Sequence Nodes
- 客户端与Zookeeper 断开连接后,该节点被删除,只是Zookeeper给该节点名称进行顺序编号
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: 子节点数量
ZooKeeper监听器原理**
监听原理
- 首先要有一个main()线程。
- 在main线程中创建Zookeeper客户端,这时就会创建两个线程,一个负责网络连接通信(connet),一个负责监听(listener)。
- 通过connect线程将注册的监听事件发送给Zookeeper。
- 在Zookeeper的注册监听器列表中将注册的监听事件添加到列表中。
- Zookeeper监听到有数据或路径变化,就会将这个消息发送给listener线程。
- listener线程内部调用了
process()
方法。
常见的监听
- 监听节点数据的变化
get path [watch]
- 监听子节点增减的变化
ls path [watch]
监控机制(watch)
- 在数据节点上设置
- 客户端被动收到通知
- 各种读请求,如
getData()
,getChildren()
,exists()
三个关键点
- 一次性监控,触发后需要重新设置
- 保证先收到事件再收到数据修改的信息
- 传递性
- 如create节点,会触发父节点的监控点监控变化
- 如delete会节点,会触发节点数据监控点,同时也会触发父节点的监控点
风险
- 客户端有可能看不到所有数据的变化
- 多个事件的监控,有可能只会触发一次
- 一个客户端设置了关于某个数据点exists和getData的监控,则当该数据被删除的时候,只会触发“文件被删除”的通知。
- 客户端网络中断的过程的无法收到监控的窗口时间,要由模块进行容错设计
ZooKeeper选举机制**
- 半数机制:集群中半数以上机器存活,集群可用。所以 Zookeeper 适合安装奇数台服务器。
- Zookeeper 虽然在配置文件中并没有指定 Master 和Slave。但是,Zookeeper 工作时,是有一个节点为Leader,其他则为Follower,Leader 是通过内部的选举机制临时产生的。
- 以一个简单的例子来说明整个选举的过程。
假设有五台服务器组成的Zookeeper 集群,它们的id 从1-5,同时它们都是最新启动的,也就是没有历史数据,在存放数据量这一点上,都是一样的。假设这些服务器依序启动,来看看会发生什么,如图所示。
1.服务器1 启动,发起一次选举。服务器 1 投自己一票。此时服务器 1 票数一票,不够半数以上(3 票),选举无法完成,服务器 1 状态保持为LOOKING;
2.服务器2 启动,再发起一次选举。服务器1 和2 分别投自己一票并交换选票信息:此时服务器 1 发现服务器 2 的 ID 比自己目前投票推举的(服务器 1)大,更改选票为推举服务器2。此时服务器 1 票数0 票,服务器2 票数2 票,没有半数以上结果,选举无法完成,服务器1,2 状态保持 LOOKING ;
3.服务器 3 启动,发起一次选举。此时服务器 1 和 2 都会更改选票为服务器 3。此次投票结果:服务器1 为0 票,服务器2 为0 票,服务器 3 为3 票。此时服务器 3 的票数已经超过半数,服务器3 当选Leader。服务器1,2 更改状态为FOLLOWING,服务器 3 更改状态为LEADING;
- 服务器 4 启动,发起一次选举。此时服务器 1,2,3 已经不是 LOOKING 状态,不会更改选票信息。交换选票信息结果:服务器3 为3 票,服务器4 为 1 票。此时服务器4服从多数,更改选票信息为服务器 3,并更改状态为 FOLLOWING;
5.服务器5 启动,同4一样当Follower。
ZooKeeper写数据流程
ZooKeeper数据访问
ZK每个节点上的“访问控制链”(ACL, Access Control List)保存了各客户端对于该节点的访问权限。
用一个三元组来定义客户端的访问权限:(scheme:expression, perms)
- (ip:19.22.0.0/16, READ)表示IP地址以19.22开头的主机有该数据节点的读权限。
zookeeper本身提供了ACL机制,表示为 scheme:id:permissions
,第一个字段表示采用哪一种机制,第二个id表示用户,permissions表示相关权限(如只读,读写,管理等)
ZooKeeper一致性保证
- 序列一致性:
- 客户端发送的更新将按序在Zookeeper进行更新
- 原子一致性:
- 更新只能成功或者失败,没有中间状态
- 单系统镜像:
- 无论连接哪台Zookeeper服务器,客户端看到的服务器数据一致
- 可靠性:
- 任何一个更新成功后都会持续生效,直到另一个更新将它覆盖。可靠性有两个关键点:
- 第一,当客户端的更新得到成功的返回值时,可以保证更新已经生效,但在某些异常情况下(超时, 连接失败),客户端无法知道更新是否成功;
- 第二,当更新成功后,不会回滚到以前的状态,即使是在服务器失效重启之后;
- 任何一个更新成功后都会持续生效,直到另一个更新将它覆盖。可靠性有两个关键点:
- 实时性:
- Zookeeper保证客户端将在一个时间间隔范围内获得服务器的更新信息,或者服务器失效的信息。但由于网络延时等原因,
- Zookeeper不能保证两个客户端能同时得到刚更新的数据,如果需要最新数据,应该在读数据之前调用sync()接口;
ZooKeeper实践
ZK客户端命令行操作
临时节点与永久节点操作
1.ZooKeeper启动客户端
1 | (base) [root@main ~]# zkCli.sh |
2.显示所有操作命令
1 | [zk: localhost:2181(CONNECTING) 0] help |
3.查看当前znode所包含的内容,“[]”里是节点名称。
1 | [zk: localhost:2181(CONNECTED) 1] ls / |
4.查看当前节点详细数据
1 | [zk: localhost:2181(CONNECTED) 2] ls2 / |
5.分别创建 2 个普通节点
1 | [zk: localhost:2181(CONNECTED) 4] create /subject "3subjects" |
6.获得节点的值
1 | [zk: localhost:2181(CONNECTED) 6] get /subject |
7.创建临时节点
1 | [zk: localhost:2181(CONNECTED) 8] create -e /subject/lnode "moment" |
临时节点在当前客户端和其他节点是可以看到的,现在退出客户端重新登陆查看。
1 | [zk: localhost:2181(CONNECTED) 13] quit |
可以看到,退出创建临时节点的客户端后,临时节点就没了。
创建带序号的节点 (顺序节点)
1.创建带序号的节点
1 | [zk: localhost:2181(CONNECTED) 7] create -s /subject/Chinese/Junior "100score" |
如果原来没有序号节点,序号从0 开始依次递增。如果原节点下已有2 个节点,则再排序时从2 开始,以此类推。
结合之前在Subject创建了一个永久节点,一个临时节点,所以创建顺序节点English时,是从2开始的。
1 | [zk: localhost:2181(CONNECTED) 3] create -s /subject/English "120score" |
2.修改节点数据值
1 | [zk: localhost:2181(CONNECTED) 10] get /subject |
节点值变化监听
1.在slaves2节点上注册监听/subject
节点数据变化
1 | # slaves2 |
2.在main节点上对/subject
节点进行修改
1 | [zk: localhost:2181(CONNECTED) 13] set /subject "9subjects" |
修改被slaves2节点上的监听器监听到
节点的子节点变化监听(路径变化)
在slaves2 节点上注册监听/subject 节点的子节点变化。
1 | [zk: localhost:2181(CONNECTED) 4] ls /subject watch |
在main主机的/subject 节点上创建子节点
1 | [zk: localhost:2181(CONNECTED) 14] create -s /subject/math "120scores" |
观察slaves2收到子节点上的变化监听
删除节点
1 | [zk: localhost:2181(CONNECTED) 15] ls /subject |
递归删除节点
1 | [zk: localhost:2181(CONNECTED) 19] ls /subject |
查看节点状态
1 | [zk: localhost:2181(CONNECTED) 24] stat /subject/English0000000002 |
补充
zk共享锁
共享锁理解为排队形式。
server007
[server006, server007, server008]
此时server006正在使用且其ID小于server007,server007继续等待。
[server007, server008]
此时查看,发现server007最小和自己匹配,server007开始提供服务。