上周四公司有乙個服務端程式的交流,中間討論到如何有效利用現在伺服器的多核優勢,提高單服的負載。
稍有經驗的網路遊戲服務端開發人員,首先會想說把檔案讀寫等操作慢速裝置的操作獨立出來,變成單獨的執行緒,那麼我們可能會把網路io,以及檔案log獨立出來,用單獨的執行緒處理。
如果乙個遊戲伺服器,僅開了幾個io執行緒,比如說開兩個執行緒,乙個做網路io,乙個做檔案log,再加乙個邏輯執行緒的話,那麼這個程序的負載應該會受限於單個邏輯執行緒的運算量。
但只是這樣的話是遠遠不夠的。現在我們使用的後端遊戲伺服器,動輒都是4核,8核的執行緒,稍好一點的可能都會有16個以上的cpu核心。
上面的做法,至多只能利用到一台機器的三個核心,如果說我們的產品是一些sns 遊戲或是casual game之類的大世界產品,那我們還可以說在同一臺機器上多開幾個相同的遊戲程序,利用並行的方法充分利用cpu資源,那如果說我們做的是mmo arpg之類的產品呢?
對mmo遊戲而言,在同乙個世界(乙個區或是一條線),策劃設計的人數上限應該是整個世界的所有場景和副本,能夠負載的人數之和。理論上如果我們一直擴充套件新的場景,那麼應該是可以支援越來越多的玩家。
但是對程式而言,乙個世界的負載上限應該是會受限於遊戲邏輯執行緒的處理極限,我的經驗是,不管是c++與指令碼的混合程式設計,還是說單獨用c/c++編碼,上限都不太可能突破到三千人以上。這個數字,可能對現在的很多遊戲而言,都不是乙個理想的值。
那麼我們應該怎麼再做改進呢?會上有同事提出,我們可以把邏輯再做拆分,把整個邏輯中比較耗計算的部分,單獨拆成乙個執行緒來處理。比如說我把一些怪物ai,或是aoi之類的操作,單獨擴成乙個執行緒來處理。
理論上這確實可以這麼做,但是我覺得這麼做是不可取的。
主要是因為,當我們把邏輯做這種拆分的話,會需要做非常多的資料共享的處理。那只有兩種辦法,要麼加各種鎖,要麼做各種執行緒間的通訊,我覺得不管哪一種,都會大大加重邏輯程式設計師的負擔。
這樣等於對寫邏輯的程式設計師,提出了更高的要求。再熟練的程式設計師,都不可能在這種情況下,寫出沒有bug的**。這等於是把整個遊戲邏輯,置於乙個非常容易出bug的基礎之上。
再加上現在像我們這種頁遊公司,產品開發的週期越來越短,新手又多,想象一下,就覺得這對開發者來說是乙個惡夢。
我覺得把邏輯計算拆分到多個執行緒是很有必要的,但是最好不要按邏輯功能做這種拆分,而是應該把遊戲邏輯,按場景,或是休閒遊戲的房單,或是sns的session,這種可以相互獨立的邏輯單位,再把這些邏輯單位拆分到各個執行緒中。
以討論最多的mmo arpg為例,我們可以把單個的場景,理解成乙個獨立的邏輯單元,事實上我們大部分的邏輯,都是在獨立場景中完成的,像aoi,戰鬥計算,怪物ai,尋路等等,跨場景的處理非常少,只有像組隊,或是跨場景聊天等少數幾個訊息,當我們把不同的場景處理,放在不同的執行緒中時,這樣的邏輯,可以單獨拿乙個執行緒來處理,做乙個執行緒間的訊息處理即可。這個部分可以用zmq等成熟的第三方庫,也可以自己實現,對稍有c++開發經驗的同學,都不是太複雜。
最後整個程序會變成這樣:
這樣的設計有兩個好處,一是整個程序的負載是可擴充套件的,而且擴充套件起來很容易,當新的場景增加時,我們可以簡單改乙個配置,配到新的場景。當場景不變,當遊戲邏輯內容更多時,我們也可以把原來分在兩個執行緒中的場景,拆分到更多執行緒中來動態擴充套件。
另外乙個就是,這種設計,讓執行緒這間的互動變得非常少,我們可以把乙個場景,理解成乙個單獨的邏輯單元,在這個邏輯單元內,所有的邏輯都不需要考慮多執行緒的操作,不容易出錯,也更容易理解。
要把遊戲程序設計成這樣,有兩個小技巧。
一是所有的邏輯系統,都不應該是單件,因為單件打亂了把場景做成乙個可以自由擴充套件的單元這個設定,會導致各種加鎖和bug。我們需要實現乙個物件管理器,每乙個場景乙個,對於各個邏輯系統,都應該是每個場景單獨乙個物件,場景之間是互相獨立的。這樣邏輯寫起來很方便,也不容易在場景間出現邏輯混亂。
那麼,對場景內的某個系統的引用,會變成這樣
aisystem* pkai = (aisystem*)getobjectmanager()->getobject("aisystem");
更好一點,會變成這樣
aisystem* pkai = getobjectmanager()->getobject("aisystem");
用這種方法來取用場景繫結的單件。
另乙個就是,如果處理新玩家的聯接,以及拋送到邏輯執行緒。我們的做法是,在主線程接受聯接,驗證過玩家在哪個場景後,直接用執行緒間通訊的訊息,把這個玩家物件的指標傳送到對應的執行緒,沒有額外的拷貝和開銷。
至於說幾條邏輯執行緒,如何與io執行緒通迅,我的做法是每個聯接開有乙個單讀單寫的buffer,這個buffer是無鎖的,以保證最快的速度。io執行緒讀出網路層的資料,寫入到這個buffer。這個buffer是和玩家物件繫結在一起的,由具體處理這個玩家邏輯的執行緒來做讀資料和處理。由於每個聯接有乙個buffer,這個buffer又是無鎖的,這種做法,會比多個邏輯執行緒爭用同乙個讀資料佇列更快,也不容易出錯。關於這個部分,之後看能不能單獨開貼說明一下。
當前我們的產品《怪物世界》就是採用了這種設計,只是這是乙個大世界的產品,單線的負載遠沒有達到他的設計極限,現在我們在8核
e5606
cpu的
伺服器上,實際跑到過7千多人一台機,我覺得理論峰值應該能過9千。對乙個戰鬥複雜的mmo來說,我覺得應該算不錯的了。
網路遊戲 伺服器
using system using system.collections.generic using system.linq using system.text using system.threading.tasks using system.net.sockets using system.n...
遊戲服務端開發 一
資料儲存伺服器 遊戲中的資料大致分為靜態配置資料和動態的玩家資料。這裡主要討論玩家資料儲存的解決方案。雖然遊戲應用的寫操作要多於讀操作,但是加入快取層仍然有其必要性。多個應用伺服器啟動時從資料庫讀取資料會在瞬間給資料庫造成巨大壓力,如果將相對靜態的資料以檔案的形式放在應用伺服器本地,可以避免這個問題...
遊戲服務端開發 二
應用伺服器的設計 上 應用伺服器的工作有 0 同步廣播玩家的行為 1作為第三方對玩家個體和玩家之間互動行為計算,並將計算結果推送到資料儲存系統 2驅動遊戲中的 npc 3作為乙個特殊的遊戲參與者,與玩家相互作用。應用伺服器最重要的工作莫過於同步廣播玩家之間的行為,使玩家之間能夠互視,多人同時遊戲才有...