問(wèn)題
最近游戲開始技術(shù)封測(cè)了,不過(guò)剛剛上線3個(gè)小時(shí),Server就掛了,掛在框架代碼里,一個(gè)不可能掛的地方。
從CallStack看,是在獲取數(shù)據(jù)時(shí)發(fā)送請(qǐng)求包的時(shí)候掛的,由于框架部分是其他部門的同事開發(fā)的,所以查問(wèn)題的時(shí)候就拉上他們了,
大家折騰了2天,沒(méi)有實(shí)質(zhì)性的進(jìn)展,服務(wù)器還是基本上每3個(gè)小時(shí)宕機(jī)一次。由于上層邏輯大部分都在我那,所以壓力比較大,宕機(jī)的直接原因是hashtable的一個(gè)桶的指針異常,
這個(gè)hashtable是框架代碼的一個(gè)內(nèi)部成員,按道理我們是無(wú)從破壞的,只有可能是多線程環(huán)境下迭代器損壞導(dǎo)致的。
但是框架代碼在這個(gè)地方確實(shí)無(wú)懈可擊,所以真正的原因應(yīng)該還是上層代碼破壞了堆內(nèi)存,很可能是一個(gè)memcpy越界導(dǎo)致的。這畢竟是個(gè)猜想,如何找到證據(jù)呢,這是個(gè)問(wèn)題。
把所有代碼里的memcpy瀏覽了一遍,沒(méi)有發(fā)現(xiàn)明顯問(wèn)題。
猜測(cè)
一般游戲中比較容易出現(xiàn)但是不好查的問(wèn)題很多時(shí)候都是腳本(lua)導(dǎo)致的,我們的腳本部分是一個(gè)同事幾年前寫的,在幾個(gè)產(chǎn)品中都使用過(guò),按道理沒(méi)這么脆弱,不過(guò)老大還是和最初開發(fā)這個(gè)模塊的部門溝通了下,
還真發(fā)現(xiàn)問(wèn)題了,趕緊拿了新的版本更新上去。經(jīng)過(guò)一天的觀察,服務(wù)器沒(méi)有宕機(jī)了,OK,問(wèn)題碰巧解決了,背了這么久的黑鍋,終于放下來(lái)了。
PageHeap
假如沒(méi)有碰巧解決了這個(gè)問(wèn)題,正常的思路該如何解決這個(gè)問(wèn)題呢,這個(gè)時(shí)候我懷念windows了,在windows下有PageHeap來(lái)解決這類寫越界的問(wèn)題。基本思路就是每次分配內(nèi)存的時(shí)候,都將內(nèi)存的結(jié)尾放在頁(yè)的邊緣,緊接著這塊內(nèi)存分配一塊不能寫的內(nèi)存,這樣,一旦寫越界,就會(huì)寫異常,導(dǎo)致宕機(jī)。linux下沒(méi)有現(xiàn)成的工具,但是linux提供了mmap功能,我們可以自己實(shí)現(xiàn)這樣一個(gè)功能,當(dāng)然,這一切都不用自己動(dòng)手了,tcmalloc已經(jīng)包含了
這個(gè)功能了,不過(guò)在文檔里基本沒(méi)有介紹,我也是在閱讀tcmalloc代碼時(shí)看到的,這個(gè)功能默認(rèn)是關(guān)閉的,打開這個(gè)開關(guān)需要改寫代碼:
這個(gè)代碼在debugallocation.cc里:
DEFINE_bool(malloc_page_fence,
EnvToBool("TCMALLOC_PAGE_FENCE", false),
"Enables putting of memory allocations at page boundaries "
"with a guard page following the allocation (to catch buffer "
"overruns right when they happen).");
把false改成true就可以了。
想要在項(xiàng)目里加入PageHeap功能,只需要鏈接的時(shí)候加上 -ltcmalloc_debug即可。把它加入項(xiàng)目中,試著運(yùn)行下,直接掛了,
仔細(xì)一看,原來(lái)是項(xiàng)目中很多成員變量沒(méi)有初始化導(dǎo)致的,tcmalloc_debug會(huì)自動(dòng)將new 和malloc出來(lái)的內(nèi)存初始化為指定值,這樣,一旦變量沒(méi)有初始化,很容易就暴露了。
修改完這個(gè)問(wèn)題后,編譯,再運(yùn)行,還是掛,這個(gè)是mprotect的時(shí)候掛的,錯(cuò)誤是內(nèi)存不夠,這怎么可能呢,其實(shí)是達(dá)到了資源限制了。
echo 128000 > /proc/sys/vm/max_map_count
把map數(shù)量限制加大,再運(yùn)行,OK了!
但是游戲Server啟動(dòng)后,發(fā)現(xiàn)一個(gè)問(wèn)題,CPU長(zhǎng)期處于100%,導(dǎo)致登陸一個(gè)玩家都很困難,gdb中斷后,info thread,發(fā)現(xiàn)大部分的操作都在mmap和mprotect,最開始
懷疑我的linux版本有問(wèn)題,導(dǎo)致這2個(gè)AP慢,寫了測(cè)試程序試了下,發(fā)現(xiàn)其實(shí)API不慢,估計(jì)是頻繁調(diào)用導(dǎo)致的。
所以得換種思路優(yōu)化下才可以,其實(shí)大部分情況下,我們free的時(shí)候,無(wú)需將頁(yè)面munmap掉,可以先cache進(jìn)來(lái),下次分配的時(shí)候,如果有,直接拿來(lái)用就可以了。
最簡(jiǎn)單的cache算法就是定義一個(gè)void* s_pageCache[50000]數(shù)組,頁(yè)面數(shù)相同的內(nèi)存組成一個(gè)鏈表,掛在一個(gè)數(shù)組項(xiàng)下,這個(gè)很像STL的小內(nèi)存處理,我們可以將mmap出來(lái)的內(nèi)存的
前面幾個(gè)字節(jié)(一個(gè)指針大小)用于索引下一個(gè)freePage。當(dāng)然這個(gè)過(guò)程需要加鎖,不能用pthread的鎖(因?yàn)樗麄儠?huì)調(diào)用malloc等內(nèi)存分配函數(shù)),必須用spinlock,從linux源碼里直接抄一個(gè)過(guò)來(lái)即可。
static void* s_pagePool[MAX_PAGE_ALLOC]={0};
malloc的時(shí)候,先從pagePool里面獲取:
// 先從pagePool找
void* pFreePage = NULL;
spin_lock(&s_pageHeapLock);
assert(nPageNum < MAX_PAGE_ALLOC);
if(s_pagePool[nPageNum])
{
pFreePage = s_pagePool[nPageNum];
void* pNextFreePage = *((void**)pFreePage);
s_pagePool[nPageNum] = pNextFreePage;
}
spin_unlock(&s_pageHeapLock);
free內(nèi)存的時(shí)候,直接放到pagePoll里:
spin_lock(&s_pageHeapLock);
assert(nPageNum < MAX_PAGE_ALLOC);
void* pNextFree = s_pagePool[nPageNum];
*(void**)pAddress = pNextFree;
s_pagePool[nPageNum] = pAddress;
spin_unlock(&s_pageHeapLock);
編譯、運(yùn)行,OK了,CPU迅速降下來(lái)了,空載的時(shí)候不到1%,而且也能達(dá)到檢測(cè)寫溢出的問(wèn)題。
posted on 2011-05-14 21:16
feixuwu 閱讀(2059)
評(píng)論(1) 編輯 收藏 引用 所屬分類:
游戲開發(fā)