在本文中我們將討論的是:
- 用 GoLang 編寫的類似 Apache Camel 的路由引擎
- 嵌入式 WebAssembly 引擎,用於可擴充套件且安全的訊息路由和轉換
- Actors 模型
- OCI 工件
背景
- Apache Camel是一個開源的整合框架,提供了一套工具和模式,以簡化不同應用程式、系統和技術之間的整合。
- WebAssembly(Wasm)是一種低階位元組碼格式,設計為C、C++和Rust等高階語言的可移植編譯目標。
- Actor模型(Actors Model)是一種用於開發並行和分散式系統的併發計算模型。
- OCI Artifacts是一種使用符合Open Container Initiative規範的容器登錄檔來儲存任意檔案的方式。
我們來定義一下首字母縮略詞的含義:
- EIP:企業整合模式
- DSL:領域特定語言
讓我們從一個簡單的例子開始:
- route: |
在 Apache Camel 中,從高層次上講,每條路由都被轉換為一系列函式( Camel 術語中的處理器),每個函式都實現一個特定的 EIP,形成一個執行管道。當事件觸發路由的執行時,管道會將每個函式提交給內部執行器引擎,以便線上程池中執行。
由於新引擎是用 Go 編寫的,我們現在可以利用 Go 的併發構建塊(goroutines、channels)來實現類似的模型。我們甚至可以更進一步,將路由引擎實現為一個 Actor 系統(在這個階段,引擎利用優秀的Proto.Actor庫作為基礎)。
Actor 模型
因為Actor 有一些特點,使得他們在我們的用例中非常適合和有趣,舉一些例子:
- 狀態:表示參與者的內部狀態,取決於具體的參與者;有些參與者是無狀態的,但有些可能需要儲存某些狀態(即節流訊息)
- 行為:在某一時間點針對訊息做出反應而採取的行動。
- 郵箱:連線傳送者和接收者;每個參與者都有一個郵箱,所有傳送者都會將他們的訊息放入其中
- 子級:演員可以建立子級來委派子任務,在這種情況下,演員將自動監督他們。
- 主管: 主管將任務委託給下屬,因此必須對其故障做出響應。當下屬檢測到故障(即恐慌)時,它會向其主管傳送一條訊息,表示故障**。**預設情況下,子 Actor 會將錯誤轉發給他們的主管,直到到達全域性主管。
除了 Actor 的常見功能外,Prto.Actor 庫還提供了一些附加功能(尚未成為 POC 的一部分):
- 中介軟體:允許攔截傳入和傳出的訊息並新增一些特定的行為,例如跟蹤、指標和日誌記錄。
- 位置透明性:參與者的所有互動都使用純訊息傳遞,並且一切都是非同步的,因此在單個參與者系統或機器叢集中執行時,所有功能都應該平等可用,從而可以根據資料親和性規則安排一些處理邏輯
- 永續性:在某些情況下,需要或非常希望永久儲存參與者的狀態,以便可以在參與者重新啟動時恢復,從而使系統能夠從其離開的位置進行恢復。
當在 Camel Go 執行時中載入路由時,執行時會建立一個根參與者,該參與者通常由from定義來描述,然後它會生成其所有子參與者,而這些子參與者本身又可以生成其他子參與者。
這棵Actor 演員樹被稱為Actor 演員系統。
每個父級 Actor 都知道子級 Actor,並可以訪問子級 Actor 的地址,因此父級 Actor 可以向子級 Actor 傳送訊息。子級 Actor 知道他們收到的每條訊息的傳送者是誰,並且會在訊息處理完畢後以訊息的形式回覆傳送者。
參與者系統的一個重要特性是,由於參與者只能透過訊息進行通訊,並且它們依賴於郵箱來確保一次只處理一條訊息,因此參與者可以在單執行緒假象中執行,從而保護參與者的狀態免受任何正常的併發問題的影響。
Wasm 用於擴充套件
我一直在致力於為 Apache Kafka 提供託管連線服務,其中最關鍵但最困難的部分之一是如何以更簡單、更安全的方式執行非平凡的處理邏輯。隨著時間的推移,我們嘗試了許多選項,例如函式、指令碼語言、自定義影像等,但沒有一個真正令人滿意,即
- 指令碼語言:
- 要求使用者最終學習一門新語言
- 由於指令碼可以訪問檔案、環境和其他主機資源,因此執行可能會損害主機系統
- 使應用程式的部署更加複雜,因為它需要與其他資源一起部署(功能)
- 成本通常較高,因為必須在主應用程式和功能之間傳輸資料,這會導致 I/O 並且經常脫離資料區域性性
- 故障處理變得更加複雜
因此,我決定嘗試一下 Wasm,因為 Wasm 的初衷是:
- 多語言:許多語言都支援 Wasm 作為編譯目標,這吸引了更多可能不熟悉執行時語言的開發人員。
- 安全:WebAssembly 的主要目標之一是在沙箱內安全地執行不受信任的程式碼,只有主機可以配置在沙箱中執行的程式碼可以訪問的內容,這使其非常適合外掛/擴充套件。
- 可嵌入: Wasm 執行時可以嵌入到主機應用程式中,從而可以使用任何可以編譯為 Wasm 的語言進行安全地擴充套件,而無需離開應用程式的額外基礎設施或資料。
目前有許多 Wasm 執行時,但由於 Go 是此 POC 的首選語言,因此我利用Wazero,因為它是唯一零依賴的 WebAssembly 執行時(即它不需要任何本機庫繫結)。
在此階段,執行時期望的訊息處理的偽簽名是:
func (inOut Message) error
儘管這是一個非常簡單的函式,但從主機程式呼叫它並不簡單,因為您需要跨越主機/客戶機記憶體邊界,這可以透過多種方式完成,其中包括:
- 透過手動處理 WASM 線性記憶體中的記憶體分配和釋放
- 透過使用 STDIN/OUT 作為交換資料的方式(CGI 任何人)
我的第一次嘗試是使用選項A,這導致我進行了非常長時間的研究,以瞭解如何安全地管理主機和客戶機之間的記憶體,特別是與具有垃圾收集器的語言(例如 Go)相關的記憶體。透過檢視wazero repo 上的分配示例可以看到一些結果(感謝 Adrian Cole 和 Edoardo Vacchi 的耐心和指導),但為了簡單起見(請記住,這只是一個 POC)和可移植性,我決定轉向選項B,在主機端,它最終與此處的示例類似:
func (p *Plugin) invoke(in any, out any) error { |
為了簡化編寫處理器的過程,已經實現了一個小型 SDK:
type Processor func(context.Context, *Message) (*Message, error) |
然後可以利用它來編寫處理器,而不必處理序列化/反序列化和/或分配:
func main() { |
要在路由引擎中使用 Wasm 函式,我們可以利用wasm 語言。例如:
- route: |
Wasm 分發版的 OCI Artifacts
現在我們有了一個可以以某種方式執行的路由引擎(注意: 現階段僅支援少數 EIP),它支援 Wasm 作為實現轉換邏輯的一種方式,我們必須定義如何讓使用者輕鬆運送和使用 Wasm 工件。
當然,有很多方法可以做到這一點,例如使用已編譯的 Wasm 模型構建自定義容器映象,或者使 Wasm 模組在引擎可以讀取的檔案系統中可用,但是,由於幾乎每個雲原生系統都必須處理容器映象登錄檔,我們可以利用 OCI 登錄檔和 OCI Artifacts。
那麼,什麼是 OCI Artifact?
這是一項正在進行的開放容器倡議,旨在定義允許 OCI Registry 儲存任意檔案的規範。這並不是什麼新鮮事,許多專案已經開始使用 OCI Artifacts,例如:
- Web 程式集中心
- 使用基於 OCI 的登錄檔
- GitHub Packages 容器登錄檔現已正式釋出
- sigstore/cosign:容器簽名
- OCI 備忘單 | Flux
在我們的案例中,我們利用ORAS專案輕鬆地將 Wasm 模組打包為 OCI 工件,因此我們需要做的就是設定正確的媒體型別,讓路由引擎識別提供 Wasm 模組的層,例如:
oras push \ |
此命令將使用 Go Routing Engine 所需的媒體型別將simple_process.wasm模組檔案推送到 quay.io(相容的 OCI 登錄檔)。然後,儲存 wasm 模組的層將以檔案路徑命名,因此它將是etc/wasm/fn/simple_process.wasm。
要使用 OCI Artifact,只需指示wasm 語言從影像中查詢模組即可:
- route: |
此時,Camel Go 的wasm語言會檢查已配置的容器映象,然後下載並載入包含已配置的 Wasm 模組的層。
結論
在第一部分中,我詳細介紹了用 GoLang 編寫的類似 Apache Camel 的路由引擎的實現,該引擎利用 Wasm 實現可擴充套件性。在下一篇文章中,我將提供更多實現細節和部署選項。
程式碼可以在我的camel-go儲存庫中找到