高可用系統不追求「零故障」,而是追求「故障發生時核心功能不受影響或秒級恢復」。本文基於實戰經驗,將故障場景劃分為「非關鍵路徑故障波及關鍵路徑」與「關鍵路徑自身故障」兩大類,給出四類典型 Case 與解決方案,總結出四大通用保障措施:監測報警、隔離、簡化依賴、優雅降級。
| Case | 場景描述 | 根因 | 解決方案 |
|---|---|---|---|
| I | A/B 非關鍵系統與 C 關鍵系統共用 D 系統。A 故障瘋狂請求 D,擠佔 C 資源 | 共享資源無隔離,單點擠佔 | 部署期路由隔離:A/B 訪問 D 集群子集,C 訪問另一子集;運行期熔斷開關:故障時關閉 A/B 訪問 D,資源全留給 C |
| II | B 調用 D 時單請求資源消耗極大,B 流量微增即擠垮 C | 資源消耗不均,無配額控制 | 同 Case I:路由隔離 + 配額限流 |
| III | 單系統 E 內部:非關鍵路徑處理慢佔滿線程池/連接池/CPU/內存,關鍵路徑無資源可用 | 進程內資源共享無隔離 | 功能開關降級:Servlet 開關動態關閉非關鍵功能(James Hamilton「Graceful Degrade」) |
| IV | 誤判依賴:以為非關鍵的系統實則被關鍵路徑強依賴 | 依賴治理缺失 | 依賴分級管理:嚴格界定關鍵路徑依賴邊界;架構簡化:減少關鍵路徑依賴鏈長度 |
| Case | 場景描述 | 解決方案 |
|---|---|---|
| I | 關鍵系統單機部署,機器故障(硬體/網路)導致核心功能不可用 | 冷備或叢集化(多實例 + 健康檢查 + 自動切換) |
| II | 關鍵系統單機房部署,機房級故障(斷電/斷纜/火災) | 多機房災備(異地多活/冷備),解決數據同步、流量切換、一致性等技術挑戰 |
| III | A/B 均為關鍵系統,A 調用 B。B 處理變慢 → A 線程池/連接池耗盡 → 甚至 OOM 進程退出 | 超時必設且不可過長;非必要不等待(快速失敗/拒絕);等待隊列長度硬限制(防止雪崩) |
| IV | 流量超過系統極限承載能力 | 流量控制(令牌桶/漏桶/熔斷);容量規劃:壓測得極限,水位 70% 即擴容/啟動保護 |
硬體 LB / 地址列表訪問下游叢集,中間層掛掉即全軍覆沒。需準備:
高可用不是堆疊硬體,而是系統性地在架構、部署、運維、代碼四個層面預埋「隔離艙壁」與「救生艇」。
對於構建高可用的系統而言,都希望盡可能的避免故障,但通常來說故障是不可避免的,要盡可能做到的應該是在故障出現時能快速的屏蔽故障對核心功能的影響或快速修復,在這篇blog中,來分析下該如何更好的面對程序故障(這裡就不討論人工操作造成的狀況),保障系統的高可用,由於這些更多的來自自己對廠內的經驗的總結,必然會有一定的狹隘性,希望大家多多拍磚。
所有的系統必然都有其核心功能,我們把核心功能相關的操作稱為關鍵路徑,其他的功能稱為非關鍵路徑,對於系統而言,通常只有關鍵路徑故障了,我們才認為其不可用,以下為造成關鍵路徑不可用的一些場景和我們的應對方法:
1、非關鍵路徑的故障
非關鍵路徑的故障為什麼會造成關鍵路徑的故障呢,我們看看一些典型的場景:
Case I
A、B兩個系統為非關鍵路徑的系統,C為關鍵路徑的系統,A、B、C都依賴了D系統,A系統出現故障,導致了對D系統的訪問太多,由於D系統的總體處理能力是有限的,導致D系統處理不過來C系統的請求了,從而出現故障。
Solution
對於這樣的狀況,我們選擇的方法是在部署期間通過路由將關鍵路徑的系統和非關鍵路徑的系統隔離開,例如A/B訪問的是D集群中的一組機器,C訪問的是D集群中的另外一組機器。
同時還提供了D關閉某訪問者的功能的支持,這樣可以在未做隔離的情況下,一旦出現故障,可以考慮先將資源都提供給關鍵路徑的系統使用,從而臨時避免故障。
可能有些同學會提到干脆把提供給A/B的不同接口拆分出了做單獨的系統得了,這在很多時候是不太好做的,一方面會帶來很高的維護成本,另一方面很多時候有互相依賴的問題。
Case II
還是上面的場景,不同的是B系統調用D系統時,D系統在處理B的請求時需要消耗較多的資源,這樣當B系統對D系統的訪問稍微多一點後就導致了D系統處理C系統的請求的資源必然也將下降,從而出現故障。
Solution
對於這樣的狀況,我們選擇的方法同樣是通過路由來進行隔離。
Case III
E系統上同時有關鍵路徑的功能和非關鍵路徑的功能,出現了非關鍵路徑耗資源導致關鍵路徑資源不夠用,從而出現故障,例如一個web應用,用於處理請求的線程數必然是有限的,如果非關鍵路徑處理慢了,導致線程消耗多了,這樣關鍵路徑能用來處理的線程數必然也就少了,因此就故障了,另外的例子還有連接池、CPU、Memory、IO等資源。
Solution
對於這樣的狀況,我們選擇的方法是采用開關的方式來控制非關鍵路徑,例如一旦非關鍵路徑處理慢並影響到關鍵路徑了,我們就關閉此非關鍵路徑的功能,實現方式其實非常土,但非常有效,就是提供一個servlet,通過傳入一些參數來打開或關閉某些功能,這招在很多時候都非常有效,也就是James Hamilton在他那篇著名的論文中說到的Graceful Degrade。
還有一種在大型系統裡也很容易出現,就是你認為的非關鍵路徑其實成為了關鍵路徑,這種現象就很杯具了,對於這種現象,只能說必須管理好依賴,一旦沒管理好,就將會出現看似一個不相關的系統出問題,核心功能也受影響了,另外一方面就是要簡化系統,盡可能讓關鍵路徑不要出現太多的依賴,否則就意味著關鍵路徑的穩定性取決於眾多的依賴,那就痛苦了。
由於非關鍵路徑不是系統的核心功能,因此我們應該盡可能做到非關鍵路徑不影響到關鍵路徑。
2、關鍵路徑上的故障
關鍵路徑上的故障同樣也分成很多種,有些是只能通過修改代碼來fix的,例如程序bug導致關鍵路徑執行出錯等,對於這類故障通常來說只能靠提高代碼質量來保證,在此就不討論了,而有些關鍵路徑上的故障還是有辦法來應對的,來看看一些典型的場景:
Case I
關鍵路徑的系統部署在一台機器上,機器出現故障後(硬件、網絡等)導致核心功能出現故障。
Solution
對於這種狀況,要麼采取冷備,要麼就是集群了。
Case II
關鍵路徑的系統部署在一個機房,當機房出現故障後導致核心功能出現故障。
Solution
對於這種狀況,就需要做多機房的容災了,當然,這會帶來其他很多的技術挑戰。
Case III
A/B兩系統均為關鍵路徑,A需要調用B系統,B系統處理變慢,導致A系統請求處理線程耗光,從而出現嚴重故障,類似的狀況還有連接池,A系統依賴數據庫,數據庫的訪問突然變慢,導致A系統很多請求在等待連接池,有些時候可能會因此導致啟動的線程太多,最終內存消耗完畢,進程退出,這是最悲慘的狀況,這是典型的慢現象造成的嚴重後果。
Solution
對於這種狀況,我們選擇的方法是一方面是超時時間設置的不會太長,另外一方面是在能不等待的情況就不等待,而直接拒絕,但像連接池這類的情況,通常也不太好直接采用拒絕的方式,因此我們選擇的是控制等待的線程個數,避免影響太多線程,當然,這不一定能挽救故障,但對恢復而言會有很大的幫助。
Case IV
請求的流量超過了系統所能支撐的量,從而導致故障。
Solution
對於這種狀況,我們選擇的方法一方面是控制請求的數量,另一方面是容量規劃,評估好系統能夠支撐的極限量,但接近極限量時即進行擴容或啟動保護措施。
集群場景中還會出現個別很高危的應用,例如A訪問集群B時,必然要走中間的硬件負載設備或通過地址列表的方式去訪問,這時如中間這個高危點出現故障,就得有一些簡單有效的挽救方法。
總結一下為了保障系統的高可用,通常可采取的措施:
1、監測 & 報警
監測和報警有助於幫助你立刻知道故障,在做的好的情況下甚至可以做到提前知道故障要發生,怎麼樣做監測最好呢,當然,實現一套這樣的系統也不容易,maybe scribe可以看看,系統的開發人員做這個才能做的好,因為只有熟悉系統細節的人才知道到底要監測什麼和什麼情況下要報警,才能保證關鍵功能是正常運轉的。
2、隔離
采用簡單有效的辦法隔離開核心的功能以及其他功能所依賴的共同資源。
3、簡化系統
系統依賴越多,就意味著系統要保障可用性就需要所有依賴的系統都保持穩定,因此要盡可能的減少系統的依賴。
4、優雅降級
在系統中留好一些控制開關,尤其是對外部依賴的點,以便在資源不夠時更簡單的將資源讓出給核心功能使用,在促銷類的活動中這招尤其重要。