雲風同學開源的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>