應用層 CQRS 的實現與困難

微服務架構的天堂路

Fred Chien(錢逢祥)
Brobridge - 寬橋微服務
9 min readMar 11, 2020

--

Photo by Kolar.io on Unsplash

本文整理了一些關於 CQRS 的實現議題,供有同樣困擾的人參考,其經驗來自於過去 Brobridge 提供給客戶的微服務架構顧問和實作。因為 CQRS 機制在實作上確實有許多考量,也有一定複雜度,從開發、部署到管理維運面都有其難點,所以在服務客戶的過程中,甚至逐步整理並開發了一套開箱即用的 CQRS 解決方案 Gravity,如果您在理解了 CQRS 之後仍有需求,可以聯絡我們。

不可避免的,在微服務架構下,即使服務的工作、任務都已經拆分完成,最終仍然要面對資料處理架構的問題。尤其是在分散式架構下,對資料存取的需求大幅提升,資料庫系統壓力將變得更為沈重。所以,資料庫不拆分、資料處理模式不設計,反而會帶來更多的效能問題、維運問題以及更多的系統風險。

要知道,資料庫原本就已經是整體系統效能瓶頸的關鍵處,什麼都不做的情況下,只會在導入微服務架構和分散式系統的設計之後,使得處境變得更為艱困。

有鑑於此,在面對高乘載量的服務和微服務架構實現時,CQRS 是一種經常被採用甚至是必備的資料處理模式。運用「讀寫分離」的概念拆分資料處理邏輯,降低資料耦合複雜度、去關聯性,使資料庫的存取效能得到改善和提升,也讓資料庫規模化擴展變得可能。

CQRS 機制的運作原理

CQRS 機制模型

實現上,CQRS 主要是把「命令(Command)」和「查詢(Query)」從同一個商業邏輯上分開,在實務中會拆分成兩個服務來處理兩種不同的操作,更進一步來說,「命令」動作對應的任務會是「寫入」,而「查詢」所對應的任務是「讀取」。而「寫入端」到「讀取端」之間,採用事件的方式通知更新,以達到資料一致。

在這樣的架構下,由於寫入工作不影響讀取,讀取的工作也不影響寫入,能大量提高資料庫的存取效能。

利用 CQRS 優化關聯式查詢

建立資料視圖,去資料關聯性

在應用層使用 CQRS 的主要好處,是可以利用 CQRS 的機制建立視圖(View),是一種提升資料查詢效能的主要手段,主要在於「避免關聯式查詢」的動作發生。我們可以將這種方法視為在資料被建立時,就已經預先進行了關聯動作,因此無需在查詢時進行複雜度高的關聯行為。有些人又稱這樣的做法稱作快取(Cache),但其實是同樣一件事。

由於資料之間的關聯性已經扁平化,成為一種視圖(View)的存在, 因此查詢和讀取將變成非常有效率。此外,因為新的視圖已經與原始資料庫實體分離,因此查詢工作也不會影響寫入工作的效能。如果你熟悉微服務架構設計的議題,這其實也是微服務架構中所說,重複大於重用的具體實現。

以 CQRS 所實現的視圖,與關聯式資料庫(RDBMS)所內建的視圖,其最大的不同在於,以 CQRS 所實現的視圖與原始資料可實體分離,甚至採用異質資料庫來存放。因為資料視圖的建立,是圍繞在業務需求,只要使用事件驅動,甚至可以彈性的創建各種樣態的視圖,而不影響既有資料庫效能。

CQRS 實作和應用方法

利用事件進行資料的拆分處理與同步,實現 CQRS

實現 CQRS 機制,多半採用的方法是使用事件驅動(Event-driven)來進行,這代表我們首先要搭建一套訊息佇列系統(Message Queuing System)來推送事件訊息。

實現「讀取端服務」時,就去監聽事件訊息,因為這些事件訊息即代表著資料的變更,就依照事件描述的資料變更,同步寫入到服務自己的資料庫中。

一旦某個服務接受了一個「命令」,執行了資料更新和寫入動作,會拋出一個「事件」,而讀取端服務或是對該資料有興趣的應用程式,就會接收事件並進行資料同步。

另外,多座資料庫的情形,其實也可以採用傳統 Replication 和 CDC 的作法,不過考量到這類「讀取端服務」的資料庫生命週期,往往等於服務的生命週期,其服務擴展和維護維運上,可能相較於應用層的做法,會比較沒有彈性。

資料遺失、容錯、回復的解決方法

使用 Event Sourcing 達成事件回放,以回復資料的同步狀態

如果今天,我們的視圖資料不幸損毀,需要重頭同步並回復整個資料,又或是我們的讀取端服務突然中斷,導致事件遺失,該怎麼處理?這時,我們會採用 Event Sourcing 的方法實現事件回放,從特定時間點開始把事件回放,然後依照回放的事件內容執行同步工作。

為了實現 Event Sourcing 的機制,通常要仰賴日誌型(Log-based)的訊息佇列系統才能達成,例如:Kafka、NATS Streaming 等。這類訊息佇列系統,會將訊息照順序堆積儲存起來,並讓訊息消費者(Consumer)記錄自己最後讀取的位置(offset),並可以決定自己要從哪一個位置開始回放訊息。

運用這種回放機制,可以重新回復資料,也不用擔心事件遺失等問題。如果今天我們想要增加視圖的資料副本,也可以直接部署一個新的視圖服務,就可以從事件生成出一套完整的視圖資料庫。

快速重置、服務擴展的需求

實現 Data Snapshot (Cache) 以及快速擴展的處理機制

使用 Event Sourcing 來進行資料回復通常已經能解決大部分的問題,但問題是當事件非常多的時候怎麼辦?首先是訊息佇列系統可能有容量限制,不會無限度地存放所有訊息;再來是當訊息非常多,用回放方式來重建資料庫是一件耗時耗力、可能跑到天荒地老的工作。

這時,資料快照(Data Snapshot)或快取(Cache)就是一個需要被實現的機制,將資料庫最後的資料樣貌儲存一份,當我們在做服務擴展而要重建一個新的資料庫時,就可以從資料快照取得一份,並從該資料最後的事件位置繼續回放和接收同步。

當然,你也可以不準備資料快照,直接從既有正常的視圖服務資料庫中,直接用資料庫系統的 Replication 機制,或是寫程式去複製一份資料出來進行重建資料庫的工作。只不過,這樣會衝擊其中其中一個正在運行的服務效能。

降低與外部的訊息佇列系統的耦合程度

設計微服務架構時,服務間需要講求低耦合(Loose),而且要時時刻刻考慮單點故障(Single Point of Failure)的問題。這代表,我們的視圖服務,應該最大限度的與外界保持低耦合的狀態,而且當外部系統有問題,不該影響我們服務內的機制。

這問題的關鍵點在於過於中心化的訊息佇列系統,如果我們只有架一座訊息佇列系統,讓所有的服務共用,這代表除了實現正常的訊息傳遞之外,連 Event Sourcing 的工作(如:回放機制)壓力都在同一座訊息佇列。

導致每次回放領域事件(Domain Event),我們就會促使這個外部的訊息佇列系統,回放所有的事件, 衝擊效能。此外,假設這個外部的訊息佇列系統失效,也將會使我們的視圖服務的擴展和回復工作無法進行。

解決方法是在服務內部搭建一個獨立使用的訊息佇列系統,使外部系統無法直接影響服務的正常。

好麻煩!什麼時候輪到我的商業邏輯?

實作 CQRS 時會碰到的各種技術工作

CQRS 是依賴許多機制所整合而成的資料處理架構,實作上有一定的複雜度,若跟商業邏輯放在一起,會遭遇服務難以擴展的窘境。所以實現上,如果能將資料同步的部分獨立出來,是最妥當的作法。

儘管如此,CQRS 仍然是一個苦工,尤其是實現微服務架構後,幾乎每個服務多少都會運用到 CQRS 的同步機制,這意味著開發者需要幫每一個服務都實作一次 CQRS 架構,相當痛苦。

因此在我們的 Gravity 解決方案中,才會試圖將資料處理和應用程式獨立開來,讓開發者能夠專心於應用程式商業邏輯的開發。

採用 Gravity 將資料處理邏輯和應用程式分離開來

CQRS 的資料處理架構歸誰管?

CQRS 的應用上時常如同傳統資料庫系統的視圖,又保有實體資料的存在,其資料生命週期亦常伴隨著服務本身,所以主要由應用程式開發者維護。這對於一些組織上有編制 DBA 的企業來說,可能會有所衝突,而且,因應業務需求後,有更多欄位定義不同的資料表存在,這維運管理工作,對所有人來說都是一件苦差事。

因此,如何將 CQRS 所涉及到的資料流和資料架構,從應用程式層抽取出來,就是一個大議題。

總結

總結來說,實現 CQRS 機制需要考量下列議題,如果你有興趣,可以自行去進一步暸解和研究:

  • 搭建並維運訊息佇列系統 (Message Queuing System)
  • 考慮資料一致性的解決方法
  • 實現事件監聽和處理
  • 意外處理事件遺失、容錯、資料回復
  • 實現事件來源(Event Sourcing)
  • 可快速重置、服務擴展,資料回復時避免衝擊其他系統效能
  • 實現資料快照(Data Snapshot)

後記

無論是不是採用微服務架構,使用 CQRS 都能帶來改善資料庫效能的好處。只是採用之後,系統複雜度將會提升,考驗著開發者的程式開發功力。

對於著重業務快速發展的企業來說,CQRS 是一個必要卻又不易實現的機制,因為跟資料息息相關,很容易伴隨著各種風險。這時,採用開箱即用且能快速導入的解決方案,就是最好的選擇。

Gravity 是我們準備好的 CQRS 解決方案,無論是想要尋找框架導入微服務架構,或是改善既有資料庫效能,都能滿足您。如果您有需求,歡迎立即與我們 Brobridge 聯絡。

--

--