作者:颯然Hang
rowkey.me/blog/2018/06/02/microservice-pitfall/
O’Reilly的電子書《Microservices AntiPatterns and Pitfalls》講述了在微服務設計實作時十種最常見的反模式和陷阱,本文基于此書,將這十個點列出,
資料驅動遷移反模式(Data-Driven Migration)


如上圖所示,此種反模式的問題在于微服務的粒度沒有最終確定之前就做了資料遷移,如此當不斷的調整服務粒度時,那么資料庫就免不了頻繁遷移,帶來極大的成本,更好的方式如下圖所示:

即先分離功能,資料庫先保持之前的單體,等到服務粒度最終確定之后,再分離資料庫,前后端分離與不分離的本質區別,推薦看下,
超時反模式(The Timeout)

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

-
可用性:服務消費方能夠連接服務方,并可以向其發送請求,
-
回應性:服務方能夠在消費方期望時間內給予請求回應,
為了防止服務的不可用和無法回應,通常的做法就是設定一個呼叫超時,此種做法表面上看是沒問題的,但是試想一下如下情景:發起一個購買100個商品的請求,請求成功回傳一個確認號,如果當請求超時但是請求在服務端已經成功執行了,此時這個交易實際是完成的,但是消費方沒有拿到確認號,如果重試請求,那么服務方需要一個復雜的機制判斷這是否一次重復提交,
一種解決此問題的方案是設定一個較長的超時時間,如一個服務的通常回應耗時需要2s,最大耗時需要5s,那么超時時間可以設定為10s,但這樣的問題就是如果服務不可用,所有消費方都得等待10s,這個是非常損耗性能的,
解決超時反模式的方案就是使用“斷路器模式”,就類似于房屋中的電源斷路器,當斷路器關閉,電流可以通過,當斷路器打開,那么電流中斷一直到斷路器關閉,斷路器模式就是說當檢測到服務方無法回應時就打開,后續的請求都會被拒絕掉,一旦服務方可回應了,那么斷路器關閉,恢復請求,其作業模式如下圖所示:

斷路器會持續地監測遠程服務,確保其是可回應的,只要服務可回應,那么斷路器會一直關閉,允許請求通過,如果服務突然不可回應,那么斷路器打開,拒絕后續的請求,而后續如果斷路器又檢測到服務恢復了,那么斷路器會自動關閉,請求也就恢復了,此種方案與超時時間相比,最大的優勢就是一旦服務不可回應,那么斷路器模式可以讓請求立刻回傳而不是需要等待一定的時間,
Hystrix的Netflix是此種斷路器模式的一種開源實作,此外,Akka中也包含了一個斷路器實作:Akka CircuitBreaker類,
關于“斷路器模式”的詳細資訊可見:https://martinfowler.com/bliki/CircuitBreaker.html,關注微信公眾號:Java技術堆疊,在后臺回復:設計模式,可以獲取我整理的 N 篇設計模式 教程,都是干貨,
共享反模式(“I Was Taught to Share”)

微服務被普遍認為是一種不共享任何東西的架構,但實際上只能是盡可能地少共享,畢竟在某些層面代碼被多個服務共享也能帶來一定好處,
例如,與單獨部署一套安全服務(認證和授權)其他所有服務都通過遠程訪問此服務相比,把安全相關的功能封裝成jar包(security.jar),然后其他服務都集成此jar包,就能夠避免每次都要發起對安全服務的訪問,從而提高性能和可靠性,但后面的方案帶來的問題就是依賴噩夢:每一個服務都依賴多個自定義的jar包,如此不僅打破了服務之間的邊界背景關系,同時也引入了諸如總體可靠性、變更控制、易測驗性、部署等問題,
在一個使用面向物件編程語言的單體應用中,使用abstract類和介面實作代碼復用和共享是一個良好的實踐,但當從單體切換到微服務架構時,對于很多自定義的共享類和工具類(日期、字串、計算)的處理要考慮到微服務間共享的東西越少越有利于保持服務間的邊界背景關系,從而更利于快速測驗和部署,以下是幾種推薦的方式,也是解決“共享反模式”的方案:
**共享專案
**

將共享的代碼作為一個專案在編譯期與各個服務集成,此種方式便于變更和開發軟體,但是最大的問題在于很難發覺哪一個共享模塊被修改以及修改的原因,也無法確定自己的服務是否需要這些變更,尤其是在服務發布前期發現某一個共享模塊發生了變動的話需要再一次的測驗才能走后續流程,
共享庫

此種方式即將共享的代碼作為類別庫集成到服務中,如此每次共享的庫有改動,服務都需要重新打包、測驗、重啟,但相比起第一種,其有版本標記,能夠更好地控制服務的部署和開發,服務開發者可以自己控制何時將共享庫的改動集成進來,
更進一步的,如果采用此種方案,一定要避免把所有共享的代碼都打包進一個jar包中如common.jar,否則會很難確定何時要把庫的變動集成到服務中,更好的做法是將共享代碼分成幾個單獨背景關系的庫,如:security.jar、dateutils.jar、persistence.jar等,如此會比較容易的確定何時去集成共享庫的變動,
冗余

此種方案違反DRY原則,在每一服務中都冗余一份共享代碼,能夠避免依賴共享也能夠保持邊界背景關系,但是一旦共享的代碼有變動,那么所有服務都需要改動,因此,此種方案適用于共享模塊非常穩定,極小可能變動的情況,
**服務合并
**

當多個服務共享的代碼變動比較頻繁時可以采用此種方案合并成一個服務,如此就避免了多了服務頻繁的測驗和部署,也避免了依賴共享庫,
可達性報告反模式(Reach-in Reporting)

微服務中各個服務以及其相應的資料都是包含在一個單獨的邊界背景關系中的,也就是說資料是隔離到多個資料庫中的,因此,這也會使得收集微服務的各種資料生成報告變得相對困難,一般來說有四種方案解決這個問題,其中,前三種都是從各個微服務中拉取資料,是這里所說的反模式,被稱作“Reach-in Reporting”,
資料庫拉取模式

報告服務直接從各個服務的資料庫中拉取資料從而生成各種報告,此種方式簡單迅速,但是會讓報告服務和業務服務相互依賴,是一種資料庫共享集成風格(通過共享的資料庫將多個應用耦合在一起),如此一旦資料庫有改動,所有相關服務都要改動,也就打破了微服務中極為重要的邊界背景關系,
HTTP拉取模式

與資料庫拉取模式相比,此種方式不再是直接去訪問服務的資料庫,而是通過HTTP介面去請求服務的資料,此種方式能夠保持服務的邊界背景關系,但是性能比較慢,而且HTTP請求無法很好的承載大資料,
批量拉取模式

此種方式會有一個單獨的報告資料庫/資料倉庫來存盤各個服務的聚合資料,會通過一個批量任務(離線或者基于增量實時)將服務更新的資料匯入到報告資料庫/資料倉庫中,與資料庫拉取模式一樣,此種方式這也是一種資料庫共享集成風格,會打破服務的邊界背景關系,
異步事件推送模式

此種方式即解決“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)

微服務是目前非常流行的架構理念,越來越多的公司也都在緊跟這個潮流紛紛轉型微服務架構,而不管到底自己是否真的需要,為了避免此陷阱,需要首先了解微服務的優點和缺點,
優點:
-
易部署:容易部署是微服務的一個很大的優點,畢竟相比起一個龐大的單體應用,一個小并且職責單一的微服務的部署非常簡單并且帶來的風險也會小很多,而持續部署技術則進一步放大了這個優點,
-
易測驗:職責單一、共享依賴少使得測驗一個微服務是很容易的,而基于微服務做回歸測驗與單體大應用相比也是很容易的, 控制變更:每個服務的范圍和邊界背景關系使得很容易控制服務的功能變動,
-
模塊化:微服務就是一個高度模塊化的架構風格,這種風格也是一種敏捷方式的表達,能夠很快的回應變化,一個系統模塊化程度越高,就越容易測驗、部署和發布變更,一個服務粒度劃分合理的微服務系統是所有架構中模塊化程度最高的架構形式,
-
可擴展性:由于每一個服務都是一個職責單一的細粒度服務,因此此種架構風格是所有架構分隔中可擴展性最高的,其非常容易擴展某一個或者某幾個功能從而滿足整體系統的需求,而得益于服務的容器化特性以及各種運維監控工具,服務也能夠自動化進行啟動和關閉,
缺點:
-
組織變動:微服務需要組織在很多層面進行變動,研發團隊需要包含UI、后端開發、規則處理、資料庫處理建模等多種職位,從而使得一個小的團隊能夠具有實作微服務的所有技術堆疊,同時,傳統的單體、分層應用架構的軟體發布流程也需要更新為自動化、高效的部署流水線,
-
性能:由于服務都是隔離的,因此發起對服務的遠程呼叫肯定是會影響性能的,服務編排、運行環境都是影響性能的很大因素,了解遠程呼叫的延遲、需要與多少服務通信都是與性能相關的需要掌握的資訊,
-
可靠性:和性能一樣,服務的遠程呼叫越多,那么失敗的幾率就越高,總體的可靠性就會越低,
-
DevOps:隨著微服務架構而來的是成千上百的服務,手動管理這么多的服務是很不現實的,這就對于自動化運維部署、協作提出了很高的挑戰,需要依賴非常多的操作工具和實踐,是一個非常復雜的作業,目前差不多有12種型別的操作工具(監控工具、服務注冊、發現工具、部署工具等)和框架在微服務架構中被使用,其中每一種又包含了很多具體的工具和產品供選擇,對于這些工具和框架的選擇一般都會需要將近數月的研究、測驗、權衡分析才能做出最適合的技術選型,
了解了微服務的優缺點后,下一步則需要根據實際的業務來分析微服務是不是解決這些問題的最佳方案,可以采取以下問題:
-
業務和技術的目標是什么?
-
使用微服務是為了完成什么?
-
目前和可預知的痛點是什么?
-
應用的最關鍵的技術特性是什么?(性能、易部署性、易測驗性、可擴展性)
回答這些問題再結合微服務的優缺點能夠讓你明確現在是否是使用微服務的適當時機,
除了微服務以外,還有其他7種比較普遍使用的架構供選擇:
-
基于服務的架構(Service-Based)
-
面向服務的架構(Service-Oriented)
-
分層架構(Layered)
-
微內核架構(Microkernel)
-
基于空間的架構(Space-Based)
-
事件驅動架構(Event-Driven)
-
流水線架構(Pipeline)
靜態合約陷阱(The Static Contract)

微服務的消費方和服務提供方之間會有一個合約/協議用來規定輸入輸出資料的格式、操作名稱等等,一般情況下這個合約是不變的,但是如果沒有使用版本號來管理服務介面,那么就會進入“靜態合約”陷阱,
給合約打上版本標記不僅僅能夠避免巨大的變動(服務提供方修改合約使得所有消費方也都得修改),還能夠提供向后兼容性,這里有兩種技術可以實作合約的版本號:
在頭部資訊附加版本號

如圖,此種方式即在遠程訪問協議的頭部添加版本資訊,而如果遠程協議使用的是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);
在合約本身中附加版本號

此種方式版本號獨立于遠程訪問協議,與頭部資訊版本號相比,這也是其最大的優點,但與此同時,其缺點比較多,首先要從請求資訊主體中決議版本號,會出現很多決議的問題,其次,合約的模式可能會非常復雜,使得很難做資料轉換,最后,服務還要引入對模式的驗證邏輯,
我們到了嗎陷阱(Are We There Yet)

微服務架構中,各個服務都是獨立的個體,也就意味著所有客戶端或者API層和服務之間的通信都是一次遠程呼叫,如果對這些遠程呼叫的耗時沒有什么概念,那么就陷入了“Are We There Yet”陷阱,合理的做法需要去測驗遠程訪問的平均延遲、長尾延遲(95%、99%、99.%之外的請求延遲)等指標,而很多時候即使有很好的平均延遲,但是較差的長尾延遲會造成非常大的破壞,
在生產環境或者準生產環境測驗有助于去了解應用的真實性能,例如,一個業務請求需要呼叫四個服務,假設一個服務呼叫的延遲是100毫秒,那么加上業務請求本身的延遲,完成此次業務請求共需要500毫秒的延遲,這和單單從代碼上去看得出的結論是不一樣的,
了解目前所用協議的平均延遲是一方面,另一方面則需要對比其他遠程協議的延遲,從而在合適的地方使用合適的協議,如:JMS、AMQP、MSMQ,

如圖,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之前會一直保存在佇列中),
因此對于服務消費者需要合并多個遠程請求到一個事務中的場景可以選擇事務訊息,
關注公眾號Java技術堆疊回復"面試"獲取我整理的2020最全面試題及答案,
推薦去我的博客閱讀更多:
1.Java JVM、集合、多執行緒、新特性系列教程
2.Spring MVC、Spring Boot、Spring Cloud 系列教程
3.Maven、Git、Eclipse、Intellij IDEA 系列工具教程
4.Java、后端、架構、阿里巴巴等大廠最新面試題
覺得不錯,別忘了點贊+轉發哦!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/148105.html
標籤:Java
上一篇:Java 日期大小對比及日期轉換
下一篇:變數的四種參考
