Zookeeper中的Watcher机制到底是啥?

1,761 阅读6分钟

啥是watcher机制

Zookeeper的watcher机制是其一个非常核心的机制,zookeeper提供的发布/订阅,监听节点变化(如节点的删除,内容的变化,或者子节点状态的变化)等核心功能都是基于watcher机制来是是实现的

而watcher机制的实现其实说白了就是一个观察者模式,只不过这个模式是分布式的,而不是单机的。

watcher机制涉及状态的监听,而这一状态其实包含了两种状态:客户端与服务端之间的连接状态(通知状态)以及节点的状态(事件类型)

通知状态(KeeperState)

KeeperState描述的其实就是客户端和服务端之间的连接状态发生变化时的一些通知类型,在ZK的Java客户端中,有一个枚举专门保存着这些状态:org.apache.zookeeper.Watcher.Event.KeeperState

一些主要属性如下:

枚举属性名 描述
DisConnected 很好理解,就是CS处于未连接的状态
SyncConnected 正常连接状态
Expired 会话超时。客户端和服务端连接时,服务端会为其分配一个占有时间,如果到期了还没有进行”续约“,那么就会进行Expired状态
NoSyncConnected 属性超时
AuthFailed 身份认证失败,服务端拒绝连接
ConnectedReadOnly 这个是zookeeper3.3版本开始才提供的模式。如果客户端设置了允许ReadOnly的话,那么当集群中有过半机器出现异常的时候,按照以往的做法,是整个服务直接无法对外提供;而如果设置了ReadOnly的话,就算出现了过半异常,客户端还可以做只读

事件类型(EventType)

这个用的其实是更多的,因为它描述的是Znode节点的状态变更,来完成一些发布/订阅等功能。同样的,在Java客户端中,也有一个枚举与其对应:org.apache.zookeeper.Watcher.Event.EventType

枚举属性名 描述
NodeCreated Watcher监听的节点被创建
NodeDeleted Watcher监听的节点被删除
NodeDataChanged Watcher监听的节点值被修改
NodeChildrenChanged Watcher监听的节点的子节点状态变化

EventType注册与通知之客户端实现

其实本质上就是三个HashMap,直接上代码就能看得很清楚

//这些Map的key都是节点路径,类似于/a,/b;而value则是该节点对应的所有watch

//节点内容监听
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>>();

EventType注册与通知之服务端实现

同样的,也是有两个map与之相关,直接贴代码

/*
* 这里的key代表节点路径,value代表客户端连接的集合
* 作用:当节点发生变动时,直接获取到所有监听该节点的watcher,然后进行逐个通知
*/
private final HashMap<String, HashSet<Watcher>> watchTable =
    new HashMap<String, HashSet<Watcher>>();

/**
* key代表一个客户端watcher,而value代表其监听的所有节点路径
* 为啥要这样弄勒?很简单,当一个客户端断开了连接,那么与之相关的一些监听就也要去掉,也就是从第一个map中对应路径的set中去掉这个watcher
* 所以这个map就是能快速定位到该watcher对应的所有path
*/
private final HashMap<Watcher, HashSet<String>> watch2Paths =
    new HashMap<Watcher, HashSet<String>>();

另外需要注意的是,服务端这里的watcher并不是代码里面的那个Watcher,而只是一个实现了Watcher接口的ServerCnxn抽象类,

EventType注册与通知流程

当客户端调用对应的api(如getData等)发起注册事件后,在客户端会将该请求封装成一个Packet对象,然后加入outGoingQueue队列中等待发送。注意的是,此时还没有将对应的worker维护进HashMap里面,这一步需要等服务都安回调之后再进行

当服务端收到该对象之后,就会将watcher进行注册,也就是更新一下那两个HashMap,然后响应给客户端。

客户端接收到响应后,就会将对应的watcher注册到自己的HashMap上。这样,注册就完成了

而通知过程也很简单,服务端对对应节点进行修改,然后去掉自己的watchTable中找到所有的watcher,对它们进行一一通知,而客户端的watcher的process函数则会被调用,完成一个通知的机制

需要注意的是,zookeeper中的事件通知是一次性的,也就是说,当服务端进行一次通知之后,就会把该watcher删除掉(主要是处于性能考虑)

zookeeper中Watcher机制的特性

  • 一次性:无论是客户端还是服务端,watcher一旦被触发,那么它就会被zk删除,所以如果希望一直监听的话,在每次回调之后还需要手动添加
  • 顺序性。客户端将收到的请求封装为Packet对象后,将其加入一个交outgoingQueue的FIFO队列中,按照先来先服务的顺序进行发送,从而保证了顺序性
  • 轻量:对于服务端来说,它通知客户端,并不通知事件的内容(比如具体节点内容进行了怎么样修改),它只告诉客户端,发生了事件;而对于客户端来说,它也并不会把整个watcher对象传过去,只使用一个boolean来进行是否需要监听嘛

简单代码示例

Talk is cheap嘛~

import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.zookeeper.CreateMode;
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.ZooDefs.Ids;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;

/**
 * Zookeeper Wathcher 
 * 本类就是一个Watcher类(实现了org.apache.zookeeper.Watcher类)
 * @author(alienware)
 * @since 2015-6-14
 */
public class ZooKeeperWatcher implements Watcher {
    /** 定义session失效时间 */
    private static final int SESSION_TIMEOUT = 5000;
    /** zookeeper服务器地址 */
    private static final String CONNECTION_ADDR = "ip1:port1,ip2:port2,ip3:port3";
    /** zk父路径设置 */
    private static final String PARENT_PATH = "/a";
    /** zk子路径设置 */
    private static final String CHILDREN_PATH = "/b/c";
    /** zk变量 */
    private ZooKeeper zk = null;

    /**
     * 创建ZK连接
     * @param connectAddr ZK服务器地址列表
     * @param sessionTimeout Session超时时间
     */
    public void createConnection(String connectAddr, int sessionTimeout) {
        this.releaseConnection();
        try {
            //this表示把当前对象进行传递到其中去(也就是在主函数里实例化的new ZooKeeperWatcher()实例对象)
            zk = new ZooKeeper(connectAddr, sessionTimeout, this);
            System.out.println(LOG_PREFIX_OF_MAIN + "开始连接ZK服务器");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 关闭ZK连接
     */
    public void releaseConnection() {
        if (this.zk != null) {
            try {
                this.zk.close();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    
    /**
     * 收到来自Server的Watcher通知后的处理。
     */
    @Override
    public void process(WatchedEvent event) {
        
        System.out.println("进入process方法");
        
        if (event == null) {
            return;
        }
        
        // 获取连接状态
        KeeperState keeperState = event.getState();
        // 事件类型
        EventType eventType = event.getType();
        // 受影响的path
        String path = event.getPath();
        System.out.println("连接状态:\t" + keeperState.toString());
        System.out.println("事件类型:\t" + eventType.toString());

        if (KeeperState.SyncConnected == keeperState) {
            // 成功连接上ZK服务器
            if (EventType.None == eventType) {
                System.out.println( "成功连接上ZK服务器");
                connectedSemaphore.countDown();
            } 
            //创建节点
            else if (EventType.NodeCreated == eventType) {
                System.out.println("节点创建");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } 
            //更新节点
            else if (EventType.NodeDataChanged == eventType) {
                System.out.println("节点数据更新");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } 
            //更新子节点
            else if (EventType.NodeChildrenChanged == eventType) {
                System.out.println("子节点变更");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } 
            //删除节点
            else if (EventType.NodeDeleted == eventType) {
                System.out.println("节点 " + path + " 被删除");
            }
            else ;
        } 
        else if (KeeperState.Disconnected == keeperState) {
            System.out.println("与ZK服务器断开连接");
        } 
        else if (KeeperState.AuthFailed == keeperState) {
            System.out.println("权限检查失败");
        } 
        else if (KeeperState.Expired == keeperState) {
            System.out.println("会话失效");
        }

    }

}