使用 Go 語言開發遊戲服務端的是如何忍受無法熱更新的?

Go 語言不能熱更新,真的適合用在遊戲服務端嗎。看很多同學在用 Go 語言寫遊戲服務端,本人目前項目用 Python + 少量 C,無法想像不能熱更新的遊戲要怎麼運營。也有看到有人用 Go 結合腳本的,但是大部分邏輯代碼應該還是 Go 語言吧,你無法判斷一段代碼什麼時候會出問題需要 reload 的,也就不好在開發一個功能的時候就決定使用腳本還是 Go。


當初從erlang切換到go,最難適應的就是沒熱更新。緊急修復BUG,在線修數據,等等,都是很救命的。

再加上我們的服務端設計是整個資料庫載入進內存的,重啟需要較長時間,所以就特別擔心頻繁重啟對遊戲會很傷。

後來產品上線了感覺還好,沒有熱更新的確沒那麼方便,但是也沒那麼可怕。

原因:

1. 需要臨時重啟更新就運營公告,如果實際較長就適當發放補償。

2. Go載入數據到內存的速度也比之前快很多,重啟壓力也沒想像的那麼大。

3. 強類型語法在編譯器提前排除了很多之前要到線上運行時才能發現的問題,所以BUG率低了。

所以沒有熱更新也順利跑下來了。

不過以上只能做為參考,不同項目需求不一樣,還是得結合實際情況來判斷。

熱更新肯定是可以做的,方案挺多,數據驅動、內嵌腳本或者無狀態進程都可行,只是花多大代價換多少回報的問題。

如果評估下來覺得熱更新必做不可,那麼用再大代價也得做,這是項目存亡問題。

如果不是必須的,那就需要評估性價比了。

做熱更新、換編程語言或者換服務端架構所花的代價,換來的產品在運營、運維或開發各方面的效率提升,是否划算。


=== 2016-09-17 ===

現在已經支持plugin模式,純go的動態載入方式:plugin: new package for loading plugins · golang/go@0cbb12f · GitHub

=== 原答案 ===

1.5 支持編譯成so,所以可以像C一樣用dlopen做hot reload。舉個例子

主程序:

package main

/*
#include & #cgo LDFLAGS: -ldl

void (*foo)(int);

void set_foo(void* fn) {
foo = fn;
}

void call_foo(int i) {
foo(i);
}
*/
import "C"
import "fmt"

func main() {
n := 0
var bar string
for {
hd := C.dlopen(C.CString(fmt.Sprintf("./foo-%d.so", n)), C.RTLD_LAZY)
if hd == nil {
panic("dlopen")
}
foo := C.dlsym(hd, C.CString("foo"))
if foo == nil {
panic("dlsym")
}
fmt.Printf("%v
", foo)
C.set_foo(foo)
C.call_foo(42)
fmt.Scanf("%s", bar)
n++
}
}

因為go編譯出的so不能dlclose(這裡說了原因:runtime: dlclose in shared library causes segmentation fault · Issue #11100 · golang/go · GitHub),所以每次只能load不同的,所以內存和線程會泄漏一些。

另外由於cgo的限制,不使用set_foo和call_foo似乎不行,所以會引入cgo調用的開銷。

所以如果不在意上面這些缺陷,可以試試。

so源碼:

package main

import "fmt"
import "C"

func main() {
}

//export foo
func foo(i C.int) {
fmt.Printf("%d-2
", i)
}

用 go build -buildmode=c-shared -o foo-1.so mod.go 編譯。需要1.5的編譯器,8月初會出正式版。

這是藉助C的機制來實現的,go的execution modes文檔提到會有go原生的plugin模式,就不需要藉助C的了,不過什麼時候實現也未定。


有一種做法是可以將服務端微服務化;這樣每個服務的重啟成本很低,像達達說的reload資料庫到內存的時間成本就會更低。

------分割------

另外為服務化後,可以針對不同的服務是否有必要熱更新,結合腳本或其它方法實現(如:遊戲運維的活動服務需要頻繁變更)。而像一些基本的如用戶,遊戲邏輯等介面設計靈活一點的情況下;是完全沒必要熱更新的;每次版本變更停服重啟就ok。


2017.12.8更新

讓Go伺服器做到無狀態,和http伺服器一樣,就可以隨時重啟,這是熱更新的最佳方案

2017.7.6更新

1.9已經beta, 8月將更新, 早在1.8時, 已經支持plugin方式, 但是這種方式不完全用於熱更新, 而且有很多限制. 詳細的研究結論, 請參考我的博文

Golang 熱更新研究筆記 - 戰魂小築 - C++博客

==================================================================

謝邀

Golang和熱更新

1. Golang 1.4還不支持編譯為動態鏈接庫和把自己作為動態鏈接庫進行載入

因此要通過動態鏈接庫方式進行熱更新是行不通的

2. Golang是通過編譯為native code外加自己runtime配合跑起來的. 更接近JIT.

所以, 想要熱更新的話, 我估計底層和設計改動會很大

Golang現在還不支持把自己的語言當成腳本進行嵌入式運行.

因此, 想做類似lua那種腳本方式, 使用自己當做腳本的運行也不行

3. Lua+Golang, 這種方式其實只是把Golang當成一個網路庫來用, 邏輯全用Lua來寫. 跟客戶端里的Unity3D+Lua有異曲同工之妙. 但不得不說, 這種做法還不如用skynet來的舒服點, 人家本來就設計有熱更新, 何苦自己造輪子. 長遠點說, Unity3D+Lua做客戶端熱更新現在已經風險很大了, 而且渠道也慢慢接受了測試+大更新的方式了. 因此Lua來解決熱更新的方案遲早會變成廢技術


go不能熱更新是開發遊戲伺服器的硬傷。從技術角度來說熱更新不是必須的。但是從產品角度來說,每一次停機維護都是對在線數的一次巨大傷害。

目前的大部分的手游都會做斷網自動重連,因此在停機10秒之內影響不明顯。但是如果超過10秒,從監控曲線看會掉一截,而再回到峰值是需要一段時間的。人數下降,收入必然也受到影響。

總的來說停機維護代價是很大的。尤其是遊戲剛上線特別容易出bug,停機維護的頻率高對第一批用戶的傷害是巨大的。

我帶過的3個遊戲項目除了第一個是c++,後面的都是以lua為主語言。最新的項目是erlang+lua的方式,無論是底層、協議、配置還是邏輯都支持熱更新。保守估計停機的機率降低90%。

目前也在考慮用go開發,但是熱更新必須要有一個方案,不能接受任何小bug的修改都要停機維護。可行的思路只有盡量把服務拆細,單獨更新各個服務。


熱更新可以有很多實現方式,比較經典的就是像nginx那樣,啟動多一個master進程,然後通過環境變數記錄和接管old master的listen socket;新連接由新的worker進程(代表新的binary)接受,而舊連接由舊的worker進程繼續處理流量;直到舊連接都斷開後,則可以關掉舊的進程。

可以看到,兩套新舊進程都是在一個機器上運行,並存一段時間。

這個跟語言沒有太大關係,大部分語言也很少有完全自我替換的功能,例如C語言沒有內置的熱更新,那nginx還不一樣能做到。整個熱更新過程只需要一些操作系統級別的技巧:進程和信號。

例如上面有朋友提及的:

GitHub - rcrowley/goagain: Zero-downtime restarts in Go

也能做到近似nginx的熱更新效果。

我說一個變通的實現方法,不需要用到以上技巧:一般的項目部署都會用負載均衡器,例如lvs,其實可以增加一個新的rs,讓新的進程綁定一個新的偵聽埠,然後將舊rs的權重置為零,等到舊rs的連接都斷開後,則關掉舊進程,刪掉舊rs,這個效果跟熱更新是一樣的。


個人感覺客戶端的熱更新要比伺服器的熱更重要,從手游這個領域來看,伺服器相比客戶端會穩定一些,我們遊戲內測了三次:第一次內測,伺服器因為Bug或策劃數據表更新而熱更了好幾次,後面兩次慢慢減少;而客戶端呢,基本上每次測試的第一天,都是要打十幾次Patch的。

個人感覺伺服器不支持熱更,可能真的沒想像的那麼嚴重,伺服器需要更新,不外乎下面幾種情況:

1. 策劃數據表的更新,這個本來就應該做好數據重載入的支持,如果因為這個需要重啟則說明這塊沒有做好。另外,系統功能要儘可能參數化,讓策划去配表而不是寫死程序中。

2. 代碼出BUG,重啟後看情況運營補償,最好是前期做好測試,別出一些可以刷的漏洞。(當然這塊有熱是會方便很多)

3. 策劃新製作的系統,這個要求伺服器重啟也不過份吧。

4. 運營活動,基本上也是新功能,客戶端必然也是要相應更新的,伺服器重啟似乎也不過份吧?

所以我想熱更新更多的應該是解決一些小問題,大功能應該不大可能通過熱更可以完成。


鵝廠自研用C++寫後台,很多場景也是熱更的。能否熱更看你基礎架構如何,而語言只是基礎架構的一部分


(⊙o⊙)…golang可以做熱更新啊

rcrowley/goagain · GitHub

facebookgo/grace · GitHub


其實最簡單的就是開一個新服務,老服務的負載逐步轉過去,到了合適的時候,關閉老服務,雖然有不少限制,但不依賴語言


我們也是golang開發的 不支持熱更新 只要伺服器能夠快速重啟 客服端支持自動重連不就行了 我們都線上運營兩年多了 單月流水也過億 不是正常運營嘛


快速重啟,客戶端做好重連機制


c++可以熱更?那麼多遊戲都用c++做服務端人家怎麼運營的?還是你遊戲問題太多?


誰說不可以呀,親測有效

fvbock/endless


用nginx做一個反向代理和負載均衡隨便做熱更新,方法總困難多啊


就算用lua,伺服器到底怎麼熱更新嘛,原來沒接觸的時候老聽說更新多方便,後來一用才發現,還是一樣的啊,用lua照樣很多狀態數據存在lua上面,怎麼說更新就更新嘛。手機遊戲客戶端不過是可以把腳本當做資源更新不需要打包發布而已,還是要客戶端重啟。網上查了,lua伺服器熱更新,方法都很詭異,要注意這注意那,模塊設置為空,重新load模塊,內部發送命令等等,那伺服器用lua這算哪門子方便嘛,其他語言不都一樣的


如果把熱更新轉成進程間通信的問題去處理可以吧?與客戶端連接的進程不要關閉,把邏輯轉移到其它進程處理就好了,這樣可以分批更新邏輯處理進程,保證服務可用


推薦閱讀:

阿里雲工程師解決不了IO問題?
為什麼手游伺服器一般將登陸伺服器和遊戲伺服器分開?
Linux 下多線程和多進程程序的優缺點,各自適合什麼樣的業務場景?
如何使用 Go 語言寫遊戲伺服器?
對於很多發燒級PC硬體玩家來說,伺服器cpu、recc內存和RAID陣列等等伺服器上的硬體或者技術真的有必要麼?

TAG:編程 | 伺服器 | Go語言 |