分解微服務,還是平衡分佈式系統的複雜性

分解微服務,還是平衡分佈式系統的複雜性

著者:Vladik Khononov
 
 
    微服務的蜜月期已經結束了。Uber正在把數千個微服務重構成一個更容易管理的解決方案;Kelsey Hightower又開始預言單體架構就是未來;連Sam Newman(《微服務設計》的作者)都在宣稱說微服務不應該是默認首選,而是最後的手段。
    發生什麼事情?爲什麼如此多的項目變得難以維護,儘管微服務承諾簡單和靈活?或者說單體架構更加好?
    在這篇文章裏,我想解答這些問題。你將會瞭解到從微服務轉變成分佈式大泥球所遇到的一些問題,當然你能避免它們。
    首先,我們一起來澄清一下什麼是單體。
 
單體
 
    微服務總是被定位爲單體代碼庫的一個解決方案。但是,單體應用一定有問題嗎?根據維基百科的定義,一個單體應用是自包含並獨立於其他計算機應用。獨立於其他應用?這不正是我們在設計微服務時候所追求的嗎?David Heinemeier Hansson馬上疾呼抹黑單體應用,他警告說分佈式系統天生的可靠性和挑戰,並使用Basecamp來證明一個服務於數百萬客戶的大型系統也是可以通過一個單體代碼庫實現和維護。
    因此,微服務沒有解決掉單體。真正的問題是微服務應該解決的真正問題是無法交付業務目標。通常,團隊之所以不能實現業務目標,是因爲變更成本呈指數級增長,或者更糟糕的是成本不可預測。換句話說,系統無法跟上業務需求的節奏。不受控的變更成本不是單體應用的屬性,而是一個大泥球。
 
大泥球是一個雜亂結構的、鬆散的、帶一堆管道或補丁、意大利麪式風格的叢林(暗指危險地帶)。這類系統展示出不受控的增長和重複性的臨時修復等明顯跡象。信息在系統中的相互遠離的元素間共享,常常達到差不多所有重要信息變成全局性或者重複性的程度。
變更或者演進一個大泥球的複雜性可有由多個因素所引起:協調多個團隊的工作、相互衝突的非功能性需求、或者是一個輔助的業務領域。無論如何,我們經常試圖通過把這種笨拙的解決方案分解成微服務來解決這種複雜性。
 
微什麼?
 
    “微服務”這個名詞暗示着一個服務的某部分能夠被度量,並且它的價值應該被最小化。但是,微服務究竟意味着什麼?讓我們看看這個詞的集中常見用法。
 
微團隊
    第一個是服務團隊的規模。這個標準應該用披薩來度量。嚴肅的說,如果一個服務團隊可以用兩個披薩來填飽肚子,那麼這就是微服務。我發現這真是一種富含啓發性的傳聞,因爲我曾經和一個能被一個披薩餵飽的團隊一起建設過一個項目,而且我敢說沒人敢把那些泥球稱爲微服務!
 
微代碼庫
    另一種廣泛使用的方法是根據代碼庫的大小來設計微服務。有些人將這個概念發揮到了極致,試圖將服務大小限制在一定數量的代碼行。也就是說,組成一個微服務所需的確切行數還有待去發現。而一旦這個軟件架構的聖盃被發現,我們將進入下一個問題——構建微服務的推薦編輯器的寬度是什麼?
    更嚴肅地說,這種方法的一個不那麼極端的版本是最流行的。代碼庫的大小通常被用作判斷是否微服務的一個富含啓發性的方法。
    在某種程度上,這種方法是講得通的。因爲代碼庫越小,業務域的範圍就越小。因此,它更容易被理解、實現和演進。此外,更小的代碼庫轉變成一個大的泥球的機會更少。如果它確實發生了,那麼重構就更簡單了。
    不幸的是,上述的簡單性只是一種幻覺。當我們基於服務本身評估服務的設計時,我們忽略了系統設計的一個關鍵部分。我們忘記了系統本身,就是那個服務只是其中一個組件的那個系統。
    
定義服務的邊界有許多有用的啓發方法。大小是最沒用的。——Nick Tune
 
我們建設系統
 
    我們建設的是系統,而不是一堆服務。我們利用微服務架構來優化一個系統設計,而不是設計一堆獨立的服務。不管其他人怎麼說,微服務不能,並且將來也永遠既不能被完全解耦,也不能完全獨立。你不能使用獨立的組件來構建一個系統,因爲那違背了“系統”的定義。
 
1. 一組連接在一起的事物或設備
2. 一套用於特定目的的計算機設備和程序
 
    服務之間總是會互相交互而形成一個系統。如果你通過優化它的服務但是卻忽視他們之間的交互設計來設計這個系統的話,以下就是你將會見到的一個局面。
    
    

 

        這些“微服務”可能單獨很簡單,但是系統本身就是一個複雜的地獄!那麼我們如何設計微服務來處理服務和系統級別的複雜性呢?這是一個很難回答的問題,但幸運的是這個問題很久以前就有人回答了。

 
關於複雜性的系統性視角
 
    四十年前,沒有云計算,沒有全球範圍的需求,也不需要每11.7秒部署一個系統。但是,工程師們仍然不得不馴服系統的複雜性。儘管當時的工具有所不同,但現在的挑戰(更重要的是解決方案)都是相關的,可以應用到基於微服務的系統設計中。
    在Glenford J. Myers的書《複合/結構化設計》中,討論到如何構造過程代碼以減少其複雜性。在該書的第一頁寫道:
 
There is much more to the subject of complexity than simply attempting to minimize the local complexity of each part of a program. A much more important type of complexity is global complexity: the complexity of the overall structure of a program or system (i.e., the degree of association or interdependence among the major pieces of a program)。
    在我們的上下文中,所謂的局部複雜性就是每個微服務的複雜性,而全局複雜性是整個系統的複雜性。局部複雜性取決於服務如何實現,而全局複雜性則是由服務間的交互和依賴關係所定義的。
    那麼,哪種複雜性更重要,局部複雜性還是全局複雜性?讓我們看看當只處理其中一種複雜性時會發生什麼。
    將全局複雜性最小化是非常容易的,我們所要做的就是消除系統各組成部分之間的相互作用,例如直接在一個單體服務中實現所有的功能。正如我們前面看到的,這種策略在某些情況下可能有效。在另一些地方,它可能會導致可怕的大泥球,即可能是局部複雜性的最高級別。
    另一方面,如果只優化局部複雜性,而忽略了系統的全局複雜性,即更可怕的分佈式大泥球,那我們知道將會發生什麼。
    
    
 
    因此,如果我們只關注其中一種複雜性,那麼選擇哪一種已經不重要了。在一個非常複雜的分佈式系統中,相反的那個複雜性會陡增(如果只優化一個的話)。因此,我們不能只優化一個,相反地我們必須平衡局部複雜性和全局複雜性。
    有趣的是,《複合/結構化設計》一書描述的平衡複雜性的辦法不僅僅跟分佈式系統有關,他們還提供如何設計微服務的見解。
 
微服務
    讓我們來準確定義我們正在討論的那些服務和微服務是什麼吧。
 
    什麼是服務?
    根據OASIS標準,一個服務是一種允許通過規定的接口的方式來訪問一個或多個功能的機制。
    
    規定的接口部分是很關鍵的,因爲一個服務接口定義了它向外界提供的功能。根據Randy Shoup的說法,服務的公共接口說白了就是從服務中獲取或提供數據的一個簡單機制。它可以是類似普通的請求/響應模型的同步模式,也可以是像生產與消耗的事件驅動的異步模式。不管是同步的還是異步的,公共接口只是一個將數據輸入或輸出的方法。Randy還將服務的公共接口描述爲它的前門。
 
    服務是由其公共接口定義的,這個定義足以定義是什麼使服務成爲微服務。
 
微服務是什麼?
 
    如果一個服務是被它的公共接口所定義的話,那微服務就是一個帶有微公共接口(微前門)的一個服務。
    這個簡單的啓發在過程式編程時代就已經被採用了,而且它在分佈式系統領域非常重要。你所暴露的公共服務越小,其實現就越簡單,其局部複雜性就越低。從全局複雜性的角度來看,越小的公共接口在服務之間產生更少的依賴和連接。
 
    微接口的概念也說明了微服務不暴露其內部數據庫的廣泛實踐。任何微服務都不能直接訪問另一個微服務的數據庫,而只能通過它的公共接口。這是爲什麼呢?好吧,數據庫將是一個巨大的公共接口!只需考慮一下你在關係數據庫上可以執行各種各樣的操作。
 
    因此,重申一下,在分佈式系統中,我們通過最小化服務的公共接口來平衡本地和全局複雜性,從而使它們成爲微服務。
 
警告
 
    這種啓示聽起來似乎很簡單。如果一個微服務只是一個具有微公共接口的服務,那麼我們可以將公共接口限制爲只有一個方法。既然前門不能再小了,那就應該是完美的微服務,對吧?其實不然。爲了說明爲什麼,我將使用我的另一篇關於這個主題的文章中的例子:
 
    這裏我們假設有以下的backlog management服務:
 
    一旦我們把它解耦成8個服務,每個有一個單一公共方法,那我們就得到那些具有完美局部複雜性的服務。
 
    
    但是我們可以將它們拼接在到實際管理backlog的系統中嗎?其實不然。爲了形成系統,服務間必須彼此交互並共享對每個服務狀態的更改。但它們不能,因爲它們的公共接口不支持這一點。
    因此,我們必須使用支持服務之間集成的公共方法來擴展這個前門。
 
    
 
    Boom。如果我們只是單獨地優化每個服務的複雜性(即局部複雜性),那麼簡單的分解就可以工作得很好。然而,當我們試圖將服務連接到系統中時,全局複雜性就會引入。結果是系統不僅變成一個糾纏不清的爛攤子,而且爲了集成我們還必須擴展公共接口,而這是超出(或者說違背)我們最初的意圖。改述一下Randy Shoup的話,除了小小的前門,我們創造了一個巨大的“員工專用”通道,而這會引出了一個重要的問題:
    對分佈式大泥球來說,一個服務裏面它具有爲集成而引入的方法比業務方法更多的話,這本身就是極具啓發性的。
    因此,使服務的公共接口最小化的閾值不僅取決於服務本身,而且主要取決於服務所屬的系統。對微服務的適當分解應該綜合平衡系統的全局複雜性和其服務的局部複雜性。
 
設計服務邊界
    
    Udi Dahan曾經說過:“發現服務邊界是真的很難,這裏沒有流程圖。”。
    
    上述說法對於基於微服務架構的系統尤其正確。設計微服務的邊界是困難的,而且可能無法在第一次就做好。這使得設計一個相當複雜的基於微服務的系統成爲一個迭代過程。
 
    因此,更安全的做法是從更寬的邊界入手(可能是適當的有界上下文的邊界),然後隨着對系統及其業務領域獲得更多知識再將它們逐步分解爲微服務,這與包含核心業務域的服務尤其相關。
 
分佈式系統以外的微服務
 
    即使微服務是最近才“發明”出來的,你也可以在其他行業找到大量相同設計原則的實踐。這些簡單的理念包括:
    
跨職能團隊
    我們都知道跨職能團隊是最有效的團隊,這樣的團隊是由爲了完成同一個任務而由不同專業人員組成的。這樣一個高效的跨職能團隊能夠最大化團隊內部的溝通和最小化團隊外部的溝通。
 
    我們的行業最近才發現了跨職能團隊,但是任務組已經存在很久了。這些的基本原則與基於微服務的系統的基本原則相同:團隊內部的高內聚性和團隊之間的低耦合。團隊的“公共接口”通過將完成任務所需的技能納入團隊(即實現細節)。
 
微處理器
    我在Vaughn Vernon關於同一主題的精彩博客中偶然發現了這個例子。在他的文章中,Vaughn將微服務和微處理器進行了有趣的對比。他特別討論了處理器和微處理器之間的區別:
我發現一個有趣的現象,這裏有一個給定的大小分類來幫助確定一箇中央處理器(CPU)是否被認爲是一個微處理器,這就是它的數據總線的大小。
 
    微處理器的數據總線是它的公共接口——它定義了微處理器和其他組件之間可以傳遞的數據量。公共接口有一個嚴格的大小分類,它定義了一箇中央處理單元(即CPU)是否被認爲是微處理器。
 
Unix哲學
    Unix哲學,或者說Unix方式,是一組文化規範和哲學方法,用於抽象化、模塊化的軟件開發。
 
    有人可能會說,Unix的哲學與我的觀點相矛盾,即不能用完全獨立的組件來構建一個系統。難道Unix程序不是完全獨立的嗎?事實正好相反,Unix方式幾乎總是定義程序必須暴露微交互。讓我們看看Unix哲學的原則如何與微服務的概念相呼應。
 
    第一個原則要求程序的公共接口必須暴露一個高內聚的功能,而不是用與原始目標毫不相干的功能來把程序弄得一團糟。
目標:讓每個程序都做好一件事。要完成一項新的工作,應該重新構建而不是通過添加新的“特性”使舊的程序變得複雜。
    
    即使Unix命令之間被認爲完全互相獨立,實際上它們不是。它們仍然必須與其他通訊。還有,第二個原則定義了通訊接口應該被如何設計:
期望每個程序的輸出成爲另一個未知程序的輸入。不要用無關的信息干擾輸出,避免嚴格的列或二進制輸入的格式,不要堅持交互式輸入。
    不僅通信接口受到嚴格的限制(標準輸入、標準輸出和標準錯誤),而且根據這個原則,在命令之間傳遞的數據也應該受到嚴格的限制。例如,Unix命令必須公開微接口,並且永遠不能依賴於彼此的實現細節。
 
Nano-Service(納米服務)又怎樣?
 
    術語“nanoservice”通常用來描述規模非常小的服務。有人會這樣說,前面示例中那些簡單的單方法服務就是納米服務。然而,我並不一定同意這種分類。
 
    納米服務被用來描述單個服務而忽略了系統的整體性。在上面的例子中,一旦我們將系統放入等式中,服務的接口就必須增長。事實上,如果我們將原始的單服務實踐與簡單的分解進行比較,我們可以看到,一旦我們將服務連接到一個系統中,整個系統的公共方法將會從8個增加到38個。此外,每個服務的公共方法的平均數量會從期望的1個變成4.75個。
 
    因此,如果我們優化服務的公共接口而不是優化代碼庫,術語納米服務將不再適用,因爲服務將被迫重新變大以支持其系統的用例(暗指支持系統的整體功能)。
 
就這些嗎?
 
    不。雖然最小化服務的公共接口對於微服務設計來說是一個很好的指導原則,但它仍然只是一種啓示但並不能取代常識。實際上微接口只是一種抽象,它涵蓋了更基本、但更復雜的耦合和內聚設計原則。
 
    例如,如果兩個服務具有微公共接口,但是它們必須在分佈式事務中進行協調,它們之間仍然是高度耦合的。
 
    也就是說,以微接口爲目標仍然是解決不同類型耦合(如功能耦合、開發耦合和語義耦合)的一種強大的啓示,但這就是另一個話題。
 
從理論到實踐
 
    不幸的是,我們還沒有一個客觀的方法來量化局部和全局複雜性。另一方面,我們確實有一些設計啓發可以用來改進分佈式系統的設計。
 
    這篇文章的主要想表達的是你應該通過提出以下問題來不斷地評估你的服務的公共接口:
 
    1、在給定的服務中,面向業務和麪向集成的端點的比例是多少?
    2、服務中是否存在業務上不相關的端點?您能否在不引入面向集成的端點的情況下將它們拆分爲兩個或多個服務?
    3、合併兩個服務是否會消除由於集成原始服務所引入的端點嗎?
 
    請使用這些啓示來指導服務邊界和接口的設計。
 
總結
 
    我想通過 Eliyahu Goldratt的一個觀察來總結這一切。在他的書中,他反覆提到:告訴我你如何度量我,那我會告訴你我如何表現。
 
    在設計基於微服務的系統時,測量和優化正確的度量是至關重要的。爲微代碼庫和微團隊設計邊界肯定更容易。然而,要建立一個系統,我們必須考慮它。微服務是關於設計系統的,而不是設計單獨服務的。
 
    這又把我們帶回到文中的標題:“分解微服務,還是平衡分佈式系統的複雜性”。解決微服務的唯一方法是平衡每個服務的局部複雜性和整個系統的全局複雜性。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章