經驗分享:採用事件溯源的誤區(以及我們是如何避免的)

banq發表於2019-07-02

在過去一年左右的時間裡,我們一直在構建一個具有事件源架構的新系統。事件溯源非常適合我們的需求,因為我們的組織希望保留系統管理的資訊的準確歷史記錄,並對其進行欺詐檢測(以及其他事項)進行分析。
然而,當我們開始時,我們之前都沒有人建立過具有事件源架構的系統。儘管閱讀了很多關於做什麼和避免什麼的建議,並且經歷了其他專案的報告,但我們在設計中犯了一些重大錯誤。本文描述了我們出錯的地方,希望其他人可以從我們的失敗中吸取教訓。

但這並不都是壞訊息。我們能夠輕鬆地從錯誤中恢復,這讓我們感到驚訝。我還將描述允許我們輕鬆改變我們的架構的因素,希望其他人也可以從我們的成功中學習。

1.沒有分離持久化事件歷史記錄與持續檢視當前狀態之間關係
應用程式根據其歷史事件維護了實體當前狀態的關係模型。事件與狀態之間是透過“projection投射”實施,這本身不會是壞事,但是,我們讓命令處理程式記錄事件的同時,還讓它更新關係模型來實現當前狀態。這意味著(a)無法確保從記錄的事件中重建實體狀態,並且(b)管理關係模型的遷移是一個重要的開銷,而應用程式正在快速變化。

當然,這錯過了採用事件溯源的全部意義嗎?

嗯,是。人們來自不同背景和技術偏好的專案。我們中的一些人確實注意到該架構與事件溯源文獻所描述的不同,但沒有立即做出反應。我們希望團隊(包括我們自己)能夠建立對事件源架構中固有的優勢,劣勢和權衡的直覺,而不是應用千篇一律的模式風格。而且我們不知道這種混合架構將如何發揮作用 - 它可能對我們所知道的一切都非常成功 - 所以我們不想僅僅基於從技術文章和會議會議中收集的理論上的理解來忽視這個想法。因此,我們繼續走這條路,直到上述困難明顯超過了收益。然後我們進行了技術回顧,其中我們檢查了規範事件源架構和我們的架構之間的差異。結果是我們都理解了為什麼規範事件溯源架構比我們的應用程式的當前設計更好,並同意改變其架構以匹配。

2.事件驅動和事件源架構之間的混淆
在事件驅動的體系結構中,元件響應於接收事件而執行,併發出事件以觸發其他元件中的事件;而在事件源體系結構中,元件記錄它們管理的實體發生的事件的歷史記錄,並根據與其相關的事件序列計算實體的狀態。

我們在兩者之間感到困惑,並且在歷史記錄中透過一個元件觸發其他活動來記錄事件(將兩者混合在一起)。
我們意識到,我們必須讓實體在讀取事件時候進行分辨以便逐個作出反應,讀取事件是為了瞭解過去發生的事情,這裡我們犯了一個錯誤。

3.使用事件儲存作為訊息匯流排
我們向事件儲存中新增了通知,因此服務可以訂閱更新並使其投射到最新狀態。這是餿主意!我們的事件儲存這時開始被用作事件匯流排啦,用於元件之間的瞬時通訊了,我們的歷史記錄實際上還包括與業務流程沒有明確關係的技術事件。
我們注意到,我們必須從顯示給使用者的歷史記錄中過濾技術事件。我們的有關於技術事件例如“嘗試使用IOException傳送電子郵件失敗”,使用者並不關心,他們希望看到業務流程的歷史。

文獻將事件源和事件驅動的架構描述為正交,這絆倒了我們,我們逐漸認識到,明確區分觸發活動的命令和代表過去發生事件的事件比命令/查詢責任隔離更重要!尤其是在我們系統的適度規模和嚴格的一致性要求下。

“事件”一詞是一個過度使用的術語,我們討論瞭如何命名不同型別的事件以區分事件源歷史的一部分,包括我們的活動的監視應用發出的通知型別的事件,可能會該觸發其他一些活動,等等。在我們的新應用程式中,對於我們的事件溯源中記錄的歷史事件,我們使用專門術語:"業務流程事件"。

4.被最終的一致性所誘惑
最初,我們為事件儲存提供了一個HTTP介面和用於讀取和儲存事件的應用程式元件。但是,這意味著客戶端無法處理ACID事務中的事件,我們發現自己在應用程式中構建機制以保持一致性。

注意到我們的錯誤
幸運的是,在我們的設計決策影響了我們的實時系統的事件歷史之前,我們在常規架構“ wizengamot ”中早期發現了這些錯誤。
我們決定用直接資料庫連線和可序列化事務替換命令處理器和事件儲存之間的HTTP使用。我們保留HTTP服務以遍歷事件歷史記錄,但僅限於維護讀取最佳化檢視的外圍服務,這些檢視最終可以保持一致(每日報告,業務指標)。

我們決定停止使用來自事件儲存的通知來觸發事件,而是重新使用REST(特別是HATEOAS)以方便地在元件之間傳遞資料和控制。

我們決定不更新命令處理程式中實體當前狀態的記錄。相反,當從資料庫載入實體時,應用程式將根據事件歷史記錄計算當前狀態。應用程式仍然維護當前實體狀態的“投射”,同時將投影視為讀取快取,用於最佳化載入實體,以便它不必在每個事務上載入所有實體的事件,並且選擇當前活動實體的子集,這樣就不必載入所有實體的所有事件。快取中事件條目會過期失效:每個投射都是一組表和函式,根據傳遞的每個事件,在這個表中建立,更新和刪除相應的資料行作為響應。

執行命令的邏輯現在看起來像:

  1. 將實體的最近狀態載入到記憶體模型中
  2. 在寫入事務中[list=1]
  3. 載入那些自最近投射到記憶體模型以來實體發生的事件
  4. 執行業務邏輯
  5. 記錄執行命令產生的事件
  • 將記憶體狀態儲存為最新投射,前提是:如果它是從最近的事件建立的,而不是根據當前持久化投射的(持久狀態可能已被併發命令更新)
    讀取事務不記錄事件,因此可以彼此並行執行並寫入事務。

    我們決定更換關係模型,這需要在應用程式發展時進行大量遷移,使用從域模型序列化的JSON blob,當持久化狀態與最新版本的應用程式不相容時,可以自動丟棄和重建。感謝Postgres的JSONB列,我們仍然可以索引實體狀態的屬性並批次選擇實體,而無需新增非規範化資料列進行過濾。

    該應用程式還保留了其他用途的投射(投射是重播事件到狀態),這些用途的一致性要求不太嚴格。例如,我們定期更新後臺報告的預測。

    重新設計系統架構
    我們擔心繫統架構的這種重大變化會對我們的交付時間表造成打擊。但事實證明這非常簡單。

    除了使用事件源外,該應用程式還具有埠和介面卡(又名“六邊形”)架構。載入實體的當前狀態對於由Adapter類實現的Port介面後面的應用程式邏輯是隱藏的。我的同事Ivan Sanchez能夠將應用程式切換到從事件歷史記錄中計算實體的當前狀態,並在大約一小時內將持久實體狀態視為讀取快取(如上所述)。然後團隊取代了關係模型,這需要在應用程式同時前進發展時進行大量遷移,從域模型序列化JSON blob,當持久化狀態與最新版本的應用程式不相容時,可以自動丟棄和重建。這一變化是在當天結束時進行的。

    我們還在我們的持續部署管道中執行了廣泛的功能測試。這些是為了利用Ports-and-Adapters架構編寫的,這種架構我們稱之為“域驅動測試”。它們根據使用者需求和問題域中的概念捕獲應用程式的功能行為,而無需參考應用程式技術基礎結構的詳細資訊。它們可以針對域模型,記憶體,針對應用程式服務的HTTP介面,或透過瀏覽器,針對在開發人員工作站上執行的例項或部署到我們的雲環境中的例項執行。

    功能測試有兩個目的,當我們不得不對應用程式的體系結構進行重大更改時,這些目的得到了很好的回報。
    首先,它們迫使我們遵循Ports-and-Adapters架構。我們的測試無法參考應用程式技術基礎的詳細資訊(HTTP,資料庫,使用者介面控制元件,HTML,JSON等)。如果我們透過在HTTP介面卡層編寫業務邏輯來違反架構限制,我們會收到預警,因為編寫一個可以單獨針對域模型執行的測試變得不可能。
    因此,對應用程式技術體系結構的更改嚴格地與其功能行為的定義和實現分開,當我們更改體系結構時,這兩者都不需要更改。這使他們能夠實現第二個目的:快速驗證應用程式仍然執行與我們對其體系結構進行大量更改相同的使用者可見行為。

    結論
    在實施系統時,你不可避免地會犯錯誤,特別是在採用團隊不熟悉的新架構風格時。系統的架構必須解決您從這些錯誤中恢復的方式。
    在我們的案例中,使用埠和介面卡的六邊形架構風格,團隊擁有豐富的經驗,並將測試和部署基礎架構視為系統架構的重要組成部分,這使我們能夠採用事件源,我們對此完全不熟悉,並且隨著系統的發展,我們會從誤解中恢復過來。

  • 相關文章