不断的有人问在 skynet 里怎么给跨服务调用加上超时处理。我不太想再解释为什么在 skynet 这样的系统中,加入超时机制会使得在其上构建的系统增加不必要的复杂度。那只是为了想不到更好的方法而做的一点变通。
你应把 skynet 视为一个整体,正如你平常所写的程序,大多数 api 调用的时候不会设置超时一样。只有在明确的特殊需求时,才在上层在封装一层带超时参数的调用(而不是直接使用 skynet.call )。
当昨天再次收到这个特性请求时,我仔细考虑了一下。发现了一个有趣的方案,可以非入侵性的在现在的框架下加上超时返回这个特性,下面做一个记录:
修改 skynet.lua 这个基础库很不划算,会改变很多基础的调度代码。另外写一套带超时的 call 又会和已有代码大量重复。其实,如果我们单独做一个代理隧道服务,专门转发请求,超时处理就会简单的多。
我们可以实现一个代理服务,它的作用仅仅是将外部请求转发给指定的服务,并将回应包转发回去就够了。在这个代理服务中可以设定一个超时值,一旦转发的请求在指定时间内没有响应,则给调用者一个 error ,并在真正收到回应时忽略掉就可以了。
我们甚至可以在这个代理服务中做许多额外的监测和统计工作,易于发现系统中有问题的设计。而代理服务将实际的服务包裹起来后,对调用者来说,是完全透明的。超时设置只在启动时(或写在配置文件中)设置,使用时,完全不需要修改原有的代码。
担心加一层额外的代理会损失性能是完全没必要的。代理服务几乎没有状态,可以直接用 C 语言编写。主要工作只是做消息转发,甚至不需要额外拷贝消息,而只需要传递指针。用 C 语言编写意味着内存开销可以忽略不计(没有额外的 lua vm ),而纯消息转发的时间开销也是 O(1) 的。
对于超时计时,如果需要不是那么精确的话,以秒为单位即可。我们不需要为每次调用都向系统索取一个 timer ,而可以做成一个时间轮结构。用一个低频心跳工作,我认为 10Hz 甚至 2 Hz 都够。每个心跳检查一下有没有调用超时了,若有,就抛处一个 error 。
暂时我不打算将这个特性加入 skynet 现在的版本(毕竟已经快发布正式版了),除非在我们自己的项目或有人大量使用它证明是有用的,可以考虑在以后的版本中收录。
我用 lua 做了一个简单的实现,放在 gist 上,有兴趣的同学可以玩玩。
https://gist.github.com/cloudwu/63c08b7d6f9be1d0950f
这里有两个文件,totun.lua 是我写的这个代理服务;而 testtun.lua 是一个简单的测试程序。
注意,我特地使用的 skynet 的核心 api 实现的 totun ,而没有使用高层的封装。这使得它可以很容易的翻译成 C 版本(如果有必要)。没有任何 coroutine ,只有对 C 函数的调用。
对 skynet 的基础部分工作原理有兴趣的同学,也可以阅读这份实现。搞清楚仅靠设置一个消息回调函数,skynet 服务是怎样工作的。
由于没有使用上层框架,它虽然是一个 lua 服务,但不能直接使用 skynet.newservice 启动。在 debug console 里也看不到状态,无法用 debug 控制指令控制。如果期望实用的话,还可以进一步做封装。
实际上这个代理服务并不要求一对一,而可以多对一的。即多个调用者对一个服务。它内部重排了 session 号以支持这一点。(如果是一对一设计,可以简单的转发 session 号即可)所以你可以按 service mgr 那样对所有需要代理的服务做统一管理,同一个服务只生成唯一一份代理,而不是在每个使用使用的地方都生成独立的代理。