CQRS之旅——旅程8(後記:經驗教訓)
旅程8:後記:經驗教訓
我們的地圖有多好?我們走了多遠?我們學到了什麼?我們迷路了嗎?
“這片土地可能對那些願意冒險的人有益。”亨利.哈德遜
這一章總結了我們旅程中的發現。它強調了我們在這個過程中所學到的最重要的經驗教訓,提出瞭如果我們用新知識開始這段旅程,我們將以不同的方式做的一些事情,並指出了Contoso會議管理系統的一些未來道路。
你應該記住,這個總結反映的是我們的具體旅程,並非所有這些發現都適用於你自己的CQRS旅行。例如,我們的目標之一是探索如何在部署到Microsoft Azure並在利用雲的可伸縮性和可靠性的應用程式中實現CQRS模式。對於我們的專案,這意味著使用訊息傳遞來支援多個角色型別和例項之間的通訊。您的專案可能不需要多個角色例項,或者沒有部署到雲中,因此可能不需要如此廣泛地(或者根本不需要)使用訊息傳遞。
我們希望這些發現能夠被證明是有用的,特別是當您剛剛開始使用CQRS和事件源時。
我們學到了什麼
本節描述了我們學到的主要經驗教訓。它們沒有以任何特定的順序呈現。
效能問題
在我們的旅程開始時,我們對CQRS模式的一個概念是,通過分離應用程式的讀和寫方面,我們可以優化每個方面的效能。CQRS社群的許多人都認同這一觀點,例如:
“CQRS告訴我,我可以分別優化讀和寫,而且我不必總是手動的反規範化到平面表中。”
- Kelly Sommers - CQRS顧問
這在我們的實踐過程中得到了證實,當我們確實需要解決效能問題時,這種分離使我們受益匪淺。
在旅程的最後階段,測試揭示了應用程式中的一組效能問題。當我們研究它們時,發現它們與我們實現CQRS模式的方式關係不大,而與我們使用基礎設施的方式關係更大。發現這些問題的根源是困難的,由於應用程式中有如此多的活動部件,獲得正確的跟蹤和用於分析的正確資料是一項挑戰。一旦我們確定了瓶頸,修復它們就相對容易了,這主要是因為CQRS模式使您能夠清楚地分離系統的不同元素,比如讀和寫。儘管實現CQRS模式所導致的關注點分離會使識別問題變得更加困難,但是一旦您識別出一個問題,不僅更容易修復它,而且更容易防止它的重現。解耦的體系結構使得編寫重現問題的單元測試更加簡單。
我們在處理系統中的效能問題時遇到的挑戰更多地是由於我們的系統是一個分散式的、基於訊息的系統,而不是因為它實現了CQRS模式。
第7章“新增彈性和優化效能”提供了關於我們處理系統中效能問題的方法的更多資訊,並對我們想要進行但沒有時間實現的額外更改提出了一些建議。
實現訊息驅動系統遠非易事
我們這個專案的基礎設施是在旅程中根據需要開發它。我們沒有預料(也沒有預先警告)需要多少時間和精力來建立應用程式所需的健壯基礎設施。我們在許多開發任務上花費的時間至少是最初計劃的兩倍,因為我們持續發現與基礎設施相關的額外需求。特別是,我們從一開始就瞭解到擁有健壯的事件儲存是至關重要的。我們從經驗中得到的另一個關鍵思想是,訊息總線上的所有I/O都應該是非同步的。
Jana(軟體架構師)發言:
雖然我們的事件儲存還不是生產環境完備的,但是如果您決定實現自己的事件儲存,那麼當前的實現很好地指示了應該處理的問題型別。
儘管我們的應用程式並不大,但它向我們清楚地說明了end-to-end跟蹤的重要性,以及幫助我們理解系統中所有訊息流的工具的價值。第4章“擴充套件和增強訂單和註冊限界上下文”描述了測試在幫助我們理解系統方面的價值,並討論了由我們的顧問之一Josh Elster建立的訊息傳遞中間語言(messaging intermediate language, MIL)。
Gary(CQRS專家)發言:
如果我們有一個用於訊息傳遞的標準符號,就可以幫助我們與領域專家和核心團隊之外的人員溝通一些問題,這也會有所幫助。
總之,我們一路上遇到的許多問題都與CQRS模式沒有特定的關係,而是與我們解決方案的分散式、訊息驅動特性更相關。
Jana(軟體架構師)發言:
我們發現,使用不同的Topic來傳輸由不同聚合釋出的事件,通過這樣來劃分服務匯流排有助於實現可伸縮性。有關更多資訊,請參見第7章“新增彈性和優化效能”。另外,請參閱這些部落格文章:“Microsoft Azure Storage Abstractions and their Scalability Targets”和“Best Practices for Performance Improvements Using Service Bus Brokered Messaging”。
使用雲帶來的挑戰
雖然雲提供了很多好處,比如可靠的、可伸縮的、現成的服務,您只需單擊幾下滑鼠就可以使用這些服務,但是雲環境也帶來了一些挑戰:
- 您可能無法在任何您想要的地方使用事務,因為雲的分散式特性使得ACID(原子性、一致性、隔離性、永續性)事務在許多場景中不切實際。因此,您需要了解如何使用最終的一致性。例如,請參見第5章“準備釋出V1版本”,以及第7章“新增彈性和優化效能”中減少UI延遲的部分章節。
- 您可能需要重新檢查關於如何將應用程式組織到不同層的假設。例如,參見第7章“新增彈性和優化效能”中關於程序內同步命令的討論。
- 您不僅必須考慮瀏覽器或內部環境與雲之間的延遲,還必須考慮在雲中執行的系統的不同部分之間的延遲。
- 您必須考慮到瞬時錯誤,並瞭解不同的雲服務可能如何實現節流。如果您的應用程式使用幾個可能被節流的雲服務,那麼您必須協調應用程式如何處理不同服務在不同時間進行節流。
Markus(軟體開發人員)發言:
我們發現,程式碼中只有一個匯流排抽象,這掩蓋了這樣一個事實,即有些訊息是在本地程序內處理的,有些訊息是在不同的角色例項中處理的。要檢視這是如何實現的,請檢視ICommandBus介面以及CommandBus和SynchronousCommandBusDecorator類。第七章“增加彈性和優化效能”包括了對SynchronousCommandBusDecorator類的討論。
備註:我們的Visual Studio解決方案中的多個構建配置是為部分解決這個問題而設計的,也幫助人們下載和使用程式碼來快速入門。
CQRS是不同的
在我們的旅程開始時,有人警告我們,儘管CQRS模式看起來很簡單,但實際上它要求您在考慮專案的許多方面時進行重大的轉變。我們在旅途中的經歷再次證明了這一點。您必須準備拋棄許多假設和預先設想的想法,在開始充分理解從模式中獲得的好處之前,您可能需要先在幾個限界上下文中實現CQRS模式。
這方面的一個例子是最終一致性的概念。如果您來自關係資料庫背景,並且已經習慣了事務的ACID屬性,那麼在系統的所有級別上接受最終的一致性並理解其含義是一個很大的步驟。第5章“準備釋出V1版本”和第7章“新增彈性和優化效能”都討論了系統不同領域的最終一致性。
除了與您可能熟悉的不同之外,還沒有一種正確的方法來實現CQRS模式。由於我們對模式和方法的不熟悉,我們在功能塊上做了更多錯誤的開始,並且對所需的時間估計很差。隨著我們對這種方法越來越熟悉,我們希望能夠更快地確定如何在特定情況下實現模式,並提高我們估算的準確性。
Markus(軟體開發人員)發言:
CQRS模式在概念上很簡單,而細節才決定成敗。
我們花了一些時間來理解CQRS方法及其含義的另一種情況是在限界上下文之間的整合期間。第5章“準備釋出V1版本(https://www.cnblogs.com/angrymoto/p/cqrs-journey-day5.html)”詳細討論了團隊如何處理會議管理與訂單和註冊上下文之間的整合問題。這部分旅程揭示了一些額外的複雜性,當您使用事件作為整合機制時,這些複雜性與限界上下文之間的耦合級別有關。我們的假設是,事件應該只包含關於聚合或限界上下文中變化的資訊,但事實證明這種假設是沒有幫助的,事件可以包含對一個或多個訂閱者有用的附加資訊,並有助於減少訂閱者必須執行的工作量。
CQRS模式為如何劃分系統引入了額外的思考。您不僅需要考慮如何將系統劃分為層,還需要考慮如何將系統劃分為限界上下文,其中一些上下文將包含CQRS模式的實現。在旅程的最後階段,我們修改了關於層的一些假設,將一些處理從最初完成處理的工作者角色引入到web角色中。在第7章“增加彈性和優化效能”中討論瞭如何在程序中傳送和處理命令。應該根據領域模型將系統劃分為限界上下文,每個限界上下文都有自己的領域模型和通用語言。一旦確定了限界上下文,就可以確定在哪些限界上下文中實現CQRS模式。這將影響如何以及在何處需要實現這些隔離限界上下文之間的整合。第二章“[分解領域]”介紹了我們對Contoso會議管理系統的所作的決策。
Gary(CQRS專家)發言:
單個程序(部署中的角色例項)可以承載多個限界上下文。在此場景中,您不需要為限界上下文使用服務匯流排來彼此通訊。
實現CQRS模式比實現傳統的(建立、讀取、更新、刪除)CRUD風格的系統更復雜。對於這個專案,第一次學習CQRS和建立分散式、非同步訊息傳遞基礎設施的開銷也很大。我們在此過程中的經驗清楚地向我們證實了為什麼CQRS模式不是頂級體系結構。您必須確保實現基於CQRS的限界上下文相關的成本是值得的,通常,您將在高競爭、高協作的領域中看到CQRS模式的好處。
Gary(CQRS專家)發言:
分析業務需求、構建有用的模型、維護模型、用程式碼表示它以及使用CQRS模式實現它都需要時間和金錢。如果這是您第一次實現CQRS模式,那麼您還需要對基礎設施元素(如訊息匯流排和事件儲存)進行開銷投資。
事件源和事務日誌
對於事件源和事務日誌是否等同於同一件事,我們進行了一些討論:它們都建立了所發生事情的記錄,並且都允許您通過重播歷史資料來重新建立系統的狀態。結論是,事件的顯著特徵是除了記錄所發生的事實之外,還能捕獲意圖。有關我們所說的意圖的更多細節,請參閱參考指南中的第4章“深入CQRS和ES”。
涉及到領域專家的
實現CQRS模式鼓勵領域專家的參與。該模式使您能夠將寫端上的領域和讀端上的報告需求分離出來,並將它們與基礎設施關注點分離開來。這種分離使領域專家更容易參與系統中他的專業知識最有價值的方面。使用領域驅動的設計概念,如限界上下文和通用語言,也有助於集中團隊的注意力,並促進與領域專家的清晰溝通。
我們的驗收測試證明是一種有效的方法,可以讓領域專家參與進來並獲取他的知識。第4章“擴充套件和增強訂單和註冊有界上下文”詳細描述了這種測試方法。
Jana(軟體架構師)發言:
作為一個副作用,這些驗收測試還有助於我們處理偽生產版本的快速釋出,因為它們使我們能夠在UI級別執行一組完整的測試,以驗證除單元測試和整合測試之外的系統行為。
除了幫助團隊定義系統的功能需求之外,領域專家還應該參與評估一致性、可用性、永續性和成本之間的權衡。例如,領域專家應該幫助確定什麼時候手動流程是可接受的,以及在系統的不同區域中需要什麼級別的一致性。
Gary(CQRS專家)發言:
開發人員傾向於將所有內容都鎖定到事務中,以確保完全的一致性,但有時並不值得這樣做。
何時使用CQRS
現在我們已經完成了我們的旅程,我們現在可以建議您應該評估的一些標準,以確定是否應該考慮在應用程式中的一個或多個限界上下文中實現CQRS模式。您能正面回答的問題越多,就越有可能將CQRS模式應用到給定的限界上下文中,從而使您的解決方案受益:
- 限界上下文是否實現了業務功能的一個領域,這個領域是您的市場中的一個關鍵區別點?
- 限界上下文字質上是否與可能在執行時具有高爭用級別的元素協作?換句話說,多個使用者是否會為了訪問相同的資源而競爭?
- 限界上下文是否可能經歷不斷變化的業務規則?
- 您是否已經具備了健壯的、可伸縮的訊息傳遞和永續性基礎設施?
- 可伸縮性是這個限界上下文面臨的挑戰之一嗎?
- 限界上下文中的業務邏輯複雜嗎?
- 您清楚CQRS模式將給這個限界上下文帶來的好處嗎?
Gary(CQRS專家)發言:
這些都是經驗法則,不是硬性規定。
如果我們重新開始,會有什麼不同?
本節是我們反思我們的旅程的結果,以及確定了一些我們想以不同方式去做的事情和一些我們希望追求的其他機會。如果在我們掌握了現在我們所瞭解的CQRS和ES知識之後重來一次的話。
從訊息傳遞和永續性的堅實基礎設施開始
我們將從一個可靠的訊息傳遞和永續性基礎設施開始。我們採取的方法是從簡單的先開始,並根據需要建立基礎設施,這意味著我們在旅程中積累了技術債務。我們還發現,採用這種方法意味著在某些情況下,我們對基礎設施的選擇影響了我們實現領域的方式。
Jana(軟體架構師)發言:
從旅行的角度來看,如果我們從一個堅實的基礎設施開始,我們將有時間處理領域中一些更復雜的部分,比如等待列表(Wating-list)。
從一個可靠的基礎設施開始也能使我們更早地開始效能測試。我們還將進一步研究其他人如何在基於CQRS的系統上進行效能測試,並在其他系統上尋找效能基準,比如Jonathan Oliver的EventStore。
我們採取這種方法的原因之一是我們從顧問那裡得到的建議:“不要擔心基礎設施。”
更多地利用基礎設施的能力
從一個堅實的基礎設施開始也將允許我們更多地利用基礎設施的能力。例如,當我們釋出一個事件時,我們使用訊息發起者的ID作為會話ID在Azure服務匯流排傳遞,但從系統處理事件的部分來看,這並不總是最好的使用會話ID的方式。
作為其中的一部分,我們還將研究基礎設施如何支援其他最終一致性的特殊情況,如時間一致性、單調一致性、“read my writes”和自我一致性。
我們想探討的另一個想法是使用基礎設施來支援版本之間的遷移。我們可以考慮使用基於訊息的流程或實時通訊流程來協調把新版本上線,而不是針對每個版本以特定的方式處理遷移。
採用更系統的方法來實現過程管理器
我們在旅程的早期就開始實現我們的過程管理器,並且仍然在強化它,並確保它的行為在旅程的最後階段是冪等的。同樣,從為流程管理人員提供一些堅實的基礎設施支援開始,使他們更有彈性,這將對我們有所幫助。但是,如果我們要重新開始,我們也會等到過程的後期再實現流程管理器,而不是直接開始。
在旅程的第一階段,我們開始實現RegistrationProcessManager類。第3章“訂單和註冊限界上下文”描述了初始實現。在旅程的每個後續階段,我們都對流程管理器進行了更改。
以不同的方式劃分應用程式
在專案開始時,我們會更仔細地考慮系統的分層。我們發現我們的劃分的方式是把應用程式分到web角色和工作者角色中,這在第4章“擴充套件和增強訂單和註冊限界上下文“中進行了描述。但這不是最優的,在旅程的最後階段,在第7章“增加彈性和優化效能”中,作為效能優化的一部分,我們對架構做了一些重大改變。
例如,在旅程的最後階段,作為重新組織的一部分,我們在web應用程式中引入了同步命令處理,同時引入了已存在的非同步命令處理。
以不同的方式組織開發團隊
我們學習CQRS模式的方法是迭代開發、回顧、討論,然後重構。但是,我們可以通過讓幾個開發人員在相同的特性上獨立工作,然後比較結果,從而學到更多。這可能揭示了更廣泛的解決方案和方法。
評估領域域和限界上下文是否適合使用CQRS模式
我們希望從一組更清晰的啟發開始(如本章前面概述的啟發),以確定特定的限界上下文是否會受益於CQRS模式。如果我們關注領域中更復雜的地方,比如等待列表(Wating-list),而不是訂單、註冊和支付的限界上下文,我們可能會學到更多。
效能計劃
我們將在旅程的早期處理效能問題。我們尤其要:
- 提前設定明確的效能目標。
- 在過程中更早地執行效能測試。
- 使用更大更實際的負載。
我們沒有做任何效能測試,直到旅程的最後階段。有關我們發現的問題以及如何解決這些問題的詳細討論,請參見第7章“新增彈性和優化效能”。
在旅程的最後階段,我們在服務總線上引入了一些分割槽,以提高事件的吞吐量。此分割槽是基於事件的釋出者完成的,因此由同一個聚合型別釋出的事件將釋出到同一個Topic。我們希望把當前使用一個Topic的擴充套件到使用多個Topic,可能會基於訊息中OrderID的hash進行分割槽(這種方法通常稱為分片)。這將為應用程式提供更大的擴充套件。
以不同的方式思考UI
我們認為UI與讀寫模型互動的方式,以及它處理最終一致性的方式都很好,並且滿足了業務需求。特別是,UI檢查預訂是否可能成功並相應地修改其行為的方式,以及UI允許使用者在等待更新讀模型時繼續輸入資料的方式。有關當前解決方案如何工作的更多細節,請參見第7章“新增彈性和優化效能”中的“優化UI”一節。
我們想研究除非絕對需要,其他避免在UI中等待的方法,比如使用瀏覽器推送技術。在某些地方,當前系統中的UI仍然需要等待針對讀模型的非同步更新。
探索事件源的一些額外好處
我們發現在旅程的第三階段,第5章“準備釋出V1版本”中,修改訂單和註冊限界上下文來使用事件有助於簡化這個限界上下文的實現,一部分是因為它已經使用了大量的事件。
在當前的旅程中,我們沒有機會進一步探索靈活性的承諾,以及從事件源中挖掘過去事件以獲得新的業務見解的能力。但是,我們確實確保系統儲存了所有事件的副本(不僅僅是那些重建聚合狀態所需的副本)和命令,以便在將來啟用這些型別的場景。
Gary(CQRS專家)發言:
同樣有趣的是,通過事件源或其他技術(如資料庫事務日誌或SQL Server的StreamInsight特性)來挖掘過去的事件流以獲取新的業務洞察是否更容易實現?
探索關於限界上下文整合的相關問題
在我們的V3版本中,所有限界上下文都由同一個核心開發團隊實現。我們希望研究在實踐中,由不同開發團隊實現的限界上下文與現有系統整合起來有多容易。
這是您為學習經驗做出貢獻的一個很好的機會:繼續實現另一個限界上下文(請參閱產品backlog中的優秀使用者故事),將它整合到Contoso會議管理系統中,並在旅程的另一章中描述您的經驗。