早早就想寫這篇文章,但這段時間忙于工作的事情,就不自覺地給了自己各種懶惰的理由。現在回頭看下這個問題,總結下erlang 遊戲開發經驗。就當是,為我過去一段時間的erlang開發經曆,畫上一個小句号。
在寫這篇文章前,我看過孔慶泉同學寫過的Erlang 性能優化總結[2],字裡行間有一點自己的體會,使得我心血來潮,好像重新做回了erlang開發的感覺。是以,現在回過頭,整理下遊戲開發對erlang的使用。
1. 架構設計
很多人都說Erlang天生分布式,歸結原因是erlang分布式這塊實作比較完善。 節點間的通信是透明的,無論是節點内的程序,還是不同節點的程序,都可以使用 erlang:send/2 發送消息,而且資料不需要做任何轉換。 是以,很多項目會選用多節點架構,可以通過橫向拓展(增加機器數量)來支撐更多負載,把性能壓力大的子系統配置設定到不同的節點,這樣,就可以 支援更多的線上玩家。 這樣做的出發點是好的,但會引起一系列的問題: 1、業務邏輯的問題: 跨節點邏輯變得複雜,玩家資料同步、一緻性問題,節點斷開及重連的處理等 2、語言局限性問題: 節點間消息通訊要序列化和反序列化,原子 傳輸要轉成字元串,二進制複制 等
那架構選擇單節點,還是多節點? 沒有絕對,這要看遊戲而定。假如你的遊戲像頁遊那樣,分服而且各個服之間互動較少,單節點會比較适合。但如果像lol那種對戰玩法的遊戲,多節點會比較合适,登入和玩法在不同的節點,玩家對戰時選擇叢集内壓力較小的節點做戰鬥計算。
單節點,好處是玩家資料容易保證一緻,運維友善。以常見的MMORPG遊戲,單節點在16G記憶體,8核心CPU,可以支撐2k人穩定線上,考慮非活躍玩家,達到5000也是可能的。當然,遊戲的架構要合理設計,做一些妥協,比如玩家尋路不能在服務端做,地圖 九宮格的設計,排行榜不實時重新整理,控制同屏玩家數量等 等。現在,單節點做遊戲服,還有一個重要原因是,現在的機器性能相較以前高很多了,加上erlang無鎖的Actor設計在并發場合下解放了cpu,從某種程度上講提高了cpu的運算能力。
單節點的設計:

多節點的設計:
erlang節點注意一個問題,預設erlang節點是全聯通的,也就是當一個節點加入叢集時,叢集其他所有節點會和新加入的節點建立聯系。全聯通帶來的問題,叢集節點間兩兩連接配接,随着節點增加,連接配接數量呈N*(N-1)/2增長,越發恐怖,連接配接本身占用了端口資源。更壞的是,為了檢測節點的存活,erlang會定期發心跳包檢查,即使一分鐘一個tick,節點多的話也會造成大量的網絡風暴。 解決方法就是在叢集中隐藏節點,就可以避免全聯通,隻要erlang啟動加個參數即可 -hidden
2. 資料庫
資料庫io向來都是遊戲的主要性能瓶頸,處理不好容易導緻 遊戲卡頓,甚至崩潰。 而通用的持久化政策就是,記憶體讀寫+定時持久化。玩家上線時,加載玩家頻繁用到的資料到記憶體,玩家大多時候就是在讀寫這些資料。然後定時把這些資料從記憶體刷到磁盤,在玩家下線時也做一次持久化。
說下erlang自帶資料庫 - mnesia 實際上,erlang已實作了這樣一套持久化機制,也就是 mnesia資料庫。 mnesia資料存儲是基于ets和dets實作,對于ram_copies表使用ets;disc_copies表同時使用ets和dets,資料讀寫使用ets,dets做持久化;而disc_only_copies表使用的是dets 這裡先讨論 disc_copies的情況, mnesia啟動時,會将 disc_copies表所有資料加載到ets表中,每次讀資料都是讀ets,寫資料則會先寫到ets,然後再寫一份到日志檔案中,等待定時(或定量)持久化刷到dets。 通常disc_copies表可以滿足我們的業務需求,但使用mnesia要注意一個問題。前面也提到了,mnesia啟動會将表中的資料加載到ets,假如你的表過大,就會導緻記憶體被急劇消耗掉。(特征就是,ets所占的記憶體比率過大) 是以,使用mnesia表時,經常都是要disc_copies表和disc_only_copies表配合使用。那問題來了, 什麼時候使用 disc_copies表,什麼時候disc_only_copies表。
最簡單的就是,對玩家資料動刀。玩家資料預設用disc_copies表,如果長時間沒登入後将這個玩家的資料移到disc_only_copies表,等到他下次登入時再将資料移到disc_copies表。 之是以可以這麼做,理由有兩個: 1. 遊戲中玩家資料所占的比例較大,調整玩家資料可以獲得明顯收益。
2. 玩家流失後回到遊戲的可能性很小,就算有的話比例也不大。 這麼做的弊端就是玩家流失後重新登入的時間較長,但通過這種方式減少的記憶體很可觀。
mnesia的使用,還要注意3個問題: 1. mnesia單個表2G檔案大小限制,是以要自己分表,或者使用表分片 2. mnesia叢集功能,過多的人說有坑,但我沒有這方面的經驗,就不做讨論 3. mnesia事務并發性太差,盡可能不用mnesia事務,多髒寫;事務可利用程序實作,保證資料安全
3. 程序
每個玩家一個程序的設計已經成為了erlang遊戲開發的潛規則了。這個沒什麼好講,玩家程序修改自己的資料,程序消息同步處理機制保證資料一緻性。可能有些遊戲還會将玩家程序和scoket程序獨立開,負責連接配接的建立和維護,協定封包解包,甚至做攻擊的防範等。但如果玩家程序和socket程序同在一個節點内,顯然整合在一個程序較好,erlang消息基于複制,中間多了一個程序,一次前後端互動要多了2次記憶體複制。
那麼,除玩家外,其他程序怎麼确定? 1. 地圖程序 每個玩家都是獨立的程序,玩家pk要交換兩個程序的私有資料,就要發消息給另一個程序處理。 假如是強pk的遊戲,同時有N個玩家一起打鬥,消息就會繁多。因為資料一緻性問題,程序間的并發機制就會弱化成同步機制,增加了戰鬥時延。 是以,這裡會引入地圖程序,通常以一個地圖一個程序。玩家進入地圖時,會同步戰鬥相關資料到地圖程序,玩家離開地圖時,再将戰鬥資料同步回玩家程序。而在玩家進入地圖到離開前的這段時間,一切的戰鬥計算都由地圖程序完成。 或者有人會有疑惑,就算有了地圖程序,還是有同步問題,地圖程序還是要同步處理pk請求,無法并發處理,玩家程序還是要等待地圖程序操作完成。 其實,對于玩家的pk請求,處理至少有兩個過程,第一個過程是驗證攻擊的合法性,如是否有這個技能,技能cd,等等。第二個過程才是戰鬥計算,玩家程序檢查合法性,再由地圖程序做核心的戰鬥計算。另外一個,玩家 程序除了戰鬥請求外,還有其他業務邏輯上的消息,容易出現程序挂起的情況,這時候,玩家程序不可能處理到戰鬥計算,就會導緻戰鬥卡頓。
2.公共程序 公共程序指的是那些提供公共服務的程序,比如: 1. 社交類,有好友、幫派、組隊等,這些服務管理着多數玩家的資料,都需要一個程序來管理。 2. 計算類,這類有一定的計算量,比如說排行榜,要有一個程序來承擔計算。 3. 廣播類,有聊天室、世界聊天、幫派聊天、地圖廣播等 4. 開關類,有 活動系統,比賽系統等等,控制遊戲活動的開啟和關閉
erlang程序雖然廉價,但是不要太過随意建立程序,比如建立一個臨時程序異步傳輸資料等等。雖然這在某種程度上提高了并發性,但程序的建立和銷毀需要一定的系統消耗,而且會導緻項目中程序數量不可控,可能系統莫名其妙多了很多程序,這些維護起來也麻煩。再說,erlang 同時存在的程序有最大 數量限制
4. 程序字典與ets
程序字典是erlang遊戲開發中最為常用的資料記錄方式,理由很簡單,因為它夠快,差不多比ets快了一個數量級。但是,程序字典的資料為所在程序私有,無法跨程序直接get到程序字典的資料,而且,在程序被銷毀時,程序字典的資料也會被回收。 再說下ets,對比程序字典,ets的适用場景是跨程序讀寫資料。遇到一個資料頻繁被多個程序讀到,就要考慮使用ets了。另外,ets有歸屬程序,但歸屬程序銷毀時,ets的資料就會被系統回收。
對比程序字典和ets的實作,差別如下: 1. 鎖方面,程序字典為無鎖操作,ets是讀寫鎖 2. 資料方面,程序字典資料在程序中,查詢沒有多餘的複制操作;而ets,因為資料不在程序中,查詢時會複制一份到程序。
是以,程序字典通常是最優先的選擇。假如玩家的資料是存儲在mnesia,特别是玩家的核心資料,就有必要從mnesia讀到放在程序字典中。前面講到了mnesia是利用ets和dets實作的,玩家上線時将核心資料讀到程序字典,這樣,讀寫都在程序字典,就可以利用程序字典帶來的性能提升。這個提升是很可觀,畢竟快了一個數量級。
前面提到了ets歸屬程序銷毀時,ets資料也會被回收? 那麼如果預防資料丢失的問題,ets也提供了方法,通過設定繼承者程序就可以了。 ets:new(person, [named_table, {heir, HeirPid, HeirData}]) 當歸屬程序崩潰時,繼承者程序就會收到資訊 {'ETS-TRANSFER', Tid, FromPid, HeirData},并且,ets的歸屬權就會轉交到繼承者程序。
5. 消息廣播
消息廣播是遊戲中的性能消耗大頭,主要包括地圖的行走、戰鬥廣播,世界聊天廣播。世界聊天廣播可以通過控制玩家發送時間間隔來限制, 地圖中的廣播包,比如行走和戰鬥包的廣播實時性高,隻需發給視野内的玩家就可以,不用全地圖廣播。是以常見的處理方式是,玩家的視野通過九宮格劃分,玩家的地圖廣播隻發送給九宮格内的玩家。
如何在伺服器壓力大時,進一步優化廣播消息:
針對時效性高的資料包,不重要的廣播包,可以選擇性丢棄: 1、挂機玩家不發送戰鬥包,特别是戰鬥buff。 2、每N個包丢棄一個 挂機玩家的判定,前端以玩家長時間沒動作來判定,或者将遊戲切到背景運作時判定。等玩家在遊戲内點選滑鼠,或鍵盤時解除挂機狀态。
這裡再談下,消息廣播還可能進一步優化。通常,後端發資料給前端都會對協定資料進行序列化(封包),然後再發給前端反序列化(解包)。而廣播資料,對于每個玩家可能都是一樣的,沒必要每個玩家發給前端時都各自序列化一次,就隻要序列化一次,然後每個玩家拿到資料後直接發給前端。
erlang消息廣播要注意什麼問題? 1、reduction計數 通常會啟動一個消息管理程序,這個程序就負責把廣播消息轉發給對應的所有玩家程序。啟用管理程序的一個好處是,程序發消息會扣除reduction,而且這個reduction扣除大小還受到接收者程序影響。假如直接在地圖程序做消息廣播,就會導緻地圖程序受到的排程極度減少,影響戰鬥計算。 2、消息複制 erlang消息發送基于複制,但對于比較大的二進制資料,則會優化成二進制引用,減少二進制複制帶來的開銷。是以,當一個消息要發給多個程序,特别是協定資料(發給前端也要先轉成二進制),可以先轉成二進制再發送。
結束語
文章到這裡就結束了,這是我做 Erlang 遊戲開發經驗總結的一些經驗,後續,我還會找時間總結更多的開發經驗。最後呢,建議大家少上複制網站,如紅黑聯盟,這些網站複制了還會把文章來源網址删了,然後給了搜尋引擎幾個錢就排到前面去了。這樣帶來的問題是,文章我一開始寫錯了,但後來改過來了,這些網站是不會更新的,看到的人還以為原來的内容是對的。
最後補充下,慶亮同學在知乎上提到的 erlang 遊戲開發的情況:
1. 捷遊的夢幻飛仙線上人數峰值過了 4700(不分線、回合制)
2. 明朝的仙落凡塵線上人數峰值到了 4300(不分線、ARPG)
有興趣的同學不妨看看這篇知乎讨論, 為什麼一些網頁遊戲喜歡用Erlang做服務端
參考:
[1] http://blog.csdn.net/mycwq/article/details/50939354
[2] http://www.kongqingquan.com/archives/221