前言
在之前的博客中,我们已经基本上把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:
这个switch
的case
,这里就有区别了,我们知道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
这个队列就是存档那些还没有处理的节点的队列。其实在create
、set
或者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
无关。