ZooKeeper的超时异常包括两种:
1)客户端的readTimeout导致连接丢失。
2)服务端会话超时sessionTimeout导致客户端连接失效。
客户端的readTimeout导致连接丢失
ZooKeeper客户端的readTimeout无法显示设置,根据会话超时时间计算得来:
1. 当客户端还未完成连接(即服务端还未完成客户端会话的创建,未通知客户端Watcher.Event.KeeperState.SyncConnected消息)之前。
此时readTimeout为客户端设置的sessionTimeout * 2 / 3(即ZooKeeper.ZooKeeper(String, int, Watcher)中的sessionTimeout参数值)。
参考ZooKeeper源码org.apache.zookeeper.ClientCnxn:
2. 当客户端完成连接后
readTimeout为客户端和服务端协商后的negotiatedSessionTimeout * 2 / 3。
参考ZooKeeper源码org.apache.zookeeper.ClientCnxn:
当客户端在readTimeout时间内,都未收到服务端发送的数据包,将发生连接丢失。
参考ZooKeeper源码org.apache.zookeeper.ClientCnxn:
当发生连接丢失时:
- 客户端的请求操作将抛出org.apache.zookeeper.KeeperException.ConnectionLossException异常
- 客户端注册的Watcher也将收到Watcher.Event.KeeperState.Disconnected通知
但是,这种时候一般还未发生会话超时,ZooKeeper客户端在下次执行请求操作的时候,会先执行自动重连,重新连接成功后,再执行操作请求。因此下一次操作请求一般情况下并不会出现问题。
服务端会话超时sessionTimeout导致客户端连接失效
ZooKeeper服务端有两个配置项:最小超时时间(minSessionTimeout)和最大超时时间(maxSessionTimeout), 它们的默认值分别为tickTime的2倍和20倍(也可以通过zoo.cfg进行设置)。
参考zookeeper源码org.apache.zookeeper.server.quorum.QuorumPeerConfig:
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秒,就会发生会话超时的现象。
测试代码
public class ZooKeeperTimeoutHandleUsage {
private static final Logger LOG = LoggerFactory.getLogger(ZooKeeperTimeoutHandleUsage.class);
private volatile int counter = 0;
private int sessionTimeout = 9000;
private ZooKeeper zooKeeper;
public ZooKeeperTimeoutHandleUsage() {
zooKeeper = ZooKeeperUtil.buildInstance(sessionTimeout);
}
public static void main(String[] args) throws Exception {
ZooKeeperTimeoutHandleUsage usage = new ZooKeeperTimeoutHandleUsage();
usage.rightUsage();
}
/**
* ZooKeeper操作的包装,主要处理连接丢失和会话超时的重试
*/
public <V> V wrapperOperation(String operation, Callable<V> command) {
int seq = ++counter;
int retryTimes = 0;
while (retryTimes <= 3) {
try {
LOG.info("[{}]准备执行操作:{}", seq, operation);
V result = command.call();
LOG.info("[{}]{}成功", seq, operation);
return result;
} catch (ConnectionLossException e) {
// 连接丢失异常,重试。因为ZooKeeper客户端会自动重连
LOG.error("[" + seq + "]" + operation + "失败!准备重试", e);
} catch (SessionExpiredException e) {
// 客户端会话超时,重新建立客户端连接
LOG.error("[" + seq + "]" + operation + "失败!会话超时,准备重新创建会话,并重试操作", e);
zooKeeper = ZooKeeperUtil.buildInstance(sessionTimeout);
} catch (Exception e) {
LOG.error("[" + seq + "]" + operation + "失败!", e);
} finally {
retryTimes++;
}
}
return null;
}
public Watcher existsWatcher = new Watcher() {
@Override
public void process(WatchedEvent event) {
ZooKeeperUtil.logWatchedEvent(LOG, event);
if (KeeperState.SyncConnected == event.getState() && null != event.getPath()) {
registerExistsWatcher(event.getPath());
}
}
};
public void registerExistsWatcher(String path) {
try {
zooKeeper.exists(path, existsWatcher);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public void rightUsage() throws Exception {
String path = ZooKeeperUtil.usagePath("/right-usage");
String data = "demonstrate right usage of zookeeper client";
registerExistsWatcher(path);
// 创建节点
String realPath = wrapperOperation("创建节点", () -> {
return zooKeeper.create(path, data.getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT_SEQUENTIAL);
});
Long startTime = System.currentTimeMillis();
// 在下面这一行设置断点,并让断点停留以模拟连接丢失或会话超时(假设会话超时时间为9秒)
// 如果停留的时间在(sessionTimeout * 2 / 3 = 6秒, sessionTimeout = 9秒)之间,将发生连接丢失
// 如果停留的时间大于sessionTimeout = 9秒,将发生会话超时
LOG.info("模拟ZooKeeper客户端和服务端失去网络连接{}秒。", (System.currentTimeMillis() - startTime) / 1000);
// 获取节点数据
wrapperOperation("获取节点数据", () -> {
return zooKeeper.getData(realPath, false, new Stat());
});
// 获取节点数据
wrapperOperation("设置节点数据", () -> {
return zooKeeper.setData(realPath, (data + "-a").getBytes(), -1);
});
// 获取节点数据
wrapperOperation("获取节点数据", () -> {
return zooKeeper.getData(realPath, false, new Stat());
});
}
}
public class ZooKeeperUtil {
/**
* 按指定的超时时间构建ZooKeeper客户端实例
* @param sessionTimeout
* @return
*/
public static ZooKeeper buildInstance(int sessionTimeout) {
final CountDownLatch connectedSemaphore = new CountDownLatch(1);
Watcher watcher = (watchedEvent) -> {
ZooKeeperUtil.logWatchedEvent(LOG, watchedEvent);
connectedSemaphore.countDown();
};
ZooKeeper zooKeeper;
try {
zooKeeper = new ZooKeeper(SERVERS, sessionTimeout, watcher);
connectedSemaphore.await();
} catch (Exception e) {
throw new RuntimeException(e);
}
return zooKeeper;
}
}