云风同学开源的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>
skynet代码中,handle的存取使用了读写锁,任务特定队列的访问使用了自旋锁,全局队列的访问使用了wait-free的免锁设计。除了handle存取时多读少写,使用读写锁比较合适之外,任务特定队列和全局队列的调度设计都有待改进。系统中每处理一个消息时会涉及全局队列的一次出队列和一次入队列,全局队列的使用非常频繁,因此看起来wait-free的设计没什么问题。但仔细分析后会发现,在系统比较繁忙时,wait-free设计会导致部分冲突线程出现没必要的等待。将全局队列改为线程特定队列,入队列使用自旋锁而出队列不加锁,会提高多核使用效率。任务特定队列只有一个并发消费者,使用FIFO的队列后,出队列不需要加锁。
转载自 http://spartan1.iteye.com/blog/2059120