zk的watcher机制

zk的注册总体图

zk的watcher机制_第1张图片
客户端在向zk服务器注册Watcher的同时,会将Watcher对象存储在客户端的WatchManager中。
当zk服务端触发Watcher事件后,会向客户端发送通知,客户端线程从WatcherManager中取出对应的Watcher对象。来执行回调逻辑。

Watcher接口

Watcher是一个标准的事件处理器,其定义了事件通知相关的逻辑,包含KeeperState和EventState,分别代表通知状态和事件类型,在回调process中进行处理。

 public abstract void process(WatchedEvent event); 

WatchedEvent类

  public class WatchedEvent
  {
    //是一个枚举类,代表通知状态,是链接的,还是断开的。
    keeperState;
    //是一个枚举类,代表事件类型,当处于连接时,事件是子节点变化等事件
    eventType
    //节点路径
    path
  }

WatcherEvent类

 WatcherEvent
 {
   //代表事件类型
   type:int;
   //代表节点状态
   state:int;
   path:String
 }

总结:WatchedEvent和WatcherEvent表示的是同一个事物。WatchedEvent是一个逻辑事件,用于服务端和客户端程序执行过程中所需的逻辑对象。而WatcherEvent因为实现了序列化接口,因此,可以用于网络传输。
服务端在生成WatchedEvent事件之后,会调用getWrapper方法将自己包装成一个WatcherEvent,以便通过网络传输到客户端,客户端在接收到服务端的这个事件对象后,首先会将WatcherEvent事件还原成一个WatchedEvent事件,并传递给process方法进行处理。
从事件可以看到,客户端无法直接从该事件中获取到对应数据节点的原始数据内容以及变更后的新数据内容。

客户端注册Watcher

  1. 在创建一个zk对象实例时,可以在构造函数中传入一个默认的Watcher。默认的Watcher作为整个zk会话期间的默认Watcher,会一直保存在客户端的ZkWatchManager的defaultWatcher中。
  2. ZkWatchManager数据结构
public class  ZkWatchManager
{
  dataWatches:Map>
  existWatches: Map>
  childWatches: Map>
  defaultWatches: Watcher
}

举例:getData(String path,boolean watch,Stat stat)
getData(String path,Watcher watcher,Stat stat)
3. 在向getData接口注册Watcher后,客户端首先会对当前客户端请求request进行标记
request.setWatch(watcher != null);标记为使用watcher监听。

  1. 封装一个WatchRegistration对象,用于暂存数据节点的路径和Watcher的对应关系。
  2. 在zk中,通信的协议单元称为packet,用于客户端与服务端的网络传输,任何需要传输的对象都要包装成一个Packet,并且把WatchRegistration封装到packet中,以便请求成功后回调用。
packet queuePacket(RequestHeader h,ReplayHeader r,Record request,Record response,AsyncCallback cb,String clientPath,String serverPath,Object ctx,WatchRegistraction registration)
{
  Packet packet = null;
  ,,,
  synchronized(outgoingQueue)
  {
    packet = new Packet(h,r,request,response,watchRegistration);
    ,,,
    outgoingQueue.add(packet);
  }
}
  1. 发送报文
    zk客户端向服务端发送请求,同时等待请求的返回,把返回的结果注册到packet中的replayHeader r及record response中。
    当接收到服务器的响应后,在finishPacket方法中把watcher真正的注册到ZkWatcherManager中。

    private void finishPacket(Packet p)
    {
      if (p.watchRegistration != null)
      {
        p.watchRegistration.register(p.replayHeader.getErr());
      }
    }
    

    WatchRegistration对象的register方法:

  //根据返回的码注册到对应的set中
 public void register(int rc)
 {
   if (shouldAddWatch(rc)
   {
     Map> watches = getWatches(rc);
     ,,,
     watches.put(clientPath,watchers);
   }
 }

注:在底层网络传输过程中,并没有将WatchRegistration对象完全的序列化到底层字节数组中。
通过查看序列化源码:void createBB(),Packet的方法只会序列化:requestHeader和request,请求头和请求体。

服务端处理Watcher

  1. 在请求报文中判断是否需要注册。
 getDataRequest.getWatch?cnxn:null
 ...
 byte[] b = zks.getZkDatabase().getData(getDataRequest,getPath(),stat,getDataRequest.getWatch()?cnxn:null);
 rsp = new GetDataResponse(b,stat);
 break;
  1. ServerCnxn代表一个客户端和服务端的链接。
    是基于netty的实现;NettyServerCnxn,

  2. ServerCnxn实现了Watcher的process接口

  3. 数据节点的节点路径和serverCnxn最终会存储在WatchManager的watchTable和watch2Paths中。WatchManager是服务端的Watcher的管理者。WatchManager只是一个统称,在服务端,DataTree中会托管两个watchManager,分别是dataWatches和childWatches;
WatchManager
//watchTable是从数据节点的路径的粒度来托管Watcher。
--watchTable:HashMap>
//watch2Paths;是从Watcher的粒度来控制事件触发需要触发的数据节点。
--watch2Paths:HashMap>;
//定义对watcher的增删改查和触发watcher
triggerWatch();
addWatch();
removeWatch();
  1. zk服务端的数据存储
 DataNode n = nodes.get(path);
 setData(String path,byte[] data,int version,long zxid,long time)
 {
   DataNode n = nodes.get(path);
   synchronized(n);
   //触发节点通知
   dataWatches.triggerWatch(path,EventType,NodeDataChanged);
 }
 //触发watcher实现
 public set triggerWatch(String path,EventTye type ,Set process)
 {
    WatchedEvent e = new WatchedEvent(type,KeeperState,path);
    HashSet watches;
    synchronized(this)
    {
      watchers = watchTable.remove(path);
      //...
      for(Watcher w:watchers)
      {
        HashSet paths = watch2Paths..
        if (paths != null)
        {
          paths.remove(path);
        }
      }
     for(Watch w:watchers)
     {
       w.process(e);//进行处理
     }
    }
    return watchers;
 }

服务端处理watcher总结

  1. 封装WatchedEvent; 首先将通知状态(KeeperState),事件类型(EventType)以及节点路径(path)封装成WatchedEvent。
  2. 查询Watcher,根据数据节点的路径从watchTable中取出对应的watcher,如果没有,说明没有注册watcher。如果找到这个watcher,将其提取出来,同时会直接从watchTable和watch2Paths中将其删除。watcher在server端是一次性的。
  3. 调用process触发watcher。逐个调用触发;zk会把当前请求的ServerCnxn作为一个Watcher进行存储。
 public class NIOServerCnxn extends ServerCnxn
 {
   synchronized void process (WathedEvent event)
   {
     ReplayHeader h = new ReplayHeader(-1,-1l,0);
     ,,,
     watcherEvent e = event.getWrapper();
     sendResponse(h,e,"notification");
   }
 }
 在请求头中,标记为"-1",表面当前是一个通知。
 将watched包装成watcherEvent,以便进行网络传输。

客户端回调Watcher

通过使用ServerCnxn对应的TCP链接向客户端发送一个WatcherEvent事件。

 //SendThread接收事件通知
 class SendThread extends Thread
 {
   void readResponse(ByteBuffer inbuffer)
   {
     if (replayHder.getXid()== -1)
     {
       //-1 means notification
       //...
       组装:WatcherEvent
       WatcherEvent event = new WatherEvent();
       //设置path,KeeperState,EventType
     }
     WatchedEvent we = new WatchedEvent(event);
     eventThread.queueEvent(we);
     return;
   }
 }

处理步骤:
1. 反序列化;将子节点流转换成WatcherEvent对象
2. 处理chrootPath,如果customer设置了chootPath,需要对服务端传过来的完整的节点路径进行chrootPath处理。如,chrootPath=/app/,那么对应的响应节点为:/app/locks,经过处理后就会变成一个相对路径:/locks,
3. 还原WatchedEvent
4. 回调Watcher,将watchedEvent对象交给EventThread线程,在下一轮周期中进行watcher回调。
EventThread处理事件通知:

  EventThread
  --waitingEvents:LinkedBlockingQueue;
  ++queueEvent(WatchEvent);
  ++run()
  ++processEvent(Object);
public void queueEvent(WatchedEvent event)
{
  WatcherSetEventPair pair = new WatcherSetEventPair(watcher.materialize(event.getState(),eventType()),event.getPath())
  waitingEvents.add(pair);
} 
  

materialize的方法根据clientPath获取Set,客户端在识别出事件类型EventType后,会从相应的Watcher存储(dataWatches,existWatches或childWatches中)去除对应的Watcher。表明了客户端的Watcher机制也是一次性的触发失效的。WaitingEvents是一个待处理的watcher的队列,EventThread的run方法不断对该队列进行处理。

总结:Watcher的特性

1.一次性
2.客户端串行执行(注意:不要因为一个Watcher的处理逻辑影响了整个客户端的Watcher回调)。
3.轻量:WatchedEvent只会包含三个部分;通知状态,事件类型和节点路径。

你可能感兴趣的:(zookeeper)