ZooKeeper简介与原理
概述
ZooKeeper 是 Apache 软件基金会的一个软件项目,它为大型分布式计算提供开源的分布式配置服务、同步服务和命名注册 。
ZooKeeper 的架构通过冗余服务实现高可用性(CP)。
最终一致性,通过Zxid保证。
ZooKeeper 的设计目标是将那些复杂且容易出错的分布式一致性服务封装起来,构成一个高效可靠的原语集,并以一系列简单易用的接口提供给用户使用。
一个典型的分布式数据一致性的解决方案,分布式应用程序可以基于它实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能 。
分布式协调服务:主要解决分布式应用中经常遇到的数据管理问题
ZooKeeper本质是小型的文件存储系统+监听机制的功能。
数据模型
ZooKeeper本身是一个树形目录服务(名称空间),非常类似于标准文件系统(树形层次结构),key-value 的形式存储。名 称 key 由斜线/分割的一系列路径元素,ZooKeeper 名称空间中的每个节点都是由一个路径来标识的。
- 每个路径下的节点key(完整路径,名称)是唯一的,即同一级节 点 key 名称是唯一的
- 每个节点中存储了节点value和对应的状态属性(多个)
每个znode存储的数据大小至多1MB,所以适合用来做配置文件信息、状态信息存储
znode由3部分组成:
- data
- stat:状态信息:版本,权限,事务id…
- 子节点列表
节点操作
节点类型
类型 | 描述 |
---|---|
PERSISTENT | 持久节点,默认 |
PERSISTENT SEQUENTIAL | 持久顺序节点,创建时ZooKeeper 会在路径上加上序号作为后缀,非常适合用于分布式锁、分布式选举等场景,创建时添加 -s 参数,如图: |
EPHEMERAL | 临时节点(不可在拥有子节点),跟连接会话绑定,临时节点会在客户端会话断开后由zk服务端自动删除。适用于心跳,服务发现等场景,创建时添加-e参数 |
EPHEMERAL SEQUENTIAL | 临时顺序节点(不可在拥有子节点),与持久顺序节点类似,不同之处在于EPHEMERAL_SEQUENTIAL是临时的,会在会话断开后删除,创建时添加 -e -s 参数 |
CONTAINER | 容器节点,当子节点都被删除后,Container 也随即删除,创建时添加 -c 参数 |
PERSISTENT WITH_TTL | TTL节点,客户端断开连接后不会自动删除Znode,如果该Znode没有子Znode且在给定TTL时间内无修改,该Znode将会被删除;单位是毫秒;创建时添加 -t 参数 |
常用命令
命令 | 描述 | 用法示例 | 说明 |
---|---|---|---|
ls | 查看某个路径下目录列表 | ls [-s] [-w] [-R] path | path:代表路径,完整路径 -s:返回状态信息 -w:监听节点变化 -R:递归查看某路径下目录列表 |
create | 创建节点并赋值 | create [-s] [-e] [-c] [-t ttl] path [data] [acl] | [-s] [-e]:-s 和 -e 都是可选的,-s 代表顺序节点, -e 代表临 时节点,注意其中 -s 和 -e 可以同时使用的,并且临时节点不 能再创建子节点 path:指定要创建节点的路径,比如 /runoob data:要在此节点存储的数据 acl:访问权限相关,默认是 world,相当于全世界都能访问 |
set | 修改节点存储的数据 | set [-s] [-v version] path data | path:节点路径。 data:需要存储的数据。 [version]:可选项,版本号(可用作乐观锁) |
get | 获取节点数据和状态信息 | get [-s] [-w] path | -s:返回结果带上状态信息 -w:返回数据并对对节点进行事件监听 |
stat | 查看节点状态信息 | stat [-w] path | path:代表路径 -w:对节点进行事件监听 |
delete /deleteall | 删除某节点 | delete [-v version] path deleteall path [-b batch size] |
如果某节点不为空,则不能用delete命令删除 |
Znode结构详解
状态属性 | 描述 |
---|---|
cZxid | 创建节点时的事务ID |
ctime | 创建节点时的时间 |
mZxid | 最后修改节点时的事务ID |
mtime | 最后修改节点时的时间 |
pZxid | 表示该节点的子节点列表最后一次修改的事务ID,添加子节点或删除子节点就会影响子节点列表,但是修改子节点的数据内容则不影响该ID(注意,只有子节点列表 变更了才会变更pzxid,子节点内容变更不会影响pzxid) |
cversion | 子节点版本号,子节点每次修改版本号加1 |
dataversion | 数据版本号,数据每次修改该版本号加1 |
aclversion | 权限版本号,权限每次修改该版本号加1 |
ephemeralOwner | 创建该临时节点的会话的sessionID。 (如果该节点是持久节点,那么这个属性值为0) |
dataLength | 该节点的数据长度 |
numChildren | 该节点拥有子节点的数量(只统计直接子节点的数量) |
Java客户端
ZooKeeper(不推荐)
原生API 不支持session超时重连,没有解决watch注册一次就失效的问题,不支持递归操作…
zkClient
不支持一些场景(不如需要自己写分布式锁)
Curator
提供fluent编程风格,提供了一些应用场景(分布式锁)
集群策略
ZooKeeper 集群节点个数推荐配置奇数个。
容错率
需要保证集群能够有半数进行投票
- 2台服务器,至少2台正常运行才行(2的半数为1,半数以上最少为2),正常运行1台服务器都不允许挂掉,但是相对于 单节点服务器,2台服务器还有两个单点故障,所以直接排除了。
- 3台服务器,至少2台正常运行才行(3的半数为1.5,半数以上最少为2),正常运行可以允许1台服务器挂掉。
- 4台服务器,至少3台正常运行才行(4的半数为2,半数以上最少为3),正常运行可以允许1台服务器挂掉。
- 5台服务器,至少3台正常运行才行(5的半数为2.5,半数以上最少为3),正常运行可以允许2台服务器挂掉。
防脑裂
脑裂集群的脑裂通常是发生在节点之间通信不可达的情况下,集群会分裂成不同的小集群,小集群各自选出自己的leader节点,导致原有的集群出现多个leader节点的情况,这就是脑裂。
- 3台服务器,投票选举半数为1.5,一台服务裂开,和另外两台服务器无法通行,这时候2台服务器的集群(2票大于半数1.5票),所以可以选举出leader,而 1 台服务器的集群无法选举。
- 4台服务器,投票选举半数为2,可以分成 1,3两个集群或者2,2两个集群,对于 1,3集群,3集群可以选举;对于2,2集群,则不能选择,造成没有leader节点。
- 5台服务器,投票选举半数为2.5,可以分成1,4两个集群,或者2,3两集群,这两个集群分别都只能选举一个集群,满足ZooKeeper集群搭建数目。
以上分析,我们从容错率以及防止脑裂两方面说明了3台服务器是搭建集群的最少数目,4台发生脑裂时会造成没有leader节点的错误
选举策略
Paxos
Paxos算法是基于消息传递且具有高度容错特性的一致性算法,是目前公认的解决分布式一致性问题最有效的算法之一,其解决的问题就是在分布式系统中如何就某个值(决议)达成一致,paxos是一个分布式选举算法。
该算法定义了三种角色
- Proposer:提案(决议)发起者
- Acceptor:提案接收者,可同意或不同意
- Learners:虽然不同意提案,但也只能被动接收学习;或者是后来的,只能被动接受
提案遵循少数服从多数的原则,过半原则。
ZAB协议
ZooKeeper使用的是ZAB协议作为数据一致性的算法, ZAB(ZooKeeper Atomic Broadcast ) 全称为:原子消息广播协议。在Paxos算法基础上进行了扩展改造而来的,ZAB协议设计了支持原子广播、崩溃恢复,ZAB协议保证Leader广播的变更序列被顺序的处理。
四种状态,其中三种跟选举有关
LOOKING:系统刚启动时或者Leader崩溃后正处于选举状态
FOLLOWING:Follower节点所处的状态,同步leader状态,参与投票
LEADING:Leader所处状态
OBSERVING,观察状态,同步leader状态,不参与投票
选举时也是半数以上通过才算通过。
数据读写
读请求
当Client向ZooKeeper发出读请求时,无论是Leader还是Follower,都直接返回查询结果。
Leader写请求
Client向Leader发出写请求,Leader将数据写入到本节点,并将数据发送到所有的Follower节点,等待
Follower节点返回,当Leader接收到一半以上节点(包含自己)返回写成功的信息之后,返回写入成功消息给client。
follow写请求
Client向Follower发出写请求,Follower节点将请求转发给Leader,Leader将数据写入到本节点,并将数据发送到所有的Follower节点,等待Follower节点返回,当Leader接收到一半以上节点(包含自己)返回写成功的信息之后,返回写入成功消息给原来的Follower,原来的Follower返回写入成功消息给Client。
应用场景
分布式锁
分布式锁,指在分布式环境下,保护跨进程、跨主机、跨网络的共享资源,实现互斥访问,保证一致性;在zk中,锁就是一个数据节点。
- 普通实现
注册临时节点,谁注册成功谁获取锁,其他监听该节点的删除事件,一旦被删除,通知其他客户端,再次重复该流程;此为最简单的实现,但存在惊群效应问题。
惊群效应:ZooKeeper分布式锁场景中的羊群效应指的是所有的客户端都尝试对一个临时节点去加锁,当一个锁被占有的时候,其他的客户端都会监听这个临时节点。一旦锁被释放,ZooKeeper反向通知添加监听的客户端,然后大量的客户端都尝试去对同一个临时节点创建锁,最后也只有一个客户端能获得锁,但是大量的请求造成了很大的网络开销,加重了网络的负载,影响ZooKeeper的性能。
优化实现
所有服务要获取锁时都去ZooKeeper中注册一个临时顺序节点,并将基本信息写入临时节点 。
所有服务获取节点列表并判断自己的节点是否是最小的那个,谁最小谁就获取了 锁 。
未获取锁的客户端添加对前一个节点删除事件的监听 。
锁释放/持有锁的客户端宕机后,节点被删除 。
下一个节点的客户端收到通知,重复上述流程。
共享锁
又称读锁。如果事务T1对数据对象O1加上了共享锁,那么当前事务只能对O1进行读取操作,其他事务也只能对这个数据对象加共享锁,直到该数据对象上的所有共享锁都被释放。
排它锁
又称写锁或独占锁。如果事务T1对数据对象O1加上了排他锁,那么在整个加锁期间,只允许事务T1对O1进行读取或更新操作,其他任务事务都不能对这个数据对象进行任何操作,直到T1释放了排他锁。
读写分离
ZooKeeper可以将临时有序节点分为读锁节点和写锁节点
- 当操作是读请求,也就是要获取共享锁,如果没有比自己更小的节点,或比自己小的节点都是读请求,则可以获取到读锁。 若比自己小的节点中有写请求,则当前客户端无法获取到读锁,只能等待前面的写请求完成。
- 如果操作是写请求,也就是要获取独占锁,如果没有比自己更小的节点,则表示当前客户端可以直接获取到写锁,对数据进行修改。 如果发现有比自己更小的节点,无论是读操作还是写操作,当前客户端都无法获取到写锁,等待前面所有的操作完成。
Curator锁实现
- InterProcessMutex:分布式可重入排它锁(可重入可以借助LocalMap存计数器)
- InterProcessSemaphoreMutex:分布式排它锁
- InterProcessMultiLock:将多个锁作为单个实体管理的容器
- InterProcessReadWriteLock:分布式读写锁