微服務之集成(四)下

  • 2019 年 10 月 3 日
  • 筆記

8. 實現基於事件的非同步協作方式

前面講了一些與請求/響應模式相關的技術。那麼基於事件的非同步通訊呢?

8.1 技術選擇

主要有兩個部分需要考慮:微服務發布事件機制和消費者接收事件機制。

方法一:使用消息代理

傳統上來說,像RabbitMQ這樣的消息代理能夠處理上述兩個方面的問題。生產者(producer)使用API向代理髮布事件,代理也可以向消費者提供訂閱服務,並且在時間發生時通知消費者。

不過需要注意的是,消息代理僅僅是中間件世界中的一個小部分而是。有一個原則需要謹記:盡量讓中間件保持簡單,而把業務邏輯放在自己的服務中。

方法二:使用HTTP來傳播事件

ATOM是一個符合REST規範的協議,可以通過它提供資源聚合(feed)的發布服務,而且有很多現成的客戶端可以用來消費該聚合。

如果你已經有了一個好的,具有彈性的消息代理的話,就用它來處理事件的訂閱和發布吧。

8.2 非同步架構的複雜性

事件驅動的系統看起來耦合度非常低,而且伸縮性很好。但是這種編程風格也會帶來一定的複雜性,這種複雜性並不僅僅包括對消息的發布訂閱操作。

當我們的系統因為程式碼中bug,工作者崩潰時,我們需要有災難故障轉移(catastrophic failover)。

而且,我們最好設置一個作業最大重試次數。但是我們還需要有一種方式來查看甚至重發這些有問題的消息。所以最後實現了一個消息醫院(或者叫死信隊列),所有失敗的消息都會被發送到這裡。我們還可以創建一個介面來顯示這些消息,如果需要的話還可以觸發一個重試。

事件驅動架構和非同步編程會帶來一定的複雜性,所以通常需要謹慎的選用。你需要確保各個流程有很好的監控機制,並考慮使用關聯ID,這種機制可以幫助你對跨進程的請求進行跟蹤。

9.服務即狀態

前面已經提到過,服務應該根據限界上下文進行劃分。我們的客戶端微服務應該擁有與這個上下文中行為相關的所有邏輯。

把關鍵領域的生命周期顯示建模出來非常有用。我們不但可以在唯一的一個地方處理狀態衝突(比如,嘗試更新已經被移除的用戶),而且可以在這些狀態變化的基礎上封裝一些行為。

10.響應式擴展

響應式擴展(Reactive extensions, Rx)提供了一種機制,在此之上,你可以把多個調用結果組裝起來並在此基礎上執行操作

很多Rx實現都在分散式系統中找到了歸宿。因為調用的細節被屏蔽了,所以事情也更容易處理。我們可以簡單的對下游服務調用的結果進行觀察,而不需要關心它是阻塞的還是非阻塞的,唯一需要做的就是等待結果並作出響應。其漂亮之處在於,我們可以把多個不同的調用組合起來,這樣就可以更容易對下游服務的並發調用做處理。

11.微服務世界中的DRY和程式碼重用的危險

DRY, Don’t Repeat Yourself。

使用DRY可以得到重用性比較好的程式碼。把重複程式碼抽取出來,然後就可以在多個地方進行調用。比如說可以創建一個隨處可用的共享庫。但是這個方法在微服務的架構中可能是危險的。

我們想要避免微服務和消費者之間的過度耦合,否則對微服務任何小的改動都會引起消費方的改動。而共享程式碼就有可能導致這種耦合。

跨服務共用程式碼很有可能會引入耦合。但是使用像日誌庫這樣的公共程式碼就沒什麼問題,因為它們對外是不可見的。

推薦的做法:在微服務內部不要違反DRY,但在跨服務的情況下可以適當違反DRY。服務之間引入大量的耦合會比重複程式碼帶來更糟糕的問題。

客戶端庫

很多團隊堅持在最開始的時候為服務開發一個客戶端庫。原因在於,這樣不僅能簡化對服務的使用,還能避免不同消費者之間存在重複的與服務交互的程式碼。

但是如果開發服務端API和客戶端API的是同一批人,那麼服務端的邏輯很有可能泄露到客戶端中。潛入客戶端庫的邏輯越多,內聚性就越差,然後你必須在修復一個服務端問題的同時,也需對多個客戶端進行修改。

如果你想要使用客戶端庫,一定要保證其中只包含處理底層傳輸協議的程式碼,比如服務發現和故障處理等。千萬不要把與目標服務相關的邏輯放到客戶端庫中。想清楚你是否要堅持使用客戶端庫,或者你是否允許別人使用不同的技術棧來對底層API進行調用。最後,確保由客戶端來負責何時進行客戶端庫的升級,這樣才能保證每個服務可以獨立於其他服務進行發布。

12.按引用訪問

如何傳遞領域實體的相關資訊是一個值得討論的話題

很重要的一個想法,微服務應該包含核心領域實體(比如客戶)全生命周期的相關操作。在這種設計下,如果想要做任何與客戶相關的改動,就必須向客戶服務發起請求。它遵守了一個原則,即客戶服務應該是關於客戶資訊的唯一可靠來源。

想像這樣一個場景,你從客戶服務獲取了一個客戶資源,那麼就能看到該資源在你發起請求那一刻的樣子。但是有可能在你發送了請求之後,其他人對該資源進行了修改,所以你所持有的其實是該客戶資源曾經的樣子。你持有這個資源的時間越久,其內容失效的可能性就越高。當然,避免不必要的數據請求可以讓系統更高效。

有時候使用本地副本沒什麼問題,但在其他場景下你需要知道該副本是否已經失效。所以,當你持有一個本地副本時,請確保同時持有一個指向原始資源的引用,這樣在你需要的時候就可以對本地副本進行更新。

當然在使用引用時也需要做一些取捨。如果總是從客戶服務去查詢給定客戶的相關資訊,那麼客戶服務的負載就會過大。如果在獲取資源的同時,可以得到資源的有效性時限(即該資源再什麼時間之前是有效的)資訊的話,就可以進行相應的快取,從而減小服務的負載。

另一個問題是,有些服務可能不需要知道整個客戶資源,所以堅持進行查詢這種方式會引入潛在的耦合。

原則上講,應該在不確定數據是否能保持有效的情況下,謹慎的進行處理。

13.版本管理

13.1 儘可能推遲破壞性修改

首先,減小破壞性修改影響的最好辦法就是盡量不要做這樣的修改。比如在很多的集成技術中,你可以通過選擇正確的技術來做到這一點。比如資料庫集成很容易引入破壞性的修改;而REST就好很多,因為內部的修改不太容易引起外部服務介面的變化。

另一個延遲破壞性修改的關鍵是鼓勵客戶端的正確行為,避免過早的將客戶端和服務端緊密的綁定起來

客戶端儘可能靈活的消費服務響應這一點符合Postel法則(也叫做魯棒性原則),該法則認為,系統中的每個模組都應該“寬進嚴出”,即對自己發送的東西要嚴格,對接收的東西則要寬容。

13.2 及早發現破壞性修改

這裡強烈建議使用消費者驅動的契約來及早定位這些問題

如果你支援多種不同的客戶端庫,那麼最好針對最新的服務對所有的客戶端運行測試。一旦發現,你可能會對某個消費者造成破壞,那麼可以選擇要麼盡量避免破壞性修改,要麼接受它,並跟維護這些服務的人員好好聊一聊。

13.3 使用語義化的版本管理

如果一個客戶端能夠僅僅通過查看服務的版本號,就知道它能否與之進行集成,那就太好了

語義化版本管理就是一種能夠支援這種方式的規格說明。

語義化版本管理的每一個版本號都遵循這樣的格式MAJOR.MINOR.PATCH。其中MAJOR的改變意味著其中包含向後不兼容的修改MINOR的改變意味著有新功能的增加但應該是向後兼容的;最後,PATCH的改變代表對已有功能的缺陷修復

13.4 不同的介面共存

我們可以在同一個服務上使新介面和老介面同時存在。所以,在發布一個破壞性修改時,可以部署一個同時包含新老介面的版本。

這可以幫助我們儘快發布新版本的微服務,其中包含了新的介面,同時也給了消費者時間做遷移。一旦消費者不再訪問老的介面,就可以刪除掉該介面及相關的程式碼。

這其實就是一個擴展/收縮的實例它允許我們對破壞性修改進行平滑的過度。首先擴張服務的能力,對新老兩種服務都進行支援。然後等到老的消費者都採用了新的方式,再通過收縮API去掉就的功能。

13.5 同時使用多個版本的服務

另一種經常被提起的版本管理的方法是,同時運行不同版本的服務,然後把老用戶路由到老版本的服務,而新用戶可以看到新版本的服務。

14.用戶介面

最重要的其實是,考慮該介面是否能夠很好的支援服務之間的集成。畢竟用戶介面是連接各個微服務的工具,而只有把各個微服務集成起來才能真正的為客戶創造價值。

14.1 走向數字化

我們不應該對網頁端和移動端區別對待,相反應該對數字化策略做全局考慮,即如何讓客戶更好的使用我們的服務。

通過把服務的功能進行不同的組合,可以為桌面應用程式、移動端設備、可穿戴設備的客戶提供不同的體驗。

14.2 約束

在用戶與系統之間,需要考慮不同的交互形式中存在的一些約束。比如在桌面Web應用中,需要考慮與用戶瀏覽器及螢幕解析度相關的約束。

14.3 API組合

假設我們的服務彼此之間已經通過XML或者JSON通訊了,那麼可以讓用戶介面直接與這個API進行交互。

圖4-7:使用多個API來表示用戶介面

這種方式有一些問題。首先,很難為不同的設備訂製不同的響應。另一個問題是,誰來創建用戶介面?維護服務的人往往不是服務的使用者。

使用API入口(gateway)可以很好的緩解這一問題,在這種模式下多個底層的調用會被聚合成為一個調用,當然它也有一定的局限性。

14.4 UI片段的組合

相比UI主動訪問所有的API,然後再將狀態同步到UI控制項,另一種選擇是讓服務直接暴露出一部分UI,然後只需要簡單的把這些片段組合在一起就可以創建出整體UI

14.5 為前端服務的後端

對與前端交互比較頻繁的介面及需要給不同設備提供不同內容的介面來說,一個常見的解決方案是,使用服務端的聚合介面或API入口該入口可以對多個後端調用進行編排,並為不同的設備提供訂製化的內容

4-9 使用單塊入口來處理與UI之間的交互

這樣做會得到一個聚合所有服務的巨大層。由於所有的東西都被放在了一起,也就失去了不同用戶介面之間的隔離性,從而限制了獨立於彼此進行發布的能力。

還有一種模式,保證一個後端服務只為一個應用或者用戶介面服務。

這種模式有時也叫做BFF(Backends For Frontends,為前端服務的後端)

API認證和授權層可以處在BFF和UI之間。

與任何一種聚合層類似,使用這種方法的風險在於包含不該包含的邏輯。業務邏輯應該處在服務中,而不應該泄露到這一層。這些BFF應該僅僅包含與實現某種特定的用戶體驗相關的邏輯。

14.6 一種混合方式

前面提到的那些選擇各自都有其使用的範圍。一個組織會選擇基於片段組裝的方式來構建網站,但對於移動端應用來說,BFF可能是更好的方式關鍵是要保持底層服務能力的內聚性。

15. 小結

前面了解了很多不同的集成選擇,也談了什麼樣的選擇能夠最大程度的保證微服務之間的低耦合:

  • 無論如何,避免資料庫集成
  • 理解REST和RPC之間的取捨,但總是使用REST作為請求/響應模式的起點
  • 相比編排,優先選擇協同
  • 避免破壞性修改、理解Postel法則、使用容錯性讀取器
  • 將用戶介面視為一個組合層