skynet剛開始是單進程多線程的,它是由一個一個的服務組成的。在skynet上做開發,實際上就是在寫服務。服務與服務之間通過消息隊列進行通信。
做為核心功能,Skynet 僅解決一個問題:
把一個符合規范的 C 模塊,從動態庫(so 文件)中啟動起來,綁定一個永不重復(即使模塊退出)的數字 id 做為其 handle 。模塊被稱為服務(Service),服務間可以自由發送消息。每個模塊可以向 Skynet 框架注冊一個 callback 函數,用來接收發給它的消息。每個服務都是被一個個消息包驅動,當沒有包到來的時候,它們就會處於掛起狀態,對 CPU 資源零消耗。如果需要自主邏輯,則可以利用 Skynet 系統提供的 timeout 消息,定期觸發。
一個服務,默認不會執行任何邏輯,需要別人向它發出請求時,才會執行對應的邏輯(定時器也是通過消息隊列,告訴指定服務,要執行定時事件),並在需要時返回結果給請求者。請求者往往也是其他服務。服務間的請求、響應和推送,並不是直接調用對方的api來執行,而是通過一個消息隊列,也就是說,不論是請求、回應還是推送,都需要通過這個消息隊列轉發到另一個服務中。skynet的消息隊列,分為兩級,一個全局消息隊列,他包含一個頭尾指針,分別指向兩個隸屬於指定服務的次級消息隊列。skynet中的每一個服務,都有一個唯一的、專屬的次級消息隊列。
skynet服務的本質
每個skynet服務都是一個lua state,也就是一個lua虛擬機實例。而且,每個服務都是隔離的,各自使用自己獨立的內存空間,服務之間通過發消息來完成數據交換。
lua state本身沒有多線程支持的,為了實現cpu的攤分,skynet實現上在一個線程運行多個lua state實例。而同一時間下,調度線程只運行一個服務實例。為了提高系統的並發性,skynet會啟動一定數量的調度線程。同時,為了提高服務的並發性,就利用lua協程並發處理。
所以,skynet的並發性有3點:
1、多個調度線程並發
2、lua協程並發處理
3、服務調度的切換
skynet服務的設計基於Actor模型。有兩個特點:
1. 每個Actor依次處理收到的消息
2. 不同的Actor可同時處理各自的消息
實現上,cpu會按照一定規則分攤給每個Actor,每個Actor不會獨占cpu,在處理一定數量消息后主動讓出cpu,給其他進程處理消息。
skynet的例子是怎么調用的
simpledb.lua: skynet.register “SIMPLEDB” 向skynet里注冊一個服務
agent.lua: skynet.call(“SIMPLEDB”, “text”, text) 調用相應的服務
main.lua: skynet.newservice(“simpledb”) 啟動一個服務
以上函數都在\lualib\skynet.lua 文件內
以下是幾個寫服務時經常要用到的函數。
uniqueservice(name, ...) 啟動一個唯一服務,如果服務該服務已經啟動,則返回已啟動的服務地址。
queryservice(name) 查詢一個由 uniqueservice 啟動的唯一服務的地址,若該服務尚未啟動則等待。
localname(name) 返回同一進程內,用 register 注冊的具名服務的地址。
newservice可以在一個進程里啟動多個服務,這適用於無狀態的服務。
uniqueservice則是類似於設計模式中的單件(singleton),這適用於需要唯一性的服務。舉個例子,比如寫日志,只想寫一份。或者是全局共享的數據。
skynet_handle.c實際上就做了兩個核心的事情,一是給服務分配一個handle,二是把handle和name關聯起來。
把handle和name關聯起來比較容易懂,實際上使用一個數組,關聯的時候使用二分查找到數組里查名字,如果名字不存在,就插入一個元素,然后把名字和handle關聯起來。插入元素的時候,如果數組空間不足了,就擴容為原來的2倍。
而給服務分配handle稍復雜一些,實際上也是使用一個slot數組,數組下標使用的是一個hash,數組元素指向服務的上下文。這個hash的算法是比較簡單粗暴的,就是看從handle_indx開始累計到slot_size,看中間有沒有空閑的下標(也就是下標指向為null的),如果遍歷完了還是沒有,就把slot擴大一倍,還是沒有就再擴大一倍,直到找到空位為止,或者是slot長度超出限制為止。
取到了handle以后呢,還要將harbor id附到handle的高8位。
每個服務分三個運行階段:
首先是服務加載階段,當服務的源文件被加載時,就會按 lua 的運行規則被執行到。這個階段不可以調用任何有可能阻塞住該服務的 skynet api 。因為,在這個階段中,和服務配套的 skynet 設置並沒有初始化完畢。
然后是服務初始化階段,由 skynet.start 這個 api 注冊的初始化函數執行。這個初始化函數理論上可以調用任何 skynet api 了,但啟動該服務的 skynet.newservice 這個 api 會一直等待到初始化函數結束才會返回。
最后是服務工作階段,當你在初始化階段注冊了消息處理函數的話,只要有消息輸入,就會觸發注冊的消息處理函數。這些消息都是 skynet 內部消息,外部的網絡數據,定時器也會通過內部消息的形式表達出來。
從 skynet 底層框架來看,每個服務就是一個消息處理器。但在應用層看來並非如此。它是利用 lua 的 coroutine 工作的。當你的服務向另一個服務發送一個請求(即一個帶 session 的消息)后,可以認為當前的消息已經處理完畢,服務會被 skynet 掛起。待對應服務收到請求並做出回應(發送一個回應類型的消息)后,服務會找到掛起的 coroutine ,把回應信息傳入,延續之前未完的業務流程。從使用者角度看,更像是一個獨立線程在處理這個業務流程,每個業務流程有自己獨立的上下文,而不像 nodejs 等其它框架中使用的 callback 模式。
但框架已經提供了一個叫做 snlua 的用 C 開發的服務模塊,它可以用來解析一段 Lua 腳本來實現業務邏輯。也就是說,你可以在 skynet 啟動任意份 snlua 服務,只是它們承載的 Lua 腳本不同。這樣,我們只使用 Lua 來進行開發就足夠了。
ShareData
當你把業務拆分到多個服務中去后,數據如何共享,可能是最易面臨的問題。
最簡單粗暴的方法是通過消息傳遞數據。如果 A 服務需要 B 服務中的數據,可以由 B 服務發送一個消息,將數據打包攜帶過去。如果是一份數據,很多地方都需要獲得它,那么用一個服務裝下這組數據,提供一組查詢接口即可。DataCenter 模塊對此做了簡單的封裝。
datacenter 可用來在整個 skynet 網絡做跨節點的數據共享。
如果你僅僅需要一組只讀的結構信息分享給很多服務(比如一些配置數據),你可以把數據寫到一個 lua 文件中,讓不同的服務加載它。Cluster 的配置文件就是這樣做的。注意:默認 skynet 使用自帶的修改版 lua ,會緩存 lua 源文件。當一個 lua 文件通過 loadfile 加載后,磁盤上的修改不會影響下一次加載。所以你需要直接用 io.open 打開文件,再用 load 加載內存中的 string 。
另一個更好的方法是使用 sharedata 模塊。
當大量的服務可能需要共享一大塊並不太需要更新的結構化數據,每個服務卻只使用其中一小部分。你可以設想成,這些數據在開發時就放在一個數據倉庫中,各個服務按需要檢索出需要的部分。
整個工程需要的數據倉庫可能規模龐大,每個服務卻只需要使用其中一小部分數據,如果每個服務都把所有數據加載進內存,服務數量很多時,就因為重復加載了大量不會觸碰的數據而浪費了大量內存。在開發期,卻很難把數據切分成更小的粒度,因為很難時刻根據需求的變化重新切分。
如果使用 DataCenter 這種中心式管理方案,卻無法避免每次在檢索數據時都要進行一次 RPC 調用,性能或許無法承受。
sharedata 模塊正是為了解決這種需求而設計出來的。sharedata 只支持在同一節點內(同一進程下)共享數據,如果需要跨節點,需要自行同步處理。
skynet call的實現--服務與服務的交互
https://blog.csdn.net/zxm342698145/article/details/79786802
如果一個服務生產了大量數據,想傳給您一個服務消費,在同一進程下,是不必經過序列化過程,而只需要通過消息傳遞內存地址指針即可。這個優化存在 O(1) 和 O(n) 的性能差別,不可以無視。
架構圖
TimeOutCall
https://github.com/cloudwu/skynet/wiki/TimeOutCall
TinyService
https://github.com/cloudwu/skynet/wiki/TinyService
參考: