微服務設計實現時的十大常見反模式和陷阱

數據驅動遷移反模式(Data-Driven Migration)

1.png


如上圖所示,此種反模式的問題在於微服務的粒度沒有最終肯定以前就作了數據遷移,如此當不斷的調整服務粒度時,那麼數據庫就免不了頻繁遷移,帶來極大的成本。更好的方式以下圖所示:數據庫

2.png


即先分離功能,數據庫先保持以前的單體,等到服務粒度最終肯定以後,再分離數據庫。編程

超時反模式(The Timeout)

微服務架構是由一系列分離的服務組成的,這些服務之間經過一些遠程協議進行互相之間的通訊。其中牽扯到了服務的可用性和響應性問題。以下圖所示:json

3.png

 

  • 可用性:服務消費方可以鏈接服務方,並能夠向其發送請求。
  • 響應性:服務方可以在消費方指望時間內給予請求響應。


爲了防止服務的不可用和沒法響應,一般的作法就是設置一個調用超時。此種作法表面上看是沒問題的,可是試想一下以下情景:發起一個購買100個商品的請求,請求成功返回一個確認號。若是當請求超時可是請求在服務端已經成功執行了,此時這個交易實際是完成的,可是消費方沒有拿到確認號,若是重試請求,那麼服務方須要一個複雜的機制判斷這是否一次重複提交。

一種解決此問題的方案是設置一個較長的超時時間,如一個服務的一般響應耗時須要2s,最大耗時須要5s,那麼超時時間能夠設置爲10s。但這樣的問題就是若是服務不可用,全部消費方都得等待10s,這個是很是損耗性能的。

解決超時反模式的方案就是使用「斷路器模式」。就相似於房屋中的電源斷路器,當斷路器關閉,電流能夠經過,當斷路器打開,那麼電流中斷一直到斷路器關閉。斷路器模式就是說當檢測到服務方沒法響應時就打開,後續的請求都會被拒絕掉。一旦服務方可響應了,那麼斷路器關閉,恢復請求。其工做模式以下圖所示:後端

4.png


斷路器會持續地監測遠程服務,確保其是可響應的。只要服務可響應,那麼斷路器會一直關閉,容許請求經過。若是服務忽然不可響應,那麼斷路器打開,拒絕後續的請求。然後續若是斷路器又檢測到服務恢復了,那麼斷路器會自動關閉,請求也就恢復了。此種方案與超時時間相比,最大的優點就是一旦服務不可響應,那麼斷路器模式可讓請求馬上返回而不是須要等待必定的時間。

Hystrix的Netflix是此種斷路器模式的一種開源實現。此外,Akka中也包含了一個斷路器實現:Akka CircuitBreaker類。
 安全

共享反模式(「I Was Taught to Share」)

微服務被廣泛認爲是一種不共享任何東西的架構。但實際上只能是儘量地少共享,畢竟在某些層面代碼被多個服務共享也能帶來必定好處。例如,與單獨部署一套安全服務(認證和受權)其餘全部服務都經過遠程訪問此服務相比,把安全相關的功能封裝成jar包(security.jar),而後其餘服務都集成此jar包,就可以避免每次都要發起對安全服務的訪問,從而提升性能和可靠性。但後面的方案帶來的問題就是依賴噩夢:每個服務都依賴多個自定義的jar包。如此不只打破了服務之間的邊界上下文,同時也引入了諸如整體可靠性、變動控制、易測試性、部署等問題。

在一個使用面向對象編程語言的單體應用中,使用abstract類和接口實現代碼複用和共享是一個良好的實踐。但當從單體切換到微服務架構時,對於不少自定義的共享類和工具類(日期、字符串、計算)的處理要考慮到微服務間共享的東西越少越有利於保持服務間的邊界上下文,從而更利於快速測試和部署。如下是幾種推薦的方式,也是解決「共享反模式」的方案:性能優化

共享項目

5.png


將共享的代碼做爲一個項目在編譯期與各個服務集成。此種方式便於變動和開發軟件,可是最大的問題在於很難發覺哪個共享模塊被修改以及修改的緣由,也沒法肯定本身的服務是否須要這些變動。尤爲是在服務發佈前期發現某一個共享模塊發生了變更的話須要再一次的測試才能走後續流程。架構

共享庫

6.png


此種方式即將共享的代碼做爲類庫集成到服務中。如此每次共享的庫有改動,服務都須要從新打包、測試、重啓。但相比起第一種,其有版本標記,可以更好地控制服務的部署和開發,服務開發者能夠本身控制什麼時候將共享庫的改動集成進來。

更進一步的,若是採用此種方案,必定要避免把全部共享的代碼都打包進一個jar包中如common.jar。不然會很難肯定什麼時候要把庫的變更集成到服務中。更好的作法是將共享代碼分紅幾個單獨上下文的庫,如:security.jar、dateutils.jar、persistence.jar等,如此會比較容易的肯定什麼時候去集成共享庫的變更。併發

冗餘

7.png


此種方案違反DRY原則,在每一服務中都冗餘一份共享代碼,可以避免依賴共享也可以保持邊界上下文。可是一旦共享的代碼有變更,那麼全部服務都須要改動。所以,此種方案適用於共享模塊很是穩定,極小可能變更的狀況。app

服務合併

8.png


當多個服務共享的代碼變更比較頻繁時能夠採用此種方案合併成一個服務,如此就避免了多了服務頻繁的測試和部署,也避免了依賴共享庫。框架

可達性報告反模式(Reach-in Reporting)

微服務中各個服務以及其相應的數據都是包含在一個單獨的邊界上下文中的,也就是說數據是隔離到多個數據庫中的。所以,這也會使得收集微服務的各類數據生成報告變得相對困難。通常來講有四種方案解決這個問題。其中,前三種都是從各個微服務中拉取數據,是這裏所說的反模式,被稱做「Reach-in Reporting」。

數據庫拉取模式

9.png


報告服務直接從各個服務的數據庫中拉取數據從而生成各類報告。此種方式簡單迅速,可是會讓報告服務和業務服務相互依賴,是一種數據庫共享集成風格(經過共享的數據庫將多個應用耦合在一塊兒)。如此一旦數據庫有改動,全部相關服務都要改動,也就打破了微服務中極爲重要的邊界上下文。

HTTP拉取模式

10.png


與數據庫拉取模式相比,此種方式再也不是直接去訪問服務的數據庫,而是經過HTTP接口去請求服務的數據。此種方式可以保持服務的邊界上下文,可是性能比較慢,並且HTTP請求沒法很好的承載大數據。

批量拉取模式

11.png


此種方式會有一個單獨的報告數據庫/數據倉庫來存儲各個服務的聚合數據。會經過一個批量任務(離線或者基於增量實時)將服務更新的數據導入到報告數據庫/數據倉庫中。與數據庫拉取模式同樣,此種方式這也是一種數據庫共享集成風格,會打破服務的邊界上下文。

異步事件推送模式

12.png


此種方式即解決「Reach-in Reporting」反模式的方案。每一個服務都把本身的發生的事件異步推送到一個數據捕獲服務,後續數據捕獲服務會將數據解析存儲到報告數據庫中。此種方式實現起來較複雜,須要在服務和數據捕獲服務之間制定一種協議用於異步傳輸事件數據。但其可以保持服務的邊界上下文,同時也能保證數據的時效性。

沙粒陷阱(Grains of Sand)

微服務實現中最有挑戰的問題在於如何拆分service,如何控制服務的粒度,而正確的服務粒度則決定了微服務是否可以成功實現。服務粒度也可以影響到性能、健壯性、可靠性、易測試性、部署等。

「沙粒陷阱」即把服務拆分的太細。其中的一個緣由就是不少時候開發者會把一個class與一個服務等同。合理的,應該是一個服務組件(Service component)對應一個服務。一個服務組件具備清晰、簡潔的角色、職責,具備一組定義好的操做。其通常經過多個模塊(Java Class)實現。若是組件和模塊是一對一的關係,那麼不只僅會形成服務粒度過細同時也是一種很差的編程實踐:服務的實現都是經過一個Class,那麼此Class會很是大而且承擔太多的責任,不利於測試和維護。

更進一步的,服務的粒度並不該該受其中實現類的數目影響:有些服務可能只須要一個類就能夠實現,而有些服務會須要多個類來實現。

爲了不「沙粒陷阱」,能夠經過如下三種測試來判斷服務粒度是否合理:

分析服務範圍和功能

要明確服務用來幹什麼?有哪些操做?通常經過使用文檔或者語言來描述服務的範圍和功能就可以看出來服務是否作的工做太多。若是在描述中使用了「和」(「and」)或者「此外」(「in addition」)之類的詞,頗有可能就是此服務職責太多。

服務的高內聚是一種良好的實踐,其明確一個服務提供的操做之間必需要是有關聯的。如對於一個顧客服務,有如下操做:

  • 添加顧客
  • 更新顧客信息
  • 獲取顧客信息
  • 通知顧客
  • 記錄顧客評論
  • 獲取顧客評論


其中的前三個操做都是對顧客的CRUD操做,是相關聯的。然後三者則無關。爲了實現服務的高內聚,合理的應該是把此服務拆分紅三個服務:顧客維護、顧客通知、顧客評論。

如此,以粗粒度的服務開始,而後逐漸拆分紅細粒度的服務有利於對微服務的拆分。

分析數據庫事務

傳統的關係型數據庫都提供了ACID事務特性用於把多個更新操做打包成一個總體提交,要麼都成功,要麼都失敗。而在微服務中,因爲服務都是一個個分離的應用,很難實現ACID,通常實現BASE事務(basic availability、soft state、eventual consistence)便可。可是沒法避免的,仍然會有一些場景是須要ACID的。所以,當你不斷的須要在BASE和ACID事務作判斷和取捨的時候,頗有可能就是服務粒度過細。

若是業務場景沒法接受最終一致性,那麼最好就是將服務粒度粗化一些,把多個更新操做放到一個服務中。

分析服務編排

這裏主要說的是服務之間的互相通訊。因爲對服務的調用都是一次遠程調用,所以服務編排會很是大的影響微應用整體的性能。此外,它也會影響系統總體的健壯性和可靠性,越多的遠程調用,那麼越高的概率會有失敗或者超時的請求出現。

若是發現完成一次業務邏輯須要調用太多的遠程服務,就說明服務的粒度可能太細了。這時候就須要將服務粗化。而合併細粒度服務還可以提升性能,提高整體的健壯性和可靠性。同時也減小了多個服務間的依賴,更利於測試和部署。

此外,使用響應式編程技術異步並行調用遠程服務也是一種提高性能和可靠性的方案。

無因的開發者陷阱(Developer Without a Cause)

此陷阱主要講的是開發者或者架構師在作設計時不少時候是拍腦殼在作,沒有任何合理的緣由或者緣由是錯誤的,也不會作取捨。而想要解決此問題,不只僅是架構師,開發者也須要同時瞭解技術帶來的好處以及缺陷,從中作權衡。

瞭解業務驅動是避免此陷阱的關鍵一步。每個開發者和架構師都應該清楚的瞭解下面這些問題的答案:

  • 爲何要使用微服務?
  • 最重要的業務驅動是什麼?
  • 架構中的哪一點是最爲重要的?


假如易部署性、性能、健壯性、可擴展性是系統最看重的特性,那麼對於不一樣的業務側重點,微服務的粒度需求也是不一樣的。細粒度的服務可以達到更好的易測試性和易部署性,而粗粒度的服務則有更好的性能、健壯性以及可靠性。
 

追隨流行陷阱(Jump on the Bandwagon)

微服務是目前很是流行的架構理念,愈來愈多的公司也都在緊跟這個潮流紛紛轉型微服務架構,而無論到底本身是否真的須要。爲了不此陷阱,須要首先了解微服務的優勢和缺點。

優勢:

  • 易部署:容易部署是微服務的一個很大的優勢。畢竟相比起一個龐大的單體應用,一個小而且職責單一的微服務的部署很是簡單而且帶來的風險也會小不少。而持續部署技術則進一步放大了這個優勢。
  • 易測試:職責單1、共享依賴少使得測試一個微服務是很容易的。而基於微服務作迴歸測試與單體大應用相比也是很容易的。 控制變動:每一個服務的範圍和邊界上下文使得很容易控制服務的功能變更。
  • 模塊化:微服務就是一個高度模塊化的架構風格。這種風格也是一種敏捷方式的表達,可以很快的響應變化。一個系統模塊化程度越高,就越容易測試、部署和發佈變動。一個服務粒度劃分合理的微服務系統是全部架構中模塊化程度最高的架構形式。
  • 可擴展性:因爲每個服務都是一個職責單一的細粒度服務,所以此種架構風格是全部架構分隔中可擴展性最高的。其很是容易擴展某一個或者某幾個功能從而知足總體系統的需求。而得益於服務的容器化特性以及各類運維監控工具,服務也可以自動化進行啓動和關閉。


缺點:

  • 組織變更:微服務須要組織在不少層面進行變更。研發團隊須要包含UI、後端開發、規則處理、數據庫處理建模等多種職位,從而使得一個小的團隊可以具備實現微服務的全部技術棧。同時,傳統的單體、分層應用架構的軟件發佈流程也須要更新爲自動化、高效的部署流水線。
  • 性能:因爲服務都是隔離的,所以發起對服務的遠程調用確定是會影響性能的。服務編排、運行環境都是影響性能的很大因素。瞭解遠程調用的延遲、須要與多少服務通訊都是與性能相關的須要掌握的信息。
  • 可靠性:和性能同樣。服務的遠程調用越多,那麼失敗的概率就越高,整體的可靠性就會越低。
  • DevOps:隨着微服務架構而來的是成千上百的服務。手動管理這麼多的服務是很不現實的。這就對於自動化運維部署、協做提出了很高的挑戰。須要依賴很是多的操做工具和實踐,是一個很是複雜的工做。目前差很少有12種類型的操做工具(監控工具、服務註冊、發現工具、部署工具等)和框架在微服務架構中被使用,其中每一種又包含了不少具體的工具和產品供選擇。對於這些工具和框架的選擇通常都會須要將近數月的研究、測試、權衡分析才能作出最適合的技術選型。


瞭解了微服務的優缺點後,下一步則須要根據實際的業務來分析微服務是否是解決這些問題的最佳方案。能夠採起如下問題:

  • 業務和技術的目標是什麼?
  • 使用微服務是爲了完成什麼?
  • 目前和可預知的痛點是什麼?
  • 應用的最關鍵的技術特性是什麼?(性能、易部署性、易測試性、可擴展性)


回答這些問題再結合微服務的優缺點可以讓你明確如今是不是使用微服務的適當時機。

除了微服務之外,還有其餘7種比較廣泛使用的架構供選擇:

  • 基於服務的架構(Service-Based)
  • 面向服務的架構(Service-Oriented)
  • 分層架構(Layered)
  • 微內核架構(Microkernel)
  • 基於空間的架構(Space-Based)
  • 事件驅動架構(Event-Driven)
  • 流水線架構(Pipeline)

 

靜態合約陷阱(The Static Contract)

微服務的消費方和服務提供方之間會有一個合約/協議用來規定輸入輸出數據的格式、操做名稱等等。通常狀況下這個合約是不變的。可是若是沒有使用版本號來管理服務接口,那麼就會進入「靜態合約」陷阱。

給合約打上版本標記不只僅可以避免巨大的變更(服務提供方修改合約使得全部消費方也都得修改),還可以提供向後兼容性。這裏有兩種技術能夠實現合約的版本號:

在頭部信息附加版本號

13.png


如圖,此種方式即在遠程訪問協議的頭部添加版本信息。而若是遠程協議使用的是REST,那麼還可使用vendor mime type(vnd)來指定合約的版本號。以下:

POST /trade/buy
Accept: application/vnd.svc.trade.v2+json


服務接受到請求,可以經過正則等手段簡單解析出其中的合約版本號再根據版本號作相應的處理。

若是使用消息隊列,那麼能夠將版本號放置在屬性部分(Property section)。JMS的一個例子以下:

String msg = createJSON("acct","12345","sedol","2046251","shares","1000");
jsmContext.createProducer()
  .setProperty("version",2)
  .send(queue,msg);

 

在合約自己中附加版本號

14.png


此種方式版本號獨立於遠程訪問協議,與頭部信息版本號相比,這也是其最大的優勢。但與此同時,其缺點比較多。首先要從請求信息主體中解析版本號,會出現不少解析的問題。其次,合約的模式可能會很是複雜,使得很難作數據轉換。最後,服務還要引入對模式的驗證邏輯。

咱們到了嗎陷阱(Are We There Yet)

微服務架構中,各個服務都是獨立的個體,也就意味着全部客戶端或者API層和服務之間的通訊都是一次遠程調用。若是對這些遠程調用的耗時沒有什麼概念,那麼就陷入了「Are We There Yet」陷阱。合理的作法須要去測試遠程訪問的平均延遲、長尾延遲(95%、99%、99.%以外的請求延遲)等指標。而不少時候即便有很好的平均延遲,可是較差的長尾延遲會形成很是大的破壞。

在生產環境或者準生產環境測試有助於去了解應用的真實性能。例如,一個業務請求須要調用四個服務,假設一個服務調用的延遲是100毫秒,那麼加上業務請求自己的延遲,完成這次業務請求共須要500毫秒的延遲。這和單單從代碼上去看得出的結論是不同的。

瞭解目前所用協議的平均延遲是一方面,另外一方面則須要對比其餘遠程協議的延遲,從而在合適的地方使用合適的協議。如:JMS、AMQP、MSMQ。

15.png


如圖,AMQP協議的性能是最好的。那麼結合業務場景,就能夠選擇REST做爲客戶端與服務間的通訊協議,AMQP作爲服務之間的通訊協議以提升應用的性能。

固然,性能並不是在選擇遠程協議時惟一考慮的因素。下一節中就會考慮利用消息隊列的一些額外功能。

REST使用陷阱(Give It a Rest)

REST如今是微服務中用的最多的通訊協議。流行的開發框架如DropWizard、Spring Boot都提供了REST支持。可是若是隻選擇REST這一種協議,不去考慮其餘諸如消息隊列的優點,那麼就陷入了「REST使用」陷阱。畢竟異步通訊、廣播、合併請求事務這些需求,REST是很難實現的。

消息隊列標準目前包括平臺特定和平臺無關兩種。前者包括Java平臺中的JMS和C#平臺的MSMQ,後者則是AMQP。對於平臺特定的消息標準JMS,其規範了API,所以切換broker實現(ActiveMQ、HornetQ)時無需修改API,但因爲底層通訊協議是不一樣的,集成的客戶端或者服務端jar包須要隨着修改。對於平臺無關的消息標準,其規範了協議實現標準,並無規範API。使得不一樣平臺之間均可以互相通訊,而無論實際產品是什麼。如一個使用了RabbitMQ的客戶端能夠很容易地與一個StormMQ通訊(假設使用的協議相同)。也就是其獨立於平臺的特性使得RabbitMQ成爲微服務架構中最流行的消息隊列。

異步請求

異步通訊是消息隊列適用的場景之一。服務消費者發起請求後無需等待服務方響應可以提升整體的性能,同時調用方無需擔憂調用超時,也就無需使用斷路器,從而提升了系統的可靠性。

廣播

將消息廣播給多個service是消息隊列的又一個適用場景。一個消息生產者向多個消息接受者發送消息,無需知道誰在接受消息以及如何處理它。

事務請求

消息系統提供了對事務消息的支持:若是多個消息被髮送到了在一個交易上下文的多個隊列或者主題中時,那麼直到消息發送者commit,服務纔會真正的接受到相應的全部消息(在commit以前會一直保存在隊列中)。

所以對於服務消費者須要合併多個遠程請求到一個事務中的場景能夠選擇事務消息。

推薦一個交流學習羣:705127209 裏面會分享一些資深架構師錄製的視頻錄像:有Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化這些成爲架構師必備的知識體系。還能領取免費的學習資源,目前受益良多:

相關文章
相關標籤/搜索