首先Alloc Free
這個詞是我自創的, 來源於Lock Free
. Lock Free
是說經過原子操做來避免鎖的使用, 從而來提升並行程序的性能; 與Lock Free
相似, Alloc Free
是說經過減小內存分配, 從而提升託管內存語言的性能.html
對於一個遊戲服務器來說, 玩家數量是必定的, 那麼這些玩家的輸入也就是必定的; 對於每個輸入, 處理邏輯的時候, 必然會產生一些臨時對象, 那麼就須要Alloc
(New)對象; 而後每次Alloc的時候, 都有可能會觸發GC的過程; GC又會將整個進程Stop
一下子(無論什麼GC, 都會Stop一下子, 只是長短不同); 進而Stop又會影響到輸入處理的速度.git
這個鏈式反應循環, 就是一個假設. 只要每一個過程產生下一步, 足夠多(或者時間長了), 可以維持鏈式反應. 那麼最終的表現就是系統過載. 消費速度愈來愈慢, 玩家的請求反應遲鈍, 進程的內存愈來愈多, 進而OOM.github
若是每一個消息處理的耗時比較長, 那麼堆積在一塊兒的是輸入; 若是每一個消息處理的Alloc比較多, 那麼堆積在一塊兒的是GC. 這是兩個基本的觀點.shell
再回頭考慮咱們所要解決的問題, 咱們要解決一個進程處理5000玩家Online. 那這5000我的, 一秒所能生產的消息數量也就是5000左右個消息, 而咱們編程面對的CPU, 一秒處理但是上萬甚至更高的數量級. 因此大機率不會堆積在輸入這邊.編程
可是Alloc就不同, 每一個業務邏輯消息, 都有其當然的複雜性, 頗有可能一個消息處理, 產生了10個小的臨時對象, 處理完成後就是垃圾對象. 那麼就有10倍的係數, 瞬間將數量級提升一倍. 若是問題再複雜一點呢, 是否是有可能再提升到一個數量級?服務器
這是有可能的!性能
某遊戲服務器內部有物理引擎, 有ARPG的戰鬥計算, 每一個法球/子彈都是一個對象, 中間所能產生的垃圾對象是很是多的, 因此大一兩個數量級, 是很容易作到的.測試
最開始, 我在優化某遊戲服務器的時候, 忽略了這一點, 花了很長時間才定位到真正的問題. 直到定位到問題, 能夠解釋問題, 而後fix掉以後, 整個過程就變得很容易理解, 也很容易理解這個混沌系統爲什麼運行的比較慢.優化
最開始在Windows上面編譯, 調試和優化服務器. 覺得問題就這麼簡單, 可是實際上在Linux上面跑的時候, 仍是碰到了一點問題.ui
這是服務器最開始用WorkStationGC跑2500人時候的火焰圖, 最左面有不少一塊時間在跑SpinLock, 問了微軟的人, 微軟的人也不知道.
而後當時相同的版本在Intel和AMD CPU下面跑起來, 有大相徑庭的效果(AMD SA2性能要高一些, 價格要低一些). 以致於覺得是Intel CPU的BUG, 或者是其餘緣由.
WorkStationGC
和ServerGC
切換貌似對服務器性能影響也不是很大----都是過載, 機器人開了以後就沒法正常的玩遊戲, 延遲會很是高.
服務器內部有用XLua來封裝和調用Lua腳本, 有不少腳本都是策劃本身搞定的, 其中包括戰鬥公式和技能之類的.
咱們都知道MMOG的戰鬥公式會很複雜, 可能一下砍怪, 會調獲取玩家和怪物的屬性幾十次(由於有不少種不一樣的戰鬥屬性). 而後又是一個無目標的ARPG, 加上物理之類的, 一次砍殺可能會調用十幾回戰鬥公式, 因此數量級會有提高.
XLua在作FFI
的時候, 會將對象的輸入
和輸出
保留在本身的XLua.ObjectTranslator
對象上, 以致於該對象的字典裏面包含了數百萬個元素. 因此調用會變得很是慢, 而後內存佔用也會比較高. 這是其一.
第二就是, 每一個參數pass的時候, 可能都會產生new/delete. 由於服務器這邊字符串傳參用的很是多, 因此每次參數傳遞, 可能都會對Lua VM或者CLR產生額外的壓力.
基於這兩點緣由, 我把戰鬥公式從Lua內挪到C#內, 而後對Lua GC參數作了相應的調整. 而後發現有明顯的提高.
後來的事情就比較簡單了, 由於發現減小此次大量的Alloc, 會極大的提升程序的性能. 因此後續的工做重點就放在了減小Alloc上, 而後火焰圖上會有明顯的對比差異.
這是中間一個版本, 左邊pthread mutex的佔比少了一些.
這是4月優化後的版本, pthread mutex佔比已經小於10%, 可能在5%之內.
而服務器目前的版本, pthread mutex佔比已經小於2%. 幾乎沒有高頻的內存分配.
這就是我說的Alloc Free
.
繼續回到最開始的那個圖, 若是不砍斷Alloc, 那麼就會GC Stop, 進而就會影響處處理速度.
這是C#在Programming Language Benchmark Game上的測試, 能夠看到C#單純討論計算性能, 和C++的差距已經不是很大.
而某遊戲服務器內, 數百人跑在一個Server進程內, 都會都會出現處理速度不足, 猜測起來核心的問題就在GC Stop. 這是一個業務內找到AllocateString耗時的細節, 其中大部分在作WKS::gc_heap::garbage_collect
. 這種狀況在WorkStationGC下面比較突出, ServerGC下面也會有明顯的問題. 核心的矛盾仍是要減小沒必要要的內存分配, 降到CLR的負載.
固然這個例子比較極端, 從優化過程的經驗來看, 10%的Alloc大概有5%的GC消耗. 當一個服務器進程有30%+的Alloc時, 服務器的性能不管如何也上不去.
這是最核心的矛盾
. 只有CPU大部分時間都在處理業務邏輯, 才能儘量的消費更多的消息, 進而系統纔不會出現過載現象, 文章最開始說的鏈式反應也就不會發生.
實際上就變成了怎麼減小內存分配的次數. 這裏面就須要知道一些最基本的最佳實踐, 例如優先使用struct, 少裝箱拆箱, 不要拼接字符串(而是使用StringBuilder)等等等等.
可是單單有這些仍是不夠的, 還須要解決複雜業務邏輯內部產生的垃圾對象, 還須要不影響正常業務邏輯的開發. 關於這部分, 在後面一文中會詳細討論, 此處就不作展開.
C#程序內存的分配, 實際上還包含Native部分alloc的內存, 這一點是比較隱性的. 並且因爲Windows libc的內存分配器和Linux內存分配器的差別性, 會致使一些不一樣.
咱們在使用dotMemory軟件獲取進程Snapshot的時候, 能夠得到完整託管對象的個數, 數據, 以及統計信息; 可是對非託管內存的統計信息缺沒有. 因爲服務器在Windows Server上面通過長時間的測試, 例如開4000個機器人跑幾天, 內存都沒有明顯的上漲, 那麼能夠大概判斷出來大部分邏輯是沒有內存泄漏的.
Linux上應用和Windows上不同的, 還有glog的日誌上報, 可是關閉測試以後發現也沒有影響. 因此問題就回到了, Windows和Linux有什麼差別?
帶着這個問題搜索了一番, 發現Java程序有相似的問題. Java程序也會由於Linux內存分配器而致使非託管堆變大的問題, 具體能夠看Java堆外內存增加問題排查Case.
後來將Linux的啓動命令改爲:
LD_PRELOAD=/usr/lib/libjemalloc.so $(pwd)/GameServer
以後, 跑了一夜發現內存佔用穩定. 基本上就能夠判定該問題和Java在Linux上碰到的問題同樣.
後來通過搜索, 發現大部分託管內存語言在Linux都有相似的優化技巧. 包括.net core github內某些issue提到的. 這一點能夠爲公司後續用Lua作邏輯開發的項目提供一點經驗, 而沒必要再走一次彎路.
參考: