一、背景
最近幾年,隨著微服務的流行,服務與服務之間依賴越來越強,調用也越來越復雜,服務間的穩定性變突顯出來。特別是在遇到突發請求時,常常需要通過緩存、限流、熔斷降級、負載均衡等多種方式保證服務的穩定性。其中限流是不可或缺的一環。一般每一個對外提供的接口都需要做流量控制,這與保險絲的原理一樣。當接口的流量請求超過核定值時就得對請求進行引流或者直接拒絕等操作。特別是在系統應對大流量,高并發的訪問時,限流算法可以很好地控制流量,從而避免系統負載過高而崩潰。
二、限流算法
下面介紹的限流方式主要是窗口算法和桶算法,各有優勢。
- 窗口算法實現簡單,邏輯清晰,可以很直觀的得到當前的 QPS 情況,但是會有時間窗口的臨界突變問題,而且不像桶一樣有隊列可以緩沖;另外,在細粒度上難以對流量曲線進行調整,即不能實現削峰填谷的效果,這可能對某些需要平滑流量的場景造成問題。限流簡單暴力,對于C端產品來說,這可能不是一種友好的選擇。
- 桶算法雖然稍微復雜,不好統計 QPS 情況,但是桶算法也有優勢所在。
- 漏桶模式消費速率恒定,可以很好的保護自身系統,可以對流量進行整形,但是面對突發流量不能快速響應。應用場景:自身服務調用第三方服務時,第三方服務需要我們來限制速度。
- 令牌桶模式可以面對突發流量,但是啟動時會有緩慢加速的過程,不過常見的開源工具中已經對此優化。應用場景:別人調用自身服務時可以使用(畢竟對自身服務qps有一定的了解)。
限流算法 | 原理 | 優點 | 缺點 | 使用場景 | 流量整形 | 處理延時 |
滑動窗口 | 窗口在時間軸上滑動,記錄請求次數 | 實時控制,流量較均勻 | 難以實現流量曲線整形,無法削峰填谷 | 單機或單點網關限流 | 否 | 否 |
漏桶 | 流入請求如水流入桶,滿則丟棄 | 能夠控制突發流量(主要看容量) | 流出速率恒定,對突發流量反應不夠快速 | 瞬時高并發流量場景或調第三方服務 | 是 | 是 |
令牌桶 | 以一定速率產生令牌,請求需拿令牌 | 可動態調整處理速度,允許突發流量在一定程序上得到滿足 | 實現復雜,以及令牌的數量控制 | 別人調用自身服務控制量級 | 是 | 是 |
2.1 漏桶算法
漏桶算法主要目的是控制請求速率,平滑網絡上的突發流量。如上圖示例,固定速率流出請求,流入請求速率任意,請求是否被處理看桶中的令牌是否足夠。它常用于限制網絡流量、控制數據傳輸率等場景,但是無法應用突發的高流量。
JSF不使用此類算法的原因:
1、由于漏桶出口速度固定,不能靈活應對后端能力的提升,如后端服務動態擴容后,漏桶沒有辦法。
2、無法有效利用資源,服務器處理能力并不是均衡絕對的,即任意時間都是固定的,如服務處理能力為1kqps,可能前6s為2k,后面時間為500,即一小段時間服務器資源是可以承受這段請求壓力的,但是漏桶算法在這種情況下會丟棄一部分請求。
2.2 令牌桶算法
令牌桶算法是另一種經典的限流算法,它的原理是將請求放入一個令牌桶中。然后按照一定速度不斷地放出令牌。只有在令牌桶中有令牌時,才能夠發出令牌。令牌桶算法可以控制單位時間內的請求速率,同時可以應對突發流量(累積令牌)如后端能力的提升,因為只有在足夠多的令牌,才可以放請求過去。
這里思考一個問題,令牌是如何添加的?如果按照指定時間間隔添加令牌,則需要開一個線程去定時添加,當有多個接口需要限流時,線程數就會隨之增加,這顯然不是一個好的辦法。在Guava的RateLimiter是這樣做的——每次令牌獲取時計算令牌是否足夠:它通過存儲的下一個令牌生成的時間,和當前獲取令牌的時間差,再結合閾值,去計算令牌是否足夠,同時再記錄下一個令牌的生成時間以便下一次調用。
下面是 Guava 中 RateLimiter 類的子類 SmoothRateLimiter 的resync()
方法的代碼分析,可以看到其中的令牌計算邏輯。
void resync(long nowMicros) { // 當前微秒時間// 當前時間是否大于下一個令牌生成時間if (nowMicros > this.nextFreeTicketMicros) { // 可生成的令牌數 newPermits = (當前時間 - 下一個令牌生成時間)/ 令牌生成時間間隔。// 如果 QPS 為2,這里的 coolDownIntervalMicros 就是 500000.0 微秒(500ms)doublenewPermits= (double)(nowMicros - this.nextFreeTicketMicros) / this.coolDownIntervalMicros();// 更新令牌庫存 storedPermits。this.storedPermits = Math.min(this.maxPermits, this.storedPermits + newPermits);// 更新下一個令牌生成時間 nextFreeTicketMicrosthis.nextFreeTicketMicros = nowMicros;}
}
2.3 滑動窗口限流
滑動窗口限流將單位時間周期分為n個小周期,分別記錄每個小周期內接口的訪問次數,并且根據時間滑動刪除過期的小周期,每個小周期都有自己獨立的計數器。
當滑動窗口的格子周期劃分的越多,滑動窗口滾動就越平滑。滑動窗口算法雖然解決了固定窗口的臨界問題,但一旦達到限流后,請求都會直接暴力被拒絕的。對于C端產品來說,這可能不是一種友好的選擇。
三、解決方案
在介紹完目前主流的限流算法及其優缺點之后,讓我們探討一個實際場景:接口提供方已設定限流策略(限流算法為令牌桶算法),而接口調用方在整點前后實施了降級措施(暫不考慮降級是否必要)。在降級恢復時,我們注意到突發流量在第一秒內超過了預設的限流閾值。
前面已經對令牌桶算法有了深入的講解,這里超出限流閾值的原因就不在過多介紹,接下來講講解決方案。
3.1 計算閾值
限流閾值再怎么設置,總會出現由于所有請求都是消耗資源很多的請求可能導致應用承受不住負載而崩潰。那如何計算閾值呢?總體上思路有幾個,看服務的觀測數據、借鑒、手動計算、壓測。
看服務的性能數據屬于常規解法,如基于完善的監控查看峰值期間的QPS。不過我個人覺得,最好的方式應該是線上執行全鏈路壓測,選擇響應時間穩定不變對應的壓測QPS。
盡管設置了限流閾值,但仍然可能出現由于所有請求都消耗大量資源而導致應用負載過高并崩潰的情況。針對這種情況,我們可以采用以下幾種方法來計算閾值:
- 借鑒:參考類似服務的閾值設置經驗。
- 手動計算:根據業務需求和預期負載進行估算。
- 線上執行全鏈路壓測:這是最理想的解決方案,通過選擇響應時間穩定不變的壓測QPS,能夠更準確地評估系統的承載能力。
當然,基于完善的監控系統觀察服務的性能數據也是常規的解決方法,例如查看峰值期間的QPS。然而,我認為線上執行全鏈路壓測是最佳的選擇,因為它能直接模擬真實環境下的負載情況,從而得出更加精確的閾值。
3.2 選擇限流算法
選擇適當的限流算法需要根據具體的應用場景和需求進行評估。例如,對于注重歷史流量參考價值的場景,滑動窗口算法可能更為適用;而對于需要平滑流量的場景,則更適合使用漏桶算法;而對于既要平滑流量又需要處理突發流量的場景,令牌桶算法則可能是最佳選擇。
對于上面提供的場景,令牌桶算法與滑動窗口算法都是可選擇的,只不過前者需要自身應用應對突發流量,而后者在流量超出最大閾值時直接拒絕。