简单来说Zookeeper=文件系统+监听通知机制。
zookeeper功能非常强大,可以实现诸如分布式应用配置管理、统一命名服务、状态同步服务、集群管理等功能,我们这里拿比较简单的分布式应用配置管理为例来说明。
假设我们的程序是分布式部署在多台机器上,如果我们要改变程序的配置文件,需要逐台机器去修改,非常麻烦,现在把这些配置全部放到zookeeper上去,保存在 zookeeper 的某个目录节点中,然后所有相关应用程序对这个目录节点进行监听,一旦配置信息发生变化,每个应用程序就会收到 zookeeper 的通知,然后从 zookeeper 获取新的配置信息应用到系统中。
集群角色
读写区分
1.ZooKeeper 集群的所有机器通过一个 Leader 选举过程来选定一台被称为『Leader』
的机器,Leader服务器为客户端提供读和写服务。
2.Follower 和 Observer 都能提供读服务,不能提供写服务。两者唯一的区别在于,
Observer机器不参与 Leader 选举过程,也不参与写操作的『过半写成功』策略,因
此 Observer 可以在不影响写性能的情况下提升集群的读性能。
Session
ZooKeeper 对外的服务端口默认是2181,客户端启动时,首先会与服务器建立一个TCP
连接,从第一次连接建立开始,客户端会话的生命周期也开始了,通过这个连接,客户端能够通
过心跳检测和服务器保持有效的会话,也能够向 ZooKeeper 服务器发送请求并接受响应,同
时还能通过该连接接收来自服务器的 Watch 事件通知。
SessionTimeout 值用来设置一个客户端会话的超时时间。当由于服务器
压力太大、网络故障或是客户端主动断开连接等各种原因导致客户端连接断开时,只要在
SessionTimeout 规定的时间内能够重新连接上集群中任意一台服务器,那么之前创建的会话
仍然有效。
节点
zookeeper的结构其实就是一个树形结构,leader就相当于其中的根结点,其它节点就相当于
follow节点,每个节点都保留自己的内容。
zookeeper的节点分两类:持久节点和临时节点
- 持久节点:
所谓持久节点是指一旦这个 树形结构上被创建了,除非主动进行对树节点的移除操
作,否则这个 节点将一直保存在 ZooKeeper 上。
- 临时节点:
临时节点的生命周期跟客户端会话绑定,一旦客户端会话失效,那么这个客户端创
建的所有临时节点都会被移除。
Watcher
是 ZooKeeper 中一个很重要的特性。ZooKeeper允许用户在指定节点上注册一些 Watcher,
特定事件触发的时候,ZooKeeper 服务端会将事件通知客户端。是 ZooKeeper 实现分布式协调服务机制。
分布式锁
zookeeper实现分布式锁就是应用了临时顺序节点,具体流程如下:
① 100名客户在极短的时间内去访问商品服务,进行购买商品的操作。
② 获取锁
zookeeper创建100个临时顺序节点,这些节点按照请求的先后顺序进行排序。排名第一的 req1 优先获取锁,排名第二 req2 的向只比他靠前一名的 req1 注册Watcher,排名第三的 req3 向 req2 注册Watcher,依次类推。
③ 释放锁
◉正常执行完释放锁
当 req1 执行完成之后,zookeper会删除该节点,排名第二的 req2 获得锁,依次类推。
◉客户端崩溃释放锁
当排名第一的用户在购买时断网,因为zookeeper为其创建的是临时顺序节点,此时排名第一的用户获得的锁也将释放。排名第二的用户获得锁。
ZooKeeper 配置很简单,每个节点的配置文件(zoo.cfg)都是一样的,只有 myid 文件不一样。myid 的值必须是 zoo.cfg中server.{数值} 的{数值}部分。
使用说明
import java.util.concurrent.CountDownLatch;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.Watcher.Event.EventType;
import org.apache.zookeeper.Watcher.Event.KeeperState;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;
/**
* 分布式配置中心demo
* @author
*
*/
public class ZooKeeperProSync implements Watcher {
private static CountDownLatch connectedSemaphore = new CountDownLatch(1);
private static ZooKeeper zk = null;
private static Stat stat = new Stat();
public static void main(String[] args) throws Exception {
//zookeeper配置数据存放路径
String path = "/username";
//连接zookeeper并且注册一个默认的监听器
zk = new ZooKeeper("192.168.31.100:2181", 5000, //
new ZooKeeperProSync());
//等待zk连接成功的通知
connectedSemaphore.await();
//获取path目录节点的配置数据,并注册默认的监听器
System.out.println(new String(zk.getData(path, true, stat)));
Thread.sleep(Integer.MAX_VALUE);
}
public void process(WatchedEvent event) {
if (KeeperState.SyncConnected == event.getState()) { //zk连接成功通知事件
if (EventType.None == event.getType() && null == event.getPath()) {
connectedSemaphore.countDown();
} else if (event.getType() == EventType.NodeDataChanged) { //zk目录节点数据变化通知事件
try {
System.out.println("配置已修改,新值为:" + new String(zk.getData(event.getPath(), true, stat)));
} catch (Exception e) {
}
}
}
}
}
集群的配置
tickTime:这个时间是作为 Zookeeper 服务器之间或客户端与服务器之间维持心跳的时间间隔,也就是每个 tickTime 时间就会发送一个心跳。
initLimit:这个配置项是用来配置 Zookeeper 接受客户端(这里所说的客户端不是用户连接 Zookeeper 服务器的客户端,而是 Zookeeper 服务器集群中连接到 Leader 的 Follower 服务器)初始化连接时最长能忍受多少个心跳时间间隔数。当已经超过 10个心跳的时间(也就是 tickTime)长度后 Zookeeper 服务器还没有收到客户端的返回信息,那么表明这个客户端连接失败。总的时间长度就是 10*2000=20 秒
syncLimit:这个配置项标识 Leader 与 Follower 之间发送消息,请求和应答时间长度,最长不能超过多少个 tickTime 的时间长度,总的时间长度就是 5*2000=10秒
dataDir:顾名思义就是 Zookeeper 保存数据的目录,默认情况下,Zookeeper 将写数据的日志文件也保存在这个目录里。
clientPort:这个端口就是客户端连接 Zookeeper 服务器的端口,Zookeeper 会监听这个端口,接受客户端的访问请求。
server.A=B:C:D:其中 A 是一个数字,表示这个是第几号服务器;B 是这个服务器的 ip 地址;C 表示的是这个服务器与集群中的 Leader 服务器交换信息的端口;D 表示的是万一集群中的 Leader 服务器挂了,需要一个端口来重新进行选举,选出一个新的 Leader,而这个端口就是用来执行选举时服务器相互通信的端口。如果是伪集群的配置方式,由于 B 都是一样,所以不同的 Zookeeper 实例通信端口号不能一样,所以要给它们分配不同的端口号。
ZAB协议:(Zookeeper 原子广播协议)ZAB 协议是为分布式协调服务 Zookeeper 专门设计的一种支持 崩溃恢复 和 原子广播 协议。下面我们会重点讲这两个东西。
简单介绍完,开始重点介绍 消息广播 和 崩溃恢复。整个 Zookeeper 就是在这两个模式之间切换。 简而言之,当 Leader 服务可以正常使用,就进入消息广播模式,当 Leader 不可用时,则进入崩溃恢复模式。
消息广播:
客户端发送的写请求,全部由 Leader 接收,Leader 将请求封装成一个事务 Proposal,将其发送给所有 Follwer ,然后,根据所有 Follwer 的反馈,如果超过半数成功响应,则执行 commit 操作(先提交自己,再发送 commit 给所有 Follwer)。步骤如下:
1. Leader 在收到客户端请求之后,会将这个请求封装成一个事务,并给这个事务分配一个全局递增的唯一 ID,称为事务ID(ZXID),ZAB 兮协议需要保证事务的顺序,因此必须将每一个事务按照 ZXID 进行先后排序然后处理。
2. 在 Leader 和 Follwer 之间还有一个消息队列,用来解耦他们之间的耦合,解除同步阻塞。
3. zookeeper集群中为保证任何所有进程能够有序的顺序执行,只能是 Leader 服务器接受写请求,即使是 Follower 服务器接受到客户端的请求,也会转发到 Leader 服务器进行处理。
其他问题如
假设1:Leader 在复制数据给所有 Follwer 之后崩溃,怎么办?
假设2:Leader 在收到 Ack 并提交了自己,同时发送了部分 commit 出去之后崩溃怎么办?
选举算法进行选举,能够确保提交已经被 Leader 提交的事务,同时丢弃已经被跳过的事务。如果让 Leader 选举算法能够保证新选举出来的 Leader 服务器拥有集群总所有机器编号(即 ZXID 最大)的事务,那么就能够保证这个新选举出来的 Leader 一定具有所有已经提交的提案,即leader和follower数据一致。如果不包含所有的ZXID,follower 的 peerLastZxid小于 leader 的 maxCommittedLog,那就同步,则告知 follower 回滚至 maxCommittedLog,那么就回滚,再同步
**如何同步?**在 ZAB 协议的事务编号 ZXID 设计中,ZXID 是一个 64 位的数字,其中低 32 位可以看作是一个简单的递增的计数器,针对客户端的每一个事务请求,Leader 都会产生一个新的事务 Proposal 并对该计数器进行 + 1 操作。而高 32 位则代表了 Leader 服务器上取出本地日志中最大事务 Proposal 的 ZXID。当 Follower 链接上 Leader 之后,Leader 服务器会根据自己服务器上最后被提交的 ZXID 和 Follower 上的 ZXID 进行本机保存的数据比较,比对结果要么回滚,要么和 Leader 同步。