云风同学开源的skynet,当前规模是8K+ C代码和2K+ lua代码,实现了一个多线程高并发的在线游戏后台服务框架,提供定时器、并发调度、服务扩展框架、异步消息队列、命名服务等基础能力,支持lua脚本。单服务器支持10K+客户端接入和处理。
我个人比较关注高性能和并发调度这块,这两天分析了一下skynet的代码,简单总结一下。
1. 总体架构
一图胜千言,去掉监控、服务扩展、定时器等功能,skynet服务处理的简化框架如下图所示:
每个在线客户的客户端,在skynet server上都对应有一个socket与其连接。一个socket在skynet内部对应一个lua虚拟机和一个”客户特定消息队列“(per client mq)。当客户特定消息队列中有消息时,该队列就会挂载到全局队列(global message queue)上,供工作线程(worker threads)进行调度处理。
skynet的服务处理主流程比较简单:一个socket线程轮询所有的socket,收到客户端请求后将请求打包成一个消息,发送到该socket对应的客户特定消息队列中,然后将该消息队列挂到全局队列队尾;N个工作线程从全局队列头部获取client特定的消息队列,从客户特定消息队列中取出一个消息进行处理,处理完后将该消息队列重新挂到全局队列队尾。
实际代码要更复杂一些:定时器线程会周期性检查一下设置的定时器,将到期的定时器消息发送到client消息队列中;每个lua vm在运行过程中也会向其他lua vm(或自己)的客户特定消息队列发送消息;monitor线程监控各个客户端状态,看是否有死循环的消息等等。本文重点关注消息的调度,因为消息调度的任何细微调整都可能对服务端性能产生很大影响。
另外可以看出,每个客户处理消息时,都是按照消息到达的顺序进行处理。同一时刻,一个客户的消息只会被一个工作线程调度,因此客户处理逻辑无需考虑多线程并发,基本不需要加锁。
2. 并发任务调度方式
lua支持non-preemptive的coroutine,一个lua虚拟机中可以支持海量并发的协作任务,coroutine主要的问题是不支持多核,无法充分利用当前服务器普遍提供的多核能力。所以目前有很多项目为lua添加OS thread支持,比如Lua Lanes,LuaProc等,这些项目都要解决的一个问题就是并发任务的组成以及调度问题。
并发任务可以使用coroutine表示:每个OS线程上创建一个lua虚拟机(lua_State),虚拟机上可以创建海量的coroutine,这种调度如下图所示:
这种OS线程与lua vm 1:1的调度方式有很多优点:
- 每个OS线程都有私有的消息队列,该队列有多个写入者,但只有一个读取者,可以实现读端免锁设计。
- OS线程可以与lua vm绑定,也可以不绑定。由于现代OS都会尽量将CPU core和OS线程绑定,所以如果OS线程与lua vm绑定的话,可以大大减少cpu cache的刷新,提高cache命中率
- lua vm与OS线程个数相当,与任务数无关。大量任务可以共用同一个lua vm,共享其lua bytecode,字符串常量等信息,极大减少每个任务的内存占用。
- 不支持任务跨lua vm迁移。每个任务是一个coroutine,而coroutine是lua vm内部的数据结构,执行中其stack引用了lua vm内部的大量共享数据,无法迁移到另一个lua vm上执行。当一个lua vm上的多个任务都比较繁忙时,只能由一个OS线程串行执行,无法通过work stealing等方式交给其他OS线程并行处理。我以前参与的一个电信项目中就是这种业务和线程绑定的处理方式,对于业务逻辑比较固定的电信业务,各个客户请求的处理工作量类似,因为绑定后CPU使用比较均衡。由于cache locality比较好的原因,这种处理方式性能极高。但对于一些工作量不固定甚至经常变动的客户请求,这种方式很容易造成某些线程很忙,另外一些线程很闲,无法有效利用多核能力。
- 在同一个lua vm内的多个任务,共享lua vm的内存空间。一个任务出现问题时,很容易影响到其他任务。简单说就是任务间的隔离性不好。
static void
skynet_globalmq_push(struct message_queue * queue) {
struct global_queue *q= Q;
uint32_t tail = GP(__sync_fetch_and_add(&q->tail,1));
q->queue[tail] = queue;
__sync_synchronize();
q->flag[tail] = true;
}
struct message_queue *
skynet_globalmq_pop() {
struct global_queue *q = Q;
uint32_t head = q->head;
uint32_t head_ptr = GP(head);
if (head_ptr == GP(q->tail)) {
return NULL;
}
if(!q->flag[head_ptr]) {
return NULL;
}
__sync_synchronize();
struct message_queue * mq = q->queue[head_ptr];
if (!__sync_bool_compare_and_swap(&q->head, head, head+1)) {
return NULL;
}
q->flag[head_ptr] = false;
return mq;
}
static void *
_worker(void *p) {
struct worker_parm *wp = p;
int id = wp->id;
struct monitor *m = wp->m;
struct skynet_monitor *sm = m->m[id];
for (;;) {
if (skynet_context_message_dispatch(sm)) {
//没有取到消息时,会进入这里进行wait,线程挂起
CHECK_ABORT
if (pthread_mutex_lock(&m->mutex) == 0) {
++ m->sleep;
pthread_cond_wait(&m->cond, &m->mutex);
-- m->sleep;
if (pthread_mutex_unlock(&m->mutex)) {
fprintf(stderr, "unlock mutex error");
exit(1);
}
}
}
}
return NULL;
}
static void
wakeup(struct monitor *m, int busy) {
if (m->sleep >= m->count - busy) {
//busy=0,意味着只有挂起线程数sleep=工作线程数count时才会唤醒线程
pthread_cond_signal(&m->cond);
}
}
static void *
_socket(void *p) {
struct monitor * m = p;
for (;;) {
int r = skynet_socket_poll();
if (r==0)
break;
if (r<0) {
CHECK_ABORT
continue;
}
wakeup(m,0); // 参数busy为0
}
return NULL;
}
#define LOCK(q) while (__sync_lock_test_and_set(&(q)->lock,1)) {}
#define UNLOCK(q) __sync_lock_release(&(q)->lock);
int
skynet_mq_pop(struct message_queue *q, struct skynet_message *message) {
int ret = 1;
LOCK(q)
if (q->head != q->tail) {
*message = q->queue[q->head];
ret = 0;
if ( ++ q->head >= q->cap) {
q->head = 0;
}
}
if (ret) {
q->in_global = 0;
}
UNLOCK(q)
return ret;
}
void
skynet_mq_push(struct message_queue *q, struct skynet_message *message) {
assert(message);
LOCK(q)
if (q->lock_session !=0 && message->session == q->lock_session) {
_pushhead(q,message);
} else {
q->queue[q->tail] = *message;
if (++ q->tail >= q->cap) {
q->tail = 0;
}
if (q->head == q->tail) {
expand_queue(q);
}
if (q->lock_session == 0) {
if (q->in_global == 0) {
q->in_global = MQ_IN_GLOBAL;
skynet_globalmq_push(q);
}
}
}
UNLOCK(q)
}
struct message_queue {
uint32_t handle;
int cap;
int head;
int tail;
int lock;
int release;
int lock_session;
int in_global;
struct skynet_message *queue;
};
function skynet.blockcall(addr, typename , ...)
local p = proto[typename]
c.command("LOCK")
local session = c.send(addr, p.id , nil , p.pack(...))
if session == nil then
c.command("UNLOCK")
error("call to invalid address " .. skynet.address(addr))
end
return p.unpack(yield_call(addr, session))
end
static void
_pushhead(struct message_queue *q, struct skynet_message *message) {
int head = q->head - 1;
if (head < 0) {
head = q->cap - 1;
}
if (head == q->tail) {
expand_queue(q);
--q->tail;
head = q->cap - 1;
}
q->queue[head] = *message;
q->head = head;
_unlock(q);
}
static void
_unlock(struct message_queue *q) {
// this api use in push a unlock message, so the in_global flags must not be 0 ,
// but the q is not exist in global queue.
if (q->in_global == MQ_LOCKED) {
skynet_globalmq_push(q);
q->in_global = MQ_IN_GLOBAL;
} else {
assert(q->in_global == MQ_DISPATCHING);
}
q->lock_session = 0;
}