盡管微服務中的“微”一詞表示服務的規模,但它并不是使用微服務的唯一标準。當團隊轉向基于微服務的架構時,他們旨在提高靈活性以及自主且頻繁地部署功能。很難确定這種架構風格的簡單定義。我喜歡Adrian Cockcroft的關于微服務的簡短定義:“ 面向服務的體系結構,它由松散耦合的、具有上下文邊界的元素組成。”
盡管這定義了進階設計啟發式技術,但微服務架構具有一些獨特的特性,使其有别于以往的面向服務的架構。以下是其中一些特征。這些以及其他一些文檔都有據可查-Martin Fowler的文章和Sam Newman的Building Microservices,僅舉幾例。
- 服務具有圍繞業務上下文而不是任意技術上抽象的明确定義的邊界
- 通過意圖公開界面隐藏實作細節并公開功能
- 服務不會共享超出其邊界的内部結構。例如,不共享資料庫。
- 服務可以抵抗故障。
- 團隊獨立擁有職能,并能夠自主釋出變更
- 團隊擁護自動化文化。例如,自動化測試,持續內建和持續傳遞
簡而言之,我們可以将這種體系結構樣式總結如下:
松耦合的面向服務的體系結構,其中每個服務都包含在定義明确的有界上下文中,進而可以快速,頻繁且可靠地傳遞應用程式。
領域驅動設計和有界上下文
微服務的力量來自明确定義其職責并劃分它們之間的邊界。此處的目的是在邊界内建立高凝聚力,并在邊界外建立低耦合(banq注:高凝聚低耦合)。也就是說,趨于一起改變的事物應該屬于同一事物。就像在許多現實生活中的問題一樣,這說起來容易做起來難,業務在不斷發展,邏輯的假設前提也會随之改變。是以,重構的能力是設計系統時要考慮的另一關鍵問題。
領域驅動設計(DDD)是關鍵,在我們看來,這是設計微服務時必不可少的工具,無論是打破整體還是實施未開發項目。領域驅動的設計(Eric Evans在他的書中提出)是一組思想、原理和模式,可幫助基于業務領域的基礎模型設計軟體系統。開發人員和領域專家共同合作,以通用的通用語言建立業務模型。然後,他們将這些模型綁定到有意義的系統,并在這些系統與從事這些服務的團隊之間建立協作協定。更重要的是,他們設計了系統之間的概念輪廓或邊界。
微服務設計從這些概念中汲取了靈感,因為所有這些原理都有助于建構可以互相獨立變化和發展的子產品化系統。
在繼續進行之前,讓我們快速了解一下DDD的一些基本術語。域驅動設計的完整概述超出了本部落格的範圍。我們強烈建議任何嘗試建構微服務的人推薦Eric Evans的書籍。
領域:代表組織的工作。例如它是零售或電子商務。
子域:組織或組織内的業務部門。域由多個子域組成。
無所不在的語言:這是用于表達模型的語言。在下面的示例中,Item是一個模型,屬于這些子域中每個子域的通用語言。開發人員,産品經理,領域專家和業務涉衆都同意使用相同的語言,并在其工件(代碼,産品文檔等)中使用該語言。
有界上下文:域驅動的設計将有界上下文定義為“單詞或語句能确定其含義的設定”。簡而言之,這意味着模型是有意義的邊界。在上面的示例中,“項目”在每種上下文中的含義不同。在目錄上下文中,項目表示可售産品,而在購物車上下文中,則表示客戶已将其添加到購物車中的項目。在“運輸”上下文中,它表示将要運送給客戶的倉庫物料。這些模型中的每一個都是不同的,并且每個都有不同的含義,并且可能包含不同的屬性。通過将這些模型分離并隔離在它們各自的邊界内,我們可以自由地表達模型而沒有歧義。
注意:必須了解子域和有界上下文之間的差別。子域屬于問題空間,即您的企業如何看待問題,而受限上下文屬于解決方案空間,即我們将如何實施問題的解決方案。從理論上講,每個子域可能具有多個有界上下文,盡管我們努力為每個子域提供一個有界上下文。
微服務與有限上下文如何相關
現在,微服務在哪裡适合?可以說每個有界上下文都映射到微服務嗎?是的,我們将明白為什麼。在某些情況下,有界上下文的邊界或輪廓可能很大。

考慮上面的例子。定價綁定上下文具有三個不同的模型-價格,定價項目和折扣,每個模型負責目錄項目的價格,計算項目清單的總價格并分别應用折扣。我們可以建立一個包含以上所有模型的系統,但是它可能會成為一個不合理的大型應用程式。如前所述,每個資料模型都有其不變性和業務規則。随着時間的流逝,如果我們不小心的話,系統可能會變成一個泥濘的大球,邊界模糊,職責重疊,甚至可能回到我們開始的地方—一個整體。
對系統進行模組化的另一種方法是将相關模型分離或分組為單獨的微服務。在DDD中,這些模型(價格,定價項目和折扣)被稱為聚合Aggregates。聚合是組成相關模型的獨立模型。您隻能通過已釋出的界面更改聚合的狀态,并且聚合可確定一緻性,并且不變量保持良好狀态。
聚合是關聯對象的叢集,被視為資料更改的單元。外部引用僅限于AGGREGATE的一個成員,稱為根。一組一緻性規則适用于AGGREGATE的邊界。
同樣,沒有必要一定要将每個聚合模組化為一個獨特的微服務。圖中的服務(聚合)是一對一關系,但這不一定是規則。在某些情況下,在單個服務中托管多個聚合可能是有意義的,尤其是當我們不完全了解業務領域時。需要注意的重要一點是,隻能在單個聚合中保證一緻性,并且隻能通過已釋出的界面修改聚合。任何違反這些規定的行為都有變成大泥球的風險。
上下文映射—精确劃分微服務邊界的一種方法
整體結構通常由不同的模型組成,大多數模型是緊密耦合的-模型可能知道彼此的親密細節,更改一個模型可能會對另一個模型産生副作用,依此類推。分解整體時,确定這些模型(在這種情況下為集合)及其關系至關重要。上下文映射可以幫助我們做到這一點。它們用于辨別和定義各種有界上下文和聚合之間的關系。在上面的示例中,有界上下文定義了模型的邊界(價格,折扣等),而上下文映射定義了這些模型之間以及不同有界上下文之間的關系。
上下文映射的完整探索不在本部落格的讨論範圍之内,但我們将通過一個示例進行說明。下圖顯示了處理電子商務訂單付款的各種應用程式。
- 購物車上下文負責訂單的線上授權;訂單上下文流程過帳付款後的付款流程;聯絡中心會處理所有例外情況,例如重試付款和更改用于訂單的付款方式
- 為了簡單起見,讓我們假設所有這些上下文都是作為單獨的服務實作的
- 所有這些上下文封裝了相同的模型。
- 請注意,這些模型在邏輯上是相同的。也就是說,它們都遵循相同的通用域語言-付款方式,授權和結算。隻是它們是不同上下文的一部分。
重新定義服務邊界—将聚合映射到正确的上下文
錯誤案例如下圖:
電子商務中所有模型都直接與單個支付聚合的網關上下文(payment gateway context)內建,支付需要保證事務性,但是由于與多個服務內建,支付的事務性就不能通過在各種服務之間強制執行不變性和一緻性來實作,(banq注:當然有人就提出分布式事務的概念來在這些不同服務之間實作支付過程的事務性,這其實是在錯誤設計基礎上的僞概念)。
請注意,支付網關中的任何更改都将迫使更改多個服務,并可能更改多個團隊,因為不同的組可以擁有這些上下文。
進行一些調整并使聚合與正确的上下文對齊,我們可以更好地表示這些子域如下圖。發生了很多變化。讓我們回顧一下更改:
通常,整體(單體)或遺留應用程式具有許多聚合,通常具有重疊的邊界。建立這些聚合及其依賴關系的上下文地圖有助于我們了解将要從這些整體中擺脫出來的任何新微服務的輪廓。請記住,微服務架構的成功或失敗取決于聚合之間的低耦合以及這些聚合之間的高内聚性。
同樣重要的是要注意,有界上下文本身就是合适的内聚單元。即使上下文具有多個聚合,整個上下文及其聚合也可以組成一個微服務。我們發現這種啟發式方法對于有些晦澀的領域特别有用-考慮組織正在涉足的新業務領域。您可能對分離的正确邊界沒有足夠的了解,并且聚集體的任何過早分解都可能導緻昂貴的重構。想象一下,由于資料遷移,不得不将兩個資料庫合并為一個,因為我們偶然發現兩個聚合屬于同一類。但是請確定通過接口将這些聚合充分隔離,以使它們不知道彼此的複雜細節。
事件風暴-識别服務邊界的另一種技術
事件風暴是識别系統中的聚合(以及微服務)的另一種必不可少的技術。這對于破壞整體結構以及設計複雜的微服務生态系統都是有用的工具。我們已經使用這種技術分解了一個複雜的應用程式,并且打算在單獨的部落格中介紹Event Storming的經驗。對于此部落格的範圍,我們想給出一個快速的進階概述。如果您有興趣進一步探索,請觀看Alberto Brandelloni的視訊。
簡而言之,事件風暴是在應用程式團隊(在我們的情況下為整體)中進行的頭腦風暴,以識别系統中發生的各種領域事件和流程。團隊還确定這些事件影響及其後續影響的彙總或模型。在團隊進行此練習時,他們會确定不同的重疊概念,模棱兩可的領域語言以及互相沖突的業務流程。他們将相關模型分組,重新定義聚合并确定重複的過程。随着這些工作的進行,這些集合所屬的有限上下文變得清晰起來。如果所有團隊都在同一個房間(實體或虛拟)中,并開始在Scrum風格的白闆上繪制事件,指令和過程的映射,那麼Event Storming研讨會将非常有用。在本練習結束時,
- 重新定義的聚合清單。這些可能成為新的微服務
- 這些微服務之間需要流動的域事件
- 直接從其他應用程式或使用者調用的指令
我們在一場Event Storming研讨會結束時展示了一個示例闆。對于團隊來說,這是一次很棒的協作活動,以就正确的聚合和有限的上下文達成一緻。除了進行出色的團隊建設活動外,團隊在本次會議中脫穎而出,對領域,通用語言和精确的服務邊界有着共同的了解。
微服務之間的通信
一個整體在一個流程邊界内托管了多個聚合體。是以,在此邊界内可以管理聚合體的事務一緻性,例如,如果客戶下訂單,我們可以減少項目的庫存,并向客戶發送電子郵件,全部都在一次交易事務中。所有操作都會成功,或者全部都會失敗。但是,當我們打破整體并将聚合散布到不同的環境中時,我們将擁有數十甚至數百個微服務。迄今為止,在整體結構的單個邊界記憶體在的流程現在分布在多個分布式系統中。要在所有這些分布式系統上實作事務的完整性和一緻性非常困難,而且要付出代價-系統的可用性。
微服務也是分布式系統。是以,CAP定理也适用于它們- “分布式系統隻能提供三個所需特征中的兩個:一緻性,可用性和分區容限(CAP中的“ C”,“ A”和“ P”)。” 在現實世界的系統中,分區容忍度是無法商議的- 網絡不可靠,虛拟機可能當機,區域之間的延遲會變得更糟,等等。
是以,我們可以選擇“可用性”或“一緻性”。現在,我們知道在任何現代應用程式中,犧牲可用性也不是一個好主意。
圍繞最終一緻性設計應用程式
如果您嘗試跨多個分布式系統建構事務,那麼您将再次陷入困境。變成最糟糕的一種分布式整體事務。如果任何一個系統點不可用,則整個流程将不可用,通常會導緻令人沮喪的客戶體驗、失去保障失敗的承諾等。
此外,對一項服務的更改通常可能需要對另一項服務進行更改,進而導緻複雜而昂貴的部署。是以,我們最好根據自己的用例來設計應用程式,以容忍一點點的不一緻性,以提高可用性。對于上面的示例,我們可以使所有程序異步,進而最終保持一緻。我們可以異步發送電子郵件,而與其他流程無關。
如果承諾的物品以後在倉庫中不可用,該項目可能被延期訂購,或者我們可以停止接受超過某個門檻值的項目的訂單。
有時,您可能會遇到一個場景,該場景可能需要跨越不同流程邊界的兩個聚合中的強ACID樣式事務。這是重新檢視這些聚合并将它們合并為一個極好的标志。在我們開始在不同過程邊界中分解這些聚合之前,事件風暴和上下文映射将有助于及早識别這些依賴性。将兩個微服務合并為一個成本很高,這是我們應該努力避免的事情。
支援事件驅動的架構
微服務可能會在其聚合上發出本質上的變化。這些稱為領域事件,并且對這些更改感興趣的任何服務都可以偵聽這些事件并在其域内采取相應的操作。這種方法避免了任何行為上的耦合:一個域不規定其他域應該做什麼,以及時間耦合-一個過程的成功完成不依賴于同時可用的所有系統。當然,這将意味着系統最終将保持一緻。
在上面的示例中,訂單服務釋出了一個事件-訂單已取消。訂閱該事件的其他服務處理其各自的域功能:付款服務退還款項,庫存服務調整項目的庫存,依此類推。要確定此內建的可靠性和彈性的幾點注意事項:
- 生産者應確定至少生産一次事件。如果這樣做失敗,則應确儲存在回退機制以重新觸發事件
- 消費者應確定以幂等的方式消費事件。如果再次發生同一事件,那麼在使用者端不應有任何副作用。事件也可能不按順序到達。消費者可以使用時間戳記或版本号字段來保證事件的唯一性。
由于某些用例的性質,不一定總是可以使用基于事件的內建。請檢視購物車服務和付款服務之間的內建。這是一個同步內建,是以我們需要注意一些事項。這是行為耦合的一個示例-Cart服務可能從Payment服務中調用REST API,并訓示其授權訂單付款,而時間耦合則需要Payment服務用于Cart服務才能接受訂單。這種耦合減少了這些上下文的自治性,并且可能減少了不希望的依賴性。有幾種方法可以避免這種耦合,但是使用所有這些選項,我們将失去向客戶提供即時回報的能力。
- 将REST API轉換為基于事件的內建。但是,如果支付服務僅公開REST API,則此選項可能不可用
- 購物車服務立即接受訂單,并且有一個批處理作業來接管訂單并調用支付服務API
- 購物車服務會産生一個本地事件,然後調用付款服務API
在失敗和上遊依賴項(付款服務)不可用的情況下,将上述内容與重試結合使用可以使設計更具彈性。例如,在發生故障的情況下,可以通過事件或基于批次的重試來備份購物車和付款服務之間的同步內建。這種方法會對客戶體驗産生額外的影響:客戶可能輸入了不正确的付款明細,并且當我們離線處理付款時,我們不會将其線上。否則,收回失敗的付款可能會增加業務成本。但是,很有可能,購物車服務對于支付服務的不可用性或故障具有彈性,其缺點勝于缺點。例如,如果我們無法離線收款,我們可以通知客戶。
避免針對特定消費者資料需求的服務之間的編排
任何面向服務的體系結構中的反模式之一是,這些服務都可以滿足消費者的特定通路模式。通常,這發生在消費者團隊與服務團隊緊密合作時。如果團隊正在開發整體應用程式,則他們通常會建立一個跨不同聚合邊界的單一API,進而緊密耦合這些聚合。
讓我們考慮一個例子。說Web中的“訂單詳細資訊”頁面,移動應用程式需要在單個頁面上同時顯示訂單的詳細資訊和針對該訂單處理的退款的詳細資訊。在整體應用程式中,Order GET API(假設它是REST API)一起查詢Orders和Refunds,合并兩個聚合,然後将複合響應發送給調用方。由于聚合屬于相同的過程邊界,是以無需太多開銷即可執行此操作。是以,消費者可以在一個調用中獲得所有必要的資料。
如果訂單和退款是不同上下文的一部分,則資料不再存在于單個微服務或聚合邊界内。為消費者保留相同功能的一種選擇是使訂單服務負責調用退款服務并建立複合響應。此方法引起以下問題:
- 訂單服務現在與另一個服務內建在一起,純粹是為了支援需要退款資料和訂單資料的消費者。現在,訂單服務的自治性降低了,因為退款總額中的任何更改都将導緻訂單總額中的更改。
- 訂單服務具有另一個內建,是以要考慮另一個故障點-如果退款服務出現故障,訂購服務是否仍可以發送部分資料,并且消費者可以正常地故障嗎?
- 如果消費者需要更改以從“退款”聚合中擷取更多資料,則現在需要兩個團隊來進行更改
- 如果在整個平台上都遵循這種模式,則可能導緻各種域服務之間的依存關系錯綜複雜,所有這些都是因為這些服務滿足了調用者的特定通路模式。
前端的後端BFF
減輕這種風險的一種方法是讓消費者團隊管理各種域服務之間的編排。畢竟,呼叫者會更好地了解通路模式,并且可以完全控制對這些模式的任何更改。這種方法将域服務與表示層分離開來,使它們專注于核心業務流程。但是,如果Web和移動應用程式開始直接調用不同的服務而不是從整體中調用一個複合API,則可能會導緻這些應用程式的性能開銷–通過較低帶寬網絡進行多次調用,處理和合并來自不同API的資料,等等。。
相反,可以使用另一種稱為前端的後端的模式。在這種設計模式下,由消費者(在本例中為Web和移動團隊)建立和管理的後端服務負責跨多個域服務的內建,純粹是為了向客戶提供前端體驗。Web和移動團隊現在可以根據他們的用例設計資料合同。他們甚至可以使用GraphQL而不是REST API來靈活地查詢并準确擷取所需的資訊。重要的是要注意,此服務是由使用者團隊擁有和維護的,而不是由擁有域服務的團隊擁有和維護的。前端團隊現在可以根據自己的需求進行優化-移動應用程式可以請求更小的有效負載,減少來自移動應用程式的呼叫次數等等。檢視下面的業務流程的修訂視圖。
結論
在此部落格中,我們觸及了各種概念,政策和設計啟發法,以便在我們進入微服務領域時,尤其是在嘗試将整體式服務拆分為基于域的微服務時,加以考慮。其中許多主題本身就是廣闊的主題,我認為我們沒有做出足夠的公正性來詳細解釋它們,但是我們想介紹一些關鍵主題以及我們采用這些主題的經驗。進一步閱讀(連結)部分提供了一些參考資料和一些有用的内容,供任何希望采用此方法的人使用。