Skynet服务器框架(⼗三)在线调试Lua代码
⼀直有⼈问,如何调试 skynet 构建的服务。
我的简单答案是,仔细 review 代码,加 log 输出。长⼀点的答案是,尽量熟悉 skynet 的构造,充分利⽤预留的监控接⼝,⾃⼰编写⼯具辅助调试。
之前的好多年,我也写过很多 lua 的调试器,这⾥就不⼀⼀翻旧帖了。今天要说的是,我最终还是计划加⼊ 1.0 正式版的调试控制台。
也就是单步跟踪调试单个 lua coroutine 的能⼒。这对许多新⼿来说是个学⾛路的拐杖,虽然有⼈⼀辈⼦都扔不掉。
⼀开始我想实现两种模式:冻结住整个服务,慢慢跟踪调试;以及不破坏服务处理其它消息的能⼒,单步调试单条消息的处理流程。
冻结模式的好处是,在调试过程中,服务的整个 lua 虚拟机是挂起的,所以其内部状态是不变的。⽽如果运⾏在调试过程还可以处理其它消息,那么内部状态可能就变来变去了。
但坏处也是很明显的,如果在线上环境,如果是关键服务,可能很快就让整个系统过载(处理消息的速度远远低于正常⽔平,做⾼并发状态下,冻结⼀两秒都是致命的)。
我最后放弃了冻结模式。因为如果在开发期,你的系统同时可能就处理⼏条消息,你也只会调试专⼼编写的部分。所以状态改变的影响是很⼩的。如果有,也可以多加⼀些 log 来提⽰。⽽不打断对外服务能⼒的调试⽅式看起来要舒服的多。
当然,为实现这个调试器需要做的⼀些 C 层的基础设施还是按可以满⾜两种需求来做的。万⼀有⼀天需要冻结模式调试,也⽅便加上。
底层主要是实现⼀个额外的通讯管道,不⾛ skynet 的 message queue 。这样才⽅便绕开 skynet 的服务间通讯机制来调试服务本⾝。
为了可以单步跟踪,我们还需要稍微改造⼀下 lua ⾃带的 debug hook ,让其可以在 hook 中 yield 出来。lua 的 C debug api 本⾝是⽀持的,但是 lua 版 api 屏蔽了这个特性。(这⾥有个坑,可能是 lua 实现的 bug ,后⾯我会谈谈)
有了基础设施后,我们就可以⽤ lua 愉快的搭建调试⼯具了。
⽤户的⼊⼝可以是 debug_console 。但每次需要调试⼀个 lua 编写的服务时,可以单独启动⼀个调试服务利⽤前⾯所述的通讯管道和被调试服务通讯。被调试服务可以在每个消息进来时,处理它之前留个钩⼦,⼀旦发现正被调试,就取出⽤户的操作指令运⾏。
由于 skynet 的服务在没有消息处理时是完全挂起的,所以⼀旦想调试⼀个服务,还必须给它安⼀个定时器定期唤醒检查调试管道(因为调试管道的消息不经过 skynet 的消息调度)。我暂时设置的是⼀秒检查 100 次。通过⼀个调试指令动态开关。
由于 skynet 的服务就是消息驱动的,所以我加了⼀条叫 watch 的指令,可以监控某⼀类消息,并可以附加⼀个条件函数,在条件满⾜时才中断下来。
利⽤前两天编写的,我们可以在消息处理流程中断下来后,观察和改变它的环境。
在 skynet 的 lua53 分⽀上,我已经提交了调试器的代码。有兴趣的同学可以玩玩。⽬前还只提供了⼀些基本特性,以后可能加上更多东西。
使⽤⽅法⼤致是,在启动了 debug console 服务时(默认的 example config 配置启动了它),使⽤ telnet 连接上调试端⼝(通常是
127.0.0.1:8000)。
使⽤ debug address 来 attach ⼊要调试的服务,address 是服务地址。
这个时候,会出现命令⾏提⽰符。当没有 watch 任何消息时,提⽰符是当前服务的地址。
任何使⽤输⼊ cont 都会脱离调试状态。
此刻,上下⽂在这个服务的主线程中。你可以随意输⼊⼀些合法的 lua 表达式或 lua 指令运⾏。也可以调⽤ watch(proto, cond) 函数来加⼀个断点。
watch 的第⼀个参数是⼀个字符串,表⽰你想关注的协议名,⼀般是 "lua" 。
第⼆个参数是⼀个可选参数,它是⼀个函数,参数会传⼊当前消息的参数。如果你返回 true 表⽰关注这条消息。不写第⼆个参数表⽰只要协议类型匹配上即可。
每次 watch 只对⼀条消息有效。
如果匹配到关注的消息,消息处理流程会被挂起。提⽰符会变成停下来的源⽂件名以及⾏号。
除了可以输⼊ lua 表达式以及 lua 指令外(输⼊的语句会放在当前位置执⾏),还可以⽤三条特殊指令:
c 表⽰继续处理这条消息,离开关注状态。
s 表⽰单步运⾏⼀⾏,如果是函数调⽤,会跟踪进去。
n 表⽰单步运⾏⼀⾏,如果有函数调⽤,不会跟踪进去。
例如,你可以⽤ ./skynet example/config 启动⼀个简单的 skynet 进程。如果你没有修改过配置,这个使⽤会启动⼀个叫 simpledb 的服务。
接下来,你可以使⽤ nc 127.0.0.1 8000 接⼊调试控制台。正确接⼊的话,会看到
Welcome to skynet console
这⾏字。
如果你 list 的话,可以看到所有服务:
:01000004      snlua cmaster
:01000005      snlua cslave
:01000007      snlua datacenterd
:01000008      snlua service_mgr
:0100000a      snlua console
:0100000b      snlua debug_console 8000
:0100000c      snlua simpledb
在线代码运行器:0100000d      snlua watchdog
:0100000e      snlua gate
我们可以⽤ simpledb 这个服务做实验。注意:⽬前仅限于调试同⼀进程内的服务。(这个限制是因为实现者特别懒)
输⼊ debug c 或 debug :0100000c 可以 attach 进 simpledb ,然后你会看到 :0100000c> 这样的提⽰符。
你可以输⼊ ... 来检查当前消息是什么。通常你会看到这样的信息(表⽰当前是⼀个 timeout 消息)。因为这个时候 simpledb 在不停的调⽤timer 保持和你的交互。
1      userdata: (nil) 0      226    0
当然,你也可以运⾏你想运⾏的任何 lua 代码。
调试器在这⾥只提供了⼀个叫 watch 的函数,让我们下⼀个条件断点,并跟踪运⾏它。
:0100000c>watch("lua", function(_,_,cmd) return cmd=="get" end)
这时,启动⼀下测试客户端,并输⼊ get hello 。
./3rd/lua/lua examples/client.lua
我们会看到,在输⼊ get hello 后,调试控制台的提⽰符会变成 ./examples/simpledb.lua(18) 表⽰停在了 simpledb.lua 的 18 ⾏。接下来可以⽤ ... 检查这个函数的参数。⽤ n 继续⼀⾏⾏运⾏,直到消息处理完毕。
:0100000c>./examples/simpledb.lua(18)>...
get hello
./examples/simpledb.lua(18)>n
./examples/simpledb.lua(19)>n
./examples/simpledb.lua(23)>n
:0100000c>
如果⽤ s 的话还会跟踪进⼊⼦函数内部。为了⽅便调试,调试器不会进⼊定义在 skynet.lua 的函数⾥(通常你不需要关⼼ skynet 本⾝的实现)。
另外,调试器还提供了⼀个叫_CO的变量,保存在正在调试的协程对象。如果你想使⽤ debug api ,这个变量可能有⽤。例如,可以⽤aceback(_CO) 查看调⽤栈:
:0100000c>watch "lua"
:0100000c>./examples/simpledb.lua(18)>_CO
thread: 0x7fe7f9811dc8
./examples/simpledb.lua(18)&aceback(_CO)
stack traceback:
./examples/simpledb.lua:18: in upvalue 'dispatch'
./lualib/skynet/remotedebug.lua:150: in upvalue 'f'
./lualib/skynet.lua:111: in function <./lualib/skynet.lua:105>
./examples/simpledb.lua(18)>s
./examples/simpledb.lua(19)>s
./examples/simpledb.lua(10)>s
./examples/simpledb.lua(11)&aceback(_CO)
stack traceback:
./examples/simpledb.lua:11: in local 'f'
./examples/simpledb.lua:20: in upvalue 'dispatch'
./lualib/skynet/remotedebug.lua:150: in upvalue 'f'
./lualib/skynet.lua:111: in function <./lualib/skynet.lua:105>
./examples/simpledb.lua(11)>c
最后谈谈 lua 的⼀个问题。
根据 lua 5.3 的⽂档,在 debug hook ⾥是可以调⽤ yield 让出线程的。只要满⾜两个条件:1. 不传⼊任何值,2. 只在 line 和 count 模式下调⽤。
这给实现单⾏运⾏指定 coroutine 提供了⽅便。你可以给指定的 coroutine 挂上 debug hook ,每运⾏⼀⾏就 yield 出来。这样不会影响其它coroutine 的处理,还可以在主线程中去观察它。
但是,⽬前的 lua 5.3 (包括 lua 5.2),如果在 hook 中 yield 后,调⽤ getlocal 去观察挂起线程的局部变量时,进程会 crash 掉。
我花了⼀晚上寻原因。
似乎是因为,挂起的线程,lua 在 callinfo 结构中调整了 func 的值,⽤于保护调⽤栈上的临时变量。(这样⽤ api 去访问挂起线程时,是看不到那些临时变量的)。可以理解为 callinfo 的 func 就是当前栈帧的底。但通常,对于 lua 函数,这个底同时也指向函数对象。⽽ debug api 则需要从这个对象中获得调试信息。
所以⼀旦访问挂起线程顶部 lua 函数的调试信息,很可能访问到⼀个⾮函数对象,结果就挂掉了。
我们平时⽤ coroutine.yield 来让出 coroutine 则不会有问题。这是因为,coroutine.yield 是⼀个 C 函数,也就是栈顶并⾮⼀个 lua 函数,也没有局部变量这样的调试信息可以获取。
只有从 debug hook 中 yield 的线程才有这个问题。它从直接从⼀个 lua 函数中让出,⽽栈顶的信息对于调试 api 来说是错误的。
我尝试打了个 patch (见 skynet 上的提交)绕过这个问题。⼀旦发现从⼀个 yielded 的 coroutine 的顶部取lua 函数的调试信息,则从 extra 域⽽不是 func 域读去函数对象。
bug 已经提交到 lua mailling list 中,不知道 lua 开发团队是否有更好的解决⽅案。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。