1. 程式人生 > 實用技巧 >微服務的反模式和陷阱

微服務的反模式和陷阱

微服務的反模式和陷阱

本文轉自

資料驅動的遷移反模式

Data-Driven Migration AntiPattern

微服務會建立大量小的、分散式的、單一用途的服務,每個服務擁有自己的資料。這種服務和資料耦合支援一個有界的上下文和一個無共享資料的架構,其中,每個服務及其對應的資料是獨立一塊,完全獨立於所有其他服務。服務只暴露了一個明確的介面(服務契約)。有界的上下文可以允許開發者以最小的依賴快速輕鬆地開發,測試和部署。

採用資料驅動的遷移反模式大多是當你從一個單體(monolithic)應用程式到微服務架構遷移的時候。我們將之稱為反模式原因是,它似乎是在開始建立微服務的時候看起來是一個好主意,服務和相應的資料獨立成一個微服務,非常的美好,但正如書中接下來介紹的,這可能會將你引向一個錯誤的道路上,問題是高風險,過剩成本和額外的遷移工作。

單體應用遷移到微服務架構有兩個主要目標:第一個目標是單體應用程式的功能分割成小的,單一用途的服務。第二個目標是單體應用的資料遷移到每個服務自己獨佔的小資料庫(或獨立的服務)。

如上圖所示,理想很豐滿,但是現實很骨感。

太多的資料遷移

這種遷移最主要的問題是你很難一次將資料庫的粒度很好的分割成獨立的每個微服務獨佔的資料。

當然你開始可以分割成粗粒度的資料和服務,然後再進一步的分割成更小的微服務和資料,你可能要頻繁地進行服務調整:粒度太小,合併微服務;粒度太大,分割成更小的微服務。資料遷移要比原始碼遷移更復雜,更容易出錯,理想情況下只為微服務遷移資料一次。理解資料遷移的風險性是避免這種反模式的第一步。

功能分割優先,資料遷移最後

避免這種反模式的主要技術就是首先遷移功能,然後再考慮劃分這個微服務和它的資料的有界上下文。

超時反模式

微服務是一種分散式的架構,它所有的元件(也就是服務)會被部署為單獨的應用程式,並通過某種遠端訪問協議進行通訊。分散式應用的挑戰之一就是如何管理遠端服務的可用性和它們的響應。雖然服務可用性和服務響應都涉及到服務的通訊,但它們是兩個完全不同的東西。服務可用性是服務消費者連線服務並能夠傳送請求的能力,服務響應則關注服務的響應時間。

如果服務不可用,服務消費者會在毫秒級的時間內得到通知,它可以返回錯誤資訊或者嘗試連線。但是如果服務接收了請求但是不響應,服務消費者該怎麼辦?可以無限等待或者設定一個超時時間。

超時看起來也挺不錯,但也會導致反模式。

使用超時

你可能很困惑,難道不應該設定一個超時時間嗎?你理解的很對,但是在大部分的情況超時時間的錯誤設定會帶來問題。比如當你買東西的時候,你提交了訂單,服務一直在處理沒有返回,你在超時的時候再提交訂單,顯然伺服器需要更復雜的邏輯來處理重複提交訂單的問題。

所以你可能不想設定超時時間太短,但是多少合適呢?一種基於資料庫的超時來計算服務的超時時間,另一種更常用,計算大壓力下最長的處理時間,把它乘以2作為超時時間。但是這個值可能非常的不合適,有可能使用者覺得等待太久就把頁面關了,所以還的尋找更好的方式。

使用熔斷器設計模式

Circuit Breaker Pattern

這種設計模式就像家裡的電器的保險絲一樣,當負載過大,或者電路發生故障或異常時,電流會不斷升高,為防止升高的電流有可能損壞電路中的某些重要器件或貴重器件,燒燬電路甚至造成火災。保險絲會在電流異常升高到一定的高度和熱度的時候,自身熔斷切斷電流,從而起到保護電路安全執行的作用。

當一個軟體熔斷器監測到服務沒有響應的時候,它就會熔斷,拒絕請求。一旦服務恢復,熔斷器就會接上,允許服務通過。

熔斷器有多種方式監控服務的可用性,最簡單的方式就是心跳檢查,也可以用呼叫虛擬的服務精確監測,還可以實時地監控服務呼叫的狀態,一旦到達一個閾值,則熔斷器進入限流的狀態。

共享反模式

I Was Taught to Share" AntiPattern

微服務是一種無共享的架構,我更傾向於叫它為"儘量不分享"模式(share-as-little-as-possible), 因為總有一些程式碼會在微服務之間共享。比如不提供一個身份驗證的微服務,而是將身份驗證的程式碼打包成一個jar檔案:security.jar,其它服務都能使用。如果安全檢查是服務級別的功能,每個服務接收到請求都會檢查安全性,這種方式可以很好的提高效能。

但是這容易引起"依賴噩夢":

太多依賴

如果你使用面向物件的開發語言,肯定會遇到下面的繼承關係,成百的模組拆成微服務的時候都會依賴很多的共享庫。

微服務的目標之一就是儘量少的共享,這會幫助微服務確定它的有界上下文,服務更容易的測試和部署。服務之間依賴越多,服務則更難被隔離。

共享程式碼的技術

說起來容易做起來難,下圖介紹了程式碼共享帶來問題的四種場景。

前三種的共享程式碼的方式好理解,第四種方式是將共享程式碼的邏輯分割出來,做成一個單獨的服務,比如身份驗證服務。

對於共享庫來說,不要把所有共享的程式碼放在一個庫中,比如common.jar,而是根據它們的邏輯劃分成更小的庫,比如security.jar, persistence.jar, dateutils.jar

到達報告反模式

Reach-in Reporting AntiPattern

有四種方式可以處理微服務架構中的報告。

  • database pull model
  • HTTP pull model
  • batch pull model
  • event-based push model

前三個模型都是從微服務自己的資料庫中拉取資料,所以這個反模式就叫"rearch-in reporting"。既然前三種會出現這中反模式,我們就先看看為什麼它們會帶來麻煩。

微服務報告的問題

問題有兩面:

  1. 如何定時的獲取報告的資料?
  2. 仍然保持著微服務和資料的的有界上下文?

下圖是database pull model,直接訪問資料庫,這會帶來資料庫的非獨立性

下圖是HTTP pull model,微服務提供資料報告介面,但是會影響微服務的效能,尤其是複雜報告的查詢。

下圖是batch pull model,批處理程式成批地將微服務的資料倒入到報告的資料庫中,但是問題和HTTP pull model一樣,這會帶來資料庫的非獨立性,微服務資料庫格式的改變也會影響報告服務。

Asynchronous Event Pushing

下圖是event-based push model,也叫 data pump。雖然相對複雜,但是不會違背本節開始提出的兩個問題。

沙粒陷阱

Grains of Sand Pitfall

架構師和開發人員在採用微服務架構的時候最大的挑戰之一就是服務粒度的問題。微服務的服務粒度多大合適?服務粒度至關重要,它會影響應用的效能、健壯性、可靠性、可測性、設定釋出模型。

當服務的粒度太小的時候就會遇到沙粒陷阱。微服務的微並不意味著服務越小越好,但是多小是小?

如果你檢視一下微服務實現的專案,會發現很多一個類實現的微服務,在這種情況下會容易遇到沙粒陷阱。

當然微服務的粒度並不是靠服務實現的類的數量所決定的,有些服務很簡單,只需一個簡單的類就可以實現,而有些確需要更多的類。既然類的數量不能用來決定微服務的粒度,那麼用什麼標準來衡量微服務的粒度是合適的呢?主要依賴:服務的範圍(scope)和功能(functionality)、資料庫事務的需求以及服務編排的等級。

分析服務的範圍和功能

服務要做什麼?有那些操作?

完整性(Cohesion)扮演了很重要的角色。比如一個顧客服務(customer service)有下面的操作:

  • add_customer
  • update_customer
  • get_customer
  • notify_customer
  • record_customer_comments
  • get_customer_comments

前三個操作是相關的,它們用來管理和維護顧客資訊。但是後三個並不和基本的CRUD操作相關。在分析這個服務的完整性的時候,我們就比較清晰了,這個服務可以被分成三個服務:顧客資訊服務、顧客通知服務和顧客評論服務。

Sam Newman提供了一個很好的可操作的方法,開始不妨將服務劃分成粗粒度的服務,隨著對服務瞭解更多,再進一步劃分成更小粒度的服務。

分析資料庫事務

資料庫事務更正式的叫做 ACID 事務 (atomicity, consistency, isolation, and durability)。ACID事務封裝多個數據庫更新為一個工作單元,工作單元要不整體完成,要不就出現錯誤而回滾。

因為微服務架構中服務是分散式的獨立的應用,再兩個或者多個服務之間維護 ACID 事務就極度困難,所以微服務架構中經常會依賴 BASE (basic availability, soft state, and eventual consistency)。儘管如此,你還是再特定的服務中要使用 ACID 事務。當你需要在 ACID vs. BASE 事務中做艱難的決定的時候,可能你的服務劃分的就太細了。

當發現不能使用最終一致性時,你通常就會把服務從細粒度調整為粗粒度的服務,如圖所示。

分析服務編排

第三個衡量方式是分析服務編排。服務編排是指服務之間的通訊,通常也指內部服務通訊。
遠端呼叫服務是需要花時間的,它會降低應用整體的效能。再者,它也會影響服務的健壯性和可靠性。

如果你發現完成一個邏輯請求需要呼叫太多的服務時,服務的劃分可能粒度就太小了。

整合服務、合併到更粗粒度可以提升應用的整體效能,提高應用的健壯性和可靠性。你還可以移除服務之間的依賴,可以更好的控制、測試和釋出。

當然你可能會說呼叫多個服務可以並行的執行,提到應用的響應,比如 reactive 架構的非同步程式設計方式, 關鍵還是要權衡利弊, 確保對使用者的及時響應以及系統整體的可靠性。

無因的開發者陷阱

名字來自詹姆斯·迪恩演的電影《無因的反叛》(Rebel Without a Cause),一個問題青年因為錯誤的原因做了錯誤的決定。

很多架構師和開發者在微服務的開發中權衡利弊, 比如服務粒度和運維工具,但是基於錯誤的原因,做了錯誤的決定。

下圖就是一個場景。服務被認為粒度太細,影響效能和可靠性,所以要遷移到一個單一的粒度更粗的服務上

看起來合情合理,但是沒有考慮tradeoff。釋出、改變控制、測試都深受影響。

下面正好相反,將粗粒度服務劃分成細粒度的服務。

如果你不考慮這種改變帶來的tradeoff,可能影響是很大的。

作者指出,要深刻理解選擇微服務後面的商業驅動。

隨大流陷阱

Jump on the Bandwagon Pitfall

因為微服務是現在的潮流,所以你選擇了微服務,還沒有仔細的分析你的商業需求、商業驅動、組織架構和技術環境,這就是隨大流陷阱。

微服務並不適合所有的場景。

避免這個陷阱的方式充分理解微服務的好處和短處,俗話說,知己知彼,百戰不殆。

好處:

  • 釋出:易於釋出
  • 測試:易於測試
  • 改變控制:更容易的改變一個服務的功能
  • 模組-
  • 規模可擴充套件

短處

  • Team組織改變
  • 效能
  • 可靠性降低
  • 運維難度加大

所以理解了微服務的優缺點,結合自己的實際情況,來決定是否要採用微服務。

其它架構模式

微服務的架構很好,但是不是唯一的架構模式,比如下面還有一些其它的架構模式:

  • Service-Based Architecture
  • Service-Oriented Architecture
  • Layered Architecture
  • Microkernel Architecture
  • Space-Based Architecture
  • Event-Driven Architecture
  • Pipeline Architecture

當然你並不一定只使用唯一的一種架構模式,你可能在系統中混用這些架構模式。

下面有一些架構的參考資料:

靜態契約陷阱

The Static Contract Pitfall

這一節主要講服務的版本控制。入股服務一開始就沒有考慮版本控制,服務的schema發生變化時,或者內部實現邏輯有變化時消費者和伺服器之間的通訊和業務處理就會發生問題。

所以你要為你的服務設計版本號。

有兩種實現方式,在header中加入版本號,或者在服務的schema中加入版本號。

我們到了嗎陷阱

Are We There Yet Pitfall

這個陷阱發生在你不知道遠端呼叫要花多長時間的情況。50秒?平均多長時間呢,長尾的延遲呢?

首先你應該測量服務的呼叫時間,至少能知道一個服務遠端呼叫的大概時間。

然後你應該評估不同的服務通訊協議的效能,比如REST、JMS、AMQP等。

當然效能也不是唯一個衡量遠端通訊協議的因素,比如下面一節中講到的內容。

REST陷阱

使用REST風格非常的流行,大部分的軟體框架也選擇它作為通訊的方式,比如DropWizard, Spring Boot等,有興趣的讀者可以閱讀我寫的Java RESTful框架的效能比較

既然大家都在用它,還怎麼是個陷阱呢?

如果把REST作為唯一的通訊方式,就有可能掉入這個陷阱。比如如何處理非同步通訊(http 1.1是blocking的)、如何在一個事務中管理多次服務呼叫?如何支援廣播?

你應該考慮兩種型別的訊息標準作為微服務架構中的訊息傳遞:特定平臺的標準和平臺無關的標準。

特定平臺的標準比如 JMS for java、MSMQ for .net。平臺無關的比如 AMQP。

使用訊息系統的好處可以非同步請求,還可以實現廣播的方式,還可以實現事務請求。