RustCon Asia 實錄 | Distributed Actor System in Rust

做者介紹: Zimon Dai,阿里雲城市大腦 Rust 開發工程師。前端

本文根據 Zimon 在首屆 RustCon Asia 大會上的演講整理。編程

你們好,我今天分享的是咱們團隊在作的 Distributed Actor System。首先我想說一下這個 Talk 「不是」關於哪些內容的,由於不少人看到這個標題的時候可能會有一些誤解。安全

圖 1

第一點,咱們不會詳細講一個完整的 Actor System 是怎麼實現的,由於 Actor System 有一個很完善的標準,好比說像 Java 的 Akka, Rust 的 Actix 這些都是很成熟的庫,在這裏講沒有特別大的意義。第二,咱們也不會去跟別的流行的 Rust 的 Actor System 作比較和競爭。可能不少人作 Rust 開發的一個緣由是 Rust 寫的服務器在 Techpower 的 benchmark 上排在很前面,好比微軟開發的 Actix,咱們以爲 Actix 確實寫的很好,而咱們也沒有必要本身搞一套 Actix。第三,咱們不會介紹具體的功能,由於這個庫如今並無開源,但這也是咱們今年的計劃。服務器

這個 Talk 主要會講下面幾個方向(如圖 2),就是咱們在作一個 Actor System 或者你們在用 Actor System 相似想法去實現一個東西的時候,會遇到的一些常見的問題網絡

圖 2

首先我會講一講 Compilation-stable 的 TypeId 和 Proc macros,而後分享一個目前尚未 Stable 的 Rust Feature,叫作 Specialization, 最後咱們會介紹怎麼作一個基於 Tick 的 Actor System,若是你是作遊戲開發或者有前端背景的話會比較瞭解 Tick 這個概念,好比作遊戲的話,有 frame rate,你要作 60 幀,每幀大概就是 16 毫秒,大概這樣是一個 Tick;前端的每個 Interval 有一個固定的時長,好比說 5 毫秒,這就是一個 Tick。架構

1. The TypeId Problem

圖 3

首先講一下 TypeId。如圖 3 ,好比說咱們如今已經有了兩個Actor,它們多是在分佈式系統裏面的不一樣的節點上,要進行網絡傳輸。這個時候你能想到一個很簡單的方式:Actor A 經過機器的 Broker A 發了一個消息,這個消息經過網絡請求到達了另外一個 Broker B,經過這個 Broker B,把這個 Buffer 變成一個 Message 給了目標 Actor B,這是一個常見的網絡通訊。異步

圖 4

可是這裏面會有一個問題,好比,咱們要進行網絡通信的時候,咱們其實是把他編譯成了一個沒有信息的 Buffer,就是一個 Vec,Message 自己是有 Type 的(由於Rust 是強類型的語言,Rust 中全部東西都是有類型的)。怎麼把這個信息抹掉,而後當到了目標 Actor 的時候,再把這個類型恢復回來?這是咱們今天要講 TypeId 的問題。分佈式

1.1 常見的解決辦法

有一個很常見的解決方法,就是給每個 message 的消息頭裏加上這個 message 的類型描述,你們能夠看下圖是一段我寫的僞代碼:ide

圖 5

最重要的就是第一個 field,叫作 type_uid,這個 Message 裏 payload 具體是什麼類型。若是咱們給 Actor System 裏每個消息類型都賦予一個獨特的 TypeId,那麼就能夠根據  TypeId 猜出來這個 Message 的 payload 具體是什麼東西。第二個  field 就是 receiver,其實就是一個目標的 address。 第三個是一個 Buffer,是經過 serialization 的 Buffer。fetch

如今咱們把這個問題聚焦到一個更小的具體問題上:咱們怎麼給每一個消息類型賦予一個獨特的 TypeId?恰好 Rust 有一個東西能夠作這個事情——std::any::Any(圖 6)。

圖 6

Rust 裏面全部的類型都實現了 Any 這個 Trait, 它有一個核心方法,叫作 get _type_id,這個方法剛剛在上週 stable。對任何一個類型調用這個方法的話,就能獲得一個獨特的 TypeId,它裏面是一個 64 位的整數。

有了 TypeId 以後,你們能夠想一下對 TypeId 會有什麼樣的要求?下圖中我列舉了一些最重要的事情

圖 7

首先,這個 TypeId 要對全部的節點都是一致的。好比你有一個消息類型, TypeId 是 1,但在另外一個節點裏面 1 這個整數可能表示的是另外一個消息類型,若是按照新的消息類型去解碼這個消息的話,會出現解碼錯誤。因此咱們但願這個 TypeId 是在整個 Network 裏面都是穩定的。這就致使咱們並不可使用 std 提供的 TypeId。由於很不幸的是 std 的 TypeId 是跟編譯的流程綁定的,在你每次編譯時都會生成新的 TypeId,也就是說若是整個網絡裏部署的軟件正好是來自兩次不一樣的 Rust 編譯的話,TypeId 就會有 mismatch。

這樣就會致使一個問題:即使是更新了一個小小的組件,也可能要從新編譯整個網絡,這是很誇張的。因此咱們如今是利用 Proc Macro 來得到一個穩定的 TypeId 從而解決這個問題。

1.2 Proc Macro

其實這也是社區裏面一個很長久的問題,大概從 2015 年左右就有人開始問,特別是不少作遊戲編程的人,由於遊戲裏 identity 都須要固定的 TypeId。

圖 8

這個問題怎麼解決呢?很簡單,用一個很粗暴的方式:若是咱們可以知道每個消息名字 name,就能夠給每個 name 分一個固定的整數 id,而後把這個組合存到一個文件裏,每次編譯的時候都去讀這個文件,這樣就能夠保證每次生成的代碼裏面是固定的寫入一個整數,這樣 TypeId 就是固定的。

咱們怎麼作到在編譯的時候去讀一個文件呢?其實如今幾乎是惟一的方法,就是去用 Proc Macro 來作這事。咱們看一下這邊咱們定義了(圖 9)一個本身的 TypeId 的類型:

圖 9

UniqueTypeId 這個 Trait 只有一個方法,就是獲取 Type-uid,至關於 std 的 Any; struct TypeId 內部只有一個 field,一個整數 t, TypeId 就至關於 std 的 TypeId。

圖 10

圖 10 上半部分有一個 Message 叫作 StartTaskRequest,這是咱們要使用的消息。而後咱們在上面寫一個 customer derive。圖 10 下半部分就是咱們真正去實現它的時候寫的 Proc Macro,你們能夠看到,咱們是用的 quote,裏面是真正去實現前面咱們講的 UniqueTypeId 的這個 Trait。而後裏面這個 type_uid 方法他返回的 TypeId,其實是固定寫死的。這個 t 的值是 #id,#id 能夠在 customer derive 寫的過程當中從文件中固定讀出來的一個變量。

經過這種方法,咱們就能夠固定的生成代碼,每次就寫好這個 Type,就是這個 integer,不少的 customer derive 可能只是爲了簡化代碼,可是固定 TypeId 是不用 Proc macro 和 Customer derive 絕對作不到的事情。

而後咱們只須要在本地指定一個固定的文件,好比 .toml (圖 10 右下角),讓裏面每個 message 類型都有一個固定的 TypeId,就能夠解決這個問題。

圖 11

得到固定的 TypeId 以後,就能夠用來擦除 Rust 中的類型。能夠經過 serde 或者 Proto Buffer 來作。把 TypeId 序列化成一個 Buffer,再把 Buffer 反序列化成一個具體的 Type。

圖 12

前面講了一種方法,根據 Buffer header 的 signature 猜 Type 類型。這個方法總體感受很像 Java 的 Reflection,就是動態判斷一個 Buffer 的具體類型。具體判斷可能寫這樣的代碼依次判斷這個 message 的 TypeId 是什麼(如圖 12),好比先判斷它是不是 PayloadA 的 TypeId,若是不是的話再判斷是不是 PayloadB 的 TypeId……一直往下寫,可是你這樣也會寫不少不少代碼,並且須要根據全部的類型去匹配。怎麼解決這個問題呢?咱們仍是要用 Proc Macro 來作這個事情。

圖 13

如圖 13,咱們在 Actor 裏定義一個 message 叫作 handle_message,它內部實際上是一個 Macro,這個 Macro 會根據你在寫這個 Actor 時註冊的全部的消息類型把這些 if else 的判斷不停的重複寫完。

圖 14

最後咱們會獲得一個很是簡單的 Actor 的架構(如圖 14)。咱們這裏好比說寫一個 Sample Actor,首先你須要  customer derive Actor,它會幫你實現 Actor 這個 Trait。接下來要申明接收哪幾種消息,#[Message(PayloadA, PayloadB)] 表示 SampleActor 接收的是 PayloadA 和 PayloadB,而後在實現 Actor 這個 Trait 時,customer derive 就會把 if else 類型匹配所有寫徹底,而後只須要實現一個 Handler 的類把消息處理的方法再寫一下。這樣下來整個程序架構會很是清晰。

圖 15

總的來講,經過 Proc Macro 咱們能夠獲得一個很是乾淨的、有 self-explaining 的 Actor Design,同時還能夠把 Actor 的聲明和具體的消息處理的過程徹底分割開,最重要的是咱們能夠把不安全的 type casting 所有都藏在背後,給用戶一個安全的接口。並且這個運行損耗會很是低,由於是在作 integer comparison。

2. Specialization

第二個議題是介紹一下 Specialization,這是 Rust 的一個尚未進入 Stable 的 Feature,不少人可能還不太瞭解,它是 Trait 方向上的一個重要的 Feature。

圖 16

圖 16 中有一個特殊的問題。若是某個消息是有多種編碼模式,好比 Serde 有一個很流行的編碼叫 bincode(把一個 struct 編碼成一個 Buffer),固然也有不少人也會用 Proto-buffer,那麼若是 Message 是來自不一樣的編碼模式,要怎麼用一樣的一種 API 去解碼不一樣的消息呢?

圖 17

這裏須要用到一個很新的 RFC#1212 叫作 Specialization,它主要是提供兩個功能:第一個是它可讓 Trait 的功能實現互相覆蓋,第二個是它容許 Trait 有一個默認的實現。

圖 18

好比說咱們先定義了一個 Payload(如圖 18),這個 Payload 必須支持 Serde 的 Serialization 和 Deserialization, Payload 的方法也是常規的方法,Serialize 和 Deserialize。最重要的是默認的狀況下,若是一個消息只支持 Serde  的編碼解碼,那咱們就調用 bincode。

圖 19

這樣咱們就能夠寫一個實現(圖 19),前面加一個 Default,加了 Default 以後,若是一個 struct 有這幾個 Trait 的支持,那他就會調用 Default。若是多了一個 Trait 的話,就會用多出來的 Trait 的那個新方法。這樣你們就能夠不斷的去經過限制更多的範圍來支持更多 Codec。

Specialization 這個 feature,如今只有 nightly 上有,而後只須要開一個 #![feature(specialization)] 就能夠用。

3. Tick-based actor system

圖 20

下面來介紹一下 Tick-based actor system,就是咱們怎麼在一個基於 Tokio 的 actor system 上面實現Tick,你們都知道  Tokio  是異步的架構,可是咱們想作成基於 Tick 的。

Tick 有哪些好處呢?首先 Tick 這個概念會用在不少的地方,而後包括好比說遊戲設計、Dataflow、Stream computation(流式計算),還有 JavaScript 的 API,也有點 Tick 的 感受。若是整個邏輯是基於 Tick 的話,會讓邏輯和等待機制變得更加簡單,同時也能夠作 event hook。

圖 21

具體作法其實很簡單。咱們能夠設計一個新的 struct,好比圖 21 中的 WaitForOnce,首先聲明一個 deadline,意思是在多少個 Tick 以內我必須得收到一個消息,而後能夠提交這個消息的 signature。咱們在使用 Tokio  來進行 Network IO 時就能夠生成一個 stream,把 stream 每次輸出時 Tick 加 1,咱們就只須要維護一個 concurrent 的 SkipMap,而後把每個 Tick 的 waits 所有註冊進來。當到達這個 Tick 時,若是該 Tick 全部的 waits 都已經覆蓋到了,那你就能夠 release 這個 feature,解決掉。

另外,經過 Tick 也能夠去作一些 actor system 這個 spec 裏面沒有的東西。

圖 22

好比在圖 22 中列舉的,第一點 actor system 不多會容許等待別的 actor,可是基於 Tick 的架構是能夠作的,好比設置 deadline 等於 1,表示在下一個 Tick 執行以前,必須得收到這個消息,實際上就實現了一種 actor 之間互相依賴消息的設置。第二個,咱們還能夠作 pre-fetch,好比如今要去抓取一些資源作預存,不會馬上用這個資源,這樣當我真正使用這些資源的時候他能夠很快獲得,那麼能夠設置一個比較「遙遠」可是沒有那麼「遙遠」的 deadline,好比設置 1000 個 tick 以後,必須拿到一個什麼東西,實際上這個消息的 fetch 會有比較大的時間容錯。

4. 總結

圖 23

最後總結一下咱們的 Distributed Actor System 的一些特性,首先它是基於 Tick 的,而且能夠經過 Specialization 支持多種不一樣的 codecs,而後咱們能夠經過 TypeId 實現相似 reflection 的效果。最後咱們計劃在 2019 年左右的時候開源這個 actor system。其實咱們有不少系統和線上的業務都是基於 Rust 的,咱們也會逐漸的公開這些東西,但願可以在從今年開始跟社區有更多的互動,有更多的東西能夠和你們交流。

RustCon Asia 2019 年 4 月 23 日,由祕猿科技和 PingCAP 主辦的 首屆 RustCon Asia 在北京圓滿落幕,300 餘位來自中國、美國、加拿大、德國、俄羅斯、印度、澳大利亞等國家和地區的 Rust 愛好者參加了本次大會。做爲 Rust 亞洲社區首次「大型網友面基 Party」,本屆大會召集了 20 餘位海內外頂尖 Rust 開發者講師,爲你們帶來一天半節奏緊湊的分享和兩天 Workshop 實操輔導,內容包括 Rust 在分佈式數據存儲、安全領域、搜索引擎、嵌入式 IoT、圖像處理等等跨行業、跨領域的應用實踐。

大會 Talk 視頻合集

www.youtube.com/playlist?li…

相關文章
相關標籤/搜索