文章目錄
- 一、架構的定義
- 1. 系統與子系統
- 2. 模塊與組件
- 3. 框架與架構
- 4. 重新定義架構
- 二、架構設計的目的
- 三、復雜度來源:高性能
- 1. 單機復雜度
- 2. 集群復雜度
- 2.1 任務分配
- 2.2 任務分解(微服務)
- 四、復雜度來源:高可用
- 1. 計算高可用(業務)
- 2. 存儲高可用
- 3. 高可用狀態決策
- 3.1 獨裁式
- 3.2 協商式
- 3.3 民主式
- 五、復雜度來源:可擴展性
- 1. 預測變化
- 2. 應對變化
- 2.1 將“變化”封裝在一個“變化層”,將不變的部分封裝在一個獨立的“穩定層”。
- 2.2 提煉出一個“抽象層”和一個“實現層”
- 六、復雜度來源:低成本、安全、規模
- 1. 低成本
- 2. 安全
- 2.1 功能安全
- 2.2 架構安全
- 3. 規模
- 3.1 功能越來越多,導致系統復雜度指數級上升
- 3.2 數據越來越多,系統復雜度發生質變
- 七、架構設計的三原則及案例
- 1. 合適原則
- 2. 簡單原則
- 2.1 結構的復雜性
- 2.2 邏輯的復雜性
- 3. 演化原則
- 4. 案例
- 4.1 案例一:淘寶
- 4.2 案例二:手機QQ
- 八、架構設計流程
- 1. 識別復雜度
- 2. 設計備選方案
- 2.1 常見錯誤一:設計最優秀的方案
- 2.2 常見錯誤二:只做一個方案
- 2.3 常見錯誤三:備選方案過于詳細
- 3. 評估和選擇備選方案
- 4. 詳細方案設計
一、架構的定義
1. 系統與子系統
- 關聯:系統是由一群有關聯的個體組成的,沒有關聯的個體堆在一起不能成為一個系統。
- 規則:系統內的個體需要按照指定的規則運作,而不是單個個體各自為政。規則規定了系統內個體分工和協作的方式。
- 能力:系統能力與個體能力有本質的差別,系統能力不是個體能力之和,而是產生了新的能力。
子系統的定義和系統定義是一樣的,只是觀察的角度有差異,一個系統可能是另外一個更大系統的子系統。
2. 模塊與組件
模塊和組件都是系統的組成部分,只是從不同的角度拆分系統而已。
從邏輯的角度來拆分系統后,得到的單元就是“模塊”;從物理的角度來拆分系統后,得到的單元就是“組件”。劃分模塊的主要目的是職責分離;劃分組件的主要目的是單元復用。其實,“組件”的英文component也可翻譯成中文的“零件”一詞,“零件”更容易理解一些,“零件”是一個物理的概念,并且具備“獨立且可替換”的特點。
3. 框架與架構
軟件框架(Software framework)通常指的是為了實現某個業界標準或完成特定基本任務的軟件組件規范,也指為了實現某個軟件組件規范時,提供規范所要求之基礎功能的軟件產品。
提煉一下其中關鍵部分:
- 框架是組件規范:例如,MVC就是一種最常見的開發規范,類似的還有MVP、MVVM、J2EE等框架。
- 框架提供基礎功能的產品:例如,Spring MVC是MVC的開發框架,除了滿足MVC的規范,Spring提供了很多基礎功能來幫助我們實現功能,包括注解(@Controller等)、Spring Security、Spring JPA等很多基礎功能。
軟件架構指軟件系統的“基礎結構”,創造這些基礎結構的準則,以及對這些結構的描述。
框架關注的是“規范”,架構關注的是“結構”。框架的英文是Framework,架構的英文是Architecture。
4. 重新定義架構
軟件架構指軟件系統的頂層結構
首先,“系統是一群關聯個體組成”,這些“個體”可以是“子系統”“模塊”“組件”等;架構需要明確系統包含哪些“個體”。
其次,系統中的個體需要“根據某種規則”運作,架構需要明確個體運作和協作的規則。
第三,維基百科定義的架構用到了“基礎結構”這個說法,我改為“頂層結構”,可以更好地區分系統和子系統,避免將系統架構和子系統架構混淆在一起導致架構層次混亂。
二、架構設計的目的
架構設計的主要目的是為了解決軟件系統復雜度帶來的問題。
首先,遵循這條準則能夠讓“新手”架構師心中有數,而不是一頭霧水。
其次,遵循這條準則能夠讓“老鳥”架構師有的放矢,而不是貪大求全。
復雜度分析的幾個角度:
- 高性能
- 高可用
- 可擴展性
- 成本
- 安全性
- 規模
三、復雜度來源:高性能
軟件系統中高性能帶來的復雜度主要體現在兩方面
- 一方面是單臺計算機內部為了高性能帶來的復雜度;
- 另一方面是多臺計算機集群為了高性能帶來的復雜度。
1. 單機復雜度
操作系統是軟件系統的運行環境,操作系統的復雜度直接決定了軟件系統的復雜度。操作系統和性能最相關的就是進程和線程。
操作系統發展到現在,如果我們要完成一個高性能的軟件系統,需要考慮如多進程、多線程、進程間通信、多線程并發等技術點,而且這些技術并不是最新的就是最好的,也不是非此即彼的選擇。在做架構設計的時候,需要花費很大的精力來結合業務進行分析、判斷、選擇、組合,這個過程同樣很復雜。舉一個最簡單的例子:Nginx可以用多進程也可以用多線程,JBoss采用的是多線程;Redis采用的是單進程,Memcache采用的是多線程,這些系統都實現了高性能,但內部實現差異卻很大。
2. 集群復雜度
2.1 任務分配
“任務”涵蓋的范圍很廣,可以指完整的業務處理,也可以單指某個具體的任務。例如,“存儲”“運算”“緩存”等都可以作為一項任務,因此存儲系統、運算系統、緩存系統都可以按照任務分配的方式來搭建架構。此外,“任務分配器”也并不一定只能是物理上存在的機器或者一個獨立運行的程序,也可以是嵌入在其他程序中的算法,例如Memcache的集群架構。
2.2 任務分解(微服務)
通過這種任務分解的方式,能夠把原來大一統但復雜的業務系統,拆分成小而簡單但需要多個系統配合的業務系統。從業務的角度來看,任務分解既不會減少功能,也不會減少代碼量(事實上代碼量可能還會增加,因為從代碼內部調用改為通過服務器之間的接口調用),那為何通過任務分解就能夠提升性能呢?
主要有幾方面的因素:
- 簡單的系統更加容易做到高性能
- 可以針對單個任務進行擴展
雖然系統拆分可能在某種程度上能提升業務處理性能,但提升性能也是有限的,因為最終決定業務處理性能的還是業務邏輯本身,業務邏輯本身沒有發生大的變化下,理論上的性能是有一個上限的,系統拆分能夠讓性能逼近這個極限,但無法突破這個極限。因此,任務分解帶來的性能收益是有一個度的,并不是任務分解越細越好,而對于架構設計來說,如何把握這個粒度就非常關鍵了。
四、復雜度來源:高可用
系統無中斷地執行其功能的能力,代表系統的可用性程度,是進行系統設計時的準則之一。
這個定義的關鍵在于“無中斷”,但恰好難點也在“無中斷”上面。
系統的高可用方案五花八門,但萬變不離其宗,本質上都是通過“冗余”來實現高可用。通俗點來講,就是一臺機器不夠就兩臺,兩臺不夠就四臺;一個機房可能斷電,那就部署兩個機房;一條通道可能故障,那就用兩條,兩條不夠那就用三條(移動、電信、聯通一起上)。高可用的“冗余”解決方案,單純從形式上來看,和之前講的高性能是一樣的,都是通過增加更多機器來達到目的,但其實本質上是有根本區別的:高性能增加機器目的在于“擴展”處理性能;高可用增加機器目的在于“冗余”處理單元。
1. 計算高可用(業務)
這里的“計算”指的是業務的邏輯處理。計算有一個特點就是無論在哪臺機器上進行計算,同樣的算法和輸入數據,產出的結果都是一樣的。
高可用集群具體應該采用哪種分配方式,需要結合實際業務需求來分析和判斷,并不存在某種算法就一定優于另外的算法。例如,ZooKeeper采用的就是1主多備,而Memcached采用的就是全主0備。
2. 存儲高可用
存儲與計算相比,有一個本質上的區別:將數據從一臺機器搬到到另一臺機器,需要經過線路進行傳輸。線路傳輸的速度是毫秒級別,同一機房內部能夠做到幾毫秒;分布在不同地方的機房,傳輸耗時需要幾十甚至上百毫秒。對于高可用系統來說,這意味著整個系統在某個時間點上,數據肯定是不一致的。
除了物理上的傳輸速度限制,傳輸線路本身也存在可用性問題,傳輸線路可能中斷、可能擁塞、可能異常(錯包、丟包),并且傳輸線路的故障時間一般都特別長,短的十幾分鐘,長的幾個小時都是可能的。
綜合分析,無論是正常情況下的傳輸延遲,還是異常情況下的傳輸中斷,都會導致系統的數據在某個時間點或者時間段是不一致的,而數據的不一致又會導致業務問題;但如果完全不做冗余,系統的整體高可用又無法保證,所以存儲高可用的難點不在于如何備份數據,而在于如何減少或者規避數據不一致對業務造成的影響。
分布式領域里面著名的CAP定理,從理論上論證了存儲高可用的復雜度。也就是說,存儲高可用不可能同時滿足“一致性、可用性、分區容錯性”,最多滿足其中兩個,這就要求我們在做架構設計時結合業務進行取舍。
3. 高可用狀態決策
無論是計算高可用還是存儲高可用,其基礎都是“狀態決策”,即系統需要能夠判斷當前的狀態是正常還是異常,如果出現了異常就要采取行動來保證高可用。如果狀態決策本身都是有錯誤或者有偏差的,那么后續的任何行動和處理無論多么完美也都沒有意義和價值。但在具體實踐的過程中,恰好存在一個本質的矛盾:通過冗余來實現的高可用系統,狀態決策本質上就不可能做到完全正確。下面基于幾種常見的決策方式進行詳細分析。
3.1 獨裁式
獨裁式決策指的是存在一個獨立的決策主體稱為“決策者”,負責收集信息然后進行決策;所有冗余的個體稱為“上報者”,都將狀態信息發送給決策者。
當決策者本身故障時,整個系統就無法實現準確的狀態決策。如果決策者本身又做一套狀態決策,那就陷入一個遞歸的死循環了。
3.2 協商式
協商式決策指的是兩個獨立的個體通過交流信息,然后根據規則進行決策,最常用的協商式決策就是主備決策。
這個架構的基本協商規則可以設計成:
- 2臺服務器啟動時都是備機。
- 2臺服務器建立連接。
- 2臺服務器交換狀態信息。
- 某1臺服務器做出決策,成為主機;另一臺服務器繼續保持備機身份。
如果兩者的信息交換出現問題(比如主備連接中斷),此時狀態決策應該怎么做。
- 如果備機在連接中斷的情況下認為主機故障,那么備機需要升級為主機,但實際上此時主機并沒有故障,那么系統就出現了兩個主機,這與設計初衷(1主1備)是不符合的;
- 如果備機在連接中斷的情況下不認為主機故障,則此時如果主機真的發生故障,那么系統就沒有主機了,這同樣與設計初衷(1主1備)是不符合的;
- 如果為了規避連接中斷對狀態決策帶來的影響,可以增加更多的連接。例如,雙連接、三連接。這樣雖然能夠降低連接中斷對狀態帶來的影響(注意:只能降低,不能徹底解決),但同時又引入了這幾條連接之間信息取舍的問題,即如果不同連接傳遞的信息不同,應該以哪個連接為準?實際上這也是一個無解的答案,無論以哪個連接為準,在特定場景下都可能存在問題。
3.3 民主式
民主式決策指的是多個獨立的個體通過投票的方式來進行狀態決策。例如,ZooKeeper集群在選舉leader時就是采用這種方式。
民主式決策和協商式決策比較類似,其基礎都是獨立的個體之間交換信息,每個個體做出自己的決策,然后按照“多數取勝”的規則來確定最終的狀態。不同點在于民主式決策比協商式決策要復雜得多。
除了算法復雜,民主式決策還有一個固有的缺陷:腦裂。腦裂的根本原因是,原來統一的集群因為連接中斷,造成了兩個獨立分隔的子集群,每個子集群單獨進行選舉,于是選出了2個主機,相當于人體有兩個大腦了。
為了解決腦裂問題,民主式決策的系統一般都采用“投票節點數必須超過系統總節點數一半”規則來處理。如圖中那種情況,節點4和節點5形成的子集群總節點數只有2個,沒有達到總節點數5個的一半,因此這個子集群不會進行選舉。這種方式雖然解決了腦裂問題,但同時降低了系統整體的可用性,即如果系統不是因為腦裂問題導致投票節點數過少,而真的是因為節點故障(例如,節點1、節點2、節點3真的發生了故障),此時系統也不會選出主節點,整個系統就相當于宕機了,盡管此時還有節點4和節點5是正常的。
五、復雜度來源:可擴展性
設計具備良好可擴展性的系統,有兩個基本條件:正確預測變化、完美封裝變化。
1. 預測變化
預測變化的復雜性在于:
- 不能每個設計點都考慮可擴展性。
- 不能完全不考慮可擴展性。
- 所有的預測都存在出錯的可能性。
對于架構師來說,如何把握預測的程度和提升預測結果的準確性,是一件很復雜的事情,而且沒有通用的標準可以簡單套上去,更多是靠自己的經驗、直覺,所以架構設計評審的時候經常會出現兩個設計師對某個判斷爭得面紅耳赤的情況,原因就在于沒有明確標準,不同的人理解和判斷有偏差,而最終又只能選擇一個判斷。
2. 應對變化
2.1 將“變化”封裝在一個“變化層”,將不變的部分封裝在一個獨立的“穩定層”。
無論是變化層依賴穩定層,還是穩定層依賴變化層都是可以的,需要根據具體業務情況來設計。例如,如果系統需要支持XML、JSON、ProtocolBuffer三種接入方式,那么最終的架構就是上面圖中的“形式1”架構,也就是下面這樣。
如果系統需要支持MySQL、Oracle、DB2數據庫存儲,那么最終的架構就變成了“形式2”的架構了,可以看下面這張圖。
通過剝離變化層和穩定層的方式應對變化,都會帶來兩個主要的復雜性相關的問題。
- 系統需要拆分出變化層和穩定層
對于哪些屬于變化層,哪些屬于穩定層,很多時候并不是像前面的示例(不同接口協議或者不同數據庫)那樣明確,不同的人有不同的理解,導致架構設計評審的時候可能吵翻天。 - 需要設計變化層和穩定層之間的接口
接口設計同樣至關重要,對于穩定層來說,接口肯定是越穩定越好;但對于變化層來說,在有差異的多個實現方式中找出共同點,并且還要保證當加入新的功能時原有的接口設計不需要太大修改,這是一件很復雜的事情。
2.2 提煉出一個“抽象層”和一個“實現層”
抽象層是穩定的,實現層可以根據具體業務需要定制開發,當加入新的功能時,只需要增加新的實現,無須修改抽象層。這種方案典型的實踐就是設計模式和規則引擎。
六、復雜度來源:低成本、安全、規模
1. 低成本
低成本本質上是與高性能和高可用沖突的,所以低成本很多時候不會是架構設計的首要目標,而是架構設計的附加約束。也就是說,我們首先設定一個成本目標,當我們根據高性能、高可用的要求設計出方案時,評估一下方案是否能滿足成本目標,如果不行,就需要重新設計架構;如果無論如何都無法設計出滿足成本要求的方案,那就只能找老板調整成本目標了。
低成本給架構設計帶來的主要復雜度體現在,往往只有“創新”才能達到低成本目標。這里的“創新”既包括開創一個全新的技術領域(這個要求對絕大部分公司太高),也包括引入新技術,如果沒有找到能夠解決自己問題的新技術,那么就真的需要自己創造新技術了。
相比來說,創造新技術復雜度更高,因此一般中小公司基本都是靠引入新技術來達到低成本的目標;而大公司更有可能自己去創造新的技術來達到低成本的目標,因為大公司才有足夠的資源、技術和時間去創造新技術。
2. 安全
從技術的角度來講,安全可以分為兩類:一類是功能上的安全,一類是架構上的安全。
2.1 功能安全
例如,常見的XSS攻擊、CSRF攻擊、SQL注入、Windows漏洞、密碼破解等,本質上是因為系統實現有漏洞,黑客有了可乘之機。黑客會利用各種漏洞潛入系統,這種行為就像小偷一樣,黑客和小偷的手法都是利用系統或家中不完善的地方潛入,并進行破壞或者盜取。因此形象地說,功能安全其實就是“防小偷”。
從實現的角度來看,功能安全更多地是和具體的編碼相關,與架構關系不大。現在很多開發框架都內嵌了常見的安全功能,能夠大大減少安全相關功能的重復開發,但框架只能預防常見的安全漏洞和風險(常見的XSS攻擊、CSRF攻擊、SQL注入等),無法預知新的安全問題,而且框架本身很多時候也存在漏洞(例如,流行的Apache Struts2就多次爆出了調用遠程代碼執行的高危漏洞,給整個互聯網都造成了一定的恐慌)。所以功能安全是一個逐步完善的過程,而且往往都是在問題出現后才能有針對性的提出解決方案,我們永遠無法預測系統下一個漏洞在哪里,也不敢說自己的系統肯定沒有任何問題。換句話講,功能安全其實也是一個“攻”與“防”的矛盾,只能在這種攻防大戰中逐步完善,不可能在系統架構設計的時候一勞永逸地解決。
2.2 架構安全
架構安全就是“防強盜”,強盜會直接用大錘將門砸開,或者用炸藥將圍墻炸倒;小偷是偷東西,而強盜很多時候就是故意搞破壞,對系統的影響也大得多。因此架構設計時需要特別關注架構安全,尤其是互聯網時代,理論上來說系統部署在互聯網上時,全球任何地方都可以發起攻擊。
傳統的架構安全主要依靠防火墻,防火墻最基本的功能就是隔離網絡,通過將網絡劃分成不同的區域,制定出不同區域之間的訪問控制策略來控制不同信任程度區域間傳送的數據流。
防火墻的功能雖然強大,但性能一般,所以在傳統的銀行和企業應用領域應用較多。但在互聯網領域,防火墻的應用場景并不多。因為互聯網的業務具有海量用戶訪問和高并發的特點,防火墻的性能不足以支撐;尤其是互聯網領域的DDoS攻擊,輕則幾GB,重則幾十GB。
公司一般不會堆防火墻來防DDoS攻擊,因為DDoS攻擊最大的影響是大量消耗機房的出口總帶寬。不管防火墻處理能力有多強,當出口帶寬被耗盡時,整個業務在用戶看來就是不可用的,因為用戶的正常請求已經無法到達系統了。防火墻能夠保證內部系統不受沖擊,但用戶也是進不來的。對于用戶來說,業務都已經受到影響了,至于是因為用戶自己進不去,還是因為系統出故障,用戶其實根本不會關心。
基于上述原因,互聯網系統的架構安全目前并沒有太好的設計手段來實現,更多地是依靠運營商或者云服務商強大的帶寬和流量清洗的能力,較少自己來設計和實現。
3. 規模
規模帶來復雜度的主要原因就是“量變引起質變”,當數量超過一定的閾值后,復雜度會發生質的變化。常見的規模帶來的復雜度有:
3.1 功能越來越多,導致系統復雜度指數級上升
隨著系統功能數量增多,功能之間的連接呈指數級增長。下圖形象地展示了功能數量的增多帶來了復雜度。
3.2 數據越來越多,系統復雜度發生質變
數據太多以后,傳統的數據收集、加工、存儲、分析的手段和工具已經無法適應,必須應用新的技術才能解決。目前的大數據理論基礎是Google發表的三篇大數據相關論文,其中Google File System是大數據文件存儲的技術理論,Google Bigtable是列式數據存儲的技術理論,Google MapReduce是大數據運算的技術理論,這三篇技術論文各自開創了一個新的技術領域。
即使數據沒有達到大數據規模,數據的增長也可能給系統帶來復雜性。最典型的例子莫過于使用關系數據庫存儲數據,以MySQL為例,MySQL單表的數據因不同的業務和應用場景會有不同的最優值,但不管怎樣都肯定是有一定的限度的,一般推薦在5000萬行左右。如果因為業務的發展,單表數據達到了10億行,就會產生很多問題,例如:
- 添加索引會很慢,可能需要幾個小時,這幾個小時內數據庫表是無法插入數據的,相當于業務停機了。
- 修改表結構和添加索引存在類似的問題,耗時可能會很長。
- 即使有索引,索引的性能也可能會很低,因為數據量太大。
- 數據庫備份耗時很長。
- ……
因此,當MySQL單表數據量太大時,我們必須考慮將單表拆分為多表,這個拆分過程也會引入更多復雜性,例如:
- 拆表的規則是什么?
以用戶表為例:是按照用戶id拆分表,還是按照用戶注冊時間拆表? - 拆完表后查詢如何處理?
以用戶表為例:假設按照用戶id拆表,當業務需要查詢學歷為“本科”以上的用戶時,要去很多表查詢才能得到最終結果,怎么保證性能? - …
七、架構設計的三原則及案例
優秀程序員和架構師之間還有一個明顯的鴻溝需要跨越,這個鴻溝就是“不確定性”。
對于編程來說,本質上是不能存在不確定的。而對于架構設計來說,本質上是不確定的。相比編程來說,架構設計并沒有像編程語言那樣的語法來進行約束,更多的時候是面對多種可能性時進行選擇。這個問題的關鍵原因在于架構設計領域并沒有一套通用的規范來指導架構師進行架構設計,更多是依賴架構師的經驗和直覺,因此架構設計有時候也會被看作一項比較神秘的工作。
業務千變萬化,技術層出不窮,設計理念也是百花齊放,看起來似乎很難有一套通用的規范來適用所有的架構設計場景。但是有幾個共性的原則隱含其中,這就是:合適原則、簡單原則、演化原則,架構設計時遵循這幾個原則,有助于做出最好的選擇。
面對“不確定性”時架構設計的三原則,分別是合適優于業界領先、簡單優于復雜、演化優于一步到位。
1. 合適原則
合適原則宣言:“合適優于業界領先”。
再好的夢想,也需要腳踏實地實現!這里的“腳踏實地”主要體現在下面幾個方面。
- 將軍難打無兵之仗:沒那么多人,卻想干那么多活,是失敗的第一個主要原因。
- 羅馬不是一天建成的:沒有那么多積累,卻想一步登天,是失敗的第二個主要原因。
- 冰山下面才是關鍵:沒有那么卓越的業務場景,卻幻想靈光一閃成為天才,是失敗的第三個主要原因。
真正優秀的架構都是在企業當前人力、條件、業務等各種約束下設計出來的,能夠合理地將資源整合在一起并發揮出最大功效,并且能夠快速落地。這也是很多BAT出來的架構師到了小公司或者創業團隊反而做不出成績的原因,因為沒有了大公司的平臺、資源、積累,只是生搬硬套大公司的做法,失敗的概率非常高。
2. 簡單原則
簡單原則宣言:“簡單優于復雜”。
“復雜”在制造領域代表先進,在建筑領域代表領先,但在軟件領域,卻恰恰相反,代表的是“問題”。軟件領域的復雜性體現在兩個方面:
2.1 結構的復雜性
結構復雜的系統幾乎毫無例外具備兩個特點:
- 組成復雜系統的組件數量更多;
- 同時這些組件之間的關系也更加復雜。
結構上的復雜性存在的第一個問題是,組件越多,就越有可能其中某個組件出現故障,從而導致系統故障。
結構上的復雜性存在的第二個問題是,某個組件改動,會影響關聯的所有組件,這些被影響的組件同樣會繼續遞歸影響更多的組件。這個問題會影響整個系統的開發效率,因為一旦變更涉及外部系統,需要協調各方統一進行方案評估、資源協調、上線配合。
結構上的復雜性存在的第三個問題是,定位一個復雜系統中的問題總是比簡單系統更加困難。首先是組件多,每個組件都有嫌疑,因此要逐一排查;其次組件間的關系復雜,有可能表現故障的組件并不是真正問題的根源。
2.2 邏輯的復雜性
意識到結構的復雜性后,我們的第一反應可能就是“降低組件數量”,畢竟組件數量越少,系統結構越簡單。最簡單的結構就是整個系統只有一個組件,即系統本身,所有的功能和邏輯都在這一個組件中實現。
不過這樣做是行不通的,原因在于除了結構的復雜性,還有邏輯的復雜性,即如果某個組件的邏輯太復雜,一樣會帶來各種問題。邏輯復雜幾乎會導致軟件工程的每個環節都有問題。
功能復雜的組件,另外一個典型特征就是采用了復雜的算法。復雜算法導致的問題主要是難以理解,進而導致難以實現、難以修改,并且出了問題難以快速解決。
綜合來看,無論是結構的復雜性,還是邏輯的復雜性,都會存在各種問題,所以架構設計時如果簡單的方案和復雜的方案都可以滿足需求,最好選擇簡單的方案。《UNIX編程藝術》總結的KISS(Keep It Simple, Stupid!)原則一樣適應于架構設計。
3. 演化原則
演化原則宣言:“演化優于一步到位”。
對于建筑來說,永恒是主題;而對于軟件來說,變化才是主題。軟件架構需要根據業務的發展而不斷變化。
如果沒有把握“軟件架構需要根據業務發展不斷變化”這個本質,在做架構設計的時候就很容易陷入一個誤區:試圖一步到位設計一個軟件架構,期望不管業務如何變化,架構都穩如磐石。
考慮到軟件架構需要根據業務發展不斷變化這個本質特點,軟件架構設計其實更加類似于大自然“設計”一個生物,通過演化讓生物適應環境,逐步變得更加強大,軟件架構設計同樣是類似的過程:
- 首先,設計出來的架構要滿足當時的業務需要。
- 其次,架構要不斷地在實際應用過程中迭代,保留優秀的設計,修復有缺陷的設計,改正錯誤的設計,去掉無用的設計,使得架構逐漸完善。
- 第三,當業務發生變化時,架構要擴展、重構,甚至重寫;代碼也許會重寫,但有價值的經驗、教訓、邏輯、設計等(類似生物體內的基因)卻可以在新架構中延續。
架構師在進行架構設計時需要牢記這個原則,時刻提醒自己不要貪大求全,或者盲目照搬大公司的做法。應該認真分析當前業務的特點,明確業務面臨的主要問題,設計合理的架構,快速落地以滿足業務需要,然后在運行過程中不斷完善架構,不斷隨著業務演化架構。
即使是大公司的團隊,在設計一個新系統的架構時,也需要遵循演化的原則,而不應該認為團隊人員多、資源多,不管什么系統上來就要一步到位,因為業務的發展和變化是很快的,不管多牛的團隊,也不可能完美預測所有的業務發展和變化路徑。
4. 案例
即使是現在非常復雜、非常強大的架構,也并不是一開始就進行了復雜設計,而是首先采取了簡單的方式(簡單原則),滿足了當時的業務需要(合適原則),隨著業務的發展逐步演化而來的(演化原則)。
4.1 案例一:淘寶
淘寶技術發展主要經歷了“個人網站”→“Oracle/支付寶/旺旺”→“Java時代1.0”→“Java時代2.0”→“Java時代3.0”→“分布式時代”。
- 個人網站
買一個系統是為了“快速可用”,而買一個輕量級的系統是為了“快速開發”。因為系統上線后肯定有大量的需求需要做,這時能夠快速開發就非常重要。
從這個實例我們可以看到:淘寶最開始的時候業務要求就是“快”,因此反過來要求技術同樣要“快”,業務決定技術,這里架構設計和選擇主要遵循的是“合適原則”和“簡單原則”。
- Oracle/支付寶/旺旺
技術的替代方案非常簡單,就是換成Oracle。換Oracle的原因除了它容量大、穩定、安全、性能高,還有人才方面的原因。
此時離剛上線才半年不到,業務飛速發展,最快的方式支撐業務的發展還是去買。如果說第一階段買的是“方案”,這個階段買的就是“性能”,這里架構設計和選擇主要遵循的還是“合適原則”和“簡單原則”。
- Java時代1.0
淘寶切換到Java的原因很有趣,主要因為找了一個PHP的開源連接池SQL Relay連接到Oracle,而這個代理經常死鎖,死鎖了就必須重啟,而數據庫又必須用Oracle,于是決定換個開發語言。
這次切換的最主要原因是因為技術影響了業務的發展,頻繁的死鎖和重啟對用戶業務產生了嚴重的影響,從業務的角度來看這是不得不解決的技術問題。而且Java是當時最成熟的網站開發語言,它有比較良好的企業開發框架,被世界上主流的大規模網站普遍采用,另外有Java開發經驗的人才也比較多,后續維護成本會比較低。
綜合來看,這次架構的變化沒有再簡單通過“買”來解決,而是通過重構來解決,架構設計和選擇遵循了“演化原則”。
- Java時代2.0
隨著數據量的繼續增長,采取“買”的方式已無法解決容量、性能、成本問題。
另外,隨著規模的增大,純粹靠買的一個典型問題開始成為重要的考慮因素,那就是成本。這就是“量變帶來質變”的一個典型案例,業務和系統發生質變后,架構設計遵循“演化原則”的思想,需要再一次重構甚至重寫。
- Java 時代3.0和分布式時代
到了這個階段,業務規模急劇上升后,原來并不是主要復雜度的IOE成本開始成為了主要的問題,因此通過自研系統來降低IOE的成本,去IOE也是系統架構的再一次演化。
4.2 案例二:手機QQ
手機QQ的發展歷程按照用戶規模可以粗略劃分為4個階段:十萬級、百萬級、千萬級、億級,不同的用戶規模,IM后臺的架構也不同,而且基本上都是用戶規模先上去,然后產生各種問題,倒逼技術架構升級。
- 十萬級IM 1.X
當時業務剛開始,架構設計遵循的是“合適原則”和“簡單原則”。
- 百萬級IM 2.X
按照“演化原則”的指導進行了重構,重構的方案相比現在來說也還是簡單得多,因此當時做架構設計時也遵循了“合適原則”和“簡單原則”。
-
千萬級IM 3.X
架構需要繼續改造升級,再一次“演化”。可以看到這次的方案相比之前的方案來說并不簡單了,這是業務特性決定的。 -
億級IM 4.X
IM后臺從1.0到3.5都是在原來基礎上做改造升級的,但是持續打補丁已經難以支撐億級在線,IM后臺4.0必須從頭開始,重新設計實現!這里再次遵循了“演化原則”。
重新設計的IM 4.0架構如圖所示,和之前的架構相比,架構本身都拆分為兩個主要的架構:存儲架構和通信架構。
(1)存儲架構
(2)通信架構
八、架構設計流程
1. 識別復雜度
在設計架構時,首先就要分析系統的復雜性。只有正確分析出了系統的復雜性,后續的架構設計方案才不會偏離方向;否則,如果對系統的復雜性判斷錯誤,即使后續的架構設計方案再完美再先進,都是南轅北轍,做的越好,錯的越多、越離譜。
架構的復雜度主要來源于“高性能”“高可用”“可擴展”等幾個方面,但架構師在具體判斷復雜性的時候,不能生搬硬套,認為任何時候架構都必須同時滿足這三方面的要求。實際上大部分場景下,復雜度只是其中的某一個,少數情況下包含其中兩個,如果真的出現同時需要解決三個或者三個以上的復雜度,要么說明這個系統之前設計的有問題,要么可能就是架構師的判斷出現了失誤,即使真的認為要同時滿足這三方面的要求,也必須要進行優先級排序。
如果運氣真的不好,接手了一個每個復雜度都存在問題的系統,也要一個個來解決問題,不要幻想一次架構重構解決所有問題。如果同時要解決這些問題,就可能會面臨這些困境:
- 要做的事情太多,反而感覺無從下手。
- 設計方案本身太復雜,落地時間遙遙無期。
- 同一個方案要解決不同的復雜性,有的設計點是互相矛盾的。例如,要提升系統可用性,就需要將數據及時存儲到硬盤上,而硬盤刷盤反過來又會影響系統性能。
因此,正確的做法是將主要的復雜度問題列出來,然后根據業務、技術、團隊等綜合情況進行排序,優先解決當前面臨的最主要的復雜度問題。
對于按照復雜度優先級解決的方式,存在一個普遍的擔憂:如果按照優先級來解決復雜度,可能會出現解決了優先級排在前面的復雜度后,解決后續復雜度的方案需要將已經落地的方案推倒重來。這個擔憂理論上是可能的,但現實中幾乎是不可能出現的,原因在于軟件系統的可塑性和易變性。對于同一個復雜度問題,軟件系統的方案可以有多個,總是可以挑出綜合來看性價比最高的方案。
即使架構師決定要推倒重來,這個新的方案也必須能夠同時解決已經被解決的復雜度問題,一般來說能夠達到這種理想狀態的方案基本都是依靠新技術的引入。例如,Hadoop能夠將高可用、高性能、大容量三個大數據處理的復雜度問題同時解決。
識別復雜度對架構師來說是一項挑戰,因為原始的需求中并沒有哪個地方會明確地說明復雜度在哪里,需要架構師在理解需求的基礎上進行分析。有經驗的架構師可能一看需求就知道復雜度大概在哪里;如果經驗不足,那只能采取“排查法”,從不同的角度逐一進行分析。
2. 設計備選方案
架構師的工作并不神秘,成熟的架構師需要對已經存在的技術非常熟悉,對已經經過驗證的架構模式爛熟于心,然后根據自己對業務的理解,挑選合適的架構模式進行組合,再對組合后的方案進行修改和調整。
雖說基于已有的技術或者架構模式進行組合,然后調整,大部分情況下就能夠得到我們需要的方案,但并不意味著架構設計是一件很簡單的事情。因為可選的模式有很多,組合的方案更多,往往一個問題的解決方案有很多個;如果再在組合的方案上進行一些創新,解決方案會更多。因此,如何設計最終的方案,并不是一件容易的事情,這個階段也是很多架構師容易犯錯的地方。
2.1 常見錯誤一:設計最優秀的方案
根據架構設計原則中“合適原則”和“簡單原則“的要求,挑選合適自己業務、團隊、技術能力的方案才是好方案;否則要么浪費大量資源開發了無用的系統(例如,之前提過的“億級用戶平臺”的案例,設計了TPS 50000的系統,實際TPS只有500),要么根本無法實現(例如,10個人的團隊要開發現在的整個淘寶系統)。
2.2 常見錯誤二:只做一個方案
這樣做有很多弊端:
- 心里評估過于簡單,可能沒有想得全面,只是因為某一個缺點就把某個方案給否決了,而實際上沒有哪個方案是完美的,某個地方有缺點的方案可能是綜合來看最好的方案。
- 架構師再怎么牛,經驗知識和技能也有局限,有可能某個評估的標準或者經驗是不正確的,或者是老的經驗不適合新的情況,甚至有的評估標準是架構師自己原來就理解錯了。
- 單一方案設計會出現過度辯護的情況,即架構評審時,針對方案存在的問題和疑問,架構師會竭盡全力去為自己的設計進行辯護,經驗不足的設計人員可能會強詞奪理。
合理的做法是:
- 備選方案的數量以3 ~ 5個為最佳。少于3個方案可能是因為思維狹隘,考慮不周全;多于5個則需要耗費大量的精力和時間,并且方案之間的差別可能不明顯。
- 備選方案的差異要比較明顯。例如,主備方案和集群方案差異就很明顯,或者同樣是主備方案,用ZooKeeper做主備決策和用Keepalived做主備決策的差異也很明顯。但是都用ZooKeeper做主備決策,一個檢測周期是1分鐘,一個檢測周期是5分鐘,這就不是架構上的差異,而是細節上的差異了,不適合做成兩個方案。
- 備選方案的技術不要只局限于已經熟悉的技術。設計架構時,架構師需要將視野放寬,考慮更多可能性。很多架構師或者設計師積累了一些成功的經驗,出于快速完成任務和降低風險的目的,可能自覺或者不自覺地傾向于使用自己已經熟悉的技術,對于新的技術有一種不放心的感覺。就像那句俗語說的:“如果你手里有一把錘子,所有的問題在你看來都是釘子”。例如,架構師對MySQL很熟悉,因此不管什么存儲都基于MySQL去設計方案,系統性能不夠了,首先考慮的就是MySQL分庫分表,而事實上也許引入一個Memcache緩存就能夠解決問題。
2.3 常見錯誤三:備選方案過于詳細
有的架構師或者設計師在寫備選方案時,錯誤地將備選方案等同于最終的方案,每個備選方案都寫得很細。這樣做的弊端顯而易見:
- 耗費了大量的時間和精力。
- 將注意力集中到細節中,忽略了整體的技術設計,導致備選方案數量不夠或者差異不大。
- 評審的時候其他人會被很多細節給繞進去,評審效果很差。例如,評審的時候針對某個定時器應該是1分鐘還是30秒,爭論得不可開交。
正確的做法是備選階段關注的是技術選型,而不是技術細節,技術選型的差異要比較明顯。
3. 評估和選擇備選方案
在完成備選方案設計后,如何挑選出最終的方案也是一個很大的挑戰,主要原因有:
- 每個方案都是可行的,如果方案不可行就根本不應該作為備選方案。
- 沒有哪個方案是完美的。例如,A方案有性能的缺點,B方案有成本的缺點,C方案有新技術不成熟的風險。
- 評價標準主觀性比較強,比如設計師說A方案比B方案復雜,但另外一個設計師可能會認為差不多,因為比較難將“復雜”一詞進行量化。因此,方案評審的時候我們經常會遇到幾個設計師針對某個方案或者某個技術點爭論得面紅耳赤。
實踐中很多設計師或者架構師采取了下面幾種指導思想:
- 最簡派
設計師挑選一個看起來最簡單的方案。例如,我們要做全文搜索功能,方案1基于MySQL,方案2基于Elasticsearch。MySQL的查詢功能比較簡單,而Elasticsearch的倒排索引設計要復雜得多,寫入數據到Elasticsearch,要設計Elasticsearch的索引,要設計Elasticsearch的分布式……全套下來復雜度很高,所以干脆就挑選MySQL來做吧。 - 最牛派
最牛派的做法和最簡派正好相反,設計師會傾向于挑選技術上看起來最牛的方案。例如,性能最高的、可用性最好的、功能最強大的,或者淘寶用的、微信開源的、Google出品的等。
我們以緩存方案中的Memcache和Redis為例,假如我們要挑選一個搭配MySQL使用的緩存,Memcache是純內存緩存,支持基于一致性hash的集群;而Redis同時支持持久化、支持數據字典、支持主備、支持集群,看起來比Memcache好很多啊,所以就選Redis好了。 - 最熟派
設計師基于自己的過往經驗,挑選自己最熟悉的方案。我以編程語言為例,假如設計師曾經是一個C++經驗豐富的開發人員,現在要設計一個運維管理系統,由于對Python或者Ruby on Rails不熟悉,因此繼續選擇C++來做運維管理系統。 - 領導派
領導派就更加聰明了,列出備選方案,設計師自己拿捏不定,然后就讓領導來定奪,反正最后方案選的對那是領導厲害,方案選的不對怎么辦?那也是領導“背鍋”。
其實這些不同的做法本身并不存在絕對的正確或者絕對的錯誤,關鍵是不同的場景應該采取不同的方式。也就是說,有時候我們要挑選最簡單的方案,有時候要挑選最優秀的方案,有時候要挑選最熟悉的方案,甚至有時候真的要領導拍板。
應該選擇哪種方法來評估和選擇備選方案呢?答案就是“360度環評”!具體的操作方式為:列出我們需要關注的質量屬性點,然后分別從這些質量屬性的維度去評估每個方案,再綜合挑選適合當時情況的最優方案。
常見的方案質量屬性點有:性能、可用性、硬件成本、項目投入、復雜度、安全性、可擴展性等。在評估這些質量屬性時,需要遵循架構設計原則1“合適原則”和原則2“簡單原則”,避免貪大求全,基本上某個質量屬性能夠滿足一定時期內業務發展就可以了。
360度環評表示例:
有的設計師會有這樣的擔心:如果我們運氣真的很好,業務直接一年翻了10倍,之前的方案不合適了,又要重新做方案?這種情況確實有可能存在,但概率很小,如果每次做方案都考慮這種小概率事件,我們的方案會出現過度設計,導致投入浪費。考慮這個問題的時候,需要遵循架構設計原則3“演化原則”,避免過度設計、一步到位的想法。按照原則3的思想,即使真的出現這種情況,那就算是重新做方案,代價也是可以接受的,因為業務如此迅猛發展,錢和人都不是問題。通常情況下,如果某個質量屬性評估和業務發展有關系(例如,性能、硬件成本等),需要評估未來業務發展的規模時,一種簡單的方式是將當前的業務規模乘以2 ~4即可,如果現在的基數較低,可以乘以4;如果現在基數較高,可以乘以2。
最理想的情況是設計一個方案,能夠簡單地擴容就能夠跟上業務的發展。但現實往往沒那么理想,因為量變會引起質變,具體哪些地方質變,是很難提前很長時間能預判到的。
完成方案的360度環評后,可以基于評估結果整理出360度環評表,一目了然地看到各個方案的優劣點。但是360度環評表也只能幫助我們分析各個備選方案,還是沒有告訴我們具體選哪個方案,原因就在于沒有哪個方案是完美的,極少出現某個方案在所有對比維度上都是最優的。
!!面臨這種選擇上的困難,有幾種看似正確但實際錯誤的做法:
- 數量對比法:簡單地看哪個方案的優點多就選哪個。例如,總共5個質量屬性的對比,其中A方案占優的有3個,B方案占優的有2個,所以就挑選A方案。
這種方案主要的問題在于把所有質量屬性的重要性等同,而沒有考慮質量屬性的優先級。其次,有時候會出現兩個方案的優點數量是一樣的情況。如果為了數量上的不對稱,強行再增加一個質量屬性進行對比,這個最后增加的不重要的屬性反而成了影響方案選擇的關鍵因素,這又犯了沒有區分質量屬性的優先級的問題。 - 加權法:每個質量屬性給一個權重。例如,性能的權重高中低分別得10分、5分、3分,成本權重高中低分別是5分、3分、1分,然后將每個方案的權重得分加起來,最后看哪個方案的權重得分最高就選哪個。
這種方案主要的問題是無法客觀地給出每個質量屬性的權重得分。這個分數是很難確定的,沒有明確的標準,甚至會出現為了選某個方案,設計師故意將某些權重分值調高而降低另外一些權重分值,最后方案的選擇就變成了一個數字游戲了。
正確的做法是按優先級選擇,即架構師綜合當前的業務發展情況、團隊人員規模和技能、業務發展預測等因素,將質量屬性按照優先級排序,首先挑選滿足第一優先級的,如果方案都滿足,那就再看第二優先級……以此類推。那會不會出現兩個或者多個方案,每個質量屬性的優缺點都一樣的情況呢?理論上是可能的,但實際上是不可能的。前面提到在做備選方案設計時,不同的備選方案之間的差異要比較明顯,差異明顯的備選方案不可能所有的優缺點都是一樣的。
備選方案的選擇和很多因素相關,并不單單考慮性能高低、技術是否優越這些純技術因素。業務的需求特點、運維團隊的經驗、已有的技術體系、團隊人員的技術水平都會影響備選方案的選擇。
4. 詳細方案設計
詳細方案設計就是將方案涉及的關鍵技術細節給確定下來。
- 假如我們確定使用Elasticsearch來做全文搜索,那么就需要確定Elasticsearch的索引是按照業務劃分,還是一個大索引就可以了;副本數量是2個、3個還是4個,集群節點數量是3個還是6個等。
- 假如我們確定使用MySQL分庫分表,那么就需要確定哪些表要分庫分表,按照什么維度來分庫分表,分庫分表后聯合查詢怎么處理等。
- 假如我們確定引入Nginx來做負載均衡,那么Nginx的主備怎么做,Nginx的負載均衡策略用哪個(權重分配?輪詢?ip_hash?)等。
可以看到,詳細設計方案里面其實也有一些技術點和備選方案類似。例如,Nginx的負載均衡策略,備選有輪詢、權重分配、ip_hash、fair、url_hash五個,具體選哪個呢?看起來和備選方案階段面臨的問題類似,但實際上這里的技術方案選擇是很輕量級的,我們無須像備選方案階段那樣操作,而只需要簡單根據這些技術的適用場景選擇就可以了。
詳細設計方案階段可能遇到的一種極端情況就是在詳細設計階段發現備選方案不可行,一般情況下主要的原因是備選方案設計時遺漏了某個關鍵技術點或者關鍵的質量屬性。 這種情況可以通過下面方式有效地避免:
- 架構師不但要進行備選方案設計和選型,還需要對備選方案的關鍵細節有較深入的理解。例如,架構師選擇了Elasticsearch作為全文搜索解決方案,前提必須是架構師自己對Elasticsearch的設計原理有深入的理解,比如索引、副本、集群等技術點;而不能道聽途說Elasticsearch很牛,所以選擇它,更不能成為把“細節我們不討論”這句話掛在嘴邊的“PPT架構師”。
- 通過分步驟、分階段、分系統等方式,盡量降低方案復雜度,方案本身的復雜度越高,某個細節推翻整個方案的可能性就越高,適當降低復雜性,可以減少這種風險。
- 如果方案本身就很復雜,那就采取設計團隊的方式來進行設計,博采眾長,匯集大家的智慧和經驗,防止只有1~2個架構師可能出現的思維盲點或者經驗盲區。