B 站高可用架構實踐

流量洪峯下要做好高服務質量的架構是一件具備挑戰的事情,從 Google SRE 的系統方法論以及實際業務的應對過程中出發,分享一些體系化的可用性設計。對我們瞭解系統的全貌上下游的聯防有更進一步的瞭解。

負載均衡

BFE 就是指邊緣節點,BFE 選擇下游 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 限制的陷阱:

每一個 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 立馬下來,算法抖動非常厲害。圖二右側黃色線表示抖動非常高,綠色線表示放行的流量也是抖動非常高,所以又加了冷卻時間,比如持續幾秒鐘,再重新判斷。

重試

問題:每一層都重試,這一層 3 次,那一層 3 次,會指數級的放大。解決:只在失敗這一層重試,如果重試之後失敗,請返回一個全局約定好的錯誤碼,比如說:過載,無需重試,發現這個錯誤碼,通通放行,避免級聯重試。

重試都應該無腦的重試三次嗎?API 級別的重試需要考慮集羣的過載情況。是不是可以約定一個重試比例呢?比如只允許 10% 的流量進行重試,Client 端做統計,當發現有 10% 都是重試,那麼剩下的都拒絕掉。這樣最多產生 1.1 倍的放大,重試 3 次,極端情況下,會產生 3 倍放大。還有在重試的時候,儘量引入隨機、指數遞增的一個重試周期,大家不要都重試 1 秒鐘,有可能會堆砌一個重試的波峯

重試的統計圖和記錄 QPS 的圖分開。問題診斷的時候,可以知道它是來自流量重試導致的問題放大。

某個服務不可用的時候,用戶總是會猛點,那麼這個時候,需要去限制它的頻次,一個短週期內不允許發重複請求。這種策略,有可能會根據不同的過載情況經常調這種策略,那麼可以掛載到每一個 API 裏面。

超時

大部分的故障都是因爲超時控制不合理導致的。

某個服務需要在 1 秒返回,內部可能需要訪問 Redis,需要訪問 RPC,需要訪問數據庫,時間加起來就超過 1 秒,那麼訪問完每一層,應該計算供下一層使用的超時時間還剩多少可用。在 go 語言裏,可能會使用 Context,每一個網絡請求開始的階段,都要根據配置文件配置的超時時間,和當前剩餘多少,取一個最小值,最終整個超時時間不會超過 1 秒。

通過 RPC 的元數據傳遞,類似 HTTP 的 request header,帶給其它服務。例如在圖中,就是把 700ms 這個配額傳遞給 Service B。

下游服務作爲服務提供者,在他的 RPC.IDL 文件中把自己的超時要配上,那麼用 IDL 文件的時候,就知道是 200 ms,不用去問。

應對連鎖故障

優雅降級:一開始千人千面,後來只返回熱門的

QA











轉自:

kunzhao.org/docs/cloud-plus-bbs/bilibili-high-availability/

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