如何在 skynet 死循环中输出堆栈
skynet版本:v1.4.0 (2020-11-16)
最近在做武侠mmo的游戏。因为武侠战场有一个tick在计算状态,然而在tick里的逻辑出现了问题,具体原因是一个怪物的ai返回错误码值有问题,而外层tick又没有捕捉到这个错误码,所以出现了死循环,导致一直在执行ai,创建了一堆table,最后导致内存爆了。
这里不讨论逻辑问题,主要讲述当时是如何定位到bug代码的,然后可以如何改进。
当时得亏还在内测阶段出现问题,而且这个是可以重现的,具体表现是在固定的副本里的固定的怪物释放了固定的技能导致,当然这是找到的结果。
在出现问题的时候,因为是比较难触发到,所以也无法立刻判断出是技能的bug,在重现过程中,依赖于skynet本身的debug_conole 的 signal 1命令来输出当前的堆栈,最终也能定位到问题。
解决完bug后,后面想如何能在出现问题的时候尽可能得收集到当前bug的相关信息呢?回想到查找bug的过程,主要还是要打印当前的堆栈,所以想是否能在 monitor 线程监控的时候发现死循环时打印出lua当前的堆栈呢?觉得是可行的,因为signal 1 的信号实际也是调用了c层面的方法而已。
很容易的,能够想到在 skynet_monitor.c 里加入如下代码,直接调用command接口:
1 void 2 skynet_monitor_check(struct skynet_monitor *sm) { 3 if (sm->version == sm->check_version) { 4 if (sm->destination) { 5 skynet_context_endless(sm->destination); 6 skynet_error(NULL, "A message from [ :%08x ] to [ :%08x ] maybe in an endless loop (version = %d)", sm->source , sm->destination, sm->version); 7 8 char tmp[20]; // 加入的代码,调用具体服务的command接口。 9 sprintf(tmp, ":%08x 9", sm->source); 10 skynet_command(NULL, "SIGNAL", tmp); 11 12 sprintf(tmp, ":%08x 9", sm->destination); 13 skynet_command(NULL, "SIGNAL", tmp); 14 } 15 } else { 16 sm->check_version = sm->version; 17 } 18 }
如上述代码的第8到13行,是分别向两个服务的 serivce_snlua 发出 signal 9 调用具体的回调函数。
那么接下来是对 service_snlua.c 的代码进行修改,这里一开始写了一版在多线程环境下有bug的代码,如下:
1 void 2 snlua_signal(struct snlua *l, int signal) { 3 skynet_error(l->ctx, "recv a signal %d", signal); 4 if (signal == 0) { 5 if (ATOM_LOAD(&l->trap) == 0) { 6 // only one thread can set trap ( l->trap 0->1 ) 7 if (!ATOM_CAS(&l->trap, 0, 1)) 8 return; 9 lua_sethook (l->activeL, signal_hook, LUA_MASKCOUNT, 1); 10 // finish set ( l->trap 1 -> -1 ) 11 ATOM_CAS(&l->trap, 1, -1); 12 } 13 } else if (signal == 1) { 14 skynet_error(l->ctx, "Current Memory %.3fK", (float)l->mem / 1024); 15 } else if (signal == 9){ 16 lua_State* L = l->activeL; 17 CallInfo* ci = L->ci; 18 for ( ci = L->ci; ci != NULL && ci != &L->base_ci; ci = ci->previous) { 19 if ((ci->func.p->tbclist.tt_ & 0x3F) == 6) { 20 Proto *sp = ((union GCUnion *)(ci->func.p->tbclist.value_.gc))->cl.l.p; 21 char * filename = sp->source ? (char *) ((char *)(sp->source) + 22 sizeof(TString)) : "unknown"; 23 skynet_error(l->ctx, "<TRACE %s %d %d>" , filename, sp->linedefined, 24 sp->lastlinedefined); 25 } 26 } 27 } 28 }
上面的第15行开始就是所加代码,当时没考虑在monitor的线程中直接对lua虚拟机进行操作的同时,在worker线程调用中可能会修改到lua虚拟机的一些状态,另外也是直接对callinfo的链表进行了输出,可能格式也有一些难看。
后面参考了siganl 0 的操作,然后就有了最终版本:
1 static void 2 signal_9_hook(lua_State *L, lua_Debug *ar) { 3 void *ud = NULL; 4 lua_getallocf(L, &ud); 5 struct snlua *l = (struct snlua *)ud; 6 7 lua_sethook (L, NULL, 0, 0); 8 if (ATOM_LOAD(&l->trap)) { 9 ATOM_STORE(&l->trap , 0); 10 luaL_where(L, 1); 11 const char *msg = lua_tostring(L, -1); 12 luaL_traceback(L, L, msg, 1); 13 skynet_error(l->ctx, lua_tostring(L, -1)); 14 } 15 } 16 17 void 18 snlua_signal(struct snlua *l, int signal) { 19 skynet_error(l->ctx, "recv a signal %d", signal); 20 if (signal == 0) { 21 if (ATOM_LOAD(&l->trap) == 0) { 22 // only one thread can set trap ( l->trap 0->1 ) 23 if (!ATOM_CAS(&l->trap, 0, 1)) 24 return; 25 lua_sethook (l->activeL, signal_hook, LUA_MASKCOUNT, 1); 26 // finish set ( l->trap 1 -> -1 ) 27 ATOM_CAS(&l->trap, 1, -1); 28 } 29 } else if (signal == 1) { 30 skynet_error(l->ctx, "Current Memory %.3fK", (float)l->mem / 1024); 31 } else if (signal == 9){ 32 // lua_State* L = l->activeL; 33 // CallInfo* ci = L->ci; 34 // for ( ci = L->ci; ci != NULL && ci != &L->base_ci; ci = ci->previous) { 35 // if ((ci->func.p->tbclist.tt_ & 0x3F) == 6) { 36 // Proto *sp = ((union GCUnion *)(ci->func.p->tbclist.value_.gc))->cl.l.p; 37 // char * filename = sp->source ? (char *) ((char *)(sp->source) + 38 // sizeof(TString)) : "unknown"; 39 // skynet_error(l->ctx, "<TRACE %s %d %d>" , filename, sp->linedefined, 40 // sp->lastlinedefined); 41 // } 42 // } 43 44 if (ATOM_LOAD(&l->trap) == 0) { 45 // only one thread can set trap ( l->trap 0->1 ) 46 if (!ATOM_CAS(&l->trap, 0, 1)) 47 return; 48 49 lua_sethook (l->activeL, signal_9_hook, LUA_MASKCOUNT, 1); 50 ATOM_CAS(&l->trap, 1, -1); 51 } 52 } 53 }
最终版本的操作,主要是做了重入的判断,以及对lua虚拟机直接进行了sethook操作,在worker线程中执行了LUA_MASKCOUNT 条指令后,调用一次 signal_9_hook函数,这样也可以保证同时只有一个线程对lua环境进行操作,另外也直接调用了
luaL_traceback 函数生成了堆栈的信息,也比较好看一些。
下面是使用修改后的skynet的一些实际效果图:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)