一、引言
CAP理论:
1、一致性(Consistency)(C): 在分布式系统中的所有数据备份,在同一时刻是否同样的值。 (等同于所有节点访问同一份最新的数据副本)
2、可用性(Availability)(A): 在集群中一部分节点故障后,在一定时间内,集群整体是否 还能响应客户端的读写请求。(对数据更新具备高可用性)
3、分区容错性(Partition tolerance)(P): 以实际效果而言,分区相当于对通信的时限要求。 系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在 C 和 A 之间做出选择。
定理:任何分布式存储系统只能同时满足两点,没法三者兼顾。
原因:数据备份的节点越多,分区容错性P越高,但要复制更新的数据越多,一致性C就很难保证。为了保证一致性,更新所有节点所需的时间就会加长,可用性A就会降低。
Mysql:满足CA
Zookeeper:满足CP
二、Zookeeper简介
ZooKeeper 是一个分布式的,开放源码的分布式应用程序协调服务,是 Google 的 Chubby 一个开源的实现。它提供了简单原始的功能,分布式应用可以基于它实现更高级的服务,比如分布式同步,配置管理,集群管理,命名管理,队列管理。它被设计为易于编程,使用文件系统目录树作为数据模型。
ZooKeeper 是集群的管理者,监视着集群中各节点的状态,根据节点提交的反馈进行下一步合理的操作。最终将简单易用的接口和功能稳定、性能高效的系统提供给用户。
三、Zookeeper提供的功能
Zookeeper = 文件系统 + 监听机制
1. 文件系统
ZooKeeper 的命名空间就是 ZooKeeper 应用的文件系统,它和 linux 的文件系统很像,也是树状,这样就可以确定每个路径都是唯一的,对于命名空间的操作必须都是绝对路径操作。与 linux 文件系统不同的是,Zookeeper没有文件和文件夹的概念,所有节点统一叫做Znode, 一个 Znode 节点可以包含子 Znode,同时也可以包含数据。
znode既是文件又是文件夹,每个znode有唯一的路径标识,既能存储数据(相当于文件),同时又能创建子znode(相当于文件夹中创建子文件)。
Znode只适合存储非常小量的数据,不能超过1M,最好不要超过1K。
Znode介绍:
1)Znode有两种类型:
临时节点(ephemeral):客户端断开连接后自动删除
永久节点(persistent):只能手动删除
2)Znode有四种形式的目录节点(默认是persistent)
PERSISTENT 永久节点 | 持久化 znode 节点,一旦创建这个 znode 点存储的数据不会主动消失,除非客户端主动delete |
PERSISTENT_SEQUENTIAL 永久有编号节点 | 自动增加顺序编号的 znode 节点。每当有客户端创建一个有编号节点,都会在节点名称后加上一个编号串(0000000000依次递增),编号为当前zk命名空间最大znode编号+1,命名空间由父znode维护。即任意一个 Client 去创建 znode 都是保证得到的 znode 是递增的,而且是唯一的 |
EPHEMERAL 临时节点 | 客户端连接zk service时会建立一个session,临时节点的声明周期与session相同。一旦客户端断开连接,服务器就会清楚session,临时节点就会被自动删除。 |
EPHEMERAL_SEQUENTIAL 临时有编号节点 | 临时有编号节点,Znode节点编号会自增,但会随session消失而消失,但命名空间中的编号不会重置 |
3)有编号节点的编号值由父节点维护,即不同Znode的子目录的命名空间相互独立
4)在分布式系统中,顺序号可以被用于为所有的事件进行全局排序,这样客户端可以通过 顺
序号推断事件的顺序。
5)临时节点不能创建子节点
6)客户端可以在Znode上添加监听
Znode中的信息字段解释:
cZxid = 0x600000aad | 节点创建时候的zxid |
ctime = Thu Dec 16 16:38:27 CST 2021 | 节点创建时间 |
mZxid = 0x600000aad | 节点修改的zxid,与子节点修改无关 |
mtime = Thu Dec 16 16:38:27 CST 2021 | 节点的修改时间 |
pZxid = 0x600000aad | 子节点创建/删除的zxid,和修改无关,与孙子节点无关 |
cversion = 0 | 子节点的更新次数 |
dataVersion = 0 | 节点数据的更新次数 |
aclVersion = 0 | 节点(ACL)的更新次数 |
ephemeralOwner = 0x0 | 临时节点:值为sessionId。永久节点:0 |
dataLength = 0 | 节点数据的字节数 |
numChildren = 0 | 子节点个数,不包括孙子节点 |
cZxid、mZxid、pZxid都是全局唯一且循序递增,这3个id共同标识全局事件的提交顺序(标识对某一节点操作的事件的顺序)
Zxid共64位,其中:
1. 高32位:标识Leader关系是否发生改变。选出一个新的Leader时,该值才会变化。
2. 低32位:事件的提交顺序。该命名空间由leader维护,按事件的提交顺序递增。
leader发生改变后重置。
2. 监听机制
客户端注册监听它关心的目录节点,当目录节点发生变化(数据改变、节点删除、子目录节 点增加删除)时,ZooKeeper 会通知客户端。监听机制保证 ZooKeeper 保存的任何的数据的 任何改变都能快速的响应到监听了该节点的应用程序。
监听器的工作机制,其实是在客户端上专门创建一个监听线程,在本机的一个端口上等待 zk 集群发送过来事件。
监听事件有4类:
nodedatachanged | 节点内容变化事件 |
nodecreated | 节点创建事件 |
nodedeleted | 节点删除事件 |
nodechildrenchanged | 子节点发生变化事件 |
Zookeeper就是运用监听机制,监听自身的数据变化,最后将变化发送给感兴趣的客户端(添加了监听事件的客户端)
[注]:监听只能生效一次
监听工作原理:
四、Zookeeper典型应用场景
1. 配置管理
在分布式系统中,配置文件分布在多台机器上,若要改变配置需要逐个改变所有机器上的配置十分困难。此时,可以将配置存储在Zookeeper上。将配置保存在Zookeeper某个目录的节点上,之后所有相关应用对该目录节点添加监听,一旦配置信息发生变化,所有应用都会收到Zookeeper的通知,然后从Zookeeper中下载最新的配置即可。
2. 集群管理
所谓集群管理无外乎两点:是否有节点加入或退出,集群选主
对于第一点,所有机器约定在父目录GroupMembers下创建临时节点,并对该目录添加子节点变化的监听。一旦有新节点加入或有节点挂掉,集群中的节点都会接收到Zookeeper的通知。
对于第二点,选主策略将会后续的Zookeeper选主中讲解。
3. 分布式锁
锁服务有两种类型:
写锁:对写操作加锁,对资源保持独占,又叫排他锁
读锁:对读操作加锁,可共享访问,又叫共享锁
对于写锁,可以将Zookeeper的一个Znode看做是锁,如/distribute_lock。当有客户端需要做写操作时,先在Zookeeper中创建/distribute_lock节点,若节点不存在,则创建成功,相当于获得锁。当写操作完成后再删除节点,相当于释放锁;若节点已存在,说明有其它客户端正在占用锁,进入等待。
对于读锁,/distribute_lock节点已经预先存在,所有客户端想要获得锁在其下面创建临时有编号节点,每次在该目录下选择编号最小的节点获得锁。
五、Zookeeper集群Cli使用
首先,我们可以是用命令 bin/zkCli.sh 进入 ZooKeeper 的命令行客户端,这种是直接连接本 机的 ZooKeeper 服务器,还有一种方式,可以连接其他的 ZooKeeper 服务器,只需要我们在命令后面接一个参数-server 就可以了。例如:zkCli.sh -server hadoop01:2181
此处会出现一个小bug,在进入Zookeeper客户端的目录下会生成一个zookeeper.out文件,解决办法参考
进入命令行后键入help可以查看帮助文档,帮助文档中列出了所有操作以及参数设置如下图
常用命令:
查看Znode子节点内容 ls / ls /zk |
创建Znode节点 create /zk "data" # 默认创建永久无编号节点。 -e 创建临时节点。 -s 创建有编号节点。 |
获取Znode数据 get /zk/node |
设置Znode数据 set /zk/node "data" |
监听Znode事件 ls /zk watch # 对zk节点的子节点变化事件注册监听 get /zk watch # 对zk节点的数据内容变化事件注册监听 |
删除Znode节点 delete /zk # 只能删除没有子节点的节点(空目录) rmr /zk # 强制删除 |
六、Zookeeper集群Java API使用
常用API:
create(path, data, flags) | 创建一个Znode,flags标识Znode类型: PERSISTEN, PERSISTENT_SEQUENTAIL, EPHEMERAL, EPHEMERAL_SEQUENTAIL |
delete(path, version) | 删除一个Znode,可以通过version删除指定版本,version=-1代表删除所欲版本 |
exists(path, watch) | 判断Znode是否存在。watch=null表示不添加监听 |
getData(path, watch) | 读取指定Znode的数据 |
setData(path, watch) | 更新指定Znode的数据 |
getChildren(path, watch) | 获取指定Znode的所有子Znode的名字 |
[注]:所有方法中watch参数代表添加的监听。Watcher是在创建Zookeeper实例时指定的。
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;
import java.io.IOException;
import java.util.List;
public class ZkDemo {
// 客户端去请求链接的时候的服务器链接地址信息
static final String CONNECT_STRING = "hadoop01:2181";
// 客户端去请求链接的超时时长
static final int SESSION_TIMEOUT = 3000;
static ZooKeeper zk = null;
public static void main(String[] args) throws IOException, KeeperException, InterruptedException {
/**
* 创建 Zookeeper 对象,获取 Zookeeper 连接
* 参数1: 连接 主机名:2181
* 参数2: 连接的超时时间(ms)
* 参数3: 监听对象,不需要监听传null
*/
zk = new ZooKeeper(CONNECT_STRING, SESSION_TIMEOUT, null);
/**
* 创建节点
* 参数1: 节点路径 参数2: 节点存储的路径 byte[]
* 参数3: 权限 OPEN_ACL_UNSAFE 最大权限
* 参数4: 节点的属性(节点的模式) -- 永久有(无)编号,临时有(无)编号
*/
zk.create("/test", "zk_test".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
/**
* 删除节点,不能删除非空节点,API中没有提供rmr
* 参数1: 路径
* 参数2: 版本,不知道写-1(代表当前版本)
*/
zk.delete("/test", -1);
// 修改节点内容
zk.setData("/test", "test_zk".getBytes(), -1);
// 获取节点内容,返回byte[]
byte[] data = zk.getData("/test", null, null);
// 获取节点的子节点列表
List<String> children = zk.getChildren("/test", null);
/**
* 判断节点是否存在。存在返回状态信息,不存在返回null
* 返回值是 Stat 对象,Stat对象中封装着节点的Zxid,version等信息,可通过get()方法查看
*/
Stat exists = zk.exists("/test01", null);
zk.close();
}
}
添加监听:如何循环添加监听?
import org.apache.zookeeper.*;
import java.io.IOException;
public class ZkWatch {
// 客户端去请求链接的时候的服务器链接地址信息
static final String CONNECT_STRING = "hadoop01:2181";
// 客户端去请求链接的超时时长
static final int SESSION_TIMEOUT = 3000;
static ZooKeeper zk = null;
public static void main(String[] args) throws IOException, KeeperException, InterruptedException {
zk = new ZooKeeper(CONNECT_STRING, SESSION_TIMEOUT, new Watcher() {
/**
* 回调方法,触发监听的时候会进行回调
* watchedEvent: 监听事件对象,提供3个重要属性:
* 1) Path: 监听事件路径
* 2) Type: 监听事件类型
* 3) State: 监听事件状态
*/
public void process(WatchedEvent watchedEvent) {
String path = watchedEvent.getPath();
Event.EventType type = watchedEvent.getType();
Event.KeeperState state = watchedEvent.getState();
System.out.println(path + "--" + type + "--" + state);
// 循环添加监听,每次触发监听事件调用process函数后继续添加监听。
try {
zk.exists("/test", true);
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 添加监听 getChildren getData exists
/**
* 参数2的三种传参方式
* 1. null: 不添加监听
* 2. true: 添加监听 监听触发的时候,回调创建对象时的Watcher对象的process方法
* false: ==null 不添加监听
* 3. watcher对象 触发监听的时候调用的是此处的process
* 监听每触发一次,都会回调process函数
*/
zk.exists("/test", true);
zk.exists("/tt02", new org.apache.zookeeper.Watcher() {
public void process(WatchedEvent watchedEvent) {
String path = watchedEvent.getPath();
Event.EventType type = watchedEvent.getType();
Event.KeeperState state = watchedEvent.getState();
System.out.println(path + "--" + type + "--" + state);
}
});
// 触发监听 exists添加的监听可由create delete setData触发
zk.create("/test", "test".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
zk.close();
}
}