1. 程式人生 > >談談我對微服務的理解

談談我對微服務的理解

微服務是一個近些年說的非常熱的概念,尤其在網際網路的大背景下,微服務的理論有機會被廣泛實踐。但是在實踐過程中,大家對微服務的理解確大相徑庭,到底要怎樣做才能真正掌握微服務的架構理論呢?通過此文筆者想和大家分享一下對微服務架構的認識和理解。

什麼是微服務

微服務的出現給我們的系統帶來了很多好處,比如子系統與子系統之間的技術異構,單個系統的彈性擴容等。那怎樣的服務才算是微服務呢?我們先看看它的定義:微服務是一種分散式系統解決方案,推動細粒度服務的使用,每個服務都可以獨立執行, 且這些服務可以協同工作。微服務的關鍵就是“微”,而怎樣才算微,筆者的理解就是小而獨立。
先說說“小”,小就是簡單,簡單到通過名字或者定義就可以知道它大概能幹什麼事了,而且只幹這一件事。
再說“獨立”,筆者在遇到過的一些專案中,見過有的人為了能搭建一套基於微服務的系統,就把本屬於一個獨立的系統拆分成多個,這樣看起來好像更符合“微”的定義,但其實不然,微服務定義中的任何一個系統都應該可以獨立部署、獨立執行,並能獨立完成一個業務閉環。如果你的系統在啟動之後,並不能對外提供什麼實質的功能,而是要完全依賴其它的服務,那這個系統就不算一個獨立的系統。
微服務的概念我們瞭解了,但是它又和同樣是最近幾年說的比較多的SOA架構有什麼區別和聯絡呢。首先微服務架構和SOA架構都是一種基於分散式系統的而且面向服務的架構風格,這裡請注意,是“風格”,不是框架。這些概念的提出只是給我們定義了它的規範和原則,並沒有給我們輸出可以直接落地的技術方案。那它們有什麼區別嗎,如果只從定義上好像很難說的清。但是從它們實踐的過程和方式上來看,還是有很大區別的。下面我從幾個主要方面說一下.

  • 領域驅動設計(DDD):

    筆者認為這應該是最重要的一點,微服務的提出最根本原因是解決越來越複雜的系統設計與開發的問題。當系統過於複雜時,它所面臨的問題難度要比只有幾個簡單功能的系統面臨問題難度大得多。而原因就在於系統內部已經有了太多的耦合,系統的每一次迭代都可能是致命的,而DDD理論的出現就是為了解決這樣的問題。微服務把它作為了業務劃分的重要指導理論依據。

  • 去中心化:

    SOA在提出時有一個重要的角色ESB(企業服務匯流排),它的根本是協調各系統進行功能輸出,所以天生就是一個帶有中心角色的架構,而微服務是去中心化的,它認為任何單獨的服務都可以獨立自治,不受其它系統的制約和調配。

  • 獨立資料庫:

    服務要保持獨立自治性,就應該完全使用自己的資料庫,而不是和別的系統共用。當自己的業務模型發生修改時,只修改自己內部的資料結構就可以了,而且也不用關心會對其它服務產生影響,而SOA中並沒有對這個原則有明確的要求。

微服務設計

微服務架構的核心目標是把複雜問題簡單化,通過服務劃分,把一個完整的系統拆分成多個高內聚、低耦合的小的子系統。使每個子系統可以獨立的執行、升級和測試。然後再通過一些整合手段將這些子系統組合在一起,對外提供完整功能的過程。所以我們對微服務設計的過程就是對系統如何做拆分和整合的過程。
拆分
對微服務的拆分,從根本上是對業務的拆分,我們可以從以下幾個方面來考慮。

  • 限界上下文

    限界上下文是在領域驅動設計的方法論中提出的,它認為每個給定的領域都可以包含多個限界上下文,而每個上下文可以分成內部和外部兩部分,內部部分是系統內的,不會受其它子域模型的變化而變化,同樣自己內部模型的變化也不會影響到其它子域。而外部部分需要參與和其它上下文的通訊。
    每個上下文都有明確的介面,該介面的定義決定了它會暴露哪些功能給其它上下文。所以我們在定義限界上下文時就是在劃分子域邊界,而且每個邊界內的職責一定是單一的。同時我們在定義上下文的名字的時候一定要在自然語言上保持獨立,這個被叫做正交原則。它是為了告訴我們,每個服務子域的定義要清晰獨立,不能產生歧義,也不要和其它上下文的名字產生混淆。
    限界上下文的劃分面向的是業務領域,而不是技術領域,有的人喜歡通過技術分層的方法把系統進行拆分,最後美其名曰採用的是微服務架構思想。我經歷過一次糟糕的專案經歷,在本應該是一個獨立服務的系統中,按照技術框架將服務分成了上下兩層(基於web應用的前端業務層和基於RPC協議的後端業務層),最後整個專案一點沒有因為這樣的分層而達到解耦的目的,反而是增加了開發和測試的實施難度。限界上下文的劃分同樣也不是面向資料模型的,之前說過,微服務和其它分散式服務架構最大的一個區別就是看是否採用了領域驅動設計的理論指導,這個理論的主導思想告訴我們系統模型的建立,不應該採用針對資料CRUD的操作,這被定義為“貧血”模式,它提倡的是針對領域模型驅動的“充血”模式。

  • 資料模型

找到限界上下文,我們劃分出了各個獨立的服務子域,下一步就可以對資料庫進行拆分了,將資料庫進行必要的拆分能將複雜資料結構進行分解,具體該怎麼做呢?下面我通過幾個例子來分析一下。
    
a找到缺失領域
筆者之前負責的一個系統中有兩個對外的服務,策略分析服務和威脅預警服務,兩個系統都要使用到行為特徵的資源資料,而且都會在一張表裡寫入和讀取,如圖1:

圖1

這樣顯然是不合理的,分析其根本,這裡缺失了一個領域概念:特徵庫。我們再重新拆分一下,如圖2:

圖2

經過抽象新的業務子域,我們添加了一個特徵庫的服務,它可以通過API介面的方式向兩個系統提供服務。而其自身也可以通過獨立的操作介面來維護特徵的定義和生命週期了。

b共享靜態資源
還有一些靜態字典表,我們需要在多個系統中使用,我們之前很常見的做法是將這些靜態資料單獨儲存到一個表或者直接從別的庫裡拷貝一份出來,在查詢的時候直接訪問資料庫獲得字典資料,而微服務提倡的做法是把這些靜態資料的查詢當做一個單獨的服務提供給外部,這樣在資料字典維護的時候也只需要維護一份資料就可以了。

c讀寫分離與不分離
服務的細粒度拆分,導致每個獨立的子服務變得很簡單,但是也會導致很多複雜的查詢業務實現起來變得困難,這時候我們可以把資料資源的維護和查詢分離開,比如統計報表。甚至我們可以通過採用不同的儲存引擎來提高效能。
還有一些系統,查詢的邏輯並不複雜,但是受到的資源約束比較多,這樣的系統我們更多建議讀和寫的操作都放到一起,還是以上面那個特徵庫系統為例,特徵庫服務的核心功能是給其它服務提供資料查詢能力,但是該系統在儲存的時候除了資料本身外還儲存了很多屬性做約束,比如特徵的生命週期和版本(涉及到資料是否可以對外暴露),系統在做資料寫入的時候同時需要對這些屬性進行維護。可是對外的查詢業務完全沒有必要關心這些細節,只需要知道特徵資料需要以什麼樣的格式和方式提供給其它系統就可以了,而不應該關心能不能查到。這時候,我們需要把針對特徵庫讀和寫的核心操作進行合併,分離出了一個核心資料基礎服務子系統,如圖3所示

圖3 圖3 通過把系統從左邊的結構演化到右邊後,對外提供查詢的服務不會直接訪問特徵資料庫,而是通過訪問特徵資料基礎服務來獲取特徵資料,而這個基礎服務應該由一個人或者組進行負責,畢竟只有維護資料如何寫入的人才更清楚應該怎樣把它查出來。
  • 康威定律

當我們通過上面提到的方法和原則進行服務拆分後,是不是就一定會得到一個可以實施的微服務架構呢?實際上可能還有其它的因素對其進行制約,開發團隊的組織結構就是非常重要的一個因素。也就是說微服務的設計要適應於實施它的開發團隊的組織結構。這個理論就是康威定律。從軟體開發成本角度來看,團隊內部成員的溝通成本應該是最低的,所以要把一個獨立的系統開發工作交付給一個團隊肯定比交付給多個團隊更能有利於溝通成本的降低,也最有利於軟體交付。限界上下文在劃分的時候首先就應該考慮這一點。這是一個團隊的多個人負責一個服務的情況,那麼從另一個角度想,可不可以由一個人負責多個服務呢,這肯定也是不被建議的,為什麼呢?當一個人在開發多個系統時,必然會在測試,部署等環節增加額外的工作量,遠沒有在一個程序內對系統進行開發方便。如果確實可以拆分出更細粒度的子系統來,建議先通過工程進行拆分,而不是服務,等系統發展起來後,需要加入更多的成員來完成這一個系統時,再拆分服務。所以當一個專案由一個5人的團隊開發時,最後劃分出10個子服務來肯定是有問題的。

整合
整個系統被拆分成多個獨立的子系統後,面臨的第一個問題就是如何整合在一起,對外形成一個完整的系統。業內出現了一些成熟的開源的技術框架,比如spring-cloud,dubbo等,都是針對微服務,SOA等面向服務的架構風格的技術實現。但除了這些技術框架,我們還應該掌握哪些整合原則呢?下面我從幾個方面給大家介紹一下。

  • 寬進嚴出

    我們設計的服務可能會作為消費方去呼叫別人的服務,同時也會作為服務方為別的系統提供服務,但是在一些安全或者引數的規範性校驗上,我們卻知之甚少,當需要和關聯服務互動時,在介面設計上我們應該做到寬進嚴出,就是說我們對自己要給別人提供的服務嚴格按照介面規範去輸出,儘量不給對方返回沒有在介面文件中出現的錯誤,而對於我們依賴的服務,要做到不信任原則,及要儘量考慮對方介面返回的未知錯誤,做到最大限度的容錯性。

  • 同步還是非同步

    當面對系統之間的整合時,我們經常會猶豫到底應該採用同步方式,還是非同步方式,同步帶來的好處就是互動簡單,呼叫端可以在一個執行緒或程序中就完成對依賴服務的請求和相應,這也是我們在服務整合時最先考慮的方式。但是由於隨著系統規模的不斷變大,產生的併發請求量也會急劇增加,對系統的響應延時和吞吐量的要求也愈來愈高,所以一些不需要立即獲取響應結果的請求完全可以通過非同步的方式來處理。
    不管是同步還是非同步都有其存在的道理,我們應該對具體的業務要求進行取捨,不過近年來,一種新的呼叫機制逐漸被人們應用起來,這就是響應式呼叫,響應式的呼叫方式使呼叫的細節被遮蔽,呼叫方只需要簡單的對呼叫的結果進行觀察,而不用關心呼叫本身是阻塞的還是非阻塞的,唯一需要做的就是等待結果返回後作出響應就可以了,同時它更棒的地方在於,呼叫方可以將多個呼叫組合起來,這樣很容易實現了對下游服務併發呼叫的需求。

  • 編排和協同

    當一個複雜的業務流程需要多個服務的配合來完成時,我們該採用怎樣的方式來組合這些服務呢,前面提到過,基於SOA的服務架構是一種把各個服務通過編排的方式整合在一起的,如下圖4所示

圖4 當用戶註冊成功後,我們要呼叫安全中心同步使用者資訊,還要呼叫積分中心給新使用者增加積分,最後呼叫訊息中心給使用者發訊息。這種通過編排把各個服務組織在一起的方式導致了使用者中心服務承擔了太多的責任,它的業務邏輯中耦合了很多無關的業務程式碼,這是服務中心化的一種體現。我們應該去除它,如下圖5 圖5                       

這種方式被定義為協同,它是通過訊息事件的訂閱和釋出來減少系統與系統之間的耦合,從根本上消除了中心服務節點。所以在設計非強依賴系統間呼叫關係時,建議採用協同方式來代替編排。

相信大家看到這兒,對微服務的設計應該有了一個大概的瞭解,上面筆者給大家介紹了幾個服務拆分和整合的常用方法,但這遠遠不夠,微服務架構面向的更多是業務層面,業務與業務之間是完全不同的,而且一個業務本身也是不斷變化的,所以想把某一個微服務架構的設計方法當成銀彈來使用,就大錯特錯了。知道這些原則並不難,關鍵是如何運用這些原則靈活得設計出一個高內聚、低耦合的系統。

微服拆分帶來的問題

微服務給我們帶來的好處顯而易見,我們通過一定的設計原則也一定能夠設計出最適合自己業務的服務來,但是這樣做會給我們系統的執行、監控、測試、部署帶來什麼副作用嗎?
答案是肯定的,由於微服務天生是基於分散式系統架構的,所以分散式系統固有的問題它都有,比如資料的一致性、服務的可用性等。另外,由於微服務對系統劃分的粒度非常細,本來很多單體的系統被部署到了多個程序中,這給測試、部署、監控等環節所帶來的複雜性也是超乎想象的。那因此我們就放棄微服務了嗎?當然不會,任何系統架構的設計過程,其實都是在解決系統複雜度的過程。微服務架構的複雜度雖然大,但是並不是不能解決的,下面針對幾個常見的問題來分析一下在微服務架構中應該如何應對。

  • 資料的一致性

    我們在一個只擁有一個數據庫的系統中處理事務是很簡單的事情,很多關係型資料對ACID的實現都是非常嚴格的,但是到了微服務架構的環境中,保持事務的完整性似乎變的不可能,由於網路傳輸的不可靠性(雖然這種情況出現的概率很小),被分散在多個程序中的系統在共同完成一個寫操作時就會可能出現維護自己的資料狀態時不能保持一致性的問題,比如不能夠同時提交本地事務,或者不能夠同時回滾。對於這個問題,我們應該怎麼做呢?
    首先要有重試機制,及當對一個子服務的寫操作請求失敗後,可以重試幾次,直到系統的響應成功為止,這就要求每個有寫操作的子服務需要支援冪等,因為有可能上一次已經寫成功了,只是由於網路問題,沒有正常返回結果。
    由於許多時候網路問題不是瞬間的,可能會持續一段時間,那麼我們就一直重試下去嗎?肯定也不會,我們會採用補償機制來修復事務的一致性,比如庫存系統在扣減成功後,需要在訂單系統上修改客戶訂單狀態,但是嘗試多次後,訂單系統一直響應失敗,那怎麼辦呢,可以給庫存系統發一個增加庫存的請求,算是補償。這樣整個系統在一定時間段內還是保證了資料是一致的狀態。
    最後可以使用柔性事務(TCC),柔性事務這個詞最早是由支付寶的團隊提出的,它的核心思想是要求在涉及寫操作的業務中支援一個軟狀態,怎麼理解這個“軟狀態”呢,就是不是一個確定的狀態,比如正在處理中的訂單。舉個例子,系統在從A狀態到達B狀態時必須要先經過C(軟狀態),而C不是一個實際的業務狀態,這樣做不但可以起到資源檢查並預佔的作用,同時還可以做為回退到A還是前進到B的中間狀態節點。

  • 服務的容錯性

    在微服務架構中,肯定會有一個系統依賴其它系統來完成一次操作的情況,那麼當依賴的下游系統宕機或者由於網路問題導致服務長時間不能響應的時候怎麼辦,上游系統不可能一直重試吧。這時我們需要採用熔斷保護措施才能保證上游系統不被下游系統拖死,具體我們應該怎麼做呢?
    超時,首先我們對任何服務的呼叫都應該有超時機制,當長時間沒有應答時就要認為呼叫失敗並終止等待了。
    隔離,即便有超時機制,系統也可能會因為超時時間過長,而處於“漫長”的等待中,更可怕的是當某個下游服務的整個叢集全掛了的時候,上游服務不應該被這一個服務拖死。我們應該對資源進行合理分配,把呼叫下游介面的資源進行隔離,其實有很多時候只是一個不影響主業務流程的下游服務掛了,這不應該成為我們整個服務不能訪問的原因,我們可以採用為每個對下游資源的呼叫分配最大個數的執行緒池或者連線池來解決這個問題。
    斷路保護,在網際網路的場景中,高併發、高吞吐的請求是非常普遍的要求,這種情況下,即便我們做了超時機制和資源隔離,也會由於大量的請求使我們的系統處於不必要的繁忙狀態,我們應該對那些一直出錯誤的響應進行處理,把已經嘗試過多次的失敗請求進行累計,當達到一定值的時候,忽略對它的呼叫。我們之後定時去監控,看看它恢復了沒有就可以了。
    服務降級,當系統出現斷路的時候除了不再訪問下游資源,還需要做些什麼呢?總不能給使用者返回一個下游服務有異常的錯誤,通常情況下我們會通過快取或者靜態資源來給使用者返回一些非實時的資料,讓使用者無感知,這就是服務降級。
    當然系統的容錯處理方式遠不止這些,很多時候是根據實際的業務場景來處理的,只要我們一直不要忘記網路是不可靠的這個事實,對一切可能出現的異常情況進行捕獲和規避,就能做到系統穩定可靠的執行

  • 系統的效能

    在微服務環境中,當用戶對系統發起一個請求後,最上游的服務要經過多次網路傳輸、多次請求/應答資料解析才能完成一個整體的請求互動,所以必將帶來更多的資源開銷和更大的訪問延時,我們應該如何應對呢?
    首先,可以考慮使用RPC進行遠端通訊,雖然目前基於restful的http請求很熱,它所帶來的簡單性和規範性是一般RPC框架不具備的,但是不得不承認,基於二進位制傳輸協議的RPC通訊框架在效能上的優勢是http協議很難達到的。所以在一些對效能要求比較苛刻的系統中,優先考慮RPC。
    其次,增加快取使用,快取雖然不是分散式系統架構中的銀彈,但是它無處不在,它的好處在這裡我們就不深入討論了。微服務系統性能瓶頸歸根到底還是多次網路請求,解決它最好的辦法就是減少呼叫,我們儘量把每次對下游系統的請求進行快取,雖然有時這確實是一廂情願,但在設計微服務系統時,請你首先考慮使用它。
    最後,我們說說擴充套件,前面聊了幾點其實也只是儘量在縮小和單體系統之間的差距而已,並沒有從根本上解決效能瓶頸。而對於系統擴充套件性,是微服務系統與生俱來就有的,通過擴充套件我們可以減少單個服務的複雜性,使某個服務的請求所要處理的業務邏輯簡單很多,最常見的情況是資料庫的結構簡單了,之前需要各種關聯的查詢沒有了,因為我們每個服務只負責自己的那幾張表。這有什麼好處呢,首先是垂直擴充套件簡單了,系統的關聯性減弱後,我們可以很容易的對系統進行拆分,使之前許多複雜的多表查詢變成了簡單的單表查詢,從而使sql優化這些讓我們經常頭疼的東西減少了,更重要的是單表查詢為我們帶來的一個更大的好處就是使資料庫的水平擴充套件也變得十分容易。所以在微服務架構的環境中,我們要始終考慮如何用多個簡單的業務邏輯來代替一個複雜的業務邏輯,因為這樣更容易實現資料庫在水平和垂直方向上的擴充套件。

  • 實施複雜

    當系統被拆分成多個單獨的服務時,無論在部署、測試還是監控環節,都會產生大大超出單個系統的工作量,這在做整合測試的時候會尤為突出。小而多的應用服務必然需要更多的部署節點,應對這樣的問題,需要我們必須接受自動化文化,自動化部署和自動化測試方案必須要在微服務實施開始前就規劃好。關於自動化部署和測試的方案有很多,而且大多數都已經很成熟。由於篇幅所限,筆者就不在這裡展開說了,這也不是本文的重點,希望在後面的文章中有機會跟大家分享。
    總結
    到此,我們從微服務的定義、拆分、整合、帶來的問題和解決方案等幾個方面全方位的介紹了一遍微服務,相信大家也會通過我的分享對微服務有了進一步的認識,最後,筆者通過一個拓撲圖把微服務的設計原則做一個總結。

圖6:微服務設計原則                        

再叮囑一下,原則不一定要遵守,但遵守了一定有好處。