微服務架構的陷阱:從單體到分佈式單體
微服務架構的陷阱:從單體到分佈式單體
你好,我是看山。
前面咱們聊了架構的演進過程,提到單體架構、SOA 架構、微服務架構、無服務架構。整個過程如下圖:
架構演進過程:單體架構、早期服務化、SOA 架構、微服務架構
目前無服務架構還未成熟,只能滿足一些簡單場景。所以大家在設計軟件架構時,首選還是微服務架構。然後我們又聊了聊如何把單體架構改造爲微服務架構,推薦採用絞殺模式,一步一步的實現系統微服務化。
在這個過程中,我們會碰到微服務架構的一個大坑:分佈式錯覺,即將分佈式當成了微服務的全部(充要條件)。
原因
出現分佈式單體的主要原因在於,只是用進程間的遠程調用替換進程內的方法調用。
模塊 A 與模塊 B 之間的通信方式
從上圖可以看出,單體架構在模塊 A 與模塊 B 之間的請求是通過進程內通信(通常是方法調用)實現的;在微服務架構中,兩者之間是通過 REST 或 RPC 調用。拋開進程和消息通知機制的差異,兩種架構中模塊 A 與模塊 B 之間的通信形式完全一致:
單體 / 微服務架構中模塊間通信方式
在這種情況下,模塊 A 與模塊 B 耦合在一起,任何一方變更請求契約(方法簽名或接口參數),另外一個都必須同步修改。更糟糕的是,由於微服務架構服務之間是通過網絡通信,由於其不可靠性和不穩定性,大大增加了出錯的概率,使模塊之間的調用關係更加脆弱。
模塊 A 與模塊 B 之間的網絡請求是同步調用,請求過程中會佔用一個網絡連接和至少一個線程,如果模塊 A 與模塊 B 所在的服務的承壓能力不同,很有可能模塊 B 所在服務被打滿,後續模塊 A 的請求會阻塞等待,直到請求超時。
那又是什麼原因讓大家沒有意識到這種方式不妥呢?原因有兩個:
-
想要在微服務架構中實現單體架構中模塊間的關係;
-
想要在分佈式應用中實現數據的強一致性。
針對於微服務架構中的數據一致性問題,可以參考 關於微服務系統中數據一致性的總結。
下面我們重點說說如果解決第一個問題。
方法
對於模塊之間的關係,主要在於通信模式,對於查詢請求,由於數據依賴,模塊之間的耦合是天然的,我們這裏要解耦的是數據變更(增、刪、改)時的模塊調用。
類比一下現實,我們如果想要通知某些人一個消息,會怎麼處理?一般來說,有兩種方式:
-
點對點主動通知,直接找到這個人,給這個人打電話,不通繼續打,保證對方能夠收到消息;
-
點對面廣播通知,比如,羣裏發給公告、公司的告示欄等,這種方式需要消息接受者主動查看公告信息。
這兩種方式對應了我們系統設計中消息傳遞的兩個模式:指令(Command)、事件(Event)。
指令(Command)和事件(Event)
command 與 event,指令與事件
指令(Command)是表示從發起者(source)向執行者(destination)傳遞(send)一個必須執行某個動作(action)的請求(request)。這個模式有如下特點:
-
明確的知道發起者和執行者,發起者依賴執行者;
-
請求發送方式一般是點對點同步請求,一般是 RPC 請求;
-
動作已發生或即將發生,有可能由於執行者拒絕執行而取消;
-
執行者有可能拒絕執行;
-
執行者會很明確的告知發起者指令執行情況:拒絕、成功、失敗等。
-
爲了保證指令的有效觸達,發起者在網絡超時時會重複調用執行者,所以執行者需要實現請求冪等;
-
執行者可能會成爲下一個指令的發起者。
事件(event)是表示由生產者(producer)發佈(public)一個已經發生的事情,表示行爲(action)已經發生,某些狀態(status)發生了改變,消費者(consumer)訂閱這些事件,然後做出響應。這個模式有如下特點:
-
事件有明確的生產者,但是消費者不明確,甚至可能不存在;
-
一般藉助消息中間件實現事件發送、存儲、傳遞等;
-
行爲已經發生,不可改變,不可逆,事件是對已經發生事情的客觀描述;
-
消費者根據消息選擇處理方式:執行、拋棄等;
-
消費者處理完成後,不需要回復生產者;
-
一般消息中間件採用 “至少一次” 通知機制,所以消費者需要實現消息處理的冪等;
-
消費者可能會成爲下一個事件的生產者。
指令與事件之間的區別
由於請求模式的不同,在依賴關係上就會發生改變:
在指令模式中,模塊 A 調用模塊 B,屬於直接調用,模塊 A 需要依賴模塊 B;在事件模式中,模塊 A 把事件發送給消息中間件,其他需要訂閱事件的服務,直接從消息中間件獲取,這種會產生依賴倒置,模塊 B 依賴模塊 A。這是解耦模塊 A 與模塊 B 很好的方式。
重新定義微服務
我們再回過頭來看看我們的問題:
單體 / 微服務架構中模塊間通信方式
此時我們會比較清晰,由於全系統中使用了指令模式,上次調用者依賴下層,由於是同步請求,依賴會發生傳遞,這種依賴傳遞,將整個系統耦合在一起,一處修改,處處變動,也就是我們在抨擊單體架構時常說的牽一髮而動全身。
此時,我們就可以藉助事件模式,將依賴鏈條打斷。但是需要注意,不要矯枉過正的全部改爲事件模式,那將會是另一個火坑。一般我們會將系統改造成下面的樣子:
重新定義微服務
根據業務具體情況,我們可以歸納一下改造結果:
-
服務 A 接到的請求可能是事件或指令;
-
服務 A 會向服務 B 發送指令,也會向消息中間件發送事件;
-
服務 B 接到指令後開始執行,執行完畢後,可能會向消息中間件發送事件;
-
服務 C 定於事件,從消息中間件接到消息後處理,它可能發送事件或指令。
需要注意的是,每個服務內部還有有一些操作。抽象一下,整個系統中的指令、事件、操作如下圖:
指令和事件的定義
-
輸入:以一個指令或事件作爲輸入,開始整個業務執行;
-
服務內部操作:服務內部會有執行邏輯,比如操作數據庫、訪問緩存服務等。可選,0-N 個;
-
指令調用:同步調用依賴服務,發送指令,獲得結果。可選,0-N 個;
-
發佈事件:以消息形式發佈事件,一般發佈到消息中間件,其他發佈訂閱消息中間件,執行事件需要的行爲。可選,事件一般是 0-1 個。
架構設計的過程不是非此即彼,全部指令會造成耦合,全部事件會致使開發難度提升以及邊界不清。我們需要理性的看待兩種模式,做到不偏不倚。
文末總結
本文從分佈式單體陷阱展開,講述了分佈式錯覺帶來的問題,然後通過事件、指令兩種模式相結合的方式解決問題。微服務是目前比較完善的架構風格,從單體到微服務架構,是要實現架構的升級,所以調用模式不會一成不變。這個陷阱,也是我們在做新系統時需要避免的。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/b7my-EHc0fGOE3veeQyxBg