事件驅動架構在 vivo 內容平臺的實踐

作者:vivo 互聯網服務器團隊 - Gao Xiang

一、什麼是事件驅動架構

當下,隨着微服務的興起,容器化技術的發展,以及雲原生、serverless 概念的普及,事件驅動再次引起業界的廣泛關注。

所謂事件驅動的架構,也就是使用事件來實現跨多個服務的業務邏輯。事件驅動架構是一種設計應用的軟件架構和模型,可以最大程度減少耦合度,很好地擴展與適配不同類型的服務組件。在這一架構裏,當有重要事件發生時,比如更新業務數據,某個服務會發布事件,其它服務則訂閱這些事件;當某一服務接收到事件就可以執行自己的業務流程,更新業務數據,同時發佈新的事件觸發下一步。

事件的發佈與訂閱,需要依賴於一個可靠的消息代理。見下圖:

圖片

當然,事實上有不少軟件項目都使用了消息隊列,但是這裏需要明確的是,對消息隊列的使用並不意味着你的項目就一定是事件驅動架構,很多項目只是由於技術方面的驅動,小範圍地採用了某些消息隊列的產品而已。偌大一個系統,如果你的消息隊列只是用作郵件發送的通知,那麼這樣系統自然談不上採用了事件驅動架構。

在採用事件驅動架構時,我們需要考慮業務的建模、事件的設計、上下文的邊界以及更多技術方面的因素,這個系統工程應該如何從頭到尾的落地,是需要經過思考和推敲的。總而言之,“事件驅動架構” 的設計並不是一件易事。本文在後面有個例子供參考。

另外,如果盲目使用事件驅動設計架構,就有可能要承擔中斷業務邏輯的風險,因爲這些業務邏輯具有概念上的高度內聚,卻採用瞭解耦機制將它們聯繫在一起。換句話說,就是將原本需要組織在一起的代碼強行分離,並且這樣難於定位處理流程,還有數據一致性保證等問題。爲了防止我們的代碼變成一堆複雜的邏輯,我們應當在某些明確場景下使用事件驅動架構。以經驗來講,以下三 種場景可以使用事件驅動開發:

二、什麼時候使用事件驅動架構

2.1 組件的解耦

當服務(或組件) A 需要執行服務 B 中的業務邏輯,相比於直接調用,我們可以向事件代理(事件分發器)中發送一個事件。服務 B 通過監聽分發器中的特殊事件類型,然後當這類事件被接收到時去執行它。

這意味着服務 A 和服務 B 都依賴於事件代理和事件,而無需關注彼此實現:即完成它們的解耦。見下圖:

圖片

基於這種松耦合,服務可以用不同的語言實現。解耦後的服務能夠輕鬆地在網絡上相互獨立地擴展,通過動態添加或刪除事件生產者和消費者來修改他們的系統,而不需要更改任何服務中的任何邏輯。

2.2 執行異步任務

有時我們會有一系列需要執行的業務邏輯,但是由於它們需要耗費相當長的執行時間,所以我們不想看到用戶耗費時間去等待這些邏輯處理完成。在這種情況下,最好將它們作爲異步任務來運行,並立即向用戶返回一條信息,通知其稍後繼續處理相關操作。

比如,內容字段的檢查等入庫流程可以採用 “同步” 執行處理,但是執行內容理解則採用”異步“任務去處理。在這種情況下,我們所要做的是觸發一個事件,將事件加入到任務隊列中,直到一個服務能夠獲取並執行這個任務。此時,相關的業務邏輯是否處在同一個上下文中環境中並不重要,不管怎麼說,業務邏輯都是被執行了。

2.3 跟蹤狀態的變化

在傳統的數據存儲方式中,我們通過實體模型存數據。當這些實體模型中的數據發生變化時,我們只需更新數據庫中的行記錄來表示新的值。這裏有個問題,就是業務上我們無法準確存儲數據的變更和修改時間。但是在事件驅動架構中,可以通過事件溯源將包含修改的內容存入到事件裏。下面會詳細討論 “事件溯源 “。

三、爲什麼使用事件驅動架構

當大家談論事件驅動架構時,比如大家說自己恰好在最近的項目中採用了事件驅動架構,實際上,他們可能在談論下面這四種模式中的一種或者幾種:

注:概念來源 2017 年 GOTO Conference 上 Martin Fowler 分享的 The many meanings of Event-Driven architecture。

3.1 事件通知

假設我們現在想要設計一個簡易的內容平臺,包含三部分:

當內容創作者通過內容引入系統上傳視頻之後,會觸發如下的一個調用流程見下圖:

圖片

  1. 內容引入系統收到創作者上傳的視頻,執行入庫流程;

  2. 內容引入系統調用作者微服務的 API,增加 “視頻 - 創作者” 的從屬關係;

  3. 作者服務調用關注中心的 API,讓關注中心給關注了這個創作者的其他用戶發送作者視頻更新的通知。

上面這個調用流程,不可避免地創建了下面的依賴關係:

這種依賴關係很有可能並不是我們所期望的。內容引入系統是一個比較通用的業務,不同類型的內容引入系統很可能會有相似功能,如字段類型檢查、入內容庫、啓動高敏審覈等。作者服務則是一個非常專業的系統,如不同源、不同類型的內容關於作者的業務邏輯是不同的。讓一個通用的系統依賴於一個專業的系統,不管從設計角度,還是後續系統維護角度,都是不一個好的方案。作者微服務可能會經常根據業務需求做變更,但內容引入系統相對穩定,而上面這種依賴關係讓我們難以在 “不對內容引入系統做調整的情況” 下隨意更改作者微服務。

從架構層面,我們希望讓作者微服務依賴於內容引入系統,讓一個專業的系統依賴於一個穩定的、通用的系統,增加系統的穩定性。這個時候我們可以藉助於 “事件通知”。見下圖:

圖片

優點

缺點

“事件通知” 的缺點和優點相對應,正是因爲它提供了很好的解耦能力,我們會比較難通過閱讀代碼去得到整個系統和流程的全貌。因爲這些邏輯之間的關係不再是之前的依賴關係。這將會是一個挑戰。

3.2 事件承載狀態轉移

我們在使用事件通知時,事件裏面往往不會包含下游系統處理這個事件需要的所有信息。比如當內容發生下架變更時,內容平臺會生成一個 “內容下架 “的事件,但當下遊系統處理這個事件時,往往還需要知道,該內容上個狀態是什麼,是誰觸發下架等信息,才能完成後續處理。所以不可避免地,下游系統在處理這個事件時,往往還需要通過平臺服務來獲取這些額外信息。

爲了解決這個問題,我們引入一個種新的模式,叫做 “事件承載狀態轉移”。簡單來說,就是讓事件的消費方自己保留一份在業務處理過程中需要用到的上游系統的數據。比如讓下游系統保留一份在處理內容狀態變更事件時所需要用到的內容變更前的狀態,避免回頭去平臺查詢。

優點

缺點

3.3 事件溯源

有些時候我們不但關心繫統當前的狀態,我們還關心如何變成當前這個狀態的,但是數據庫僅僅簡單地保存實體的當前狀態。事件溯源可以幫助我們解決這個問題。

事件溯源是一個特別的思路,它並不持久化實體對象,而是隻把初始狀態和每次變更的事件記錄下來,並在內存中根據事件還原實體對象的最新狀態,mysql 主從備份用到的 binary log 以及 redis 的 aof 持久化機制,都可以認爲是 “事件溯源” 的實現。

事件溯源在做完數據庫更新之後,它將事件的發送操作轉換爲往數據庫或者日誌系統中寫入一條事件記錄,其它節點通過查詢數據庫或者文件系統,來得到這些事件,並通過回放來確保數據的最終一致性。

優點

缺點

3.4 CQRS

CQRS 全稱是 Command Query Responsibility Segregation。簡單來說,就是針對系統的讀寫操作,使用不同的數據模型、API 接口、安全機制等,來達到對讀寫操作的完全隔離,滿足不同的業務需求。見下圖:

圖片

根據存儲在事件庫中的事件集合,可以計算得到每個業務實體的狀態,這些狀態以物化視圖的方式存儲在一個數據庫中。當有新的事件產生時,也同樣會自動更新視圖。這樣,視圖查詢服務就可以像查詢普通的數據庫數據一樣實現各種查詢場景。具體的設計可參考下圖所示:

圖片

四、事件驅動架構在內容平臺中的實踐

在當今社會,內容 “橫行” 的時代,內容平臺企業需要有極強的靈活性和應變能力。特別是在中國這樣一個內容行業(如視頻)飛速發展的市場裏,企業要求平臺能夠快速地對內容業務需求做出應對,否則就會喪失先發優勢。這有點類似於現代戰爭條件下,各國都要求部隊具備快速反應能力,這種能力主要體現在平臺能夠通過快速開發或者重用 / 整合現有資源來達到快速響應業務需求。

隨着內容行業業務越來越龐大複雜,所涉及的存儲類型、處理器、賬號體系、效率工具、數據和結算系統等非常多,這就要求平臺有很強的整合能力以及對異構環境的適配能力。

最後,由於內容行業的發展日新月異,特定類型的內容業務(如小視頻)都會在其初中期發展後迎來一個快速膨脹期,業務量和業務類型會急劇增加,這也要求平臺有很好的可擴展性。相關平臺架構見下圖:

圖片

4.1 創建事件

事件其實是 DDD(領域驅動設計)中的一個概念,表示的是在一個領域中所發生的一次對業務有價值的事情,落到技術層面就是任何影響業務流程或者狀態的改變。事件具有自己的屬性,比如發生的時間、發生了什麼、事件之間的關係、狀態以及變化,事件也可以生成新的事件,根據不同的事件生成新的業務事件。在創建事件時,首先需要記錄事件的一些通用信息,比如唯一標識 ID 和創建時間等,爲此創建事件基類 ContentEvent:

public abstract class AbstractContentEvent {
    private String eventId;
    private String publisher;
    private String receiver;
    private Long publishTime;      
}

在一般場景下,事件一般隨着聚合根(也是 DDD 的一個概念,這裏泛指視頻 id)狀態的更新而產生,另外,在事件的消費方,有時我們希望監聽發生在某個聚合根下的所有事件,爲此建議爲每一個聚合根對象創建相應的事件基類,其中包含聚合根 videoId,比如對於視頻(Video)類,創建 VideoEvent:

public class VideoEvent extends AbstractContentEvent {
    private final String videoId;
}

然後對於實際的視頻事件,統一繼承自 VideoEvent,比如對於視頻引入的 VideoInputEvent 事件;

public class VideoInputEvent extends VideoEvent {
    private Article article; // 視頻基本信息
}

視頻域事件的繼承鏈見下圖;

圖片

在創建事件時,需要注意兩點:

  1. 事件本身應該是不變的;

  2. 事件應該攜帶與事件發生時相關的上下文數據信息,但是並不是整個聚合根的狀態數據。例如,在視頻引入時可以攜帶視頻的基本信息 article,而對於視頻狀態更新的 VideoStatusChangeEvent 事件,則應該同時包含更新前後的狀態 status:

public class VideoStatusChangeEvent extends VideoEvent {
    private String preStatus; //更新前的狀態
    private String status; // 更新後的狀態
}

4.2 發佈事件

發佈事件有多種方式,比如可以在應用程序中發佈。通常的業務處理過程都會更新數據庫然後發佈事件,這裏一個比較常見的場景是:需要保證數據庫更新和事件發佈之間的原子性,也即要麼二者都成功,要麼都失敗;當然也有不需要保證原子性的場景。如果需要保證原子性,以 “內容引入” 的業務流程爲例,見下圖:

圖片

4.3 消費事件

在消費事件時,除了完成基本的消息處理邏輯外,我們需要重點關注以下三點:

對於 “冪等性”,事件的發送機制保證的是“至少一次投遞”,這是有消息中間件保證,技術選型時需要注意。爲了能夠正確地處理重複消息,要求消費方是冪等的,即多次消費事件與單次消費該事件的效果相同。保證“消費冪等性” 的方法有很多,這裏介紹一種。在消費方創建一個事件表,用於記錄已經消費過的事件,在處理事件時,首先檢查該事件是否已經被消費過,如果是則不做任何消費處理。

對於第二點,依然沿用前文講到的 “事件表” 的方式。事實上,無論是處理服務請求,還是作爲消息的消費方,對於聚合根(videoId)來講都是無感知的,事件由聚合根產生進而由事件庫持久化,這些過程都與具體的業務操作源頭無關。

對於 “數據一致性”,本質上是由第二點引出,事件驅動架構在業務對象之間通過異步的消息來同步狀態,有些消息也可以同時發佈給多個服務,在“消息引起了一個服務的同步” 後可能會引起另外的消息,事件會擴散開。嚴格意義上的事件驅動是沒有同步調用的,如何保證一致性,就要比非事件驅動架構要複雜,通常採用 “cache aside” 模式和 “分佈式鎖” 來保證一致性。

綜上,在消費事件的過程中,應用程序需要更新業務表、事件記錄表,此時整個事件的發佈和消費過程見下圖;

圖片

五、總結

主流場景下,傳統面向服務(或以數據驅動)的平臺存在系統性不足,需要增強以下能力:

”事件驅動架構 “天然地滿足了這些能力要求。事件驅動架構” 天生“的優點,比如,封裝、高內聚和低耦合,還可以提升代碼的可維護性、性能和業務增長的需求,通過事件溯源模式,還能提高系統數據的可靠性。

不過,事件驅動同樣存在弊端,因爲無論是概念上的複雜度還是技術上的複雜度都增加了,當它被濫用時將導致災難性的後果。所以,在技術棧的選用方面,給出以下寄語:

1)不要 “盲目的追新” 技術人員的喜好往往是什麼技術流行就追什麼技術。現在的技術發展快,前後端不斷湧現各種框架,我們恨不得把這些框架都用在自己的項目裏纔行,按實際出發,按需所用,適當的預留技術預研的空間。

2)不要 “按技術站隊,以結果反推 “ 很多人把手段當成了目的,成爲了框架的信徒。用了 Java 開發,你的設計就一定是面向對象嗎?用了 Spring boot 就是微服務了嗎?一定要技術和實際場景結合,架構師也要深入瞭解掌握技術,但是更多的是瞭解技術的優劣和使用場景,而不是簡單的生搬硬套。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/DpaSVMXSNomXsXb0dma0CA