天天看點

軟體架構-事件驅動架構

軟體架構-事件驅動架構

你好,我是看山。

本文源自并發程式設計網的翻譯邀請,翻譯的是 Jakob Jenkov 的 《軟體架構》 中關于事件驅動的内容,雖然是 2014 年的文章,但是從軟體架構層面上,并不過時。

以下是正文。

事件驅動架構是一種系統或元件之間通過發送事件和響應事件彼此互動的架構風格。當某個事件發生時,元件A不直接調用元件B,而隻是發出一個事件。元件A不知道哪些元件監聽并處理這些事件。事件驅動架構可以在程序内和程序間使用。比如,GUI架構中會大量使用事件驅動。【譯者注:目前很多系統采用微服務架構,事件驅動使用的更加廣泛了。】此外,正如我在并發模型教程 中所提到的,裝配線并發模型(AKA reactive,非阻塞并發模型)也使用了事件驅動架構。

本文主要介紹程序之間的事件驅動架構,後文提到這個詞的時候也是指程序互動方式。

程序間的事件驅動架構

事件驅動架構是一種架構風格,先将請求事件集中存放在一個或多個事件隊列中,然後事件從這些事件隊列轉發到後端服務,處理這些事件。

因為事件可以被看做是消息流,是以事件驅動架構也被稱為消息驅動架構或者流處理架構。流處理架構又可以被稱為lambda架構。為了保證統一,後文會繼續使用事件驅動這個名詞。

事件隊列

在事件驅動架構中,你會有一個或多個集中的事件隊列,所有的事件被處理前,會先儲存在集中的事件隊列中。下面給出一個簡單示例:

軟體架構-事件驅動架構

事件插入隊列時是有序的,這樣就可以順序處理這些事件。

事件日志

寫入事件隊列時,消息可能寫入到事件日志(通常是磁盤存儲)中。如果發生系統崩潰,系統隻需要重放事件日志即可恢複到崩潰前的狀态。下面是一個事件驅動架構的示例,其中包括一個用于持久化事件的事件日志:

軟體架構-事件驅動架構

我們還可以通過備份事件日志,來備份系統狀态。在将新版本的系統部署在生産環境之前,可以使用這個備份資料對其性能進行測試。或者,通過重放事件日志的備份,來重制某些錯誤。

事件收集器

請求都是通過網絡傳輸,比如HTTP或者其他協定。為了保持一緻,可以通過事件采集器接收來自不同來源的事件。下面是一個添加了事件收集器的事件驅動架構示例:

軟體架構-事件驅動架構

響應隊列

有時,我們還需要向請求(即事件)傳回響應,是以,很多事件驅動架構除了包含事件隊列,還會有一個響應隊列。下面是包含事件隊列(入隊隊列)和響應隊列(出隊隊列)的事件驅動架構示例:

軟體架構-事件驅動架構

如你所見,響應隊列必須路由到正确的事件收集器。比如,如果HTTP收集器(本質上是web伺服器)通過HTTP接收的請求發送到事件隊列中,則該事件生成的響應可能也需要通過HTTP收集器發回用戶端。

通常,響應隊列不會持久化,也就意味着它不會寫入事件日志,隻有輸入的事件才會持久化到事件日志中。

讀事件 vs. 寫事件

如果将所有傳入的請求都認為是事件,就需要将這些事件都推送到事件隊列中。如果事件隊列是實作了持久化(持久化到事件日志中),就意味着所有事件都需要持久化。通常持久化都比較慢,如果我們能夠過濾掉一些不需要持久化的事件,我們就能夠提升隊列的性能。

我們将事件持久化到事件日志的原因是,我們可以重放事件日志,并重建因為事件引起的系統狀态變化。為了支援這個特性,實際上隻需要持久化更改系統狀态的事件。換句話說,我們隻需要将事件分為讀事件和寫事件。讀事件隻讀取系統資料,不會更改,寫事件會更改系統資料。

通過根據讀和寫劃分事件,我們隻需要持久化寫事件的消息即可。這将提升事件隊列的性能,提升比例大小,取決于讀寫事件之間的比例。

為了将事件劃分為讀寫事件,需要在事件到達事件隊列之前,也就是事件收集器中進行區分。否則,事件隊列無法知道到達的事件是否需要持久化。

還可以将事件隊列拆分為兩個,一個用于存儲讀事件的事件隊列,一個用于存儲寫事件的事件隊列。這樣讀事件就不會慢于寫事件,事件隊列也不需要檢查每條事件是否需要持久化。讀事件隊列不需要進行持久化,寫事件隊列始終持久化事件。

下面是一個事件驅動架構的示例,其中事件隊列分為讀和寫事件隊列:

軟體架構-事件驅動架構

上圖示例中箭頭比較亂,但實際上建立3個丢列并在它們之間分發消息簡單很多。

事件日志重放的挑戰

事件驅動架構的一大優點是,在系統崩潰或系統重新開機情況下,隻需要重放事件日志,就能夠重建系統狀态。在日志可以獨立于時間和周邊系統的情況下重放日志,這是一個很大的優勢。

但是,完全獨立于時間重放事件日志有時候很難實作。接下來介紹下事件日志重放的一些挑戰。

處理動态資料

如前所述,寫事件處理時可能會修改系統資料。有些情況,這種資料的修改受事件處理時動态資料的影響。比如,處理事件的日期和時間或者特定日期和時間的貨币匯率。

這些動态資料會對事件重放造成困難。如果在不同的時間重放事件日志,處理該事件的服務可能會解析不同的動态值,比如其他的日期和時間或其他匯率。是以,在不同的日期重放事件日志,可能會出現重建系統資料與最初處理事件産生的資料不一緻。

要解決動态資料的問題,可以讓寫事件隊列将所需的動态資料标記在事件中。但是,要實作這種方案,需要事件隊列知道每條事件消息需要哪些動态資料。這樣會使事件隊列的設計複雜化,每次需要新的動态資料時,事件隊列都需要知道如何查找這些動态資料。

另外一種解決方案是,寫事件隊列隻在寫事件上标記事件的日期和時間。使用事件的原始日期和時間,處理事件的服務可以查找給定日期和時間對應的動态資料。比如,可以通過原始的日期和時間,查詢當時有效的匯率。這就要求處理事件的服務需要基于日期和時間查詢動态資料,但是這隻是理想狀态。

與外部系統的互動

事件日志重放的另一個挑戰是與外部系統的協調。比如,事件日志中包含電商平台的訂單,在第一次處理這個事件時,需要将訂單發送到外部支付網關,以從客戶信用卡中收費。

如果重放事件日志,就不希望再次為同一個訂單向客戶收費。是以,就不希望在事件重放時,将訂單發送到外部支付網關。

事件日志重放解決方案

解決重放事件日志問題挺不容易的。有些系統沒有問題,可以直接重放事件日志;有些系統可能需要知道原始事件的日期和時間;有些系統可能需要知道更多類似于事件原始處理過程中從外部系統擷取的原始資料。

重放模式

在任何情況下,傾聽寫事件隊列中事件的任何服務都必須知道傳入事件是原始事件還是重放事件。這樣,處理服務就能夠确定如何處理動态資料或者如何與外部系統互動了。

多步驟事件隊列

另外一個解決方案是采用多步驟事件隊列。第一步,收集所有寫事件;第二步,解析動态資料;第三步,與外部系統互動。如果需要重放事件日志,隻需要跳過第一步和第二步,重放第三步即可。具體如何實作,需要取決于具體的系統設計。