luajit表记录监控(忆一次项目上线中遇到的luajit对象内存泄漏)
1. 背景
我們項(xiàng)目為ARPG手游(也沒(méi)啥見(jiàn)不得人的,就叫暗黑血統(tǒng)手游,后期不少坑錢活動(dòng)的實(shí)現(xiàn)出自我手,輕拍。。。)。我們的服務(wù)器底層設(shè)計(jì)源于某大廠,c/c++和luajit的實(shí)現(xiàn),這次要說(shuō)的是項(xiàng)目上線時(shí)(2014年11月左右)的一次luajit對(duì)象內(nèi)存泄漏(廢棄的數(shù)據(jù)沒(méi)刪,我們都叫泄漏)和相應(yīng)的解決方案。
2. 問(wèn)題表現(xiàn)
內(nèi)存增長(zhǎng),速率大概為200~300MB/天。
我們?nèi)罩緯?huì)周期性打印Tcmalloc內(nèi)存(Tcmalloc分享另見(jiàn)同事Wallen的博客TCMalloc解密)和lua部分內(nèi)存。獲取方法如下:
//tc malloc部分 size_t tc_memory = 0; MallocExtension::instance()->GetNumericProperty( "generic.current_allocated_bytes", &tc_memory );//luajit部分 int lua_memory = lua_gc( _L, LUA_GCCOUNT, 0 ); 復(fù)制代碼通過(guò)日志發(fā)現(xiàn)在線人數(shù)相似時(shí),lua部分內(nèi)存和總內(nèi)存在同步增長(zhǎng),c/c++部分內(nèi)存(即上兩個(gè)部分差值,主要是網(wǎng)絡(luò)庫(kù)、對(duì)象體系對(duì)象和World部分的內(nèi)存)基本穩(wěn)定。日志顯示lua的gc正常。
3. 分析
- c/c++部分沒(méi)有泄漏,player/monster等對(duì)象釋放沒(méi)有問(wèn)題,c層對(duì)象析構(gòu)基本由lua層觸發(fā),lua層對(duì)應(yīng)的對(duì)象內(nèi)存釋放也是沒(méi)有問(wèn)題的。
- 通過(guò)日志顯示,場(chǎng)景/副本管理的釋放也是沒(méi)有問(wèn)題的,在線導(dǎo)出各個(gè)場(chǎng)景/副本內(nèi)的對(duì)象個(gè)數(shù)/類型,經(jīng)過(guò)分析,也沒(méi)發(fā)現(xiàn)什么問(wèn)題,日志監(jiān)控的數(shù)據(jù)也都正常。
- 最后懷疑是一些非主要的lua對(duì)象,存在表里沒(méi)清除。因?yàn)閘ua對(duì)象里,我們沒(méi)有太復(fù)雜的表結(jié)構(gòu),因此這些泄漏的內(nèi)存會(huì)扁平地存在少量的幾個(gè)表里。因此,只要能知道各個(gè)表的記錄數(shù),結(jié)合在線人數(shù)推算其最大可能數(shù),二者相比較,就能找出泄漏的表,在檢查表的增刪邏輯,就可以找到泄漏的邏輯。
4. lua表記錄數(shù)告警方案
如前述,只要知道各個(gè)表的記錄數(shù),結(jié)合在線人數(shù)推算其最大可能數(shù),二者相比較,就能找出泄漏的表。但是如果直接這么做,勢(shì)必影響性能,即使熱更gm指令用lua全遍歷1次(因?yàn)閠able的值也可能是table,實(shí)際上要遍歷一棵樹(shù)),都是分鐘級(jí)別的。分幀做?這坑好大,如果不行也只能這么干了。按泄漏的速度,內(nèi)存可以撐到下一次維護(hù),所以,不慌。
然后看看c層luajit表的相關(guān)操作,看看有沒(méi)有更有效率的獲取方法(我們c層代碼不熱更,改c層代碼需要等維護(hù)重啟后才能生效,按泄漏速度,可以接受)。
代碼里主要關(guān)注的是lua table的增加和刪除記錄。然后看到lua table的resize(下面luajit相關(guān)代碼都是luajit2.1分支代碼,和1會(huì)長(zhǎng)得有些不一樣,我們已升luajit2.1,將就一下)
/* Resize a table to fit the new array/hash part sizes. */ void lj_tab_resize(lua_State *L, GCtab *t, uint32_t asize, uint32_t hbits) { ... } 復(fù)制代碼邏輯其實(shí)就是數(shù)組段或者哈希段每次超過(guò)2的n次冪,會(huì)重新分配內(nèi)存。
我們不需要精確的記錄數(shù),其實(shí)只要在他每次resize的時(shí)候打條日志就能知道這個(gè)table大概的記錄數(shù),比如上一條日志是1024->2048,那么記錄數(shù)在1024~2048之間。日志的優(yōu)化見(jiàn)工程化部分。
怎么確定是哪個(gè)table?這里我們能取到的是table的地址,取不到table的名字,根據(jù)地址取名字也是一場(chǎng)噩夢(mèng)。這里我們曲線救國(guó),既然能拿到lua_State,可以把lua的堆棧打出來(lái),根據(jù)文件名和行號(hào)可以定位到代碼行號(hào),一行代碼沒(méi)幾個(gè)table,這樣就能確定下來(lái)了。
//打印lua層堆棧,編譯lua加上調(diào)試信息 int32_t c_bt( lua_State* _L ) { lua_Debug ldb;LOG("[LUAWRAPPER](lua_stack) begin .......... ");for(int32_t i = 0; lua_getstack( _L, i, &ldb)==1; i++){lua_getinfo(_L, "Slnu", &ldb);const char * name = ldb.name;if (!name)name = "";const char * filename = ldb.source;LOG("[LUAWRAPPER](bt) #%d: %s:'%s', '%s' line %d", i, ldb.what, name, filename, ldb.currentline );}LOG("[LUAWRAPPER](lua_stack) end .......... ");return 0; } 復(fù)制代碼到此,表的記錄數(shù)我們能拿到個(gè)粗略的值,也知道是哪張表了,每張表最大的數(shù)值也可以根據(jù)在線人數(shù)估計(jì)(大部分近似在線人數(shù)+暫時(shí)斷線的+跨服的,Buff之類的可以乘以一個(gè)最大倍數(shù)),剩下的就交給時(shí)間和人工分析日志比較了。過(guò)濾掉正常的日志,就能得到包含了泄漏對(duì)象的表了,在分析增刪邏輯就能找到廢棄又沒(méi)有清楚的數(shù)據(jù)了。
5. 工程化
前面講的只是方案,真正應(yīng)用的時(shí)候,需要減少日志的條數(shù),以減輕分析的工作量。減少日志通過(guò)下面兩種方式:
- 超過(guò)一定大小,才打日志,我們一個(gè)服在線是3k左右,閥值取4096。
- 實(shí)踐發(fā)現(xiàn),如果表在2的n次冪邊界發(fā)生頻繁切換時(shí),resize日志會(huì)重復(fù)打,所以修改了表結(jié)構(gòu),實(shí)現(xiàn)每個(gè)邊界只打一次。
所以,如果漏的不嚴(yán)重(<4096)又不會(huì)隨時(shí)間增長(zhǎng),是查不出來(lái)的,也就算了。
5.1. lua table表結(jié)構(gòu)的修改
typedef struct GCtab {...//extended by ludongint32_t max_sizearray;int32_t max_sizeobj; } GCtab; 復(fù)制代碼5.2. 初始化(luajit1的數(shù)值是不一樣的)
/* Create a new table. Note: the slots are not initialized (yet). */ static GCtab *newtab(lua_State *L, uint32_t asize, uint32_t hbits) {...t->max_sizearray = 4096;t->max_sizeobj = 4096;return t; } 復(fù)制代碼5.3. 寫日志
為了解決庫(kù)編譯依賴的問(wèn)題,將上層日志函數(shù)定義為一個(gè)函數(shù)變量,進(jìn)程啟動(dòng)時(shí)注冊(cè)賦值。函數(shù)實(shí)現(xiàn)賦值就不貼了。
/* -- Table resizing ------------------------------------------------------ */typedef void (*lua_large_table_warn_func)( lua_State *L, char* fmt, ... ); lua_large_table_warn_func g_lua_large_table_warn = NULL;/* Resize a table to fit the new array/hash part sizes. */ void lj_tab_resize(lua_State *L, GCtab *t, uint32_t asize, uint32_t hbits) {...//--------------------------------------------------// by ludong// check resize timeif (t->asize > t->max_sizearray || t->hmask > t->max_sizeobj) {int32_t old_max_sizearray = t->max_sizearray;int32_t old_max_sizeobj = t->max_sizeobj;t->max_sizearray = (t->asize > t->max_sizearray) ? t->asize : t->max_sizearray;t->max_sizeobj = (t->hmask > t->max_sizeobj) ? t->hmask : t->max_sizeobj;if (g_lua_large_table_warn) {g_lua_large_table_warn( L, "[ltable.c](resize) table:0x%x, array check size:%d, now:%d, node check size:%d, now:%d",(size_t)t,old_max_sizearray, t->asize,old_max_sizeobj, t->hmask);}}} 復(fù)制代碼5.4. 性能影響
每個(gè)表多8字節(jié)(我們64位了,對(duì)齊后一樣的,32位自己摳一摳),對(duì)大部分表多兩個(gè)邏輯判斷,對(duì)大表每次日志邊界打一條日志和lua堆棧,內(nèi)存和cpu基本都沒(méi)沒(méi)感覺(jué)。
6. 后記
這個(gè)方案做好之后基本是我們項(xiàng)目上線必檢查的日志了,總會(huì)有一些不小心就沒(méi)刪的。
總結(jié)
以上是生活随笔為你收集整理的luajit表记录监控(忆一次项目上线中遇到的luajit对象内存泄漏)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: ldap客户端以及jenkins的配置
- 下一篇: 不发邮件的问题处理