做者介紹: Zimon Dai,阿里雲城市大腦 Rust 開發工程師。前端
本文根據 Zimon 在首屆 RustCon Asia 大會上的演講整理。編程
你們好,我今天分享的是咱們團隊在作的 Distributed Actor System。首先我想說一下這個 Talk 「不是」關於哪些內容的,由於不少人看到這個標題的時候可能會有一些誤解。安全
第一點,咱們不會詳細講一個完整的 Actor System 是怎麼實現的,由於 Actor System 有一個很完善的標準,好比說像 Java 的 Akka, Rust 的 Actix 這些都是很成熟的庫,在這裏講沒有特別大的意義。第二,咱們也不會去跟別的流行的 Rust 的 Actor System 作比較和競爭。可能不少人作 Rust 開發的一個緣由是 Rust 寫的服務器在 Techpower 的 benchmark 上排在很前面,好比微軟開發的 Actix,咱們以爲 Actix 確實寫的很好,而咱們也沒有必要本身搞一套 Actix。第三,咱們不會介紹具體的功能,由於這個庫如今並無開源,但這也是咱們今年的計劃。服務器
這個 Talk 主要會講下面幾個方向(如圖 2),就是咱們在作一個 Actor System 或者你們在用 Actor System 相似想法去實現一個東西的時候,會遇到的一些常見的問題。網絡
首先我會講一講 Compilation-stable 的 TypeId 和 Proc macros,而後分享一個目前尚未 Stable 的 Rust Feature,叫作 Specialization, 最後咱們會介紹怎麼作一個基於 Tick 的 Actor System,若是你是作遊戲開發或者有前端背景的話會比較瞭解 Tick 這個概念,好比作遊戲的話,有 frame rate,你要作 60 幀,每幀大概就是 16 毫秒,大概這樣是一個 Tick;前端的每個 Interval 有一個固定的時長,好比說 5 毫秒,這就是一個 Tick。架構
首先講一下 TypeId。如圖 3 ,好比說咱們如今已經有了兩個Actor,它們多是在分佈式系統裏面的不一樣的節點上,要進行網絡傳輸。這個時候你能想到一個很簡單的方式:Actor A 經過機器的 Broker A 發了一個消息,這個消息經過網絡請求到達了另外一個 Broker B,經過這個 Broker B,把這個 Buffer 變成一個 Message 給了目標 Actor B,這是一個常見的網絡通訊。異步
可是這裏面會有一個問題,好比,咱們要進行網絡通信的時候,咱們其實是把他編譯成了一個沒有信息的 Buffer,就是一個 Vec,Message 自己是有 Type 的(由於Rust 是強類型的語言,Rust 中全部東西都是有類型的)。怎麼把這個信息抹掉,而後當到了目標 Actor 的時候,再把這個類型恢復回來?這是咱們今天要講 TypeId 的問題。分佈式
有一個很常見的解決方法,就是給每個 message 的消息頭裏加上這個 message 的類型描述,你們能夠看下圖是一段我寫的僞代碼:ide
最重要的就是第一個 field,叫作 type_uid,這個 Message 裏 payload 具體是什麼類型。若是咱們給 Actor System 裏每個消息類型都賦予一個獨特的 TypeId,那麼就能夠根據 TypeId 猜出來這個 Message 的 payload 具體是什麼東西。第二個 field 就是 receiver,其實就是一個目標的 address。 第三個是一個 Buffer,是經過 serialization 的 Buffer。fetch
如今咱們把這個問題聚焦到一個更小的具體問題上:咱們怎麼給每一個消息類型賦予一個獨特的 TypeId?恰好 Rust 有一個東西能夠作這個事情——std::any::Any(圖 6)。
Rust 裏面全部的類型都實現了 Any 這個 Trait, 它有一個核心方法,叫作 get _type_id,這個方法剛剛在上週 stable。對任何一個類型調用這個方法的話,就能獲得一個獨特的 TypeId,它裏面是一個 64 位的整數。
有了 TypeId 以後,你們能夠想一下對 TypeId 會有什麼樣的要求?下圖中我列舉了一些最重要的事情:
首先,這個 TypeId 要對全部的節點都是一致的。好比你有一個消息類型, TypeId 是 1,但在另外一個節點裏面 1 這個整數可能表示的是另外一個消息類型,若是按照新的消息類型去解碼這個消息的話,會出現解碼錯誤。因此咱們但願這個 TypeId 是在整個 Network 裏面都是穩定的。這就致使咱們並不可使用 std 提供的 TypeId。由於很不幸的是 std 的 TypeId 是跟編譯的流程綁定的,在你每次編譯時都會生成新的 TypeId,也就是說若是整個網絡裏部署的軟件正好是來自兩次不一樣的 Rust 編譯的話,TypeId 就會有 mismatch。
這樣就會致使一個問題:即使是更新了一個小小的組件,也可能要從新編譯整個網絡,這是很誇張的。因此咱們如今是利用 Proc Macro 來得到一個穩定的 TypeId 從而解決這個問題。
其實這也是社區裏面一個很長久的問題,大概從 2015 年左右就有人開始問,特別是不少作遊戲編程的人,由於遊戲裏 identity 都須要固定的 TypeId。
這個問題怎麼解決呢?很簡單,用一個很粗暴的方式:若是咱們可以知道每個消息名字 name,就能夠給每個 name 分一個固定的整數 id,而後把這個組合存到一個文件裏,每次編譯的時候都去讀這個文件,這樣就能夠保證每次生成的代碼裏面是固定的寫入一個整數,這樣 TypeId 就是固定的。
咱們怎麼作到在編譯的時候去讀一個文件呢?其實如今幾乎是惟一的方法,就是去用 Proc Macro 來作這事。咱們看一下這邊咱們定義了(圖 9)一個本身的 TypeId 的類型:
UniqueTypeId 這個 Trait 只有一個方法,就是獲取 Type-uid,至關於 std 的 Any; struct TypeId 內部只有一個 field,一個整數 t, TypeId 就至關於 std 的 TypeId。
圖 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,就能夠解決這個問題。
得到固定的 TypeId 以後,就能夠用來擦除 Rust 中的類型。能夠經過 serde 或者 Proto Buffer 來作。把 TypeId 序列化成一個 Buffer,再把 Buffer 反序列化成一個具體的 Type。
前面講了一種方法,根據 Buffer header 的 signature 猜 Type 類型。這個方法總體感受很像 Java 的 Reflection,就是動態判斷一個 Buffer 的具體類型。具體判斷可能寫這樣的代碼依次判斷這個 message 的 TypeId 是什麼(如圖 12),好比先判斷它是不是 PayloadA 的 TypeId,若是不是的話再判斷是不是 PayloadB 的 TypeId……一直往下寫,可是你這樣也會寫不少不少代碼,並且須要根據全部的類型去匹配。怎麼解決這個問題呢?咱們仍是要用 Proc Macro 來作這個事情。
如圖 13,咱們在 Actor 裏定義一個 message 叫作 handle_message,它內部實際上是一個 Macro,這個 Macro 會根據你在寫這個 Actor 時註冊的全部的消息類型把這些 if else 的判斷不停的重複寫完。
最後咱們會獲得一個很是簡單的 Actor 的架構(如圖 14)。咱們這裏好比說寫一個 Sample Actor,首先你須要 customer derive Actor,它會幫你實現 Actor 這個 Trait。接下來要申明接收哪幾種消息,#[Message(PayloadA, PayloadB)] 表示 SampleActor 接收的是 PayloadA 和 PayloadB,而後在實現 Actor 這個 Trait 時,customer derive 就會把 if else 類型匹配所有寫徹底,而後只須要實現一個 Handler 的類把消息處理的方法再寫一下。這樣下來整個程序架構會很是清晰。
總的來講,經過 Proc Macro 咱們能夠獲得一個很是乾淨的、有 self-explaining 的 Actor Design,同時還能夠把 Actor 的聲明和具體的消息處理的過程徹底分割開,最重要的是咱們能夠把不安全的 type casting 所有都藏在背後,給用戶一個安全的接口。並且這個運行損耗會很是低,由於是在作 integer comparison。
第二個議題是介紹一下 Specialization,這是 Rust 的一個尚未進入 Stable 的 Feature,不少人可能還不太瞭解,它是 Trait 方向上的一個重要的 Feature。
圖 16 中有一個特殊的問題。若是某個消息是有多種編碼模式,好比 Serde 有一個很流行的編碼叫 bincode(把一個 struct 編碼成一個 Buffer),固然也有不少人也會用 Proto-buffer,那麼若是 Message 是來自不一樣的編碼模式,要怎麼用一樣的一種 API 去解碼不一樣的消息呢?
這裏須要用到一個很新的 RFC#1212 叫作 Specialization,它主要是提供兩個功能:第一個是它可讓 Trait 的功能實現互相覆蓋,第二個是它容許 Trait 有一個默認的實現。
好比說咱們先定義了一個 Payload(如圖 18),這個 Payload 必須支持 Serde 的 Serialization 和 Deserialization, Payload 的方法也是常規的方法,Serialize 和 Deserialize。最重要的是默認的狀況下,若是一個消息只支持 Serde 的編碼解碼,那咱們就調用 bincode。
這樣咱們就能夠寫一個實現(圖 19),前面加一個 Default,加了 Default 以後,若是一個 struct 有這幾個 Trait 的支持,那他就會調用 Default。若是多了一個 Trait 的話,就會用多出來的 Trait 的那個新方法。這樣你們就能夠不斷的去經過限制更多的範圍來支持更多 Codec。
Specialization 這個 feature,如今只有 nightly 上有,而後只須要開一個 #![feature(specialization)] 就能夠用。
下面來介紹一下 Tick-based actor system,就是咱們怎麼在一個基於 Tokio 的 actor system 上面實現Tick,你們都知道 Tokio 是異步的架構,可是咱們想作成基於 Tick 的。
Tick 有哪些好處呢?首先 Tick 這個概念會用在不少的地方,而後包括好比說遊戲設計、Dataflow、Stream computation(流式計算),還有 JavaScript 的 API,也有點 Tick 的 感受。若是整個邏輯是基於 Tick 的話,會讓邏輯和等待機制變得更加簡單,同時也能夠作 event hook。
具體作法其實很簡單。咱們能夠設計一個新的 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 中列舉的,第一點 actor system 不多會容許等待別的 actor,可是基於 Tick 的架構是能夠作的,好比設置 deadline 等於 1,表示在下一個 Tick 執行以前,必須得收到這個消息,實際上就實現了一種 actor 之間互相依賴消息的設置。第二個,咱們還能夠作 pre-fetch,好比如今要去抓取一些資源作預存,不會馬上用這個資源,這樣當我真正使用這些資源的時候他能夠很快獲得,那麼能夠設置一個比較「遙遠」可是沒有那麼「遙遠」的 deadline,好比設置 1000 個 tick 以後,必須拿到一個什麼東西,實際上這個消息的 fetch 會有比較大的時間容錯。
最後總結一下咱們的 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、圖像處理等等跨行業、跨領域的應用實踐。