Zookeeper是什么

官方文档上这么解释zookeeper,它是一个分布式服务框架,是Apache Hadoop 的一个子项目,它主要是用来解决分布式应用中经常遇到的一些数据管理问题,如:统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等。
简单来说,zk有两大要点:

  • 文件结构(包含node节点类型)
  • watch机制

类似分布式锁,集群管理,队列,统一管理配置等使用,都是基于以上两个特点展开的。

文件结构(包含node节点类型)

Zookeeper维护一个类似文件系统的数据结构

resource manager向 ZooKeeper 写入太多数据并达到缓冲区限制 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

下面是对应关系:

resource manager向 ZooKeeper 写入太多数据并达到缓冲区限制 zookeeper数据存在哪里_zookeeper_02


注册的接口基本上是我们先实现一个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 方式进行入队和出队操作。

第一类,在约定目录下创建临时目录节点,监听节点数目是否是我们要求的数目。

第二类,和分布式锁服务中的控制时序场景基本原理一致,入列有编号,出列按编号。