最近整理了過(guò)去一年發(fā)生過(guò)的bug,包含跟其他項(xiàng)目組程序朋友交流的例子, 都是大家發(fā)生過(guò)的真實(shí)營(yíng)運(yùn)事故。
游戲服務(wù)器程序,很多bug的原因都是共通的。抽象出了以下10點(diǎn)啟示, 作為checklist, 寫(xiě)下來(lái)以后寫(xiě)程序review時(shí)自檢:
1. 安全邊界問(wèn)題 對(duì)于有界限的東西(數(shù)值,buffer空間,隊(duì)列或一切對(duì)象容器),一定要考慮越界判斷。
啟示:用snprint, strncpy等限制長(zhǎng)度. 永遠(yuǎn)都要考慮超過(guò)邊界的情況
數(shù)值加法和乘法:考慮上限溢出;
減法:考慮負(fù)數(shù); 除法,判斷分母
2. 輸入?yún)?shù)非法
case1: 扣錢(qián)邏輯,減去一個(gè)負(fù)數(shù),變成了加錢(qián)。
case2: int型大負(fù)數(shù)相加,負(fù)溢出變成大正數(shù)
啟示:test case要全覆蓋輸入?yún)?shù)范圍, 處理各種可能的情況
3. 上下文改變錯(cuò)誤
共享變量/全局變量被外部改變,這似乎很常見(jiàn),而且有時(shí)很隱蔽。在異步回調(diào)的情況下更常見(jiàn)。
check A變量
call func_B()
....
A變量被func_B改變了, 但繼續(xù)信任A變量check的結(jié)果。
啟示:白盒復(fù)查代碼時(shí),注意檢查調(diào)用后的變化。
減少共享變量和全局變量的使用
外部接口調(diào)用后,注意共享變量的更新和恢復(fù)
啟示:在最接近執(zhí)行的地方,檢查上下文變量。不信任調(diào)用者,如果效率不關(guān)鍵,多一遍冗余檢查沒(méi)有壞處
4. 執(zhí)行中斷
動(dòng)態(tài)腳本拋異常,或者引擎層面的EINTR中斷信號(hào),都有可能中斷代碼執(zhí)行,需要考慮函數(shù)的重入性問(wèn)題。
啟示:要檢查一致性,有些邏輯不允許多次被執(zhí)行(比如發(fā)獎(jiǎng)勵(lì)),需要有狀態(tài)變量確保只執(zhí)行1次(避免出刷bug)
推廣到異步環(huán)境(多線程,多進(jìn)程,各種回調(diào)),事務(wù)的中斷也有一個(gè)重入性問(wèn)題,解決方法也只有一個(gè):用一個(gè)唯一可辨認(rèn)的狀態(tài)變量,保證某些邏輯不會(huì)被多次執(zhí)行(比如購(gòu)物應(yīng)用中,用唯一訂單號(hào)來(lái)識(shí)別,狀態(tài)改變是一次性的,當(dāng)邏輯運(yùn)行多次,也不會(huì)重復(fù)加物品,或者重復(fù)扣錢(qián)了)
5. 終止條件問(wèn)題--死循環(huán)
case: 異步環(huán)境中,RPC遠(yuǎn)程調(diào)用,調(diào)用成環(huán),邏輯一直不結(jié)束。
啟示:while或遞歸的終止條件,邏輯全覆蓋檢查,避免死循環(huán)。較深層次的互相調(diào)用,要注意是否出現(xiàn)了遞歸,是否有可能死循環(huán)。
6. 關(guān)聯(lián)數(shù)據(jù)操作的不一致
例子:Employee對(duì)象有company變量, Company中有employee變量,
如果操作改變其中一方,而另一方?jīng)]有改變,則造成數(shù)據(jù)不一致。
(數(shù)據(jù)庫(kù)表可以指定constrain, 關(guān)聯(lián)表刪除, 但代碼變量中需要程序員自己實(shí)現(xiàn))
雙向引用的數(shù)據(jù)一致性問(wèn)題,要特別注意。
為什么要雙向引用?為了查找效率,而避免遍歷其中一方.
這個(gè)問(wèn)題本質(zhì)是數(shù)據(jù)一致性問(wèn)題,編程中遇到的很多bug也歸結(jié)到這個(gè)問(wèn)題,比如野指針,就是因?yàn)閿?shù)據(jù)結(jié)構(gòu)相互引用的操作不一致造成的。
處理這個(gè)問(wèn)題,個(gè)人經(jīng)驗(yàn)是,他們的attach,detach操作盡可能在同一個(gè)模塊,不要分散在多個(gè)地方隨意修改,所有修改都集中在同一級(jí)接口做。
同理適用于new, delete, malloc, free這些分配,釋放,都集中在同一層的接口/模塊文件中做,debug起來(lái)也容易;非常反感在一個(gè)地方new, 然后不知道哪個(gè)模塊去delete, 很容易泄漏或者野指針, 無(wú)論如何,想辦法傳遞這些指針,一直傳到分配他所在的模塊文件中釋放,而且new和delete的接口代碼要靠近,方便查找問(wèn)題。
7. 涉及多玩家,防止筆誤傳錯(cuò)參數(shù)
經(jīng)典錯(cuò)誤: foreach(uid in team) some_func(usernum, xxx)
經(jīng)典錯(cuò)誤:有usernum和target兩個(gè)對(duì)象,調(diào)用函數(shù)搞混了。review時(shí)要仔細(xì)檢查
8. 特殊分支忘了return
異常判斷等if分支忘了return。導(dǎo)致邏輯繼續(xù)往下走。這屬于筆誤問(wèn)題,測(cè)試期間未必能留意的到。
9. 異步返回沒(méi)清變量
對(duì)于異步操作,如果在返回時(shí)清變量,這時(shí)如果不能保證把變量清掉(比如期間玩家下線無(wú)法離線修改該變量),就會(huì)出刷。
啟示:對(duì)于已獎(jiǎng)勵(lì)標(biāo)記,一定要保證各種情況下領(lǐng)獎(jiǎng)后能正確記錄。
10. 瞬爆容量上限
case1: 網(wǎng)絡(luò)待發(fā)送隊(duì)列,因?yàn)樗查g大量請(qǐng)求,塞滿拋異常,導(dǎo)致流程受影響。
case2: 大量連接請(qǐng)求,listen的accept沒(méi)有規(guī)定單次讀事件的accept,用了while(true), 導(dǎo)致爆機(jī)
在listen fd的讀事件回調(diào)中, 通常會(huì)accept所有新的連接請(qǐng)求,如果用while(true)而不設(shè)一個(gè)上限,就有可能被攻擊(想象一下客戶端也用一個(gè)死循環(huán)來(lái)做connect)。
一方面要限制單次接受的socket次數(shù), 另外各個(gè)狀態(tài)要有超時(shí)機(jī)制,踢掉不尋常的連接,以防被攻擊占盡資源。
case3: 異步情況下,要限制操作者連續(xù)頻繁的操作。(比如在請(qǐng)求入口處增加最少時(shí)間間隔限制,避免玩家狂點(diǎn),形成雪崩效應(yīng))
(同時(shí)要考慮用戶體驗(yàn),不要讓玩家死等,可以做一個(gè)提示跳轉(zhuǎn),或者等候的動(dòng)畫(huà))
參考資料:
附上最近看的一篇文章
<Writing-reliable-online-game-services> 作者曾是魔獸爭(zhēng)霸和星際爭(zhēng)霸,battle.net的開(kāi)發(fā)者,
里面講的point也是游戲里經(jīng)常遇到的可靠性問(wèn)題。
http://www.codeofhonor.com/blog/wp-content/uploads/2012/04/Patrick-Wyatt-Writing-reliable-online-game-services.pdf
by Daly
網(wǎng)游服務(wù)器程序優(yōu)化要解決的最主要矛盾無(wú)非就是在保證流暢游戲體驗(yàn)(響應(yīng)時(shí)間在可接受范圍)的前提下,容納更多的玩家,當(dāng)然還要保證開(kāi)發(fā)的便捷性。一個(gè)靠譜的MMOG游戲服務(wù)器基本上都是多線程或多進(jìn)程的架構(gòu), 利用多個(gè)CPU核把串行處理變成并行處理,以容納更大的并發(fā)玩家規(guī)模。
然而并行處理程序會(huì)使開(kāi)發(fā)的復(fù)雜度增加,一不小心很容易出一些詭異bug。為什么這樣說(shuō)呢?實(shí)際環(huán)境的大部分程序,函數(shù)的執(zhí)行結(jié)果與狀態(tài)數(shù)據(jù)相關(guān)(外部狀態(tài),全局?jǐn)?shù)據(jù)),并且函數(shù)執(zhí)行可能會(huì)改變這些狀態(tài)。如果把處理模塊拆成多進(jìn)程,進(jìn)程間的這些狀態(tài)數(shù)據(jù)的一致性和處理時(shí)序,會(huì)影響到結(jié)果的正確性。多進(jìn)程狀態(tài)數(shù)據(jù)的管理,讀寫(xiě)和同步更新機(jī)制,便是本文要探討的主要問(wèn)題。
如果函數(shù)能變成無(wú)狀態(tài)的(結(jié)果只與輸入?yún)?shù)相關(guān)),則分拆成多進(jìn)程毫無(wú)壓力。于是業(yè)界開(kāi)始探討erlang這種函數(shù)式編程語(yǔ)言,并有已有實(shí)際游戲項(xiàng)目(參看:http://www.qingliangcn.com/) 。不過(guò)筆者覺(jué)得,erlang的無(wú)狀態(tài),本質(zhì)上是把狀態(tài)數(shù)據(jù)通過(guò)函數(shù)參數(shù)傳遞,這樣意味著頻繁而大量的數(shù)據(jù)復(fù)制和傳遞,是否更適合于MMORPG開(kāi)發(fā)很難說(shuō),本文不予討論,可見(jiàn)文章末尾參考資料。下面探討一下?tīng)顟B(tài)數(shù)據(jù)在多進(jìn)程之間的問(wèn)題。
為了容易描述,整個(gè)架構(gòu)如下圖
G
client <--->║ <------> A
║ <------> B
其中G表示接入網(wǎng)關(guān),負(fù)責(zé)把client協(xié)議分發(fā)到內(nèi)網(wǎng)對(duì)應(yīng)處理進(jìn)程,A,B是負(fù)責(zé)不同功能的處理進(jìn)程,client表示客戶端,玩家狀態(tài)數(shù)據(jù)只有個(gè)v和w兩個(gè)。用reqA,reqB分別表示client對(duì)A, B的處理請(qǐng)求,respA, respB表示A,B返回給client的處理結(jié)果。
游戲邏輯大部分情況下需要保證狀態(tài)數(shù)據(jù)的強(qiáng)一致性,基于過(guò)期的數(shù)據(jù)進(jìn)行處理會(huì)得到錯(cuò)誤的結(jié)果(分布式數(shù)據(jù)一致性的工程問(wèn)題見(jiàn)文末的參考資料)。舉個(gè)有點(diǎn)蹩腳的例子,假設(shè)client先后發(fā)出reqA, reqB兩個(gè)請(qǐng)求,reqA是換武器,reqB是發(fā)起攻擊,變量v是攻擊輸出量(dps)。reqB在reqA之后發(fā)出,攻擊理應(yīng)是按穿上武器后的dps數(shù)值來(lái)計(jì)算的。但多進(jìn)程情況下,卻有可能reqB先于reqA處理(比如A進(jìn)程很忙),這時(shí)reqB的邏輯會(huì)基于還沒(méi)穿上裝備時(shí)的變量v來(lái)計(jì)算結(jié)果。下面分別討論幾種解決數(shù)據(jù)一致性問(wèn)題的方案。
模式一:共享內(nèi)存
適合于單機(jī)多進(jìn)程或多線程的模式。
優(yōu)點(diǎn):數(shù)據(jù)只有一份,可以保證強(qiáng)一致性。
缺點(diǎn):進(jìn)程無(wú)法擴(kuò)展到多臺(tái)服務(wù)器;
需要加鎖,加鎖相當(dāng)于把處理串行化,還是有可能被某一個(gè)較忙的進(jìn)程卡住。如果精心設(shè)計(jì)和劃分?jǐn)?shù)據(jù),減少鎖的粒度可以提高性能,但細(xì)粒度的鎖(設(shè)計(jì)成類似MySQL的行級(jí)鎖),在涉及多個(gè)玩家數(shù)據(jù)的交互邏輯時(shí),稍有不慎又容易導(dǎo)致死鎖。隨手寫(xiě)一個(gè):
假設(shè)進(jìn)程A和B同樣執(zhí)行以下類似的邏輯
foreach( user in mapA) {
lock(user);
lock(user‘s friend);
do_something();
unlock(user's friend);
unlock(user_id);
}
由于遍歷的是map, 進(jìn)程A和B中的user順序有可能交叉, 假設(shè)交叉的兩個(gè)user互為friend,就可能死鎖了。
參考資料[4]采用了這種模式的方案。
模式二:狀態(tài)數(shù)據(jù)只由一個(gè)進(jìn)程管理
把狀態(tài)數(shù)據(jù)根據(jù)游戲邏輯進(jìn)行劃分,比如變量v只由A讀寫(xiě), 變量w只由B讀寫(xiě)。假如A邏輯需要用到w,則通過(guò)異步請(qǐng)求B獲取w。
優(yōu)點(diǎn):保證強(qiáng)一致性;數(shù)據(jù)只有一份,無(wú)需進(jìn)程間復(fù)制更新。
缺點(diǎn):異步請(qǐng)求增加了響應(yīng)時(shí)間(嗯,又從并行變成了串行); 異步寫(xiě)起來(lái)的代碼有點(diǎn)ugly,到處是callback, 回來(lái)要檢查上下文,不然又是詭異bug.
適用范圍:如果狀態(tài)數(shù)據(jù)能比較好的劃分(即絕大多數(shù)情況下,某個(gè)數(shù)據(jù)只會(huì)在某個(gè)進(jìn)程的邏輯中用到),用這種方案比較適合,因?yàn)楹?jiǎn)單。比如玩家位置只由AOI進(jìn)程管理,玩家好友由聊天進(jìn)程管理。
模式三:多個(gè)writer, 類似MVCC方案
這是完全的分布式設(shè)計(jì)。每個(gè)進(jìn)程有自己版本的狀態(tài)數(shù)據(jù),進(jìn)程間可互相同步更新, 狀態(tài)數(shù)據(jù)v分別在A,B都有一份。互相update時(shí),根據(jù)版本信息進(jìn)行merge。
這種方案不能保證強(qiáng)一致性,而且merge時(shí)會(huì)有可能發(fā)生沖突,需要邏輯開(kāi)發(fā)者仲裁這種沖突(比如按時(shí)間先后)。不同于互聯(lián)網(wǎng)應(yīng)用,游戲需要較強(qiáng)的數(shù)據(jù)一致性和實(shí)時(shí)性,這種方案比較復(fù)雜且不太可控。
模式四:Master-Slave模式
這個(gè)是對(duì)模式二的一個(gè)擴(kuò)展,某個(gè)狀態(tài)數(shù)據(jù)還是只由一個(gè)進(jìn)程進(jìn)行寫(xiě)操作,但其他進(jìn)程會(huì)維持一份cache進(jìn)行讀操作,比如變量v由進(jìn)程A管理,v的更新會(huì)同步到進(jìn)程B,進(jìn)程B邏輯如果要用到v,直接讀自己的cache就可以了。對(duì)于變量v
特點(diǎn):這種方式也是不能保證強(qiáng)一致性,只能保證最終一致性。作為模式二的補(bǔ)充,有些數(shù)據(jù)不需要保證更新時(shí)序,根據(jù)過(guò)期數(shù)據(jù)進(jìn)行處理也可以接受(這個(gè)是代價(jià),需要權(quán)衡玩家體驗(yàn)),可以采取這種方式。而對(duì)于不能接受的,走模式二。某些需求reqA,reqB雖然先后發(fā)出,如果respA還沒(méi)反饋回來(lái)的話,即使邏輯上reqB先于reqA處理,在玩家體驗(yàn)上也是可以接受的。比如reqA穿裝備, 然后reqB攻擊,但是respA還沒(méi)返回,客戶端還是看作是沒(méi)穿上裝備,這時(shí)候按照老的屬性計(jì)算攻擊值是可接受的。廣域網(wǎng)幾百毫秒的延遲,reqB要晚于reqA + respA這種概率很小了,如果真的發(fā)生,服務(wù)器已經(jīng)很卡了。
又比如聊天進(jìn)程,reqA離開(kāi)場(chǎng)景,然后reqB發(fā)聊天消息往當(dāng)前場(chǎng)景頻道,需要知道當(dāng)前場(chǎng)景的玩家列表(假設(shè)場(chǎng)景玩家列表在AOI進(jìn)程管理),如果reqB先到達(dá)聊天進(jìn)程,拿到舊的場(chǎng)景玩家列表, 那么這個(gè)廣播就不準(zhǔn)確了。這種不一致性的代價(jià)可以忍受的話就沒(méi)問(wèn)題(在這個(gè)聊天欄例子,在跳場(chǎng)景的瞬間發(fā)錯(cuò)人了也可以忍),實(shí)際情況,進(jìn)程間通信幾個(gè)毫秒,發(fā)生這種處理時(shí)序反轉(zhuǎn)的幾率其實(shí)非常小了。
綜上,如果要設(shè)計(jì)多進(jìn)程結(jié)構(gòu),個(gè)人比較推崇模式四。這時(shí)又引申出幾個(gè)問(wèn)題:狀態(tài)數(shù)據(jù)如何合理劃分?何時(shí)更新?同步給誰(shuí)?
如何劃分?
有些功能很好劃分。比如聊天進(jìn)程,狀態(tài)數(shù)據(jù)只與好友列表有關(guān),這個(gè)需求可以忍受過(guò)期數(shù)據(jù),好友關(guān)系由主進(jìn)程修改,同步到聊天進(jìn)程。玩家position, 由AOI進(jìn)程管理,修改同步到主進(jìn)程,主進(jìn)程幾乎沒(méi)有需要用到position的邏輯。
但有些數(shù)據(jù)就可能很糾結(jié),比如背包數(shù)據(jù)。玩家交易,在線獎(jiǎng)勵(lì),戰(zhàn)斗都需要修改背包物品數(shù)據(jù),而且必須保證強(qiáng)一致性,否則就可能出現(xiàn)丟失或物品復(fù)制,該由誰(shuí)做這個(gè)數(shù)據(jù)的管理者呢?如果AOI進(jìn)程管理,物品使用效果可以馬上生效,但是交易和在線獎(jiǎng)勵(lì)也需要驗(yàn)證背包物品,這些邏輯也放到AOI進(jìn)程么,如果放,則又牽扯出更多的變量,如果不放,則需要退化成模式2的異步請(qǐng)求。如果放主進(jìn)程,則使用物品后產(chǎn)生的效果不能立刻同步到AOI進(jìn)程。可以經(jīng)過(guò)仔細(xì)對(duì)比,AOI與背包數(shù)據(jù)交互的頻率遠(yuǎn)高于主進(jìn)程,于是背包數(shù)據(jù)可由AOI進(jìn)程管理。
何時(shí)更新?
兩種選擇:一有修改立馬發(fā)送更新給其他進(jìn)程;隊(duì)列buffer住所有更新,定時(shí)送出去(比如每2秒同步一次);既然是無(wú)法保證強(qiáng)一致性,后者性能容易優(yōu)化些。比如AOI進(jìn)程中的位置信息變化很頻繁,但主進(jìn)程對(duì)位置實(shí)時(shí)性不敏感(比如只用于持久化,掉線重上后的位置恢復(fù)),則更新間隔可以長(zhǎng)一些,否則會(huì)有頻繁而大量的位置數(shù)據(jù)更新;定時(shí)更新也利于同步間隔內(nèi)數(shù)據(jù)修改的合并,減少同步量。
同步給誰(shuí)?
某類數(shù)據(jù)有修改時(shí),需要通知哪些進(jìn)程,意味著要維持一個(gè)映射表。可以在編碼階段,在數(shù)據(jù)定義時(shí)靜態(tài)寫(xiě)死某類數(shù)據(jù)要通知哪一類功能進(jìn)程; 也可以在運(yùn)行期設(shè)計(jì)成pub-sub模式(或者叫observer模式), 動(dòng)態(tài)增刪訂閱者。筆者覺(jué)得前者可控一點(diǎn),因?yàn)檫M(jìn)程要用到哪些數(shù)據(jù),在編碼階段是可以清楚規(guī)劃的,根據(jù)這個(gè)原則把數(shù)據(jù)劃分成一個(gè)個(gè)模塊,比如玩家數(shù)據(jù)分為基本角色屬性,avatar, 位置/朝向, 好友數(shù)據(jù).... 然后決定歸屬。
多進(jìn)程可以提升系統(tǒng)并發(fā)規(guī)模,但同時(shí)有各種異步調(diào)用和數(shù)據(jù)一致性問(wèn)題,帶來(lái)的代價(jià)就是bug的風(fēng)險(xiǎn)增加(尤其團(tuán)隊(duì)水平不能保證個(gè)個(gè)都很高的情況下,一個(gè)菜鳥(niǎo)程序員就夠受了,還很難跟蹤),開(kāi)發(fā)難度增大。這個(gè)需要仔細(xì)profile和實(shí)驗(yàn)確定瓶頸在哪,真的跑滿CPU或者卡IO才有必要分出去,想當(dāng)然的把模塊拆分很多進(jìn)程,設(shè)計(jì)看上去很優(yōu)雅也很牛逼,往往是麻煩的開(kāi)始 ——> 開(kāi)發(fā)效率降低,出bug意味著啥?加班,加班,深夜運(yùn)維的奪命追魂call... ...
參考資料
替代系統(tǒng)自帶的malloc/new原因無(wú)非兩個(gè):
reason 1. 做內(nèi)存profile或查找問(wèn)題
reason 2. 自定義的分配方案提高性能
不過(guò)文章[1]中說(shuō)明了,替代全局new不是一個(gè)好做法. 其實(shí)要達(dá)到以上兩點(diǎn)目的,筆者認(rèn)為用valgrind工具鏈就可以了。
解決方案:
1. 用valgrind和massif
valgrind的memcheck做內(nèi)存泄露和bug的查找, 里面的massif工具包做內(nèi)存性能profile, 足矣。比自己山寨的一個(gè)profiler要好。
注意:tcmalloc目前還不能很好支持valgrind, 實(shí)測(cè)中jemalloc可以
2. linux下C的程序可以用wrap的方式(相當(dāng)于python的decorator)
編譯加上選項(xiàng):gcc -Wl,-wrap,malloc
可以做到對(duì)malloc這個(gè)函數(shù),linker會(huì)調(diào)用__wrap_malloc代替之, 若要調(diào)用原來(lái)的malloc函數(shù)__real_malloc
缺點(diǎn):依賴于編譯器支持; 對(duì)c++的new不起作用 --> 不實(shí)用
啟示:這個(gè)方法作為function裝飾器,對(duì)于調(diào)試別的問(wèn)題倒有幫助。(例如不改變函數(shù)的情況下,wrap一層,輸出些調(diào)試信息)
3. 用__malloc_hook
#include <malloc.h>
void *(*__malloc_hook)(size_t size, const void *caller);
缺點(diǎn):依賴GNU編譯工具鏈; 容易死循環(huán)(想利用原有malloc,要參考例子中,把原__malloc_hook變量保存起來(lái)使用,并恢復(fù)現(xiàn)場(chǎng))
4. LD_PRELOAD注入.so ,替代原
環(huán)境變量LD_PRELOAD指定程序運(yùn)行時(shí)優(yōu)先加載的動(dòng)態(tài)連接庫(kù),這個(gè)動(dòng)態(tài)鏈接庫(kù)中的符號(hào)優(yōu)先級(jí)是最高的。標(biāo)準(zhǔn)C的各種函數(shù)都是存放在libc.so.6的文件中,在程序運(yùn)行時(shí)自動(dòng)鏈接。使用LD_PRELOAD后,自己編寫(xiě)的malloc的加載順序高于glibc中的malloc,這樣就實(shí)現(xiàn)了替換。用法 LD_PRELOAD=" ./mymalloc.so"
缺點(diǎn):在生產(chǎn)環(huán)境不現(xiàn)實(shí)。因?yàn)長(zhǎng)D_PRELOAD相當(dāng)于庫(kù)注入,有安全性問(wèn)題,是必須禁止的。(生產(chǎn)環(huán)境很多時(shí)候用-static連接)
5. 用宏或另外的函數(shù)替代new/malloc
比如定義一個(gè)宏或者指定的函數(shù),規(guī)定所有的分配釋放都調(diào)用他。這樣相當(dāng)于給項(xiàng)目引入了額外的代碼規(guī)則(而且是一立項(xiàng)就要遵循這個(gè)規(guī)則,否則該方法無(wú)效),不能很自然的new/delete, 如果分配和釋放調(diào)用得不一致,會(huì)產(chǎn)生問(wèn)題的。某產(chǎn)品組就是用宏,然后加上__FILE__, __LINE__之類的信息。
有時(shí)候valgrind的效率是個(gè)問(wèn)題(尤其生產(chǎn)環(huán)境),這種方案有其價(jià)值所在, 就是代碼看上去比較ugly罷了
用宏的例子:
#define _New(Type, Catergory) (Type*)MyMemController::New((new Type), #Type, 1, sizeof(Type), Catergory, __FILE__, __LINE__, false)
#define _NewArray(Type, N, Catergory) (Type*)MyMemController::New((new Type[N]), #Type, N, sizeof(Type)*(N), Catergory, __FILE__, __LINE__, true)
MALLOC的替代品:
自己寫(xiě)一個(gè)malloc其實(shí)很復(fù)雜,要考慮線程安全等各種問(wèn)題,性能到頭來(lái)可能更差。google 的tcmalloc, facebook使用的jemalloc. 多線程下性能較好,可以考慮使用。
缺點(diǎn):筆者嘗試過(guò)。tcmalloc不能正確用valgrind,只能用自帶gperftools(運(yùn)行中會(huì)core)
jemalloc可以使用valgrind,不過(guò)還沒(méi)完全驗(yàn)證是否都準(zhǔn)確。
tcmalloc相關(guān):
在64位系統(tǒng)上要裝libunwind, 對(duì)x86-64架構(gòu)使用還有些問(wèn)題
源碼包的INSTALL文檔里面也提到了這個(gè)問(wèn)題。
CAUTION: if you install libunwind from the url above, be aware that
you may have trouble if you try to statically link your binary with
perftools: that is, if you link with 'gcc -static -lgcc_eh ...'.
This is because both libunwind and libgcc implement the same C++
exception handling APIs, but they implement them differently on
some platforms. This is not likely to be a problem on ia64, but
may be on x86-64.
主要是64位機(jī)frame-pointer的影響, 他的profile工具里的backtrace用libunwind這個(gè)庫(kù),這個(gè)庫(kù)又有版本問(wèn)題,各種囧啊....
筆者試過(guò)系統(tǒng)x86-64, freebsd,用靜態(tài)鏈接。實(shí)際用了一下,問(wèn)題很多很折騰,等他fix了再說(shuō)吧.
windows下可以參考:
jemalloc暫時(shí)未發(fā)現(xiàn)有什么兼容性問(wèn)題,運(yùn)行得挺好的。
最近組內(nèi)發(fā)表一篇小論文,是關(guān)于改進(jìn)游戲儲(chǔ)存系統(tǒng)的IO性能思路。老大原來(lái)早有相同的想法,并且已經(jīng)實(shí)現(xiàn)了大部分模塊,后來(lái)和老大一同努力,新的儲(chǔ)存引擎終于逐步完善。在外服環(huán)境跑了兩個(gè)多月,性能和可靠性得到了明顯的提升。具體的細(xì)節(jié)就不方便發(fā)表了,實(shí)踐證明,用binlog來(lái)做MMORPG的數(shù)據(jù)儲(chǔ)存是行得通的。
幾個(gè)事實(shí):
1. 磁盤(pán)IO的瓶頸在尋道,順序?qū)懶阅鼙入S機(jī)寫(xiě)性能高一個(gè)數(shù)量級(jí)。
目前典型硬盤(pán)的順序?qū)懭胨俣却蠹s是60MB/s , 而尋道時(shí)間在5~8ms (200次/秒)。可以看到硬盤(pán)IO的主要瓶頸在于磁頭尋道,也就是隨機(jī)寫(xiě)。在linux開(kāi)發(fā)服(非虛擬機(jī),Xeon 3.0G 4核/16G內(nèi)存)上做了一個(gè)benchmark。
順序?qū)?/span>50MB: 700ms
寫(xiě)5000個(gè)文件,每個(gè)10KB(共50MB): 12秒
10000次隨機(jī)寫(xiě),每次1KB(共10MB): 21秒
2. 游戲數(shù)據(jù)都是K-V數(shù)據(jù),關(guān)系查詢需求極少;k-v數(shù)據(jù)的update很頻繁(實(shí)測(cè)是每玩家每5秒一次修改)
3. MMORPG單服的玩家同時(shí)在線數(shù)量是10K級(jí)別, 這個(gè)數(shù)量級(jí)可以有效估算binlog的規(guī)模,使得方案可行。
一般MMORPG系統(tǒng)的存盤(pán)策略: 定時(shí)存盤(pán)。就是過(guò)一段時(shí)間(比如5分鐘)把在線有修改過(guò)的玩家數(shù)據(jù),整個(gè)snapshot存下去(mysql也好,文件系統(tǒng)也好)。這樣有兩個(gè)主要問(wèn)題:一到保存點(diǎn),IO隨機(jī)寫(xiě)暴增,玩家卡機(jī);如果系統(tǒng)down機(jī), 數(shù)據(jù)就會(huì)有幾分鐘的回檔。而性能和數(shù)據(jù)可靠性兩則是矛盾的,存盤(pán)間隔過(guò)小,玩家卡機(jī),過(guò)大,故障后數(shù)據(jù)回檔時(shí)間長(zhǎng)。需知現(xiàn)在的MMORPG,貴價(jià)武器價(jià)值都成千上萬(wàn)RMB,數(shù)據(jù)可靠性對(duì)游戲營(yíng)運(yùn)影響還是很大的。
so, 可以用定制的binlog來(lái)記錄玩家數(shù)據(jù),也就是說(shuō),不記錄整個(gè)snapshot,而是每個(gè)k-v變化時(shí)記錄opcode馬上寫(xiě)入binlog文件, binlog的格式根據(jù)游戲情況可以高度定制,盡量減少空間。由于是順序?qū)懀阅芸梢苑浅8摺H绻鹍own機(jī),可以根據(jù)binlog來(lái)恢復(fù),基本上沒(méi)有回檔。不過(guò)要解決一個(gè)問(wèn)題:binlog增長(zhǎng)過(guò)大 --> 崩潰恢復(fù)時(shí)間過(guò)程 & binlog文件本身?yè)p壞的風(fēng)險(xiǎn)增大 & 磁盤(pán)空間用光。因此binlog需要有rotate機(jī)制, rotate的時(shí)候需要存一次在線玩家數(shù)據(jù)的snapshot, 這樣舊的binlog就可以存到遠(yuǎn)處或者丟棄。rotate的過(guò)程中需要考慮恢復(fù)時(shí)玩家數(shù)據(jù)一致性和完備性等等一系列細(xì)節(jié)問(wèn)題,后來(lái)一一解決了。
這是最近做的成就感的事。幾年沒(méi)寫(xiě)blog了,筆記都記在evernote里,最近又想在公開(kāi)的地方寫(xiě)點(diǎn)東西,發(fā)個(gè)文紀(jì)念一下。
摘要: 很多程序都需要處理一系列定時(shí)事件, 本文就見(jiàn)過(guò)的程序中,幾種實(shí)現(xiàn)Timer的方法。用到的數(shù)據(jù)結(jié)構(gòu)一般有鏈表, 堆, RB樹(shù),hash table等,還有一些比較優(yōu)化的方法。
閱讀全文