zookeeper 的 watch 机制源码分析学习

watcher监听机制是Zookeeper中非常重要的特性,我们基于zookeeper 上创建的节点,可以对这些节点绑定监听事件。比如可以监听节点数据的变更、节点删除、子节点状态变更等事件。通过这个事件机制,可以基于zookeeper实现分布式锁,集群管理等功能。

事件特性

当数据发生变化时,zookeeper 会产生一个watcher 事件,并且会发送到客户端,但是客户的只会收到一次通知。如果后续这个节点再次发生变化,那么之前设置 watcher 的客户端不会再次收到消息。即watcher是一次性的操作。可以通过循环监听去达到永久监听的效果

注册事件

通过这三个操作来绑定事件:

  • getData
  • exists
  • getChildren

凡是事务类型的操作,都会触发监听事件。(create/delete/setData)

事件类型

事件类型发生原因
None(-1)客户端连接状态发生变化的时候,会收到none的事件
NodeCreated(1)创建节点的事件。比如 create /demo
NodeDeleted(2)删除节点的事件 。比如 delete /demo
NodeDataChanaged(3)节点数据发生变更。
NodeChildrenChanged(4)子节点被创建、删除会触发事件

事件操作类型对应事件机制

操作类型/demo 监听事件 (exists/getData/getChild)/demo/chindren
create(/demo)NodeCreated (exists/getData)
delete(/demo)NodeDeleted (exists/getData)
setData(/demo)NodeDataChanaged (exists/getData)
create(/demo/children)NodeChildChangerend (getChild)NodeCreated
delete(/demo/children)NodeChildChangerend (getChild)NodeDeleted
setData(/demo/children)NodeDataChanaged

客户端进行绑定 watch 事件, 服务器怎么知道是哪个客户端的呢?

客户端 注册 watch 事件, 将事件放到 outgoingQueue 队列中,
SendThread 线程会从 outgoingQueue 队列中拿出数据包发送到zk server,
客户端传入数据到 zk server 上注册 watch 事件 :path : /path , watcher : true

服务器里存储了对应的 path 和 path 对应的 watch 事件

// path ,对应的客户端的网络处理类
HashMap<String,HashSet<Watcher>> watchTable;

客户端本地会记录所有的 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>>();

服务端记录了 每个 path 对应的 所有的 watch 事件的集合

  • 每个 watch 里存储的是客户端的 网络处理类。
  • 服务端可以通过 watch 的网络处理类来与客户端进行通信

客户端记录了每个 path 下的所有 watch 事件的集合

  • 每个 watch 里存储的是真实的 watcher 类

客户端通过传入一个 path 和 watch=true ,在服务端注册一个事件的监听 watchTable 并服务端返回 stat ,客户端接收到响应之后在本地注册事件 dataWatches ,记录了path 和 watch 实现类

当监听的 path 存在 create 、 delete 、 setData 操作时,服务端会通过记录的 watch (客户端的网络处理类)发送一个事件给客户端, 客户端收到之后通过 path 找到具体的 watch 处理类,调用 process 方法进行调用。

Watcher 事件源码分析

注册事件,发送数据到服务端

  1. 构造Zookeeper客户端,客户端注册Watcher 事件,并发送数据给服务端
//构造一个Zookeeper的方法,传入一个Watcher事件,
ZooKeeper zooKeeper = new ZooKeeper("192.168.45.131:2181", 4000, new Watcher() {
    @Override
    public void process(WatchedEvent event) {
        if(Event.KeeperState.SyncConnected==event.getState()){
            //如果是连接响应
            countDownLatch.countDown();
        }
    }
});
//绑定一个exists watcher事件
zooKeeper.exists("/demo", new Watcher() {
    @Override
    public void process(WatchedEvent event) {
        System.out.println("内部watcher:"+event.getType()+":"+event.getPath());
    }
});

源码的实现

public ZooKeeper(String connectString, int sessionTimeout, Watcher watcher,
            boolean canBeReadOnly) throws IOException {
    //将构造时传过来的watcher作为watcherManage的默认watcher
    watchManager.defaultWatcher = watcher;
    
    ConnectStringParser connectStringParser = new ConnectStringParser(
        connectString);
    HostProvider hostProvider = new StaticHostProvider(
        connectStringParser.getServerAddresses());
    //初始化ClientCnxn,并实例化SendThread 和 EventThread 两个内部类线程
    cnxn = new ClientCnxn(connectStringParser.getChrootPath(),
                          hostProvider, sessionTimeout, this, watchManager,
                          getClientCnxnSocket(), canBeReadOnly);
    //调用启动方法,启动sendThread.start();eventThread.start(); 两个线程
    cnxn.start();
}

至此客户端进行 zookeeper 的构造结束。

  1. 下面是 exists 的实现
public Stat exists(final String path, Watcher watcher) {
    final String clientPath = path;
    PathUtils.validatePath(clientPath);

    // the watch contains the un-chroot path
    WatchRegistration wcb = null;
    if (watcher != null) {
        //对 watcher 进行绑定注册
        wcb = new ExistsWatchRegistration(watcher, clientPath);
    }

    final String serverPath = prependChroot(clientPath);

   	//封装请求头,声明 请求类型是 exists
    RequestHeader h = new RequestHeader();
    h.setType(ZooDefs.OpCode.exists);
    //封装 exists 的请求信息,将 节点path 和是否存在watcher 进行封装
    ExistsRequest request = new ExistsRequest();
    request.setPath(serverPath);
    request.setWatch(watcher != null);
    // 设置服务端响应的接收类
    SetDataResponse response = new SetDataResponse();
    //将 requestHeader、existsRequest、setDataResponse、watchRegistration 提交到发送队列中去
    //此时的 cnxn 便是构造 zookeeper 时的 ClientCnxn 对象
    ReplyHeader r = cnxn.submitRequest(h, request, response, wcb);
    //返回exists 得到的结果信息 stat 信息
    return response.getStat().getCzxid() == -1 ? null : response.getStat();
}

进入到 ClientCnxn 的 submitRequest 方法中

public ReplyHeader submitRequest(RequestHeader h, Record request,
            Record response, WatchRegistration watchRegistration)
            throws InterruptedException {
    ReplyHeader r = new ReplyHeader();
    //将消息添加到队列,并构造一个Packet 传输对象
    //组装数据包,packet 是客户端和服务端进行通信的最小单元
    Packet packet = queuePacket(h, r, request, response, null, null, null,
                                null, watchRegistration);
    synchronized (packet) {
        while (!packet.finished) {
            //在数据包没有处理完成之前,一致阻塞
            packet.wait();
        }
    }
    return r;
}
Packet queuePacket(RequestHeader h, ReplyHeader r, Record request,
            Record response, AsyncCallback cb, String clientPath,
            String serverPath, Object ctx, WatchRegistration watchRegistration){
    Packet packet = null;

    // Note that we do not generate the Xid for the packet yet. It is
    // generated later at send-time, by an implementation of ClientCnxnSocket::doIO(),
    // where the packet is actually sent.
    synchronized (outgoingQueue) {
        // 将相关传输对象装换成Packet
        packet = new Packet(h, r, request, response, watchRegistration);
        packet.cb = cb;
        packet.ctx = ctx;
        packet.clientPath = clientPath;
        packet.serverPath = serverPath;
        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;
            }
            //添加到 队列中,
            // LinkedBlockingDeque是一个由链表结构组成的双向阻塞队列,
            //即可以从队列的两端插入和移除元素。
            // 双向队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。
            // LinkedList类是双向列表
            outgoingQueue.add(packet);
        }
    }
    //此处是多路复用机制,唤醒Selector ,告诉他有数据包添加过来了
    sendThread.getClientCnxnSocket().wakeupCnxn();
    return packet;
}
  1. SendThread 的发送过程

在初始化连接的时候,zookeeper初始化了两个线程并且启动了。接下来是分析SendThread 的发送过程,因为是一个线程,所以会调用run方法

   //SendThread#run 方法中,这个调用最为重要,
   //调用 clientCnxnSocket 发起传输,进入到ClientCnxnSocketNIO.doTransport 方法中
   //其中 pendingQueue 是一个用来存放已经发送、等待回应的 packet 队列
   //outgoingQueue 是 exists 中将事件通过封装成 Packet 添加到该队列中
   clientCnxnSocket.doTransport(to, pendingQueue, outgoingQueue, ClientCnxn.this);

总结:client 调用 exists 注册监听以后,会做几个事情:

  • 将请求数据封装为 packet ,添加到 outgoingQueue
  • SendThread 线程会执行数据发送操作,主要是将 outgoingQueue 队列中的数据发送到服务端
  • 通过ClientCnxnSocket.doTransport ,其中ClientCnxnSocket 只是zookeeper 客户端和服务端连接通信的封装。
  • 最后在ClientCnxnSocketNIO.doIO()方法中通过 sock.write(p.bb); 将请求的数据包发送到服务端

服务端接受请求处理流程

  1. 从服务端NettyServerCnxn类 ,开始分析

    服务端有个NettyServerCnxn 类,用来处理客户端发送过来的请求

      //服务端接受客户端发来的数据 NettyServerCnxn 
      public void receiveMessage(ChannelBuffer message) {
          try {
              while(message.readable() && !throttled) {
                  //byteBuffer 不为空
                  if (bb != null) {
                      //已经读完
                      if (bb.remaining() == 0) {
                         if (initialized) {
                             	// 处理客户端传过来的数据包
                              zks.processPacket(this, bb);
                          }
                      }
                  } 
              }
          } catch(IOException e) {
              LOG.warn("Closing connection to " + getRemoteSocketAddress(), e);
              close();
          }
      }
      //ZooKeeperServer 
      public void processPacket(ServerCnxn cnxn, ByteBuffer incomingBuffer)  {
          if (h.getType() == OpCode.auth) {
          } else {
              if (h.getType() == OpCode.sasl) {
              }else {
                  //不是权限也不是sasl,组装请求参数
                  Request si = new Request(cnxn, cnxn.getSessionId(), h.getXid(),
                                           h.getType(), incomingBuffer, cnxn.getAuthInfo());
                  si.setOwner(ServerCnxn.me);
                  //提交请求
                  submitRequest(si);
              }
          }
          cnxn.incrOutstandingRequests(h);
      }
      public void submitRequest(Request si) {
          try {
              touch(si.cnxn);
              boolean validpacket = Request.isValid(si.type);
              if (validpacket) {
                  //访问调用链中的方法
                  firstProcessor.processRequest(si);
                  if (si.cnxn != null) {
                      incInProcess();
                  }
              }
          } catch (MissingSessionException e) {
              if (LOG.isDebugEnabled()) {
                  LOG.debug("Dropping request: " + e.getMessage());
              }
          } catch (RequestProcessorException e) {
              LOG.error("Unable to process request:" + e.getMessage(), e);
          }
      }
      
  此处的firstProcessor.processRequest(si); 是一个调用链,查看源码 找到实例化的过程
      /**
       * 封装了三个调用链:
       *  PrepRequestProcessor(SyncRequestProcessor(FinalRequestProcessor()))
       */
      protected void setupRequestProcessors() {
         
          RequestProcessor finalProcessor = new FinalRequestProcessor(this);
          RequestProcessor syncProcessor = new SyncRequestProcessor(this,
          			finalProcessor);
          //此处和下面启动了两个线程,
          //所以在processRequest 方法执行完后,会用到线程异步去执行
          ((SyncRequestProcessor)syncProcessor).start();
          firstProcessor = new PrepRequestProcessor(this, syncProcessor);
          ((PrepRequestProcessor)firstProcessor).start();
      }
  所以了解了调用过程,接下来 `firstProcessor.processRequest(si);` 实际上是调用 PrepRequestProcessor.processRequest() 方法
      //PrepRequestProcessor
      public void processRequest(Request request) {
          // request.addRQRec(">prep="+zks.outstandingChanges.size());
          // 添加到阻塞队列里之后,会在 run 方法中进行处理
          submittedRequests.add(request);
      }
      public void run() {
          try {
              while (true) {
                  Request request = submittedRequests.take();
                  long traceMask = ZooTrace.CLIENT_REQUEST_TRACE_MASK;
                  if (request.type == OpCode.ping) {
                      traceMask = ZooTrace.CLIENT_PING_TRACE_MASK;
                  }
                  if (LOG.isTraceEnabled()) {
                      ZooTrace.logRequest(LOG, traceMask, 'P', request, "");
                  }
                  if (Request.requestOfDeath == request) {
                      break;
                  }
                 	//去处理请求,处理请求中会按照request.type 进行类处理
                  // 而watcher 此时的请求类型是在exists 方法中指定了 exists 类型
                  // 最终都会执行 nextProcessor.processRequest(request); 方法
                  pRequest(request);
              }
          } catch (RequestProcessorException e) {
              if (e.getCause() instanceof XidRolloverException) {
                  LOG.info(e.getCause().getMessage());
              }
              handleException(this.getName(), e);
          } catch (Exception e) {
              handleException(this.getName(), e);
          }
          LOG.info("PrepRequestProcessor exited loop!");
      }
      protected void pRequest(Request request) throws RequestProcessorException {
          //调用 SyncRequestProcessor.processRequest()
          nextProcessor.processRequest(request);
      }
  SyncRequestProcessor.processRequest() 方法
      public void processRequest(Request request) {
          // request.addRQRec(">sync");
          // 也是通过调用run 方法进行异步操作
          queuedRequests.add(request);
      }
      @Override
      public void run() {
          try {
              while (true) {
                  if (si != null) {
                  } else if (toFlush.isEmpty()) {
                      // optimization for read heavy workloads
                      // iff this is a read, and there are no pending
                      // flushes (writes), then just pass this to the next
                      // processor
                      if (nextProcessor != null) {
                         	//调用 FinalRequestProcessor.processRequest()
                          nextProcessor.processRequest(si);
                          if (nextProcessor instanceof Flushable) {
                              ((Flushable)nextProcessor).flush();
                          }
                      }
                      continue;
                  }
              }
          }
      } catch (Throwable t) {
      }
      }
  调用 FinalRequestProcessor.processRequest() 方法
      //调用 FinalRequestProcessor
      public void processRequest(Request request) {
          ServerCnxn cnxn = request.cnxn;
          try {
              switch (request.type) {
                  case OpCode.exists: {
                      lastOp = "EXIS";
                      // TODO we need to figure out the security requirement for this!
                      ExistsRequest existsRequest = new ExistsRequest();
                      ByteBufferInputStream.byteBuffer2Record(request.request,
                                                              existsRequest);
                      String path = existsRequest.getPath();
                      if (path.indexOf('\0') != -1) {
                          throw new KeeperException.BadArgumentsException();
                      }
                      //获得 节点信息 ,最后调用DataTree.statNode 将watch 事件进行存储
                      Stat stat = zks.getZKDatabase().statNode(path, existsRequest
                                                               .getWatch() ? cnxn : null);
                      rsp = new ExistsResponse(stat);+++
                      break;
      		} 
      	} catch (SessionMovedException e) { 
      	}
      }

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值