Zookeeper 源码解读系列, 单机模式(四)

本文深入解析ZooKeeper单机模式下服务器的启动流程,包括配置参数解析、服务端启动步骤、数据加载及ZookeeperServer启动。重点介绍了请求处理器链的概念与运作逻辑,从PrepRequestProcessor到SyncRequestProcessor再到FinalRequestProcessor,详细分析了各处理器如何处理请求、事务持久化及内存更新过程。

前情提要

上一篇【Zookeeper 源码解读系列, 单机模式(三)】我们讲了单机模式下服务器的启动,配置参数的解析,ZooKeeperServerMain的启动,服务端启动的步骤,启动时加载数据,最后是ZookeeperServer的启动。由于篇幅太长,考虑到大家阅读起来比较困难,就把单机模式下最重要也是最不好理解的一篇拆分出来,单独做为一篇以降低读者阅读时的疲劳感。那么我们就接着上一次的话题继续讲ZookeeperServer启动结束以后,又做了什么事情。本篇也会被收录到【Zookeeper 源码解读系列目录】中。

请求处理器链的概念

现在我们先把代码放一边,先总结下到目前为止服务器都做了什么。刚才我们说到服务端启动的时候,在NIOServerCxnFactory.startup()的方法里启动了一个线程,就是NIOServerCxnFactory这个NIO的线程,这是第一件事情;第二件事情就是加载数据;然后又做了第三件事情,在ZookeeperServer.startup()方法里启动了一个session的跟踪器,这个也是一个线程,我们刚刚说国;最后就是setupRequestProcessors(),这里看名字是在设置请求处理器(RequestProcessors)。顾名思义就是要处理请求的,这里其实一个非常重要的点,为什么说很重要呢?

因为这几乎就是Zookeeper处理事务的核心逻辑,在讲解代码之前,需要先从概念上理清楚逻辑,否则直接看代码真的是一场噩梦。我们先进入setupRequestProcessors();看看这里又写了什么事情:

protected void setupRequestProcessors() {
   
   
    RequestProcessor finalProcessor = new FinalRequestProcessor(this);
    RequestProcessor syncProcessor = new SyncRequestProcessor(this, finalProcessor);
    ((SyncRequestProcessor)syncProcessor).start();
    firstProcessor = new PrepRequestProcessor(this, syncProcessor);
    //这里启动的是PrepRequestProcessor首个处理器
    ((PrepRequestProcessor)firstProcessor).start();
}

进去以后看到这里面有三个处理器Processor的类而且,全都new了出来,这些是什么呢?那么就要重点介绍一下ZooKeeper的请求处理器(RequestProcessors)的逻辑,这个逻辑和我们一般意义上的链表很像,所以权且把这个逻辑叫做处理器链。每当一个请求过来的时候,就会通过不同的请求处理器去处理不同的逻辑。在单机模式下有三种处理器(也是三个线程类):
PrepRequestProcessor、SyncRequestProcessor、FinalRequestProcessor,这些处理器分开来说,就是做下面这些任务的:
PrepRequestProcessor->做认证,生成txn事务对象,完成后转交给SyncRP
SyncRequestProcessor->事务对象持久化(生成事务文件),打快照,完成后转交给FinalRP
FinalRequestProcessor->做内存更改,处理事件响应啥的都是在这里弄的,返回客户端响应
所以整条链就是这样的:
PrepRequestProcessor.next=SyncRequestProcessor.next=FinalRequestProcessor
为什么可以这样写呢?因为这些处理器里面都有一个next的属性,指向下一下处理器。此处要注意记住一点:第一个请求firstProcessorNIOServerCnxn中被调用的,也就是说处理器链的实例化以及启动都是在服务端。

介绍完什么是处理器链的概念,我们现在说到这里,其实遗留了两大问题:

  1. NIOServerCxnFactory这个线程做了什么事情?
  2. 我们根据NIO的思想从socket-channel取出数据以后,谁去做了处理?

根据我们之前的分析,NIO是处理Socket连接的,所以问题1取数据应当是NIOServerCxnFactory做的。而问题2呢,当然应该是我们的处理器链做的。那么后面我们看下代码里面是不是和我们的猜想一样。

NIOServerCnxnFactory线程

如刚才所说先去看run()方法,其实从道理上讲,NIOServerCnxnFactory的逻辑应该和我们客户端NIOClientCnxn的逻辑应该是镜像相反的。所以这里面也应该有读和写,那让我们看下代码:

public void run() {
   
   
  while (!ss.socket().isClosed()) {
   
   
    try {
   
   
      /**构建选择器,从channel里面拿出数据,封装为selectedList,略过**/
      for (SelectionKey k : selectedList) {
   
   
          //如果取到的是一个connection的请求,则建立连接
          if ((k.readyOps() & SelectionKey.OP_ACCEPT) != 0) {
   
   
              SocketChannel sc = ((ServerSocketChannel) k .channel()).accept();
              InetAddress ia = sc.socket().getInetAddress();
              int cnxncount = getClientCnxnCount(ia);
              if (maxClientCnxns > 0 && cnxncount >= maxClientCnxns){
   
   
                  LOG.warn("Too many connections from " + ia + " - max is " + maxClientCnxns );
                  sc.close();
              } else {
   
   
                  LOG.info("Accepted socket connection from "
                           + sc.socket().getRemoteSocketAddress());
                  sc.configureBlocking(false);
                  SelectionKey sk = sc.register(selector, SelectionKey.OP_READ);
                  NIOServerCnxn cnxn = createConnection(sc, sk);//建立连接
                  sk.attach(cnxn);
                  addCnxn(cnxn);
              }
          } else if ((k.readyOps() & (SelectionKey.OP_READ | SelectionKey.OP_WRITE)) != 0) {
   
   
              //如果不是建立连接的,就是一些写或者读的数据
              NIOServerCnxn c = (NIOServerCnxn) k.attachment();
              c.doIO(k);//和客户端一样开始doIO
          } else {
   
   
              if (LOG.isDebugEnabled()) {
   
   
                  LOG.debug("Unexpected ops in select " + k.readyOps());
              }
          }
      }
      selected.clear();
    } catch (***Exception e) {
   
   
    	/**异常略**/
    }
  }
  closeAll();
  LOG.info("NIOServerCnxn factory exited run method");
}

我们略过选择器的部分,到for (SelectionKey k : selectedList)循环这里,碰见了第一个判断条件if ((k.readyOps() & SelectionKey.OP_ACCEPT) != 0),这里的连接是和channel连接,所以是OP_ACCEPT字段,所以说取到的是一个Connection的请求,那么就会走进去,做一些绑定地址,校验参数之类的工作,然后建立连接NIOServerCnxn cnxn = createConnection(sc, sk);,这里没什么好说的。我们走到else if ((k.readyOps() & (SelectionKey.OP_READ | SelectionKey.OP_WRITE)) != 0) 里面,如果不是建立连接的,就是一些写或者读的数据,当然也会间歇性的收到客户端的ping。那么我们是不是找到了一个很熟悉的方法c.doIO(k);呢,所以我们就着重去里面看:

void doIO(SelectionKey k) throws InterruptedException {
   
   
    try {
   
   
		/**验证socket没有打开,return;,略过**/
        if (k.isReadable()) {
   
   //读取准备完毕
            int rc = sock.read(incomingBuffer);
            if (rc < 0) {
   
   //剩下的数据小于0,报错
            	/**抛出EndOfStreamException数据异常**/
            }
            if (incomingBuffer.remaining() == 0) {
   
   
                boolean isPayload;
                if (incomingBuffer == lenBuffer) {
   
   
                    incomingBuffer.flip();
                    isPayload = readLength(k);
                    incomingBuffer.clear();
                } else {
   
   
                    isPayload = true;
                }
                if (isPayload) {
   
   
                    readPayload();//正式加载数据
                }
                else {
   
    return; }
            }
        }
        if (k.isWritable()) {
   
   
        	/**写就绪,略过**/
        }
    } catch (***Exception e) {
   
   
        /**各种异常的逻辑,略过**/
    }
}

我们进入以后看到如果k.isReadable()都就绪了,那么就读取客户端发来的数据,这里没什么可说的,如果一切都做好了那么就开始readPayload();正式加载数据了:

private void readPayload() throws IOException, InterruptedException {
   
   
    if (incomingBuffer.remaining() != 0) {
   
   //是否还有遗留的数据
        int rc = sock.read(incomingBuffer);//接着读剩下的数据
        if (rc < 0) {
   
   //剩下的数据小于0,报错
           /**抛出EndOfStreamException数据异常**/
        }
    }
    if (incomingBuffer.remaining() == 0) {
   
    //如果全部读完了
        packetReceived();//计数的方法
        incomingBuffer.flip();
        if (!initialized) {
   
   //初始化Y/N
            readConnectRequest();//N:重新连接
        } else {
   
   
            readRequest(); //Y:开始读取请求
        }
        lenBuffer.clear();
        incomingBuffer = lenBuffer;
    }
}

进入以后又开始判断是不是还有遗留的数据if (incomingBuffer.remaining() != 0) ,如果有那就继续把剩余的sock.read(incomingBuffer)数据都读出来,如果if (incomingBuffer.remaining() == 0)如果全部读完了,就可以请求了,packetReceived();这是个计数器没什么用略过,继续判断if (!initialized)有没有初始化成功,如果没有重新连接readConnectRequest();,如果已经就绪开始读取请求readRequest();,所以我们还得去readRequest();里面看内容:

    private void readRequest() throws IOException {
   
   
        zkServer.processPacket(this, incomingBuffer);
    }

里面只有一句话,那我们还得接着往里面走,但是要说明一点:到了这里服务器实例已经开始处理收到的packet了,incomingBuffer就是socket里面的数据,下面进入processPacket(this, incomingBuffer);

public void processPacket(ServerCnxn cnxn, ByteBuffer incomingBuffer) throws IOException {
   
   
    InputStream bais = new ByteBufferInputStream(incomingBuffer);
    BinaryInputArchive bia = BinaryInputArchive.getArchive(bais);
    RequestHeader h = new RequestHeader();//构造请求头
    h.deserialize(bia, "header");
    incomingBuffer = incomingBuffer.slice();
    //根据header的不同类型进行处理
    if (h.getType() == OpCode.auth) {
   
   //这个auth其实就是addauth命令
        /**这里我们先不看,后面ACL会仔细讲解**/
    } else {
   
   //不是auth
        if (h.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值