ZooKeeper的超时异常
ZooKeeper的超时异常包括两种:1)客户端的readTimeout导致连接丢失;2)服务端会话超时sessionTimeout导致客户端连接失效
客户端的readTimeout导致连接丢失
ZooKeeper客户端的readTimeout无法设置,它的值是根据会话超时时间计算出来的。计算规则为:
- 当客户端还未完成连接(即服务端还未完成客户端会话的创建,未通知客户端Watcher.Event.KeeperState.SyncConnected消息)之前
此时readTimeout为客户端设置的sessionTimeout * 2 / 3(即ZooKeeper.ZooKeeper(String, int, Watcher)中的sessionTimeout参数值)。
参考ZooKeeper源码org.apache.zookeeper.ClientCnxn的第430行:readTimeout = sessionTimeout * 2 / 3
- 当客户端完成连接后
readTimeout为客户端和服务端协商后的sessionTimeout * 2 / 3。
参考ZooKeeper源码org.apache.zookeeper.ClientCnxn的第1405行:readTimeout = negotiatedSessionTimeout * 2 / 3
当客户端在readTimeout时间内,都未收到服务端发送的数据包,将发生连接丢失,参考ZooKeeper源码org.apache.zookeeper.ClientCnxn第1208行代码,如下所示:
to = readTimeout - 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);
}
当发生连接丢失时:
- 客户端的请求操作将抛出org.apache.zookeeper.KeeperException.ConnectionLossException异常
- 客户端注册的Watcher也将收到Watcher.Event.KeeperState.Disconnected通知
但是,这种时候一般还未发生会话超时,ZooKeeper客户端在下次执行请求操作的时候,会先执行自动重连,重新连接成功后,再执行操作请求。因此下一次操作请求一般情况下并不会出现问题。
服务端会话超时sessionTimeout导致客户端连接失效
客户端的会话超时时间sessionTimeout由客户端和服务端协商决定。 ZooKeeper客户端在和服务端建立连接的时候,会提交一个客户端设置的会话超时时间(下面使用clientSessionTimeout代称)
ZooKeeper服务端有两个配置项:最小超时时间(minSessionTimeout)和最大超时时间(maxSessionTimeout), 它们的默认值分别为tickTime的2倍和20倍(也可以通过zoo.cfg进行设置)。
最终协商的会话超时时间sessionTimeout计算规则如下所示:
if (clientSessionTimeout < minSessionTimeout) {
sessionTimeout = minSessionTimeout;
} else if (clientSessionTimeout > maxSessionTimeout) {
sessionTimeout = maxSessionTimeout;
} else {
sessionTimeout = clientSessionTimeout;
}
ZooKeeper服务端将所有客户端连接按会话超时时间进行了分桶,分桶中每一个桶的坐标为客户端会话的下一次会话超时检测时间点(按分桶的最大桶数取模,所以所有客户端的下一次会话超时检测时间点都会落在不超过最大桶数的点上)。参考ZooKeeper服务端源码{@link org.apache.zookeeper.server.ExpiryQueue},在客户端执行请求操作时(如复用sessionId和sessionPassword重新建立连接请求),服务端将检查会话是否超时,如果发生会话超时:
- 服务端对客户端的操作请求,将响应会话超时的错误码org.apache.zookeeper.KeeperException.Code.SESSIONEXPIRED
- 客户端收到服务端响应的错误码后,将抛出org.apache.zookeeper.KeeperException.SessionExpiredException异常
- 客户端注册的Watcher也将收到Watcher.Event.KeeperState.Expired通知
这种情况下,客户端需要主动重新创建连接(即重新创建ZooKeeper实例对象),然后使用新的连接重试操作。
注意
- 连接丢失异常,是由ZooKeeper客户端检测到并主动抛出的
- 会话超时异常,是由ZooKeeper服务端检测到客户端的会话超时后,通知客户端的
如何模拟readTimeout的发生?
只需要在ZooKeeper执行操作请求之前,在执行请求操作的代码行增加debug断点,并让debug断点停留的时间在(readTimeout, sessionTimeout)之间,就可以模拟发生连接丢失的现象。
例如,ZooKeeper服务端的tickTime设置的2秒,则ZooKeeper服务端的minSessionTimeout=4秒,maxSessionTimeout=40秒,如果客户端建立连接时请求的会话超时时间为9秒, 则最终协商的会话超时时间将是9秒(因为9秒大于4秒且小于40秒)。从而,readTimeout = 9 * 2 / 3 = 6秒。 只要在ZooKeeper客户端执行请求的代码处debug断点停留时间在(6秒, 9秒)之间,就会发生连接丢失的现象。
如何模拟会话超时的发生?
只需要在ZooKeeper执行操作请求之前,在执行操作的代码行增加debug断点,并让debug断点停留的时间超过sessionTimeout,就可以模拟发生会话超时的现象。
例如,ZooKeeper服务端的tickTime设置的2秒,则ZooKeeper服务端的minSessionTimeout=4秒,maxSessionTimeout=40秒,如果客户端建立连接时请求的会话超时时间为9秒, 则最终协商的会话超时时间将是9秒(因为9秒大于4秒且小于40秒)。 只要在ZooKeeper客户端执行请求的代码处debug断点停留时间大于9秒,就会发生会话超时的现象。
测试代码
package org.setamv.jsetamv.thirdparty.zookeeper.official;
import java.util.concurrent.Callable;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException.ConnectionLossException;
import org.apache.zookeeper.KeeperException.SessionExpiredException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.Watcher.Event.KeeperState;
import org.apache.zookeeper.ZooDefs.Ids;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 下面演示ZooKeeper客户端的超时异常处理。
* <p>ZooKeeper的超时异常包括两种:1)客户端的readTimeout导致连接丢失;2)客户端的会话超时sessionTimeout导致需要连接失效。
* <p><p><b>客户端的readTimeout导致连接丢失</b>
* <p>ZooKeeper客户端的readTimeout无法设置,它的值是根据会话超时时间计算出来的。计算规则为:<ul>
* <li>当客户端还未完成连接(即服务端还未完成客户端会话的创建,未通知客户端{@link KeeperState#SyncConnected}消息)之前,
* readTimeout为客户端设置的sessionTimeout * 2 / 3(即{@link ZooKeeper#ZooKeeper(String, int, Watcher)}中的sessionTimeout参数值)。<br/>
* 参考ZooKeeper源码{@link org.apache.zookeeper.ClientCnxn}的第430行:<pre>
* readTimeout = sessionTimeout * 2 / 3
* </pre></li>
* <li>当客户端完成连接后,readTimeout为客户端和服务端协商后的sessionTimeout * 2 / 3。<br/>
* 参考ZooKeeper源码{@link org.apache.zookeeper.ClientCnxn}的第1405行:<pre>
* readTimeout = negotiatedSessionTimeout * 2 / 3
* </pre></li>
* </ul>
* 当客户端在readTimeout时间内,都未收到服务端发送的数据包,将发生连接丢失,参考ZooKeeper源码{@link org.apache.zookeeper.ClientCnxn}第1208行代码,如下所示<pre>
* to = readTimeout - clientCnxnSocket.getIdleRecv();
* if (to <= 0) {
* String warnInfo;
* warnInfo = "Client session timed out, have not heard from server in "
* + clientCnxnSocket.getIdleRecv() + "ms"
* + " for sessionid 0x" + Lo