微服務拆分解決的問題
微服務架構變得越來越流行了。它是模組化的一種方法。它把一整塊應用拆分成一個個服務。它讓團隊在開發大型複雜的應用時更快地交付出高質量的軟體。團隊成員們可以輕鬆地接受到新技術,因為他們可以使用最新且推薦的技術棧來實現各自的服務。微服務架構也通過讓每個服務都被部署在最佳狀態的硬體上而改善了應用的擴充套件性。
但微服務不是萬能的。特別是在 領域模型、事務以及查詢這幾個地方,似乎總是不能適應拆分。或者說這幾塊也是微服務需要專門處理的地方,相對於過去的單體架構。
在這篇文章中,我會描述一種開發微服務的方法,這個方法可以解決這些問題。主要是通過領域模型設計,也就是DDD以及事件源(Event Sourcing)以及CQRS。讓我們首先來看看開發人員在開發微服務的時候會遇到哪些問題吧。
微服務開發過程中的挑戰
模組化在開發大型複雜的應用的時候是非常有必要的。
現在許多應用大到一個人根本無法完成。而且複雜到光靠一個人去理解是不可能的。
這種情況下,應用就必須被拆分成一個個模組。在單體應用中,模組被定義為比方一個java package。然而,這種做法在實踐中並不是很理想,時間長了,單體應用就變得越來越龐大。微服務架構把服務作為一個模組單元。
每個服務對應一個業務能力,這個業務能力是組織為了創造價值而需要的。例如,基於微服務的線上商店包括各種服務,包括訂購服務(Order Service),客戶服務(Customer Service),目錄服務(Catalog Service)。
每個服務都有一個不可滲透且很難違反的邊界。也就是每個微服務要提供一種單獨而獨立的能力。這樣的話,應用程式的模組化就更容易隨時間儲存。
微服務架構還有其他優點。包括獨立地部署服務,獨立地擴充套件服務等等這些能力。相比單體來說。
不幸的是,拆分並沒有聽起來那麼容易。相當難。
應用的領域模型,事務,查詢這三個東西就是拆分過程中和拆分後你所面臨的拆分難題。讓我們來看看具體原因吧。
問題1 – 拆分領域模型
領域模型模式是實現複雜業務邏輯的一種非常好的方式。比如針對一個線上商店,領域模型將會包含這麼幾個類: Order, OrderLineItem, Customer 和 Product。在微服務架構中,Order和OrderLineItem類是Order Service的一部分;Customer是Customer Service的一部分;Product屬於Catalog Service的一部分。
拆分領域模型的挑戰之一就是class們通常會引用一個或多個其他類。
比如,Order類引用了該訂單的客戶Customer;OrderLineItem引用了該訂單所訂產品Product。
對於這些想要橫跨服務邊界的引用,我們該怎麼辦呢?
稍後你將會看到一個來自領域模型設計的概念:聚合(Aggregate)。我們通過聚合來解決這個問題。
微服務和資料庫
微服務架構的一個非常明顯的功能就是一個服務所擁有的資料只能通過這個服務的API來訪問。
在一個電商網站中,比如,OrderService佔有一個數據庫,裡邊有一張表ORDERS;CustomerService也有自己的資料庫包含表CUSTOMERS。
通過這樣的封裝,微服務之間就解耦了。
在開發期間,開發人員可以獨立修改自己服務的資料庫shema而不需要與其他服務的開發協調勾兌。
在生產上,服務之間都是隔離的。比如,一個服務從來不會因為另外一個服務佔有了資料庫的鎖而導致阻塞等待。
不幸的是,這種資料庫的拆分讓管理資料的一致性以及不同服務間跨表查詢變得困難。
問題2 – 跨服務分散式事務實現
一個傳統的單體應用可以通過ACID事務來強制業務規則從而實現一致性。
想象一下,比如,電商裡的使用者都有信用額度,就是在建立訂單之前必須先看信用如何。
應用程式必須確保潛在的多個併發嘗試去建立訂單不超過客戶的信用限額。
如果Orders和Customers都在同一個庫中,那麼就可以使用ACID事務來搞定:
BEGIN TRANSACTION
…
SELECT ORDER_TOTAL
FROM ORDERS WHERE CUSTOMER_ID = ?
…
SELECT CREDIT_LIMIT
FROM CUSTOMERS WHERE CUSTOMER_ID = ?
…
INSERT INTO ORDERS …
…
COMMIT TRANSACTION
不幸的是,在微服務架構中我們無法通過這種方式管理資料的一致性。
ORDERS和CUSTOMERS表被不同的服務所擁有,只能通過各自的服務API訪問。他們甚至可能在不同的資料庫。
一種比較常見的做法就是使用分散式事務來搞定,比如2PC等。但是這種做法對於現代應用來說也許不是一種可行的方案。CAP定理要求你必須在可用性和一致性之間選擇,可用性通常是較好的選擇。
而且,許多現代技術,例如大多數NoSQL資料庫,甚至不支援ACID事務,更不用說2PC。
所以管理資料的一致性需要使用其他的方式。
稍後你將會看到我們使用事件驅動架構中的一種技術叫事件源(event sourcing)來解決分散式事務。
問題3 -查詢
管理資料一致性不是唯一的挑戰。還有一個問題就是查詢問題。
在傳統的單體應用中,我們通常使用join來實現跨表查詢。
比如,我們可以通過下面的sql輕鬆的查詢出最近客戶所訂的大額訂單:
SELECT *
FROM CUSTOMER c, ORDER o
WHERE
c.id = o.ID
AND o.ORDER_TOTAL > 100000
AND o.STATE = 'SHIPPED'
AND c.CREATION_DATE > ?
但我們無法在微服務架構中實現這樣的查詢。
就像前面提到的那樣,ORDERS與CUSTOMERS表分屬不同的服務,只能通過服務API來訪問。
而且他們可能使用了不同的資料庫。
而且,即使你使用事件源(Event Sourcing )處理查詢問題可能更麻煩。
稍後,你將會學習到一種解決方案就是通過一種叫CQRS(Command Query Responsibility Segregation)做法來解決分散式查詢問題。
但首先,讓我們看看領域驅動設計(DDD)這個工具,在我們的微服務架構下基於領域模型開發業務邏輯是必要的。
DDD聚合是微服務的構建塊
像你看到的那樣,為了使用微服務架構成功的開發業務應用,我們必須去解決上面所說的那些問題。
這幾個問題的解決辦法你可以去Eric Evans的書Domain-Driven Design中找得到。
這本書,是2003年出版的,主要介紹了設計複雜軟體的一些方法。這些方法對開發微服務也同樣有用。
尤其是領域驅動設計可以讓你建立一個模組化的領域模型,這個領域模型可以被多個微服務所使用。
什麼是聚合?
在領域驅動設計中,Evans為領域模型定義了幾個構建塊。
許多已經成為日常開發人員語言的一部分,包括entity,就是指一個具有唯一標識的持久化物件。value object,也就是VO,你經常聽說的,是用來存放資料的,可以與資料庫表對應,也可以不對應,有點類似用來傳輸資料的DTO。service,就是指包含業務邏輯的服務。但不應歸類到entity或者value object。
repository,表示一堆entity 的集合就是一個repository。
構建塊(building block),聚合(aggregate)常常被開發人員忽略,除了那些DDD愛好者,或者叫“狂熱分子”。
然而,聚合(aggregate)被證明是開發微服務的關鍵,非常重要。
一個聚合(aggregate)就是一組domain的集合,可以被當作一個單元來處理。這裡說的一個單元就是可以當做原子來處理。
它包含了一個root entity以及可能還有一到多個關聯的entity以及value object。
比如,針對一個線上商店的domain model就會有幾個聚合,比如Order和Customer。
Order聚合又由一個root entity Order和一個以上的OrderLineItem value object組成,而且OrderLineItem還有可能關聯有其他vo,比如快遞地址(Address)以及支付賬戶資訊PaymentInformation。
Customer聚合又由一個root entity Customer和其他的vo比如DeliveryInfo 和PaymentInformation組成。
使用聚合將領域模型(domain model)分散和參與到每個聚合中,這也使得領域模型更容易理解了。這也同時釐清了操作的scope,比如查詢操作和刪除操作等。
一個聚合通常作為一個整體被從資料庫中load出來。刪除一個聚合,也就是刪除了裡邊所有的object。
然而,聚合的好處遠遠超出了模組化一個領域模型。這是因為聚合必須遵守一定的規則。
聚合之間的引用必須使用主鍵
第一個規則就是聚合通過id(例如主鍵)來引用而不是通過物件引用。
比如,Order通過customerId來引用Customer,而不是引用Customer的物件。
類似的,OrderLineItem通過productId來引用Product。
這種做法與傳統的object modeling非常的不同。雖然後者認為通過外來鍵引用在領域模型中這樣做看起來怪怪的。
通過使用ID而不是object引用,意味著聚合是鬆耦合。你可以輕鬆地把不同的聚合放在不同的service。
事實上,一個微服務的業務邏輯是由一個領域模型組成。這個領域模型是幾個聚合的一個組合。比如,OrderService包含了Customer聚合。
一個事務只建立或更新一個聚合
第二個規則就是聚合必須遵循一個事務只能對一個聚合進行建立或更新。
當我第一次看這些規則的時候,當時並沒有什麼感覺。因為那時候,我還在開發傳統的單體應用,那種基於RDBMS的應用。所以事務可以更新任何的資料。今天,這些約束依然適用於微服務架構。它確保一個事務只被包含在一個微服務中。此約束還符合大多數NoSQL資料庫的有限事務模型。
當開發一個領域模型,一個很重要的事情就是你必須確定每個聚合得搞多大。
一方面,聚合理想情況下應該是小的。它通過分離關注點來改善模組化。
這是更有效的,因為聚合通常被全部載入。
此外,由於對每個聚合的更新是順序發生的,因此使用細粒度聚合將增加應用程式可以處理的併發請求數,從而提高可擴充套件性。
它還將改善使用者體驗,因為它降低了兩個使用者嘗試更新同一聚合的可能性。
另一方面,因為聚合是事務的範圍,您可能需要定義一個較大的聚合,以使特定的更新原子化。
例如,之前我描述了在線上商店領域模型中,Order和Customer是獨立的聚合。
另一種設計可以是把Orders作為Customer聚合的一部分。
一個較大的Customer聚合的好處就是應用可以強制對於信用額度進行原子驗證。這種方法的缺點是它將訂單和客戶管理功能組合到同一服務中。這也降低了可擴充套件性,因為更新同一客戶的不同訂單的事務將被順序化。
類似的,兩個使用者去嘗試編輯同一個客戶下的不同訂單有可能會衝突。而且,隨著訂單數量的增加,載入一個Customer聚合的成本也會變得更昂貴。
由於這些問題,儘可能的把聚合細粒度是最好的。
即使一個事務只能建立和更新一個單獨的聚合,微服務應用中也依然必須去管理聚合之間的一致性。
在Order服務中必須驗證一個新建的Order聚合將不超過Customer聚合的信用額度。
這裡有兩種不同的解決一致性的方法。
一個做法就是在單個事務中欺騙的建立和/或更新多個聚合。這種做法的前提是,所有的聚合都被一個服務所擁有並且這些聚合都被持久儲存在同一個RDBMS中才有可能。
另一個做法就是使用最終一致的事件驅動(event-driven)方法來維護聚合之間的一致性。
使用事件驅動來維護資料一致性
在現代應用中,對事務有各種約束,這使得難以在服務之間維持資料一致性。
每個服務都有自己的私有的資料,這時候2PC的方案就變得不可行了。
更重要的是,很多的應用使用的是NoSQL資料庫,這些資料庫根本就不支援本地ACID事務,更不用說分散式事務了。
因此,現代應用程式必須使用事件驅動的,最終一致的事務模型。
什麼是事件(Event)?
在本文中,我們將領域事件定義為聚合發生的事件。一個事件(event)通常表示一個狀態的改變。現在還是拿電商系統舉例,一個Order聚合。其狀態更改事件包括訂單已建立(Order Created),訂單已取消(Order Cancelled),訂單已下達(Order Shipped)。事件可以表示違反業務規則的動作,如客戶(Customer)的信用額度。
使用Event-Driven架構
服務們使用事件來管理聚合之間的一致性,像下面這樣的一個場景:一個聚合釋出事件,比如,這個聚合的狀態改變或者一次違反業務規則的嘗試等等。
其它聚合訂閱這個事件,然後負責更新他們自己的狀態。
線上商店制建立一個訂單(order)的時候驗證客戶(customer)信用額度使用下面一系列步驟:
1.一個訂單(Order)聚合建立,並且狀態為NEW,釋出一個OrderCreated 事件。
2.客戶(Customer)消費這個OrderCreated事件,然後儲存為這個訂單儲存信用值然後釋出一個CreditReserved事件。
3.訂單(Order)聚合消費CreditReserved事件,然後修改自己的狀態為APPROVED。
如果信用檢查由於資金不足而失敗,則客戶(Customer)聚合釋出CreditLimitExceeded事件。
這個事件不對應於一個狀態的改變,而是表示一次違反業務規則的失敗嘗試。訂單(Order)聚合消費這個事件後,並將自己的狀態更改為CANCELLED。
微服務架構可以比作事件驅動聚合的Web
在這個架構下,每個服務的業務邏輯都是由一個或多個聚合組成。
一個事務只能包含一個服務,並且是更新或建立一個單獨的聚合。也就是聚合內事務。
服務們通過使用事件管理聚合之間的一致性。
這種做法一個非常明顯的好處就是一個個聚合變成了鬆散而解耦的構建塊。
他們可以被作為單體應用來部署或者作為一組服務來部署。
這種情況下,在一個project開始的時候,你可以使用單體架構。
之後,隨著應用的體積和開發團隊的規模的擴大,你就可以很容易的切換到微服務架構上來。
總結
微服務架構從功能上把一整個應用拆分成了一個個服務,每個服務又都對應一個業務能力。當我們開發基於微服務架構的業務應用的時候,一個關鍵的挑戰就是事務、領域模型以及查詢,這三個主要的麻煩都是拆分之後所帶來的問題。你可以通過使用DDD聚合的概念來拆分領域模型。每個服務的業務邏輯是一個領域模型,然後這個領域模型是由一個或多個DDD聚合組成。
在每個服務中,一個事務只能建立或更新一個單獨的聚合。由於2PC對於現代應用來說並不是一個可行的解決方案,所以我們需要使用事件機制來去實現聚合之間的一致性(以及服務之間)。在下一集,我們會描述使用event sourcing來實現一個事件驅動的架構。我們也會向你展示在微服務架構下通過使用CQRS來實現查詢。