云风同学开源的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的内存空间。一个任务出现问题时,很容易影响到其他任务。简单说就是任务间的隔离性不好。
- <span style="font-size: 14px;">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;
- }
- </span>
- <span style="font-size: 14px;">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;
- }
- </span>
- <span style="font-size: 14px;">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;
- }
- </span>
- <span style="font-size: 14px;">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;
- }
- </span>
- <span style="font-size: 14px;">#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;
- }
- </span>
- <span style="font-size: 14px;">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)
- }
- </span>
- <span style="font-size: 14px;">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;
- };
- </span>
- <span style="font-size: 14px;">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
- </span>
- <span style="font-size: 14px;">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;
- }
- </span>