架構設計的問題與解法

0、引語

小到某個功能的開發方案,大到整個業務的系統設計,都可以看到架構設計的影子,但是架構設計的目的到底是什麼?作者給我們的解答是:架構設計的主要目的是爲了解決軟件系統複雜度帶來的問題

這裏其實有兩個重點:一是問題,二是解決。

首先得知道我們要解決的問題在哪裏?面前的系統到底有什麼複雜度導致的問題?只有知道了問題才能選擇解法,不能拿着錘子找釘子。

當知道了當前面臨的問題後,就要利用前人的智慧和自身的經驗,設計出合理的架構方案來解決問題。

因此對整本書的內容,可歸納分成以下四節,第一節主要描述我們通常面臨的系統複雜度及問題在哪,後面三節則是對常見的三個問題下的常用解法進行闡述。

1、基本概念與設計方法

在講解架構思想之前,先統一介紹一下基本概念的含義,避免每個人對系統、框架、架構這些名詞的理解不一致導致的誤解。下面是作者對每個名詞的定義,其作用域僅限本文範疇,不用糾結其在其他上下文中的意義。

由以上定義可見,所謂架構,是爲了解決軟件系統的某個複雜度帶來的具體問題,將模塊和組件以某種方式有機組合,基於某個具體的框架實現後的一種落地方案。

而討論架構時,往往只討論到系統與子系統這個頂層的架構。

可見,要進行架構選型,首先應該知道自己要解決的業務和系統複雜點在哪裏,是作爲秒殺系統有瞬間高併發,還是作爲金融科技有極高的數據一致性和可用性要求等。

一般來說,系統的複雜度來源有以下幾個方面:

高性能:

如果業務的訪問頻率或實時性要求較高,則會對系統提出高性能的要求。

如果是單機系統,需要利用多進程、多線程技術。

如果是集羣系統,則還涉及任務拆分、分配與調度,多機器狀態管理,機器間通信,當單機性能達到瓶頸後,即使繼續加機器也無法繼續提升性能,還是要針對單個子任務進行性能提升。

高可用:

如果業務的可用性要求較高,也會帶來高可用方面的複雜度。高可用又分爲計算高可用和存儲高可用。

針對計算高可用,可以採用主備(冷備、溫備、熱備)、多主的方式來冗餘計算能力,但會增加成本、可維護性方面的複雜度。

針對存儲高可用,同樣是增加機器來冗餘,但這也會帶來多機器導致的數據不一致問題,如遇到延遲、中斷、故障等情況。難點在於怎麼減少數據不一致對業務的影響。

既然主要解決思路是增加機器來做冗餘,那麼就涉及到了狀態決策的問題。即如果判斷當前主機的狀態是正常還是異常,以及異常了要如何採取行動(比如切換哪臺做主機)。

對主機狀態的判斷,多采用機器信息採集或請求響應情況分析等手段,但又會產生採集信息這一條通信鏈路本身是否正常的問題,下文會具體展開討論。事實上,狀態決策本質上不可能做到完全正確。

而對於決策方式,有以下幾種方式:

可擴展性:

衆所周知在互聯網行業只有變化纔是永遠不變的,而開發一個系統基本都不是一蹴而就的,那應該如何爲系統的未來可能性進行設計來保持可擴展性呢?

這裏首先要明確的一個觀點就是,在做系統設計時,既不可能完全不考慮可擴展性,也不可能每個設計點都考慮可擴展性,前者很明顯,後者則是爲了避免捨本逐末,爲了擴展而擴展,實際上可能會爲不存在的預測花費過多的精力。

那麼怎麼考慮系統的未來可能性從而做出相應的可擴展性設計呢?這裏作者給出了一個方法:只預測兩年內可能的變化,不要試圖預測五年乃至十年的變化。因爲對於變化快的行業來說,預測兩年已經足夠遠了,再多就可能計劃趕不上變化。而對變化慢的行業,則預測的意義更是不大。

要應對變化,主要是將變與不變分隔開來。

這裏可以針對業務,提煉變化層和穩定層,通過變化層將變化隔離。比如通過一個 DAO 服務來對接各種變化的存儲載體,但是上層穩定的邏輯不用知曉當前採用何種存儲,只需按照固定的接口訪問 DAO 即可獲取數據。

也可以將一些實現細節剝離開來,提煉出抽象層,僅在實現層去封裝變化。比如面對運營上經常變化的業務規則,可以提煉出一個規則引擎來實現核心的抽象邏輯,而具體的規則實現則可以按需增加。

如果是面對一箇舊系統的維護,接到了新的重複性需求,而舊系統並不支持較好的可擴展性,這時是否需要花費時間精力去重構呢?作者也提出了《重構》一書中提到的原則:事不過三,三則重構。

簡而言之,不要一開始就考慮複雜的做法去滿足可擴展性,而是等到第三次遇到類似的實現時再來重構,重構的時候採取上述說的隔離或者封裝的方案。這一原則對

這一原則對新系統開發也是適用的。總而言之就是,不要爲難以預測的未來去過度設計,爲明確的未來保留適量的可擴展性即可。

低成本:

上面說的高性能、高可用都需要增加機器,帶來的是成本的增加,而很多時候研發的預算是有限的。換句話說,低成本往往並不是架構設計的首要目標,而是設計架構時的約束限制。

那如何在有限的成本下滿足複雜性要求呢?往往只有 “創新” 才能達到低成本的目標。舉幾個例子:

上述案例都是爲了在不顯著增加成本的前提下,實現系統的目標。

這裏還要說明的是,創造新技術的複雜度本身就是很高的,因此一般中小公司基本都是靠引入現有的成熟新技術來達到低成本的目標;而大公司才更有可能自己去創造新的技術來達到低成本的目標,因爲大公司纔有足夠的資源、技術和時間去創造新技術。

安全:

安全是一個研發人員很熟悉的目標,從整體來說,安全包含兩方面:功能安全和架構安全。

功能安全是爲了 “防小偷”,即避免系統因安全漏洞而被竊取數據,如 SQL 注入。常見的安全漏洞已經有很多框架支持,所以更建議利用現有框架的安全能力,來避免重複開發,也避免因自身考慮不夠全面而遺漏。在此基礎上,仍需持續攻防來完善自身的安全。

架構安全是爲了 “防強盜 “,即避免系統被暴力攻擊導致系統故障,比如 DDOS 攻擊。這裏一方面只能通過防火牆集運營商或雲服務商的大帶寬和流量清洗的能力進行防範,另一方面也需要做好攻擊發現與干預、恢復的能力。

規模:

架構師在宣講時往往會先說自己任職和設計過的大型公司的架構,這是因爲當系統的規模達到一定程度後,複雜度會發生質的變化,即所謂量變引起質變。

這個量,體現在訪問量、功能數量及數據量上。

訪問量映射到對高性能的要求。功能數量需要視具體業務會帶來不同的複雜度。而數據量帶來的收集、加工、存儲、分析方面的挑戰,現有的方案基本都是給予 Google 的三篇大數據論文的理論:

經過上面的分析可以看到,複雜度來源很多,想要一一應對,似乎會得到一個複雜無比的架構,但對於架構設計來說,其實剛開始設計時越簡單越好,只要能解決問題,就可以從簡單開始再慢慢去演化,對應的是下面三條原則:

  1. 合適原則:不需要一開始就挑選業界領先的架構,它也許優秀,但可能不那麼適合自己,比如有很多目前用不到的能力或者大大超出訴求從而增加很多成本。其實更需要考慮的是合理地將資源整合在一起發揮出最大功效,並能夠快速落地。

  2. 簡單原則:有時候爲了顯示出自身的能力,往往會在一開始就將系統設計得非常複雜,複雜可能代表着先進,但更可能代表着 “問題”,組件越多,就越可能出故障,越可能影響關聯着的組件,定位問題也更加困難。其實只要能夠解決訴求即可。

  3. 演化原則:不要妄想一步到位,沒有人可以準確預測未來所有發展,軟件不像建築,變化纔是主題。架構的設計應該先滿足業務需求,適當的預留擴展性,然後在未來的業務發展中再不斷地迭代,保留有限的設計,修復缺陷,改正錯誤,去除無用部分。這也是重構、重寫的價值所在。

即使是 QQ、淘寶這種如今已經非常複雜的系統,剛開始時也只是一個簡單的系統,甚至淘寶都是直接買來的系統,隨着業務發展也只是先加服務器、引入一些組件解決性能問題,直到達到瓶頸纔去重構重寫,重新在新的複雜度要求下設計新的架構。

明確了設計原則後,當面對一個具體的業務,可以按照如下步驟進行架構設計:

  1. 識別複雜度:無論是新設計一個系統還是接手一個混亂的系統,第一步都是先將主要的複雜度問題列出來,然後根據業務、技術、團隊等綜合情況進行排序,優先解決當前面臨的最主要的複雜度問題。複雜度的主要來源上文已經說過,可以按照經驗或者排查法進行分析。

  2. 方案對比:先看看業界是否有類似的業務,瞭解他們是怎麼解決問題的,然後提出 3~5 個備選方案,不要只考慮做一個最優秀的方案,一個人的認知範圍常常是有限的,逼自己多思考幾個方案可以有效規避因爲思維狹隘導致的侷限性,當然也不要過多,不用給出非常詳細的方案,太消耗精力。備選方案的差異要比較明顯,纔有擴寬思路和對比的價值。

  3. 設計詳細方案:當多個方案對比得出最終選擇後,就可以對目標方案進行詳細的設計,關鍵細節需要比較深入,如果方案本身很複雜,也可以採取分步驟、分階段、分系統的實現方式來降低實現複雜度。當方案非常龐大的時候,可以彙集一個團隊的智慧和經驗來共同設計,防止因架構師的思維盲區導致問題。

2、高性能架構模式

2.1、存儲高性能

互聯網業務大多是數據密集型的業務,其對性能的壓力也常常來自於海量用戶對數據的高頻讀寫壓力上,因此解決高性能問題,首先要解決數據讀寫的存儲高性能問題。

在大多數業務中,用戶查詢和修改數據的頻率是不同的,甚至是差別很大的,大部分情況下都是讀多寫少的,因此可以將對數據的讀和寫操作分開對待,對壓力更大的讀操作提供額外的機器分擔壓力,這就是讀寫分離

讀寫分離的基本實現是搭建數據庫的主從集羣,根據需要提供一主一從或一主多從。

注意是主從不是主備,從和備的差別在於從機是要幹活的。

通常在讀多寫少的情況下,主機負責讀寫操作,從機只負責讀操作,負責幫主機分擔讀操作的壓力。而數據會通過複製機制(如全同步、半同步、異步)同步到從機,每臺服務器都有所有業務數據。

既然有數據的同步,就一定存在複製延遲導致的從機數據不一致問題,針對這個問題有幾種常見的解法,如:

除了數據同步的問題之外,只要涉及主從機同時支持業務訪問的,就一定需要制定請求分配的機制。上面說的幾個問題解法也涉及了一些分配機制的細節。具體到分配機制的實現來說,有兩種思路:

除了高頻訪問的壓力,當數據量大了以後,也會帶來數據庫存儲方面的壓力。此時就需要考慮分庫分表的問題。分庫分表既可以緩解訪問的壓力,也可以分散存儲的壓力。

先說分庫,所謂分庫,就是指業務按照功能、模塊、領域等不同,將數據分散存儲到不同的數據庫實例中。

比如原本是一個 MySQL 數據庫實例,在庫中按照不同業務建了多張表,大體可以歸類爲 A、B 兩個領域的數據。現在新建一個庫,將原庫中 A 領域的數據遷移到新的庫中存儲,還是按需建表,而 B 領域的數據繼續留在原庫中。

分庫一方面可以緩解訪問和存儲的壓力,另一方面也可以增加抗風險能力,當一個庫出問題後,另一個庫中的數據並不會受到影響,而且還能分開管理權限。

但分庫也會帶來一些問題,原本同一個庫中的不同表可以方便地進行聯表查詢,分庫後則會變得很複雜。由於數據在不同的庫中,當要操作兩個庫中的數據時,無法使用事務操作,一致性也變得更難以保障。而且當增加備庫來保障可用性的時候,成本是成倍增加的。

基於以上問題,初創的業務並不建議在一開始就做這種拆分,會增加很多開發時的成本和複雜度,拖慢業務的節奏。

再說分表,所謂分表,就是將原本存儲在一張表裏的數據,按照不同的維度,拆分成多張表來存儲。

按照訴求與業務的特性不同,可以採用垂直分表或水平分表的方式。

垂直分表相當於垂直地給原表切了一刀,把不同的字段拆分到不同的子表中,這樣拆分後,原本訪問一張表可以獲取的所有字段,現在則需要訪問不同的表獲取。

垂直分表適合將表中某些不常用又佔了大量空間的列(字段)拆分出去,可以提升訪問常用字段的性能。

但相應的,當真的需要的字段處於不同表中時,或者要新增記錄存儲所有字段數據時,要操作的表變多了。

水平分表相當於橫着給原表切了一刀,那麼原表中的記錄會被分散存儲到不同的子表中,但是每張子表的字段都是全部字段。

水平分表適合表的量級很大以至影響訪問性能的場景,何時該拆分並沒有絕對的指標,一般記錄數超過千萬時就需要警覺了。

不同於垂直分表依然能訪問到所有記錄,水平分表後無法再在一張表中訪問所有數據了,因此很多查詢操作會受到影響,比如 join 操作就需要多次查詢後合併結果,count 操作也需要計算多表的結果後相加,如果經常用到 count 的總數,可以額外維護一個總數表去更新,但也會帶來數據一致性的問題。

值得特別提出的是範圍查詢,原本的一張表可以通過範圍查詢到的數據,分表後也需要多次查詢後合併數據,如果是業務經常用到的範圍查詢,那建議乾脆就按照這種方式來分表,這也是分表的路由方式之一:範圍路由。

所謂路由方式是指:分表後當新插入記錄時,如何判斷該往哪張表插入。常用的插入方式有以下三種:

無論是垂直分表還是水平分表,單表切分爲多表後,新的表即使在同一個數據庫服務器中,也可能帶來可觀的性能提升,如果性能能夠滿足業務要求,可以不拆分到多臺數據庫服務器,畢竟分庫也會引入很多複雜性的問題;如果單表拆分爲多表後,單臺服務器依然無法滿足性能要求,那就不得不再次進行業務分庫的設計了。

上面發分庫分表討論的都是關係型數據庫的優化方案,但關係型數據庫也有其無法規避的缺點,比如無法直接存儲某種結構化的數據、擴展表結構時會鎖表影響線上性能、大數據場景下 I/O 較高、全文搜索的功能比較弱等。

基於這些缺點,也有很多新的數據庫框架被創造出來,解決其某方面的問題。

比如以 Redis 爲代表的的 KV 存儲,可以解決無法存儲結構化數據的問題;以 MongoDB 爲代表的的文檔數據庫可以解決擴展表結構被強 Schema 約束的問題;以 HBase 爲代表的的列式數據庫可以解決大數據場景下的 I/O 問題;以 ES 爲代表的的全文搜索引擎可以解決全文檢索效率的問題等。

這些數據庫統稱爲 NoSQL 數據庫,但 NoSQL 並不是全都不能寫 SQL,而是 Not Only SQL 的意思。

NoSQL 數據庫除了聚焦於解決某方面的問題以外也會有其自身的缺點,比如 Redis 沒有支持完整的 ACID 事務、列式存儲在更新一條記錄的多字段時性能較差等。因此並不是說使用了 NoSQL 就能一勞永逸,更多的是按需取用,解決業務面臨的問題。

關於 NoSQL 的更多瞭解,也可以看看《NoSQL 精粹》這本書。

如果 NoSQL 也解決不了業務的高性能訴求,那麼或許你需要加點緩存

緩存最直接的概念就是把常用的數據存在內存中,當業務請求來查詢的時候直接從內存中拿出來,不用重新去數據庫中按條件查詢,也就省去了大量的磁盤 IO 時間。

一般來說緩存都是通過 Key-Value 的方式存儲在內存中,根據存儲的位置,分爲單機緩存和集中式緩存。單機緩存就是存在自身服務器所在的機器上,那麼勢必會有不同機器數據可能不一致,或者重複緩存的問題,要解決可以使用查詢內容做路由來保障同一記錄始終到同一臺機器上查詢緩存。集中式緩存則是所有服務器都去一個地方查緩存,會增加一些調用時間。

緩存可以提升性能是很好理解的,但緩存同樣有着他的問題需要應對或規避。數據時效性是最容易想到的問題,但也可以靠同時更新緩存的策略來保障數據的時效性,除此之外還有其他幾個常見的問題。

如果某條數據不存在,緩存中勢必查不到對應的 KEY,從而就會請求數據庫確認是否有新增加這條數據,如果始終沒有這條數據,而客戶端又反覆頻繁地查詢這條數據,就會變相地對數據庫造成很大的壓力,換句話說,緩存失去了保護作用,請求穿透到了數據庫,這稱爲緩存穿透

應對緩存穿透,最好的手段就是把 “空值” 這一情況也緩存下來,當客戶端下次再查詢時,發現緩存中說明了該數據是空值,則不會再問詢數據庫。但也要注意如果真的有對應數據寫入了數據庫,應當能及時清除”空值“緩存。

爲了保障緩存的數據及時更新,常常都會根據業務特性設置一個緩存過期時間,在緩存過期後,到再次生成期間,如果出現大量的查詢,會導致請求都傳遞到數據庫,而且會多次重複生成緩存,甚至可能拖垮整個系統,這就叫緩存雪崩,和緩存穿透的區別在於,穿透是面對空值的情況,而雪崩是由於緩存重新生成的間隔期大量請求產生的連鎖效應。

既然是緩存更新時重複生成所導致的問題,那麼一種解法就是在緩存重新生成前給這個 KEY 加鎖,加鎖期間出現的請求都等待或返回默認值,而不去都嘗試重新生成緩存。

另一種方法是乾脆不要由客戶端請求來觸發緩存更新,而是由後臺腳本統一更新,同樣可以規避重複請求導致的重複生成。但是這就失去了只緩存熱點數據的能力,如果緩存因空間問題被清除了,也會因爲後臺沒及時更新導致查不到緩存數據,這就會要求更復雜的後臺更新策略,比如主動查詢緩存有效性、緩存被刪後通知後臺主動更新等。

雖說在有限的內存空間內最好緩存熱點數據,但如果數據過熱,比如微博的超級熱搜,也會導致緩存服務器壓力過大而崩潰,稱之爲緩存熱點問題。

可以複製多份緩存副本,來分散緩存服務器的單機壓力,畢竟堆機器是最簡單有效。此處也要注意,多個緩存副本不要設置相同的緩存過期時間,否則多處緩存同時過期,並同時更新,也容易引起緩存雪崩,應該設置一個時間範圍內的隨機值來更新緩存。

2.1、計算高性能

講完存儲高性能,再講計算高性能,計算性能的優化可以先從單機性能優化開始,多進程、多線程、IO 多路複用、異步 IO 等都存在很多可以優化的地方,但基本系統或框架已經提供了基本的優化能力,只需使用即可。

如果單機的性能優化已經到了瓶頸,無法應對業務的增長,就會開始增加服務器,構建集羣。對於計算來說,每一臺服務器接到同樣的輸入,都應該返回同樣的輸出,當服務器從單臺變成多臺之後,就會面臨請求來了要由哪一臺服務器處理的問題,我們當然希望由當前比較空閒的服務器去處理新的請求,這裏對請求任務的處理分配問題,就叫負載均衡

負載均衡的策略,從分類上來說,可以分爲三類:

一般來說,DNS 負載均衡用於實現地理級別的負載均衡;硬件負載均衡用於實現集羣級別的負載均衡;軟件負載均衡用於實現機器級別的負載均衡。

所以部署起來可以按照這三層去部署,第一層通過 DNS 將請求分發到北京、上海、深圳的機房;第二層通過硬件負載均衡將請求分發到當地三個集羣中的一個;第三層通過軟件策略將請求分發到具體的某臺服務器去響應業務。

就負載均衡算法來說,多是我們很熟悉的算法,如輪詢、加權輪詢、負載最低優先、性能最優優先、Hash 分配等,各有特點,按需採用即可。

3、高可用架構模式

3.1、理論方法

在說高可用之前,先來說說 CAP 理論,即:

在一個分佈式系統(指互相連接並共享數據的節點的集合)中,當涉及讀寫操作時,只能保證一致性(Consistence)、可用性(Availability)、分區容錯性(Partition Tolerance)三者中的兩個,另外一個必須被犧牲。

大家可能都知道 CAP 定理是什麼,但大家可能不知道,CAP 定理的作者(Seth Gilbert & Nancy Lynch)其實並沒有詳細解釋 CAP 三個單詞的具體含義,目前大家熟悉的解釋其實是另一個人(Robert Greiner)給出的。而且他還給出了兩版有所差異的解釋。

第二版解釋算是對第一版解釋的加強,他要加強的點主要是:

在分佈式系統下,P(分區容忍)是必須選擇的,否則當分區後系統無法履行職責時,爲了保障 C(一致性),就要拒絕寫入數據,也就是不可用了。

在此基礎上,其實我們能選擇的只有 C+P 或者 A+P,根據業務特性來選擇要優先保障一致性還是可用性。

在選擇保障策略時,有幾個需要注意的點:

伴隨 CAP 的一個退而求其次,也更現實的追求,是 BASE 理論,即基本可用,保障核心業務的可用性;軟狀態,允許系統存在數據不一致的中間狀態;最終一致性,一段時間後系統應該達到一致。

要保障高可用,怎麼下手呢?俗話說知己知彼纔能有的放矢,因此做高可用的前提是瞭解系統存在怎樣的風險,並且還要識別出風險的優先級,先治理更可能發生的、影響更大的風險。說得簡單,到底怎麼做?業界其實已經提供了排查系統風險的基本方法論,即 FMEA(Failure mode and effects analysis)——故障模式與影響分析。

FMEA 的基本思路是,面對初始的架構設計圖,考慮假設其中某個部件發生故障,對系統會造成什麼影響,進而判斷架構是否需要優化。

具體來說,需要畫一張表,按照如下步驟逐個列出:

基於上面這套方法論,可以有效地對系統的風險進行梳理,找出需要優先解決的風險點,從而提高系統的可用性。

除了 FMEA,其實還有一種應用更廣泛的風險分析和治理的理論,即 BCP——業務連續性計劃,它是一套基於業務規律的規章流程,保障業務或組織在面對突發狀況時其關鍵業務功能可以持續不中斷。

相比 FMEA,BCP 除了評估風險及重要程度,還要求詳細地描述應對方案、殘餘風險、災備恢復方案,並要求進行相應故障的培訓和演習安排,盡最大努力保障業務連續性。

知道風險在哪,優先治理何種風險之後,就可以着手優化架構。和高性能架構模式一樣,高可用架構也可以從存儲和計算兩個方面來分析。

3.2、存儲高可用

存儲高可用的本質都是通過將數據複製到多個存儲設備,通過數據冗餘的方式來提高可用性。

讓我們先從簡單的增加一臺機器開始,即雙機架構

當機器變成兩臺後,根據兩臺機器擔任的角色不同,就會分成不同的策略,比如主備、主從、主主。

主備複製的架構是指一臺機器作爲客戶端訪問的主機,另一臺機器純粹作爲冗餘備份用,當主機沒有故障時,備機不會被客戶端訪問到,僅僅需要從主機同步數據。這種策略很簡單,可以應對主機故障情況下的業務可用性問題,但在平常無法分擔主機的讀寫壓力,有點浪費。

主從複製的架構和主備複製的差別在於,從機除了複製備份數據,還需要幹活,即還需要承擔一部分的客戶端請求(一般是分擔讀操作)。當主機故障時,從機的讀操作不會受到影響,但需要增加讀操作的請求分發策略,且和主備不同,由於從機直接提供數據讀,如果主從複製延遲大,數據不一致會對業務造成更明顯的影響。

對於主備和主從兩種策略,如果主機故障,都需要讓另一臺機器變成主機,才能繼續完整地提供服務,如果全靠人工干預來切換,會比較滯後和易錯,最好是能夠自動完成切換,這就涉及雙機切換的策略。

在考慮雙機切換時,要考慮什麼?首先是需要感知機器的狀態,是兩臺機器直連傳遞互相的狀態,還是都傳遞給第三方來仲裁?所謂狀態要包含哪些內容才能定義一臺主機是故障呢?是發現一次問題就切換還是多觀察一會再切換?切換後如果主機恢復了是切換回來還是自動變備機呢?需不需要人工二次確認一下?

這些問題可能都得根據業務的特性來得出答案,此處僅給出三種常見的雙機切換模式:

最後一種雙機架構是主主複製,和前面兩種只有一主的策略不同,這次兩臺都是主機,客戶端的請求可以達到任何一臺主機,不存在切換主機的問題。但這對數據的設計就有了嚴格的要求,如果存在唯一 ID、嚴格的庫存數量等數據,就無法適用,這種策略適合那些偏臨時性、可丟失、可覆蓋的數據場景。

採用雙機架構的前提是一臺主機能夠存儲所有的業務數據並處理所有的業務請求,但機器的存儲和處理能力是有上限的,在大數據場景下就需要多臺服務器來構成數據集羣

如果是因爲處理能力達到瓶頸,此時可以增加從機幫主機分擔壓力,即一主多從,稱爲數據集中集羣。這種集羣方式需要任務分配算法將請求分散到不同機器上去,主要的問題在於數據需要複製到多臺從機,數據的一致性保障會比一主一從更爲複雜。且當主機故障時,多臺從機協商新主機的策略也會變得複雜。這裏有開源的 zookeeper ZAB 算法可以直接參考。

如果是因爲存儲量級達到瓶頸,此時可以將數據分散存儲到不同服務器,每臺服務器負責存儲一部分數據,同時也備份一部分數據,稱爲數據分散集羣。數據分散集羣同樣需要做負載均衡,在數據分區的分配上,hadoop 採用獨立服務器負責數據分區的分配,ES 集羣通過選舉一臺服務器來做數據分區的分配。除了負載均衡,還需要支持擴縮容,此外由於數據是分散存儲的,當部分服務器故障時,要能夠將故障服務器的數據在其他服務器上恢復,並把原本分配到故障服務器的數據分配到其他正常的服務器上,即分區容錯性。

數據集羣可以在單臺乃至多臺服務器故障時依然保持業務可用,但如果因爲地理級災難導致整個集羣都故障了(斷網、火災等),那整個服務就不可用了。面對這種情況,就需要基於不同地理位置做數據分區

做不同地理位置的數據分區,首先要根據業務特性制定分區規則,大多還是按照地理位置提供的服務去做數據分區,比如中國區主要存儲中國用戶的數據。

既然分區是爲了防災,那麼一個分區肯定不止存儲自身的數據,還需要做數據備份。從數據備份的策略來說,主要有三種模式:

3.3、計算高可用

從存儲高可用的思路可以看出,高可用主要是通過增加機器冗餘來實現備份,對計算高可用來說也是如此。通過增加機器,分擔服務器的壓力,並在單機發生故障的時候將請求分配到其他機器來保障業務可用性。

因此計算高可用的複雜性也主要是在多機器下任務分配的問題,比如當任務來臨(比如客戶端請求到來)時,如何選擇執行任務的服務器?如果任務執行失敗,如何重新分配呢?這裏又可以回到前文說過的負載均衡相關的解法上。

計算服務器和存儲服務器在多機器情況下的架構是類似的,也分爲主備、主從和集羣。

主備架構下,備機僅僅用作冗餘,平常不會接收到客戶端請求,當主機故障時,備機纔會升級爲主機提供服務。備機分爲冷備和溫備。冷備是指備機只准備好程序包和配置文件,但實際平常並不會啓動系統。溫備是指備機的系統是持續啓動的,只是不對外提供服務,從而可以隨時切換主機。

主從架構下,從機也要執行任務,由任務分配器按照預先定義的規則將任務分配給主機和從機。相比起主備,主從可以發揮一定的從機性能,避免成本空費,但任務的分配就變得複雜一些。

集羣架構又分爲對稱集羣和非對稱集羣。

對稱集羣也叫負載均衡集羣,其中所有的服務器都是同等對待的,任務會均衡地分配到每臺服務器。此時可以採用隨機、輪詢、Hash 等簡單的分配機制,如果某臺服務器故障,不再給他分配任務即可。

非對稱集羣下不同的服務器有不同的角色,比如分爲 master 和 slave。此時任務分配器需要有一定的規則將任務分配給不同角色的服務器,還需要有選舉策略來在 master 故障時選擇新的 master。這個選舉策略的複雜度就豐儉由人了。

講存儲高可用已經說過數據分區,計算高可用也有類似的高可用保障思路,歸納來說,它們都可以根據需要做異地多活,來提高整體的處理能力,並防範地區級的災難。異地多活中的”異地 “,就是指集羣部署到不同的地理位置,“活” 則強調集羣是隨時能提供服務的,不同於 “備” 還需要一個切換過程。

按照規模,異地多活可以分爲同城異區、跨城異地和跨國異地。顯而易見,不同模式下能夠應對的地區級故障是越來越高的,但同樣的,距離越遠,通信成本與延遲就越高,對通信通道可用性的挑戰也越高。因此跨城異地已經不適合對數據一致性要求非常高的業務,而跨國異地往往是用來給不同國家的用戶提供不同服務的。

由於異地多活需要花費很高的成本,極大地增加系統複雜度,因此在設計異地多活架構時,可以不用強求爲所有業務都做異地多活,可以優先爲核心業務實現異地多活。儘量保障絕大部分用戶的異地多活,對於沒能保障的用戶,通過掛公告、事後補償、完善失敗提示等措施進行安撫、提升體驗。畢竟要做到 100% 可用性是不可能的,只能在能接受的成本下儘量逼近,所以當可用性達到一定瓶頸後,補償手段的成本或許更低。

在異地部署的情況下,數據一定會冗餘存儲,物理上就無法實現絕對的實時同步,且距離越遠對數據一致性的挑戰越大,雖然可以靠減少距離、搭建高速專用網絡等方式來提高一致性,但也只是提高而已,因此大部分情況下, 只需考慮保障業務能接受範圍下的最終一致性即可。

在同步數據的時候,可以採用多種方式,比如通過消息隊列同步、利用數據庫自帶的同步機制同步、

通過換機房重試來解決同步延遲問題、通過 session id 讓同一數據的請求都到同一機房從而不用同步等。

可見,整個異地多活的設計步驟首先是對業務分級,挑選出核心業務做異地多活,然後對需要做異地多活的數據進行特徵分析,考慮數據量、唯一性、實時性要求、可丟失性、可恢復性等,根據數據特性設計數據同步的方案。最後考慮各種異常情況下的處理手段,比如多通道同步、日誌記錄恢復、用戶補償等,此時可以借用前文所說的 FMEA 等方法進行分析。

前面討論的都是較爲宏觀的服務器、分區級的故障發生時該怎麼辦,實際上在平常的開發中,還應該防微杜漸,從接口粒度的角度,來防範和應對接口級的故障。應對的核心思路依然是優先保障核心業務和絕大部分用戶可用。

對於接口級故障,有幾個常用的方法:限流、排隊、降級、熔斷。其中限流和排隊屬於事前防範的措施,而降級和熔斷屬於接口真的故障後的處理手段。

限流的目的在於控制接口的訪問量,避免被高頻訪問沖垮。

從限流維度來說,可以基於請求限流,即限制某個指標下、某個時間段內的請求數量,閾值的定義需要基於壓測和線上情況來逐步調優。還可以基於資源限流,比如根據連接數、文件句柄、線程數等,這種維度更適合特殊的業務。

實現限流常用的有時間窗算法和桶算法。

時間窗算法分爲固定時間窗和滑動時間窗。

固定時間窗通過統計固定時間週期內的量級來決定限流,但存在一個臨界點的問題,如果在兩個時間窗的中間發生超大流量,而在兩個時間窗內都各自沒有超出限制,就會出現無法被限流攔截的接口故障。因此滑動時間窗採用了部分重疊的時間統計週期來解決臨界點問題。

桶算法分爲漏桶和令牌桶。

漏桶算法是將請求放入桶中,處理單元從桶裏拿請求去進行處理,如果桶堆滿了就丟棄掉新的請求,可以理解爲桶下面有個漏斗將請求往處理單元流動,整個桶的容量是有限的。這種模式下流入的速率取決於請求的頻率,當桶內有堆積的待處理請求時,流出速率是勻速的。漏桶算法適用於瞬時高併發的場景(如秒殺),處理可能慢一點,但可以緩存部分請求不丟棄。

令牌桶算法是在桶內放令牌,令牌數是有限的,新的請求需要先到桶裏拿到令牌才能被處理,拿不到就會被丟棄。和漏桶勻速流出處理不同,令牌桶還能通過控制放令牌的速率來控制接收新請求的頻率,對於突發流量,可靠累計的令牌來處理,但是相對的處理速度也會突增。令牌桶算法適用於控制第三方服務訪問速度的場景,防止壓垮下游。

除了限流,還有一種控制處理速度的方法就是排隊。當新請求到來後先加入隊列,出隊端通過固定速度出隊處理請求,避免處理單元壓力過大。隊列也有長度限制,其機制和漏桶算法差不多。

如果真的事前防範真的被突破了,接口很可能或已經發生了故障,還能做什麼呢?

一種手段是熔斷,即當處理量達到閾值,就主動停掉外部接口的訪問能力,這其實也是一種防範措施,對外的表現雖然是接口訪問故障,但系統內部得以被保護,不會引起更大的問題,待存量處理被消化完,或者外部請求減弱,或完成擴容後,再開放接口。熔斷的設計主要是閾值,需要按照業務特點和統計數據制定。

當接口故障後(無論是被動還是主動斷開),最好能提供降級策略。降級是丟車保帥,放棄一下非核心業務,保障核心業務可用,或者最低程度能提供故障公告,讓用戶不要反覆嘗試請求來加重問題了。比起手動降級,更好的做法也是自動降級,需要具備檢測和發現降級時機的機制。

4、可擴展架構模式

再回顧一遍互聯網行業的金科玉律:只有變化纔是不變的。在設計架構時,一開始就要抱着業務隨時可能變動導致架構也要跟着變動的思想準備去設計,差別只在於變化的快慢而已。因此在設計架構時一定是要考慮可擴展性的。

在思考怎樣纔是可擴展的時候,先想一想平常開發中什麼情況下會覺得擴展性不好?大都是因爲系統龐大、耦合嚴重、牽一髮而動全身。因此對可擴展架構設計來說,基本的思想就是拆分

拆分也有多種指導思想,如果面向業務流程來談拆分,就是分層架構;如果面向系統服務來談拆分,就是 SOA、微服務架構;如果面向系統功能來拆分,就是微內核架構。

分層架構是我們最熟悉的,因爲互聯網業務下,已經很少有純單機的服務,因此至少都是 C/S 架構、B/S 架構,也就是至少也分爲了客戶端 / 瀏覽器和後臺服務器這兩層。如果進一步拆分,就會將後臺服務基於職責進行自頂向下的劃分,比如分爲接入層、應用層、邏輯層、領域層等。

分層的目的當然是爲了讓各個層次間的服務減少耦合,方便進行各自範疇下的優化,因此需要保證各層級間的差異是足夠清晰、邊界足夠明顯的,否則當要增加新功能的時候就會不知道該放到哪一層。各個層只處理本層邏輯,隔離關注點。

額外需注意的是一旦確定了分層,請求就必須層層傳遞,不能跳層,這是爲了避免架構混亂,增加維護和擴展的複雜度,比如爲了方便直接跨層從接入層調用領域層查詢數據,當需要進行統一的邏輯處理時,就無法切面處理所有請求了。

SOA 架構更多出現在傳統企業中,其主要解決的問題是企業中 IT 建設重複且效率低下,各部門自行接入獨立的 IT 系統,彼此之間架構、協議都不同,爲了讓各個系統的服務能夠協調工作,SOA 架構應運而生。

其有三個關鍵概念:服務、ESB 和松耦合。

服務是指各個業務功能,比如原本各部門原本的系統提供的服務,可大可小。由於各服務之間無法直接通信,因此需要 ESB,即企業服務總線進行對接,它將不同服務連接在一起,屏蔽各個服務的不同接口標準,類似計算機中的總線。松耦合是指各個服務的依賴需要儘量少,否則某個服務升級後整個系統無法使用就麻煩了。

這裏也可以看出,ESB 作爲總線,爲了對接各個服務,要處理各種不同的協議,其協議轉換耗費了大量的性能,會成爲整個系統的瓶頸。

微服務是近幾年最耳熟能詳的架構,其實它和 SOA 有一些相同之處,比如都是將各個服務拆分開來提供能力。但是和 SOA 也有一些本質的區別,微服務是沒有 ESB 的,其通信協議是一致的,因此通信管道僅僅做消息的傳遞,不理解內容和格式,也就沒有 ESB 的問題。而且爲了快速交付、迭代,其服務的粒度會劃分地更細,對自動化部署能力也就要求更高,否則部署成本太大,達不到輕量快速的目的。

當然微服務雖然很火,但也不是解決所有問題的銀彈,它也會有一些問題存在。如果服務劃分的太細,那麼互相之間的依賴關係就會變得特別複雜,服務數量、接口量、部署量過多,團隊的效率可能大降,如果沒有自動化支撐,交付效率會很低。由於調用鏈太長(多個服務),因此性能也會下降,問題定位會更困難,如果沒有服務治理的能力,管理起來會很混亂,不知道每個服務的情況如何。

因此如何拆分服務就成了每個使用微服務架構的團隊的重要考量點。這裏也提供一些拆分的思路:

微服務架構如果沒有完善的基礎設施保障服務治理,那麼也會帶來很多問題,降低效率,因此根據團隊和業務的規模,可以按以下優先級進行基礎設施的支持:

  1. 優先支持服務發現、服務路由、服務容錯(重試、流控、隔離),這些是微服務的基礎。

  2. 接着支持接口框架(統一的協議格式與規範)、API 網關(接入鑑權、權限控制、傳輸加密、請求路由等),可以提高開發效率。

  3. 然後支持自動化部署、自動化測試能力,並搭建配置中心,可以提升測試和運維的效率。

  4. 最後支持服務監控、服務跟蹤、服務安全(接入安全、數據安全、傳輸安全、配置化安全策略等)的能力,可以進一步提高運維效率。

最後說說微內核架構,也叫插件化架構,顧名思義,是面向功能拆分的,通常包含核心系統和插件模塊。在微內核架構中,核心系統需要支持插件的管理和鏈接,即如何加載插件,何時加載插件,插件如何新增和操作,插件如何和核心引擎通信等。

舉一個最常見的微內核架構的例子——規則引擎,在這個架構中,引擎是內核,負責解析規則,並將輸入通過規則處理後得到輸出。而各種規則則是插件,通常根據各種業務場景進行配置,存儲到數據庫中。

5、總結

人們通常把某項互聯網業務的發展分爲四個時期:初創期、發展期、競爭期和成熟期。

在初創期通常求快,系統能買就買,能用開源就用開源,能用的就是好的,先要活下來;到了發展期開始堆功能和優化,要求能快速實現需求,並有餘力對一些系統的問題進行優化,當優化到頂的時候就需要從架構層面來拆分優化了;進入競爭期後,經過發展期的快速迭代,可能會存在很多重複造輪子和混亂的交互,此時就需要通過平臺化、服務化來解決一些公共的問題;最後到達成熟期後,主要在於補齊短板,優化弱項,保障系統的穩定。

在整個發展的過程中,同一個功能的前後要求也是不同的,隨着用戶規模的增加,性能會越來越難保障,可用性問題的影響也會越來越大,因此複雜度就來了。

對於架構師來說,首要的任務是從當前系統的一大堆紛繁複雜的問題中識別出真正要通過架構重構來解決的問題,集中力量快速突破,但整體來說,要徐徐圖之,不要想着用重構來一次性解決所有問題。

對項目中的問題做好分類,劃分優先級,先易後難,才更容易通過較少的資源佔用,較快地得到成果,提高士氣。然後再循序漸進,每個階段控制在 1~3 個月,穩步推進。

當然,在這個過程中,免不了和上下游團隊溝通協作,需要注意的是自己的目標和其他團隊的目標可能是不同的,需要對重構的價值進行換位思考,讓雙方都可以合作共贏,才能借力前進。

還是回到開頭的那句話,架構設計的主要目的是爲了解決軟件系統複雜度帶來的問題。首先找到主要矛盾在哪,做到有的放矢,然後再結合知識、經驗進行設計,去解決面前的問題。

祝人人都成爲一名合格的架構師。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/JZNVylu602KXPKsUUO-9zg