如何使用Zebee構建高度可擴充套件的分散式工作流中介軟體?
Zeebe是一種全新的工作流/編排引擎,適用於雲原生和雲規模應用。本文介紹如何使用Zebee進入雲規模的工作流程自動化的新時代。Zeebe是一個真正的分散式系統,沒有任何中心元件,根據一流的分散式計算概念設計,符合反應性宣言,應用高效能運算技術。
事件溯源
Zeebe基於事件採購的想法。這意味著對工作流狀態的所有更改都將捕獲為事件,並且這些事件將與命令一起儲存在事件日誌中。兩者都被認為是記錄在日誌中。DDD愛好者的快速提示:這些事件是Zeebe內部的,與工作流狀態有關。如果您在域中執行自己的事件源系統,則通常會為域事件設定執行你們自己的事件儲存。
事件是不可變的,因此此事件日誌僅附加。一旦寫完就沒有任何改變 - 就像會計雜誌一樣。僅附加日誌相對容易處理,因為:
- 由於沒有更新,您不能並行存在多個衝突的更新。對狀態的衝突更改始終以清晰的順序捕獲為兩個不可變事件,以便事件源應用程式可以確定如何確定地解決該衝突。在RDMS中實現計數器:如果多個節點並行更新相同的資料,則更新會相互覆蓋。必須認識到並避免這種情況。典型的策略是樂觀或悲觀鎖定與資料庫的ACID保證相結合。僅附加日誌不需要這樣做。
- 已知的策略是複製僅附加日誌。
- 保持這些日誌是非常有效的,因為你總是提前寫。如果您執行順序寫入而不是隨機寫入,則硬碟的效能會更好。
工作流的當前狀態始終可以從這些事件中派生。這被稱為投影。Zeebe中的投影在內部儲存為利用RocksDB的快照,RocksDB是一個非常快速的鍵值儲存。這也允許Zeebe只能透過key獲取資料。純日誌甚至不允許簡單的查詢,例如“給我工作流例項2的當前狀態”。
記錄壓縮
隨著日誌的增長,您必須考慮從中刪除舊資料,這稱為日誌壓縮。例如,在理想的世界中,我們可以刪除所有已結束的工作流例項的事件。不幸的是,這非常複雜,因為來自單個工作流例項的事件可能遍佈整個地方 - 特別是如果您記住工作流例項可以執行數天甚至數月。我們的實驗清楚地表明,進行日誌壓縮不僅效率低下,而且結果日誌變得非常分散。
我們決定以不同的方式做事。一旦我們完全處理了一個事件並將其應用於快照,我們立即將其刪除。我稍後會回來“完全處理”。這使我們可以始終保持日誌乾淨整潔,而不會失去僅附加日誌和流處理的好處 。
儲存
Zeebe 將日誌寫入磁碟,RocksDB也將其狀態重新整理到磁碟。目前,這是唯一受支援的選項。我們經常討論使儲存邏輯可插拔 - 例如支援Cassandra - 但到目前為止我們專注於檔案系統,它甚至可能是大多數用例的最佳選擇,因為它只是最快和最可靠的選擇。
單寫原則
當您有多個客戶端同時訪問一個工作流例項時,您需要進行某種衝突檢測和解決。當您使用RDMS時,通常透過樂觀鎖定或某些資料庫魔法來實現。使用Zeebe,我們使用Single Writer Principle解決了這個問題。正如Martin Thompson所寫:
爭用可變狀態的訪問需要互斥或有條件的更新保護。這些保護機制中的任何一個都會導致在應用競爭更新時形成佇列。為了避免這種爭用和相關的排隊效應,所有狀態都應由單個編寫者擁有,以便進行突變,從而遵循單一編寫者原則。
因此,我們機器上的執行緒數與Zeebe叢集的總體大小無關,始終只有一個執行緒可以寫入某個日誌。這很好:排序很明確,不需要鎖定,也不會發生死鎖。您不會浪費時間管理爭用,但可以隨時進行實際工作。
如果你想知道這是否意味著Zeebe只利用一個執行緒來完成工作流邏輯,那麼到目前為止你是對的!我將在稍後討論縮放Zeebe的問題。
事件處理迴圈
為了更好地理解單個執行緒正在做什麼,讓我們看看如果客戶端想要在工作流中完成任務會發生什麼:
zeebe.newCompleteCommand(someTaskId).send() |
- 客戶端將命令傳送給Zeebe,這是一個非阻塞呼叫,但如果您願意,可以在以後獲得一個Future來接收響應。
- Zeebe將命令附加到其日誌中。
- 日誌儲存在磁碟上(並複製 - 我稍後解決)。
- Zeebe會檢查一些不變數(“我現在可以真正處理此命令嗎?”),更改快照並建立要寫入日誌的新事件。
- 檢查不變數後,即使新事件尚未寫入日誌,也會立即傳送對客戶端的響應。這是安全的,因為即使系統現在崩潰,我們總是可以重放命令並再次獲得完全相同的結果。
- 結果事件將附加到事件日誌中。
- 日誌儲存在磁碟上並進行復制。
如果你深入瞭解事務思路,你可能會問一個問題:“很好 - 但是如果我們改變RocksDB狀態(步驟4)並且在我們將事件寫入日誌之前系統崩潰(步驟6和7)會怎麼樣?”很好的問題!Zeebe僅在處理完所有事件後驗證快照。在任何其他情況下,使用較舊的快照並重新處理事件。
流處理和匯出器
我之前談的是事件採購/溯源。實際上,有一個相關的概念很重要:流處理。由事件(或準確的記錄)組成的僅附加日誌是一個固定的事件流。Zeebe內部基於處理器的概念,每個處理器都是一個執行緒(如上所述)。最重要的處理器實際上是實現BPMN工作流引擎部分,因此它在語義上理解命令和事件,並知道下一步該做什麼。它還負責拒絕無效命令。
但是有更多的流處理器,最重要的是Exporter,這些匯出器還處理流的每個事件。一個開箱即用的匯出器正在將所有資料寫入Elasticsearch,它可以保留在未來並進行查詢。例如,Zeebe操作工具Operate正在利用此資料來視覺化執行的工作流例項,事件等的狀態。
每個匯出器都知道它讀取資料的日誌位置。只要所有流處理器成功處理完資料,就會刪除資料,如上面的日誌壓縮中所述。這裡的權衡是,您不能在以後新增新的流處理器,並讓它從歷史記錄中重放所有事件,就像在Apache Kafka中一樣。
點對點叢集
為了提供容錯和彈性,您可以執行多個Zeebe代理,這些代理形成一個對等叢集。我們以不需要任何中央元件或協調器的方式設計它,因此沒有單點故障。
要形成群集,您需要將至少一個其他代理配置為代理中的已知聯絡點。在啟動代理期間,它會與此其他代理進行通訊並獲取當前的群集拓撲。之後,使用Gossip協議使群集檢視保持最新和同步。
使用Raft Consensus演算法進行復制
現在必須將事件日誌複製到網路中的其他節點。Zeebe使用分散式共識 - 更具體地說是Raft共識演算法 - 來複制經紀人之間的事件日誌。Atomix作為其實現。
基本思想是有一個領導者和一組粉絲。當Zeeber啟動時,他們將選出一位領導者。隨著叢集不斷來回傳送訊息,經紀人會認識到領導者是否已經垮臺或斷開連線並試圖選出新的領導者。只允許領導者對資料進行寫訪問。領導者寫的資料被複制給所有粉絲。只有在成功複製之後,才會在Zeebe代理中處理事件(或命令)。如果您熟悉CAP定理,則意味著我們決定了一致性而不是可用性,因此Zeebe是一個CP系統。(我向Martin Kleppmann道歉,他寫道,請停止呼叫資料庫CP或AP,但我認為這有助於理解Zeebe的架構)。
我們容忍對網路進行分割槽,因為我們必須容忍每個分散式系統中的分割槽,你根本不會對此產生影響(請參閱http://blog.cloudera.com/blog/2010/04/cap-confusion-problems-with- partition-tolerance /和https://aphyr.com/posts/293-jepsen-kafka)。我們決定一致性而不是可用性,因為一致性是工作流自動化用例的承諾之一。
一個重要的配置選項是複製組大小。為了選舉領導者或成功複製資料,您需要一個所謂的法定人數,這意味著對其他Raft成員的一定數量的確認。因為我們要保證一致性,所以Zeebe要求法定人數≥(複製組大小/ 2)+ 1.讓我們舉一個簡單的例子:
- Zeebe節點:5
- 複製組大小:5
- 法定人數:3
因此,如果有3個節點可訪問,我們仍然可以工作。一個網段內必須達到法定數量才能繼續工作,如果只有兩個節點活著則無法執行任何操作,如果有訪問這網段中的兩個節點的客戶端,則它無法繼續訪問,因為CP系統無法保證可用性。
這避免了所謂的裂腦現象,因為你不能最終得到兩個並行完成衝突工作的網段。
複製
當領導者寫入日誌條目時,他們首先會被複制到關注者,然後才能執行。
這意味著可以保證正確複製每個被處理的日誌條目。並且複製可確保不會丟失任何已提交的日誌條目。較大的複製組大小允許更高的容錯能力,但會增加網路上的流量。由於對多個節點的複製是並行完成的,因此實際上可能不會對延遲產生很大影響。此外,代理本身不會被複制阻止,因為這可以被有效地處理
複製也是克服虛擬化和容器化環境中寫入磁碟的挑戰的策略。因為在這些環境中,當資料實際物理寫入磁碟時,您無法控制。即使您呼叫fsync並且它告訴您資料是安全的,也可能不是。但我們更喜歡將資料儲存在幾個伺服器的記憶體中,而不是放在其中一個伺服器的磁碟上。
雖然複製可能會增加Zeebe中命令處理的延遲,但它不會對吞吐量產生太大影響。Zeebe中的流處理器不會堵塞地等待跟隨者的回覆。所以Zeebe可以繼續快速處理 - 但等待他的響應的客戶可能需要等待一段時間。
閘道器
要啟動新的工作流例項或完成任務,您需要與Zeebe交談。最簡單的方法是利用其中一個現成的語言客戶端,例如Java,NodeJs,C#,Go,Rust或Ruby。並且由於gRPC,幾乎可以使用任何程式語言。
客戶端與Zeebe閘道器通訊,後者知道Zeebe代理群集拓撲並將請求路由到該請求的正確領導者。這種設計使Zeebe在雲端或Kubernetes中執行變得非常容易,因為只需要從外部訪問閘道器。
透過分割槽擴充套件
到目前為止,我們討論過只有一個執行緒處理所有工作。如果要利用多個執行緒,則必須建立分割槽。每個分割槽代表一個單獨的物理追加日誌。
每個分割槽都有自己的單寫者,這意味著您可以使用分割槽進行擴充套件。可以分配分割槽
- 單個機器上的不同執行緒或
- 不同的代理節點。
每個分割槽都形成一個自己的Raft組,因此每個分割槽都有自己的領導者。如果執行Zeebe叢集,則一個節點可以是一個分割槽的領導者,也可以是其他分割槽的跟隨者。這可能是執行群集的一種非常有效的方法。
與一個工作流例項相關的所有事件必須進入同一個分割槽,否則我們會違反單寫原則,也無法在本地重新建立代理節點中的當前狀態。
一個挑戰是如何確定哪個工作流例項進入哪個分割槽。目前,這是一種簡單的迴圈機制。啟動工作流例項時,閘道器會將其放入一個分割槽。分割槽ID甚至可以獲得工作流例項ID的一部分,這使得系統的每個部分都可以非常輕鬆地瞭解每個工作流例項所在的分割槽。
一個有趣的用例是訊息關聯。工作流例項可能會等待訊息(或事件)到達。通常,該訊息不知道工作流例項ID,但與其他資訊相關,假設為order-id。因此,Zeebe需要查明是否有任何工作流例項正在等待具有該order-id的訊息。如何有效和橫向擴充套件?
Zeebe只是建立一個訊息訂閱,它位於一個可能與工作流例項不同的分割槽上。該分割槽由相關識別符號上的雜湊函式確定,因此可以透過遞交訊息的客戶端或者到達其需要等待該訊息的點的工作流例項來容易地找到。這種情況發生在哪個(參見訊息緩衝)並不重要,因為單寫者不會發生衝突。訊息訂閱始終連結回等待的工作流例項 - 可能生活在另一個分割槽上。
請注意,當前Zeebe版本中的分割槽數是靜態的。一旦代理群集投入生產,就無法更改它。雖然這在Zeebe的未來版本中可能會有所改變,但從一開始就為您的用例規劃合理數量的分割槽絕對非常重要。有一個生產指南可以幫助您做出核心決策。
多資料中心複製
使用者經常要求進行多資料中心複製。目前尚無特別支援(尚未)。Zeebe叢集在技術上可以跨越多個資料中心,但您必須為增加的延遲做好準備。如果以一種方式設定群集,只有來自兩個資料中心的節點才能達到仲裁,即使是史詩般的災難,也會以延遲為代價。
為什麼不利用Kafka或Zookeeper呢?
很多人都在問我們為什麼要自己編寫上述所有內容,而不是簡單地利用Apache Zookeeper之類的叢集管理器,甚至不是完全成熟的Apache Kafka。以下是此決定的主要原因:
- 易於使用和易於入門。我們希望避免在使用Zeebe之前需要安裝和操作的第三方依賴項。Apache Zookeeper或Apache Kafka不易操作。
- 效率。在核心代理中進行叢集管理允許我們針對具體的用例(即工作流自動化)對其進行最佳化。如果圍繞現有的通用叢集管理器構建,那麼一些功能將更難。
- 支援和控制。在我們作為開源供應商的長期經驗中,我們瞭解到在這個核心級別支援第三方依賴很難。當然,我們可以開始聘請核心Zookeeper貢獻者,但由於參與者有多方,因此仍然很難,所以這些專案的方向不在我們自己的控制之下。透過Zeebe,我們投資控制整個堆疊,使我們能夠全速前進到我們想象的方向。
效能設計
除了可擴充套件性之外,Zeebe還可以在單個節點上實現高效能。
因此,例如我們總是努力減少垃圾。Zeebe是用Java編寫的。Java有所謂的垃圾收集,無法關閉。垃圾收集器會定期啟動並檢查可以從記憶體中刪除的物件。在垃圾回收期間,系統會暫停 - 持續時間取決於檢查或刪除的物件數量。此暫停可能會給您的處理增加明顯的延遲,尤其是如果您每秒處理數百萬條訊息。所以Zeebe的程式設計方式是減少垃圾。
另一種策略是使用環形緩衝區並儘可能利用批處理語句。這也允許您使用多個執行緒而不違反上述單個寫入器原則。因此,每當您向Zeebe傳送事件時,接收器都會將資料新增到緩衝區。從那裡,另一個執行緒將實際接管並處理資料。另一個緩衝區用於需要寫入磁碟的位元組。
此方法可實現批處理操作。Zeebe可以一次性將一堆事件寫入磁碟; 或者透過一次網路往返傳送一些事件給跟隨者。
使用二進位制協議(如gRPC)到客戶端以及內部的簡單二進位制協議,可以非常有效地完成遠端通訊。
總結
Zeebe是一種全新的工作流/編排引擎,適用於雲原生和雲規模應用。Zeebe與其他所有編排/工作流引擎的區別在於它的效能以及它被設計為一個真正可擴充套件且具有彈性的系統,而沒有任何中央元件,或者需要資料庫。
Zeebe不遵循事務工作流引擎的傳統觀念,其中狀態儲存在共享資料庫中,並在從工作流中的一個步驟移動到下一個步驟時進行更新。相反,Zeebe在複製的僅附加日誌之上作為事件源系統工作。所以Zeebe與Apache Kafka等系統有很多共同之處。Zeebe客戶可以釋出/執行工作,因此完全反應式Reactive。
與市場上的其他微服務編排引擎相反,Zeebe非常關注視覺化工作流,因為我們認為視覺化工作流是在設計時,執行時和操作期間提供非同步互動可視性的關鍵。
相關文章
- 讀構建可擴充套件分散式系統:方法與實踐15可擴充套件系統的基本要素套件分散式
- 讀構建可擴充套件分散式系統:方法與實踐09可擴充套件資料庫基礎套件分散式資料庫
- 使用 Python 構建可擴充套件的社交媒體情感分析服務Python套件
- 如何構建可控,可靠,可擴充套件的 PWA 應用套件
- 讀構建可擴充套件分散式系統:方法與實踐05分散式快取套件分散式快取
- 讀構建可擴充套件分散式系統:方法與實踐08微服務套件分散式微服務
- 【軟體架構篇】常見可擴充套件模式架構套件模式
- 讀構建可擴充套件分散式系統:方法與實踐03分散式系統要點套件分散式
- uber/cadence:Cadence是一種分散式,可擴充套件,持久且高度可用的流程編排引擎分散式套件
- 讀構建可擴充套件分散式系統:方法與實踐14流處理系統套件分散式
- Airbyte如何使用Temporal擴充套件工作流程編排?AI套件
- 使用 Zephir 輕鬆構建 PHP 擴充套件PHP套件
- 如何構建一個優雅擴充套件套件
- 如何在高度可擴充套件的系統中管理後設資料套件
- DDD福音:Zeebe是一個類似Kafka的可擴充套件的分散式事件溯源工作流引擎Kafka套件分散式事件
- 讀構建可擴充套件分散式系統:方法與實踐11強一致性套件分散式
- 讀構建可擴充套件分散式系統:方法與實踐06非同步訊息傳遞套件分散式非同步
- DoorDash使用 Kafka 和 Flink 構建可擴充套件的實時事件處理Kafka套件事件
- [譯] 如何使用原生 JavaScript 構建簡單的 Chrome 擴充套件程式JavaScriptChrome套件
- 使用 Postgres 的全文搜尋構建可擴充套件的事件驅動搜尋架構套件事件架構
- Django與微服務架構:構建可擴充套件的Web應用Django微服務架構套件Web
- 讀構建可擴充套件分散式系統:方法與實踐10最終一致性套件分散式
- 使用Kotlin擴充套件函式擴充套件Spring Data案例Kotlin套件函式Spring
- 使用 .NET Core 構建可擴充套件的實時資料處理系統套件
- 閃現, 請求擴充套件, 藍圖, 中介軟體(瞭解)套件
- 讀構建可擴充套件分散式系統:方法與實踐07無伺服器處理系統套件分散式伺服器
- kotlin 擴充套件(擴充套件函式和擴充套件屬性)Kotlin套件函式
- Django內建許可權擴充套件案例Django套件
- 可擴充套件性套件
- 高度可擴充套件,EMQX 5.0 達成 1 億 MQTT 連線套件MQQT
- 讀構建可擴充套件分散式系統:方法與實踐16讀後總結與感想兼導讀套件分散式
- dubbo是如何實現可擴充套件的?套件
- 如何利用容器與中介軟體實現微服務架構下的高可用性和彈性擴充套件微服務架構套件
- 分散式訊息中介軟體分散式
- 【Kotlin】擴充套件屬性、擴充套件函式Kotlin套件函式
- AbpVnext使用分散式IDistributedCache Redis快取(自定義擴充套件方法)分散式Redis快取套件
- 構建高可用性、高效能和可擴充套件的Zabbix Server架構套件Server架構
- 基於Apache Spark以BigDL搭建可擴充套件的分散式深度學習框架ApacheSpark套件分散式深度學習框架