又拍雲網關速率限制實踐

速率限制 (Rate Limit) 經過限制調用 API 的頻率防止 API 過分使用,保護 API 免受意外或惡意的使用,在諸多業務場景中獲得普遍應用。日前,又拍雲系統開發工程師陳卓受邀在 Open Talk 公開課上做了題爲《又拍雲網關速率限制實踐》的分享,詳細解讀當前經常使用的算法以及基於網關 nginx/openresty 的實現和配置細節。如下是直播分享內容整理,查看視頻請點擊閱讀原文。html

網關速率限制是一種防護服務性措施,公共服務須要借其保護本身免受過分使用,使用速率限制主要有三個好處:nginx

  • 提高用戶體驗:用戶在使用公共服務時總會面臨一些資源加強和共享的問題,例如 CPU,當一個用戶無論是有意或無心地過分使用 API 時,勢必會對其餘的用戶形成一些影響。
  • 更加安全:咱們的服務、CPU、內存其實都是有必定的限制,過分訪問勢必會影響到服務的穩定性。假若有四個服務,每一個服務能承載 100 個請求,當其中一個服務超過 100 個請求時就可能會宕機,其它三個服務在接收到超過 100 個服務請求時,也會接着連續宕機,這會形成服務不可用。
  • 減小開銷:如今不少服務都是放到公有云上,內存、CPU 和流量都是有成本的,有一些按量計費,使用多少花多少錢,這種狀況會產生一些沒必要要的開銷。

RateLimit 的幾種算法

首先介紹四種速率限制的算法,分別是漏桶(Leaky Bucket)、令牌桶(Token Bucket)、固定窗口(Fixed Windows)、滑動窗口(Sliding Windows),不少限制措施都是基於這些算法進行的。漏桶和令牌桶雖然直觀理解看似不太同樣,可是在底層實現中這兩種算法很是類似,達到的效果差很少。固定窗口和滑動窗口屬於另一類,滑動窗口是基於固定窗口作的。git

漏桶(Leaky Bucket)github

如上圖所示,用戶請求都被放進桶裏,當桶滿了之後,請求會被拒絕掉。桶的底部有一個孔,請求會以必定的速率被放過,好比說如今是限制每分鐘 10 個請求,意味着每隔 6 秒鐘就會有一個請求經過。漏桶算法的特色在於其經過請求的速率是恆定的,能夠將流量整形的很是均勻,即使同時有 100 個請求也不會一次性經過,而是按必定間隔慢慢放行,這對後端服務迎接突發流量很是友好。golang

令牌桶(Token Bucket)redis

令牌桶,顧名思義桶裏放的是一些令牌,這些令牌會按必定的速率往桶裏放,假如每分鐘限制 10 個請求,那麼每分鐘就往桶裏放 10 個令牌,請求進來的時候須要先在令牌桶裏拿令牌,拿到令牌則請求被放行,桶爲空拿不到則意味着該請求被拒絕掉。算法

須要說明的是,令牌的個數是按必定的速率投放的,每分鐘放 10 個令牌,那麼能經過的請求確定也是每分鐘 10 個。假如勻速放令牌, 6 秒鐘放一個令牌,最終結果和每分鐘放 10 個令牌是同樣的。apache

漏桶(Leaky Bucket)算法實現後端

因爲令牌桶跟漏桶的實現效果差很少,這裏主要細講漏桶的算法和實現。先假設速率限制是每分鐘 3 個請求,即每 20 秒鐘放行一個請求。如圖所示,假設第 10 秒進來第一個請求,由於以前一直都沒有請求進入,因此該請求被容許經過。記錄下最後一次的訪問時間,即爲本次請求經過時間點。api

如今第 20 秒又過來一個請求,20 秒相對於 10 秒鐘通過了 10 秒鐘,按照計算只容許被經過 0.5 個請求,那請求就被拒絕掉了。這個 last 值仍是保持最後一次一個請求經過的時間。第 30 秒又來了一個請求:若是將 30 秒看做是最後一次更新時間,至關因而 30 秒減 10 秒,也就是通過了 20 秒,而咱們的限制是每 20 秒容許 1 個請求,那麼這個請求會被放過去,last 值如今已經變成了 30 秒。

經過上述分析能夠發現,漏桶限制很是嚴格,即使請求是第 29 秒進來也不能被經過,由於必需要通過 20 秒才容許經過一個請求,這可能會給業務帶來一個問題:例如如今每分鐘容許經過 3 個請求,用戶可能須要在前 10 秒鐘把三個請求發完,這種需求在這種算法下不會被容許。由於從發掉第一個請求到發第二個請求必需要間隔 20 秒才能夠,爲了彌補這種缺陷,須要引用另一個參數 burst(爆發),容許忽然爆發的請求。以下圖中所示,40 秒距離 30 秒實際上只通過了 10 秒鐘,按照以前的算法計算只被容許訪問 0.5 個請求,實際上應該被拒絕掉,可是咱們容許它提早多訪問一個請求(burst 爲1),算下來就是 0.5+1=1.5 個請求。

須要注意的是,雖然咱們當前時間是 40 秒,但咱們最後須要更新請求時間到 50 秒,這是由於如今已經超量使用進入到下一個時間段了,至關因而提早放行一個請求,最後一個 last 時間是 30 秒,應該加 20 秒到 50 秒。這也是該算法實現的一個特色,不少算法也都有 burst 的功能,即容許提早訪問。

45 秒又來了一個請求,儘管這個請求來時,咱們也容許它提早訪問。但因爲上一次最後訪問時間已是 50 秒了,並且在經過計算得出不到一個請求時,這一個請求也就被拒絕掉了,時間戳 last 仍是 50 秒。

漏桶算法核心的地方在於咱們在實現的時候保存最後一次的經過時間,新請求來的時候,用當前的時候減去以前的時間,而後拿到能夠容許經過的請求個數。若是能經過,就把最後一次請求時間改爲當前的時間;不能經過,當前最後一次請求時間仍是不變。若是咱們要添加 burst 的功能,即提早容許它訪問多少個請求的時候,last 時間可能再也不是最後一次放過去的時間,而是相對於以前最後一次請求的時間,它增加了多少個請求的時間,而這個 last 時間可能會超過請求的時候,總的來看主要核心的變量就是 last 的時間戳和 burst。

漏桶/令牌桶算法開源庫

漏桶跟令牌桶的開源庫也是特別多,下列幾個庫很是經典,各個語言和各個包都有實現,再加上由於我從事的工做主要是對 lua 和 golang 比較熟悉,這裏主要講他們:

  • nginx

https://www.nginx.com/blog/ra...

  • openresty/lua-resty-limit-traffic (兩個變量)

https://github.com/openresty/...

  • uber-go/ratelimit

https://github.com/uber-go/ra...

Nginx 使用漏桶實現的,這個你們有興趣去看一看,咱們稍後會講 Nginx 如何配置限制。Openresty 是基於 Nginx 之上使用 lua 編寫模塊的一個框架,它的實現裏主要有兩個參數,第一個參數是剛剛說的 rate,即每秒容許多少個請求;另外一個參數是 burst ,指的是容許提早範圍多少個,好比說每秒鐘容許請求 5 個,這裏還能夠容許它提早放過去 5 個請求。

Uber 是 Uber 公司內部用 go 語言實現的一個 rate 限制。與前面 lua 代碼不加鎖不一樣的是,這個算法加了一個自選鎖。我認爲在高併發場景中,自選鎖是一個挺好的選擇,由於這會有一個 get 和 set 的操做,爲了保證準確確定要加鎖,你們也能夠去看看。

Nginx 配置

Nginx 配置中先考慮限制維度。例如每一個用戶每分鐘只被容許訪問兩次就是按照用戶緯度來限制,或者按照 ip 和 host 來限制,還有就是按照一個 Server,好比一個 Sever 最大能承載每秒鐘 10000 個,超過 10000 個可能要被彈掉了。

以上提到的這些限制維度在 Nginx 裏都能實現,實現方式主要依賴於 Nginx 的兩個模塊:ngx_http_limit_req_module 和 ngx_http_limit_conn_module ,即限制請求數和限制鏈接數。

固定窗口(Fixed Windows)

固定窗口是最好理解的一個算法,應用在分佈式限制場景中很是容易實現,由於它不須要加鎖。

如圖是一個時間戳的窗口,咱們如今規定每分鐘 50 個請求。30 秒來了第 1 個請求,40 秒的時候來了 49 個請求,如今一分鐘的時候來了 50 個請求,因爲已經達到每秒 50 個限制,當 50 秒再來一個請求時會被直接彈掉。等到下一分鐘時,即使一會兒來了 50 個請求也會被放過,由於它已經到了下一分鐘了。

經過分析,你們能看到固定窗口算法是真的很是簡單,你的程序只須要存儲着當前時間窗口內已經有了多少個請求。至於不加鎖,則是由於咱們直接原子操做增長變量,增長完了之後須要注意有沒有超過 50,超過 50,請求被拒絕;沒有超過 50,請求會被接收,因此這裏不會出現 get 跟 set 的狀況。

固然這個也有一個弊端,如上圖所示的 00:30 到 01:30 ,也算是一個 60 秒的時間範圍,但它有 100 個請求了,和咱們限制的要求是不同的,會出現流量高峯的問題。於是這種算法只能保證在一個固定窗口請求不會超過 50 個,若是是隨機一個非固定窗口以內,它的請求就頗有可能超過 50 個,針對這種狀況又提出了滑動窗口的概念。

滑動窗口(Sliding Windows)

如圖所示,每分鐘有 50 個請求,滑動窗口的一分鐘指的的是當前時間往前的一分鐘有多少個請求,例如 01:15 以前至關於從 0:15 到 01:15 。

已知 01:00 到 01:15 有 18 個請求,但 00:15 到 01:00 這個時間段是多少個請求呢?咱們如今知道的是 00:00 到 01:00 是有 42 個請求,而滑動窗口算法的特色在於按比例。能夠將這一分鐘分紅兩段時間,前 15 秒和 後 45 秒,它按這個比例計算 00:15 到 01:00 大約有多少個請求。按比例算不是很精準,由於它只記錄了總數。經過計算

rate=42((60-15)/60)+18=42 0.75 + 18=49.5 requests

算下來是 49.5 個請求,當前這個請求應該是被拒絕掉的。

經過上述操做能夠發現滑動窗口經過比例來保證每一個分鐘內經過值和限制值相近。固然這種不許的狀況能夠經過減少窗口時間改進,例如如今窗口是 1 分鐘,你能夠減少到 10 秒鐘,這樣發生錯誤的機率就會下降,不過減少到 10 秒窗口帶來的額外存儲成本就會很高。雖然這個算法有一些缺點,可是也有很多的公司在用。

滑動窗口(Sliding Windows)是否準確的問題

Cloudflare 對來自 270000 個不一樣來源的 4 億個請求的分析顯示:

  • 0.003% 的請求被錯誤地容許或限制了速率
  • 實際利率與近似利率之間的平均差別爲 6%
  • 儘管產生了略高於閾值的流量(誤報),實際發生率比閾值率高出不到 15%

不少大公司也在使用滑動窗口算法,若是你的限制每分鐘 50 個,你能容忍它每分鐘 40 個或者 60個的話,這種算法方案也是可行的。

固定窗口/滑動窗口應用

剛剛咱們已經講到了滑動窗口限制算法不須要加鎖,使用原子操做便可,因此實現也很是簡單。

  • openresty/lua-resty-limit-traffic (atomic原子操做)

https://github.com/openresty/...

只有 100 的代碼,這裏用了一個 increment 的原子操做,不須要加鎖,對多線程、多進程的實現比較友好,開銷很是少。

  • kong

https://github.com/Kong/kong/...

這是 Kong 實現的滑動窗口應用,不過代碼比較多,你們有興趣的看看,一樣是 lua 的代碼,這個滑動窗口的實現比較全。

分佈式 Rate Limit

不少時候網關可能不止一臺,有兩臺機器的時候就要執行同步操做,例如在漏桶算法中要同步 last 值。同步的策略可使用 DB 庫。不過 DB 庫同步適合請求量比較小的場景。面對請求量特別大的時候,可使用 redis 這種高速的內存庫,同步比較快。固然以上兩種都是限制比較精準的時候可使用,若是不是特別精準,只須要防止服務不被沖垮,我以爲可使用 local 限制。

local 的限制是什麼呢?咱們剛剛提到每分鐘限制 50 個請求,若是你有兩個 Node,能夠平均分配每一個 Node 25 個,這個方案是可行的。若是有權重是 10% 的流量往一邊走,90% 的流量往另外一邊走,能夠相對調大其中一個 Node 的權重,改爲一個 Node 每分鐘限制 45 個,另外一個每分鐘限制 5 個,這樣能夠避免再接入一些 DB 的中間件。

面對這種分佈式的業務場景,APISIX 實現的還不錯 (https://github.com/apache/inc...),它是基於 openresty 作的一個庫,直接使用了 redis 做爲同步,經過固定窗口方法實現。另外一個不錯的是goredis(https://github.com/rwz/redis-...),基於 golang 的庫實現了漏桶算法。goredis 的成本稍微高一些,若是是分佈式的話,用固定窗口和滑動窗口的成本會低不少。

分佈式 Rate Limit 性能優化

前面提到每一個請求過來時都要讀取 redis 或者是 DB 類的數據,例如固定窗口讀取 count 值,去redis 把 count 減 1 會增長延遲,這種狀況下就帶來一些多餘的開銷。爲解決此問題,一些開源的企業級方案推崇不實時同步數值的作法。假如如今每分鐘有兩個請求,Node1 接到一個請求時,並非立刻執行去 redis 執行 last 減 1 的操做,而是等待一段時間,例如 1 秒鐘同步一次,而後同步到 redis ,這樣就減小了同步的次數。

可是這種操做也會帶來一個新的問題。假如如今的限制是每秒容許 2 兩個請求,Node1 和 Node2 在一秒內同時來了兩個請求,由於尚未到一秒,只是在本地計數,因此這 4 個請求被放過。當到了 1 秒的時候,去 redis 減值的時候,纔會發現已經有 4 個請求被放過。不過這種能夠經過限制它下一秒一個請求都不能被經過來補償。

固然這種狀況要看你的容忍程度,這也算是一種解決方案,不過這種解決方案實現的仍是比較少。Kong 算其中一個,它是基於 openresty 開發的一個網關產品,實現了咱們講的定時同步,不須要實時同步 count 值的功能。還有一個是 Cloudflare,也是用滑動窗口去解決性能的問題,不過它沒有開源。

總結

  • 漏桶跟令牌桶很是經典,你們能夠找到本身熟悉的語言算法實現,限制比較準確。
  • 固定窗口實現更簡單,無鎖,適於分佈式,可是會有流量高峯的問題。在一些限制不須要那麼平滑的的場景中可使用,限制相對準確。
  • 滑動窗口實現簡單,也適用於分佈式,且不會有高峯流量的問題,可是限制會有誤差,若是要使用須要容忍限制誤差的問題。

以上是陳卓在又拍雲 Open Talk 公開課上的主要分享內容,演講視頻和 PPT 詳見下方連接:

Open Talk 公開課

推薦閱讀

企業如何高效平穩實現數據遷移

三分鐘瞭解 Python3 的異步 Web 框架 FastAPI

相關文章
相關標籤/搜索