1. 程式人生 > >記錄一次遊戲伺服器的批量掉線事故(iteye文章遷移,2014)

記錄一次遊戲伺服器的批量掉線事故(iteye文章遷移,2014)

    我負責的手遊專案先後在大陸和臺灣上線,大陸服先上的,一直比較穩定,臺灣服一個多月前出現了半夜無法登陸和批量掉線的問題,由於一開始判斷錯了方向,導致找到正確的原因花了不少時間,現在把這個問題記錄下來,分享一下.也許以後碰上類似的問題能用的上.

問題描述:伺服器執行一段時間後,玩家會無法登入,然後查gsx的日誌,也沒有發現死鎖.解決的辦法就是重啟,但是重啟之後同樣的伺服器隔一兩天又會出現同樣的問題,時間有時候在半夜有時候在白天. 臺灣的伺服器人數明顯比大陸少,最火的1服高峰期也就是2000多人線上,但是大陸服跑5000人都沒有問題.

嘗試解決:

  1. 首先想到是不是程式碼出現死迴圈了,導致伺服器失去響應,無法登入

    .jstack命令在伺服器宕的時候打出所有的執行緒並對照程式碼,沒有發現有死迴圈的問題.然後因為是版本更新後出了這個問題,以懷疑是不是新加的程式碼導致了這個問題,又把這個版本的原始碼和上個版本用beyond compare挨個對比了一遍,也沒有發現什麼可疑的點.

  2. 回到原點,重新思考下問題的原因.因為臺灣服的伺服器功能相當於大陸服的子集,臺灣有的功能大陸都有,但大陸服卻一切正常.所以懷疑是不是軟硬體的差異造成的.臺灣的伺服器cpu開啟了超線,能看見24個核心,比大陸的8核要強;記憶體跟大陸一樣都是64G,但是硬碟是用dell310raid卡做的raid1, 效能較弱.gsx的日誌裡面也發現

    xdb  checkpoint要十幾秒,大陸服一般1秒內就搞定了.所以當時覺得這個很可能是問題所在.請教了xdb作者,他覺得checkpoint要這麼長時間肯定是有問題的,但是具體原因也不好說,可能跟磁碟的寫入速度有關. 我又請系統部的同事把大陸的raid5和臺灣的raid1做對比,發現寫入效能確實差了一倍還要多,於是就讓臺灣的運營商找了幾臺伺服器,raid1升級成710raid卡做raid10,並挪了幾臺出問題的伺服器.

 結果是悲劇的,checkpoint 時間過長的問題解決了,但是還是會出現跑幾天就出現無法登入的問題,說明這是兩個問題

  3.無法登入的問題還沒解決,臺灣服又出了個新問題:每天會有

1,2組伺服器出現2,300人的掉線,看原因都是checkmove頻率過快踢掉的,但是臺灣目前還沒有大面積的外掛出現,出現這種問題的原因是堆積了太多ccheckmove協議要處理,等到這些堆積的協議開始處理的時候,就會被判斷為超速被踢掉. 那為什麼協議會堆積呢,大陸服的人數比臺灣服還多,也可以處理的過來啊.第一次懷疑到了GC的身上,可能是GC的時候STW(stop the world),導致協議被堆積起來沒有處理.檢視gc.log的日誌,發現堆的大小確實比大陸服要大,而且在出現無法登陸的問題的時候,還出現了好多次full gc,應該是這些full gc導致伺服器失去響應了.但是jvm的引數大陸服和臺灣服都是一樣的,GC的策略也應該是一樣的,是不是因為臺灣服的核心跟大陸不一樣導致的? 因為之前更新核心解決了link的一個記憶體溢位的問題,所以這次也希望能解決這個奇怪的問題.由於系統部的同事也沒有裝過臺灣的機器,最後還是請老兔幫忙,臺灣那邊提供遠端卡控制伺服器,老兔弄了一個下午,終於把機器裝上了跟大陸服一模一樣的作業系統和核心.

結果再次悲劇,這次系統的升級沒有解決問題...... 臺灣方面覺得他們已經升級了機器但還沒有解決問題, 開始質疑我們的辦事效率,天天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裡面加上NewSizeMaxNewSize引數,把年輕代控制在170m左右.

這次的結果終於是正能量的,堆的大小徹底降下來了.

         6.引申出來一個問題,為什麼MaxNewSize會有一倍的差距? 我以前只知道沒有手動設定的引數,會由javaergonomic來設定,但是它會根據什麼來設定就不知道了.這個問題也比較偏,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比臺灣服小很多.