dubbo是一個成熟且被普遍運用的框架。饒是如此,在某些極端條件下基於dubbo的應用還會出現沒法重連zookeeper的問題。因爲此問題容易致使比較大的故障,因此筆者費了一番功夫去定位,現將排查過程寫成博文分享出來。java
這是一塊兒在測試環境出現的故障。原由是網工作交換機切換演練,可能因爲姿式不對,使得斷網的時間從預估的秒級達到了分鐘級。等網絡恢復後,測試環境就炸開了鍋,基本上全部應用再也沒法提供服務,在dubbo控制檯上也看不到任何提供者,他們和zk的鏈接都斷開並且彷佛徹底沒有重連的跡象。以下圖所示:git
爲了避免影響測試的進度,運維同窗緊急進行了重啓,但坑爹的是大部分系統都有啓動依賴,盲目的重啓只會由於xxx provider不存在而沒法啓動。只能從最基礎的服務開始重啓,慢慢恢復。以下圖所示:
還好只是測試環境,但爲了避免讓產線出現這種問題,必須一查到底,把這個Bug揪出來。github
測試環境的好處是咱們能夠用各類手段去模擬復現,而不用和處理產線同樣處處尋找蛛絲馬跡而後進行邏輯推理(推理是一個很是燒腦的過程)。因而筆者聯繫了SA同窗,經過iptables進行線下的斷網模擬。命令以下所示:apache
// 禁用本機和zk三臺機器的流量進出 iptables -A INPUT -s zk-1-ip/32 -j DROPiptables -A INPUT -s zk-2-ip/32 -j DROPiptables -A INPUT -s zk-3-ip/32 -j DROPiptables -A OUTPUT -s zk-1-ip/32 -j DROPiptables -A OUTPUT -s zk-2-ip/32 -j DROPiptables -A OUTPUT -s zk-3-ip/32 -j DROP
拓撲圖以下:
發如今drop對zk的包以後,無論等待多長時間,只要鏈接一放開,立馬就能重連zk!
看來dubbo對zookeeper的重連仍是很是靠譜的。緩存
因爲模擬zk斷開不會致使沒法重連的現象。因而筆者開始思考,是否交換機異常的時候致使了全部的包都沒法發送/接收,而致使重連出問題的並非對zookeeper發起鏈接。因而筆者看了看配置,是否還有其它和重連有關聯的點,仔細觀察下這個配置:網絡
// 這其中有一個不容易注意到的點,就是域名解析也須要網絡包的交互 dubbo.registry.address=zookeeper://dubbo-1.com?back=dubbo-2.com,dubbo-3.com
難道是DNS訪問不到致使了這一問題?反正測試環境,繼續模擬一發,命令以下所示:session
// 禁用本機和zk三臺機器的流量進出 iptables -A INPUT -s zk-1-ip/32 -j DROPiptables -A INPUT -s zk-2-ip/32 -j DROPiptables -A INPUT -s zk-3-ip/32 -j DROPiptables -A OUTPUT -s zk-1-ip/32 -j DROPiptables -A OUTPUT -s zk-2-ip/32 -j DROPiptables -A OUTPUT -s zk-3-ip/32 -j DROP// 禁用本機和DNS兩臺機器的流量進出 iptables -A INPUT -s dns-ip/32 -j DROPiptables -A INPUT -s dns-ip/32 -j DROPiptables -A OUTPUT -s dns-ip/32 -j DROPiptables -A OUTPUT -s dns-ip/32 -j DROP
網絡拓撲以下:
此次咱們在禁用流量後,故意先放開對zk的流量,再放開對DNS的流量,以下圖所示:
看來在dubbo對zookeeper重連過程當中,若是DNS也沒法響應,是會出現網絡恢復後也再也沒法重連的現象。可是,咱們並不能下判斷交換機的故障致使的沒法重連確定是這個Bug引發。須要找到證據來證實這一點!app
有了DNS這個信息後,先google一下,看看可否有其它人遇到過這個坑。因而找到了這個連接框架
https://github.com/sgroschupf/zkclient/issues/23
按照github上的描述,zkclient在UnknownHostException拋出以後再也沒法重連zookeeper。不過他是在Kafka中遇到的,但他推斷全部用低版本org.apache.zookeper的都會有這個問題。按照上面給出的Bug Fix連接運維
https://issues.apache.org/jira/browse/ZOOKEEPER-1576
筆者發現其在3.5.0修復
因而將對應的應用的org.apache.zookeeper版本升級到3.5.5版本,從新實驗後,發現問題解決了!
這裏有個小技巧,咱們能夠經過
zip -d xxx.jar WEB-INF/lib/zookeeper-3.4.8.jarzip -r 0 xxx.jar WEB-INF/lib/zookeeper-3.5.5.jar // 以及zip -r 其它zookeeper-3.5.5新依賴的包
使得不用從新編譯打包的方式便可修改應用使用的jar包版本,這樣在快速驗證的時候就不須要通知對應的開發修改依賴了。
因爲筆者所在的產線環境有不少老系統用的jdk1.6,而zookeeper-3.5.5之支持1.8及以上版本,因此須要尋找可以給jdk1.6使用的包。此時,筆者的同事因爲負責kafka,其對kafa作過混沌測試,堅信kafka沒有這個問題,因而筆者就用kafka依賴的zookeeper-3.4.13包繼續進行測試,發現zookeeper-3.4.13也是okay的,具體代碼改動將會在下面講到。
因爲多是UnknownHostException致使了這一問題,筆者在出問題的應用裏面尋找,確實發現了UnknownHostException。日誌以下所示:
// 下面過濾了一大堆disconnected鏈接斷開日誌,只列出核心有關的2020-03-19 21:06:28.926 [DubboZkclientConnector-EventThread] zookeeper state changed (Disconnected)2020-03-19 21:06:28.926 [ZkClient-EventThread-101-dubbo.com] Zookeeper失去鏈接2020-03-19 21:06:49.758 [DubboZkclientConnector-EventThread] zookeeper state changed (Expired)2020-03-19 21:06:49:759 [DubboZkclientConnecto-SendThread] Unable to reconnect to ZooKeeper sercice ,session 0xXXXXX has expired2020-03-19 21:07:29.793 [DubboZkclientConnector-EventThread] ERROR ClientCxnn - Error while calling watcherjava.lang.RuntimeException: Exception while restarting zk client ...... ......Caused by: java.net.UnknownHostException: dubbo-1.com at...lookupAllHostAddr... ...... at...StaticHostProvier... ...... at...reconnect...[zookeeper-3.4.8.jar:3.4.8--1]
上面日誌反應出在zookeeper session expired以後從新創建session的過程當中若是拋出java.net.UnknownHostException後,zkclient對應的線程就不再會有其它動做。
上面的證據再配合實驗的結果基本就能肯定這個DNS異常會致使dubbo沒法重連zookeeper的現象。因而筆者開發翻閱代碼,首先咱們看下dubbo重連的邏輯:
public class ZkclientZookeeperClient extends AbstractZookeeperClient<IZkChildListener> { ...... public ZkclientZookeeperClient(URL url) { super(url); client = new ZkClientWrapper(url.getBackupAddress(), 30000); client.addListener(new IZkStateListener() { public void handleStateChanged(KeeperState state) throws Exception { ZkclientZookeeperClient.this.state = state; if (state == KeeperState.Disconnected) { stateChanged(StateListener.DISCONNECTED); } else if (state == KeeperState.SyncConnected) { stateChanged(StateListener.CONNECTED); } } // 這邊是session重建的過程 public void handleNewSession() throws Exception { stateChanged(StateListener.RECONNECTED); } }); client.start(); } ...... }// StateListener.RECONNECTED的處理在zookeeperResigry.java中public class ZookeeperRegistry extends FailbackRegistry{ ...... zkClient.addStateListener(new StateListener() { // 在收到RECONNECTED事件後,進行recover也就是恢復的過程 public void stateChanged(int state) { if (state == RECONNECTED) { try { recover(); } catch (Exception e) { logger.error(e.getMessage(), e); } } } }); ...... }
由上面的代碼咱們能夠得知,在session expired以後內部會重建session,在新建session以後,dubbo的Statelistener會發送reconnected事件從而執行恢復的過程,以下圖所示:
那麼咱們看下UnknownHostException拋出後會致使什麼現象,代碼以下所示:
public class ZkClient implements Watcher { ...... private void processStateChanged(WatchedEvent event) { // 這邊對應於session expired日誌 LOG.info("zookeeper state changed (" + event.getState() + ")"); setCurrentState(event.getState()); if (getShutdownTrigger()) { return; } try { fireStateChangedEvent(event.getState()); if (event.getState() == KeeperState.Expired) { // UnknownHostException就是在這拋出。 reconnect(); fireNewSessionEvents(); } } catch (final Exception e) { // 這邊對應於Error while calling watcher日誌 throw new RuntimeException("Exception while restarting zk client", e); } } ...... }
異常是在reconnect拋出的,異常拋出後,就不會運行fireNewSessionEvents這個邏輯,也就不會執行listener中的handleNewSession邏輯,進而不會recover,從而致使dubbo沒法重連!以下圖所示:
因爲UnknownHostException是在StaticHostProviver中觸發,在這邊筆者給出了新舊版本的對應代碼,舊版本zookeeper-3.4.8
public final class StaticHostProvider implements HostProvider{ ...... public StaticHostProvider(Collection<InetSocketAddress> serverAddresses) throws UnknownHostException { for (InetSocketAddress address : serverAddresses) { InetAddress ia = address.getAddress(); // 這邊沒有抓住UnknownHostException異常 InetAddress resolvedAddresses[] = InetAddress.getAllByName((ia!=null) ? ia.getHostAddress(): address.getHostName()); for (InetAddress resolvedAddress : resolvedAddresses) { ...... if (resolvedAddress.toString().startsWith("/") && resolvedAddress.getAddress() != null) { this.serverAddresses.add( new InetSocketAddress(InetAddress.getByAddress( address.getHostName(), resolvedAddress.getAddress()), address.getPort())); } else { this.serverAddresses.add(new InetSocketAddress(resolvedAddress.getHostAddress(), address.getPort())); } } } if (this.serverAddresses.isEmpty()) { throw new IllegalArgumentException( "A HostProvider may not be empty!"); } Collections.shuffle(this.serverAddresses); } ...... }
新版本zookeeper-3.4.13小小的重構了一下,將DNS的邏輯放到next函數裏面並抓住了UnknownHostException異常。
public final class StaticHostProvider implements HostProvider{ ...... public InetSocketAddress next(long spinDelay) { currentIndex = ++currentIndex % serverAddresses.size(); if (currentIndex == lastIndex && spinDelay > 0) { try { Thread.sleep(spinDelay); } catch (InterruptedException e) { LOG.warn("Unexpected exception", e); } } else if (lastIndex == -1) { // We don't want to sleep on the first ever connect attempt. lastIndex = 0; } InetSocketAddress curAddr = serverAddresses.get(currentIndex); try { String curHostString = getHostString(curAddr); List<InetAddress> resolvedAddresses = new ArrayList<InetAddress>(Arrays.asList(this.resolver.getAllByName(curHostString))); if (resolvedAddresses.isEmpty()) { return curAddr; } Collections.shuffle(resolvedAddresses); return new InetSocketAddress(resolvedAddresses.get(0), curAddr.getPort()); } catch (UnknownHostException e) { // 就是這邊抓住了UnknownHostException進而修復了這一問題 return curAddr; } } ...... }
咱們能夠看到新版zookeeper-3.4.13抓住了這個UnknownHostException進而修復了這一問題。
在與zookeeper服務鏈接異常並session expired(默認30s),DNS緩存也超時(默認30s)同時用的低版本zookeeper jar包就很容易達成上述Bug的狀況。而以前測試環境因爲交換機切換的某些緣由使得網絡斷開超過了30s從而誘發了這一問題。並且不只僅是Dubbo,任何用zookeeper低版本jar包都有可能出現這個問題!
海恩法則指出,每一塊兒嚴重事故的背後,必然有29次輕微事故和300起未遂先兆以及1000起事故隱患。因此對測試環境的任何問題都要引發重視,從而把問題消滅在萌芽階段。