ZooKeeper 3:Watch机制与其源码分析

prev: ZooKeeper 2:数据模型与访问控制

Watch机制

之前的内容介绍了节点的内部属性,如节点里面存储了什么信息,以及对节点的访问控制。那么接下来介绍节点的watch机制。
在这里插入图片描述
图源:ZooKeeper Watch机制

watch机制,顾名思义是一个监听机制。主要可以通过exists、getData和getChildren启用。
监听的对象是事件,那么事件主要有以下几种:

事件解释
创建创建节点事件,通过调用exists启用
删除创建节点,通过调用exists、getData和getChildren启用
修改创建节点通过调用exists和getData启用
子事件创建节点,通过getChildren启用

这个监听机制需要注意以下几点:

  • 单次有效
    例如,在客户端1创建了一个监听事件对node1,在node1进行修改时,会通知客户端1,但是下次再修改的时候,不会再通知客户端1了,因为watch单次有效。如果需要再次监听需要再次注册。
  • 顺序回调
    是为了防止网络时延对客户端的影响。ZooKeeper客户端库保证所有事件都会按顺序分发;客户端会保障它在看到相应的znode的新数据之前接收到watch事件。从ZooKeeper接收到的watch事件顺序一定和ZooKeeper服务所看到的事件顺序是一致的。
  • 两个监听列表
    Znode改变有很多种方式,例如:节点创建,节点删除,节点改变,子节点改变等等。Zookeeper维护了两个Watch列表,一个节点数据Watch列表,另一个是子节点Watch列表。getData()和exists()设置数据Watch,getChildren()设置子节点Watch。所以可以存在两个watch同时监听当前节点或子节点。

需要注意的是,在ZooKeeper3.6以后,允许在NodeCreated、NodeDeleted和NodeDataChanged事件上创建触发后不会被移除的watch。并且,可以选择递归注册watch,也就是从当前znode开始所有znode都注册watch。

对于一个已经创建的watch,可以对其进行删除。即使客户端没有连接服务器,也是可以删除watch的,即在本地标志位设置位true来删除。

那么还有一个问题:如果客户端和服务器之间断开了连接会发生什么呢?
根据文档以及接下来的代码可以知道:与服务器断开连接时(例如,当服务器发生故障时),在重新建立连接之前,无法获得任何监视。

简单使用

在这里插入图片描述

仍然使用上一节的树形结构,不过由于重启了ZooKeeper,所以图上所有的临时节点都已经不存在了,可以进行验证:

[zk: 127.0.0.1:2181(CONNECTED) 8] ls /locks
[]

可以看到重启客户端后,之前客户端的临时节点都被删除了。
接下来使用命令行注册一个watch在之前的持久节点/work上,/work下面还有三个持久节点(上一篇的容器节点)/work/zs-work、/work/ls-work、/work/lw-work

# 在/work上设置一个watch,注意可以先输入printwatches命令看watch输出有没有被打开
[zk: 127.0.0.1:2181(CONNECTED) 6] ls -w /work
[ls-work, lw-work, zs-work]

接下来删除/work/zs-work

# 删除/work/zs-work
[zk: 127.0.0.1:2181(CONNECTED) 7] delete /work/zs-work

WATCHER::

WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/work

可以看到监听事件被触发了,此时我们再创建zs-work

[zk: 127.0.0.1:2181(CONNECTED) 9] create -c /work/zs-work
Created /work/zs-work

创建成功,而且也没有监听了,因为监听仅单次生效。

底层机制源码分析

源码来自版本ZooKeeper 3.8.0

客户端请求逻辑

首先,客户端需要将带watch的请求传给服务器。

/**
* 版本:ZooKeeper 3.8.0
* org.apache.zookeeper
* ZooKeeper.java
* 1952行
*/
public byte[] getData(final String path, Watcher watcher, Stat stat) throws KeeperException, InterruptedException {
        final String clientPath = path;
        PathUtils.validatePath(clientPath);
		// 在客户端注册一个watch
        // the watch contains the un-chroot path
        WatchRegistration wcb = null;
        if (watcher != null) {
            wcb = new DataWatchRegistration(watcher, clientPath);
        }

        final String serverPath = prependChroot(clientPath);
		// 装配request
        RequestHeader h = new RequestHeader();
        h.setType(ZooDefs.OpCode.getData);
        GetDataRequest request = new GetDataRequest();
        request.setPath(serverPath);
        request.setWatch(watcher != null);
        GetDataResponse response = new GetDataResponse();
        // submit请求
        ReplyHeader r = cnxn.submitRequest(h, request, response, wcb);
		// 处理返回逻辑....
        return response.getData();
    }

可以看到,客户端调用了函数submitRequest()来submit此次请求,此函数的逻辑是:

	/**
	* 版本:ZooKeeper 3.8.0
	* org.apache.zookeeper
	* ClientCnxn.java
	* 1557行
	*/
	public ReplyHeader submitRequest(
        RequestHeader h,
        Record request,
        Record response,
        WatchRegistration watchRegistration) throws InterruptedException {
        return submitRequest(h, request, response, watchRegistration, null);
    }

    public ReplyHeader submitRequest(
        RequestHeader h,
        Record request,
        Record response,
        WatchRegistration watchRegistration,
        WatchDeregistration watchDeregistration) throws InterruptedException {
        ReplyHeader r = new ReplyHeader();
        // 调用queuePacket
        Packet packet = queuePacket(
            h,
            r,
            request,
            response,
            null,
            null,
            null,
            null,
            watchRegistration,
            watchDeregistration);
        //处理请求超时
        //.....
        if (r.getErr() == Code.REQUESTTIMEOUT.intValue()) {/*处理请求失败*/}
        return r;
    }
	/**
	* 版本:ZooKeeper 3.8.0
	* org.apache.zookeeper
	* ClientCnxn.java
	* 1648行
	*/
	public Packet queuePacket(
        RequestHeader h,
        ReplyHeader r,
        Record request,
        Record response,
        AsyncCallback cb,
        String clientPath,
        String serverPath,
        Object ctx,
        WatchRegistration watchRegistration,
        WatchDeregistration watchDeregistration) {
        Packet packet = null;
        packet = new Packet(h, r, request, response, watchRegistration);
		// 装配packet
		// ....
        synchronized (state) {
            if (!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中
                outgoingQueue.add(packet);
            }
        }
        // sendThread线程去做具体的发包
        sendThread.getClientCnxnSocket().packetAdded();
        return packet;
    }

在请求发送完成后,才能将Watch注册到ZKWatchManager中:

	/**
	* 版本:ZooKeeper 3.8.0
	* org.apache.zookeeper
	* ClientCnxn.java
	* 737行
	*/
	protected void finishPacket(Packet p) {
        int err = p.replyHeader.getErr();
        if (p.watchRegistration != null) {
        	// 将watch注册
            p.watchRegistration.register(err);
        }
        // 其他逻辑
        // ........
    }

服务器

服务端将带有watch的请求发给服务器之后,拿getData举例,会首先进入操作代码判断模块,一个swich-case判断操作类型是getData并进入相应逻辑:

/**
* 版本:ZooKeeper 3.8.0
* org.apache.zookeeper.server
* FinalRequestProcessor.java
* 378行
*/
public void processRequest(Request request) {
	// .....
	// 判断请求的类型
	switch (request.type){
		//.....
		// 操作类型为getDate的逻辑
		case OpCode.getData: {
		    lastOp = "GETD";
		    GetDataRequest getDataRequest = new GetDataRequest();
		    ByteBufferInputStream.byteBuffer2Record(request.request, getDataRequest);
		    path = getDataRequest.getPath();
		    rsp = handleGetDataRequest(getDataRequest, cnxn, request.authInfo);
		    requestPathMetricsCollector.registerRequest(request.type, path);
		    break;
	    }
	}
}      

可以看到这里写了个函数handleGetDataRequest()来处理request,此函数的逻辑是:

	/**
	* 版本:ZooKeeper 3.8.0
	* org.apache.zookeeper.server
	* FinalRequestProcessor.java
	* 662行
	*/
    private Record handleGetDataRequest(Record request, ServerCnxn cnxn, List<Id> authInfo) throws KeeperException, IOException {
        GetDataRequest getDataRequest = (GetDataRequest) request;
        String path = getDataRequest.getPath();
        DataNode n = zks.getZKDatabase().getNode(path);
        // 处理路径不存在的逻辑
        if (n == null) {
            throw new KeeperException.NoNodeException();
        }
        // 检查访问控制
        zks.checkACL(cnxn, zks.getZKDatabase().aclForNode(n), ZooDefs.Perms.READ, authInfo, path, null);
        Stat stat = new Stat();
        // 注意:这里判断是否传入了watch
        byte[] b = zks.getZKDatabase().getData(path, stat, getDataRequest.getWatch() ? cnxn : null);
        return new GetDataResponse(b, stat);
    }

在这里判断了是否传入了watch。
当节点发生事件时,例如还是以节点删除为例:

/**
* 版本:ZooKeeper 3.8.0
* org.apache.zookeeper.server
* DataTree.java
* 543行
*/
public void deleteNode(String path, long zxid) throws KeeperException.NoNodeException {
		// 删除逻辑
        // ........
        // 触发节点删除事件
        WatcherOrBitSet processed = dataWatches.triggerWatch(path, EventType.NodeDeleted);
        childWatches.triggerWatch(path, EventType.NodeDeleted, processed);
        childWatches.triggerWatch("".equals(parentName) ? "/" : parentName, EventType.NodeChildrenChanged);
    }

这里的dataWatches和childWatches类型是WatchManager,实现自IWatchManager接口。具体调用的是WatchManager.triggerWatch()函数

	/**
	* 版本:ZooKeeper 3.8.0
	* org.apache.zookeeper.server
	* DataTree.java
	* 117行
	*/
	@Override
    public WatcherOrBitSet triggerWatch(String path, EventType type) {
        return triggerWatch(path, type, null);
    }
    /**
	* 版本:ZooKeeper 3.8.0
	* org.apache.zookeeper.server
	* DataTree.java
	* 122行
	*/
	public WatcherOrBitSet triggerWatch(String path, EventType type, WatcherOrBitSet supress) {
		// WatchedEvent参数:事件类型,会话状态,节点
        WatchedEvent e = new WatchedEvent(type, KeeperState.SyncConnected, path);
        Set<Watcher> watchers = new HashSet<>();
        // 获取所有的parent
        PathParentIterator pathParentIterator = getPathParentIterator(path);
        synchronized (this) {
        	// 遍历所有上层的节点,将上层所有的watch加入局部变量watchers中
            for (String localPath : pathParentIterator.asIterable()) {
            	// 获取上层某路径下的所有watch
                Set<Watcher> thisWatchers = watchTable.get(localPath);
                if (thisWatchers == null || thisWatchers.isEmpty()) {
                    continue;
                }
                // 遍历该节点下的所有watch
                Iterator<Watcher> iterator = thisWatchers.iterator();
                while (iterator.hasNext()) {
                    Watcher watcher = iterator.next();
                    // 获取watch类型
                    // 这里的类型是:STANDARD(标准)、PERSISTENT(持久)、PERSISTENT_RECURSIVE(持久递归)
                    WatcherMode watcherMode = watcherModeManager.getWatcherMode(watcher, localPath);
                    // 如果是递归watch
                    if (watcherMode.isRecursive()) {
                        if (type != EventType.NodeChildrenChanged) {
                        	// 添加到watch
                            watchers.add(watcher);
                        }
                    // 不是递归watch,且
                    } else if (!pathParentIterator.atParentPath()) {
                    	// 将这个watch加入局部变量watchers中
                        watchers.add(watcher);
                        if (!watcherMode.isPersistent()) {
                        	// 如果不是持久watch,用这一次就要删除
                            iterator.remove();
                            Set<String> paths = watch2Paths.get(watcher);
                            if (paths != null) {
                                paths.remove(localPath);
                            }
                        }
                    }
                }
                // 某路径下的所有watch都删除了,应该在watchTable中删除这个路径
                if (thisWatchers.isEmpty()) {
                    watchTable.remove(localPath);
                }
            }
        }
        // 如果没有没有watch,返回
        if (watchers.isEmpty()) {// .....}
      	
        for (Watcher w : watchers) {
			// .....
			// 调用process方法向客户端发送通知
            w.process(e);
        }
		// .....
        return new WatcherOrBitSet(watchers);
    }
    

客户端回调逻辑

服务端watch事件触发后,需要客户端执行回调逻辑。客户端使用 SendThread.readResponse() 方法来统一处理服务端的相应。

	/**
	* 版本:ZooKeeper 3.8.0
	* org.apache.zookeeper
	* ZooKeeper.java
	* 874行
	*/
	void readResponse(ByteBuffer incomingBuffer) throws IOException {
			// 反序列化逻辑
			// ...
            replyHdr.deserialize(bbia, "header");
            // 根据Xid判断这个reply的请求响应类型
            // 相应类型有:ping response、通知类型、Auth包
            switch (replyHdr.getXid()) {
            case PING_XID:
			// ....
            case AUTHPACKET_XID:
            // ....
            case NOTIFICATION_XID:
                LOG.debug("Got notification session id: 0x{}",
                    Long.toHexString(sessionId));
                WatcherEvent event = new WatcherEvent();
                event.deserialize(bbia, "response");

                // convert from a server path to a client path
				// ......
                WatchedEvent we = new WatchedEvent(event);
                LOG.debug("Got {} for session id 0x{}", we, Long.toHexString(sessionId));
                // 将接收到的事件交给EventThread线程进行处理
                eventThread.queueEvent(we);
                return;
            default:
                break;
            }
		// .....
        }

可以看到,处理逻辑中将接收到的事件交给eventThread线程进行处理

	/**
	* 版本:ZooKeeper 3.8.0
	* org.apache.zookeeper
	* ClientCnxn.java
	* 503行
	*/
	private void queueEvent(WatchedEvent event, Set<Watcher> materializedWatchers) {
            if (event.getType() == EventType.None && sessionState == event.getState()) {
                return;
            }
            sessionState = event.getState();
            final Set<Watcher> watchers;
            if (materializedWatchers == null) {
                // watch的实现逻辑
                watchers = watchManager.materialize(event.getState(), event.getType(), event.getPath());
            } 
		// 其他逻辑
		// ...
        }

实现逻辑是调用了watchManager.materialize()函数

	/**
	* 版本:ZooKeeper 3.8.0
	* org.apache.zookeeper
	* ClientCnxn.java
	* 503行
	*/
	public Set<Watcher> materialize(
        Watcher.Event.KeeperState state,
        Watcher.Event.EventType type,
        String clientPath
    ) {
		final Set<Watcher> result = new HashSet<>();

        switch (type) {
	        case NodeDataChanged:
	        case NodeCreated:
		        synchronized (dataWatches) {
		                addTo(dataWatches.remove(clientPath), result);
		            }
		            synchronized (existWatches) {
		                addTo(existWatches.remove(clientPath), result);
		            }
		            addPersistentWatches(clientPath, result);
		            break;
	        }
	        case NodeChildrenChanged:// ...
	        case NodeDeleted://...
        }
        // 其他逻辑
		// .....
	}

将查询到的Watcher存储到waitingEvents队列中,调用EventThread类中的run方法会循环取出在waitingEvents队列中等待的Watcher事件进行处理。
处理的过程就是调用watcher接口的process()接口:

		/**
		* 版本:ZooKeeper 3.8.0
		* org.apache.zookeeper
		* ClientCnxn.java
		* 571行
		*/
		private void processEvent(Object event) {
            try {
                if (event instanceof WatcherSetEventPair) {
                    // each watcher will process the event
                    WatcherSetEventPair pair = (WatcherSetEventPair) event;
                    for (Watcher watcher : pair.watchers) {
                        try {
                        	// 调用process()接口
                            watcher.process(pair.event);
                        } catch (Throwable t) {
                            LOG.error("Error while calling watcher.", t);
                        }
                    }
                }
            }
            // .....
            //其他逻辑

使用场景

作为一个典型的观察者模式,根据之前的设计模式-观察者模式的内容,观察者模式可以分为推模式和拉模式,也就是说被观察者subject可以主动选择将所需数据传递给观察者,或者也可以将此变化通知给观察者,让观察者去自己取值。
ZooKeeper可以支持这两种形式的观察者模式。
例如,一个典型的配置管理功能,我们可以使用ZooKeeper做一个配置中心:将数据库、配置文件等各种信息存储在ZooKeeper的节点上,服务器集群的各个服务对该节点注册watch,客户端在得知信息发生改变后,去ZooKeeper上拉取最新信息。

参考

02 发布订阅模式:如何使用 Watch 机制实现分布式通知
ZooKeeper Programmer’s Guide
ZooKeeper-cli: the ZooKeeper command line interface
大数据理论与实践2 分布式协调服务Zookeeper
ZooKeeper Watch机制

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值