記一次 HDFS NameNode GC 調優
沒有碰到過 GC 問題的人生對寫 Java 的人來說是不完整的。大資料生態圈的框架大都以 JVM 系語言開發(Java Scala 為主),畢竟生態成熟嘛要啥有啥。
HDFS 作為大資料領域的預設分散式檔案系統,其運作方式導致了非常容易碰到 GC 問題:
-
大量的元資料需要儲存在記憶體中,使得很容易就需要幾十 G 甚至 100 多 G 的堆
-
大量且高併發的檔案讀寫操作使得頻繁地產生新物件
下面就舉兩個例子,簡單分享下我們做的一些調優。
案例一
有業務同事反饋任務跑的慢,雖然後來確認是其他原因導致的,但在分析過程中,我們從監控觀察到 RPC 排隊時間和處理時間不是很穩定,有時會出現幾秒甚至10多秒的的毛刺,進而注意到 GC。
一分鐘一個點,大概每分鐘有 2、3秒花在 GC 上。我們用的是經典的 ParNew + CMS 的組合,檢視 GC 日誌發現大部分都是新生代的 GC,也就意味著有 3% - 5%的時間是 STW 的。這個比例看著不大,但在 NameNode 每秒幾 K 甚至幾十 K 的事務的壓力下,絕對數值和對具體業務的影響還是不能忽視的。
知道了原因,調整就很簡單了。從 NameNode 的工作原理分析,大量檔案的讀寫確實會建立很多臨時物件,調大新生代就是很自然也很正確的辦法。一方面,更大的新生代能減少 minor gc 的次數;另一方面,更多臨時物件在新生代回收也減少了晉升到老年代的物件的數量,減少了 CMS GC 的壓力。
但還有個不能忽視的問題,NameNode 的元資料會佔據非常大的老年代空間,因此新生代也不能調的太大,否則可能觸發頻繁的 CMS GC 甚至 Full GC。
具體調多大,很顯然不會有標準值,只能根據實際資料總量和 TPS 來綜合考慮。好在 NameNode 每個元資料的記憶體佔用是確定的,乘以物件數,再留些餘量,就能準確計算出老年代需要的記憶體。剩下的就可以分給新生代了。如果餘量不足,也可以考慮適當調大整個堆的大小。
在我們的生產環境下,80+G 的 heap,我們把新生代設定為 12G,到達了可以接受的效果。
除了 GC 時間變為幾分之一外,GC 次數也減少了幾乎一個數量級,RPC 排隊時間的毛刺也減少到百毫秒級別,惡劣情況下 2 秒左右,CMS GC 的次數也減少到 10 多個小時 1 次。
案例二
第一個案例調整過程中,有一天突然收到流量異常的告警,檢視 NameNode 監控,發現這樣的情況:
大部分監控資料都出現了中斷,但 Host CPU Usage 這類機器級別的監控卻正常,可以排除監控、機器和網路問題,確定是服務問題。很快發現是發生了一次 Full GC。
我們使用的 ParNew + CMS 組合,發生 Full GC 通常有這麼幾種原因:
-
主動觸發,比如程式呼叫 System.gc()
-
老年代空間不足,可能是 promotion failed 或者 concurrent mode failure 等
-
永久代空間不足
首先排除 1。而觀察日誌,老年代空間還很充足,也沒有發現 concurrent mode failure 和 concurrent mode failure 這樣的 log,Full GC 發生前全是 ParNew GC,都沒有發生 CMS GC。
那就只能是 3,永久代空間不足,發生了擴容,導致發生 Full GC。
我們查看了預設的永久代大小:
預設值是 20.75M,實在有點小,和 log 中的值對比,也確實發生了擴容,然後又再次逼近了擴容後的值。
解決方法也很簡單,我們把 -XX:PermSize 調整到 256M,並把 -XX:MaxPermSize 設定成一樣大,這樣初始值夠用,也不會再有擴容的問題。
設定上線後,再也沒有出現過 Full GC。
這兩個案例都不算難,相信大家都能順利解決。我想更值得總結的是,怎樣能更快地解決。
對 GC 的理解,對 NameNode 工作原理的理解,從諸多現象中快速分析出重點,這三點結合在一起才能保證你能迅速解決掉問題。前兩點需要平時系統性地學習,最後一點需要點天賦但也能通過不斷訓練加強。