淺析skynet底層框架下篇
這是最後一篇了,其實還有很多重要的模組要分析的,但留給以後有多餘時間再去研究吧,有興趣的可以自行下載原始碼分析。這部分主要是圍繞第三小問題展開,並附加些其他skynet中與此有關的設計,即:當併發時,如何保證訊息的正確時序,以及如何使用協程處理訊息(同步/非同步/超時);包括建立協程處理訊息,掛起協程,切換。這塊其實是針對lua上層來說的,底層框架的訊息佇列只是保證訊息順序入佇列且出佇列,如果交叉執行比如lua層的協程掛起,那麼就會出現時序問題。
先簡單回顧下前幾篇部落格的分析,包括skynet本身的設計,及C++協程。對於C++協程,比如一個請求a過來後,從協程池中pop一個協程並處理該請求a,如果需要等待,則讓出協程並掛上定時器,然後再處理下一個請求b,如果此時a和b是相關聯的,且b有可能依賴於a的執行結果,那麼就會出現問題。這對於遊戲中的業務來說,尤其涉及到金錢相關的邏輯,那是大問題。而那種獨立的請求間,只是讀之類的操作,那是沒問題的。如果需要結合業務,那麼就需要改造。
而對於skynet來說,當併發上來時,考慮到這個時序問題,底層實現相關的順序佇列,大概思路就是lua協程執行a到一半後,哪怕有b的訊息被協程排程處理,此時會把這個b協程壓入佇列(lua中的table也可以,使用陣列部分),必須等a執行完畢或超時後,再處理b的,也就是在業務上層序列化了服務的訊息處理。這樣保證了時序。
但這又引起了另一個問題,即可能存在後面的訊息都超時了,然而上層如果無法識別繼續處理,那麼就白白浪費了資源,處理了無用的訊息。這類的相關介紹在另一篇“談談快取穿透雪崩和過載保護以及一致性等問題”中有相關的介紹及應對方案。
本節分為兩個小點討論,即:
1)如何保證訊息的正確時序,以及如何使用協程處理訊息(同步/非同步/超時);
2)建立協程處理訊息,掛起協程,切換;
第一小點,撇開語言方面的限制,考慮skynet本身的框架設計,而不摻雜業務框架的設計。對於單程式多執行緒,要想併發的處理同一個客戶端的請求,不管是讀還是寫,都必須路由到同一個執行緒處理,這樣就保證了不會導致同一個client的請求分發到不同的執行緒,在skynet底層抽象client為一個agent service,有自己的訊息佇列,並且當工作執行緒處理這個agent訊息時,先把這個訊息佇列從全域性佇列出摘出來,從這個佇列pop一條訊息,處理完畢後,再把這個訊息佇列掛到全域性佇列中;而對於push訊息到agent佇列則沒有這種過程,只要獲得自旋鎖即可,相關原始碼可以見前面的分析。
這一層就保證了訊息不會亂序,但是對於業務層,使用lua協程來提高併發,那麼就要好好設計。
這裡舉例比如在主場景中,這樣可以考慮到client的所有訊息都路由到場景後需要考慮到的時序問題。
當與client有關的兩條有依賴關係的訊息a和b被場景服務dispatch分發處理時,不考慮讀還是寫,都會建立一個協程,並執行相關的處理函式。比如資料安全性不是特別嚴重的例子,玩家在幫派中,然後點領取今日獎勵b訊息,此時幫主把玩家踢出幫派a訊息,本來是a先執行完畢後再b執行的順序,這時可能出現a先執行導致掛起,而b執行完畢後,接著執行a的情況,多領了一份獎勵。當然這裡只是為了舉例,通過檢查可以避免這種問題。
簡單分析下,在skynet的做法中,為每個服務加個lua層的訊息佇列,進入該佇列的訊息會被依次處理完畢,不管中間是否掛起,這樣帶來的問題是,併發度降底了且引入了一定的複雜度。
17 dispatch = function(session, from, ...)
18 table.insert(message_queue, {session = session, addr = from, ... })
19 if thread_id then //有訊息,如果有等待則wakeup
20 skynet.wakeup(thread_id)
21 thread_id = nil
22 end
23 end
26 local function do_func(f, msg)
27 return pcall(f, table.unpack(msg))
28 end
29
30 local function message_dispatch(f)
31 while true do
32 if #message_queue==0 then //沒訊息則掛起
33 thread_id = coroutine.running()
34 skynet.wait()
35 else
36 local msg = table.remove(message_queue,1) //依次處理訊息
37 local session = msg.session
38 if session == 0 then //不需要響應
39 local ok, msg = do_func(f, msg)
40 if ok then
41 if msg then
42 skynet.fork(message_dispatch,f)
44 end
45 else
46 skynet.fork(message_dispatch,f)
48 end
49 else
50 local data, size = skynet.pack(do_func(f,msg))
51 -- 1 means response
52 c.send(msg.addr, 1, session, data, size) //需要響應
53 end
54 end
55 end
56 end
上面程式碼實現細節不作過多分析,簡單註釋了下,大致就是從table陣列中remove前面的訊息並處理之,如果會掛起則等響應結果或超時,再處理下一條。
如上面的實現,新訊息來了fork一個協程處理:
533 function skynet.fork(func,...)
534 local args = table.pack(...) //打包引數
535 local co = co_create(function()
536 func(table.unpack(args,1,args.n)) //設定協程執行函式和引數
537 end)
538 table.insert(fork_queue, co) //回收協程資源
539 return co
540 end
104 local function co_create(f)
105 local co = table.remove(coroutine_pool)
106 if co == nil then
107 co = coroutine.create(function(...)
108 f(...)
109 while true do
110 local session = session_coroutine_id[co]
111 if session and session ~= 0 then
112 local source = debug.getinfo(f,"S")
//log error
117 end
118 f = nil
119 coroutine_pool[#coroutine_pool+1] = co
120 f = coroutine_yield "EXIT"
121 f(coroutine_yield())
122 end
123 end)
124 else
125 coroutine_resume(co, f)
126 end
127 return co
128 end
上面co_create就從協程池中取一個協程物件處理訊息,如果沒有協程物件則建立。你一定會好奇執行完後,返回結果在哪?
對於lua的協程api,當create協程時它的狀態還沒開始,處於掛起suspended狀態,然後resume後會處理running狀態,執行完後為dead狀態,引用下面的:
a)coroutine.create(arg):根據一個函式建立一個協同程式,引數為一個函式;
b)coroutine.resume(co):使協同從掛起變為執行(1)啟用coroutine,也就是讓協程函式開始執行;(2)喚醒yield,使掛起的協同接著上次的地方繼續執行。該函式可以傳入引數;
c)coroutine.yield():使正在執行的協同掛起,可以傳入引數;
而真正強大之處在於當第二次resume時,resume和yield相關交換資料,具體怎麼互動的建議看下lua協程基礎。
在skynet中進行了對lua原始協程api進行封裝並管理,下面說明第二個小點,當然會把第一小點也部分說明下,畢竟是個整體,從建立到處理到回收,以及中間的注意點。通過幾個常用的介面來說明這套工作流程。
以下實現是wakeup
相關:
493 function skynet.wakeup(token)
494 if sleep_session[token] then
495 table.insert(wakeup_queue, token) //在下一次suspend時被處理
496 return true
497 end
498 end
339 function skynet.wait(token)
340 local session = c.genid()
341 token = token or coroutine.running()
342 local ret, msg = coroutine_yield("SLEEP", session, token)//切出協程(A)
343 sleep_session[token] = nil //協程切回來重置相關資料
344 session_id_coroutine[session] = nil
345 end
130 local function dispatch_wakeup()
131 local token = table.remove(wakeup_queue,1)
132 if token then
133 local session = sleep_session[token]
134 if session then
135 local co = session_id_coroutine[session]
136 local tag = session_coroutine_tracetag[co]
137 if tag then c.trace(tag, "resume") end
138 session_id_coroutine[session] = "BREAK"
139 return suspend(co, coroutine_resume(co, false, "BREAK"))(B) 排程被掛起的協程
140 end
141 end
142 end
157 function suspend(co, result, command, param, param2)
//more code
183 elseif command == "SLEEP" then
184 local tag = session_coroutine_tracetag[co]
185 if tag then c.trace(tag, "sleep", co, 2) end
186 session_id_coroutine[param] = co
187 if sleep_session[param2] then
188 error(debug.traceback(co, "token duplicative"))
189 end
190 sleep_session[param2] = param
307 dispatch_wakeup()
308 dispatch_error_queue()
309 end
把要喚醒的協程通過token插入到wakeup_queue
陣列中(注意下,很多實現邏輯是使用table的陣列部分,因為有序但帶來的問題是從索引x處刪除元素後,涉及到移動)
然後dispatch_wakeup
會處理wakeup_queue
,重點是這一句return suspend(co, coroutine_resume(co, false, "BREAK"))
,這部分在後面分析。
(A)處把當前協程切出去後,那三個引數作為主協程的返回值,即coroutine_resume
的返回值,再加一個本身返回的true or false,然後呼叫suspend
,同理coroutine_resume
的後兩個引數作為coroutine_yield
的返回值。
以上部分還是比較容易理解,這裡可以結合c++協程中的實現,有專門的協程排程器,要麼超時要麼有資料過來(響應)進而切回相應的協程處理。
不過經歷過的專案貌似沒有那種加限時的請求,如果call長時間收不到響應,可能會出問題,這個需要多研究下。不過,結合skynet基礎實現也好辦;另外底層框架也是skynet,lua層的原始碼部分都有返回,不管正確還是失敗都會返回,除非這條call請求訊息根本沒有被目標服務的訊息佇列收到(可能出錯),或者沒有被工作執行緒排程,再或者沒有被上層服務處理;前者可能基本為零,第一種可能性不大,因為框架已經保證訊息一定會被髮送到訊息佇列中(訊息佇列目前是無界的),而後面兩種可能確實存在,比如一個死迴圈或者處理耗時的功能等,這些只能靠開發人員注意及必要code review了。
311 function skynet.timeout(ti, func)
312 local session = c.intcommand("TIMEOUT",ti)
313 assert(session)
314 local co = co_create(func)
315 assert(session_id_coroutine[session] == nil)
316 session_id_coroutine[session] = co
317 end
318
319 function skynet.sleep(ti, token)
320 local session = c.intcommand("TIMEOUT",ti)
321 assert(session)
322 token = token or coroutine.running()
323 local succ, ret = coroutine_yield("SLEEP", session, token)
324 sleep_session[token] = nil
325 if succ then
326 return
327 end
328 if ret == "BREAK" then
329 return "BREAK"
330 else
331 error(ret)
332 end
333 end
上面就是超時的實現,也即弄個協程,向skynet框架註冊個定時器,當超時時,發條訊息到上層,上層建立協程處理。這個跟c++協程一樣,實現中不能有sleep這種呼叫,只能用超時,然後掛到事件列表中,超時resume協程回撥,不然阻塞其他。
剩下的不過多分析,這三篇只是簡單分析了個大概,還有蠻多值得學習,關鍵在於思考為什麼要這麼做,可以根據自己的經驗,去嘗試改進或在github上提pr,分析別人的設計,可能並不像作者一路踩坑過來,並持續重構那樣,恰到好處的設計。
接下來的一篇準備研究下鎖的效能,主要是對前幾天學習的一個總結。
skynet 中 Lua 服務的訊息處理
Lua中的協同程式 coroutine
Lua Coroutine詳解
skynet 裡的 coroutine
skynet coroutine 執行筆記
相關文章
- pyspark底層淺析Spark
- 流式處理框架storm淺析(下篇)框架ORM
- ArrayList底層原理淺析
- 【Camera專題】Qcom-Camera驅動框架淺析(Hal層->Driver層)框架
- block底層淺談BloC
- 從底層原始碼淺析Mybatis的SqlSessionFactory初始化過程原始碼MyBatisSQLSession
- 關於 ReentrantLock 中鎖 lock() 和解鎖 unlock() 的底層原理淺析ReentrantLock
- 淺析微服務框架微服務框架
- WebMagic 爬蟲框架淺析Web爬蟲框架
- 淺析Java Web框架技術JavaWeb框架
- vue.js框架原理淺析Vue.js框架
- 淺析大資料框架 Hadoop大資料框架Hadoop
- 【Flink】Flink 底層RPC框架分析RPC框架
- 淺析前端框架如何更新檢視前端框架
- 網路底層測試方法淺談
- 淺析Asp.Net Core框架IConfiguration配置ASP.NET框架
- iOS系統的底層通知框架庫iOS框架
- iOS底層原理 MVC、MVP、MVVM、分層設計淺談 — (13)iOSMVCMVPMVVM
- Kubernetes(k8s)底層網路原理刨析K8S
- 淺析數倉分層:DB+ODS+DW+DM
- 淺析Spring Framework框架容器啟動過程SpringFramework框架
- Dubbo原始碼淺析(一)—RPC框架與Dubbo原始碼RPC框架
- 淺析富文字編輯器框架Slate.js框架JS
- .NET Core 執行緒池(ThreadPool)底層原理淺談執行緒thread
- Java類集框架詳細彙總-底層分析Java框架
- 關於Laravel框架中Guard的底層實現Laravel框架
- OC底層探索(十六) KVO底層原理
- iOS Block淺淺析iOSBloC
- RunLoop 淺析OOP
- 淺析 ReentrantLockReentrantLock
- Unstated淺析
- 淺析SharedPreferences
- Nginx淺析Nginx
- 淺析PromisePromise
- ejs 淺析JS
- 淺析KubernetesStatefulSet
- AIDL淺析AI
- MongoDB淺析MongoDB