尋找架構驅動力
人類自開始學會以智慧洗亮觀察世界的雙眼之後,就明白觀察事物不能淺嘗輒止停留在表面現象,而要去看透本質。通過本質規律去建模世界,才能以“一”推演萬物。種種推演的過程,皆是要去尋找某種驅動力量作為分析或建構的起點。
例如,當我們要分析一個運動中的物體會形成如何的運動軌跡時,就需要尋找產生運動的力,包括初始的動力、重力、摩擦力以及其他可能干擾物體運動的力。有的力會推動者物體向前,例如初始動力以及與運動方向保持一致的作用力;有的力會阻礙物體的運動,如摩擦力或者空氣阻力等。通過分析這些力的方向及度量,大致可以描繪出物體可能的運動軌跡。
軟體系統的複雜度遠遠超過物體的運動模型(當然,從確定性角度講,軟體或許比物體的運動更簡單),但其推演的過程卻是相似的,因為一個軟體系統並非完全獨立的存在,而是處在一個更大的生態環境圈中,包括客戶的需求與使用體驗、上游依賴系統、下游依賴系統、硬體與網路環境、團隊技能水平等諸多因素縱橫交錯,顯式或隱式地對軟體架構的走向施加影響。這些影響因素就相當於是影響“軟體”這個物體運動的力量。架構師要做的工作就是要敏銳地從這些紛繁複雜如蛛網一般糾纏的力量中梳理出清晰的脈絡。
所謂“力”,其實是一種隱喻。雖然觀察軟體系統的視角如萬花筒一般繽紛多彩,然而若從“物理力學”的視角剖析架構,似乎更加準確直接。軟體系統正如物體一般,在各種影響力之下不停變化(運動)。不同的影響因素會決定著架構師的設計決策,而這些決策之間又相互影響著,或者相吸,或者相斥,絕對不能孤立看待。於是,架構分析與設計就變成了對軟體系統的影響力識別,這種設計的驅動力即我們所謂的RAID分析法。
RAID分析法
所謂RAID分析法,即識別軟體系統的風險(Risk)、假設(Assumption)、問題(Issue)、依賴(Dependency),準確地說,就是:
- 評估風險
- 明確假設
- 分析問題
- 識別依賴
正如在《架構之美》中John Klein、David Weiss寫道:
軟體架構師的首要關注點不是系統的功能。……你關注的是需要滿足的品質。品質關注點指明瞭功能必須以何種方式交付,才能被系統的利益相關人所接受,系統的結果包含這些人的既定利益。
這裡所謂的“品質”,即我們常說的質量屬性(Quality Attribute)。對於架構師而言,業務需求導致設計複雜度的增加僅僅是一種量的變化;而質量屬性對設計的要求,則可能隨著複雜度的增加而產生質變。以分散式系統為例,隨著對訊息佇列、分散式儲存、服務通訊與整合的引入,在資料一致性、可靠性、安全、運維管理等諸多方面,產生的複雜度與單機系統不可同日而語,設計挑戰與難度幾乎與規模形成指數增長。
系統複雜度或許是沒有限制的,而人力卻有限。我們在開始軟體系統的建構與設計時,難免有考慮不周到之處,若是沒有掌握合理的設計方法而深陷浩瀚如滄海一般的各種需求中,牽扯到各個利益相關者的糾纏中,我們就可能會迷路、困惑,或者作出不適合當下場景的設計決策。
RAID分析在一定程度上可以幫助我們重拾正確的方向,尤其在處理質量屬性方面,頗有奇效。
我的建議是將RAID分析以Workshop的形式開展,召集團隊成員通過頭腦風暴來完成。由於將所有軟體系統可能面臨的問題分為了RAID四類,從而明確了討論的範圍與類別,使得參與者能夠以更加收斂更加清晰的思路參與進來。一個典型的RAID分析結果如下圖所示:
在進行RAID分析之前,我們需要明確這四個概念之間的區別。
風險與問題
風險(Risk)與問題(Issue)常常被人混淆在一起,而二者在概念上卻有其相關性。風險其實就是未來可能出現的問題。我們在軟體設計的過程中,一直都在未來與現實中徘徊。滿足現實,卻又需要預測未來。然而,未來是不可預測的,所有的預測其實都是一種想象;我們誇誇其談預測未來,其實不過是想象未來罷了。於是,現實與未來之間就開始了痛苦的拉鋸戰,我們既不能對未來做過多預測與判斷,卻又不能僅滿足於現狀,如何做到架構設計的恰如其分,在規避過度設計的同時,又能讓我們的架構能夠在未來需求發生變化時以最小的成本應對。我們真正要做到的是前瞻未來,評估風險就是讓我們能夠前瞻未來的瞭望鏡(這世上並沒有預測未來的魔法水晶球)。
分析現在存在的問題,評估未來風險,將是這場拉鋸戰的關鍵制高點。在判定優先順序時,問題往往高於風險,需要在解決現有問題的前提上,考慮未來風險的應對方案。譬如說,系統目前存在的問題是效能堪憂,那麼除了必要的調優手段外,我們可以通過提高系統的可伸縮性來改進效能。然而,要保證系統的可伸縮性,就需要保持服務的無狀態,並在設計系統的各個分層時都需要支援水平擴充套件,則可能引入資料不一致以及系統欠穩定的風險。
假設
我們往往會忽略為系統給定假設(Assumption),而事實上,這種假設往往代表了關鍵的架構約束。
架構約束是一種非常重要的驅動力。Roy Fielding在其論文Architectural Styles and the Design of Network-based Software Architectures(《架構風格與基於網路的軟體架構設計》)中如此勾勒出約束的重要性:
屬性是由架構中的一組約束所導致的。約束往往是由在架構元素的某個方面應用軟體工程原則來驅動的。例如,統一管道和過濾器(uniform pipe-and-filter)風格通過在其元件介面之上應用通用性原則——強迫元件實現單一的介面型別,從應用中獲得了元件的可重用性和可配置性的品質。因此,架構約束是由通用性原則所驅動的“統一元件介面”,目的是獲得兩個想要得到的品質,當在架構中實現了這種風格時,這兩個品質將成為可重用和可配置元件的架構屬性。
我們在明確假設時,需要將這些約束甄別出來,以之作為架構設計的驅動力。例如,對於一個移動APP,我們明確假設:使用者在斷開網路連線時,能夠正常地查閱個人資訊與產品資訊。這個假設就對軟體架構提出約束,即在APP的客戶端需要快取資料資訊,並在使用者連線WIFI時,能夠自動同步客戶端資料到服務端。
某些假設則是系統功能性的重要約定,好似契約一般,需要在整個設計與實現階段需要遵從。例如假設電商系統需要呼叫的推薦系統為第三方系統,那麼在設計時就需要明確推薦系統公開的介面,系統之間如何整合,當推薦系統的服務發生變更時,客戶方該如何應對。這些都會直接影響我們的設計決策。
依賴
在軟體設計中,我們無時不刻不在與依賴作鬥爭。依賴本身是無善無惡的,關鍵在於我們該如何分解(內聚),如何協作(耦合),這就是我們需要遵循的高內聚低耦合設計原則。在架構層面,情況更顯複雜,除了系統內部的依賴之外,還需要考慮系統外部上游與下游的依賴。尤其是跨越物理邊界(可以視為一個程式)之間的通訊,會直接影響到可靠性、效能、可伸縮性等諸多質量屬性。
DDD的Context Map定義了九種Bounded Context之間的對映關係,其中包括防腐層、開放主機服務與釋出語言表達的就是Bounded Context之間的整合關係。如果我們能夠在架構之處識別出系統存在的依賴,再結合Cockburn提出的六邊形架構對其進行更加直觀的視覺化,找出依賴途經的埠(Port)與介面卡(Adapter),然後確定依賴之間的通訊(整合)方式,幾乎就可以得出整個軟體系統應用邏輯架構與物理架構的雛形了。
下圖將六邊形架構與識別的依賴結合起來:
實施RAID分析的案例
在多個系統的架構設計或Inception階段,我通過運用RAID分析法驅動系統的軟體架構設計,效果頗佳,雖然在細節處還欠缺精細,但從大處著手,卻可以幫助我們高屋建瓴地分析與架構整個系統。以下是針對某版本升級系統的RAID分析案例。
評估風險
通常而言,對風險的識別可以引導我們對系統質量屬性的思考,利益相關者可
以充分表達對這些屬性的擔心,從而驅動我們去尋找解決方案。
穩定性
在這次RAID分析中,有利益相關者明確提出了對穩定性的擔憂。系統的多個模組駐留在不同的節點中,部分模組還是以嵌入方式駐留在主控板上。由於業務需要,模組之間的通訊相對頻繁,主要的通訊協議為Telnet與SSH。從舊有的系統表現來看,跨界點之間的通訊在穩定性方面表現欠佳。基於這一問題,我們在後續的架構設計中對此進行了深入分析,除了保證通訊實現自身的健壯性與異常處理之外,我們還決定在主控板一端設計粗粒度的介面,一次性地傳遞版本升級需要的資訊,減少不必要的通訊。
可擴充套件性
風險對擴充套件性的識別,幫助我們確立了一個架構原則,就是版本規格包的結構不應該影響到主控板的系統。這是因為主控板系統的版本升級受到的制約最多,我們不希望當產品發生變化時,影響整個版本管理系統。
效能
當需要升級的系統數量較多時,系統的版本升級過程會變得緩慢。而業務需求有要求了系統不能長期處於shutdown狀態,否則會增加運營成本。因此,升級過程通常會選在凌晨,並且要求在較短時間內完成整個升級工作,故而效能可謂重中之重。
我們考慮採用併發方式為每個待升級系統進行升級。升級過程是一個獨立的過程,卻又牽涉到較為複雜的業務流程以及跨節點通訊。由於部署限制,後臺只能部署在一個JVM之上,通過啟動多個併發執行緒來處理升級業務。執行升級時,需要載入配置檔案到記憶體中,若同時啟動的執行緒數過多,則可能導致OutOfMemory
異常。這個風險的識別及時地為我們敲響了警鐘。我們為此安排了技術Spike,以期找到合適的配置項,在效能與可靠性之間進行最優權衡。
明確假設
假設(Assumption)可以是關鍵的架構約束,也可以是系統功能性的約定。架構約束既可能是設計的阻力,也可以成為動力。經過討論,我們基本上確定了兩條最為重要的假設:
- 系統必須支援雙向相容。這個假設的提出,則要求我們在開發過程中,只要介面已經發布,就不能再修改介面。除修復缺陷外,我們不能刪除舊有功能,只能增加新功能。即使舊有功能已被新功能取代,為保持相容性,我們也不能刪除,但可以將其置為
@deprecated
標註。 - 版本升級過程中,若前後操作具有依賴關係,則必須保證事務的一致性,要麼全部成功,要麼全部失敗。事實上,這一條假設也是對質量屬性“可靠性”的一個回應。
分析問題
整個RAID的識別都針對技術層面,而非管理層面。因此我們識別的問題也限
制在技術範圍。
在我們識別出來的問題中,最致命的一個問題是關於模組NVUM的載入。NVUM是一個JAR包。它並非一個獨立執行的系統,而是由管理系統動態載入。之所以選擇動態載入,而非靜態依賴,原因包括:
- NVUM由我們專案組維護,管理系統則屬於另外一個專案,兩邊的版本計劃完全不一致。網管系統為一個Client-Server系統,相對成熟,目前已被獨立地部署到全球多個外場。若採用靜態依賴,就需要我們將其納入到網管系統中。但NVUM的版本更新更加頻繁,外場不可能因為NVUM一個模組的調整,而付出頻繁更新管理系統的代價。
- 管理系統負責監控外場各裝置的運轉狀況。雖然系統的重啟(耗時數十分鐘)並不會影響裝置的功能,但卻可能在重啟過程中,因為未能及時掌控裝置狀態,而導致無法及時發現問題。必須避免這種事故的發生。換言之,管理系統的重啟代價太高,不能經常重啟。
JAR包的動態載入可以通過URLClassLoader
來實現,又或者選擇OSGI。前者需要充分驗證其穩定性,後者則過於重型,成本太高。另外,動態載入方式對於模組設計而言存在設計約束,即我們需要將NVUM分為interface和impl兩個模組,且必須保證interface的穩定性。
另一個方案是採用指令碼,例如選擇能夠執行在JVM上的Groovy指令碼語言。我們只需要在Java中呼叫Groovy提供的GroovyShell
,就能直接讀取groovy指令碼檔案;然後呼叫run()
方法即可執行指令碼。
識別依賴
除了NVUM與管理系統,NVUM與主控板,主控板與其他裝置之間的依賴外,牽涉到的依賴還有很多。有的屬於輸入依賴,有的則屬於輸出依賴。此外,還有版本製作工具等系統也會受到NVUM的影響。同時,NVUM還需要訪問內建的檔案系統,通過FTP讀取諸多外部檔案。通訊則可能採用Telnet、SNMP、SSH等多種協議。
這些依賴的識別便於確定本系統對其他系統可能造成的影響,事先識別有利於我們及時做好溝通,同時還需要就一些架構約定以及介面定義達成一致意見。依賴的識別也有利於我們設計系統的物理架構,考慮系統的部署方式。