記錄一次遊戲伺服器的批量掉線事故(iteye文章遷移,2014)
我負責的手遊專案先後在大陸和臺灣上線,大陸服先上的,一直比較穩定,臺灣服一個多月前出現了半夜無法登陸和批量掉線的問題,由於一開始判斷錯了方向,導致找到正確的原因花了不少時間,現在把這個問題記錄下來,分享一下.也許以後碰上類似的問題能用的上.
問題描述:伺服器執行一段時間後,玩家會無法登入,然後查gsx的日誌,也沒有發現死鎖.解決的辦法就是重啟,但是重啟之後同樣的伺服器隔一兩天又會出現同樣的問題,時間有時候在半夜有時候在白天. 臺灣的伺服器人數明顯比大陸少,最火的1服高峰期也就是2000多人線上,但是大陸服跑5000人都沒有問題.
嘗試解決:
首先想到是不是程式碼出現死迴圈了,導致伺服器失去響應,無法登入
回到原點,重新思考下問題的原因.因為臺灣服的伺服器功能相當於大陸服的子集,臺灣有的功能大陸都有,但大陸服卻一切正常.所以懷疑是不是軟硬體的差異造成的.臺灣的伺服器cpu開啟了超線程,能看見24個核心,比大陸的8核要強;記憶體跟大陸一樣都是64G,但是硬碟是用dell的310raid卡做的raid1, 效能較弱.在gsx的日誌裡面也發現
結果是悲劇的,checkpoint 時間過長的問題解決了,但是還是會出現跑幾天就出現無法登入的問題,說明這是兩個問題
3.無法登入的問題還沒解決,臺灣服又出了個新問題:每天會有
結果再次悲劇,這次系統的升級沒有解決問題...... 臺灣方面覺得他們已經升級了機器但還沒有解決問題, 開始質疑我們的辦事效率,天天qq彈窗催, 鴨梨山大.
4.回到原點,重新思考下現在擁有的線索.從GC的日誌來看,堆的大小比大陸服要大,而且還會在短時間內衝到12G的上限,導致老年代的cms concurrent mode failed,引起full gc. 這些問題在大陸服是不
存在的,而且升級到跟大陸服一樣的os和核心也沒有解決問題.那隻能從GC的引數入手了.用PrintFlagsFinal命令打出所有的JVM 引數,再在大陸服上也打一份,對比下引數,總共600多個引數,只有3個不一樣:
大陸服臺灣服
uintx InitialHeapSize := 1055530816 uintx InitialHeapSize := 1054733184
uintx MaxHeapSize := 16890462208 uintx MaxHeapSize := 16875782144
uintx ParallelGCThreads := 8 uintx ParallelGCThreads := 18
InitialHeapSize 和 MaxHeapSize 的差異應該是由於記憶體的大小的細微差別造成的, ParallelGCThreads是因為臺灣伺服器開啟了超執行緒,而ParallelGCThreads是跟cpu的數目相關的,看起來也沒什麼問題,GC執行緒多了也不是壞事,不超過cpu的數目就好.
再用jmap -heap pid 看了下runtime jvm 引數,基本上都是一樣的,除了MaxNewSize,臺灣是374m,大陸是170m左右.(這個地方逗比了,以為新生代多一百多M沒什麼問題)
using parallel threads in the new generation.
using thread-local object allocation.
Concurrent Mark-Sweep GC
Heap Configuration:
MinHeapFreeRatio = 40
MaxHeapFreeRatio = 70
MaxHeapSize = 21474836480 (20480.0MB)
NewSize = 21757952 (20.75MB)
MaxNewSize = 392560640 (374.375MB)
OldSize = 65404928 (62.375MB)
NewRatio = 7
SurvivorRatio = 8
PermSize = 268435456 (256.0MB)
MaxPermSize = 268435456 (256.0MB)
沒看出有什麼太大的問題,於是在gsxdb.properties裡面加了幾個引數,一是加大堆的上限,二是讓cms gc儘快觸發,三是略微提高下物件進入老年代的門檻(threshold 預設是4):
-Xms4G -Xmx20G -XX:CMSInitiatingOccupancyFraction=50 -XX:+UseCMSInitiatingOccupancyOnly -XX:MaxTenuringThreshold=5
結果稍微好了一點,沒有出現大面積的掉線,也沒有出現無法登入的現象,但是從gc的日誌來看,記憶體佔用還是過高...... 這個方法應該是通過堆上限的提高規避了Full GC的問題,
但是治標不治本,還是得繼續找原因.
5.還是來回的看大陸和臺灣服的gc.log檔案,突然無意中發現大陸的gc的頻率比臺灣高好多,以前都光注意堆的大小了,沒有注意時間. 想起來以前用過一個gclogviewer的軟體,搜了一下,
已經停止更新了,但是java 6還是支援的.於是把大陸和臺灣的gc.log做了個圖形化的對比:
臺灣服:
大陸服:
從圖形上看,大陸服無論是young gc,還是cms gc,都比臺灣服的頻率高很多,而且這個工具還打出了具體的數字:臺灣每0.74秒才做一個young gc,大陸是0.21秒就做一次;臺灣每151秒才做一次cms gc,大陸是55秒就來一次. 再聯想到MaxNewSize臺灣服是大陸的一倍多,推測可能是MaxNewSize大導致young gc頻率低,那麼物件的年齡就增長的慢,進入老年代的速度也慢,導致cms gc的頻率也低了.而大陸服是maxnewsize偏小,young gc的頻率很高,這樣會有較多物件進入老年代,導致cms gc的頻率升高,雖然這樣比較耗cpu,但是卻可以保證堆維持在一個較低的水平.於是在臺灣服的gsxdb.properties裡面加上NewSize和MaxNewSize引數,把年輕代控制在170m左右.
這次的結果終於是正能量的,堆的大小徹底降下來了.
6.引申出來一個問題,為什麼MaxNewSize會有一倍的差距? 我以前只知道沒有手動設定的引數,會由java的ergonomic來設定,但是它會根據什麼來設定就不知道了.這個問題也比較偏,在google
上搜,stackoverflow上提問都沒有解答,看來沒有什麼捷徑,只能從jdk的原始碼裡找了.於是從oracle的官網上下了hotspot jdk的原始碼,在Arguments.cpp裡找到了答案:
if (CMSUseOldDefaults) { // old defaults: "old" as of 6.0
if FLAG_IS_DEFAULT(CMSYoungGenPerWorker) {
FLAG_SET_ERGO(intx, CMSYoungGenPerWorker, 4*M);
}
young_gen_per_worker = 4*M;
new_ratio = (intx)15;
min_new_default = 4*M;
tenuring_default = (intx)0;
} else { // new defaults: "new" as of 6.0
young_gen_per_worker = CMSYoungGenPerWorker;
new_ratio = (intx)7;
min_new_default = 16*M;
tenuring_default = (intx)4;
}
const uintx parallel_gc_threads =
(ParallelGCThreads == 0 ? 1 : ParallelGCThreads);
const size_t preferred_max_new_size_unaligned =
ScaleForWordSize(young_gen_per_worker * parallel_gc_threads);
const size_t preferred_max_new_size =
align_size_up(preferred_max_new_size_unaligned, os::vm_page_size());
MaxNewSize的值是跟parallelgcthreads有關的,也就是說,因為臺灣服開了超執行緒,導致parallelgcthreads的值變成了18,所以MaxNewSize變大了.大陸服因為沒開超執行緒,而且之前的centos核心有問題,12核的cpu只能用8個核.導致MaxNewSize比臺灣服小很多.