Skynet任务调度

Skynet任务调度_第1张图片
Skynet总体架构

每个在线客户端在Skynet服务器上都对应有一个socket与之连接,一个socket在Skynet内部对应一个Lua虚拟机和一个“客户特定消息队列per client mq”。当客户特定消息队列中有消息时,队列就会挂载到全局队列上global message queue,供工作线程worker threads进行调度处理。

服务处理流程

一个socket线程轮询所有的socket,收到客户端请求后会将请求打包成一个消息,发送到该socket对应的客户特定消息队列中。然后将该消息队列挂到全局队列队尾。多个工作线程从全局队列头部获取客户端特定的消息队列,从客户特定消息队列中取出一个消息进行处理,处理完毕后将该消息队列重新挂到全局队列的队尾。

实际上,定时器线程会周期性检查一下设置的定时器,将到期的定时器消息发送到客户端消息队列中,每个Lua虚拟机在运行的过程中,也会向其它Lua虚拟机或自己的客户特定消息队列发送消息。监控线程monitor监视着各个客户端的状态,观察是否有死循环的消息等。

另外,每个客户处理消息时,都是按照消息到达的顺序进行处理。同一时刻,一个客户的消息只能被一个工作线程调度,因此客户处理逻辑无需考虑多线程并发,基本无需加锁。

  • 客户/任务特定消息队列

每个任务特定消息队列都是针对一个在线客户,对应有一个socket和一个Lua虚拟机,这个队列有多个并发生产者,但只有一个并发消费者。对于这种队列,一般可以在入队中使用spin lock,在出队时免锁,但需要使用内存屏障以保证CPU不会乱序执行内存访问指令。不过在Skynet中,入队和出队时都加入了spin lock

  • 全局消息队列

Skynet中有一个“全局消息队列”和在线用户特定的“任务特定消息队列”,这与GO语言1.1版本之前runtime对协程coroutine的调度类似。所有工作线程使用了一个公共的全局队列。Skynet全局队列是一个循环队列,使用数组实现。使用全局队列不是一种很高效的消息调度方式,每个消息的出队入队一般都需要加全局锁。在GO1.1中,优化了调度器算法,每个线程使用局部队列,性能提高很多。Skynet全局队列有多个并发生产者和并发消费者,通常情况下,访问该全局队列是需要加锁的。

Skynet采用wait-free的队列实现方式,在入队时直接将任务特定队列添加到全局队列队尾,没有任何判断队列满的处理,这里假定全局队列永远都不会满。全局队列在初始化时直接为其分配了65536个slot的队列空间,后续不会增长。

一台服务器并发客户端一般不会超过10000,每个客户端对应一个任务特定消息队列,每个任务特定消息队列最多添加到全局队列中一次。根据这些信息,同时在线用户小于65536时队列不会满。正是基于这种假定,Skynet让多个线程并发入队实现了wait-free。这里对任务特定消息队列的访问代码提出了比较严格要求:这些消息队列的访问必须加锁,以防止一个消息队列多次添加到全局队列中。

并发任务调度

Lua支持non-preemptivecoroutine,一个Lua虚拟机中可以支持海量并发的协作任务,coroutine协程主要的问题是不支持多核,无法充分利用当今服务器的多核能力。所以,很多项目为Lua添加操作系统线程支持,例如Lua Lanes、LuaProc等,这些项目都要解决的一个问题是并发任务的组成以及调度问题。

并发任务可以使用协程coroutine表示,每个操作系统线程上创建一个Lua虚拟机Lua_State,Lua虚拟机上可以创建海量的协程coroutine

Skynet任务调度_第2张图片
并发任务使用协程表示

操作系统线程与Lua虚拟机一对一的调度方式的优点

  • 每个操作系统线程都有私有的消息队列,私有队列会有多个写入者,但只有一个读取者,因此可以实现读端免锁设计。
  • 操作系统线程可以与Lua虚拟机绑定,也可以不绑定。现在的操作系统都会尽量将CPU核与操作系统线程绑定。如果操作系统线程与Lua虚拟机绑定的话,可以大大减少CPU缓存的刷新,提高缓存的命中率。
  • Lua虚拟机与操作系统线程个数相当,而与任务的数量无关。大量任务可以公用同一个Lua虚拟机,共享Lua字节码、字符串常量等信息,这将极大地减少每个任务的内存占用。

操作系统线程与Lua虚拟机一对一的调用方式的缺点

  • 不支持任务跨Lua虚拟机迁移,每个任务是一个协程coroutine,而协程是Lua虚拟机内部的数据结构,执行中它的堆stack引用了Lua虚拟机内部的大量共享数据,无法迁移到另一个Lua虚拟机上执行。
    当一个Lua虚拟机上的多个任务都比较繁忙的时候,只能由一个操作系统线程串行执行,无法通过工作窃取work stealing等方式交给其它操作系统线程并行处理。

  • 在同一个Lua虚拟机上的多个任务,共享Lua虚拟机的内存空间。一个任务出现问题时,很容易影响到其它任务。简单来说,就是任务之间的隔离性不好。

另一种处理方式

另一种处理方式是每个Lua虚拟机表示一个任务,系统中的海量并发任务由海量的Lua虚拟机处理。Skynet采用这种方式有效地解决了一对一处理方式上的缺陷。

每个任务完全独立,可以交给任意一个操作系统线程处理,同时任务不会共享Lua虚拟机内存空间,隔离性非常好。一个任务的问题不会影响其它任务的执行,这种方式的主要问题是存在大量内存浪费。每个Lua虚拟机都要加载大量相同的Lua字节码和常量,对内存需求量非常高。这也造成每个任务执行时无法重用CPU缓存,导致缓存命中率很低。官方对此问题的解决方案是修改Lua虚拟机的代码加载机制。同一进程内部的多个Lua虚拟机共享字节码。

具体实现上,有一个独立的Lua虚拟机专门负责加载字节码,并负责字节码的垃圾回收。同一进程中的其它Lua虚拟机共享该独立Lua 虚拟机加载的字节码。这种方式无法解决字符串常量共享的问题,仅仅解决了字节码共享问题。不过即使这样,每个在线用户也节省了1M的内存。

你可能感兴趣的:(Skynet任务调度)