組播方案multicastd

skynet 的新組播方案

最近在做 skynet 的 0.2 版。主要增加的新特性是重新設計的組播模塊。

組播模塊在 skynet 的開發過程中,以不同形式存在過。最終在 0.1 版發佈前刪除了。原因是我不希望把這個模塊放在覈心層中。

隨着 skynet 的基礎設施逐步完善,在上層提供一個組播方案變得容易的多。所以我計劃在 0.2 版中重新提供這個模塊。注:在 github 的倉庫中,0.2 版的開發在 dev 分支中,只到 0.2 版發佈纔會合併到 master 分支。這部分開發中的特性的實現和 api 隨時都可能改變。

目前,我打算提供 publish/subscribe 風格的 API 。組播消息通過 publish 接口發佈出去,所有調用過 subscribe 接口的服務都可以收到消息。

在設計上,每個 skynet 節點都存在一個用於組播的專門服務。組播並非核心層模塊,組播消息不是直接發佈出去的,而需要通過組播服務進行。

在設計組播模塊的結構時,我們需要分兩種情況處理。對於在節點內部傳播的消息,由於在同一進程內,所以會共享一塊內存,用引用計數管理聲明期。對於在網絡中傳播的消息,跨節點時需要複製一份消息內容,用於網絡投遞。

先來看較簡單的內部消息的傳播過程:

單個 skynet 節點中有一個唯一的 multicastd 的服務,在首次請求組播服務時會啓動。

首先,必須有人請求創建一個 channel 。這個 channel 是一個 32bit 的數字,循環後可複用。低 8bit 必須是節點號,這樣可以保證不同節點上創建出來的 channel 號是互不相同的。

multicastd 是用 lua 編寫的,用了一張表記錄 channel 號對應的訂閱者。這裏暫不考慮跨節點訂閱的情況,這張表裏全部記錄的是本地服務地址。

multicast 模塊的 api 全部在 lualib/multicast.lua 中以庫形式提供。一共就是 new, delete, publish, subscribe, unsubscribe 幾個。這些 api 最終都是把請求轉給 multicastd 去處理。也就是說,發佈組播消息,其實比普通的點對點消息傳播路徑要長。

所以值得注意的是:即使在同一服務中,先發送的組播消息未必比後發出的點對點消息要先抵達。但是,對 multicastd 發出 publish 請求,是一個有迴應的 rpc call 。所以同一個 coroutine 中按次序做 publish 和 send 操作還是能保證時序的。這也是爲什麼 publish 方法要設計成會阻塞住當前 coroutine 運行的原因。

組播消息在提交到 multicastd 之前,在發起方就已經被打包成一個 C 結構指針。

struct mc_package {
    int reference;
    uint32_t size;
    void *data;
};

注意被打包的是指針而不是結構。這樣才能做引用計數。消息內容也是用另一個指針間接引用的,這樣方便消息打包。

由於在服務間傳遞的是一個指針,所以這條消息是禁止傳播到進程之外的。這點由 multicast 庫保證(用戶不得直接向 multicastd 服務發送任何請求)。

multicastd 收到 publish 請求後,會統計本地訂閱者的數量,給數據包加上準確的引用次數值,並將消息轉發給所有訂閱者。因爲僅僅是轉發一個指針,比轉發消息體要廉價的多(這也是組播服務的存在意義)。

訂閱和退訂 channel 也是通過 multicastd 進行的。由於時序問題的存在,所以訂閱和退訂都被實現的有一定的容錯行。重複訂閱和重複退訂(以及刪除 channel 和退訂的時序)都會被忽略。這裏退訂被實現爲非阻塞的(不會打斷髮起退訂方的 coroutine ,不必等待確認),是因爲它需要在 gc 的流程中進行,而 gc 的執行上下文是不可控的。


如果組播只存在於進程內,那麼以上都很容易實現。不可忽略的複雜性在於跨進程的多節點組播。

爲了可以讓多個節點間的組播可行。我爲 skynet 增加了一個叫做 datacenter 的基礎組件(當然這個組件在以後別的設施中也將用到)。datacenter 在 master 節點上啓動了一個 lua 實現的樹結構內存數據庫。它對整個 skynet 網絡都是可見的。它就像一個全局註冊表一樣,任何接入 skynet 網絡的節點都可以讀寫它。

每個節點的 multicastd 啓動後,都會把自己的地址註冊到 datacenter 中,這樣別的節點的 multicastd 都可以查詢到兄弟的地址。

如果 multicastd 收到訂閱請求後,它會先檢查 channel 是不是在本地創建的。如果不是,除了要維護本地訂閱這個 channel 的訂閱者名單外,在本地第一次訂閱這個 channel 的同時,要通知 channel 的所有者本節點要訂閱這個 channel 。每個 channel 的管理者地址都可以在 datacenter 內查到。

收到遠程訂閱請求後,本地管理器僅記錄這個 channel 被哪些節點訂閱而不記錄記錄在每個遠程節點上有具體哪些訂閱者。當消息被組播時,對於有遠程訂閱者的 channel ,需要把 struct mc_package 的數據內容提取出來打包傳輸。這裏的一個實現上的優化是,直接把消息走終端客戶的組播通道。因爲 multicastd 本身不會按常規用戶那樣訂閱消息,所以數據格式可以不同(常規客戶訂閱的消息收到的是帶引用計數的結構指針,而 multicastd 收到的就是消息本身)。

對於發佈一個遠程組播包(發佈者和 channel 的創建位置不在同一個節點內),直接把包投遞到 channel 所有地,看成是從那裏發起的(但發送源地址不變)。


對於訂閱者,它收到的組播消息是從專門的協議通道(PTYPE 爲 2 )獲取的。爲了使用靈活,並沒有規定協議的具體編碼形式。需要訂閱者自己註冊 pack 和 unpack 以及 dispatch 函數。默認使用 lua 編碼協議,但可以改寫。

因爲 channel 是一個 32bit 整數,而組播消息是不需要應答的,所以可以複用消息的 session 字段,這也算是一個小優化。


multicast 目前僅提供 lua 層面的 API 。雖然理論上是可以通過 C 層直接收發包,但意義不大。API 以對象形式提供,每個 channel 都是一個 lua 對象。如果創建對象時沒有填具體的 channel 編號,就會調用本地的 multicastd 創建出一個新的 channel 。對象在 gc 時不會銷燬 channel (因爲這個 channel 號有可能被傳遞到別的服務中繼續使用),需要顯式的調用 delete 方法銷燬。但 channel gc 的時候,如果曾有訂閱,會自動退訂。

已知的設計缺陷:

由於 multicast 不在覈心層實現,所以當一條組播消息被推送到目標消息隊列中,在處理消息之前,服務退出。是沒有任何渠道去減消息的引用。這在某些邊界情況下會導致一定的內存泄露。

如果要解決這個泄露問題,必須在發送消息時記錄下消息發給了誰(因爲消息訂閱者可能發生變化)。然後再想其它途徑去釋放它。做到這一點,除了結構上增加複雜度的成本外,運行成本的增加可能也會抵消掉組播的好處(減少數據複製帶來的成本)。

所以,暫時不考慮完全解決這個問題。



對multicast源碼的理解,

(1)爲了實現一個整個skynet網絡的廣播服務(可能包括很多個skynet節點),每個skynet節點都得啓動一個multicastd服務模塊,由multicast.lua中的init()函數裏的multicastd = skynet.uniqueservice "multicastd"可知,每個multicastd服務模塊的實現文件爲multicastd.lua,在同一個節點下,別的服務不能直接給multicastd服務的消息隊列發送消息,也就是不能直接向multicastd服務提出請求,只能通過使用multicast.lua文件中提供的接口來請求multicastd的服務,multicast.lua然後通過向multicastd服務的消息隊列發送消息來達到目的。

如果skynet網絡中有一個服務A和一個服務B,這樣在服務A中multicast = require "multicast.lua" ,A服務就可以使用multicast.lua中提供的接口來使用該節點multicastd服務模塊,同樣multicastd服務模塊可以爲服務B提供服務。

由於是實現整個skynet網絡的廣播服務,而不僅僅是實現單個節點內的廣播服務,所以每個skynet節點需要知道別的skynet節點下的multicastd服務的handle,這樣不同的multicastd服務之間才能進行通信,這樣我們就得依靠一個datacenterd服務,該服務爲整個skynet網絡提供了一個共享數據庫。

當啓動一個multicastd服務時,由multicastd.lua中的skynet.start中的datacenter.set("mulcast", id, self)向數據庫中寫入db[multicast][harbor_id] = multicastd_handle,這樣任何一個multicastd服務就可以通過調用datacenter.get("multicast", harbor_id)而取得harbor節點下的multicastd服務的handle了。

一個multicastd服務中的局部表格有command,channel,channel_n,channel_remote,channel_id。

channel_n[channel_id] 保存訂閱了頻道channel_id的服務的數量;

channel[c][source] = true,保存所有訂閱了頻道c的服務;

channel_remote[channel][node] = true,保存所有訂閱了頻道channel的別的harbor節點;

command中是別的服務通過調用multicast.lua接口而調用的方法。

command.NEW(),創建一個新的chanel_id,低8位和該skynet節點的harbor_id相等。


multicast.lua向multicastd服務提出各種請求採用的協議爲lua協議,而multicastd服務向所有訂閱了該頻道的服務廣播消息時採用的協議是multicast協議。

multicast.new(conf)

如果conf.channel爲空,則新建一個頻道,否則就是綁定這個頻道。綁定這個頻道後,可以往頻道發佈消息,但是不能接收這個頻道的消息,如果想要接收這個頻道的消息就得

先調用subscribe訂閱這個頻道的消息,只有這樣,multicastd服務,纔會當有服務向頻道發佈消息時,multicastd服務向訂閱了該頻道的服務的消息隊列下發multicast協議類型的消息。而凡是需要使用廣播服務的的服務,都會require multicast.lua,從而會把multicast協議註冊到該服務,這樣該服務就可以處理別的服務發送來的消息,由於發送方會把頻道id也發送過來,這樣接收方就可以根據頻道id找到這個頻道對應的處理函數,從multicast.lua的dispatch[channel]中可以找到。

lua-multicast.c

廣播消息的結構

struct mc_package {

int reference;//引用該消息的數量

uint32_t size;//data的長度

void *data;

};

如果一個服務要通過publish(...)發佈消息,首先會調用skynet.pack,skynet.pack會給消息申請一塊內存,然後把消息放入內存中,在棧中保存執向這塊內存的指針以及內存的長度,然後再調用lua-multicast.c中的mc_packlocal(),分別給mc_package結構中的字段賦值,這樣就生成了一個廣播消息。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章