在 skynet 中,有一个叫 monitor 的内部模块,它会监测是否有服务可能陷入了死循环。
工作原理是这样的:每次处理一个服务的一个消息时,都会在一个和服务相关的全局变量处自增 1 。而 monitor 是一个独立线程,它每隔一小段时间(5 秒左右)都检测一下所有的工作线程,看有没有长期没有自增的,若有就认为其正在处理的消息可能陷入死循环了。
而发现这种异常情况后,skynet 能做的也仅仅是输出一行 log 。它无法从外部中断消息处理过程,而死循环的服务,将永久占据一个核心,让系统整体性能下降。
采用 skynet 的 kill 指令是无法杀掉死循环的服务的。
当服务用 lua 编写时,我们则有可能做多一点工作。
对于正统的 lua 程序,我们可以给正在运行的 coroutine 加上一个 debug hook 。在 debug hook 里,则可以检查有没有外部通知说需要中断运行。
由于 skynet 的 lua 服务都由一个 coroutine 的调度器管理,的确可以作到为每个请求都加上这个 debug hook 。但问题是,加上 debug hook 后,对正常运行状态的性能影响很大。
尤其是 lua 版本的 debug hook ,比直接调用 C API 设置一个 C 版的 debug hook 有更大开销。
我认为这个特性对于解决 bug, 尤其是线上解决问题很有帮助,如果能比较少的影响性能,那么用一丁点性呢功能开销才换取这个便利还是值得的。所以我动手修改了 lua 的 vm 实现。
反正在 多虚拟机共享字节码 这个问题上我已经改过一次了,再改改也不嫌多 :) 。只要还可以同时支持未修改版的 lua vm 也可以放在 skynet 里工作就好了。不喜欢的同学可以自己换掉。
我的方案是,在 lua vm 在处理 JMP CALL TAILCALL FORLOOP 这几条 opcode 时,去检查一个全局变量,如果全局变量被设置成和自己的 lua state 相同的指针,就立刻抛出一个异常。
我做了一个简单的测试,对于纯粹的空循环,for 1 亿次,加上这条全局变量检测的代码,会对性能造成 3% 左右的损失。我认为这完全是可以接受的。
剩下的工作就是给 skynet 的服务加上发送信号的接口了。
目前 skynet 的服务在初始化后,只有唯一的一个叫做 callback 的接口在每次有消息进入的时候被调用,且由框架保证线程安全(这个 callback 不会被并发)。
我增加了一个可选接口叫做 signal ,允许在任何时候调用。发送 signal 被放在 skynet 的 command 里,这样可以由任意一个服务向同进程内的其它服务发送信号。信号可以用一个整数来表示及区别其种类。目前,只有 snlua 模块实现了 signal 接口,并不区分具体信号是什么。
在 snlua_signal
中,仅仅只是设置了 lua vm 的那个全局变量。如果 lua 虚拟机用的是原版,则什么都不做。
就这样,我们以一个非常小的代价增加了对异常服务的管理能力。以后当发现 log 中出现了 "maybe in an endless loop" 的信息后,就可以用 debug console 登录上去,通过 signal address 的方式向其发送一个信号。如果 address 是一个 snlua 服务,它将会产生一个 error 。在 log 中可以看到陷入死循环的 lua 代码的调用栈了。 如果之前企图 kill 掉这个异常服务,也会因为异常的消息处理结束而自己退出。
这个 patch 提交在这里。 代码中可以了解更多细节。