流量洪峰下做好高服務質量的架構是件具備挑戰的事情,本文詳細闡述了從Google SRE的系統方法論以及實際業務的應對過程中出發,一些體系化的可用性設計。對我們了解系統的全貌、上下游的聯防有更進一步的幫助。
一、負載均衡
負載均衡具體分成兩個方向,一個是前端負載均衡,另一個是數據中心內部的負載均衡。
前端負載均衡方面,一般而言用戶流量訪問層面主要依據DNS,希望做到最小化用戶請求延遲。將用戶流量最優地分布在多個網絡鏈路上、多個數據中心、多臺服務器上,通過動態CDN的方案達到最小延遲。
用戶流量會先流入BFE的前端接入層,第一層的BFE實際上起到一個路由的作用,盡可能選擇跟接入節點比較近的一個機房,用來加速用戶請求。然后通過API網關轉發到下游的服務層,可能是內部的一些微服務或者業務的聚合層等,最終構成一個完整的流量模式。
基于此,前端服務器的負載均衡主要考慮幾個邏輯:
第一,盡量選擇最近節點;
第二,基于帶寬策略調度選擇API進入機房;
第三,基于可用服務容量平衡流量。
數據中心內部的負載均衡方面,理想情況下最忙和最不忙的節點所消耗的CPU相差幅度較小。但如果負載均衡沒做好,情況可能相差甚遠。由此可能導致資源調度、編排的困難,無法合理分配容器資源。
因此,數據中心內部負載均衡主要考慮:
均衡流量分發;
可靠識別異常節點;
scale-out,增加同質節點以擴容;
減少錯誤,提高可用性。
我們此前通過同質節點來擴容就發現,內網服務出現CPU占用率過高的異常,通過排查發現背后RPC點到點通信間的 health check 成本過高,產生了一些問題。另外一方面,底層的服務如果只有單套集群,當出現抖動的時候故障面會比較大,因此需要引入多集群來解決問題。
通過實現 client 到 backend 的子集連接,我們做到了將后端平均分配給客戶端,同時可以處理節點變更,持續不斷均衡連接,避免大幅變動。多集群下,則需要考慮集群遷移的運維成本,同時集群之間業務的數據存在較小的交集。
回到CPU忙時、閑時占用率過大的問題,我們會發現這背后跟負載均衡算法有關。
第一個問題,對于每一個qps,實際上就是每一個query、查詢、API請求,它們的成本是不同的。節點與節點之間差異非常大,即便你做了均衡的流量分發,但是從負載的角度來看,實際上還是不均勻的。
第二個問題,存在物理機環境上的差異。因為我們通常都是分年采購服務器,新買的服務器通常主頻CPU會更強一些,所以服務器本質上很難做到強同質。
基于此,參考JSQ(最閑輪訓)負載均衡算法帶來的問題,發現缺乏的是服務端全局視圖,因此我們的目標需要綜合考慮負載和可用性。我們參考了《The power of two choices in randomized load balancing》的思路,使用the choice-of-2算法,隨機選取的兩個節點進行打分,選擇更優的節點:
選擇backend:CPU,client:health、inflight、latency作為指標,使用一個簡單的線性方程進行打分;
對新啟動的節點使用常量懲罰值(penalty),以及使用探針方式最小化放量,進行預熱;
打分比較低的節點,避免進入“永久黑名單”而無法恢復,使用統計衰減的方式,讓節點指標逐漸恢復到初始狀態(即默認值)。
通過優化負載均衡算法以后,我們做到了比較好的收益。
二、限流
避免過載,是負載均衡的一個重要目標。隨著壓力增加,無論負載均衡策略如何高效,系統某個部分總會過載。我們優先考慮優雅降級,返回低質量的結果,提供有損服務。在最差的情況,妥善的限流來保證服務本身穩定。
限流這塊,我們認為主要關注以下幾點:
一是針對qps的限制,帶來請求成本不同、靜態閾值難以配置的問題;
二是根據API的重要性,按照優先級丟棄;
三是給每個用戶設置限制,全局過載發生時候,針對某些“異常”進行控制非常關鍵;
四是拒絕請求也需要成本;
五是每個服務都配置限流帶來的運維成本。
在限流策略上,我們首先采用的是分布式限流。我們通過實現一個quota-server,用于給backend針對每個client進行控制,即backend需要請求quota-server獲取quota。
這樣做的好處是減少請求Server的頻次,獲取完以后直接本地消費。算法層面使用最大最小公平算法,解決某個大消耗者導致的饑餓。
在客戶端側,當出現某個用戶超過資源配額時,后端任務會快速拒絕請求,返回“配額不足”的錯誤,有可能后端忙著不停發送拒絕請求,導致過載和依賴的資源出現大量錯誤,處于對下游的保護兩種狀況,我們選擇在client側直接進行流量,而不發送到網絡層。
我們在Google SRE里學到了一個有意思的公式,max(0, (requests- K*accepts) / (requests + 1))。通過這種公式,我們可以讓client直接發送請求,一旦超過限制,按照概率進行截流。
在過載保護方面,核心思路就是在服務過載時,丟棄一定的流量,保證系統臨近過載時的峰值流量,以求自保護。常見的做法有基于CPU、內存使用量來進行流量丟棄;使用隊列進行管理;可控延遲算法:CoDel 等。
簡單來說,當我們的CPU達到80%的時候,這個時候可以認為它接近過載,如果這個時候的吞吐達到100,瞬時值的請求是110,我就可以丟掉這10個流量,這種情況下服務就可以進行自保護,我們基于這樣的思路最終實現了一個過載保護的算法。
我們使用CPU的滑動均值(CPU > 800 )作為啟發閾值,一旦觸發就進入到過載保護階段。算法為:(MaxPass * AvgRT) < InFlight。其中MaxPass、AvgRT都為觸發前的滑動時間窗口的統計值。
限流效果生效后,CPU會在臨界值(800)附近抖動,如果不使用冷卻時間,那么一個短時間的CPU下降就可能導致大量請求被放行,嚴重時會打滿CPU。在冷卻時間后,重新判斷閾值(CPU > 800 ),是否持續進入過載保護。
三、重試
流量的走向,一般會從BFE到SLB然后經過API網關再到BFF、微服務最后到數據庫,這個過程要經過非常多層。在我們的日常工作中,當請求返回錯誤,對于backend部分節點過載的情況下,我們應該怎么做?
首先我們需要限制重試的次數,以及基于重試分布的策略;
其次,我們只應該在失敗層進行重試,當重試仍然失敗時,我們需要全局約定錯誤碼,避免級聯重試;
此外,我們需要使用隨機化、指數型遞增的充實周期,這里可以參考Exponential Backoff和Jitter;
最后,我們需要設定重試速率指標,用于診斷故障。
而在客戶端側,則需要做限速。因為用戶總是會頻繁嘗試去訪問一個不可達的服務,因此客戶端需要限制請求頻次,可以通過接口級別的error_details,掛載到每個API返回的響應里。
四、超時
我們之前講過,大部分的故障都是因為超時控制不合理導致的。首當其沖的是高并發下的高延遲服務,導致client堆積,引發線程阻塞,此時上游流量不斷涌入,最終引發故障。所以,從本質上理解超時它實際就是一種Fail Fast的策略,就是讓我們的請求盡可能消耗,類似這種堆積的請求基本上就是丟棄掉或者消耗掉。
另一個方面,當上游超時已經返回給用戶后,下游可能還在執行,這就會引發資源浪費的問題。
再一個問題,當我們對下游服務進行調優時,到底如何配置超時,默認值策略應該如何設定?生產環境下經常會遇到手抖或者錯誤配置導致配置失敗、出現故障的問題。所以我們最好是在框架層面做一些防御性的編程,讓它盡可能讓取在一個合理的區間內。
進程內的超時控制,關鍵要看一個請求在每個階段(網絡請求)開始前,檢查是否還有足夠的剩余來處理請求。另外,在進程內可能會有一些邏輯計算,我們通常認為這種時間比較少,所以一般不做控制。
現在很多RPC框架都在做跨進程超時控制,為什么要做這個?跨進程超時控制同樣可以參考進程內的超時控制思路,通過RPC的源數據傳遞,把它帶到下游服務,然后利用配額繼續傳遞,最終使得上下游鏈路不超過一秒。
審核編輯 黃宇
-
RPC
+關注
關注
0文章
111瀏覽量
11534 -
架構
+關注
關注
1文章
514瀏覽量
25470
發布評論請先 登錄
相關推薦
評論