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; } }