[06] 優化C#伺服器的思路和工具的使用

egmkang發表於2020-09-16

優化C#伺服器的思路和工具的使用

優化伺服器之前, 需要先對問題的規模做合理的預估, 然後對關鍵的資料做取樣, 做對比, 看和自己的預估是否一致, 誤差大在什麼地方, 是預估的不對, 還是系統實現有問題.

策劃對某遊戲伺服器的要求是3000到5000人線上.

大概的估算

玩了玩遊戲, 在前期任務的流程中, 客戶端對伺服器發生的有效請求數, 實際上是比較少的. 跑路, 點選NPC, 打怪等等, 每一個狀態變化中間需要的時間實際上是比較長的. 所以一秒的請求數應該是在0.5~1.0qps左右.

戰鬥因為是無目標的ARPG, 一點砍瓜切菜的感覺, 輸入大概在1.0~2.0qps, 輸出就比較高了, 目測係數有5.0~10.0. 也就是一個請求, 可能對應5-10個返回回來. 而且很有可能會有多個廣播包.

移動和普通的MMOG差別不是很大, 只是如果用鍵盤操作的話, 狀態變化會非常頻繁; 如果是手機的話, 應該在1.0qps左右, 應該就夠了. 唯一需要處理的就是廣播係數, 周圍的玩家越多, 需要廣播的包就越多. 某遊戲伺服器一個場景大概有40~50人. 目測係數有10.0左右.

還有DB IO, 也需要估算, 因為單次操作比較耗時. 戰鬥過程中大概率是不需要訪問DB的, 移動也是, 只有普通的任務和養成系統對DB寫依賴比較高; 然後玩家登陸過程中, 因為遊戲內有大量的系統, 可能需要10次load操作. 所以按照以往的經驗, 卡牌型別的遊戲1.0~2.0qps, 那麼這個ARPG遊戲伺服器可能就0.5~1.0qps的樣子.

採集資料

最開始處理的MongoDB的讀寫資料取樣. 按照我們的估算, load一個玩家需要10個DB操作, 一個玩家線上大概只需要0.5~1.0個DB操作. 但是我們用機器人去跑, 發現處理MongoDB讀寫的佇列經常因為過大, 進而系統OOM.

所以, 對已經完成DB操作, 和正在佇列中的DB操作進行統計分析, 需要統計的資料:

  • 型別(簡單標註一下自己是哪個系統的)

  • 檔案, 行數(進行準確的追蹤)

    C#有CallerLineNumber, CallerFilePath, 可以方便在編譯時期獲取類似於C/C++的__FILE__, __LINE__.

下來採集的是客戶端的輸入, 和傳送給客戶端的返回.

還有一種採集, 就是記憶體快照, 可以通過dotMemory來搞, 直接用VS獲取記憶體快照最後會發現看不清楚. dotMemory在這方面做得不錯.

處理思路

我在計算機程式設計藝術第一卷這本書裡面學到一個東西, 就是時間複雜度和係數. 我們在一般的資料結構或者演算法書裡面只會看到時間複雜度的大概分析, 不會告訴你準確的公式是什麼樣子的.

然而, 我們遊戲裡面需要處理的線上玩家數量所呈現出來的公式, 應該是一次函式:?(?)=??+?.

所以優化的思路, 肯定儘可能降低係數A. 因為我們無法降低線上玩家數量, 整個系統就一個程式, 策劃還需要3000-5000人線上, 如果我們能拆程式, 那麼就可以降低x.

MongoDB IO的處理

最開始用機器人做壓力測試, DB佇列總是會OOM. 經過取樣和分析, 發現:

  • 絕大部分操作都是道具上的

    道具佔最多這個是能想到的. 仔細研究資料和程式碼, 後來發現邏輯層程式碼有很多實現不太合. 例如:

    • 生成一個道具需要寫兩次DB, 一次記錄道具本身, 一次記錄用來做道具最大ID(算唯一ID用的)
    • 更新一個道具的時候, 很有可能更新了兩次
    • 玩家登陸的時候, 會把剛剛load的每個道具都儲存一次
    • 等等

    這是道具本身實現不太合理的地方, 還有就是機器人程式, 測試程式本身也要設計的比較合理, 但是通過分析發現, 某一些功能對DB壓力非常大. 例如:

    • 某個功能機器人會把所有的裝備都刪一遍, 然後再加一遍
    • 某個功能機器人可能會不停的新增道具(或者裝備), 最後揹包滿了, 就要往郵件裡面塞
    • 類似的功能有很多等等

    測試程式本身, 需要比較合理的設計, 儘可能去貼合玩家的真實操作.

  • 玩家的定時存檔

    大部分操作都是立即存檔的, 但是涉及到Player這張表, 就會延遲存檔(大概1-2分鐘), 這是MMOG常用的操作.

    經過觀察發現, 2分鐘網路卡流量會有一次高峰(這是正常的), 但是相應時間內計算的延遲也會增加(伺服器的幀率變低了). 這在最開始也是難以想通的. 嘗試過幾次修改, 發現MongoDB上batch操作和單次操作都無法解決幀率變低. 後來把所有玩家的2分鐘一起寫變成了每個玩家自己2分鐘想寫一次, 把批量寫換成了離散寫, 幀率才穩定.

    後來通過VS記憶體分析看到, MongoDB驅動會產生非常多的垃圾物件, 單個物件直接寫和多個物件批量寫最終所產生的的垃圾物件是一樣多. 所以只有離散寫可以降低GC的壓力.

  • DB操作的時間越來越長

    系統沒有過載的時候, DB操作耗時還比較正常. 過載了之後DB上的操作會越來越慢, 甚至會變長. 但是單獨寫一個寫DB的Benchmark程式去直連MongoDB就是好的.

    雖然減少了很多不必要的DB操作, 系統略微可以使用, 但是單獨這個優化是沒有解決DB操作變長這個問題.

廣播和網路IO處理

這個系列第一篇文章就講怎麼合理的網路程式設計. 但是實際上從NetUV更換DotNetty, 然後將整個編解碼完全重新實現, 再到後面批量傳送的實現, 還是消耗了一定的時間. 整個核心思想就是減少每一個包上的編解碼消耗(以及產生的垃圾物件).

但是通過訊息的輸入輸出統計分析, 還是發現一些端倪(重點關注遊戲內的廣播訊息), 例如:

  • 機器人移動一秒會發3次訊息

    因為客戶端有預判, 不會等到伺服器返回自己開始走, 服務返回之後會不斷矯正的位置, 差別不大就不需要矯正.

    所以機器人一秒發3次訊息是不合理的, 正常情況下一秒1次左右就夠了.

  • 一個跳躍有4個左右的訊息, 一個滑步有3個左右的訊息

    每次跳躍和滑步都需要使用怒氣(能量類似的東西), 然後這些東西加減, 也需要同步給所有客戶端, 實際上這些可以讓客戶端自己去模擬和維護.

    還有跳躍和滑步也是, 最多1~2個輸入就可以完成.

  • 戰鬥部分

    由於是無目標戰鬥, 所以大部分技能都是AOE技能, 砍一刀很有可能砍刀10個怪, 但是傷害如果發10個怪, 那麼就需要做10個編解碼, 發10次廣播訊息.

等等類似的東西.

記憶體分配的優化

記憶體分配的優化, 是C#伺服器的關鍵. 這個系列文章裡面大篇幅都圍繞著記憶體分配, 整個過程下來, 對演算法的優化幾乎沒有, 伺服器內甚至連AOI都沒有做, 就是去場景內定時遍歷維護視野列表(可以理解為N^2時間複雜度, N上限是40~50). 這跟很多人的以往的知識是相沖突的, 但是實際上通過profile工具分析的結果這個並不是重點.

當然記憶體的分析就需要藉助於Visual Studio了, 具體可以看前面的文章. 處理方式也比較簡單--逢山開路遇水搭橋, 找到一個fix一個就行了.

比較關鍵的兩個東西, 一個是閉包, 比如這個閉包在Player處理某個東西時候需要, 那麼就把閉包和閉包的狀態存在Player身上; 另外一個臨時的容器, 這個比較多, 需要同ThreadLocal來搞, 每次用的時候clear一下就行了.

還有一個比較關鍵的是, Linux下native部分的記憶體分配. 伺服器在WindowsServer下長時間跑, 都沒有記憶體洩漏, 但是在Linux下跑會有記憶體洩漏, 最後查詢原因是非託管部分洩漏了. 然後換成jemalloc之後解決, 這一點在最開始並沒有想到.

計算效能的優化

這是最後需要做的事情!!! 而不是一開始需要做的.

直接去用profile工具優化效能, 會被GC極大的干擾. 例如某遊戲伺服器內, 30%的時間是在跑物理引擎, 物理引擎內有大量的sin/cos計算, 由於GC沒有優化好, GC和sin/cos計算就有可能碰撞, 然後會發現有采樣的結果裡面有大量的sin/cos計算. 這是違反常識的.

直到後來GC問題解決掉了之後, 就看不到這樣很離譜的結果, 包括MongoDB執行更新操作耗時越來越長這種難以解釋的情況.

但是不是說系統中就沒有比較好的東西了, 優化到最後, 單個耗時比較高的函式都被搞掉, 只是物理引擎的耗時沒有被優化掉, 這塊佔整個邏輯執行緒30%的時間片.

考慮到5000人線上, 騰訊雲SA2機型32C64G機型, 大概CPU佔有率在15%的樣子, 所以就沒有再繼續優化, 如果還想要提高人數上限, 那麼就需要對物理引擎優化.

工具的使用

先優化記憶體, 直到GC對計算沒有影響之後, 再去優化計算.

記憶體分配取樣

這是一張取樣的圖片, 左下角是物件和分配次數, 右下角是分配的堆疊(可以點開, 也可以右鍵轉到原始碼). 可以非常方便的找到系統內分配記憶體次數較多的地方.

 

 

但是需要注意的是, 如果開幾百個機器人訪問伺服器, 那麼取樣的時候不能每個物件都跟蹤, 可以選擇100個物件跟蹤一次, 跑幾分鐘就可以了.

記憶體快照

dotMemory這個工具在獲取記憶體快照這方面做得非常好, Windows和Linux下均可以使用, 其中Linux是命令列程式獲取資料, 然後Windows客戶端可以開啟結果分析.

之前在跑機器人戰鬥的時候, 發現記憶體佔用越來越大, 然後通過dotMemory獲取快照, 發現LuaEnv佔用記憶體非常多, 然後找到某一個LuaEnv, 詳細的檢視其記憶體佔用.

 

 

 

 

發現光這個ObjectTranslator物件就佔用了33M記憶體, 上面100W+個元素, 後來優化Lua GC之後這個問題就不存在了(伺服器大概每2幀做一次GC).

還有dotMemoryDominators, 可以分析出各個系統之間的記憶體佔用, 例如下圖中, 道具佔比有一點不太正常, 研究後發現每個裝備都快取了大概25K的資料而且從來都沒有使用過.

 

 

效能取樣工具

之所以單獨說取樣工具, 是因為除了sampling技術外, 還有tracing技術也經常用於效能調優.

但是tracing工具, 本身是一個觀察者, 對效能比較敏感的程式會造成影響, 最終就不知道到底是觀察者有問題, 還是程式有問題, 還是GC有問題. 所以一般不太使用tracing技術, 而選用sampling技術. VS的sampling一般是1000HZ, perf的話大概選用99HZ的.

Linux下通過perf和flamegraph也能獲取到圖形化的資料, 這邊不在贅述, 可以看之前的文章. 但是一般還是在Windows下調優好, 再上Linux上面去驗證. Linux的perf只是一個輔助手段.

參考:

  1. 計算機程式設計藝術

相關文章