介紹基於事件的架構
譯自:Introduction to Event-Driven Architecture
後面將引入幾篇與EDA相關的文章,目的在於充分掌握EDA架構的優劣勢。
在前面的微服務介紹一文中討論了服務的顆粒度,以及保證鬆耦合的必要性。文中提出,服務應該是自治且完整獨立的,並儘量減少同步通訊。今天,我們將討論鬆耦合意味著什麼,並探索一種在微服務社群中越來越受歡迎的"交易技巧"-事件驅動架構。
簡單定義
事件驅動架構(EDA)是一個促進生產和消費事件的軟體架構規範。
一個事件表示一個感興趣的動作。通常,事件對應一個建立或修改某些實體狀態的動作。例如,在電子商務應用程式中下訂單是一個事件,分發一個已下單的產品也是一個事件。一個消費者提交一個對接收的產品的評論也是一個事件。
永遠不會發生的事件
關於事件的奇特之處在於它們不會明確地傳達給可能關心它們的特定服務。事件"只會發生"。更為重要的是事件只會單純地發生,與是否存在關心這些事件的特定服務無關。這聽起來像是經常被引用的哲學思想:"如果一顆森林中的樹,沒有人聽到它,那麼它會發出聲音嗎?"。但這也是事件之所以強大的原因--事件會轉換為一條對某些正在發生的事情的(自包含)記錄,事件及其擴充套件程式(從根本上講)與它們的處理程式是分離的。實際上,事件記錄的生產者並不知道消費者是誰,甚至不知道是否存在消費者。
一條記錄通常包含描述一個事件的資訊。在之前的訂單為例,其對應的事件的JSON描述如下:
{
"orderId": "760b5301-295f-4fec-95f8-6b303a3b824a",
"customerId": 28623823,
"productId": 31334,
"quantity": 1,
"timestamp": "2021-02-09T11:12:17+0000"
}
Node:儘管記錄和事件存在細微的差別,但它們經常可以互換,即術語"事件"通常指代一個事件的"記錄",為了簡化描述,本文中將自由地使用這兩個術語。
上述是對一個訂單的高度簡化。發起訂單(購物車服務)的應用並不知道誰(如何,以及為什麼)處理該訂單。生產者會保證潛在的消費者能夠捕獲處理事件所需的一切資訊。也就是說,訂單記錄不一定嚴格包含實現訂單所需的每個屬性。例如,不一定會直接指定產品的尺寸,存放位置以及消費者的送貨地址等資訊,但可以解析通過捕獲的訂單記錄中的ID間接獲得這些資訊。關聯式資料庫中的外來鍵概念也同樣適用於事件。
通道傳輸的事件
如果生產者和消費者都互不感知對方,那麼兩者該如何通訊?
答案是通過術語"記錄"進行粘合。事件通常被持久化到一個眾所周知的位置,稱為日誌(有時也會用到術語"賬簿")。日誌是底層的,只能在後續消費者可以訪問的地方附加生產者儲存的事件資料結構。brokers(位於生產者和消費者之間的持久化中介軟體)負責操作日誌。一旦產生了一個事件,任何人都可以消費該事件。
當處理事件驅動系統時,我們經常會使用術語"流"來描述一個或多個日誌介面。日誌是物理上的概念(使用檔案實現),一條流是邏輯上的概念,表示構成事件的一組沒無邊界的記錄,但記錄要遵守某種特定的順序。不同的流平臺可能使用專有名稱指代一條流。Apache Kafka使用topics和partitions來描述流。
生產者、消費者和流的關係如下:
Event-Driven Architecture Reference Model
回顧一下相關概念:
- 事件是在離散時間點發生的感興趣的動作:可能從外部進行觀察和描述。
- 事件持久化為記錄:事件和記錄儘管是相關的,但在技術上是不同的。一個事件表示事情的發生(如狀態變更),本身是無形的。而一條記錄是對該事件的精確描述。我們通常使用術語"事件"來指代其對應的記錄。
- 生產者是通過將相應的記錄釋出到流中來檢測事件的接收器。(釋出一條記錄則表示發生了一個事件)
- 流是持久化的有序的記錄。它們通常由一個或多個基於磁碟的日誌來進行持久化,當然,也可以使用資料庫表、布式共識協議,甚至是區塊鏈式的分散賬本來支援持久化。
- Brokers 負責對流的訪問,方便讀寫操作,處理消費者狀態以及在流上執行各種"內務"。例如,一個broker可能在記錄溢位時對流的內容進行擷取。
- 消費者讀取流,然後對接收到的記錄作出回應。消費者對事件的回應可能會伴隨一些額外的操作。例如,一個消費者可能會在本地資料庫中持久化一條表項(通過釋出的"更新"事件來重構遠端實體的狀態)(即更新對遠端實體的描述)。
- 消費者和生產者可能會重疊。例如,對事件的回應方,也可能產生一個或多個派生的事件。
通過非同步性和通用性進行解耦
為什麼EDA能夠大大降低耦合度?
對耦合的一種比較務實的定義是:一個元件受其他元件影響的程度。耦合存在於空間(元件在結構上相關聯)和時間(時間會影響元件之間的關係程度)上。對於後者,一個比較好的例子是,一個服務同步呼叫其他服務的REST API。如果被呼叫的服務down,則該服務將無法繼續處理(響應被阻塞)。如果兩個服務必須同時執行,則二者之間會存在一定程度的臨時耦合(temporal coupling)。如果兩個服務高度依賴,則稱之為強耦合,反之,則稱為鬆耦合。
Conceptual model of coupling
EDA採用兩種方法來抑制耦合。
- 回顧一下,事件是不能通訊的,它們只會發生。發起事件的元件(通過釋出記錄)並不知道其他元件是否存在。因此,即使消費者不可用,生產者也不會停止工作---broker會暫時快取事件,而不會對生產者施加反向壓力。
- broker對事件記錄的持久化大大消除了時間觀念。一個生產者可能會在T1時間釋出一個事件,而一個消費者可能會在T2事件才會讀取該事件,T1和T2之間的間隔可能是毫秒級別的(所有元件正常)或小時級別的(如果某些消費者down或忙於其他事情)。
EDA並不是銀彈,它沒有一併消除耦合的概念(否則,系統中的元件將不再共同作用)。現在將關注點轉移到broker上:為了讓生產者和消費者有意義地進行解耦,它們必須依賴一個broker。這種方式增加了系統架構的複雜度,並引入了其他故障點。這也是為什麼brokers必須是高效能且具有容錯能力,否則,我們只是將一組問題換成另一組。
事件處理的方式
時間處理通常分為三種常用的方式。這些方式並不互斥,它們經常會同時存在於一個大型的事件驅動系統中。
離散事件處理
用於處理離散事件:例如在社交媒體平臺上釋出一個帖子。離散事件處理的特徵在於出現的事件之間通常並無關聯,可以獨立處理。
事件流處理
用於處理一系列相關聯的無邊界事件流,事件的記錄以某種順序呈現,並攜帶一些與發生的事件有關的資訊。例如,當一個業務實體發生聯合變更時,消費者可能會按照生產者指定的順序進行變更,並在本地資料庫中儲存一份該實體的副本。由於需要關注事件處理的順序,因此不能離散地處理這類事件。消費者需要避免條件競爭,即多個消費者例項可能會同時修改資料庫中的某條記錄,進而由於亂序更新而導致資料不一致。
比較有名的流事件平臺,如Kafka會依賴記錄的key和partitions來保留更新順序。Kafka同時也保證對一個實體的所有變更會被某個消費者處理,避免多個消費者並行處理事件而導致併發競爭。
複雜事件處理
複雜事件處理(CEP)是一種從一系列簡單事件中得出或識別複雜事件的模式。例如監控一座建築內的溫度和延誤感應器,並於推斷是否發生了火情,並進行持續跟蹤。單獨的溫度變化並不足以引發報警。更具意義的是溫度峰值和變化率聚合而成的群體事件,進而有可能挽救生命。
通常更多會涉及此類處理,要求事件處理器持續跟蹤先前的事件,並提供一個有效的方式進行請求和聚合。
什麼時候使用EDA
一些場景下可以使用事件驅動架構帶來的優勢:
- 不透明的消費者生態系統。這種情況下,生產者並不瞭解消費者,後者可能是一個短暫的過程,可能在短時間內來來往往!
- 高扇出。一個事件可能由多個不同的消費者處理的場景。
- 複雜的模式匹配。可能將事件串在一起來推斷出更復雜的事件。(這類場景可能需要進行聚合,即上面描述的複雜事件處理)
- 命令查詢的責任分離。CQRS是一種分離資料儲存區的讀取和更新操作的模式。實現CQRS可以提高應用的可擴充套件性和彈性(在資料一致性上進行了取捨)。 這種模式通常與EDA相關。
EDA的好處
- 快取和容錯能力。事件消費的速率可能與生產者不同步,生產者不能為了與消費者保持一致而放慢速率。
- 生產者和消費者解耦,避免笨拙的點到點整合。EDA下很容易新增新的生產者和消費者,也很容易修改生產者和消費者的實現(前提是遵守約束事件記錄的合同/方案)。
- 大規模擴充套件。通常會把部分部分事件流切分為若干不相關的自流,然後並行處理。隨著事件的積壓,我們也可以擴充套件消費者的數量來滿足負載需求。像Kafka這樣的平臺會嚴格按序處理事件,並允許跨流進行大規模並行處理。
EDA的缺點
- 僅限非同步處理。雖然EDA是一種有效的系統解耦模式,但它也將應用限制為非同步事件處理。EDA並不能很好地處理像請求響應這樣的互動(發起者必須等待響應才能繼續處理)。
- 引入額外的複雜度。傳統的客戶端-伺服器以及請求-響應僅會涉及兩方,在採用EDA之後則引入了第三方-broker,作為生產者和消費者之間的媒介。
- 故障掩蓋。這一點比較奇特,因為它似乎與解耦系統的本質背道而馳。當系統高度耦合時,一個系統中的錯誤會快速傳遞下去,並引起我們的關注。大多數場景下,我們需要避免這種情況:當一個元件失敗時,儘量減小它對其他元件的影響。故障掩蓋的負面影響是,它會在不經意間隱藏本應引起我們注意的問題。可以通過為每個事件驅動的元件新增實時監控和日誌來解決,但這樣做也帶來了新的複雜度。
需要注意的點
EDA不是萬能藥,與很多強大的工具一樣,它有可能被錯誤地使用。下面列出的內容不應該被認為是EDA的缺點,而應該作為開發人員和架構師在設計和實現事件驅動的系統時應注意的一系列陷阱。
-
複雜的編排。使用鬆耦合元件,使用者可能會感到困惑,整個架構看起來像是一個Rube Goldburg機器(可以藉助下圖理解Rube Goldburg),整個業務邏輯也被實現為一系列(帶有副作用的包裝的)事件:一個元件發起的事件可能觸發另一個元件發起另一個事件,然後觸發另一個元件發起事件,以此類推。這種元件間的互動很快會變得無法理解。
-
將命令和事件混淆。一個事件用於單純地描述發生的事情。它不會指定如何處理事件。而一個命令是針對特定元件的直接指令。由於命令和事件都是某種型別的訊息,非常容易混淆,把命令誤以為是一個事件。
命令也可以放到EDA下,但要分清與事件的區別。命令可能會修改系統狀態,通常會需要回滾方案。
-
消費者不可知。事件應該以某種方式捕獲相關的屬性,但並不會限制如何處理這些事件。說起來容易,做起來難。有時我們可能會無法獲得足夠的資訊來限制新增到事件記錄的內容(無法確定這些新增到記錄中的資訊是否最終有用)。
個人認為最重要的是上面的第二點,要區分命令和事件。
總結
微服務架構模式是構建更可維護、可擴充套件、更健壯的軟體系統所涉及的難題之一。從問題分解的角度來看,微服務非常棒,但也帶來了很多棘手的問題,其中一個就是耦合。與一開始相比,隨意將系統拆分為少數微服務的做法可能會使您處於更糟糕的局面。有一個術語可以對其進行描述"分佈一體式"。
為了幫助解決困惑,並定位耦合的問題,我們引入了事件驅動架構。
EDA是一個可以幫助降低系統元件間的耦合的有效工具,它是一種使用生產者、消費者、事件和流進行互動的模型。一個事件表示一個感興趣的動作,任何元件都可能非同步地釋出和消費事件,而無需感知對方的存在。EDA允許元件獨立操作和演化。但它不是解決所有問題的銀彈。EDA是一個不錯的選擇,它帶來的好處大大超過了採用它的成本。可以說,EDA是成功部署微服務的必要要素。