1. 程式人生 > >遊戲伺服器開發的一些經驗

遊戲伺服器開發的一些經驗



四年前, 我進入現在這家公司, 之後我一直在做一款網頁遊戲的伺服器開發. 前不久, 我調到了另一個專案. 趁這個機會, 我把這幾年的開發和維護經驗做一下總結. 首先說一下專案的情況. 為了避嫌, 專案名字我就不說了, 專案是一款模擬經營類的網頁遊戲, 使用者量很大. 目前總使用者數超過兩億. 日活躍使用者上千萬, 同時線上百萬左右. 月流水七八百萬. 我在專案裡一直從事伺服器端開發, 因為我們沒有專門的技術人員做運維, 所以這部分工作也由我負責. 四年下來, 也有了一些心得. 下面我從兩方面來談一談. 首先是我們在專案中使用的一些技術的分析, 然後是對如何寫好程式碼的一些體會. 遊戲簡介
客戶端和伺服器使用長連線. 遊戲沒有分割槽的概念, 對玩家來說, 遊戲只有一個區, 所有玩家相互都可以進行互動. 玩家間的互動並不十分頻繁, 不存在mmorpg裡需要廣播同屏玩家資訊的情況. 客戶端向伺服器請求資料, 伺服器一般不會主動向客戶端傳送資料. 系統架構 伺服器是一個分散式系統, 整個系統由若干個獨立的伺服器組成. 每個伺服器是一個程序, 每個程序都可以根據需要部署在相同或不同的物理機上. 程序間用 socket 通訊. 所有的程序都在一個區域網中. 整個系統有以下幾種伺服器組成(每種伺服器都有若干臺), 1. 閘道器伺服器, 2. 邏輯伺服器. 3. 連線伺服器, 4. 功能伺服器, 5. 資料庫伺服器. 下面逐一介紹. 1. 閘道器伺服器 閘道器伺服器, 客戶端和閘道器伺服器連線, 對每一個連線的玩家, 閘道器伺服器分別向遊戲伺服器建立一條獨立的連線. 閘道器伺服器沒有任何邏輯處理, 它只負責建立連線和轉發訊息. 閘道器伺服器在搭建完成後, 基本不需要在做任何變化. 閘道器伺服器有這幾個作用: * 為遊戲伺服器組提供一個統一併且安全的介面, 供客戶端使用.  * 根據遊戲負載, 遊戲伺服器的數量會增加或減少, 閘道器伺服器向客戶端隔離了這個變化. 2. 邏輯伺服器 顧名思義, 邏輯伺服器處理所有的玩家邏輯. 玩家通過一個簡單的 hash 演算法, uid % logicServerNum, 分配到某一臺邏輯伺服器上. 這臺伺服器載入玩家資料, 操作它們, 然後寫入資料庫. 邏輯伺服器是多執行緒的, 包括網路執行緒, 資料庫讀寫執行緒, 邏輯執行緒等等. 邏輯執行緒負責處理玩家傳送的所有訊息. 為了降低複雜度, 邏輯執行緒只有一個.  3. 連線伺服器 假設玩家A和玩家B被分配到不同的邏輯伺服器上, A要和B有互動, 就需要用到連線伺服器. A把訊息傳送到連線伺服器, 連線伺服器把訊息傳送到B所在的邏輯伺服器. B邏輯伺服器處理完畢, 將訊息返回給連線伺服器, 連線伺服器在把訊息發給A邏輯伺服器. 4. 功能伺服器 功能伺服器處理一些特殊功能, 這些功能通常需要用到全域性資料. 比如排行榜功能和公會功能. 5. 資料庫伺服器 一個數據庫伺服器實際上就是一個 ttserver 程序. ttserver是一個key-value資料庫. 可以根據啟動引數把資料儲存在記憶體, 或是硬碟. 資料庫伺服器根據使用方式的不同, 分成這幾種: * 記憶體資料庫, 只把資料儲存在記憶體, 資料條目是有限的, 條目到達上限後, 老的資料會被刪除. * 物理資料庫, 直接讀寫硬碟. 邏輯伺服器讀資料時, 先從記憶體資料庫讀, 如果沒有, 再從物理資料庫讀. 寫資料時, 先寫入記憶體資料庫, 再寫入物理資料庫. 記憶體資料庫的作用就是減少讀寫資料庫的時間. 網路
除了資料庫伺服器外, 其它的伺服器的網路層都是一樣的. 使用 epoll 做多路複用. 為每個socket fd 維護一個讀快取和一個寫快取.  邏輯執行緒向玩家傳送資料時, 把資料放到寫快取. 讀快取裡的資料夠一條完整的訊息後, 取出這條訊息, 放入訊息佇列.  訊息處理 用一個訊息佇列來作為網路執行緒和邏輯執行緒的溝通. 往這個佇列裡pop和push訊息時, 需要加鎖. 邏輯執行緒在一個迴圈裡中訊息佇列裡pop出訊息, 處理它. --------------------------------------------------------------------------------------- 以上簡單介紹了伺服器的架構. 下面說說最近對寫程式碼的一些體會. 避免動態記憶體
雖然我們使用 tcmalloc 來做記憶體分配, 還是應該儘可能的避免動態申請記憶體. 一方面可以提高程式碼的執行速度, 更重要的是可以減少bug的產生. 一個比較好的方法是為某些物件建立記憶體池.  慎重修改別人的程式碼 程式設計師寫下的每行程式碼都有他的理由, 如果沒有充分理解別人的意圖, 絕不要修改別人的程式碼.  重構, 再重構 開發常常是迭代進行的: 先寫出大概的框架, 再一遍一遍的逐步完善. 在迭代的過程中, 如果原來的設計有問題, 不能用 "打補丁" 的方式讓程式正常工作, 應該重新設計. 只有這樣, 才能避免系統出現 "壞味道". 最終完成一個簡潔一致的設計. 一次又一次的檢查自己的程式碼 測試能夠發現的問題是有限的. 要想在功能上線之後能夠正確的執行, 做程式碼稽核是必須的, 如果可能的話, 同事之間互相來做稽核. 如果只能自己稽核自己的程式碼的話, 有一個提高效率的小技巧:  自己寫的程式碼思路記得越清楚, 越難看出問題. 程式碼寫好後, 過幾天再稽核, 思路淡了, 往往更能發現問題. 不要只稽核一次就完事了. 如果有時間, 看第二次, 第三次. 慎用技巧 技巧是有副作用的, 最大的副作用就是增加的程式的複雜度, 複雜度越高的設計一定越容易出現問題, 同時以後維護起來也更難. 在效率要求不是那麼高的時候, 簡單粗暴的設計往往更好. 先寫出一個簡單的設計, 再根據要求進行優化. 簡單的設計更容易實現的正確. 把一個正確的系統修改得更快, 比把一個有問題的系統修改正確要容易得多. 消滅重複的程式碼 重複的程式碼應該用函式封裝起來. 相似但是有細微不同的邏輯, 是有問題的, 要想辦法重構. 在同一個工程裡 複製 貼上 程式碼, 是在作踐自己的職業. 系統運維 運維事故是大大的不應該, 它們完全是可以避免的. 因為它們絕大多數是由於粗心導致的. 好的運維流程應該是按部就班的進行. 使用提前設計好的指令碼. 運維指令碼可以用 shell 和 expect 來編寫. 應該提前考慮好需要的指令碼, 早早的準備好. 編寫功能之外的輔助程式碼 完成一個功能後, 要考慮如何在功能上線後, 監控它是否正常執行, 如果出現bug, 如何關閉功能, 如何修復錯誤的資料, 如何重新上線... 為這些情況編寫功能之外的輔助程式碼. 化繁為簡, 分而治之 程式設計其實就是把現實問題抽象成邏輯模型, 再用計算機語言來實現這個模型. 這個模型應該是簡單而直觀的, 它的邊界條件和隱藏規則越少越好. 它可能又若干個模組組成,  模組之間必須是低耦合的. 每個模組只完成一個簡單的任務, 這個任務應該能用一兩句話描述出來.