最近朋友问我,说到他们项目不使用 skynet.call,他不是很理解,问我为啥。
于是,我看了 skynet.call 的代码。实现上,skynet.call 是服务 A 给服务 B 发 request 消息,等服务 B 处理完,再给服务 A 发 response 消息,最后交由服务 A 处理。(源代码可以参考 skynet服务的本质与缺陷)
通常,skynet.call 这个过程是没有问题的,但在服务 B 繁忙无法响应时,就可能有问题。
没有超时机制
skynet.call 没有超时机制,执行的过程不能中断,得一直等到目标服务处理完才返回,所以业务可能出现长时间中断。如果想实现超时,可以利用另外一个协程来唤醒自己。
例子如下:
function skynet.call_timeout(timeout, ...) local arg = table.pack(...) timeout = tonumber(timeout) and math.floor(timeout) if not timeout or timeout <= 0 then return pcall(skynet.call, table.unpack(arg)) end local res local call_id = coroutine.running() skynet.fork(function() res = table.pack(pcall(skynet.call, table.unpack(arg))) if call_id then skynet.wakeup(call_id) end end) skynet.sleep(timeout) call_id = nil if res then return table.unpack(res) else return false, "call timeout" end end
这里虽然实现了超时,但业务可能还有问题。在没有得到 skynet.call 真正响应前,你不知道 call 的目标代码有没被执行到。
这个等待过程中有 3 种情况:
1、call 请求在目标消息队列里,未处理;
2、call 请求从消息队列取出了,正在处理;
3、call 请求处理完了,响应消息在自己消息队列里
有两种办法解决这个问题:
1、加处理状态(待处理,已处理),两侧都检查状态为待处理才执行。(compare and set)
2、call携带超时时间,假设 call timeout 为10秒,目标服务检查超过8秒则不处理。
内存方面的隐患
skynet.call 的实现,是利用协程的挂起和恢复来存续调用状态,使得一个异步的过程在用户层面变成一个同步的过程。这对于用户非常友好的,代码顺序执行,不需要额外写异步处理。
但是,假如目标服务长时间无法响应时,协程就会一直挂起,当系统有大量服务 skynet.call 这个无响应服务时,就会有大量的协程挂起。这些大量服务虽然还能处理新消息,但 skynet 要创建更多的协程去处理。协程有自己的栈和局部变量,如果协程挂起时,这些数据也会一直存在,直到协程被销毁时。
有办法可以解决这个问题:利用 skynet.send 实现 skynet.call,异步处理。
通过 skynet.send 发 request 消息给目标服务,等目标服务处理完后,再 skynet.send 发 response 消息给自己。这样,服务不用挂起等待消息,但需要自行处理异步逻辑。
写在最后
既然提到服务无法响应问题,文章最后就谈下目前 skynet.call 和 skynet.send 都无法解决的问题。如果目标服务无响应,其他服务投递的消息会占据内存,如果消息消费的速度一直低于生产速度,就会导致消息堆积越来越多,占用内存越来越多,最终引起系统崩溃。
所以,对于一些容易繁忙的服务,skynet.call 或 skynet.send 执行前,最好要有办法去获取下目标服务的队列长度,如果过长的就考虑报错,或做其他异常处理。
通过 skynet.mqlen 可以获取当前服务的队列长度,不过,暂时没有接口获取其他服务的消息队列长度,方法可以参考 skynet.mqlen 处理。
// skynet_server.c int skynet_get_mglen(uint32_t handle) { struct skynet_context * ctx = skynet_handle_grab(handle); if (ctx == NULL) { return -1; } return skynet_mq_length(ctx->queue); }
这个我觉得还只是一个比较容易解决的问题,实际中遇到的比较头疼的问题是,业务逻辑经常会依赖另一个服务的数据,经常会碰到有些人在原来的逻辑中插入一个call去拿数据回来导致原先同步执行的逻辑变成异步的,并且这种变化在测试的时候很难发现异常,直到上线之后才出了问题,这时候已经很难改得动了。
困难总比方法多,程序上很多不好的写法都会招致错误,低压力是一种写法,高压力是另一种写法,很多代码在低压力下不会出错,也往往被忽视。再回到问题本身,数据共享涉及到很多情况:单进程还是多进程、读写频率、一致性等等。简单的,call拿回来没问题,但是低频写高频读,频繁call也不是很合理,是否可以考虑内存共享模块