如何使用 Go 語言寫遊戲伺服器?
之前先後用Erlang,nodejs做過tcp,http的遊戲伺服器。接觸了golang一兩個月(純新手),想在最近的tcp網遊項目中使用,但又擔心以下問題:
# 如何高性能的搭建tcp底層,並且能負載到同時在線N多人# 如何架構整個伺服器端(包括網路層,緩存層,持久化層,日誌層,邏輯分發處理層,通信協議層,以及如何有效部署)# goroutine間如何高效通信# 擔心go1.5版本及以後的gc問題
# 如何調試程序和快速定位線上問題# 壓力測試負載能力希望用過golang的前輩給出一些建議~~
謝邀,昨天就有看到這問題,因為題太大不知道從何答起,所以就沒答,今天易偉前輩邀請,不答不行了。
真有趣團隊是從Go 1.0開始使用Go開發遊戲服務端的,所以小經驗有點,但是我們還處在不斷學習摸索的階段,所以太高深的學問不多,下面我就按題主的問題順序嘗試一個個的回答吧:
# 如何高性能的搭建tcp底層,並且能負載到同時在線N多人
Go自身在特定平台會使用對應平台的io重用方案,比如epoll,kqueue等,所以底層部分效率已經不錯了,比起自己用C/C++去封裝底層或調用libevent之類的庫,優勢是Go將事件機制封裝成了CSP模式,編程變得方便了,但是需要付出goroutine調度的開銷,在遊戲項目上實踐的經驗是調度開銷可接受,無需做額外工作來優化。如果自己要在Go裡面調用epoll重新封裝一個網路層,這樣做所能提升的效率和付出的代價對比起來,性價比太低了,不值得這麼做。
所以用Go搭建TCP底層是很省事的,主要關注幾個點:
1. 盡量減少系統的IO調用次數,比如使用bufio這個包來減少實際IO次數
2. 盡量減少不必要的數據拷貝,比如消息的封包解包過程,細心點設計是可以做到極少的數據拷貝的3. IO阻塞時的邊界情況處理,比如一個請求處理過程中,如果消息回發導致處理過程阻塞,是否會影響到其他後續請求,又或者廣播過程中消息發送阻塞,是否應該把阻塞的連接關閉等我這有個簡單的庫可以提供參考:funny/link · GitHub
# 如何架構整個伺服器端(包括網路層,緩存層,持久化層,日誌層,邏輯分發處理層,通信協議層,以及如何有效部署)
這個議題挺大的,但是題主已經明確羅列出了這些項目層級和模塊劃分,說明是已經有經驗的了。Go語言跟其他語言一樣分層分模塊,沒太大特別之處。
Go在組織遊戲項目的時候有一點需要提前預防,就是業務模塊間的遞歸引用。Go從語法上是禁止包遞歸引用的。但是遊戲的業務模塊很多,交叉是很平常的事情,所以需要提前設計一個項目結構來防止業務上的交叉碰到Go的語法限制。
具體的代碼我就不寫在這邊了,思路就是通過公共介面的註冊來解耦包的引用關係,我這邊有個演示項目可以參考一下,並沒有什麼高深的設計在裡面,就是一個腦筋急轉彎而已。funny/go-project-demo · GitHub
緩存層、持久層的實現方式不同團隊差異巨大,這邊我能分享的經驗只有一點,就是盡量不要人工去維護緩存和持久化之間的關係,盡量做成自動的,這樣才不會人工引發BUG導致數據損壞。
如果要說具體說法,我們目前是MySQL做持久存儲,這樣做數據分析和備份什麼的都比較方便也比較可靠。緩存則是根據MySQL的結構自動生成代碼映射到Go裡邊的。
Go做大數據量的緩存的時候需要小心GC的負載,如果你的緩存設計是內存吃得多但是對象很少,就不用擔心這一點。如果是像我一樣一條MySQL數據對應一個Go對象到內存里的,就要小心處理,要嘛做成按需載入的,減少對象數量。要嘛就是乾脆用堆外內存來存儲緩存數據,這樣GC不會有負擔。堆外內存的一些技巧我之前網上也有分享過了,原理比較簡單,就是用cgo機制讓C來分配內存。
通訊層也是各個項目差異很大的部分,我們團隊是自己實現一套二進位協議格式,也有團隊是用protobuf,也有用json,各式各樣都有。這個按個人喜好和傳統來做就可以了,差異不會差到哪裡去的。
如果做自定義格式的協議,我這有個二進位操作的庫可以用用:funny/binary · GitHub
部署方面其實跟語言無關,單進程的結構都很好運維和部署,多進程都會麻煩一些,所有語言都一樣的,這方面我沒有太值得分享的經驗。
# goroutine間如何高效通信
goroutine就是靠chan通訊了,沒什麼好辦法。如果關心goroutine通訊的各種開銷,最好是按自己的應用場景測試看看。
有些場景下chan通訊是不划算的,比如一個簡單的map數據獲取,可能用鎖就可以了。有些場景用chan是必須的,比如做個多人互動功能。
還有就是帶buffer的chan和不帶buffer的chan的差異,最好通過試驗來讓自己有個直觀認識,除了非同步和同步的差異,還會有邊界情況的處理差異,比如帶buffer的chan阻塞了,在功能設計上需要考慮,否則可能引發嚴重問題,這個上面其實也講了。
# 擔心go1.5版本及以後的gc問題
如果有這個擔心,就最好從項目初期就提前預防,比如從設計上就避免產生大量對象,或者就是前面說的堆外內存分配,或者是通過多進程結構來分散負擔。
還有就是提前做好測試,對量級有個心理預期。
遊戲已經比實時交易系統好很多了,正常的用Go是不用擔心GC延遲導致服務質量不符合需求的,遊戲會產生大量對象的地方就是緩存了,這個地方小心設計基本上就沒什麼問題了。
1.5版本的GC我還沒測試過,因為用了堆外內存,現在不怎麼關心這個了。。。
# 如何調試程序和快速定位線上問題
調試Go確實有點麻煩,如果要用GDB調試Go,你最好關掉Go的編譯優化,否則可能出現調試不了的情況。另外就是靠列印了,所以我們項目裡面有這樣一個模塊:funny/debug · GitHub
線上問題定位要靠提前留好定位措施來實現,最常用的就是排查死鎖和排查內存泄漏,可以參考一下這個模塊:funny/pprof · GitHub
死鎖的時候通過lookup goroutine來獲取所有goroutine的堆棧跟蹤信息,然後排查死鎖的原因。
內存泄漏或者效率問題通過cpuprof和memprof來定位問題:Go語言程序的狀態監控
保存cpuprof和memprof的工具函數在 funny/pprof 包里也有。
# 壓力測試負載能力
遊戲的完整壓力測試我沒做過,感覺沒法做,遊戲操作邏輯太複雜了。所以我的測試方式是對逐個可能成為瓶頸的點做benchmark或對演算法做benchmark,來估計一個整體的效果。
另外就是開發期間持續監控所有請求的響應時間,我們團隊的要求是在小於1毫秒,實際線上平均是30多微秒(不包含IO過程),有這樣的響應速度,應該不用擔心負載問題,如果有負載問題,會在請求執行時間上暴露出來。
用來監控請求執行時間的模塊也在這個包里:funny/pprof · GitHub
能力範圍內只能回答這些了,我最近在研究怎麼進一步提高開發和運維的整體效率,所以感覺自己還很多東西不懂,懂的只是一些皮毛的東西,當拋磚引玉了。與go無關,java的。但作為遊戲伺服器框架可參考,原完美伺服器老大的最新力作。http://www.limax-project.org/
https://github.com/xtaci/gonet打開github,輸入關鍵字game server + golang,其他地址不貼了。
沒做過,幫你邀請了 @達達
剛翻郵件在OSChina的郵件列表看到這個:name5566/leaf · GitHub推薦閱讀:
※對於很多發燒級PC硬體玩家來說,伺服器cpu、recc內存和RAID陣列等等伺服器上的硬體或者技術真的有必要麼?
※為何多個 http 的 80 埠的站點可以共用同一個伺服器(IP),而 https 卻不行?
※如何寫一個web伺服器?
※Web 伺服器與應用伺服器的區別是什麼?
※如果伺服器所在的內網進行物理隔離(斷開互聯網連接),黑客有辦法突破並竊取到其中的信息嗎?