Zookeeper的CancelledKeyException異常問題

項目中用到storm+kafka+zookeeper,在實際應用中zk和kafka常出問題,這裏記錄下在使用zk過程中的問題。

注:zk版本是3.4.8,kafka是0.8.2.0。zk、storm和kafka都是運行在同一個集羣的三臺機器上。

CancelledKeyException

在開發環境測試的時候,一直沒有問題,後來原樣移植到測試環境下,zk總是出異常,導致kafka和storm連接丟失並重新發起連接請求。有時候重新連接成功而有時候鏈接失敗,導致kafka或者storm服務掛起甚至掛掉。看了下kafka和storm的日誌,最終確定問題處在zk身上,查看zk日誌,大概的異常信息如下:

ERROR [CommitProcessor:0:NIOServerCnxn@445] - Unexpected Exception:
java.nio.channels.CancelledKeyException
at sun.nio.ch.SelectionKeyImpl.ensureValid(SelectionKeyImpl.java:73)
at sun.nio.ch.SelectionKeyImpl.interestOps(SelectionKeyImpl.java:77)
at
org.apache.zookeeper.server.NIOServerCnxn.sendBuffer(NIOServerCnxn.java:418)
at
org.apache.zookeeper.server.NIOServerCnxn.sendResponse(NIOServerCnxn.java:1509)
at
org.apache.zookeeper.server.FinalRequestProcessor.processRequest(FinalRequestProcessor.java:171)
at
org.apache.zookeeper.server.quorum.CommitProcessor.run(CommitProcessor.java:73)

2013-08-09 07:06:52,280 [myid:] - WARN  [SyncThread:0:FileTxnLog@321] - fsync-ing the write ahead log in SyncThread:0 took 1724ms which will adversely effect operation latency. See the ZooKeeper troubleshooting guide 
2013-08-09 07:06:58,315 [myid:] - WARN  [SyncThread:0:FileTxnLog@321] - fsync-ing the write ahead log in SyncThread:0 took 2378ms which will adversely effect operation latency. See the ZooKeeper troubleshooting guide 
2013-08-09 07:07:01,389 [myid:] - WARN  [SyncThread:0:FileTxnLog@321] - fsync-ing the write ahead log in SyncThread:0 took 1113ms which will adversely effect operation latency. See the ZooKeeper troubleshooting guide 
2013-08-09 07:07:06,580 [myid:] - WARN  [SyncThread:0:FileTxnLog@321] - fsync-ing the write ahead log in SyncThread:0 took 2291ms which will adversely effect operation latency. See the ZooKeeper troubleshooting guide 
2013-08-09 07:07:21,583 [myid:] - WARN  [SyncThread:0:FileTxnLog@321] - fsync-ing the write ahead log in SyncThread:0 took 8001ms which will adversely effect operation latency. See the ZooKeeper troubleshooting guide 

注:之所以說是大概的異常信息,是因爲自己集羣上的日誌在一次重新部署的過程中忘了備份,已經丟失,這裏是網上找的別人家的異常日誌,所以時間和一些環境信息可能不一致,不過異常類型是一致的。

關於zk的CancelledKeyException,其實很久就發現了,後來網上找到說是zk的一個版本bug,由於不影響使用,所以一直沒理會,也不覺得是個致命的bug。所以在看到上述日誌之後,首先關注的是下面的warn,顯示同步數據延遲非常大,導致服務掛起,於是根據提示

fsync-ing the write ahead log in SyncThread:0 took 8001ms which will adversely effect operation latency. See the ZooKeeper troubleshooting guide 

去官網查了下。官方在此處給出了提示,

Having a dedicated log device has a large impact on throughput and stable latencies. It is highly recommened to dedicate a log device and set dataLogDir to point to a directory on that device, and then make sure to point dataDir to a directory not residing on that device.

意思大概是

擁有專用的日誌設備對吞吐量和穩定延遲有很大的影響。 強烈建議您使用一個日誌設備,並將dataLogDir設置爲指向該設備上的目錄,然後確保將dataDir指向不在該設備上的目錄。

以上翻譯來自Google translate。意思是希望用單獨的設備來記日誌,且並將dataLogDir和dataDir分開配置,以防止由於日誌落地磁盤與其他進程產生競爭。

說的好像很有道理,因爲zk的確日誌信息比較多,動不動就打,加上我一開始只配置了dataDir,這樣就會使得zk的事務日誌和快照存儲在同一路徑下,所以是不是真的會引起磁盤競爭!再加上,開發環境沒問題,測試環境有問題,配置一樣,所以是不是測試機器的性能不行,使得這個問題暴露的更明顯呢?

於是我去將dataDir和dataLogDir分開配置了,當然這的確是有必要的,而且邏輯上更爲清晰,儘管實際證明沒有解決自己的問題,但是還是應該這麼做。

好吧,我已經說了,實際證明並沒有什麼卵用。於是注意力再次移到這個CancelledKeyException上了。發現在測試環境上,伴隨着同步延遲問題,有大量的CancelledKeyException日誌,莫非是CancelledKeyException引起的同步延遲太高?於是準備去解決一下這個bug。

在官網上,我們看到了解釋,地址如下:https://issues.apache.org/jira/browse/ZOOKEEPER-1237

官網中(具體信息請點擊鏈接去看下)提到,這個bug影響的版本有3.3.4, 3.4.0, 3.5.0,我用到是3.4.8,不太清楚這是包含在內還是不包含?(對開源項目的bug跟蹤不太懂),顯示在版本3.5.3, 3.6.0中得到修復。然而官網上並沒有給出它這裏說的版本!!!!也許是內測版本吧,汗。

好在下方給出了patch的鏈接,也就是說我可以自己去打補丁。雖然從來沒有任何關於軟件打補丁的經驗,但好歹提供瞭解決方式,去看一下,然而又是血崩:

diff -uwp zookeeper-3.4.5/src/java/main/org/apache/zookeeper/server/NIOServerCnxn.java.ZK1237 zookeeper-3.4.5/src/java/main/org/apache/zookeeper/server/NIOServerCnxn.java
--- zookeeper-3.4.5/src/java/main/org/apache/zookeeper/server/NIOServerCnxn.java.ZK1237    2012-09-30 10:53:32.000000000 -0700
+++ zookeeper-3.4.5/src/java/main/org/apache/zookeeper/server/NIOServerCnxn.java    2013-08-07 13:20:19.227152865 -0700
@@ -150,7 +150,8 @@ public class NIOServerCnxn extends Serve
                 // We check if write interest here because if it is NOT set,
                 // nothing is queued, so we can try to send the buffer right
                 // away without waking up the selector
-                if ((sk.interestOps() & SelectionKey.OP_WRITE) == 0) {
+                if (sk.isValid() &&
+                    (sk.interestOps() & SelectionKey.OP_WRITE) == 0) {
                     try {
                         sock.write(bb);
                     } catch (IOException e) {
@@ -214,14 +215,18 @@ public class NIOServerCnxn extends Serve
 
                 return;
             }
-            if (k.isReadable()) {
+            if (k.isValid() && k.isReadable()) {
                 int rc = sock.read(incomingBuffer);
                 if (rc < 0) {
-                    throw new EndOfStreamException(
+                    if (LOG.isDebugEnabled()) {
+                        LOG.debug(
                             "Unable to read additional data from client sessionid 0x"
                             + Long.toHexString(sessionId)
                             + ", likely client has closed socket");
                 }
+                    close();
+                    return;
+                }
                 if (incomingBuffer.remaining() == 0) {
                     boolean isPayload;
                     if (incomingBuffer == lenBuffer) { // start of next request
@@ -242,7 +247,7 @@ public class NIOServerCnxn extends Serve
                     }
                 }
             }
-            if (k.isWritable()) {
+            if (k.isValid() && k.isWritable()) {
                 // ZooLog.logTraceMessage(LOG,
                 // ZooLog.CLIENT_DATA_PACKET_TRACE_MASK
                 // "outgoingBuffers.size() = " +

這簡直是慘絕人寰的補丁啊,不是可執行程序也不是壓縮包,而是源碼,還是對比之後的部分源碼……這尼瑪是要我自己去修改源碼然後編譯啊~~~

走投無路的我,去搜了一下zk編譯,然後果然有教程~~不過都是把zk源碼編譯成eclipse工程的教程,也就是說,跟着網上的步驟,我成功的將zookeeper編譯成eclipse工程,然後導入到eclipse中。接着,我看着上面的patch神代碼,認真的改了下代碼。然後怎麼辦???網上並沒有人說,於是我想既然是個ant的java project,應該也是用ant編譯吧,於是進了build.xml中講jdk版本從1.5換成1.7,然後cmd下進入到該工程,執行ant,然後顯示編譯成功。接着我去build路徑下找編譯後的jar包,果然有個新的zookeeper-3.4.8.jar,顯示日期是剛剛編譯時候的日期,但是大小比原來的小了一丟丟。

其實內心是比較懵逼的,看上面的patch應該是加了代碼啊,咋編譯後變小了?不是丟了什麼文件吧~~~官方的編譯流程是這樣的嗎???帶着這些疑問,我選擇了先不管,直接把新的jar包拿去替換原來的jar包,zk重啓。

於是奇蹟出現了,果然沒有CancelledKeyException了!!!雖然現在距離這個更換已經幾天了,但我仍然不敢說,解決了這個bug,成功的打上了補丁,因爲這一切只是我想當然去做的~

當然不用高興的太早,CancelledKeyException是沒有了,但是同步延遲的問題仍然沒有解決。同時我也將打了patch後自己變異的jar提交到了開發環境,也沒有啥問題。只是延遲的問題在測試環境中仍然存在。

這着實讓人發狂,有點不知所措。把能找到的相關的網頁都看了,基本就是按照官網說的,用專門的設備來存儲日誌,但是這個不現實,而且開發環境也沒問題啊。

有一些網友給了一些解決方案,就是在zk配置中增加時間單元,使得連接的超時時間變大,從而保證同步延遲不會超過session的超時時間。於是我嘗試修改了配置:

tickTime=4000
# The number of ticks that the initial 
# synchronization phase can take
initLimit=20
# The number of ticks that can pass between 
# sending a request and getting an acknowledgement
syncLimit=10

tickTime是zk中的時間單元,其他時間設置都是按照其倍數來確定的,這裏是4s。原來的配置是

tickTime=2000
# The number of ticks that the initial 
# synchronization phase can take
initLimit=10
# The number of ticks that can pass between 
# sending a request and getting an acknowledgement
syncLimit=5

我都增加了一倍。這樣,如果zk的forceSync消耗的時間不是特別的長,還是能在session過期之前返回,這樣連接勉強還可以維持。但是實際應用中,還是會不斷的報同步延遲過高的警告:

fsync-ing the write ahead log in SyncThread:0 took 8001ms which will adversely effect operation latency. See the ZooKeeper troubleshooting guide 

去查了下storm和kafka的日誌,還是動不動就檢測到disconnected、session time out等日誌,雖然服務基本不會掛,但說明問題還是沒有解決。

最後無奈之下采用了一個網友的建議:在zoo.cfg配置文件中新增一項配置

 forceSync=no

的確解決了問題,不再出現同步延遲太高的問題,日誌裏不再有之前的warn~

當然從該配置的意思上,我們就知道這並不是一個完美的解決方案,因爲它將默認爲yes的forceSync改爲了no。這誠然可以解決同步延遲的問題,因爲它使得forceSync不再執行!!!

我們可以這樣理解:zk的forceSync默認爲yes,意思是,每次zk接收到一些數據之後,由於forceSync=yes,所以會立刻去將當前的狀態信息同步到磁盤日誌文件中,同步完成之後纔會給出應答。在正常的情況下,這沒有是什麼問題,但是在我的測試環境下,由於某種我未知的原因,使得寫入日誌到磁盤非常的慢,於是在這期間,zk的日誌出現了

fsync-ing the write ahead log in SyncThread:0 took 8001ms which will adversely effect operation latency. See the ZooKeeper troubleshooting guide 

然後由於同步日誌耗時太久,連接得不到回覆,如果已經超過了連接的超時時間設置,那麼連接(比如kafka)會認爲,該連接已經失效,將重新申請建立~於是kafka和storm不斷的報錯,不斷的重連,偶爾還會掛掉。

看了下zk裏關於這裏的源碼:

 for (FileOutputStream log : streamsToFlush) {
        log.flush();
        if (forceSync) {
            long startSyncNS = System.nanoTime();

            log.getChannel().force(false);

            long syncElapsedMS =
                TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startSyncNS);
            if (syncElapsedMS > fsyncWarningThresholdMS) {
                LOG.warn("fsync-ing the write ahead log in "
                        + Thread.currentThread().getName()
                        + " took " + syncElapsedMS
                        + "ms which will adversely effect operation latency. "
                        + "See the ZooKeeper troubleshooting guide");
            }
        }
    }

可以看出,這種配置爲forceSync=no的多少有潛在的風險:zk默認此項配置爲yes,就是爲了保證在任何時刻,只要有狀態改變,zk一定是先保證記錄日誌到磁盤,再做應答,在任何一刻如果zk掛掉,重啓後都是不久之前的狀態,對集羣的影響可以很小。將此配置關閉,kafka或者storm看可以快速的得到應答,因爲不會立刻同步到磁盤日誌,但是如果某一刻zk掛掉,依賴zk的組件以爲狀態信息已經被zk記錄,而zk實際在記錄之前已經down了,則會出現一定的同步問題。

從源碼裏我們看到, log.flush()首先被執行,所以一般而言日誌文件還是寫進了磁盤的。只不過操作系統爲了提升寫磁盤的性能,可能會有一些寫緩存,導致雖然提交了flush,但是沒有真正的寫入磁盤,如果使用

log.getChannel().force(false);

則保證一定會立刻寫入磁盤。可以看出這樣的確更加的健壯和安全,但是也帶來一些問題,比如延遲。個人覺得,我們storm和kafka在業務上沒有直接以來zk,所以,此處設置強制同步爲no,也可以接受,何況此處的我,別無選擇~~~



轉載:https://www.jianshu.com/p/73eec030db86

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章