B 站高可用架構實踐
流量洪峯下要做好高服務質量的架構是一件具備挑戰的事情,從 Google SRE 的系統方法論以及實際業務的應對過程中出發,分享一些體系化的可用性設計。對我們瞭解系統的全貌上下游的聯防有更進一步的瞭解。
負載均衡
BFE 就是指邊緣節點,BFE 選擇下游 IDC 的邏輯權衡:
-
離 BFE 節點比較近的
-
基於帶寬的調度策略
-
某個 IDC 的流量已經過載,選擇另外一個 IDC
當流量走到某個 IDC 時,這個流量應該如何進行負載均衡?
問題:RPC 定時發送的 ping-pong,也即 healthcheck,佔用資源也非常多。服務 A 需要與賬號服務維持長連接發送 ping-pong,服務 B 也需要維持長連接發送 ping-pong。這個服務越底層,一般依賴和引用這個服務的資源就越多,一旦有任何抖動,那麼產生的這個故障面是很大的。那麼應該如何解決?
解決:以前是一個 client 跟所有的 backend 建立連接,做負載均衡。現在引入一個新的算法,子集選擇算法,一個 client 跟一小部分的 backend 建立連接。圖片中示例的算法,是從《Site Reliability Engineering》這本書裏看的。
如何規避單集羣抖動帶來的問題?多集羣。
如上述圖片所示,如果採用的是 JSQ 負載均衡算法,那麼對於 LBA 它一定是選擇 Server Y 這個節點。但如果站在全局的視角來看,就肯定不會選擇 Server Y 了,因此這個算法缺乏一個全局的視角。
如果微服務採用的是 Java 語言開發,當它處於 GC 或者 FullGC 的時候,這個時候發一個請求過去,那麼它的 latency 肯定會變得非常高,可能會產生過載。
新啓動的節點,JVM 會做 JIT,每次新啓動都會抖動一波,那麼就需要考慮如何對這個節點做預熱?
如上圖所示,採用 “the choice-of-2” 算法後,各個機器的 CPU 負載趨向於收斂,即各個機器的 CPU 負載都差不多。Client 如何拿到後臺的 Backend 的各項負載?是採用 Middleware 從 Rpc 的 Response 裏面獲取的,有很多 RPC 也支持獲取元數據信息等。
還有就是 JVM 在啓動的時候做 JIT,以前的預熱做法:手動觸發預熱代碼,然後再引入流量,再進行服務發現註冊等,不是非常通用。通過改進負載均衡算法,引入懲罰值的方式,慢慢放入流量進行預熱。
限流
用 QPS 限制的陷阱:
-
不同的參數,請求的數據量是不同的,對一個進程的一個吞吐是有影響的。
-
業務是經常迭代的,配一個靜態的閾值,這個非常困難。能否按照每一個服務用多少個 CPU 來做限流?
每一個 API 都是有重要性的:非常重要、次重要,這樣配置限流、做過載保護的時候,可以使用不同的閾值。
每個服務都要配一個限流,是非常煩人的,需要壓測,是不是可以自適應去限流?
每個 Client 如何知道自己這一次需要申請多少 Quota ?基於歷史數據窗口的 QPS。
節點與節點之間是有差異的,分配算法不夠好,會導致某些節點產生飢餓。那麼可以採用最大最小公平算法,儘可能地比較公平地去分配資源,來解決這個問題。
當量再大一點的時候,如果 Backend 一直忙着拒絕請求,比如發送 503,那麼它還是會掛掉。這種情況就要考慮從 Client 去截流。此處,又提到了 Google 《Site Reliability Engineering》這本書裏面的一個算法,即 Client 是按照一定概率去截流。那麼這個概率怎麼計算?一個是總請求量:requests
,一個是成功的請求量:accepts
。如果服務報錯率比較高,意味着 accepts
不怎麼增長,requests
一直增長,最終這個公式求極限,它會等於 1,所以它的丟棄概率是非常高的。基於這麼一個簡單的公式,不需要依賴什麼 ZooKeeper,什麼協調器之類的,就可以得到一個概率丟棄一些請求。它儘可能的在服務不掛掉的情況下,放更多的流量進去,而不是像 Netflix 一樣全部拒掉。
連鎖故障通常都是某一個節點過載了掛掉,流量又會去剩下的 n - 1 個節點,又扛不住,又掛掉,所以最終一個一個挨着雪崩。所以過載保護的目的是爲了自保。
B 站參考了阿里的 Sentinel 框架、Netflix 的一些文章等,最終採用的是類似於 TCP BBR 探測的思路和算法。簡單說:當 CPU 達到 80% 的時候,這個時候我們認爲流量過載,如果此時吞吐量比如 100,用它作爲閾值,瞬時值的請求比如是 110,那就可以丟掉 10 個流量。這樣就可以實現一個限流算法。
CPU 抖來抖去,使用 CPU 滑動均值(綠色線)可以跳動的沒有這麼厲害。這個 CPU 針對不同接口的優先級,例如低優先級 80% 觸發,高優先級 90% 觸發,可以定爲一個閾值。
那麼吞吐如何計算?利特爾法則。當前的 QPS * 延遲 = 吞吐,可以用過去的一個窗口作爲指標。一旦丟棄流量,CPU 立馬下來,算法抖動非常厲害。圖二右側黃色線表示抖動非常高,綠色線表示放行的流量也是抖動非常高,所以又加了冷卻時間,比如持續幾秒鐘,再重新判斷。
重試
-
BFE: 動態 CDN
-
SLB: LVS + Nginx 實現,四七層負載均衡
-
BFF: 業務邏輯組裝、編排
問題:每一層都重試,這一層 3 次,那一層 3 次,會指數級的放大。解決:只在失敗這一層重試,如果重試之後失敗,請返回一個全局約定好的錯誤碼,比如說:過載,無需重試,發現這個錯誤碼,通通放行,避免級聯重試。
重試都應該無腦的重試三次嗎?API 級別的重試需要考慮集羣的過載情況。是不是可以約定一個重試比例呢?比如只允許 10% 的流量進行重試,Client 端做統計,當發現有 10% 都是重試,那麼剩下的都拒絕掉。這樣最多產生 1.1 倍的放大,重試 3 次,極端情況下,會產生 3 倍放大。還有在重試的時候,儘量引入隨機、指數遞增的一個重試周期,大家不要都重試 1 秒鐘,有可能會堆砌一個重試的波峯。
重試的統計圖和記錄 QPS 的圖分開。問題診斷的時候,可以知道它是來自流量重試導致的問題放大。
某個服務不可用的時候,用戶總是會猛點,那麼這個時候,需要去限制它的頻次,一個短週期內不允許發重複請求。這種策略,有可能會根據不同的過載情況經常調這種策略,那麼可以掛載到每一個 API 裏面。
超時
大部分的故障都是因爲超時控制不合理導致的。
-
某個高延遲服務可能會導致 Client 堆積,Client 線程會阻塞,上游流量不斷進來,下游的消費速度跟不上上游的流入速度,進程會堆積越來越多請求,可能會 OOM。
-
超時的策略本質是就是爲了丟棄或者消耗掉請求。
-
下游 2 秒返回,上游配置了 1 秒,上游超時已經返回給用戶,下游還在執行,浪費資源。
某個服務需要在 1 秒返回,內部可能需要訪問 Redis,需要訪問 RPC,需要訪問數據庫,時間加起來就超過 1 秒,那麼訪問完每一層,應該計算供下一層使用的超時時間還剩多少可用。在 go 語言裏,可能會使用 Context,每一個網絡請求開始的階段,都要根據配置文件配置的超時時間,和當前剩餘多少,取一個最小值,最終整個超時時間不會超過 1 秒。
通過 RPC 的元數據傳遞,類似 HTTP 的 request header,帶給其它服務。例如在圖中,就是把 700ms 這個配額傳遞給 Service B。
下游服務作爲服務提供者,在他的 RPC.IDL 文件中把自己的超時要配上,那麼用 IDL 文件的時候,就知道是 200 ms,不用去問。
應對連鎖故障
優雅降級:一開始千人千面,後來只返回熱門的
QA
-
Q: 請問負載均衡依據的 metric 是什麼?
-
A: 服務端主要用 CPU,客戶端用的是健康度,指連接的成功率,延遲也很重要,每個 Client 往不同的 Backend 發了多少個請求,四個指標歸一,寫一個線性方程,進行打分。
-
Q: BFE 到 SLB 走公網還是專線?
-
A: 既有公網,又有專線。
-
Q: Client 幾千量級,每 10 秒 ping-pong 一下,會不會造成蠻高的 CPU?
-
A: 如果 Backend 很多的話,那麼這個的確會造成。
-
Q: 多集羣切換是否有阻塞的點?
-
A: 一個 Client 連接到各個集羣,subset 算法,每個集羣都有 Cache
-
Q: 負載均衡的探針是怎麼做的?
-
A: 懲罰值,比如 5 秒,慢慢放流量
-
Q: Quota-Server 限流有開源實現嗎?
-
A: 目前看到的都是針對單節點的。
-
Q: 客戶端統計是否有點太多?
-
A: 可以做到 Sidecar、Service Mesh 裏面
-
Q: 超時傳遞是不是太嚴格?
-
A: 有些情況下即便超時也要運行,可以通過 RPC Context 管控
-
Q: 每個 RPC 都獲取 CPU 會不會很昂貴?
-
A: 後臺開啓線程定時計算 CPU 平滑均值
-
Q: 線上壓測和測試環境壓測 CPU 不一致
-
A: RPC 路由加影子庫
-
Q: CC 攻擊
-
A: 邊緣節點或者核心機房都有防止 CC 攻擊的一些手段,只要不是分佈式搞你,都能找到流量特徵進行管控
轉自:
kunzhao.org/docs/cloud-plus-bbs/bilibili-high-availability/
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/m3pSCd_52KH6jc6H_WXHfA