zk客户端与服务端成功完成连接后,就建立了一个会话,zk会话在整个运行期间在不同的会话状态进行切换,这些状态有CONNECTING、CONNECTED、RECONNECTING、RECONNECTED和CLOSE等。客户端开始创建zk对象时,客户端状态就会变成CONNECTING,直到连接成功后,将客户端状态变更为CONNECTED。通常,伴随着网络闪断或者其他原因,客户端与服务器之间的连接会出现断开情况,一旦碰到这种情况,zk客户端会自动进行重连操作,同时将客户端状态变更为CONNECTING,直到重新连接上ZK服务器后,客户端状态又变更为CONNECTED,通常情况下,zk运行期间客户端的状态总是介于CONNECTING和CONNECTED两者之一。 如果是客户端主动退出或者权限检查失败、会话超时等,客户端的状态就会直接变更为CLOSE。
会话创建
Session是zk中的会话实体,代表了一个客户端会话,它包含了4个基本属性:
- sessionId: 用来唯一标识一个会话,每次客户端创建新会话的时候,zk都会为其分配一个全局唯一的sessionId。
- TimeOut: 会话超时时间,客户端在构造zk实例的时候,会配置一个sessionTimeout参数用于指定会话的超时时间,Zookeeper客户端向服务器发送这个超时时间后,服务器会根据自己的超时时间限制最终确定会话的超时时间。
- TickTime: 下次会话的超时时间点。
- isClosing: 该属性用于标记一个会话是否已经被关闭。通常当服务器检测到一个会话已经超时失效的时候,会将该会话的isClosing属性标记为”已关闭“,这样就能确保不再处理来自该会话的新请求了。
public static class SessionImpl implements Session {
SessionImpl(long sessionId, int timeout, long expireTime) {
this.sessionId = sessionId;
this.timeout = timeout;
this.tickTime = expireTime;
isClosing = false;
}
final long sessionId;//会话id,全局唯一
final int timeout;//会话超时时间
long tickTime;//下次会话的超时时间点,会不断刷新
boolean isClosing;//是否被关闭,如果关闭则不再处理该会话的新请求
Object owner;
public long getSessionId() { return sessionId; }
public int getTimeout() { return timeout; }
public boolean isClosing() { return isClosing; }
}
sessionId唯一性的保证
public static long initializeNextSession(long id) {
long nextSid = 0;
nextSid = (System.currentTimeMillis() << 24) >>> 8; // >>>无符号右移,防止最高位是1, 会有干扰
nextSid = nextSid | (id <<56);
return nextSid;
}
生成sessionId步骤:
-
获取当前时间的毫秒表示:1380895182327位二进制表示为:
-
将步骤1中的数值左移24位,得到:
-
右移8位:
-
添加机器标识: SID. id 表示配置在myid文件中的值,通常是整数1、2、3等,假设id为2:
-
将步骤3和步骤4得到的两个64位表示的数值进行|操作:
通过以上5步,就完成了一个sessionId的初始化,因为id是一个机器编号,经过上述算法计算后,就可以得到一个单机唯一的序列号,高8位确定了所在机器,后56位使用当前时间的毫秒表示进行随机。
思考
-
为什么是左移24位?
-
左移24位完美吗?
所以最后zk使用了无符号右移,而非符号右移,这样就可以避免高位数值对SID的干扰了,在3.4.6版本后zk采用了无符号右移。
SessionTracker
SessionTracker是zk服务端的会话管理器,负责会话的创建,管理和清理工作。
public class SessionTrackerImpl extends ZooKeeperCriticalThread implements SessionTracker {
private static final Logger LOG = LoggerFactory.getLogger(SessionTrackerImpl.class);
HashMap sessionsById = new HashMap();
HashMap sessionSets = new HashMap();
ConcurrentHashMap sessionsWithTimeout;
long nextSessionId = 0;
long nextExpirationTime;
int expirationInterval;
}
创建连接
服务端对于客户端的”会话创建“请求的处理大体流程为:由NIOServerCnxn来负责接收客户端的会话状态请求,并反序列化为ConnectRequest请求, 然后根据zk服务端的配置完成会话超时时间的协商,随后SessionTracker为该会话分配一个sessionId,并将其注册到sessionsById和sessionsWithTimeout中去,
分桶策略
zk的会话管理由SessionTracker负责,其采用了分桶策略将类似的会话放在同一区块中进行管理,
分配的原则是每个会话的下次超时时间点(ExpirationTime),ExpirationTime是指该会话最近一次可能超时的时间点,对于一个新创建的会话而言,其会话创建完毕后,Zookeeper就会为其计算ExpirationTime
ExpirationTime = CurrentTime + SessionTimeout
但上图的时间并不是这个ExpirationTime,zk的leader服务器在运行期间会定时地进行会话超时检查,其时间间隔为ExpirationInterval,默认值是tickTime,(2000),也即默认情况下每隔2000ms进行一次会话超时检查,放了方便对多个会话同时进行超时检查,完整的ExpirationTime的计算方式为:
也就是上图的ExpirationTime值总是ExpirationInterval的整数倍数。这样每次leader在进行会话超时时可以同时检查多个会话。
会话激活
为了保持客户端会话的有效性,客户端会在会话超时时间过期范围内向服务端发送PING请求来保持会话的有效性,也就是心跳检测。客户端需要不断地接收来自客户端的这个心跳检测,并且需要重新激活对应的客户端会话,重新激活的过程叫做TouchSession。其主要流程:
org.apache.zookeeper.server.SessionTrackerImpl#touchSession
synchronized public boolean touchSession(long sessionId, int timeout) {
if (LOG.isTraceEnabled()) {
ZooTrace.logTraceMessage(LOG,
ZooTrace.CLIENT_PING_TRACE_MASK,
"SessionTrackerImpl --- Touch session: 0x"
+ Long.toHexString(sessionId) + " with timeout " + timeout);
}
SessionImpl s = sessionsById.get(sessionId);
// Return false, if the session doesn't exists or marked as closing
if (s == null || s.isClosing()) {
return false;
}
long expireTime = roundToInterval(System.currentTimeMillis() + timeout);//计算出新的过期时间
if (s.tickTime >= expireTime) {
// Nothing needs to be done
return true;
}
SessionSet set = sessionSets.get(s.tickTime);
if (set != null) {
set.sessions.remove(s);//从旧的过期时间的"桶"中移除
}
s.tickTime = expireTime;
set = sessionSets.get(s.tickTime);
if (set == null) {
set = new SessionSet();
sessionSets.put(expireTime, set);
}
set.sessions.add(s);//移动到新的过期时间的"桶"中
return true;
}
//计算出最近的 一下次统一检测过期的时间
private long roundToInterval(long time) {
// We give a one interval grace period
return (time / expirationInterval + 1) * expirationInterval;
}
client什么时候会发出激活请求
- 客户端向服务端发送请求,包括读写请求,就会触发会话激活。
- 客户端发现在sessionTimeout/3时间内尚未与服务端进行任何通信,就会主动发起ping请求,服务端收到该请求后,就会触发会话激活。
会话超时检查
会话超时检查同样是由SessionTracker负责的,它有一个单独的线程专门进行会话超时检查,该线程逐个依次地对会话桶中剩下的会话进行清理。如果一个会话被激活,那么Zookeeper就会将其从上一个会话桶迁移到下一个会话桶中,如ExpirationTime 1 的session n 迁移到ExpirationTime n 中,此时ExpirationTime 1中留下的所有会话都是尚未被激活的,超时检查线程就定时检查这个会话桶中所有剩下的未被迁移的会话,超时检查线程只需要在这些指定时间点(ExpirationTime 1、ExpirationTime 2...)上进行检查即可,这样提高了检查的效率,性能也非常好。
这个会话超时的线程就是org.apache.zookeeper.server.SessionTrackerImpl,里面run方法如下
@Override
synchronized public void run() {
try {
while (running) {
currentTime = System.currentTimeMillis();
if (nextExpirationTime > currentTime) {//如果下一次超时检测的时间还没到,就等
this.wait(nextExpirationTime - currentTime);
continue;
}
SessionSet set;
set = sessionSets.remove(nextExpirationTime);//进行会话清理,这个"桶"中的会话都超时了
if (set != null) {
for (SessionImpl s : set.sessions) {
setSessionClosing(s.sessionId);//标记关闭
expirer.expire(s);//发起会话关闭请求
}
}
nextExpirationTime += expirationInterval;//设置下一次清理的时间
}
} catch (InterruptedException e) {
handleException(this.getName(), e);
}
LOG.info("SessionTrackerImpl exited loop!");
}
主要过程就是,等到下一次超时检测的周期,把对应的桶中的会话全部标记关闭,给对应client发送 会话关闭的请求
会话清理
当SessionTracker的会话超时线程整理出一些已经过期的会话后,就要开始进行会话清理。大概分为七步:
- 标记会话状态为”已关闭“。由于整个会话清理过程需要一段时间,为了保证在此期间不再处理来自该客户端的请求,SessionTracker会首先将该会话的isClosing属性标记为true。这样,即使在会话清理期间接收到该客户端的新请求,也不会处理。
- 发起”会话关闭“请求: 为了使对该会话的关闭操作在整个服务端集群中都生效,zk使用了提交”关闭会话“请求的方式,并立即交付给PreRequestProcessor处理器进行处理。
expirer.expire(s);
跟进就是
org.apache.zookeeper.server.ZooKeeperServer#expire
org.apache.zookeeper.server.ZooKeeperServer#close
org.apache.zookeeper.server.ZooKeeperServer#submitRequest(org.apache.zookeeper.server.ServerCnxn, long, int, int, java.nio.ByteBuffer, java.util.List
org.apache.zookeeper.server.ZooKeeperServer#submitRequest(org.apache.zookeeper.server.Request)
org.apache.zookeeper.server.PrepRequestProcessor#processRequest
public void processRequest(Request request) {
// request.addRQRec(">prep="+zks.outstandingChanges.size());
submittedRequests.add(request);
}
发起会话关闭请求这里异步调用,把请求提交到一个linkedBlockingQueue
- 收集需要清理的临时节点: 在zk中,一旦某个会话失效后,那么和该会话相关的临时节点都需要被清理掉,首先需要将服务器上所有和该会话相关的临时节点都整理出来。
在zk的内存数据库中,为每个会话都单独保存了一份由该会话维持的所有临时节点集合,因此,在会话清理阶段,只需要根据sessionId从内存数据库中获取到这份临时节点就可以了。但是实际应用场景中,在zk处理会话关闭请求之前,正好有下面两类请求到达服务端并正在处理中:
- 节点(包含临时与非临时)删除请求,删除的目标节点正好是上述临时节点中的一个。
- 临时节点创建,修改请求,目标节点正好是上述临时节点中的一个。
对于第一类请求,需要将所有请求对应的数据节点路径从当前临时节点列表中移出,以避免重复删除,对于第二类,需要将所有这些请求对应的数据节点路径添加到当前的临时节点列表中,以删除这些即将被创建但是尚未保存到内存数据库中的临时节点。
步骤2提到删除请求放到了一个队列,下面完成消费,
org.apache.zookeeper.server.PrepRequestProcessor#run
org.apache.zookeeper.server.PrepRequestProcessor#pRequest
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;
}
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 {
...
case OpCode.closeSession:
pRequest2Txn(request.type, zks.getNextZxid(), request, null, true);
break;
...
}
进入
org.apache.zookeeper.server.PrepRequestProcessor#pRequest2Txn
对应处理如下
protected void pRequest2Txn(int type, long zxid, Request request, Record record, boolean deserialize)
throws KeeperException, IOException, RequestProcessorException
{
case OpCode.closeSession:
// We don't want to do this check since the session expiration thread
// queues up this operation without being the session owner.
// this request is the last of the session so it should be ok
//zks.sessionTracker.checkSession(request.sessionId, request.getOwner());
HashSet es = zks.getZKDatabase()
.getEphemerals(request.sessionId);//获取sessionId对应的临时节点的路径列表
synchronized (zks.outstandingChanges) {
for (ChangeRecord c : zks.outstandingChanges) {//遍历 zk serve的事务变更队列,这些事务处理尚未完成,没有应用到内存数据库中
if (c.stat == null) {//如果当前变更记录没有状态信息(删除时才会出现,参照上面处理delete时的ChangeRecord构造参数)
// Doing a delete
es.remove(c.path);//避免多次删除
} else if (c.stat.getEphemeralOwner() == request.sessionId) {//如果变更节点是临时的,且源于当前sessionId(只有创建和修改时,stat不会为null)
es.add(c.path);//添加记录,最终要将添加或者修改的record再删除掉
}
}
for (String path2Delete : es) {//添加节点变更事务,将es中所有路径的临时节点都删掉
addChangeRecord(new ChangeRecord(request.hdr.getZxid(),
path2Delete, null, 0, null));
}
zks.sessionTracker.setSessionClosing(request.sessionId);
}
LOG.info("Processed session termination for sessionid: 0x"
+ Long.toHexString(request.sessionId));
break;
}
其中,outstandingChanges理解成zk serve的事务变更队列,事务还没有完成,尚未同步到内存数据库中的一个队列.
完成该会话相关的临时节点收集后,Zookeeper会逐个将这些临时节点转换成"节点删除"请求,并放入事务变更队列outstandingChanges中。
void addChangeRecord(ChangeRecord c) {
synchronized (zks.outstandingChanges) {
zks.outstandingChanges.add(c);
zks.outstandingChangesForPath.put(c.path, c);
}
}
FinalRequestProcessor会触发内存数据库,删除该会话对应的所有临时节点。
请求最终到了org.apache.zookeeper.server.FinalRequestProcessor#processRequest
public void processRequest(Request request) {
if (LOG.isDebugEnabled()) {
LOG.debug("Processing request:: " + request);
}
// request.addRQRec(">final");
long traceMask = ZooTrace.CLIENT_REQUEST_TRACE_MASK;
if (request.type == OpCode.ping) {
traceMask = ZooTrace.SERVER_PING_TRACE_MASK;
}
if (LOG.isTraceEnabled()) {
ZooTrace.logRequest(LOG, traceMask, 'E', request, "");
}
ProcessTxnResult rc = null;
synchronized (zks.outstandingChanges) {
while (!zks.outstandingChanges.isEmpty()
&& zks.outstandingChanges.get(0).zxid <= request.zxid) {
ChangeRecord cr = zks.outstandingChanges.remove(0);
if (cr.zxid < request.zxid) {
LOG.warn("Zxid outstanding "
+ cr.zxid
+ " is less than current " + request.zxid);
}
if (zks.outstandingChangesForPath.get(cr.path) == cr) {
zks.outstandingChangesForPath.remove(cr.path);
}
}
if (request.hdr != null) {
TxnHeader hdr = request.hdr;
Record txn = request.txn;
rc = zks.processTxn(hdr, txn);
}
// do not add non quorum packets to the queue.
if (Request.isQuorum(request.type)) {
zks.getZKDatabase().addCommittedProposal(request);
}
}
移除会话
完成节点删除后,需要将会话从SessionTracker中删除。
同样包含在上图的org.apache.zookeeper.server.FinalRequestProcessor#processRequest中
rc = zks.processTxn(hdr, txn);
org.apache.zookeeper.server.ZooKeeperServer#processTxn
public ProcessTxnResult processTxn(TxnHeader hdr, Record txn) {
ProcessTxnResult rc;
int opCode = hdr.getType();
long sessionId = hdr.getClientId();
rc = getZKDatabase().processTxn(hdr, txn);
if (opCode == OpCode.createSession) {
if (txn instanceof CreateSessionTxn) {
CreateSessionTxn cst = (CreateSessionTxn) txn;
sessionTracker.addSession(sessionId, cst
.getTimeOut());
} else {
LOG.warn("*****>>>>> Got "
+ txn.getClass() + " "
+ txn.toString());
}
} else if (opCode == OpCode.closeSession) {
sessionTracker.removeSession(sessionId);//移除会话
}
return rc;
}
org.apache.zookeeper.server.SessionTrackerImpl#removeSession
synchronized public void removeSession(long sessionId) {
SessionImpl s = sessionsById.remove(sessionId);//sessionsById中移除
sessionsWithTimeout.remove(sessionId);//sessionsWithTimeout中移除
if (LOG.isTraceEnabled()) {
ZooTrace.logTraceMessage(LOG, ZooTrace.SESSION_TRACE_MASK,
"SessionTrackerImpl --- Removing session 0x"
+ Long.toHexString(sessionId));
}
if (s != null) {
SessionSet set = sessionSets.get(s.tickTime);
// Session expiration has been removing the sessions
if(set != null){
set.sessions.remove(s);//sessionSets中移除
}
}
}
最后,从NIOServerCnxnFactory找到该会话对应的NIOServerCnxn,将其关闭。
org.apache.zookeeper.server.FinalRequestProcessor#processRequest
if (request.hdr != null && request.hdr.getType() == OpCode.closeSession) {
ServerCnxnFactory scxn = zks.getServerCnxnFactory();
// this might be possible since
// we might just be playing diffs from the leader
if (scxn != null && request.cnxn == null) {
// calling this if we have the cnxn results in the client's
// close session response being lost - we've already closed
// the session/socket here before we can send the closeSession
// in the switch block below
scxn.closeSession(request.sessionId);
return;
}
}
会话重连
当客户端与服务端之间的网络连接断开时,zk客户端会自动进行反复的重连,直到最终成功连接上zk集群中的一台机器,这种情况下,再次连接上服务端的客户端可能处于下面两种状态之一:
- CONNECTED, 如果在会话超时时间内重新连接上集群中一台服务器 。
- EXPIRED。如果在会话超时时间以外重新连接上,那么服务端其实已经对该会话进行了会话清理操作,此时会话被视为非法会话。
在客户端与服务端维持的是一个长连接,在sessionTimeout时间内,服务端会不断地检测该客户端是否还处于正常连接,服务端将客户端的每次操作视为一次有效的心跳检测来反复地进行会话激活,因此,正常情况下,客户端会话是一直有效的,然而,当客户端与服务端之间的连接断开后,用户在客户端可能主要看到两类异常:
- CONNECTION_LOSS(连接断开)
- SESSION_EXPIRED(会话过期)
CONNECTION_LOSS
因为网络闪断导致客户端与服务器断开连接,或者因为客户端当前连接的服务器,出现问题导致连接断开,这类问题都是客户端与服务端连接断开,即CONNECTION_LOSS,这种情况下,zk客户端会自动从地址列表中重新逐个选取新的地址并尝试进行重试连接,知道最终成功连接上服务器。
to = readTimeout - clientCnxnSocket.getIdleRecv(); // 连接上的情况
} else { // 还没连接的情况
to = connectTimeout - clientCnxnSocket.getIdleRecv();
}
if (to <= 0) { // 读超时或者连接超时
String warnInfo;
warnInfo = "Client session timed out, have not heard from server in "
+ clientCnxnSocket.getIdleRecv()
+ "ms"
+ " for sessionid 0x"
+ Long.toHexString(sessionId);
LOG.warn(warnInfo);
throw new SessionTimeoutException(warnInfo);
}
catch (Throwable e) {
if (closing) {
if (LOG.isDebugEnabled()) {
// closing so this is expected
LOG.debug("An exception was thrown while closing send thread for session 0x"
+ Long.toHexString(getSessionId())
+ " : " + e.getMessage());
}
break;
} else {
// this is ugly, you have a better way speak up
if (e instanceof SessionExpiredException) {
LOG.info(e.getMessage() + ", closing socket connection");
} else if (e instanceof SessionTimeoutException) {
LOG.info(e.getMessage() + RETRY_CONN_MSG);
} else if (e instanceof EndOfStreamException) {
LOG.info(e.getMessage() + RETRY_CONN_MSG);
} else if (e instanceof RWServerFoundException) {
LOG.info(e.getMessage());
} else {
LOG.warn(
"Session 0x"
+ Long.toHexString(getSessionId())
+ " for server "
+ clientCnxnSocket.getRemoteSocketAddress()
+ ", unexpected error"
+ RETRY_CONN_MSG, e);
}
cleanup();
if (state.isAlive()) {
eventThread.queueEvent(new WatchedEvent(
Event.EventType.None,
Event.KeeperState.Disconnected,
null));
}
clientCnxnSocket.updateNow();
clientCnxnSocket.updateLastSendAndHeard();
}
}
}
cleanup();
clientCnxnSocket.close();
if (state.isAlive()) {
eventThread.queueEvent(new WatchedEvent(Event.EventType.None,
Event.KeeperState.Disconnected, null));
}
ZooTrace.logTraceMessage(LOG, ZooTrace.getTextTraceLevel(),
"SendThread exited loop for session: 0x"
+ Long.toHexString(getSessionId()));
}
org.apache.zookeeper.ClientCnxn.SendThread#cleanup
private void cleanup() {//socket清理以及通知两个queue失去连接 以及 清理两个队列
clientCnxnSocket.cleanup();
synchronized (pendingQueue) {
for (Packet p : pendingQueue) {
conLossPacket(p);
}
pendingQueue.clear();
}
synchronized (outgoingQueue) {
for (Packet p : outgoingQueue) {
conLossPacket(p);
}
outgoingQueue.clear();
}
}
cleanUp执行完了之后,run方法会再次进入连接逻辑
if (!clientCnxnSocket.isConnected()) {//如果clientCnxnSocket的SelectionKey为null
if(!isFirstConnect){//如果不是第一次连接就sleep一下
try {
Thread.sleep(r.nextInt(1000));
} catch (InterruptedException e) {
LOG.warn("Unexpected exception", e);
}
}
// don't re-establish connection if we are closing
if (closing || !state.isAlive()) {
break;
}
startConnect();//开始连接
clientCnxnSocket.updateLastSendAndHeard();
}
SESSION_EXPIRED
客户端与服务端断开连接后,重连时间耗时太长,超过了会话超时时间限制后没有成功连上服务器,服务器就会进行会话清理,此时客户端不知道会话已经失效,如果客户端重新连接上服务器,服务器会告诉客户端该会话已经失效(SESSION_EXPIRED), 这种情况下,用户就需要重新实例化一个zk对象。
void readConnectResult() throws IOException {
if (LOG.isTraceEnabled()) {
StringBuilder buf = new StringBuilder("0x[");
for (byte b : incomingBuffer.array()) {
buf.append(Integer.toHexString(b) + ",");
}
buf.append("]");
LOG.trace("readConnectResult " + incomingBuffer.remaining() + " "
+ buf.toString());
}
ByteBufferInputStream bbis = new ByteBufferInputStream(incomingBuffer);
BinaryInputArchive bbia = BinaryInputArchive.getArchive(bbis);
ConnectResponse conRsp = new ConnectResponse();
conRsp.deserialize(bbia, "connect");
// read "is read-only" flag
boolean isRO = false;
try {
isRO = bbia.readBool("readOnly");
} catch (IOException e) {
// this is ok -- just a packet from an old server which
// doesn't contain readOnly field
LOG.warn("Connected to an old server; r-o mode will be unavailable");
}
this.sessionId = conRsp.getSessionId();
sendThread.onConnected(conRsp.getTimeOut(), this.sessionId,
conRsp.getPasswd(), isRO);
}
/**
* Callback invoked by the ClientCnxnSocket once a connection has been
* established.
*
* @param _negotiatedSessionTimeout
* @param _sessionId
* @param _sessionPasswd
* @param isRO
* @throws IOException
*/
void onConnected(int _negotiatedSessionTimeout, long _sessionId,
byte[] _sessionPasswd, boolean isRO) throws IOException {
negotiatedSessionTimeout = _negotiatedSessionTimeout;
if (negotiatedSessionTimeout <= 0) {
state = States.CLOSED;
eventThread.queueEvent(new WatchedEvent(
Watcher.Event.EventType.None,
Watcher.Event.KeeperState.Expired, null));
eventThread.queueEventOfDeath();
String warnInfo;
warnInfo = "Unable to reconnect to ZooKeeper service, session 0x"
+ Long.toHexString(sessionId) + " has expired";
LOG.warn(warnInfo);
throw new SessionExpiredException(warnInfo);
}
然后依旧是org.apache.zookeeper.ClientCnxn.SendThread#run处理异常,调用cleanUp.
SESSION_MOVED
客户端会话从一台服务器转移到另一台服务器,即客户端与服务端S1断开连接后,重连上了服务端S2,此时会话就从S1转移到了S2。当多个客户端使用相同的sessionId/sessionPasswd创建会话时,会收到SessionMovedException异常。因为一旦有第二个客户端连接上了服务端,就被认为是会话转移了。
这个是在server检查的时候
org.apache.zookeeper.server.SessionTrackerImpl#checkSession
synchronized public void checkSession(long sessionId, Object owner) throws KeeperException.SessionExpiredException, KeeperException.SessionMovedException {
SessionImpl session = sessionsById.get(sessionId);
if (session == null || session.isClosing()) {
throw new KeeperException.SessionExpiredException();
}
if (session.owner == null) {
session.owner = owner;
} else if (session.owner != owner) {//如果owner不一致
throw new KeeperException.SessionMovedException();
}
}
思考
会话激活中,客户端在sessionTimeout/3时间内尚未和服务端进行任何通信就发PING的代码在哪?
if (state.isConnected()) {
//1000(1 second) is to prevent race condition missing to send the second ping
//also make sure not to send too many pings when readTimeout is small
int timeToNextPing = readTimeout / 2 - clientCnxnSocket.getIdleSend() -
((clientCnxnSocket.getIdleSend() > 1000) ? 1000 : 0);
//send a ping request either time is due or no packet sent out within MAX_SEND_PING_INTERVAL
if (timeToNextPing <= 0 || clientCnxnSocket.getIdleSend() > MAX_SEND_PING_INTERVAL) {
sendPing();
clientCnxnSocket.updateLastSend();
} else {
if (timeToNextPing < to) {
to = timeToNextPing;
}
}
}
而 readTimeout = sessionTimeout * 2 / 3;因此timeToNextPing略小于sessionTimeout/3。