zookeeper
Zookeeper是开源的分布式的协调服务框架,是 Apache Hadoop的子件,适用于绝大部分分布式集群的管理
分布式引发问题:
- 死锁:至少有一个线程占用了资源,但是不占用CPU
- 活锁:所有线程都没有把持资源,但是线程却是在不断地调度占用CPU
- 需要引入一个管理节点
- 为了防止入口的单点问题,需要引入管理节点的集群
- 需要在管理阶段中选举出一个主节点
- 需要确定一套选举算法
- 主节点和从节点之间要保证数据的一致
zookeeper的特点
- 本身是一个树状结构 — Znode树
- 每一个节点称之为znode节点
- 根节点是 /
- Zookeeper的所有操作都必须以根节点为基准进行计算 /
- 每一个znode节点都必须存储数据
- 任意一个持久节点都可以有子节点
- 任意一个节点的路径都是唯一的
- Znode树是维系在内存中 — 目的是为了快速查询
- Zookeeper不适合存储海量数据。原因:1)维系在内存中,如果存储大量数据会耗费内存 2) 不是一个存储框架而是一个服务协调框架
- Zookeeper会为每一次事务(除了读取以外的所有操作都是事务)分配一个全局的事务id —Zxid
zookeeper命令
- create /node01 “hello zookeeper” 在根节点下创建子节点node01并且赋值为hello zookeeper
- set /node01 “hi” 将node01的数据更新为hi
- get /node01 获取node01的数据
- ls / 查看根节点下的所有的子节点
- delete /node02删除子节点,要求这个节点中没有子节点
- rmr /node01 删除node01及其子节点
- quit 退出客户端
- create -e /node02 ''表示在根目录下创建临时节点/node02
- create -s /node03 ‘’ 表示在根目录下创建持久顺序节点/node03000000000X
- create -e -s /node04 ‘’ 表示在根目录下创建临时顺序节点/node04000000000X
zookeeper的节点类型
- 持久节点
- 临时节点:在客户端退出之后就自动删除
- 持久顺序节点
- 临时顺序节点
zookeeper的节点信息
- cZxid - 全局分配的创建的事务id — 创建这个节点是Zookeeper的第n个操作 — 定义好之后就不变了
- ctime - 创建时间
- mZxid - 修改的事务id — 记录自己本身修改
- mtime - 修改时间
- pZxid - 表示子节点的变化的事务id — 记录直接子节点的创建或者删除
- cversion - 记录子节点变化次数
- dataVersion - 数据版本 — 记录当前节点的数据的变化次数
- aclVersion - acl版本 — 记录当前节点的acl的变化次数
- ephemeralOwner - 如果当前节点不是临时节点,那么这个属性的值为0;如果是临时节点,那么这个属性的值记录的是当前临时节点的session id
- dataLength - 数据长度 - 实际上是字节个数
- numChildren - 子节点个数
zookeeper的java api操作
在maven工程中添加pom依赖
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.tedu</groupId>
<artifactId>zookeeper</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>zk</name>
<url>http://maven.apache.org</url>
<dependencies>
<dependency>
<!-- 单元测试 -->
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.10</version>
</dependency>
<dependency>
<!-- 日志记录 -->
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>连接操作
创建节点
<version>1.6.4</version>
<scope>compile</scope>
</dependency>
<dependency>
<!-- Zookeeper -->
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.7</version>
</dependency>
</dependencies>
<build>
</build>
</project>
连接操作
@Test
public void connect() throws IOException, InterruptedException{
CountDownLatch cdl = new CountDownLatch(1);
// connectString - 连接地址+端口号
// sessionTimeout - 会话超时时间 - 表示连接超时时间,单位默认为毫秒
// watcher - 监控者 - 监控连接状态
// Zookeeper本身是一个非阻塞式连接
ZooKeeper zk = new ZooKeeper("117.50.90.232:2181", 5000, new Watcher() {
// 监控连接状态
public void process(WatchedEvent event) {
if(event.getState() == KeeperState.SyncConnected){
System.out.println("连接成功~~~");
}
cdl.countDown();
}
});
cdl.await();
}
创建节点
@Test
public void create() throws KeeperException, InterruptedException {
// path - 节点路径
// data - 数据
// acl - acl策略
// createMode - 节点类型
// 返回值表示节点的实际路径
String str = zk.create("/node08", "hello,1807".getBytes(),
Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT_SEQUENTIAL);
System.out.println(str);
}
删除节点
@Test
public void delete() throws InterruptedException, KeeperException {
// path - 节点路径
// version - 数据版本
// 在删除节点的时候回比较指定的数据版本和节点的实际数据版本是否一致
// 如果一致则删除,如果不一致则放弃该操作
// zk.delete("/node04", 0);
// -1表示强制执行
zk.delete("/node04", -1);
}
跟新数据
@Test
public void set() throws KeeperException, InterruptedException{
// path - 节点路径
//data - 数据
// version - 数据版本
// 返回节点信息
Stat s = zk.setData("/node02", "hi,zk".getBytes(), -1);
System.out.println(s);
}
获取数据
@Test
public void get() throws KeeperException, InterruptedException, UnsupportedEncodingException {
// path - 节点路径
// watch - 监控
// stat - 节点信息
Stat s = new Stat();
byte[] data = zk.getData("/node01", null, s );
System.out.println(new String(data, "utf-8"));
}
判断节点存在
@Test
public void exist() throws KeeperException, InterruptedException{
// path - 节点路径
// watch - 监控者
// 返回的节点的信息
Stat s = zk.exists("/node07", null);
System.out.println(s);
}
获取子节点
@Test
public void getChidren() throws KeeperException, InterruptedException {
// path - 节点路径
// watch - 监控者
// 返回子节点的路径
List<String> nodes = zk.getChildren("/" ,null);
for (String p : nodes) {
System.out.println(p);
}
}
监听节点数据变化
@Test
public void dataChanged() throws KeeperException, InterruptedException {
CountDownLatch cdl = new CountDownLatch(1);
// 获取的是监听之前的数据
// 获取数据和监听变化实际上是两个线程
byte[] data = zk.getData("/node01", new Watcher() {
@Override
public void process(WatchedEvent event) {
if (event.getType() == EventType.NodeDataChanged)
System.out.println("节点数据发生变化~~~");
cdl.countDown();
}
}, null);
cdl.await();
System.out.println(new String(data));
}
监听子节点变化
@Test
public void childChanged() throws KeeperException, InterruptedException {
CountDownLatch cdl = new CountDownLatch(1);
zk.getChildren("/node01", new Watcher() {
@Override
public void process(WatchedEvent event) {
if (event.getType() == EventType.NodeChildrenChanged)
System.out.println("子节点产生变化~~~");
cdl.countDown();
}
});
cdl.await();
}
监听节点变化
@Test
public void nodeChanged() throws KeeperException, InterruptedException {
CountDownLatch cdl = new CountDownLatch(1);
zk.exists("/node06", new Watcher() {
@Override
public void process(WatchedEvent event) {
if (event.getType() == EventType.NodeCreated)
System.out.println("这个节点被创建~~~");
else if (event.getType() == EventType.NodeDeleted)
System.out.println("这个节点被删除~~~");
cdl.countDown();
}
});
cdl.await();
}
zookeeper的集群选举机制
第一阶段:数据恢复阶段。
每台Zookeeper服务器在启动的时候,都会从本地的数据目录中找到自己所拥有的最大事务id。
第二阶段:选举阶段。
每一个Zookeeper的服务器都会推荐自己当leade并且提交选举协议:
1. 自己所拥有的最大事务id - Zxid
2. 自己的选举id - myid
3. 逻辑时钟值,作用是确保每一台Zookeeper服务器都会处在同一轮选举中
4. 当前状态
a. Looking - 选举状态
b. follower - 追随者
c. leader - 领导者
d. observer - 观察者
pk原则
- 在选举的时候会先比较两个节点的最大事务id即Zxid。Zxid越大,则说明事务越新。Zxid较大的会胜出
- 如果Zxid一致,则比较选举id,选举id较大的胜出。
- 满足过半性:即超过一半的节点才能成为leader
如果在集群中新加入一个节点,节点的事务id比leader的事务id大,新的节点是否会成为leader? — 不会。只要选定了一个leader,那么后续节点的事务id和myid无论是多少,一律都是follower
如果leader宕机,集群中会自动选举一个新的leader
如果超过一半的服务器宕机,那么此时Zookeeper不再对外提供服务 — 过半性
过半性
- 过半选举:即只有一个节点胜过一半的节点之后才能成为leader
- 过半服务:即只有Zookeeper集群中超过一半的节点存活才能对外提供服务–防止脑裂(集群中产生两个及以上的leader【一般讲集群节点数设置为奇数】)
- 过半操作:即只有Zookeeper集群中超过一半的节点同意才会提交请求
观察者
执行操作,但是不参与选举和投票,所以不影响集群的运行状态(适用于网络不稳定状况)
配置观察者
编辑zoo.cfg,添加:peerType=observer,再在要设置的主机之后添加observer标记,例如:server.3=10.9.130.83:2888:3888:observer,然后重新启动这一个节点
ZAB协议
Zookeeper的投票和选举的底层是基于了ZAB协议来实现的。ZAB协议是在2PC算法以及Paxos的基础上进行的设计
和延伸。
2PC(Two-Phase Commit)
将节点分为了协调者和参与者。当来一个请求的时候,协调者是将请求分发给每一个参与者,如果所有的参与者决定执行这个请求,那么协调者就真正提交操作,有参与者执行,参与者在执行完成之后返回Ack表示执行成功。如果协调者将请求分发给每一个参与者之后,有一个或者多个参与者不同意执行或者是没有返回消息,那么将这个操作回滚,不执行 — 一票否决
在Zookeeper中,接收一个请求之后,leader会将请求分发给每一个节点,由所有的节点投票确定是否执行这个请求 — 如果有超过一半的节点同意执行这个请求,那么这个时候leader才会决定执行这个操作
2PC的缺点:
同步阻塞
二阶段提交协议存在的最明显也是最大的一个问题就是同步阻塞,这会极大地限制分布式系统的性能。在二阶段提
交的执行过程中,所有参与该事务操作的逻辑都处于阻塞状态,也就是说,各个参与者在等待其他参与者响应的过
程中,将无法进行其他任何操作。
单点问题
在上面的讲解过程中,相信读者可以看出,协调者的角色在整个二阶段提交协议中起到了非常重要的作用。一旦协
调者出现问题,那么整个二阶段提交流程将无法运转,更为严重的是,如果协调者是在阶段二中出现问题的话,那
么其他参与者就无法完成事务。
太过保守
如果在协调者指示参与者进行事务提交询问的过程中,参与者出现故障而导致协调者始终无法获取到所有参与者的
响应信息的话,这时协调者只能依靠其自身的超时机制来判断是否需要中断事务,这样的策略显得比较保守。换句
话说,二阶段提交协议没有设计较为完善的容错机,任意一个节点的失败都会导致整个事务的失败。
ZAB
ZAB(Zookeeper Atomic Broadcast)协议是为分布式协调服务ZooKeeper专门设计的一种支持崩溃恢复的原子
广播协议,它是一种特别为ZooKeeper设计的崩溃可恢复的原子消息广播算法。这个算法是一种类2PC算法,在2PC
基础上做的改进
ZAB协议包括两种基本的模式,分别是:
- 消息原子广播(保证数据一致性)
- 崩溃恢复(解决2pc算法的单点问题
消息原子广播
server.x=ip:port1:port2;
port1代表原子广播的端口号,也就意味着leader和follower的通信是通过这个端口进行传送
port2代表选举的端口号。选举信息是通过这个端口进行接收的
崩溃恢复
当Zookeeper集群中的leader宕机之后,Zookeeper集群不会出现单点问题,而是在满足过半性的条件下会快速的重新选举出一个新的leader,然后对外服务。在Zookeeper的选举过程中,不会对外提供服务的。
zookeeper的事务log
事务日志指Zookeeper系统在正常运行过程中,针对所有的事务操作,在返回客户端“事务成功”的响应前,
Zookeeper会保证已经将本次更新操作的事务日志已经写到磁盘上。 Zookeeper的事务日志为二进制文件,不能通
过vim等工具直接访问。其实可以通过zookeeper自带的jar包读取事务日志文件。
查看事务log执行
- 到Zookeeper的安装目录下
- 打开lib
- 将slf4j-api-1.6.1.jar拷贝到version-2目录下 cp slf4j-api-1.6.1.jar …/tmp/version-2/
- 回到Zookeeper的安装目录下
- 将zookeeper-3.4.8.jar拷贝到version-2目录下 cp zookeeper-3.4.8.jar tmp/version-2/
- 进入到version-2目录下
- 运行java命令来查看日志文件
java -cp .:zookeeper-3.4.8.jar:slf4j-api-1.6.1.jar org.apache.zookeeper.server.LogFormatter ./log.500000001
snapshot - 快照文件 - 记录当前Zookeeper中的一部分节点信息
java -cp .:zookeeper-3.4.8.jar:slf4j-api-1.6.1.jar org.apache.zookeeper.server.SnapshotFormatter ./snapshot.400000004
在集群环境中,事务id一共有64位二进制来组成,其中高32位表示epoch id,低32位才是当前的事务id
配置信息
tickTime - 是Zookeeper中的时间单元。意味着后边计算时间都是以当前单元为基准进行计算。默认值是2000,单位是毫秒
initLimit - 当Zookeeper节点重新启动之后需要将当前节点中的数据进行恢复并且最大事务id,如果事务越大恢复时间就越长。这个属性就是限制恢复时间。默认值为10,也就意味着默认初始时间为:10*tickTime=20s。但是实际开发中,如果数据量较大,可以适当调大这个属性
syncLimit - 当有新的操作的时候,leader和follower进行通信,leader等待响应的时间。默认值是5,也就意味着,leader会等待follower10s中,如果10s中之后leader依然没有收到follower的反馈,就认为这个消息无效或者丢失
dataDir - 数据目录。快照文件默认就是存在dataDir中的version-2目录中
dataLogDir - 日志文件的存放目录。如果不配置,那么默认和dataDir一致。
server.x=ip@p1:p2 - 集群配置信息
snapCount - 1)如果一个节点中的事务操作达到了snapCount次,就会生成一个快照文件,默认数量时100000。2)每次选举出来一个leader,就会生成一个快照文件
autopurge.purgeInterval - 定时清理快照文件。默认值是1,默认单位是小时,表示每隔1个小时清理一次快照文件。从Zookeeper的3.4.0版本开始,Zookeeper新添了自动清理快照文件的功能。
autopurge.snapRetainCount - 快照文件的剩余数量。清理快照文件的时候,需要留下最新的snapRetainCount个快照
jute.maxbuffer - Zookeeper的每一个节点都要存储信息,限制每一个节点的数据大小。默认值是1M,表示每一个节点的数据大小不能超过1M
globalOutstandingLimit - 最大积压数 - leader在接收到操作之后,将操作放到一个队列中发送给follower,队列是一个阻塞式队列,也就意味着leader给follower发送的操作个数是有限的。队列已满,有了新的请求,先接受这个请求进行缓存到leader,然后等follower执行完成之后再将缓存的请求发送给follower,这样子的好处是提供了Zookeeper的吞吐量。leader缓存的操作越多,越耗费内存,会降低整体的效率,所以这个时候需要限制缓存的操作的个数。
preAllocSize - 预先开辟的磁盘空间,用于存储日志文件。默认值是64M,也就意味着日志文件的大小是64M
leaderServers - 在一个Zookeeper集群中,leader也可以对外提供服务。如果需要leader专心于协调服务而不对外提供服务,这个时候将这个属性设置为no,提高了Zookeeper的运行效率。
maxClientCnxns - 限制客户端的连接数量。默认值是60
zookeeper集群命令
- 下载nc-1.84-22.el6.x86_64.rpm — netcat,是基于TCP进行网络通信的组件
- 安装netcat
Zookeeper的集群指令
- 查看节点状态 echo stat|nc 10.9.152.65 2181
- 判断节点的存活状态 echo ruok|nc 10.9.152.65 2181 返回imok,说明节点依然存活
- 查看节点的配置echo conf|nc 10.9.152.65 2181
- 关闭节点 echo kill|nc 10.9.152.65 2181
zookeeper特性总结
- 数据一致性
客户端不论连接到哪个Zookeeper节点上,展示给它都是同一个视图,即查询的数据都是一样的。这是
Zookeeper最重要的性能。 - 原子性
对于事务决议的更新,只能是成功或者失败两种可能,没有中间状态。 要么都更新成功,要么都不更新。
即,要么整个集群中所有机器都成功应用了某一事务,要么都没有应用,一定不会出现集群中部分机器应用
了改事务,另外一部分没有应用的情况。 - 可靠性
一旦Zookeeper服务端成功的应用了一个事务,并完成对客户端的响应,那么该事务所引起的服务端状态变
更将会一直保留下来,除非有另一个事务又对其进行了改变。 - 实时性
Zookeeper保证客户端将在非常短的时间间隔范围内获得服务器的更新信息,或者服务器失效的信息,或者
指定监听事件的变化信息。(前提条件是:网络状况良好) - 顺序性
如果在一台服务器上消息a在消息b前发布,则在所有服务器上消息a都将在消息b前被发布。客户端在发起请
求时,都会跟一个递增的命令号,根据这个机制,Zookeeper会确保客户端执行的顺序性。底层指的是Zxid。
可以通过事务log来看。 - 过半性
Zookeeper集群必须有半数以上的机器存活才能正常工作。因为只有满足过半性,才能满足选举机制选出
leader。因为只有过半,在做事务决议时,事务才能更新。所以一般来说,zookeeper集群的数量最好是奇数
个。