架構設計中如何應對接口級故障?
在實際業務運行過程中,有一種故障影響可能沒有那麼大,但發生的概率較高,這就是今天聊的接口級的故障。
接口級故障的典型表現就是,系統並沒有宕機、網絡也沒有中斷,但業務卻出現問題了,例如業務響應緩慢、大量訪問超時和大量訪問出現異常(給用戶彈出提示 “無法連接數據庫”)。
這類問題的主要原因在於系統壓力太大、負載太高,導致無法快速處理業務請求,由此引發更多的後續問題。最常見的情況就是,數據庫慢查詢將數據庫的服務器資源耗盡,導致讀寫超時,業務讀寫數據庫時要麼無法連接數據庫、要麼超時,最終用戶看到的現象就是訪問很慢,一會兒訪問拋出異常,一會兒訪問又是正常結果。
如果進一步探究,導致接口級故障的原因可以分爲兩大類:
-
內部原因:包括程序 bug 導致死循環,某個接口導致數據庫慢查詢,程序邏輯不完善導致耗盡內存等。
-
外部原因:包括黑客攻擊,促銷或者搶購引入了超出平時幾倍甚至幾十倍的用戶,第三方系統大量請求,第三方系統響應緩慢等。
解決接口級故障的核心思想和異地多活基本類似,都是優先保證核心業務和優先保證絕大部分用戶。常見的應對方法有四種,降級、熔斷、限流和排隊,下面我會一一講解。
- 降級
降級指系統將某些業務或者接口的功能降低,可以是隻提供部分功能,也可以是完全停掉所有功能。
例如,論壇可以降級爲只能看帖子,不能發帖子;也可以降級爲只能看帖子和評論,不能發評論;而 App 的日誌上傳接口,可以完全停掉一段時間,這段時間內 App 都不能上傳日誌。
降級的核心思想就是丟車保帥,優先保證核心業務。
例如,對於論壇來說,90% 的流量是看帖子,那我們就優先保證看帖的功能;對於一個 App 來說,日誌上傳接口只是一個輔助的功能,故障時完全可以停掉。
常見的實現降級的方式有兩種:
1.1 系統後門降級
簡單來說,就是系統預留了後門用於降級操作。例如,系統提供一個降級 URL,當訪問這個 URL 時,就相當於執行降級指令,具體的降級指令通過 URL 的參數傳入即可。這種方案有一定的安全隱患,所以也會在 URL 中加入密碼這類安全措施。
系統後門降級的方式實現成本低,但主要缺點是如果服務器數量多,需要一臺一臺去操作,效率比較低,這在故障處理爭分奪秒的場景下是比較浪費時間的。
1.2 獨立降級系統
爲了解決系統後門降級方式的缺點,我們可以將降級操作獨立到一個單獨的系統中,實現複雜的權限管理、批量操作等功能。
其基本架構如下:
- 熔斷
熔斷是指按照規則停掉外部接口的訪問,防止某些外部接口故障導致自己的系統處理能力急劇下降或者出故障。
熔斷和降級是兩個比較容易混淆的概念,因爲單純從名字上看,好像都有禁止某個功能的意思。但它們的內涵是不同的,因爲降級的目的是應對系統自身的故障,而熔斷的目的是應對依賴的外部系統故障的情況。
假設一個這樣的場景:A 服務的 X 功能依賴 B 服務的某個接口,當 B 服務的接口響應很慢的時候,A 服務的 X 功能響應肯定也會被拖慢,進一步導致 A 服務的線程都被卡在 X 功能處理上,於是 A 服務的其他功能都會被卡住或者響應非常慢。
這時就需要熔斷機制了:A 服務不再請求 B 服務的這個接口,A 服務內部只要發現是請求 B 服務的這個接口就立即返回錯誤,從而避免 A 服務整個被拖慢甚至拖死。
實現熔斷機制有兩個關鍵點:
一是需要有一個統一的 API 調用層,由 API 調用層來進行採樣或者統計。如果接口調用散落在代碼各處,就沒法進行統一處理了。
二是閾值的設計,例如 1 分鐘內 30% 的請求響應時間超過 1 秒就熔斷,這個策略中的 “1 分鐘”“30%”“1 秒” 都對最終的熔斷效果有影響。實踐中,一般都是先根據分析確定閾值,然後上線觀察效果,再進行調優。
- 限流
降級是從系統功能優先級的角度考慮如何應對故障,而限流則是從用戶訪問壓力的角度來考慮如何應對故障。限流指只允許系統能夠承受的訪問量進來,超出系統訪問能力的請求將被丟棄。
雖然 “丟棄” 這個詞聽起來讓人不太舒服,但保證一部分請求能夠正常響應,總比全部請求都不能響應要好得多。
限流一般都是系統內實現的,常見的限流方式可以分爲兩類:基於請求限流和基於資源限流。
3.1 基於請求限流
基於請求限流指從外部訪問的請求角度考慮限流,常見的方式有兩種。
第一種是限制總量,也就是限制某個指標的累積上限,常見的是限制當前系統服務的用戶總量,例如:某個直播間限制總用戶數上限爲 100 萬,超過 100 萬後新的用戶無法進入;某個搶購活動商品數量只有 100 個,限制參與搶購的用戶上限爲 1 萬個,1 萬以後的用戶直接拒絕。
第二種是限制時間量,也就是限制一段時間內某個指標的上限,例如 1 分鐘內只允許 10000 個用戶訪問;每秒請求峯值最高爲 10 萬。
無論是限制總量還是限制時間量,共同的特點都是實現簡單,但在實踐中面臨的主要問題是比較難以找到合適的閾值。例如系統設定了 1 分鐘 10000 個用戶,但實際上 6000 個用戶的時候系統就扛不住了;或者達到 1 分鐘 10000 用戶後,其實系統壓力還不大,但此時已經開始丟棄用戶訪問了。
即使找到了合適的閾值,基於請求限流還面臨硬件相關的問題。例如一臺 32 核的機器和 64 核的機器處理能力差別很大,閾值是不同的,可能有的技術人員以爲簡單根據硬件指標進行數學運算就可以得出來,實際上這樣是不可行的,64 核的機器比 32 核的機器,業務處理性能並不是 2 倍的關係,可能是 1.5 倍,甚至可能是 1.1 倍。
爲了找到合理的閾值,通常情況下可以採用性能壓測來確定閾值,但性能壓測也存在覆蓋場景有限的問題,可能出現某個性能壓測沒有覆蓋的功能導致系統壓力很大;另外一種方式是逐步優化:先設定一個閾值然後上線觀察運行情況,發現不合理就調整閾值。
基於上述的分析,根據閾值來限制訪問量的方式更多的適應於業務功能比較簡單的系統,例如負載均衡系統、網關係統、搶購系統等。
3.2 基於資源限流
基於請求限流是從系統外部考慮的,而基於資源限流是從系統內部考慮的,也就是找到系統內部影響性能的關鍵資源,對其使用上限進行限制。常見的內部資源包括連接數、文件句柄、線程數和請求隊列等。
例如,採用 Netty 來實現服務器,每個進來的請求都先放入一個隊列,業務線程再從隊列讀取請求進行處理,隊列長度最大值爲 10000,隊列滿了就拒絕後面的請求;也可以根據 CPU 的負載或者佔用率進行限流,當 CPU 的佔用率超過 80% 的時候就開始拒絕新的請求。
基於資源限流相比基於請求限流能夠更加有效地反映當前系統的壓力,但實際設計時也面臨兩個主要的難點:如何確定關鍵資源,以及如何確定關鍵資源的閾值。
通常情況下,這也是一個逐步調優的過程:設計的時候先根據推斷選擇某個關鍵資源和閾值,然後測試驗證,再上線觀察,如果發現不合理,再進行優化。
限流算法
爲了更好地實現前面描述的各種限流方式,通常情況下我們會基於限流算法來設計方案。常見的限流算法有兩大類四小類,它們的實現原理和優缺點各不相同,在實際設計的時候需要根據業務場景來選擇。
(1)時間窗
第一大類是時間窗算法,它會限制一定時間窗口內的請求量或者資源消耗量,根據實現方式又可以細分爲 “固定時間窗” 和“滑動時間窗”。
- 固定時間窗
固定時間窗算法的實現原理是,統計固定時間週期內的請求量或者資源消耗量,超過限額就會啓動限流,如下圖所示:
它的優點是實現簡單,缺點是存在臨界點問題。例如上圖中的紅藍兩點只間隔了短短 10 秒,期間的請求數卻已經達到 200,超過了算法規定的限額(1 分鐘內處理 100)。但是因爲這些請求分別來自兩個統計窗口,從單個窗口來看還沒有超出限額,所以並不會啓動限流,結果可能導致系統因爲壓力過大而掛掉。
- 滑動時間窗
爲了解決臨界點問題,滑動時間窗算法應運而生,它的實現原理是,兩個統計週期部分重疊,從而避免短時間內的兩個統計點分屬不同的時間窗的情況,如下圖所示:
總體上來看,滑動時間窗的限流效果要比固定時間窗更好,但是實現也會稍微複雜一些。
(2)桶算法
第二大類是桶算法,用一個虛擬的 “桶” 來臨時存儲一些東西。根據桶裏面放的東西,又可以細分爲 “漏桶” 和“令牌桶”。
- 漏桶
漏桶算法的實現原理是,將請求放入 “桶”(消息隊列等),業務處理單元(線程、進程和應用等)從桶裏拿請求處理,桶滿則丟棄新的請求,如下圖所示:
我們可以看到漏桶算法的三個關鍵實現點:
-
流入速率不固定:可能瞬間流入非常多的請求,例如 0 點簽到、整點秒殺。
-
勻速 (極速) 流出:這是理解漏桶算法的關鍵,也就是說即使大量請求進入了漏桶,但是從漏桶流出的速度是勻速的,速度的最大值就是系統的極限處理速度(對應圖中的“極速”)。這樣就保證了系統在收到海量請求的時候不被壓垮,這是第一層的保護措施。需要注意的是:如果漏桶沒有堆積,那麼流出速度就等於流入速度,這個時候流出速度就不是勻速的。
-
桶滿則丟棄請求:這是第二層保護措施,也就是說漏桶不是無限容量,而是有限容量,例如漏桶最多存儲 100 萬個請求,桶滿了則直接丟棄後面的請求。
漏桶算法的技術本質是總量控制,桶大小是設計關鍵,具體的優缺點如下:
-
突發大量流量時丟棄的請求較少,因爲漏桶本身有緩存請求的作用。
-
桶大小動態調整比較困難(例如 Java BlockingQueue),需要不斷的嘗試才能找到符合業務需求的最佳桶大小。
-
無法精確控制流出速度,也就是業務的處理速度。
漏桶算法主要適用於瞬時高併發流量的場景(例如剛纔提到的 0 點簽到、整點秒殺等)。在短短几分鐘內湧入大量請求時,爲了更好的業務效果和用戶體驗,即使處理慢一些,也要做到儘量不丟棄用戶請求。
- 令牌桶算法
令牌桶算法和漏桶算法的不同之處在於,桶中放入的不是請求,而是 “令牌”,這個令牌就是業務處理前需要拿到的 “許可證”。也就是說,當系統收到一個請求時,先要到令牌桶裏面拿 “令牌”,拿到令牌才能進一步處理,拿不到就要丟棄請求。
它的實現原理是如下圖所示:
我們可以看到令牌桶算法的三個關鍵設計點:
-
有一個處理單元往桶裏面放令牌,放的速率是可以控制的。
-
桶裏面可以累積一定數量的令牌,當突發流量過來的時候,因爲桶裏面有累積的令牌,此時的業務處理速度會超過令牌放入的速度。
-
如果令牌不足,即使系統有能力處理,也會丟棄請求。
令牌桶算法的技術本質是速率控制,令牌產生的速率是設計關鍵,具體的優缺點如下:
-
可以動態調整處理速率,實現更加靈活。
-
突發大量流量的時候可能丟棄很多請求,因爲令牌桶不能累積太多令牌。
-
實現相對複雜。
令牌桶算法主要適用於兩種典型的場景,一種是需要控制訪問第三方服務的速度,防止把下游壓垮,例如支付寶需要控制訪問銀行接口的速率;另一種是需要控制自己的處理速度,防止過載,例如壓測結果顯示系統最大處理 TPS 是 100,那麼就可以用令牌桶來限制最大的處理速度。
剛纔介紹漏桶算法的時候我提到漏桶算法可以應對瞬時高併發流量,現在介紹令牌桶算法的時候,我又說令牌桶允許突發流量。
你可能會問,這兩種說法好像差不多啊,它們到底有什麼區別,到底誰更適合做秒殺呢?
其實,令牌桶的 “允許突發” 實際上只是“允許一定程度的突發”,比如系統處理能力是每秒 100 TPS,突發到 120 TPS 是可以的,但如果突發到 1000 TPS 的話,系統大概率就被壓垮了。所以處理秒殺時高併發流量,還是得用漏桶算法。
令牌桶的算法原本是用於網絡設備控制傳輸速度的,而且它控制的目的是保證一段時間內的平均傳輸速度。之所以說令牌桶適合突發流量,是指在網絡傳輸的時候,可以允許某段時間內(一般就幾秒)超過平均傳輸速率,這在網絡環境下常見的情況就是 “網絡抖動”。
但這個短時間的突發流量並不會導致雪崩效應,網絡設備也能夠處理得過來。對應到令牌桶應用到業務處理的場景,就要求即使有突發流量來了,系統自己或者下游系統要真的能夠處理的過來,否則令牌桶允許突發流量進來,結果系統或者下游處理不了,那還是會被壓垮。
因此,令牌桶在實際設計的時候,桶大小不能像漏桶那樣設計很大,需要根據系統的處理能力來進行仔細的估算。例如,漏桶算法的桶容量可以設計爲 100 萬,但是一個每秒 30 TPS 的令牌桶,桶的容量可能只能設計成 40 左右。海外有的銀行給移動錢包提供的接口 TPS 上限是 30,壓測到了 40 就真的掛了。
- 排隊
排隊實際上是限流的一個變種,限流是直接拒絕用戶,排隊是讓用戶等待一段時間,全世界最有名的排隊當屬 12306 網站排隊了。
排隊雖然沒有直接拒絕用戶,但用戶等了很長時間後進入系統,體驗並不一定比限流好。
由於排隊需要臨時緩存大量的業務請求,單個系統內部無法緩存這麼多數據,一般情況下,排隊需要用獨立的系統去實現,例如使用 Kafka 這類消息隊列來緩存用戶請求。
下圖是 1 號店的 “雙 11” 秒殺排隊系統架構:
它的基本實現摘錄如下:
【排隊模塊】 負責接收用戶的搶購請求,將請求以先入先出的方式保存下來。每一個參加秒殺活動的商品保存一個隊列,隊列的大小可以根據參與秒殺的商品數量(或加點餘量)自行定義。
【調度模塊】 負責排隊模塊到服務模塊的動態調度,不斷檢查服務模塊,一旦處理能力有空閒,就從排隊隊列頭上把用戶訪問請求調入服務模塊,並負責向服務模塊分發請求。這裏調度模塊扮演一箇中介的角色,但不只是傳遞請求而已,它還擔負着調節系統處理能力的重任。我們可以根據服務模塊的實際處理能力,動態調節向排隊系統拉取請求的速度。
【服務模塊】 負責調用真正業務來處理服務,並返回處理結果,調用排隊模塊的接口回寫業務處理結果。
小結
今天我爲你講了接口級故障的四種應對方法,分別是降級、熔斷、限流和排隊,希望對你有所幫助。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/ZyjqUSDO4CQ2YfAeX7yYJw