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

前言

在之前的博客中,我们已经基本上把Zookeeper客户端和服务端怎样通信,怎样处理事务,以及各自收到请求后所走的流程介绍完了。到目前为止,单机模式下就只剩下了两个小点,一个是Close Session,另一个是ACL机制。这两个小点虽然放到了单机模式的章节里,但是基本上单机模式下和集群模式下是通用的。笔者在这里先讲解清楚,也可以减少集群模式下讲解代码的内容量。本节我们就先讲解第一个小点:Close Session与临时节点。本篇也会被收录到【Zookeeper 源码解读系列目录】中。

客户端Close Session

要讲Close Session,我们也是需要通过一个命令作为例子讲解的,那就是quit命令。当我们输入quit时,也是通过ZooKeeperMain中的main()方法进入main.run()

void run() throws KeeperException, IOException, InterruptedException {
    if (cl.getCommand() == null) {
        /**略**/
    } else {
        processCmd(cl);
    }
}

过滤掉冗余代码以后直接进入processCmd(cl);

protected boolean processCmd(MyCommandOptions co)
    throws KeeperException, IOException, InterruptedException
{
    try {
        return processZKCmd(co);  //返回执行命令
    } catch (***Exception e) {
    	/**略**/
    }
    return false;
}

接着进入processZKCmd(co);方法找到quit的判断语句if (cmd.equals("quit")),这里就是处理quit命令时候客户端第一步的逻辑入口了:

protected boolean processZKCmd(MyCommandOptions co)
    throws KeeperException, IOException, InterruptedException
{
    /**略**/
    if (cmd.equals("quit")) { //进入客户端quit命令部分
        System.out.println("Quitting...");
        zk.close();   //zk调用了close()
        System.exit(0);
    } else if (cmd.equals("****")) {
        /**略**/
    } 
    /**略**/
}

到了这个分支里,只有ZooKeeper类对象调用了close()方法,然后就直接调用系统的exit(0);方法退出了程序。那么我们就只有close()这一个线索可追踪了,进入zk.close();

public synchronized void close() throws InterruptedException {
    if (!cnxn.getState().isAlive()) {//客户端验证存活
        /**打印Log**/
        return;
    }
    /**打印Log**/
    try {
        cnxn.close();//cnxn调用了close
    } catch (IOException e) {
        /**打印Log**/
    }
    LOG.info("Session: 0x" + Long.toHexString(getSessionId()) + " closed");
}

可以看到首先这个方法验证了一下客户端的状态是不是还活着isAlive()?如果不是存活的,就直接返回出去,客户端直接关闭了。如果还是活着的下面用cnxn调用了close()方法。我们再次回顾一下cnxn,这个对象在客户端是ClientCnxn的类对象,其实现类是NIOClientCnxn,也是通过这个封装的socket。那么我们接着进入cnxn.close();

public void close() throws IOException {
    /**打印Log**/
    try {
        RequestHeader h = new RequestHeader();
        h.setType(ZooDefs.OpCode.closeSession);  //设置操作类型是closeSession
        submitRequest(h, null, null, null); //提交
    } catch (InterruptedException e) {
        // ignore, close the send/event threads
    } finally {
        disconnect();
    }
}

进入以后,首先还是先构造一个请求头RequestHeader,然后设置h.setType(ZooDefs.OpCode.closeSession);操作类型是closeSession,之后就调用submitRequest(h, null, null, null);提交我们这个请求。最后走到finally块里调用disconnect();关闭连接。我们再回忆一下我们之前说的submitRequest(****);做了什么,首先包装请求request成为一个packet,然后把packet加入到outgoingQueue这个队列里,最后在这里等待服务端返回的结果。submitRequest(****);这个方法很多地方都使用了,请大家牢牢记住这个流程,本篇以后再碰到默认大家已经知道这一知识点了。此时这个请求已经交给了服务器端,那么我们就去服务器端查看,服务端收到quit命令后是怎么处理的。

服务端Close Session

看过我之前博客的同学一定都知道处理器链这个逻辑,而且介绍set命令的时候就已经带着大家过了一遍服务端接收请求的流程了,所以笔者这里就不再啰嗦了,我们直接找到服务端请求处理链的第一个PrepRequestProcessor.run()

public void run() {
    try {
        while (true) {
            Request request = submittedRequests.take(); //取出请求,后面开始处理
            /**略**/
            pRequest(request);//进入处理
        }
    } catch (***Exception e) {
    	/**略**/
    }
    LOG.info("PrepRequestProcessor exited loop!");
}

这里直接找到并进入pRequest(request);这个处理方法里面,然后定位到case OpCode.closeSession:这个switch分支中:

protected void pRequest(Request request) throws RequestProcessorException {
    request.hdr = null;
    request.txn = null;
    
    try {
        switch (request.type) {
        case OpCode.***:
            /**略**/
            break;
        case OpCode.closeSession:
            pRequest2Txn(request.type, zks.getNextZxid(), request, null, true);
            break;
        /**略**/
        }
    } catch (***Exception e) {
    	/**略**/
    }
    request.zxid = zks.getZxid();
    nextProcessor.processRequest(request);
}

在这里我们看到closeSession这里调用了pRequest2Txn(request.type, zks.getNextZxid(), request, null, true);,当目前为止都和其他命令没有什么本质的区别,接着进入这个pRequest2Txn(***)方法:

protected void pRequest2Txn(int type, long zxid, Request request, Record record, boolean deserialize)
    throws KeeperException, IOException, RequestProcessorException
{
    request.hdr = new TxnHeader(request.sessionId, request.cxid, zxid, Time.currentWallTime(), type);
    switch (type) {
        case OpCode.***:
            /**略**/
            break;
        case OpCode.closeSession:
            HashSet<String> es = zks.getZKDatabase()
                    .getEphemerals(request.sessionId);//拿到临时节点的名字
            synchronized (zks.outstandingChanges) {
                for (ChangeRecord c : zks.outstandingChanges) {
                    if (c.stat == null) {
                        es.remove(c.path); //删除状态不正确的节点
                    } else if (c.stat.getEphemeralOwner() == request.sessionId) {
                        es.add(c.path);
                    }
                }
                for (String path2Delete : es) {//处理临时节点
                    addChangeRecord(new ChangeRecord(request.hdr.getZxid(),
                            path2Delete, null, 0, null)); //添加changeRecord到outstandingChanges
                }
                zks.sessionTracker.setSessionClosing(request.sessionId);
            }
            /**打印Log**/
            break;
            /**略**/
        default:
            LOG.error("Invalid OpCode: {} received by PrepRequestProcessor", type);
    }
}

临时节点的删除

进入以后找到OpCode.closeSession:这个switchcase,这里就有区别了,我们知道Session和临时节点是有很大联系的,因为临时节点是会随着Session的关闭而消失的,所以既然要退出,就一定绕不开临时节点的处理。那么我们就看下关闭Session的时候是怎么处理临时节点的。要关闭临时节点首先肯定是要拿到临时节点的名字,并且存到一个HashSet里面es = zks.getZKDatabase().getEphemerals(request.sessionId);。要记住这个放临时节点的set,我们知道临时节点是和session id绑定的,我们看下这里是怎么绑定的:

public HashSet<String> getEphemerals(long sessionId) {
    return dataTree.getEphemerals(sessionId);
}

大家看这里就是把sessionId给传进DataTree这个类的getEphemerals(id),这个里面肯定是创建了一个set,然后把ephemerals里面的值放进去,有兴趣的同学可以点进去看,这里不细讲,所以这里就是要拿到当前id的所有临时节点(node)的名字。

我们接着pRequest2Txn(***)这个方法往下走,发现有个队列outstandingChanges被锁住了,并且在第二个for循环里es被使用了,那么outstandingChanges是个什么东西呢?要弄清楚这个的作用,得先去看下这个队列是哪里来的。通过搜索,我们能够找到zks.outstandingChanges.add(c);这个添加的语句,跳入这一行,进入了一个叫做addChangeRecord(ChangeRecord c)的方法:

void addChangeRecord(ChangeRecord c) {
    synchronized (zks.outstandingChanges) {
        zks.outstandingChanges.add(c);
        zks.outstandingChangesForPath.put(c.path, c);
    }
}

这个方法的名字有没有点熟悉呢?注意二个for循环里有一个addChangeRecord(new ChangeRecord(request.hdr.getZxid(), path2Delete, null, 0, null));方法,这不就是这个方法吗?这里的逻辑其实这样的:当我们有临时节点的时候,就会循环es加上一个节点的改变记录。如果没有临时节点,es这个存放临时节点的set就是null,这里的for循环就会跳过不执行里面的内容。当Session Close时候,没有临时节点就不需要删除,直接关闭就好了。所以可以知道outstandingChanges这个队列就是存档那些还没有处理的节点的队列。其实在createset或者delete命令中我们可以看到PrepRequestProcessor都会先创建一个改变记录,而后面的处理器(FinalProcessor)会用到这个队列。做完这一系列的动作以后,基本上要删除的临时节点的名字都已经有了。于是我们又到了SyncRequestProcessor,这里做了事务打快照等等逻辑,我们略过直接到FinalRequestProcessor.processRequest(request)

public void processRequest(Request request) {
    /**略**/
    synchronized (zks.outstandingChanges) { 
        /**略**/
        if (request.hdr != null) {
           /**略**/
           rc = zks.processTxn(hdr, txn);
        }
        /**略**/
    }
    /**略**/
    boolean closeSession = false;//closeSession的标记
    try {
        /**略**/
        switch (request.type) {
        	/**略,一会儿还要讲**/
        }
    } catch (***Exception e) {
    	/**略**/
    }
    /**略**/
    try {
        cnxn.sendResponse(hdr, rsp, "response");
        if (closeSession) {
            cnxn.sendCloseSession();//flag为ture给服务端发去关闭的返回
        }
    } catch (IOException e) {
        LOG.error("FIXMSG",e);
    }
}

又到了rc = zks.processTxn(hdr, txn)这个方法里:

public ProcessTxnResult processTxn(TxnHeader hdr, Record txn) {
    /**略**/
    rc = getZKDatabase().processTxn(hdr, txn);//走进这里来
    /**略**/
    return rc;
}

发现执行事务rc = getZKDatabase().processTxn(hdr, txn)

public ProcessTxnResult processTxn(TxnHeader hdr, Record txn) {
    return dataTree.processTxn(hdr, txn);
}

那我们进入后还是走到了dataTree.processTxn(hdr, txn); 继续进入:

public ProcessTxnResult processTxn(TxnHeader header, Record txn)
{
    ProcessTxnResult rc = new ProcessTxnResult();
    try {
        /**略**/
        switch (header.getType()) {
            case OpCode.***: 
                /**略**/
                break;
            case OpCode.closeSession://来到close分支
                killSession(header.getClientId(), header.getZxid());
                break;
        }
    } catch (KeeperException e) { 
    	/**略**/
    }
    /**略**/
    return rc;
}

进入后继续找到switch,然后找到OpCode.closeSession:这个分支,里面也是只有一个方法接着走进去killSession(header.getClientId(), header.getZxid());

void killSession(long session, long zxid) {
    HashSet<String> list = ephemerals.remove(session);//在这里把临时节点根据session一起拿出来
    if (list != null) {
        for (String path : list) {
            try {
                deleteNode(path, zxid);//拿出来以后,删除
                /**打印Log**/
            } catch (NoNodeException e) {
                /**打印Log**/
            }
        }
    }
}

进入killSession方法以后的第一件事情就是拿出临时节点,ephemerals这个参数就是之前在getEphemerals(id)这个方法里面赋值的时候使用的那个参数。临时节点在这里根据session被全部拿出来之后,调用deleteNode(path, zxid); 删除临时节点,这里就是删除的内存里的数据。

返回客户端消息

删除节点以后,服务端要把这个信息给到客户端,所以我们一层一层的跳出去processRequest(request)这个方法里面,这个是在FinalRequestProcessor里面,我们接着说:

public void processRequest(Request request) {
    /**略**/
    synchronized (zks.outstandingChanges) { 
        /**略**/
        if (request.hdr != null) {
           /**略**/
           rc = zks.processTxn(hdr, txn); //跳出以后往下走
        }
        /**略**/
    }
    /**略**/
    boolean closeSession = false;//closeSession的标记
    try {
        /**略**/
        switch (request.type) {
            case OpCode.closeSession: {
                lastOp = "CLOS";
                closeSession = true;//这里把flag置为true
                err = Code.get(rc.err);
                break;
            }
        }
    } catch (***Exception e) {
    	/**略**/
    }
    /**略**/
    try {
        cnxn.sendResponse(hdr, rsp, "response");
        if (closeSession) { //判断标记
            cnxn.sendCloseSession();//flag为ture给服务端发去关闭的返回
        }
    } catch (IOException e) {
        LOG.error("FIXMSG",e);
    }
}

回来以后,往下走找到一个flag标记boolean closeSession = false;,这里其实可以预测一下,一定有一个地方重置这个flag=true接着往下看switch,找到关闭Session的分支,果然我们就看到了closeSession = true;这里把标记重置。离开switch语句再往下,就找到了if (closeSession)这个判断,最终调用cnxn.sendCloseSession();关闭session并给客户端发去关闭的响应,那么到此Close Session这一个小的知识点的流程就完全结束了。

总结

Zookeeper里quit命令对应的操作叫做closeSession,其对应的这一步操作也需要到SyncProcessor里做持久化,但是这个持久化做的是删除节点的那一步,然后到FinalProcessor那里更新内存删除临时节点,这就是整个close session发生的事情。为什么临时节点要做删除的持久化?因为在创建的时候肯定做了持久化,所以关闭session删除临时节点就要做个对冲,把持久化冲掉,从这一点看临时节点和普通的节点没什么区别,唯一的区别在于临时节点和session绑定,做到session死亡临时节点自动删除,而普通的节点不绑定session,是否删除和session无关。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值