高可用之限流-01-入門介紹

老马啸西风發表於2024-10-10

限流系列

開源元件 rate-limit: 限流

高可用之限流-01-入門介紹

高可用之限流-02-如何設計限流框架

高可用之限流-03-Semaphore 訊號量做限流

高可用之限流-04-fixed window 固定視窗

高可用之限流-05-slide window 滑動視窗

高可用之限流-06-slide window 滑動視窗 sentinel 原始碼

高可用之限流-07-token bucket 令牌桶演算法

高可用之限流 08-leaky bucket漏桶演算法

高可用之限流 09-guava RateLimiter 入門使用簡介 & 原始碼分析

背景

在如今的網際網路已經作為社會基礎設施的大環境下,上面的這個場景其實離我們並不是那麼遠,同時也會顯得沒那麼極端。

例如,層出不窮的營銷玩法,一個接著一個的社會熱點,以及網際網路冰山之下的黑產、刷子的蓬勃發展,更加使得這個場景變的那麼的需要去考慮、去顧忌。

因為隨時都有可能會湧入超出你預期的流量,然後壓垮你的系統。

那麼限流的作用就很顯而易見了:只要系統沒當機,系統只是因為資源不夠,而無法應對大量的請求,為了保證有限的系統資源能夠提供最大化的服務能力,因而對系統按照預設的規則進行流量(輸出或輸入)限制的一種方法,確保被接收的流量不會超過系統所能承載的上限。

限流

服務治理本身的概念比較大,包括鑑權、限流、降級、熔斷、監控告警等等。

場景需求

比如限制微服務叢集單臺機器每秒請求次數,我們還需要針對不同呼叫方甚至不同介面進行更加細粒度限流:
比如限制 A 呼叫方對某個服務的某個的介面的每秒最大請求次數。

限流中的“流”字該如何解讀呢?要限制的指標到底是什麼?

不同的場景對“流”的定義也是不同的,可以是網路流量,頻寬,每秒處理的事務數 (TPS),每秒請求數 (hits per second),併發請求數,
甚至還可能是業務上的某個指標,比如使用者在某段時間內允許的最多請求簡訊驗證碼次數。

從保證系統穩定可用的角度考量,對於微服務系統來說,最好的一個限流指標是:併發請求數。

透過限制併發處理的請求數目,可以限制任何時刻都不會有過多的請求在消耗資源
,比如:我們透過配置 web 容器中 servlet worker 執行緒數目為 200,則任何時刻最多都只有 200 個請求在處理,超過的請求都會被阻塞排隊。

對比 TPS 和 hits per second 的兩個指標,我們選擇使用 hits per second 作為限流指標。

因為,對 TPS 的限流實際上是無法做的,TPS 表示每秒處理事務數,事務的開始是接收到介面請求,事務的結束是處理完成返回,所以有一定的時間跨度,如果事務開始限流計數器加一,事務結束限流計數器減一,則就等同於併發限流。

而如果把事務請求接收作為計數時間點,則就退化為按照 hits per second 來做限流,而如果把事務結束作為計數時間點,則計數器的數值並不能代表系統當下以及接下來的系統訪問壓力。

對 hits per second 的限流是否是一個有效的限流指標呢?答案是肯定的,這個值是可觀察可統計的,所以方便配置限流規則,而且這個值在一定程度上反應系統當前和接下來的效能壓力,對於這一指標的限流確實也可以達到限制對系統資源的使用。

有了流的定義之後,我們接下來看幾種常用的限流演算法:固定時間視窗,滑動時間視窗,令牌桶演算法,漏桶演算法以及他們的改進版本。

常見演算法

固定時間視窗

  • 演算法思想

首先需要選定一個時間起點,之後每次介面請求到來都累加計數器,如果在當前時間視窗內,根據限流規則(比如每秒鐘最大允許 100 次介面請求),
累加訪問次數超過限流值,則限流熔斷拒絕介面請求。

當進入下一個時間視窗之後,計數器清零重新計數。

  • 缺點

限流策略過於粗略,無法應對兩個時間視窗臨界時間內的突發流量。

我們舉一個例子:假設我們限流規則為每秒鐘不超過 100 次介面請求,第一個 1s 時間視窗內,100 次介面請求都集中在最後的 10ms 內,
在第二個 1s 的時間視窗內,100 次介面請求都集中在最開始的 10ms 內,雖然兩個時間視窗內流量都符合限流要求 (<=100 個請求),
但在兩個時間視窗臨界的 20ms 內會集中有 200 次介面請求,如果不做限流,集中在這 20ms 內的 200 次請求就有可能壓垮系統。

滑動時間視窗

滑動時間視窗演算法是對固定時間視窗演算法的一種改進,流量經過滑動時間視窗演算法整形之後,可以保證任意時間視窗內,都不會超過最大允許的限流值,從流量曲線上來看會更加平滑,可以部分解決上面提到的臨界突發流量問題。

對比固定時間視窗限流演算法,滑動時間視窗限流演算法的時間視窗是持續滑動的,並且除了需要一個計數器來記錄時間視窗內介面請求次數之外,還需要記錄在時間視窗內每個介面請求到達的時間點,對記憶體的佔用會比較多。

  • 演算法模型

滑動時間視窗的演算法模型如下:

滑動視窗記錄的時間點 list = (t_1, t_2, …t_k),時間視窗大小為 1 秒,起點是 list 中最小的時間點。

當 t_m 時刻新的請求到來時,我們透過以下步驟來更新滑動時間視窗並判斷是否限流熔斷:

STEP 1: 檢查介面請求的時間 t_m 是否在當前的時間視窗 [t_start, t_start+1 秒) 內。如果是,則跳轉到 STEP 3,否則跳轉到 STEP 2.

STEP 2: 向後滑動時間視窗,將時間視窗的起點 t_start 更新為 list 中的第二小時間點,並將最小的時間點從 list 中刪除。然後,跳轉到 STEP 1。

STEP 3: 判斷當前時間視窗內的介面請求數是否小於最大允許的介面請求限流值,即判斷: list.size < max_hits_limit,如果小於,則說明沒有超過限流值,允許介面請求,並將此介面請求的訪問時間放入到時間視窗內,否則直接執行限流熔斷。

  • 缺陷

即便滑動時間視窗限流演算法可以保證任意時間視窗內介面請求次數都不會超過最大限流值,但是仍然不能防止在細時間粒度上面訪問過於集中的問題。

比如上面舉的例子,第一個 1s 的時間視窗內 100 次請求都集中在最後 10ms 中。

也就是說,基於時間視窗的限流演算法,不管是固定時間視窗還是滑動時間視窗,只能在選定的時間粒度上限流,對選定時間粒度內的更加細粒度的訪問頻率不做限制。

  • 改進版本

多層次限流,我們可以對同一個介面設定多條限流規則,除了 1 秒不超過 100 次之外,我們還可以設定 100ms 不超過 20 次 (這裡需要設定的比 10 次大一些),
兩條規則同時限制,流量會更加平滑。

除此之外,還有針對滑動時間視窗限流演算法空間複雜度大的改進演算法,限於篇幅,這裡就不展開詳說了。

令牌桶演算法

令牌桶和漏桶演算法的演算法思想大體類似,可以把漏桶演算法作為令牌桶限流演算法的改進版本,所以我們以介紹令牌桶演算法為主。

  • 核心演算法

我們先來看下最基礎未經過改進的令牌桶演算法:

  1. 介面限制 t 秒內最大訪問次數為 n,則每隔 t/n 秒會放一個 token 到桶中;

  2. 桶中最多可以存放 b 個 token,如果 token 到達時令牌桶已經滿了,那麼這個 token 會被丟棄;

  3. 介面請求會先從令牌桶中取 token,拿到 token 則處理介面請求,拿不到 token 則執行限流。

令牌桶演算法看似比較複雜,每間隔固定時間都要放 token 到桶中,但並不需要專門起一個執行緒來做這件事情。

每次在取 token 之前,根據上次放入 token 的時間戳和現在的時間戳,計算出這段時間需要放多少 token 進去,一次性放進去,所以在實現上面也並沒有太大難度。

漏桶演算法

漏桶演算法稍微不同與令牌桶演算法的一點是:對於取令牌的頻率也有限制

要按照 t/n 固定的速度來取令牌,所以可以看出漏桶演算法對流量的整形效果更加好,流量更加平滑,任何突發流量都會被限流。

因為令牌桶大小為 b,所以是可以應對突發流量的。

  • 其他改進演算法

當然,對於令牌桶演算法,還有很多其他改進演算法,比如:

  1. 預熱桶

  2. 一次性放入多個令牌

  3. 支援一次性取多個令牌

  • 使用場景

令牌桶和漏桶演算法比較適合阻塞式限流。

比如一些後臺 job 類的限流,超過了最大訪問頻率之後,請求並不會被拒絕,而是會被阻塞到有令牌後再繼續執行。

對於像微服務介面這種對響應時間比較敏感的限流場景,會比較適合選擇基於時間視窗的否決式限流演算法,其中滑動時間視窗限流演算法空間複雜度較高,

記憶體佔用會比較多,所以對比來看,儘管固定時間視窗演算法處理臨界突發流量的能力較差,但實現簡單,而簡單帶來了好的效能和不容易出錯,

所以固定時間視窗演算法也不失是一個好的微服務介面限流演算法。

guava 實現

http://ifeve.com/guava-ratelimiter/

可參考 guava 的實現。

相關文章