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


優化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. 計算機程序設計藝術


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM