skynet的master/slave 和 cluster

一、同一進程下的服務通訊和跨網絡的通訊到底有什麼不同

  1. 進程內的內存是共享的,skynet 是用 lua 沙盒來隔離服務狀態,但是可以通過 C 庫來繞過沙盒直接溝通。如果一個服務生產了大量數據,想傳給您一個服務消費,在同一進程下,是不必經過序列化過程,而只需要通過消息傳遞內存地址指針即可。這個優化存在 O(1) 和 O(n) 的性能差別,不可以無視。

  2. 同一進程內的服務從底層角度來說,是同生共死的。Lua 的沙盒可以確保業務錯誤能夠被正確捕獲,而非常規代碼不可控的錯誤,比如斷電、網絡中斷,不會破壞掉系統的一部分而另一部分正常工作。所以,如果兩個 actor 你確定在同一進程內,那麼你可以像寫常規程序那樣有一個共識:如果我這個 actor 可以正常工作,那麼對端協作的另一個 actor 也一樣在正常工作。就等同於,我這個函數在運行,我當然可以放心的調用進程內的另一個函數,你不會擔心調用函數不存在,也不會擔心它永遠不返回或是收不到你的調用。這也是爲什麼我們不必爲同一進程內的服務間 RPC 設計超時的機制。不用考慮對方不相應你的情況,可以極大的簡化編寫程序的人的心智負擔。比如,常規程序中,就沒有(非 IO 處理的)程序庫的 API 會在調用接口上提供一個超時參數。

  3. 同一進程內所有服務間的通訊公平共享了同一內存總線的帶寬。這個帶寬很大,和 CPU 的處理速度是匹配的。可以基本不考慮正常業務下的服務過載問題。也就是說,大部分情況下,一個服務能生產數據的速度不太會超過另一個服務能消費數據的速度。這種情況會造成消費數據的服務過載,是我們使用 skynet 框架這幾年來 bug 出現最多的類型。而跨越網絡時,不僅會因爲生產速度和消費速度不匹配造成過載,更會因爲傳遞數據的帶寬和生產速度不匹配而過載。如果讓開發者時刻去考慮,這些數據是投遞到本地、那些數據是投遞到網絡,那麼已經違背了抹平本地和網絡差異這點設計初衷。



二、 harbor 模塊 

利用 4 個字節表示 actor 的地址,其高 8 位是節點編號,低 24 位是進程(節點)內的 id 。這樣,在同一個系統中,不管處於哪個進程下,每個 actor (在 skynet 中被成爲服務)都有唯一的地址。在投遞消息時,無需關心目的地是在同一個進程內,還是通過網絡來投遞消息。

1、 master/slave 模式

不同的 skynet 節點的 harbor 間是如何建立起網絡的呢?這依賴一個叫做 master 的服務。這個 master 服務可以單獨爲一個進程,也可以附屬在某一個 skynet 節點內部(默認配置)。

master 會監聽一個端口(在 config 裏配置爲 standalone 項),每個 skynet 節點都會根據 config 中的 master 項去連接 master 。master 再安排不同的 harbor 服務間相互建立連接。

最終一個有 5 個節點的 skynet 網絡大致是這樣的:

network.png

上面藍色的是 master 服務,下面 5 個 harbor 服務間是互連的。master 又和所有的 harbor 相連。

節點間不再需要兩條連接,而只用一條。每個節點加入網絡(首先接入 master)後,由 master 通知它網絡中已有幾個節點,他會等待所有現存節點連接過來。所以連接建立後,就關閉監聽端口。

如果再有新節點加入網絡,老節點主動去連接新節點。這樣做的好處是,已經在工作的節點不需要打開端口等待。

這套代碼實現在 cmaster.lua 和 cslave.lua 中,取代原來的 service_master.c 和 service_harbor.c ,用 lua 編寫有更大的彈性。這兩個服務還負責同步 skynet 網絡中的全局可見的服務名字,



2、服務ID skynet_handle.c

ID

  • ID的定義是一個 uint32_t 。
  • ID在一個獨立的進程中是唯一的。
  • ID在多個Harbor組成的Skynet網中是唯一的。
  • ID的高8位是harbor ID 。
  • ID的底24爲是此服務模塊在這個進程中的唯一id。
  • 每個ID對應一個獨立的服務模塊,擁有在此進程中唯一的服務名字。

服務ID管理者

handle_storage 源碼

struct handle_name {
    char * name;
    uint32_t handle;
};

struct handle_storage {
    struct rwlock lock;

    uint32_t harbor;
    uint32_t handle_index;
    int slot_size;
    struct skynet_context ** slot;

    int name_cap;
    int name_count;
    struct handle_name *name;
};
static struct handle_storage *H = NULL;

結構體解說

  • slot緩存  
    • slot成員指向一個保存服務上下文指針的緩存。實際上是一個數組,代碼實現了數組大小不足的時候的自動擴容(X2)。
    • slot_size 保存了slot緩存的當前大小。
    • handle_index保存了slot數組的使用大小,slot中的每個服務按照他們install的順序存放,如果有服務退出,後面的所有服務上下文自動前移。
  • harbor 保持次進程的harbor ID。
  • name緩存 
    • name 指向一個服務名字–ID 組合的數組緩存。 同樣具備自動擴容功能(X2)。按照name排序(利用strcmp作爲排序函數)。
    • name_cap 記錄緩存大小
    • name_count 記錄當前緩存使用大小。

提供接口

// 註冊一個服務,返回他的ID
uint32_t skynet_handle_register(struct skynet_context *ctx);

// 利用ID註銷一個服務
int skynet_handle_retire(uint32_t handle);

// 註銷全部服務
void  skynet_handle_retireall();

// 利於ID獲取服務上下文指針
struct skynet_context * skynet_handle_grab(uint32_t handle);

// 利用服務名字獲取服務ID (二分法)
uint32_t skynet_handle_findname(const char * name);

// 賦予一個ID名字
const char * skynet_handle_namehandle(uint32_t handle, const char *name) ;

// 利用給點harbor id初始化
void skynet_handle_init(int harbor);

二、skynet 的集羣方案cluster

它的工作原理是這樣的:

在每個 skynet 節點(單個進程)內,啓動一個叫 clusterd 的服務。所有需要跨進程的消息投遞都先把消息投遞到這個服務上,再由它來轉發到網絡。

在 cluster 集羣中的每個節點都使用一個字符串來命名,由一個配置表來把名字關聯到 ip 地址和端口上。理論上同一個 skynet 進程可以監聽多個消息入口,只要用名字區分開,綁定在不同的端口就可以了。

爲了和本地消息做區分,cluster 提供了單獨的庫及一組新的 API ,這個庫是對 clusterd 服務通訊的淺封裝。當然,也允許建立一個代理服務,把代理服務它收到的消息,綁上指定名字,轉發到 clusterd 。這樣就和之前的 master/slave 模式幾乎沒有區別了。


在過去,skynet 的集羣限制在 255 個節點,爲每個服務的地址留出了 8bit 做節點號。消息傳遞根據節點號,通過節點間互聯的 tcp 連接,被推送到那個 skynet 節點的 harbor 服務上,再進一步投遞。

這個方案可以隱藏兩個 skynet 服務的位置,無論是在同一進程內還是分屬不同機器上,都可以用唯一地址投遞消息。但其實現比較簡單,沒有去考慮節點間的連接不穩定的情況。通常僅用於單臺物理機承載能力不夠,希望用多臺硬件擴展處理能力的情況。這些機器也最好部署在同一臺交換機下。


之前這個方案彈性不夠。如果一臺機器掛掉,使用相同的節點 id 重新接入 skynet 的後果的不可預知的。因爲之前在線的服務很難知道一個節點下的舊地址全部失效,新啓動的進程的內部狀態已經不可能和之前相同。

所以,我用更上層的 skynet api 重新實現了一套更具彈性的集羣方案。

和之前的方案不同,這次我不打算讓集羣間的通訊透明。如果你有一個消息是發放到集羣內另一臺機器中的某個服務的,需要用特別的集羣消息投遞 api 。節點本身用字符串名字,而不是 id 區格。集羣間的消息用統一的序列化協議(爲了簡化協議)。

這套新的方案,可以參考 examples 下的 config.c1 和 config.c2 分別啓動兩個節點相互通訊。

如果使用這套方案,就可以不用老的多節點機制了(當然也可以混用)。爲了簡化配置,你可以將 skynet 配置爲 harbor = 0 ,關閉老的多節點方案。這樣,address standalone master 等配置項都不需要填寫。

取而代之的是,配置一個 cluster 項,指向一個 lua 文件,描述每個節點的名字和地址。

新的 cluster 目前只支持一個 rpc call 方法 。用來調用遠程服務。api 和 skynet.call 類似,但需要給出遠程節點的字符串名字,且通訊協議必須用 lua 類型。

這套新方案可以看成是對原有集羣的一個補充。當你需要把多臺機器部署到不同機房,節點間的關係比較弱,只是少部分具名服務間需要做 rpc 調用,那麼新的方案可能更加合適一些。因爲當遠程節點斷開聯繫後,發起 rpc 的一方會捕獲到異常;且遠程節點用名字索引,不受 255 個限制。斷開連接後,也可以通過重連恢復服務。


這種做法要求明確本地服務的調用和遠程調用的區別。雖然遠程調用的性能可能略低,但由於不像底層 harbor 那樣把本地、遠程服務的區別透明化,反倒不容易出問題。且 tcp 連接使用了更健壯的 socketchannel ,一旦連接斷開,發起 rpc 的一方會收到異常,也可以重試(自動重連)。

而底層的 harbor 假設機器間是可靠連接,不會斷開。而一旦內部網絡不健康,很可能會導致整個系統無法正常工作。它的設計目的並不是爲了提供彈性擴展的分佈式方案,而是爲了突破單機性能上限的問題。

兩個跨機方案各有利弊,所以還請設計系統的時候權衡。只使用其中一個方案或是兩個同時用,應該都有適用的場合。


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