程序員過關斬將--論系統(tǒng)設計的高可擴展性
此文僅僅代表個人意見,并非行業(yè)標準
“MQ是萬能的高擴展方式?
“面向接口是萬能的高擴展方式?
說到系統(tǒng)設計的三高,每一高都是一個很龐大的話題,甚至可以用一本書甚至N本書來詳細闡述。其中高可擴展性是系統(tǒng)架構的眾多目標之一。歸根結底,系統(tǒng)的架構要為最終的業(yè)務服務,脫離業(yè)務來談架構其實比耍流氓更無恥。
在我們心目中最理想的軟件架構要像搭積木一樣簡單,并且快捷,而且高效。但是現實往往比996更殘酷,多數的系統(tǒng)在初期為了配合業(yè)務快速上線,擴展性這個指標并不理想。別的不談,一個系統(tǒng)要完美的做到“對修改封閉,對擴展開放”其實一點也不簡單,不知道你有沒有遇到過修改一個bug蹦出另外一個bug的痛苦經歷?
為了做到系統(tǒng)的高擴展性,其實有很多借鑒的案例,尤其是設計模式。但是今天我還是要說一說我自己的看法。無論什么樣的系統(tǒng),抽象起來其實都是模塊和模塊之間的交互,這里模塊的含義是廣義的,即可以代表函數,也可以代表進程,甚至可以代表目前流行的微服務,如下圖所示
image
圖是不是很簡單?但是要想把A和B之間的交互做到高擴展其實并不容易,這要求系統(tǒng)的設計者必須要想辦法在滿足A和B正常交互的情況下盡量解耦A和B,只有正確的解耦,才能從容的應對A和B獨立擴展的業(yè)務需求
同一進程內
在同一進程內的情況是一種最常見的存在方式,對應到我們平時的代碼,表現為函數的調用,而這里的函數調用可以是同一模塊內的函數調用,比如最典型的三層架構中,業(yè)務層調用持久化層來進行數據的操作,如下代碼:
- //user 業(yè)務層
- public class UserBLL
- {
- UserDAL dal = new UserDAL();
- public int AddUser(User user)
- {
- //其他業(yè)務
- return dal.AddUser(user);
- }
- }
- //user持久化層
- public class UserDAL
- {
- public int AddUser(User user)
- {
- //進行數據庫操作
- return 0;
- }
- }
我真的希望實際項目中的代碼能像以上代碼這么簡單,畢竟代碼就和項目一樣,簡單即是美。這段代碼排除業(yè)務之外,從架構來講也有很多問題,用開頭的A和B的方式來表示,A代表的是UserBLL,B代表的是UserDAL,這里最容易看出的就是強耦合,即:A嚴重依賴于B,如果B有什么風吹草動,勢必會影響A的執(zhí)行。
怎么辦呢?所以有了B的抽象層,對應到代碼上是IDAL接口層,當然這個抽象層應該是穩(wěn)定的,如果三天兩頭修改抽象層,那說明抽象的有問題。A在執(zhí)行上改為依賴IDAL,這是系統(tǒng)內設計最常見的面向接口設計模式,其實更準確的說,應該是面向抽象設計模式。由于引入了穩(wěn)定的抽象層,不再穩(wěn)定的實現層就可以根據實際的業(yè)務去修改,這里體現的是系統(tǒng)設計中依賴倒置的原則,當然為了實現依賴倒置,你可能需要使用IOC等技術來實現項目落地。
- //user 業(yè)務層
- public class UserBLL
- {
- IUserDAL dal = "依賴注入";
- public int AddUser(User user)
- {
- //其他業(yè)務
- return dal.AddUser(user);
- }
- }
- //user的持久化層抽象
- public interface IUserDAL
- {
- int AddUser(User user);
- }
- //user持久化層
- public class UserDAL: IUserDAL
- {
- public int AddUser(User user)
- {
- //進行數據庫操作
- return 0;
- }
- }
不同進程間
不同的進程之間互相協作是目前分布式模式下主要的交互方式,例如之前的SOA,現在的微服務,都是在利用分散在不同位置的模塊來組裝系統(tǒng),這些模塊之間的通信是一個分布式系統(tǒng)必備的條件。
和進程內函數調用類似,分布式系統(tǒng)也可以抽象為A和B的關系模型,我們要解決的也是A和B能夠獨立變化的問題?,F在假設A服務依賴于B服務,B服務由于壓力大需要擴容,會有哪些影響呢?
- B自己內部的狀態(tài)變化,如果B服務是有狀態(tài)的,擴展起來可能會設計到數據的遷移等操作,如果B是無狀態(tài)的,理論來說可以很方便的橫向擴展
- B的擴容對A或者其他依賴于B的系統(tǒng)有什么影響,依賴方能否做到自動適配,而不必修改任何配置
和進程內函數調用不同,進程間的通信需要通訊協議的支持,最常見的RPC調用都是基于TCP協議,Restfull基于http協議,使用這些協議底層都需要指定明確的IP和端口。所以需要某種解決方案在被依賴方擴展的時候,依賴方能夠得到感知。聰明的你可能想到了“注冊中心”,不錯,這也是注冊中心最主要的職責。
解決方案2
用注冊中心的方式,理論上屬于通知依賴方的方案,在依賴方感知被依賴方有擴展變動的時候,需要作出對應的變化。與之對應的其實我們也可以把變動封裝在被依賴方,這個時候就引入了以下代理模式,最常見的就是網關模式。
分布式系統(tǒng)使用網關到底是好還是壞?
其實代理模式非常常見,比如Nginx做反向代理,數據庫的中間件。這些設施都是對依賴方透明的,依賴方不會因為被依賴方實施了擴展而受影響。
解決方案3
目前很多業(yè)務下有一種很常見的場景,依賴方和被依賴方通信并不需要知道執(zhí)行結果,最典型的場景像:新用戶注冊給用戶發(fā)歡迎郵件或者短信歡迎語。如果業(yè)務代碼中冗余了發(fā)郵件或者短信的代碼的話,一旦要添加新的歡迎方式就必須要修改業(yè)務代碼,無論你是否有抽象層,為了不影響主要的業(yè)務又最大化解耦系統(tǒng),一般都會把這種非主要業(yè)務通過消息的方式分離出來。最常見的解決方案就是MQ。這也是典型發(fā)布訂閱模式,但是這種模式如上所說,調用方并不能實時的得到業(yè)務處理結果。
利用MQ來進行系統(tǒng)的解耦,來實現系統(tǒng)的高可擴展是一種非常常見的方式,優(yōu)勢有很多,我不再闡述,但是需要注意消息的可靠性,因為消息經過了幾個環(huán)節(jié)之后,難保某個環(huán)節(jié)出現問題而丟失消息。
寫在最后
A和B之間的通信如果只是單向的話,可以理解為上下級關系,但是在微服務情況下,A和B很多時候是平行的互相調用的兄弟關系。有的架構師不贊成平行關系的微服務互相調用,這是有一定道理的,因為這很容易造成復雜的網絡調用模式,如果是符合MQ消息的形式通信,我也推薦首推利用MQ來解耦服務間的依賴。
高可擴展性系統(tǒng)的最終目標是在應對業(yè)務變化的時候,用最小的代價去實現。而如何實現系統(tǒng)的擴展性,并非只有以上所說的“面向接口編程”,利用MQ這些方式,你還知道哪些可以幫助系統(tǒng)擴展的解決方案嗎?歡迎你給我留言!!
“只要一提到解耦,有的“高手”一上來就說利用MQ,真的對嗎?如果調用方需要實時的業(yè)務處理結果呢?
本文轉載自微信公眾號「架構師修行之路」,可以通過以下二維碼關注。轉載本文請聯系架構師修行之路公眾號。