Zookeeper是什么
官方文档上这么解释zookeeper,它是一个分布式服务框架,是Apache Hadoop 的一个子项目,它主要是用来解决分布式应用中经常遇到的一些数据管理问题,如:统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等。
简单来说,zk有两大要点:
- 文件结构(包含node节点类型)
- watch机制
类似分布式锁,集群管理,队列,统一管理配置等使用,都是基于以上两个特点展开的。
文件结构(包含node节点类型)
Zookeeper维护一个类似文件系统的数据结构
每个子目录项如NameService都被称为znode,和文件系统一样,我们能够自由的增加、删除znode,在znode下增加、删除子znode,唯一不同的在于znode是可以存储数据的。
有4种类型的znode
1、PERSISTENT–持久化目录节点
客户端与zookeeper断开连接后,该节点依旧存在
2、PERSISTENT_SEQUENTIAL-持久化顺序编号目录节点
客户端与zookeeper断开连接后,该节点依旧存在,只是Zookeeper给该节点名称进行顺序编号
3、EPHEMERAL-临时目录节点
客户端与zookeeper断开连接后,该节点被删除
4、EPHEMERAL_SEQUENTIAL-临时顺序编号目录节点
客户端与zookeeper断开连接后,该节点被删除,只是Zookeeper给该节点名称进行顺序编号
watch机制
订阅-发布,也即观察者模式(watch机制),在zk中是通过事件注册和回调机制实现的,下面看下这部分内容。
整个注册回调过程分为三个大的部分:客户端注册,服务端发回事件,客户端回调
1.客户端注册
回调接口:
public interface Watcher {
abstract public void process(WatchedEvent event);
}
所有的事件回调接口都需要实现这个接口,并在process内部实现回调逻辑。event封装了事件的信息。event有两个层级,第一个是state,第二个是evetType。不同的state有不同的type
下面是对应关系:
注册的接口基本上是我们先实现一个watch接口,作为回调处理逻辑,然后调用以上的接口来注册感兴趣的事件。
以getData同步版本来说明,异步的其实在注册这一块是一样的,都是通过构造packet来完成。
WatchRegistration wcb = null;
if (watcher != null) {
wcb = new DataWatchRegistration(watcher, clientPath);
}
。。。
ReplyHeader r = cnxn.submitRequest(h, request, response, wcb);
在getData内部,首先构建了一个watchRegistration实例,这个类后面说,总之它封装了了回调接口和关注节点。然后把这个注册对象和packetheader一起传入了submit方法。再看submit方法:
Packet queuePacket(RequestHeader h, ReplyHeader r, Record request,
Record response, AsyncCallback cb, String clientPath,
String serverPath, Object ctx, WatchRegistration watchRegistration)
{
Packet packet = null;
synchronized (outgoingQueue) {
if (h.getType() != OpCode.ping && h.getType() != OpCode.auth) {
h.setXid(getXid());
}
packet = new Packet(h, r, request, response, null,
watchRegistration);
packet.cb = cb;
packet.ctx = ctx;
packet.clientPath = clientPath;
packet.serverPath = serverPath;
if (!zooKeeper.state.isAlive() || closing) {
conLossPacket(packet);
} else {
// If the client is asking to close the session then
// mark as closing
if (h.getType() == OpCode.closeSession) {
closing = true;
}
outgoingQueue.add(packet);
}
}
sendThread.wakeup();
return packet;
}
主要就是设置了packet的属性,然后把这个请求packet送入了发送队列。要知道我们注册回调的接口本来是用来获取数据的,所以回调依附在了获取这个过程中,这里的packet构造主要是为了获取一次数据,构建的一个请求包,我们的事件回调依附了这个过程,然后作为了这个请求packet的属性保存了起来。因为我们的是同步版本,所以packet的异步接口cb在上一步设置为了null。这里和回调相关的就是设置了packet的watchRegistration属性,也就是我们传入的回调接口,这是通过packet的构造方法完成的。所以有必要看下一个请求packet的内部:
static class Packet {
RequestHeader header;
ByteBuffer bb;
/** Client's view of the path (may differ due to chroot) **/
String clientPath;
/** Servers's view of the path (may differ due to chroot) **/
String serverPath;
ReplyHeader replyHeader;
Record request;
Record response;
boolean finished;
AsyncCallback cb;
Object ctx;
WatchRegistration watchRegistration;
这是packet的属性,这里的wathRegistration就是回调接口,cb是getData的异步版本的回调,在得到数据以后的回调函数,也就是上面我们谈到的设为null的属性,因为我们看的是getData的同步版本,所以为null。需要明确两个回调的区别。
到这里,我们的事件回调函数已经和这次getData请求的packet关联起来的。
那么,最后这个packet就会进入到outgoingQueue中被发送。
也就是在SendThread的一次write过程中。
然后getData请求的数据就会被服务器返回,在SendThread的一次read过程中,具体在readResponse函数中的最后部分,也就是finishPacket函数中,完成最后的注册:
private void finishPacket(Packet p) {
if (p.watchRegistration != null) {
p.watchRegistration.register(p.replyHeader.getErr());
}
if (p.cb == null) {
synchronized (p) {
p.finished = true;
p.notifyAll();
}
} else {
p.finished = true;
eventThread.queuePacket(p);
}
}
可以看到这里调用了一个register的方法。
下面需要了解下zk客户端与注册有关的数据结构:
在ZooKeeper类中,有一个内部类ZKWatchManager,是客户端存储所有的事件注册的类,里面有以下几个重要的属性,存储回调:
private static class ZKWatchManager implements ClientWatchManager {
private final Map<String, Set<Watcher>> dataWatches =
new HashMap<String, Set<Watcher>>();
private final Map<String, Set<Watcher>> existWatches =
new HashMap<String, Set<Watcher>>();
private final Map<String, Set<Watcher>> childWatches =
new HashMap<String, Set<Watcher>>();
private volatile Watcher defaultWatcher;
从名字上就可以看出各个属性的作用,正好对应了我们开始所说的4种回调。
map中的key就是节点的path,set就是该节点上所有的回调。因为默认的回调处理只有一个,所以就不是map,其余的事件,每一个节点都可能会有多个,所以是一个set。
再看一直出现的WatchRegistration结构:
abstract class WatchRegistration {
private Watcher watcher;
private String clientPath;
public WatchRegistration(Watcher watcher, String clientPath)
{
this.watcher = watcher;
this.clientPath = clientPath;
}
是一个抽象类,其实就是封装了一个事件注册,包括了感兴趣的节点和回调函数。data,children和exist三种事件都有一个对应的实现类。这个抽象类有一个非抽象方法register,负责将packet里面的watchRegistration放到之前的watchmanager中:
public void register(int rc) {
if (shouldAddWatch(rc)) {
Map<String, Set<Watcher>> watches = getWatches(rc);
synchronized(watches) {
Set<Watcher> watchers = watches.get(clientPath);
if (watchers == null) {
watchers = new HashSet<Watcher>();
watches.put(clientPath, watchers);
}
watchers.add(watcher);
}
}
}
首先根据事件类型拿到正确的map,然后把watch回调放入map里面。
至此客户端注册一个事件回调的逻辑就清晰了,总结就是,通过注册函数来设置回调接口为packet的属性。然后在注册函数收到其自身希望得到的数据的时候,来把回调函数注册到manager上。
服务端接受处理请求后,针对节点的修改,新增,删除,回出发节点的注册时间,服务端通过nio县城发起通知给客户端,执行客户端指定watcher的process方法,进行逻辑处理。
使用场景
1、命名服务
在zookeeper的文件系统里创建一个目录,即有唯一的path,在我们使用tborg无法确定上游程序的部署机器时即可与下游程序约定好path,通过path即能互相探索发现。
2、配置管理
程序总是需要配置的,如果程序分散部署在多台机器上,要逐个改变配置就变得困难。好吧,现在把这些配置全部放到zookeeper上去,保存在 Zookeeper 的某个目录节点中,然后所有相关应用程序对这个目录节点进行监听,一旦配置信息发生变化,每个应用程序就会收到 Zookeeper 的通知,然后从 Zookeeper 获取新的配置信息应用到系统中就好。
3、集群管理
所谓集群管理无在乎两点:是否有机器退出和加入、选举master。
对于第一点,所有机器约定在父目录GroupMembers下创建临时目录节点,然后监听父目录节点的子节点变化消息。一旦有机器挂掉,该机器与 zookeeper的连接断开,其所创建的临时目录节点被删除,所有其他机器都收到通知:某个兄弟目录被删除,于是,所有人都知道:它上船了。新机器加入 也是类似,所有机器收到通知:新兄弟目录加入,highcount又有了。
对于第二点,我们稍微改变一下,所有机器创建临时顺序编号目录节点,每次选取编号最小的机器作为master就好。
4、分布式锁
有了zookeeper的一致性文件系统,锁的问题变得容易。锁服务可以分为两类,一个是保持独占,另一个是控制时序。
对于第一类,我们将zookeeper上的一个znode看作是一把锁,通过createznode的方式来实现。所有客户端都去创建 /distribute_lock 节点,最终成功创建的那个客户端也即拥有了这把锁。厕所有言:来也冲冲,去也冲冲,用完删除掉自己创建的distribute_lock 节点就释放出锁。
对于第二类, /distribute_lock 已经预先存在,所有客户端在它下面创建临时顺序编号目录节点,和选master一样,编号最小的获得锁,用完删除,依次方便。
5、队列管理
两种类型的队列:
1、 同步队列,当一个队列的成员都聚齐时,这个队列才可用,否则一直等待所有成员到达。
2、队列按照 FIFO 方式进行入队和出队操作。
第一类,在约定目录下创建临时目录节点,监听节点数目是否是我们要求的数目。
第二类,和分布式锁服务中的控制时序场景基本原理一致,入列有编号,出列按编号。