駭客新聞上最近CQRS的討論和實踐經驗分享

banq發表於2020-11-12

自2017年以來,我一直在使用CQRS模式。它不是一個完整的系統模式,只是一個起點。您需要將系統設計為樂高積木式的。
那還需要什麼?下面是清單:
1)內部設計指南/規則/法律制度,以便每個人都能理解和遵守。即“ C”命令服務不應與其他命令服務通訊,而只能與查詢服務互動通訊。但是查詢服務能與其他查詢服務對話,您還需要服務網格和服務發現,有點複雜。
2)接收器或聚合器。如果讓查詢服務偵聽來自訊息匯流排的事件儲存在DB中為查詢請求提供服務,效率並不高。最好有專用的接收器服務,其中將資料以一定級別的流式傳輸到資料庫。
3)規則引擎或工作流系統。由於命令服務之間無法相互通訊,因此您需要在它們之間協調動作,這就是工作流系統設計的目的。它們連線到查詢和命令服務,並確保完成冗長或複雜的任務而無需做任何棘手的工作!
4)理智!使用CQRS + Sink + Workflows的那一刻,將會很容易感到不知所措。從一小部分基於MACRO的微服務開始,將所有命令打包到一個服務中,用於查詢和接收,並且隨著工作量的增長和您需要可伸縮性而逐漸分解為多個較小的微服務。這樣,您需要管理約4個微服務,如果需要API閘道器,則只有5個。
5)使用所有服務以及可能的技術,您需要一種合同方式進行通訊。您需要一個以CODE為DOC的系統!您需要Protobuf或類似的東西來設計您的架構和api,由於採取probubuf,您需要使用GRPC ...
 
我認為最重要的“補充”是:實際上有多少公司受益於這種複雜性的業務模型和架構結構?實際上,確實有一些企業和大型團隊從經過深思熟慮的架構(例如您所描述的架構)中受益匪淺。但是,大多數網際網路企業只需要在兩個或多個可用性區域中部署單個服務,即可與具有一個或多個熱備份的SQL資料庫進行通訊。上面這種複雜基本體系結構容易出現問題停止工作,您需要解決很多問題。
 

我與他人共同創立了一家初創公司(https://batch.sh),專門致力於解決事件驅動空間中的問題-我們進行事件歸檔,搜尋和重播。
您完全正確-僅CQRS模式是不夠的。以我的經驗,大多數走CQRS路徑的架構也傾向於使用其他事件驅動模式,例如事件源/事件溯源/事後溯源。
與大多數情況一樣,存在優缺點,最明顯的缺點是複雜性增加,以換取更高的可靠性和規模。
您的建議很棒,尤其是決定採用結構化的訊息格式(例如protobuf)時。我會100%避免使用JSON模式,因為人們可能會忘記填寫欄位。到目前為止,Protobuf擁有出色的支援,並且有大量的支援工具。此時,我很難選擇Avro(除非您是Java或Kafka商店,已經在其生態系統中擁有了它)。
另外,如果您已經在使用CQRS,我建議您考慮完全接受事件驅動-透過使用某種事件匯流排(RabbitMQ,kafka,eventbridge),可以使訊息傳遞完全非同步(並避免使用gRPC) ,正如您提到的,這是另一層複雜性)。
我的一個好朋友曾經說過,為了成功地實現事件驅動,您必須保持良好的狀態並保持最終的一致性,我認為這就是這一點的核心。如果您對最終的一致性表示滿意,那麼您將瞭解所帶來的複雜性負擔。
 
不幸的是,使訊息匯流排成為真理的唯一來源並不是那麼容易甚至是令人恐懼。Kafka是一個昂貴的家庭親戚,您最好確保自己擁有的配置接近完美,否則仍然需要一些專家來幫助您正確處理流資料。
Nats Steaming是Kafka的不錯替代品,我個人使用並推薦它,但在大規模事件上並不理想,例如規模有每天數十億次事件。作為Nats Streaming的繼任者,Jetstream即將面世,但它還處於預覽階段,需要再過一年左右的時間才能獲得產品使用資格。
 
接收器或聚合器是一種有效地將聚合狀態(將其儲存在資料庫中)與實際查詢(僅查詢資料庫)分離的方式。這樣,您可以隨時停止聚合/接收器,並從上次中斷的地方恢復。我們已經在PostgreSQL之上實現了事件儲存。我不喜歡使用Kafka作為事件儲存。
  
我對CQRS看到的最大缺點(尤其是在事件驅動的體系結構中)已增加了複雜性,通常是為了增加可伸縮性。我是“做可能可行的最簡單的事情”的忠實擁護者,因此除某些特殊情況外,我大多反對CQRS。應用程式的CRUD設計可以很好地擴充套件,以使大多數企業獲得可觀的收入數字。當您開始遇到限制時,便可以開始重構。在此之前,CRUD應用程式的簡單性可以讓您更快地進行原型設計和交付,並顯著增加新開發人員的入職時間。
  
一個經過微調後正常執行的資料庫可以處理多少工作量。在我的上一份工作,是根據CQRSish設計的,引入了不必要的複雜性或低效率降低了許多事情的速度。人們通常會強調“我們需要擴充套件,這將使其更快”,而真正的答案通常是“我們為何專注於一些基礎架構,只是使我們的程式碼更高效?”。
 
我看不到CQRS是如何增加複雜性的。畢竟,CQRS歸結為將查詢/讀取/不可變操作和命令/寫入/可變操作的介面分開。除非您不拘泥於諸如事件源之類的不相關概念,否則CQRS相比通常的CRUD應用程式並不是增加複雜性的重要來源。您能否闡明使用CQRS的哪些方面導致更高的複雜性?
 
我認為問題確實在於,當人們談論CQRS時,他們幾乎總是在談論事件源+ CQRS,這就是複雜性所在。
我的意思是,請看一下GraphQL,從本質上來說,實現“普通” CQRS並不容易,因為對於狀態和查詢而言,總是存在明確分開的操作,並且為這些操作提供單獨的物件定義很容易(這與許多REST形成直接對比架構,您的動詞-GET,PUT,POST,DELETE等-基本上都作用於相同的物件)。
但是我想這些天人們談論CQRS時,實際上是在談論底層資料不同(例如,寫操作日誌與可查詢物件的儲存庫),這通常意味著諸如事件源之類的東西,並且有了該模型涉及大量的複雜性,而且由於許多更新操作還涉及查詢,您也會遇到事務實現的困難。
 
正確,這就是我的意思。即使它們在技術上是不同的概念,但在實踐中我發現它們幾乎總是耦合在一起的。這些天來,我通常在我的API中使用GraphQL而不是REST,GraphQL也能讓我們在分離查詢和狀態上獲得的一些優勢。但是正如您所說,除非絕對不可避免,否則我最終希望將資訊最終以一種一致的格式儲存在資料庫中。有時這是不可避免的,但是就像文章中提到的那樣,將“報告資料庫”與“主資料”資料庫分開可以經常擺脫。
一旦您開始將資料分散到多個資料庫中並將其儲存在由具有不同API的不同應用程式管理的不同結構中,突然達到最終的一致性就會迅速成為一個不小的問題,通常可以透過增加更多的複雜性來解決或緩解該問題。
有時候,確實需要這種複雜性,因為一個專案上的規模龐大、龐大/龐大的工程團隊或獨特的業務需求-但我的原始觀點是,在大多數情況下,尤其是新產品/專案不會擁有1000萬+每天釋出的使用者都應該保持簡單,直到您知道它需要完整的CQRS +事件源系統的複雜性為止。
 
CQRS一個明顯的缺點是樣板程式碼和多個檔案的數量。儘管本質可能不再複雜,但是檔案之間的“雜音”和碎片數量會增加精神負擔。
 
我已經在小型企業中開發了類似CQRS的系統已有一段時間了。我還一直在借鑑域驅動設計(DDD)原理。需要花更多的時間進行實驗才能找到如何最好地將其應用於較小規模的軟體,但是結果卻非常令人滿意。
我首先草擬了一個計劃,該計劃將我如何嚴格地將DDD和CQRS應用於問題領域,但知道這可能會過於複雜。然後我放棄了我認為對我的特定情況不必要的元件。
結果是重量相當輕,沒有太多樣板,並且非常可靠。有關客戶肯定已經發現它是核心業務流程中不可或缺的一部分。
對此的需求並非來自任何效能要求,而是因為我認為必須有一種明智的方式來構建SME商業軟體。尤其是在業務需求和流程經常變化的創業起步環境中。我覺得DDD允許我實際在軟體中對業務流程進行建模,而不是要求業務需要適應我正在開發的軟體。
對於企業規模的開發人員來說,這可能是個老新聞,但是作為自由職業者,來自較小企業的一族,這是一件大事。
作為此過程的一部分,我開發了一個Python庫來簡化編寫事件/ RPCed系統(事件源或其他事件),並編寫了一些非常粗糙的體系結構技巧作為文件的一部分。它們的水平很高,但可能是一個有趣的(有點自以為是)起點。

[1]: https://lightbus.org

[2]: https://lightbus.org/dev/explanation/architecture-tips/

 
一直在研究CQRS,人們的想法/示例/實現都不相同,權威答案來自Greg Young的文章
原來的服務:

CustomerService

void MakeCustomerPreferred(CustomerId)

Customer GetCustomer(CustomerId)

CustomerSet GetCustomersWithName(Name)

CustomerSet GetPreferredCustomers()

void ChangeCustomerLocale(CustomerId, NewLocale)

void CreateCustomer(Customer)

void EditCustomerDetails(CustomerDetails)


使用CQRS 讀寫分離以後:

---------

CustomerWriteService

void MakeCustomerPreferred(CustomerId)

void ChangeCustomerLocale(CustomerId, NewLocale)

void CreateCustomer(Customer)

void EditCustomerDetails(CustomerDetails)

---------

CustomerReadService

Customer GetCustomer(CustomerId)

CustomerSet GetCustomersWithName(Name)

CustomerSet GetPreferredCustomers()

---------


 就是這樣,沒有任務/中介架構,沒有事件源,什麼也沒有。
 
使用CQRS讀寫分離,這在GraphQL中非常容易實現,因為GraphQL明確地將查詢與狀態分開(這是我最喜歡的主題部落格, https://www.apollographql.com/blog/designing-graphql-mutatio ...),但是由於某種原因,當他們像這樣實現時,沒有人將其稱為CQRS。實際上,在過去的5年中,我只聽說過在也使用事件源的體系結構中使用CQRS的情況。
 
在工作中(諮詢/開發店),幾年前我們走的很遠。結論:
1.如果認真執行CQRS + ES,效果很好,這需要紀律。我們最終建立了自己的框架(ugh),以便輕鬆地走上正確的道路。
2.當然,需要權衡取捨。
3.您只是一個精明的敏捷思考者,可以遠離規程並建立真正棘手的複雜性和更多的負面權衡。
4.在我們正在研究的典型企業業務解決方案中,對這種方法的市場需求很小。儘管據我瞭解,某些金融服務領域出現了一些需求高峰。
(奇怪的是……我們有金融服務客戶,但不在需要CQRS的特定領域內。)
 
幾年前,在我的老東家公司中,我們也走了這條路(CQRS + ES)。實現它很有趣,但是確實增加了一些複雜性。當然,它具有很好的可擴充套件性
有趣的是,我們在企業訊息傳遞應用程式中使用了它,儘管在釋出它之前我已經離開,我不知道今天的程式碼看起來如何。
 
以我的經驗,很多銀行業務,一些交易。在這兩種情況下,它都用於歷史記錄,審計和精度。
這個想法是,對於銀行業來說,僅獲得當前狀態是不夠的-更重要的是某人如何達到該狀態。
將歷史記錄新增到事務中並不是什麼新鮮事物-因此,您不必花很多時間在歷史記錄/審計機制上,而是將兩者都淘汰了-更高的彈性,分散式系統+內建的審計/歷史記錄機制。
 
儘管有這些好處,但您在使用CQRS時應非常謹慎。許多資訊系統都非常適合以讀取資訊的方式進行更新的資訊庫概念,將CQRS新增到這樣的系統可能會增加相當大的複雜性。我當然已經看到過這樣的情況,它極大地拖累了生產力,甚至在一個有能力的團隊的手中,也給專案增加了不必要的風險。因此,儘管CQRS是在工具箱中很好的一種模式,但是請注意,很難很好地使用它。
 
CQRS是一個很好的模式。但是,就像任何模式主義者一樣,不應將其應用於整個系統。
imo,它不適合作為系統體系結構。它更多的是子系統設計,您可以將其應用於具有許多寫操作和最終一致的讀儲存的物件,這些儲存當然可以從寫儲存中構建。
無需新增事件源就可以輕鬆完成此操作,並且兩者本身並不是需要並存。
關鍵是您有時無法基於不斷寫入系統的資料來構建“足夠快”的檢視。因此,在CQRS設計中具有處理查詢的系統對於基於此設計非常重要。
我將其視為一種模式,因為幾乎不可能使查詢執行大量的命令/寫入操作,因此它確實可以幫助您執行讀取/查詢負載
 
最近在F#和postgres中實現了CQRS + ES設計模式。查詢端由於業務需要而不斷變化,而命令端則保持不變。
在我們的例子中,查詢狀態又稱為投影是非同步的,這使得它非常靈活和快速地使用。投影可以存在於Web伺服器,Redis或資料庫中的任何位置。
 
我目前是使用CQRS + ES的系統的貢獻者。我們是一家金融科技公司,採用它所帶來的積極影響要大於負面影響。
我們使用Lagom框架在Akka Actor和相關技術上實施CQRS + ES。Lagom框架和文件中有足夠的護欄/指南,我認為不同的部分非常好地結合在一起!
總體而言,系統複雜性與可伸縮性之間的權衡並不太簡單。
 
CQRS + ES更多地與事件源有關,而與CQRS無關。該方案最困難的部分是CAP定理,而不是如何將操作分離為命令和查詢。
 
為了實現可伸縮性,我已經實現了幾次。
實際上,在我不得不使用它的地方,我們最終透過一個物化檢視來滿足查詢部分,該物化檢視對資料進行了組織和聚合,從而使查詢在幾毫秒內而不是數分鐘或數十分鐘就變得可行。
對於雲中的大量資料來源而言,這種模式可能非常重要,但對於“本地”應用程式中的較小資料集甚至可能很有用。
就像任何東西一樣,如果您嘗試將其應用於所有地方,那只是糟糕的體系結構。
 
我喜歡GraphQL如何透過顯式為查詢和狀態提供不同的DTO來為您的公共API普及CQRS(無事件源!)方法的優勢。這是執行此操作的一種非常簡潔的方法,而不是像Axon + Spring Boot這樣的超級笨拙的框架。
 
REST將資源放在首位,並且通常在同一資源上使用不同的動詞:透過PUT / POST建立新資源,然後可以獲取該資源以讀取它,然後透過PUT / PATCH對其進行修改,刪除資源以將其刪除。
CQRS通常意味著命令(修改系統)的處理方式與查詢(檢查系統的當前狀態)的處理方式不同。
當然,您絕對可以建立一個REST API,其中有一組您可以修改但不能讀取的資源(命令資源),以及一組可以讀取但不能修改的資源(查詢資源)。但這絕對不符合人們對REST API期望的一般想法。
而且,對於那些關心“最終” /“真實” REST的人來說,要使HATEOAS相容,這樣的API將相對困難。
 
舉一個更具體的例子,我們有一個Angular應用程式。該應用程式有兩個區域,一個區域使用者可以使用自由文字搜尋記錄,而另一個區域使用者可以建立或更新記錄。我們的真相來源是Postgres資料庫,其中所有更新和插入都使用微服務傳送。資料使用AWS中的SQS從Postgres流向Elasticsearch。然後,Angular應用程式使用其他微服務查詢Elasticsearch。因此,本質上我們的命令域模型與我們的查詢域模型是分開的。
 
如何處理分散式事務中的丟失/失敗事件?這實際上不是CQRS問題。這更多是圍繞您希望如何處理重播事件的系統功能。如果圍繞冪等性設計系統,則應該能夠重播事件並獲得相同的結果。



 

相關文章