使用Go 語(yǔ)言開(kāi)發(fā)大型 MMORPG 游戲伺服器怎么樣
主要從事網(wǎng)頁(yè)設(shè)計(jì)、PC網(wǎng)站建設(shè)(電腦版網(wǎng)站建設(shè))、wap網(wǎng)站建設(shè)(手機(jī)版網(wǎng)站建設(shè))、響應(yīng)式網(wǎng)站開(kāi)發(fā)、程序開(kāi)發(fā)、微網(wǎng)站、微信小程序開(kāi)發(fā)等,憑借多年來(lái)在互聯(lián)網(wǎng)的打拼,我們?cè)诨ヂ?lián)網(wǎng)網(wǎng)站建設(shè)行業(yè)積累了豐富的成都做網(wǎng)站、成都網(wǎng)站設(shè)計(jì)、成都外貿(mào)網(wǎng)站建設(shè)、網(wǎng)絡(luò)營(yíng)銷(xiāo)經(jīng)驗(yàn),集策劃、開(kāi)發(fā)、設(shè)計(jì)、營(yíng)銷(xiāo)、管理等多方位專(zhuān)業(yè)化運(yùn)作于一體,具備承接不同規(guī)模與類(lèi)型的建設(shè)項(xiàng)目的能力。
如果是大型網(wǎng)路游戲的話(huà),我覺(jué)得是不合適的?,F(xiàn)階段go語(yǔ)言的執(zhí)行效率還是太低了。在底層編譯器的優(yōu)化方面做得和c++相比還是差了不少。go語(yǔ)言也是比較適合快速開(kāi)發(fā)的專(zhuān)案比較合適
從2013年起,經(jīng)朋友推薦開(kāi)始用Golang編寫(xiě)游戲登陸伺服器, 配合C++做第三方平臺(tái)驗(yàn)證. 到編寫(xiě)?yīng)毩⒐ぞ邔?dǎo)表工具GitHub - davyxu/tabtoy: 跨平臺(tái)的高效能便捷電子表格匯出器. 以及網(wǎng)路庫(kù)GitHub - davyxu/cell: 簡(jiǎn)單,方便,高效的Go語(yǔ)言的游戲伺服器底層. 最終使用這些工具及庫(kù)編寫(xiě)整個(gè)游戲伺服器框架, 我的感受是很不錯(cuò)的
細(xì)節(jié)看來(lái), 有如下的幾個(gè)點(diǎn):
語(yǔ)言, 庫(kù)
Golang語(yǔ)言特性和C很像, 簡(jiǎn)單, 一張A4紙就能寫(xiě)完所有特性. 你想想看, C++到了領(lǐng)悟階段, 也只用那幾個(gè)簡(jiǎn)單特性, 剩下的都是一大堆解決各種記憶體問(wèn)題的技巧. 而Golang一開(kāi)始就簡(jiǎn)單, 何必浪費(fèi)生命去研究那一大堆的奇技淫巧呢?
Golang的坑只有2個(gè):1. interface{}和nil配合使用, 2. for回圈時(shí), 將回圈變數(shù)引入閉包(Golang, Lua, C#閉包變數(shù)捕獲差異) 完全不影響正常使用, 復(fù)合語(yǔ)言概念, 只是看官方后面怎么有效的避免
用Golang就忘記繼承那套東西, 用組合+介面
用Golang伺服器如何保證解決游戲伺服器存檔一致性問(wèn)題? s the world是肯定的, 但是Golang可以從語(yǔ)言層并發(fā)序列化玩家資料, 再通過(guò)后臺(tái)存檔
channel是goroutine雖然是Golang的語(yǔ)言特性. 但是在編寫(xiě)伺服器時(shí), 其實(shí)只有底層用的比較多.
Golang的第三方庫(kù)簡(jiǎn)直多如牛毛, 好的也很多
不要說(shuō)模板了, C#的也不好用, 官方在糾結(jié)也不要加, 使用中, 沒(méi)模板確實(shí)有點(diǎn)不方便. 用interface{}/反射做泛型對(duì)于Golang這種強(qiáng)型別語(yǔ)言來(lái)說(shuō),還是有點(diǎn)打臉
執(zhí)行期
Golang和C++比效能的話(huà), 這是C++的優(yōu)勢(shì), Golang因?yàn)闆](méi)虛擬機(jī)器, 只有薄薄的一層排程層. 因此效能是非常高的, 用一點(diǎn)效能犧牲換開(kāi)發(fā)效率, 妥妥的
1.6版后的GC優(yōu)化的已經(jīng)很好了, 如果你不是高效能,高并發(fā)Web應(yīng)用, 非要找出一堆的優(yōu)化技巧的話(huà). 只用Golang寫(xiě)點(diǎn)游戲伺服器, 那點(diǎn)GC損耗可以忽略不計(jì)
和其他現(xiàn)代語(yǔ)言一樣, 崩潰捕捉是標(biāo)配功能, 我用Golang的伺服器線(xiàn)上跑, 基本沒(méi)碰到過(guò)崩潰情況
熱更新: 官方已經(jīng)有plugin系統(tǒng)的提交, 跨平臺(tái)的. 估計(jì)很快就可以告別手動(dòng)cgo做so熱更新
開(kāi)發(fā), 除錯(cuò), 部署, 優(yōu)化
LiteIDE是我首選的Golang的IDE, 雖然有童鞋說(shuō)B格不高. 但這估計(jì)實(shí)在是找不到缺點(diǎn)說(shuō)了, 別跟我說(shuō)Visual Studio, 那是宇宙級(jí)的...
曾經(jīng)聽(tīng)說(shuō)有人不看好Golang, 我問(wèn)為啥: 說(shuō)這么新的語(yǔ)言, 不好招人,后面打聽(tīng)到他是個(gè)策劃... 好吧
真實(shí)情況是這樣的: Golang對(duì)于有點(diǎn)程式設(shè)計(jì)基礎(chǔ)的新人來(lái)說(shuō), 1周左右可以開(kāi)始貢獻(xiàn)程式碼. 老司機(jī)2~3天.
開(kāi)發(fā)效率還是不錯(cuò)的, 一般大的游戲功能, 2*2人一周3~4個(gè)整完. 這換C++時(shí)代, 大概也就1~2個(gè)還寫(xiě)不完. 對(duì)接伺服器sdk的話(huà), 大概1天接個(gè)10多個(gè)沒(méi)問(wèn)題
Golang自帶效能調(diào)優(yōu)工具, 從記憶體, CPU, 阻塞點(diǎn)等幾個(gè)方面直接出圖進(jìn)行分析, 非常直觀, 可以參考我部落格幾年前的分析: 使用Golang進(jìn)行效能分析(Profiling)
Golang支 *** 叉編譯, 跨平臺(tái)部署, 什么概念? linux是吧? 不問(wèn)你什么版本, 直接windows上編譯輸出一個(gè)elf, 甩到伺服器上開(kāi)跑.不超過(guò)1分鐘時(shí)間..
1.為什么golang的開(kāi)發(fā)效率高?
golang是一編譯型的強(qiáng)型別語(yǔ)言,它在開(kāi)發(fā)上的高效率主要來(lái)自于后發(fā)優(yōu)勢(shì),不用考慮舊有惡心的歷史,又有一個(gè)較高的工程視角。良好的避免了程式設(shè)計(jì)師因?yàn)椤?{ 需不需要獨(dú)占一行 ”這種革命問(wèn)題打架,也解決了一部分趁編譯時(shí)間找產(chǎn)品妹妹搭訕的階級(jí)敵人。
它有自己的包管理機(jī)制,工具鏈成熟,從開(kāi)發(fā)、除錯(cuò)到釋出都很簡(jiǎn)單方便;
有反向介面、defer、coroutine等大量的syntactic sugar;
編譯速度快,因?yàn)槭菑?qiáng)型別語(yǔ)言又有g(shù)c,只要通過(guò)編譯,非業(yè)務(wù)毛病就很少了;
它在語(yǔ)法級(jí)別上支援了goroutine,這是大家說(shuō)到最多的內(nèi)容,這里重點(diǎn)提一下。首先,coroutine并不稀罕,語(yǔ)言并不能超越硬體、作業(yè)系統(tǒng)實(shí)現(xiàn)神乎其神的功能。golang可以做到事情,其他語(yǔ)言也可以做到,譬如c++,在boost庫(kù)里面自己就有的coroutine實(shí)現(xiàn)(當(dāng)然用起來(lái)跟其他boost庫(kù)一樣惡心)。golang做的事情,是把這一套東西的使用過(guò)程簡(jiǎn)化了,并且提供了一套channel的通訊模式,使得程式設(shè)計(jì)師可以忽略諸如死鎖等問(wèn)題。
goroutine的目的是描述并發(fā)程式設(shè)計(jì)模型。并發(fā)與并行不同,它并不需要多核的硬體支援,它不是一種物理執(zhí)行狀態(tài),而是一種程式邏輯流程。它的主要目的不是利用多核提高執(zhí)行效率,而是提供一種更容易理解、不容易出錯(cuò)的語(yǔ)言來(lái)描述問(wèn)題。
實(shí)際上golang預(yù)設(shè)就是執(zhí)行在單OS程序上面的,通過(guò)指定環(huán)境變數(shù)GOMAXPROCS才能轉(zhuǎn)身跑在多OS程序上面。有人提到了網(wǎng)易的pomelo,開(kāi)源本來(lái)是一件很不錯(cuò)的事情,但是基于自己對(duì)callback hell的偏見(jiàn),我一直持有這種態(tài)度:敢用nodejs寫(xiě)大規(guī)模游戲伺服器的人,都是真正的勇士 : ) 。
2、Erlang與Golang的coroutine有啥區(qū)別,coroutine是啥?
coroutine本質(zhì)上是語(yǔ)言開(kāi)發(fā)者自己實(shí)現(xiàn)的、處于user space內(nèi)的執(zhí)行緒,無(wú)論是erlang、還是golang都是這樣。需要解決沒(méi)有時(shí)鐘中斷;碰著阻塞式i\o,整個(gè)程序都會(huì)被作業(yè)系統(tǒng)主動(dòng)掛起;需要自己擁有排程控制能力(放在并行環(huán)境下面還是挺麻煩的一件事)等等問(wèn)題。那為啥要廢老大的勁自己做一套執(zhí)行緒放user space里面呢?
并發(fā)是伺服器語(yǔ)言必須要解決的問(wèn)題;
system space的程序還有執(zhí)行緒排程都太慢了、占用的空間也太大了。
把執(zhí)行緒放到user space的可以避免了陷入system call進(jìn)行上下文切換以及高速緩沖更新,執(zhí)行緒本身以及切換等操作可以做得非常的輕量。這也就是golang這類(lèi)語(yǔ)言反復(fù)提及的超高并發(fā)能力,分分鐘給你開(kāi)上幾千個(gè)執(zhí)行緒不費(fèi)力。
不同的是,golang的并發(fā)排程在i/o等易發(fā)阻塞的時(shí)候才會(huì)發(fā)生,一般是內(nèi)封在庫(kù)函式內(nèi);erlang則更夸張,對(duì)每個(gè)coroutine維持一個(gè)計(jì)數(shù)器,常用語(yǔ)句都會(huì)導(dǎo)致這個(gè)計(jì)數(shù)器進(jìn)行reduction,一旦到點(diǎn),立即切換排程函式。
中斷介入程度的不同,導(dǎo)致erlang看上去擁有了preemptive scheduling的能力,而golang則是cooperative shceduling的。golang一旦寫(xiě)出純計(jì)算死回圈,程序內(nèi)所有會(huì)話(huà)必死無(wú)疑;要有大計(jì)算量少i\o的函式還得自己主動(dòng)叫runtime.Sched()來(lái)進(jìn)行排程切換。
3、golang的執(zhí)行效率怎么樣?
我是相當(dāng)反感所謂的ping\pong式benchmark,執(zhí)行效率需要放到具體的工作環(huán)境下面考慮。
首先,它再快也是快不過(guò)c的,畢竟底下做了那么多工作,又有排程,又有g(shù)c什么的。那為什么在那些benchmark里面,golang、nodejs、erlang的響應(yīng)效率看上去那么優(yōu)秀呢,響應(yīng)快,并發(fā)強(qiáng)?并發(fā)能力強(qiáng)的原因上面已經(jīng)提到了,響應(yīng)快是因?yàn)榇罅糠亲枞絠\o操作出現(xiàn)的原因。這一點(diǎn)c也可以做到,并且能力更強(qiáng),但是得多寫(xiě)不少優(yōu)質(zhì)程式碼。
然后,針對(duì)游戲伺服器這種高實(shí)時(shí)性的執(zhí)行環(huán)境,GC所造成的跳幀問(wèn)題確實(shí)比較麻煩,前面的大神 @達(dá)達(dá) 有比較詳細(xì)的論述和緩解方案,就不累述了 。隨著golang的持續(xù)開(kāi)發(fā),相信應(yīng)該會(huì)有非常大的改進(jìn)。一是遮蔽記憶體操作是現(xiàn)代語(yǔ)言的大勢(shì)所趨,它肯定是需要被實(shí)現(xiàn)的;二是GC演算法已經(jīng)相當(dāng)?shù)某墒?,效率勉勉?qiáng)強(qiáng)過(guò)得去;三是可以通過(guò)incremental的操作來(lái)均攤cpu消耗。
用這一點(diǎn)點(diǎn)效率損失換取一個(gè)更高的生產(chǎn)能力是不是值得呢?我覺(jué)得是值得的,硬體已經(jīng)很便宜了,人生苦短,讓自己的生活更輕松一點(diǎn)吧: )。
4、基于以上的論述,我認(rèn)為采用go進(jìn)行小范圍的MMORPG開(kāi)發(fā)是可行的。
如果跟C語(yǔ)言比,大部分指令碼都勝出啊。Go, Node.js, Python ......
網(wǎng)易弄過(guò)一個(gè)Node.js的開(kāi)源伺服器框架。
至于IDE, 不重要,做伺服器開(kāi)發(fā)很少會(huì)要開(kāi)著IDE除錯(cuò)的。最常用的手段就是打Log. 設(shè)定了斷點(diǎn)也很難調(diào),多個(gè)客戶(hù)端并發(fā)。
那種單客戶(hù)端連線(xiàn)進(jìn)來(lái)就可以重現(xiàn)的bug倒是可以用IDE調(diào),但是這種bug本來(lái)就容易解決。
用指令碼語(yǔ)言,有一個(gè)很大的好處是容易做自動(dòng)測(cè)試,可以更好地保證程式碼質(zhì)量。
--------------------------
開(kāi)發(fā)效率當(dāng)然是指令碼高。執(zhí)行效率,其實(shí)更重要的是并發(fā),框架合理的話(huà)增加機(jī)器就可以直接提高效率增加人數(shù)。
用Go開(kāi)發(fā)大型mmorpg服務(wù)端不會(huì)有問(wèn)題的,如果掉坑里肯定不會(huì)是語(yǔ)言的問(wèn)題。
唯一比較可能掉進(jìn)去的坑就只有GC,其實(shí)很容易預(yù)防和調(diào)整的,具體細(xì)節(jié)可以看我部落格分享的文章。
但是技術(shù)選型不只是選語(yǔ)言,如果當(dāng)時(shí)我手頭有一套效能滿(mǎn)意,開(kāi)發(fā)效率OK,人員補(bǔ)給不會(huì)有問(wèn)題的技術(shù)方案,不管是什么語(yǔ)言的,我肯定不會(huì)放棄它而選擇冒險(xiǎn)的。
public void actionPerformed(ActionEvent e)
{
if(e.getSource()==xinjian)
{
text.setText("");
}
if(e.getSource()==dakai)
{
openFD.show();
String s;
Go和java產(chǎn)生的背景和語(yǔ)言的定義不一樣,比較他們的好壞沒(méi)多大意義,適合就是最好的,當(dāng)你去做一個(gè)大型的項(xiàng)目的時(shí)候,使用java能夠更清晰地展示出你的業(yè)務(wù)。自身提供了比較完善的庫(kù)。
Go語(yǔ)言也有面向函數(shù)和面向?qū)ο蟮淖兂煞桨?,其自身的性能決定了go語(yǔ)言更適合做中間件,底層的各種框架。
語(yǔ)言對(duì)企業(yè)來(lái)說(shuō),是制造生產(chǎn)力的,哪種語(yǔ)言能夠給我們帶來(lái)更好的收益,我們才選擇它們。
一、關(guān)于連接池
一個(gè)數(shù)據(jù)庫(kù)服務(wù)器只擁有有限的資源,并且如果你沒(méi)有充分使用這些資源,你可以通過(guò)使用更多的連接來(lái)提高吞吐量。一旦所有的資源都在使用,那么你就不 能通過(guò)增加更多的連接來(lái)提高吞吐量。事實(shí)上,吞吐量在連接負(fù)載較大時(shí)就開(kāi)始下降了。通??梢酝ㄟ^(guò)限制與可用的資源相匹配的數(shù)據(jù)庫(kù)連接的數(shù)量來(lái)提高延遲和吞 吐量。
如何在Go語(yǔ)言中使用Redis連接池
如果不使用連接池,那么,每次傳輸數(shù)據(jù),我們都需要進(jìn)行創(chuàng)建連接,收發(fā)數(shù)據(jù),關(guān)閉連接。在并發(fā)量不高的場(chǎng)景,基本上不會(huì)有什么問(wèn)題,一旦并發(fā)量上去了,那么,一般就會(huì)遇到下面幾個(gè)常見(jiàn)問(wèn)題:
性能普遍上不去
CPU 大量資源被系統(tǒng)消耗
網(wǎng)絡(luò)一旦抖動(dòng),會(huì)有大量 TIME_WAIT 產(chǎn)生,不得不定期重啟服務(wù)或定期重啟機(jī)器
服務(wù)器工作不穩(wěn)定,QPS 忽高忽低
要想解決這些問(wèn)題,我們就要用到連接池了。連接池的思路很簡(jiǎn)單,在初始化時(shí),創(chuàng)建一定數(shù)量的連接,先把所有長(zhǎng)連接存起來(lái),然后,誰(shuí)需要使用,從這里取走,干完活立馬放回來(lái)。 如果請(qǐng)求數(shù)超出連接池容量,那么就排隊(duì)等待、退化成短連接或者直接丟棄掉。
二、使用連接池遇到的坑
最近在一個(gè)項(xiàng)目中,需要實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 Web Server 提供 Redis 的 HTTP interface,提供 JSON 形式的返回結(jié)果??紤]用 Go 來(lái)實(shí)現(xiàn)。
首先,去看一下 Redis 官方推薦的 Go Redis driver。官方 Star 的項(xiàng)目有兩個(gè):Radix.v2 和 Redigo。經(jīng)過(guò)簡(jiǎn)單的比較后,選擇了更加輕量級(jí)和實(shí)現(xiàn)更加優(yōu)雅的 Radix.v2。
Radix.v2 包是根據(jù)功能劃分成一個(gè)個(gè)的 sub package,每一個(gè) sub package 在一個(gè)獨(dú)立的子目錄中,結(jié)構(gòu)非常清晰。我的項(xiàng)目中會(huì)用到的 sub package 有 redis 和 pool。
由于我想讓這種被 fork 的進(jìn)程最好簡(jiǎn)單點(diǎn),做的事情單一一些,所以,在沒(méi)有深入去看 Radix.v2 的 pool 的實(shí)現(xiàn)之前,我選擇了自己實(shí)現(xiàn)一個(gè) Redis pool。(這里,就不貼代碼了。后來(lái)發(fā)現(xiàn)自己實(shí)現(xiàn)的 Redis pool 與 Radix.v2 實(shí)現(xiàn)的 Redis pool 的原理是一樣的,都是基于 channel 實(shí)現(xiàn)的, 遇到的問(wèn)題也是一樣的。)
不過(guò)在測(cè)試過(guò)程中,發(fā)現(xiàn)了一個(gè)詭異的問(wèn)題。在請(qǐng)求過(guò)程中經(jīng)常會(huì)報(bào) EOF 錯(cuò)誤。而且是概率性出現(xiàn),一會(huì)有問(wèn)題,一會(huì)又好了。通過(guò)反復(fù)的測(cè)試,發(fā)現(xiàn) bug 是有規(guī)律的,當(dāng)程序空閑一會(huì)后,再進(jìn)行連續(xù)請(qǐng)求,會(huì)發(fā)生3次失敗,然后之后的請(qǐng)求都能成功,而我的連接池大小設(shè)置的是3。再進(jìn)一步分析,程序空閑300秒 后,再請(qǐng)求就會(huì)失敗,發(fā)現(xiàn)我的 Redis server 配置了 timeout 300,至此,問(wèn)題就清楚了。是連接超時(shí) Redis server 主動(dòng)斷開(kāi)了連接??蛻?hù)端這邊從一個(gè)超時(shí)的連接請(qǐng)求就會(huì)得到 EOF 錯(cuò)誤。
然后我看了一下 Radix.v2 的 pool 包的源碼,發(fā)現(xiàn)這個(gè)庫(kù)本身并沒(méi)有檢測(cè)壞的連接,并替換為新server{location/pool{content_by_lua_block{localredis=require"resty.redis"localred=redis:new()localok,err=red:connect("127.0.0.1",6379)ifnotokthenngx.say("failedtoconnect:",err)returnendok,err=red:set("hello","world")ifnotokthenreturnendred:set_keepalive(10000,100)}}}
發(fā)現(xiàn)有個(gè) set_keepalive 的方法,查了一下官方文檔,方法的原型是 syntax: ok, err = red:set_keepalive(max_idle_timeout, pool_size) 貌似 max_idle_timeout 這個(gè)參數(shù),就是我們所缺少的東西,然后進(jìn)一步跟蹤源碼,看看里面是怎么保證連接有效的。
function_M.set_keepalive(self,...)localsock=self.sockifnotsockthenreturnnil,"notinitialized"endifself.subscribedthenreturnnil,"subscribedstate"endreturnsock:setkeepalive(...)end
至此,已經(jīng)清楚了,使用了 tcp 的 keepalive 心跳機(jī)制。
于是,通過(guò)與 Radix.v2 的作者一些討論,選擇自己在 redis 這層使用心跳機(jī)制,來(lái)解決這個(gè)問(wèn)題。
四、最后的解決方案
在創(chuàng)建連接池之后,起一個(gè) goroutine,每隔一段 idleTime 發(fā)送一個(gè) PING 到 Redis server。其中,idleTime 略小于 Redis server 的 timeout 配置。連接池初始化部分代碼如下:
p,err:=pool.New("tcp",u.Host,concurrency)errHndlr(err)gofunc(){for{p.Cmd("PING")time.Sleep(idelTime*time.Second)}}()
使用 redis 傳輸數(shù)據(jù)部分代碼如下:
funcredisDo(p*pool.Pool,cmdstring,args...interface{})(reply*redis.Resp,errerror){reply=p.Cmd(cmd,args...)iferr=reply.Err;err!=nil{iferr!=io.EOF{Fatal.Println("redis",cmd,args,"erris",err)}}return}
其中,Radix.v2 連接池內(nèi)部進(jìn)行了連接池內(nèi)連接的獲取和放回,代碼如下:
//Cmdautomaticallygetsoneclientfromthepool,executesthegivencommand//(returningitsresult),andputstheclientbackinthepoolfunc(p*Pool)Cmd(cmdstring,args...interface{})*redis.Resp{c,err:=p.Get()iferr!=nil{returnredis.NewResp(err)}deferp.Put(c)returnc.Cmd(cmd,args...)}
這樣,我們就有了 keepalive 的機(jī)制,不會(huì)出現(xiàn) timeout 的連接了,從 redis 連接池里面取出的連接都是可用的連接了??此坪?jiǎn)單的代碼,卻完美的解決了連接池里面超時(shí)連接的問(wèn)題。同時(shí),就算 Redis server 重啟等情況,也能保證連接自動(dòng)重連。