Categories
程式開發

微服務到底該多大?如何設計微服務的粒度?


本文最初發佈於Medium博客,經原作者授權由InfoQ中文站翻譯並分享。

微服務架構似乎終於成為了一種架構模式。上個月,距離Martin Fowler和James Lewis發表這篇關於這一主題的開創性論文已過了6年。感覺我們在與任何人討論架構時,都會涉及這個主題,即使只是這個主題的一部分。

與任何新技術一樣,微服務的發展也符合Gartner的技術曲線。從在推特上收集到的信息來看,它處於低谷期,或者是複蘇期的早期階段。也就是說,在過去6年左右的時間裡,我們在構建微服務方面積累了一些重要經驗,其中之一就是要確保以恰當的方式考慮每個微服務的範圍。

眾所周知,微服務往往把人往溝裡帶,並沒有解決他們的實際問題。當你想到“微服務”這個詞時,首先看到的是前綴“微”(Micro)。據An Introduction to Greek一書的介紹,在柏拉圖和亞里士多德那裡,μικρός只表示少或小。在日常英語中,“微”往往表示小得異乎尋常的東西——畢竟,“微米”是一米的百萬分之一,而“顯微鏡”是用來觀察那些用肉眼看不見的東西,因為它們的尺寸太小。

問題就出在這種不同的認知上。與之前的大型單體服務相比,微服務應該是“小”的。然而,它不應該太小——把微服務做得過小是很多團隊實現微服務架構時遇到的最常見錯誤之一

正是這種“微服務必須非常小”的思想導致了這個問題。關於微服務,我們經常聽到的另一個抱怨是,在銀行等複雜的領域使用它們太困難了,因為所需的REST或消息傳遞接口沒有提供跨多個微服務進行兩階段提交的方法。每當我們聽到這種抱怨時,頭腦中的警鐘就會響起——通常,這種抱怨是一個症狀,表明團隊把他們的微服務看作是非常微小的東西。為了解決這個問題,讓我們從一個簡單的示例開始尋找解決方法,然後再回過頭來,進一步考慮該解決方案對整個微服務架構的影響

示例從設計一個簡單的Account服務開始,從這個沒有包含微服務實現的設計開始,以便你可以看清它的演進過程。假設團隊正在使用領域驅動設計(稍後會詳細介紹),在他們的第一次嘗試中,發現Account實體需要引用兩個關聯依賴實體——Entry和Owner。

微服務到底該多大?如何設計微服務的粒度? 1

最初的Account設計

很快,團隊就發現Account上有一些定義良好的操作:借、貸、開、關。幸運的是,這些操作可以很好地映射到REST接口,因此,採用這種簡單的基於實體的設計並將其映射到微服務實現起來比較容易。然而,很快他們就發現了問題——他們還沒有弄清楚賬戶之間如何進行轉賬,如下圖所示:

微服務到底該多大?如何設計微服務的粒度? 2

具有Transfer服務的Account設計

問題是,在賬戶之間轉賬需要一個全新的REST接口,這一點顯而易見。問題是如何實現,這是否代表了一個全新的微服務?

最簡單的假設是(也是許多團隊都會採用的假設),微服務和REST接口之間應該存在1對1映射。然而,當把這個映射關係應用示例中時,我們很快就遇到了一個問題:新的微服務如何實現?最簡單的方法似乎是讓Transfer微服務調用Account微服務兩次;一次從“from”賬戶借出,一次貸入“to”賬戶。所以,我們的實現可能是這樣的:

微服務到底該多大?如何設計微服務的粒度? 3

調用其他服務的服務實現

但這將陷入前面討論過的兩階段提交問題!如果借出成功,貸入失敗,那麼這個客戶賬戶裡的錢就少了,還沒有任何追索權。這種情況顯然是不可接受的;因此,許多團隊嘗試了以下解決方案:

微服務到底該多大?如何設計微服務的粒度? 4

在數據庫引入耦合的Account設計

這解決了兩階段提交問題,但這是在數據庫中引入耦合,違反了微服務設計的原則之一,即服務應該擁有自己的數據,而不是通過共享數據庫“隱式”耦合。正確的做法是什麼呢?在我看來,許多團隊都走遠了,並開始追求涉及Saga模式的解決方案,以便通過補償事務來處理問題。雖然Saga模式可以解決問題,但它不是這種簡單的情況的最優解決方案。

考慮下下面的解決方案,它打破了之前的那個假設:

微服務到底該多大?如何設計微服務的粒度? 5

在服務邊界暴露多個服務

在這個解決方案中,我們重新考慮了之前假設的那個約束——一個微服務正好對應於一個REST接口。這個假設已經寫入網絡上的許多微服務教程中。然而,如果你仔細閱讀Fowler的原始論文,就會發現其中從未指出過這個假設。在微服務邊界上可以暴露多個服務。但這並不是這個小練習的真正目的。讓我們回到主題,通過這個練習來闡明更常見的問題。

我們在上文中說過,示例中的團隊使用領域驅動設計作為其微服務設計流程的一部分(要了解更多信息,請閱讀Eric Evans的著作《領域驅動設計:軟件核心複雜性應對之道》) 。在這方面,團隊是對的。我們看到,在微服務設計領域,其中一個最大的問題是,他們經常不是從領域驅動設計這樣的技術開始,而是從其他地方開始,比如從現有系統的設計開始,並試圖從那裡得出自己的微服務。或者,從一個架構開始(通常以工具和框架的形式指定),然後嘗試讓微服務“有機地”發展。在這兩種情況下,最終得到的都不是我所說的微服務——它們往往非常注重技術,與企業業務完全無關。而我們所說的微服務是用業務術語描述的“業務微服務”,業務人員可以識別它們,並可以從設計中找到它們。設計不足的症狀之一是解決方案中很少或基本沒有業務微服務,這是因為沒有從業務詞彙表開始設計。從業務詞彙表開始設計是至關重要的一步,這就是為什麼我們建議所有構建微服務的團隊將領域驅動設計作為其設計過程的一部分。

如果不首先從業務詞彙表入手,那麼通常會搞成如下架構:

微服務到底該多大?如何設計微服務的粒度? 6

你們很多人看到這個可能會說“這到底有什麼問題嗎?”這看起來就是我們要的微服務架構!要回答這個問題,我們必須回顧下Fowler在他最初關於微服務的論文中提出的觀點。他和Lewis提出的觀點是,當你團隊由技術專家組成時,所生產的軟件也將按技術領域進行組織——這就是實踐中的康威定律。 Fowler的解決方案是,按業務能力組織跨職能團隊。

當你開發一個與上文類似的微服務架構時,就已經回到了微服務本來要解決的問題!你不僅重新創建了一個單體,而且還是一個分佈式單體,情況變得更糟糕了。這違反了可能是Martin寫過的最重要的一句話:

Fowler的分佈式對象第一定律:不要使用分佈式對象。

那麼,更好的架構應該是什麼樣的呢?首先在最上層,考慮下垂直畫線,而不是水平畫線。

微服務到底該多大?如何設計微服務的粒度? 7

從大處著眼,微服務架構就是這個樣子。該架構由一組獨立的服務組成,按業務領域組織,而不是面向複雜的端到端網絡(取決於技術領域劃分)。回顧完什麼是微服務,讓我們回到文章開頭提出的問題——每個微服務應該有多大?我們從下界開始,然後逐步推出上界。

下界:微服務應該包含至少一個聚合(或至少一個獨立實體)以及操作該聚合實體的相關服務。

為了理解這個定義,首先要了解其中的兩個術語。聚合(Aggregate )是領域驅動設計的概念,我們已經看了它的一個具體例子。聚合是一組相關的實體,它們的生命週期被捆綁在一起,可以把它們作為一個單獨的單元來對待。典型的例子是Order/LineItem,我們例子裡的Account/Entry只是它的一個變體。

第二個術語我們也很熟悉,但可能不是你想的那樣。我們在使用DDD定義服務時使用了這個術語。服務是功能的“具體化”——我們在前面的示例中提到的“Transfer”就是這一思想的典型示例。在Evans的書中,他建議我們將這些對象建模為獨立接口,並稱之為無狀態服務,它們的接口通過領域模型的其他元素(實體和值對象)定義。關鍵是,這裡的服務指的是一個領域概念,它並沒有映射到任何特定的實體或值對象。

這裡最重要的設計要點是,在考慮微服務要多小時,必須非常仔細地考慮事務邊界。首先,你必須考慮微服務中涉及的實體的生命週期,即我們通常從持久化角度考慮的創建/讀取/更新/刪除週期。你必須考慮到所有可能發生在實體組上的更新——這些是服務將要識別的東西。你需要考慮的不僅僅是像轉賬這樣的簡單的一對一事務,而是需要更廣泛地考慮領域內關於實體組的其他操作,特別是關於批量更新和復雜查詢之類的事情。

關於這一點,一些純粹主義者可能會大喊:“等等!這樣的話,我的微服務將需要10個,也許20個獨立的REST接口!”也許就是這樣。如果領域的特定方面確實很複雜,需要在一組實體上進行許多操作,那麼它就應該作為微服務發布的最小單元。它更像一個迷你型的單體,但這好過解決由於服務劃分太小所帶來的問題。

我們發現,一開始把微服務做得太大好過做得太小。採用較大的(粗粒度的)微服務並將其分成兩個要比採用兩個細粒度的微服務並將它們組合起來更容易。

找到合適的抽象級別

如果這是微服務設計的合適下界,那麼在實際設計時該如何識別所有這些聚合,特別是與這些聚合相關的服務呢?領域驅動設計社區最近(過去幾年)針對這個問題給出了一個非常好的答案——通過事件風暴開始設計過程。

事件風暴(參見這裡這裡)是Alberto Brandolini發明的一個研討會和過程,團隊可以使用便籤和白板來快速識別業務領域內最重要的事件(Event),將這些事件按時間排列,然後確定觸發事件的命令(Command),執行這些命令所需的數據(Data),以及表示事件前後關係的策略(Policy)

在我們與客戶一起使用事件風暴的過程中,我們發現它提供了一種可重複的、易於理解的方法,可以用於在與領域專家討論領域詞彙表時識別實體和聚合。但最重要的是,它不只是通過實體和聚合標明數據結構,它還會展示用戶操作這些實體創建事件的命令,以及通常“隱藏”的策略,即將系統的各個部分連接在一起的業務邏輯。我們不會像上面的鏈接那樣帶你完成整個過程(這裡有一個完整過程的示例),但是我們將向你展示一個研討會的結果示例。

微服務到底該多大?如何設計微服務的粒度? 8

事件風暴研討會結果

在事件風暴中,黃色的便簽是操作系統的用戶(表示為角色)。綠色的便簽是用戶通過命令(藍色的便簽)與之交互的數據元素(聚合或偶爾是單獨的實體)。橙色的便簽是事件,它們有的是執行命令時產生的結果,有的是從另一個外部實體接收某種信息時產生的結果,還有的是以策略形式執行業務邏輯而產生的結果。

但重要的是最後的處理。如你所見,操作特定數據集、生成特定事件集的命令都分別分組。這在適當的粒度級別上完成了微服務初步設計。這是因為,這個流程本身在早期就傾向於將不同的參與者以及他們與系統交互的事件分開。最終得到一種由外向內的設計,避免了由於只關注數據結構或微服務之間交互的純技術考量而導致的許多過早優化。但這只是初步設計。在初次嘗試設計時就控制好微服務粒度是非常困難的。你需要對設計進行幾次迭代,以達到最恰當的粒度。

因此,如果聚合及其關聯的服務對像是微服務大小的合適下界,那麼合適的上界是什麼?在這裡,我們從本文一直在討論的領域驅動設計轉到一個完全不同的問題:

上界:微服務的大小應該能讓“雙披薩團隊”在一天內向生產環境發布一個完整的、適當大小的用戶故事。

現在,一定有部分讀者非常生氣。其中一些人已經在喊,“但是這取決於你擁有什麼樣的CI/CD工具,你的測試過程,你的QA過程,你的用戶故事大小,甚至你的團隊的開發速度!”對此,我們的回答是確實如此

這個行業採用微服務的全部原因就是讓團隊可以更快地發佈軟件,而且問題更少。到目前為止,大多數人已經閱讀了Nicole Forsgren等人的著作《加速:精益軟件和DevOps的科學:如何構建和擴展高性能的技術組織》。在這本書中,其團隊研究得出的最重要的結論是,軟件交付的速度(團隊的發布頻率)與高績效團隊的各種優秀因素相關。表現最好的團隊也是發布頻率最高的團隊。這就觸及了上述問題的核心。

微服務比單體服務更有吸引力的原因是可以減少在使用單體服務時遇到的問題。通常,單體服務的測試週期可以延續數天或數週,它們非常複雜,修改或擴展都非常有挑戰性。如果你的團隊能夠一天添加一個新特性,那麼你就不會受到測試週期、修改或複雜變更的阻礙。

但我們也聽到了一些不同的聲音,當我們向團隊建議這種節奏時,他們就開始尋找為什麼不能達到這種快速發布原因。他們開始討論“敏捷”過程開銷,比如持續數小時的站立會議,或者上層管理者命令他們使用Jira之類的工具減慢了他們的速度,或者比這些更糟,在將任何東西發佈到生產環境之前,都需要經過變更委員會。如果你也有類似問題,可以參考下面這個略有爭議的建議:

除非你已經理解並成功地實現了敏捷方法和DevOps原則(如自動化測試和持續集成),否則不要大規模採用微服務。

另一個適用於上限的常用試驗是,如果微服務失敗,檢查一下業務功能的退化程度。如果失去的業務功能不只一個,要么說明你的微服務粒度太粗,應該重構,要么說明太多的業務流程依賴於這一特定微服務(它被放在幾種不同的流的關鍵路徑上)。

總結一下。微服務是一種出色的架構創新,它有可能徹底改變我們多年遇到的單體架構問題。當在適當的粒度級別上實現微服務架構時,系統會敏捷,並減少決策的影響範圍,而且效果相當明顯。

但微服務並不是萬能藥。在成功地應用微服務之前,團隊在DevOps和敏捷實踐方面必須已經相當成熟。微服務設計應該是一個迭代過程。如果你的團隊不能採用該迭代過程,那麼要么微服務不適合你的團隊,要么你的團隊需要改變。我們發現,在修改系統之前,我們通常並不真正知道一組微服務的粒度是否合適。如果你發現實現新業務功能需要修改多個業務微服務,那這種粒度可能不合適(粒度太細)。另一方面,如果發布變更/新特性所需的測試量與編碼工作量相比過於冗長,則表示粒度可能過粗。無論哪種情況,解決方案都是進行適當的更改,然後總結經驗並更改流程,以避免將來其他微服務出現問題。

原文鏈接:

What’s the right size for a Microservice?