skynet 消息分发及服务调度的新设计
这个月 skynet 的 1.0 就会 release 最终版了,除了维护这个稳定版本。我考虑可以对一些不太满意的地方尝试做大刀阔斧的改变(当然不放在目前的稳定版本中)。
我对 skynet 解决的核心问题:多服务任务调度以及内部消息传播这块不是很满意,觉得如果换个方式实现可能会好一些。下面先把想法记下来。
目前,每个服务都有一个唯一的消息队列,且在内存足够的前提下,会无限增长。也就是说,向一个服务发消息是没有失败的可能的。多数情况下,单个服务的消息队列不会太长,在生产消费模型中,也不允许太长。太长意味着消费速度远远低于生产速度,情况多半会恶化。在历史上发生过多起事故,都是和服务过载 有关。
虽然 skynet 提供了 mqlen 这种方法供使用者查询当前服务的消息队列长度,以做出应变,但治标不治本。我想做一个大的设计改动来重新考虑这一块。
我们可以把每个服务的消息队列实现成固定长度,且固定长度并不需要太长,大约 256 个 slot 这种级别就足够了。一个固定长度的循环队列实现起来要简单的多,且很容易做成进出队列互不影响。因为出队列只发生在一个服务内,不可能并发,根本不需要考虑竞争;而仅仅只需要考虑进队列的竞争问题。
当队列满,或有人在入队列操作时,都认为队列忙,不需要做锁,直接失败即可。遇到忙的时候发送方可以自行缓存代发数据。而多方同时写入队列时,对 skynet 来说,其实不需要保证先后次序,有时序要求的仅仅是同一发送者对一个特定接受者。在队列不忙时,谁先写都是没问题的;且同一服务下,当有多个待发出队列时,先处理哪一个也不太所谓。
这样就可以把消息进出这块的所有竞争都去掉,实现起来也非常简单。而且可以大大缓解服务过载后的雪崩问题。因为退出一些有未发出消息的服务,那些没有发出的消息也自然被扔掉了。而目前的设计则会将所有消息都堆积在一条消息队列中,这些消息在玩家频繁上下线时会有大量无效信息。
这里提到的新设计比现在复杂的一点是,该什么时候唤醒一个服务。在现有的情况下,只有一个服务获得新消息,且不在热服务队列中,它才会把自己压入热服务队列,待工作线程去处理。而做出以上改变后,一个服务又未发出的消息时,也需要在接收方解除拥塞后唤醒。
我的想法是索性把服务的任务调度也一并改进。采用一个更简单更粗暴的方式来做。
目前其实把服务分成两类的,一类是热服务,就是有消息待处理的;另一类是冷服务,服务活着,但消息队列暂时为空。工作线程只要从热服务队列中依次取出服务调用回调函数即可。
这么设计是考虑到热服务的数量通常远小于总的服务数量,如此能减少工作线程轮询一个服务是否有消息可处理的开销。
是不是可以考虑另一种算法呢?
每个工作线程可以重复一个工作循环。
第一步就是遍历所有的服务,挑选出当前有事情要做(包括有消息要处理,有消息发出被拥塞)的服务,去掉正在被别的工作线程处理的那些(服务忙),把这些服务放在当前工作线程下的一个集合中。
第二步,依次处理自己集合中的服务消息。在处理工作中,如果碰到别的工作线程已经在处理,或消息队列空,或待发队列依然无法处理(接收方拥塞)则立刻将该服务从自己的集合中去掉。
由于移出集合的条件很宽松,而不会加入新的元素,所以只要不断循环第二步,自己所属的集合会越来越小。但为了防止某几个服务霸占工作线程,还可以加一个循环次数上限,相当于让过热的服务有个冷却的机会。
当第二步的集合为空,或是达到了循环上限。那么结束这个工作循环,回到第一步继续。
以上是一些初步的想法,晾到年后再动手实现。