關於zookeeper寫入數據超過1M大小的踩坑記
- 2019 年 10 月 11 日
- 筆記
背景
某天晚上集群的一個任務提交一直失敗,經過排查日誌,發現是zk客戶端寫入的數據包過大,導致報錯。我們來看下,這中間發生了什麼。
首先zk的單個znode寫入數據大小是受jute.maxbuffer參數影響的,默認是1MB,如果超過了這個數值,就會如下拋出如下的兩個異常:
客戶端:
java.io.IOException: Unreasonable length = 2054758 at org.apache.jute.BinaryInputArchive.readBuffer(BinaryInputArchive.java:100) at org.apache.zookeeper.server.quorum.QuorumPacket.deserialize(QuorumPacket.java:85) at org.apache.jute.BinaryInputArchive.readRecord(BinaryInputArchive.java:108) at org.apache.zookeeper.server.quorum.Learner.readPacket(Learner.java:152) at org.apache.zookeeper.server.quorum.Follower.followLeader(Follower.java:85) at org.apache.zookeeper.server.quorum.QuorumPeer.run(QuorumPeer.java:740)
服務端:
java.io.Exception: Len error 2054758
註:2054758 byte= 2MB
ok,問題很明顯,不就改一個參數的事嗎?如果你只是簡單的這樣的認為,那可就大錯特錯了,這個參數弄不好就會導致整個zookeeper集群掛掉,我們先來看下官網對這個參數的解釋:
鏈接:https://zookeeper.apache.org/doc/r3.3.3/zookeeperAdmin.html
注意這個選項是在高風險參數的類目下:
Unsafe Options (有一定風險的選項) The following options can be useful, but be careful when you use them. (下面的選項可能非常有用,但一定需要謹慎使用他們)
..........省略無關選項..........
jute.maxbuffer:(Java system property: jute.maxbuffer) (需要通過Java的環境變數來設置)
This option can only be set as a Java system property. There is no zookeeper prefix on it. It specifies the maximum size of the data that can be stored in a znode. The default is 0xfffff, or just under 1M. If this option is changed, the system property must be set on all servers and clients otherwise problems will arise. This is really a sanity check. ZooKeeper is designed to store data on the order of kilobytes in size.
簡單的翻譯一下:
jute.maxbuffer這個選項是需要通過Java系統變數來設置,它指定了在zk裡面一個znode節點存儲數據大小的限制,默認值是1MB,如果這個參數的值被改變,必須需要在所有的服務端和客戶端進行同步設置,否則就會出現問題(註:這是個大問題,有可能會導致zookeeper整個集群掛掉,後面再細說)。此外官網還強調了zk是用來存儲kb級別相關的數據,言外之意就是告訴警告用戶非特殊情況別亂這個值,如果操作不當,出了問題,會被老闆fuck的,切記一般不要用zk來存儲大數據包。
ok,到這裡基本弄明白了這個參數的意思,也確實與線上發生的問題有著千絲萬縷的關係,後面又經過查詢,從zk的官網jira列表裡面,找到了兩個相關的issue,如下:
https://issues.apache.org/jira/browse/ZOOKEEPER-3496
https://issues.apache.org/jira/browse/ZOOKEEPER-2101
基本證實了問題,那麼接下來,又需要思考的是,為什麼客戶端會提交這麼大一個包?後經排查確實也是客戶端程式碼存在一定的問題。
問題原因總結:
(1)客戶端程式碼,讀取了大量的不同znode的數據,然後使用了事務,將多個znode的數據打包一起發送,體積超過了1MB。
(2)因為沒有及時清理,客戶端讀取的部分數據中充斥著zk歷史上殘餘的垃圾數據
(3)服務端和客戶端相關的jute.maxbuffer參數用的都是默認值
解決辦法
如何解決,無非就是對症下藥:
(1)首先清理zk歷史上存在的垃圾數據
(2)優化程式碼,只同步有效數據和拆分事務
(3)如果1和2都暫時沒法完成,那麼只能調大jute.maxbuffer參數,來確保一定時間內安全,但非根治之法。
這裡我想重點說明下如何修改jute.maxbuffer參數,因為操作方法不當直接會引發集群宕機風險。
首先這個參數的修改,有兩種方式:
方式一:在zk的部署機器上,進入zk的conf目錄下,新建一個java.env文件,然後寫入如下內容:
export JVMFLAGS="-Xms512m -Xmx2048m -Djute.maxbuffer=10485760 " (10MB)
注意jvm堆大小,可以根據情況來控制。
方式二:
修改bin目錄下的zkServer.sh文件,在vi中搜索start單詞,然後修改如下參數:
nohup "$JAVA" "-Djute.maxbuffer=10485760 -Dzookeeper.log.dir=${ZOO_LOG_DIR}" "-Dzookeeper.root.logger=${ZOO_LOG4J_PROP}"重啟服務
這裡要說明的是,為了避免造成大範圍影響,我們操作的方式是先滾動重啟服務端zk集群,即單台修改完成之後,即可重啟,然後使用jps -lvm命令,可以在進程列表中找到我們啟動的進程,如果觀察環境參數中附帶了我們設置的值,即代表修改生效。
然後依次再滾動重啟其他的zk機器。這裡我再強調一下,啟動的順序一定是按照myid的值,從小到大依次啟動,否則有可能導致啟動失敗。
在所有服務端啟動成功後,我們可以根據需要決定是否需要啟動客戶端,如果你真的決定客戶端會寫入大於1MB的數據包,那麼就要在客戶端任務啟動的時候,同樣加上x相同的環境變數:
java -Djute.maxbuffer=10485760 -jar com.xxxx.Main
這裡面需要注意,是改服務端之後,在改客戶端的,這個順序也一定不要弄反,否則先改了客戶端重啟之後,會直接導致客戶端發送大包到服務端,這裡面又由於zk客戶端的程式碼,對寫入請求對大小,並不做校驗,僅僅對讀取請求的校驗,所以直接可以寫成功,這樣如果客戶端寫了2MB的數據成功的到了zk的leader上,這個follower節點就會去leader上同步讀取數據,而這個時候服務端的配置還是默認的1MB,所以做讀取校驗會失敗,最終導致所有的follower同步失敗,從而造成狀態不一致後引發follwer服務掛掉,此時leader認為follwer丟失之後,本身狀態可能也是不正常的,會再次發生選舉,但僅僅只能自身沒法選舉,最終導致整個zk集群掛掉,所有依賴zk做分散式協調的業務都會受到影響。所以這步一定要謹慎操作。
總結
本文主要了記錄了一次關於寫入zk數據包超過默認大小的問題,由此又詳細的分析了這裡面非常重要的一些知識和操作步驟,這告訴我們在日常開發或者運維在操作正式環境之前,一定要在測試環境多做測試,然後列出操作步驟,在正式操作時,一定嚴格按照列好的清單執行,當然這步最好做成流程化的web工具,以最大限度的降低出現問題的可能性。