日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

网关速率限制实践

發(fā)布時間:2024/4/11 编程问答 40 豆豆
生活随笔 收集整理的這篇文章主要介紹了 网关速率限制实践 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

速率限制 (Rate Limit) 通過限制調(diào)用 API 的頻率防止 API 過度使用,保護 API 免受意外或惡意的使用,在諸多業(yè)務(wù)場景中得到廣泛應用。日前,又拍云系統(tǒng)開發(fā)工程師陳卓受邀在 Open Talk 公開課上作了題為《又拍云網(wǎng)關(guān)速率限制實踐》的分享,詳細解讀當前常用的算法以及基于網(wǎng)關(guān) nginx/openresty 的實現(xiàn)和配置細節(jié)。以下是直播分享內(nèi)容整理,查看視頻請點擊閱讀原文。

網(wǎng)關(guān)速率限制是一種防御服務(wù)性措施,公共服務(wù)需要借其保護自己免受過度使用,使用速率限制主要有三個好處:

  • 提升用戶體驗:用戶在使用公共服務(wù)時總會面臨一些資源增強和共享的問題,例如 CPU,當一個用戶不管是有意或無意地過度使用 API 時,勢必會對其他的用戶造成一些影響。

  • 更加安全:我們的服務(wù)、CPU、內(nèi)存其實都是有一定的限制,過度訪問勢必會影響到服務(wù)的穩(wěn)定性。假如有四個服務(wù),每個服務(wù)能承載 100 個請求,當其中一個服務(wù)超過 100 個請求時就可能會宕機,其它三個服務(wù)在接收到超過 100 個服務(wù)請求時,也會接著連續(xù)宕機,這會造成服務(wù)不可用。

  • 減少開銷:現(xiàn)在很多服務(wù)都是放到公有云上,內(nèi)存、CPU 和流量都是有成本的,有一些按量計費,使用多少花多少錢,這種情況會產(chǎn)生一些不必要的開銷。

RateLimit 的幾種算法

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

漏桶(Leaky Bucket)

如上圖所示,用戶請求都被放進桶里,當桶滿了以后,請求會被拒絕掉。桶的底部有一個孔,請求會以一定的速率被放過,比如說現(xiàn)在是限制每分鐘 10 個請求,意味著每隔 6 秒鐘就會有一個請求通過。漏桶算法的特點在于其通過請求的速率是恒定的,可以將流量整形的非常均勻,即便同時有 100 個請求也不會一次性通過,而是按一定間隔慢慢放行,這對后端服務(wù)迎接突發(fā)流量非常友好。

令牌桶(Token Bucket)

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

需要說明的是,令牌的個數(shù)是按一定的速率投放的,每分鐘放 10 個令牌,那么能通過的請求肯定也是每分鐘 10 個。假如勻速放令牌, 6 秒鐘放一個令牌,最終結(jié)果和每分鐘放 10 個令牌是一樣的。

漏桶(Leaky Bucket)算法實現(xiàn)

由于令牌桶跟漏桶的實現(xiàn)效果差不多,這里主要細講漏桶的算法和實現(xiàn)。先假設(shè)速率限制是每分鐘 3 個請求,即每 20 秒鐘放行一個請求。如圖所示,假設(shè)第 10 秒進來第一個請求,因為之前一直都沒有請求進入,所以該請求被允許通過。記錄下最后一次的訪問時間,即為本次請求通過時間點。

現(xiàn)在第 20 秒又過來一個請求,20 秒相對于 10 秒鐘經(jīng)過了 10 秒鐘,按照計算只允許被通過 0.5 個請求,那請求就被拒絕掉了。這個 last 值還是保持最后一次一個請求通過的時間。第 30 秒又來了一個請求:如果將 30 秒看作是最后一次更新時間,相當于是 30 秒減 10 秒,也就是經(jīng)過了 20 秒,而我們的限制是每 20 秒允許 1 個請求,那么這個請求會被放過去,last 值現(xiàn)在已經(jīng)變成了 30 秒。

通過上述分析可以發(fā)現(xiàn),漏桶限制非常嚴格,即便請求是第 29 秒進來也不能被通過,因為必須要經(jīng)過 20 秒才允許通過一個請求,這可能會給業(yè)務(wù)帶來一個問題:例如現(xiàn)在每分鐘允許通過 3 個請求,用戶可能需要在前 10 秒鐘把三個請求發(fā)完,這種需求在這種算法下不會被允許。因為從發(fā)掉第一個請求到發(fā)第二個請求必須要間隔 20 秒才可以,為了彌補這種缺陷,需要引用另外一個參數(shù) burst(爆發(fā)),允許突然爆發(fā)的請求。如下圖中所示,40 秒距離 30 秒實際上只經(jīng)過了 10 秒鐘,按照之前的算法計算只被允許訪問 0.5 個請求,實際上應該被拒絕掉,但是我們允許它提前多訪問一個請求(burst 為1),算下來就是 0.5+1=1.5 個請求。

需要注意的是,雖然我們當前時間是 40 秒,但我們最后需要更新請求時間到 50 秒,這是因為現(xiàn)在已經(jīng)超量使用進入到下一個時間段了,相當于是提前放行一個請求,最后一個 last 時間是 30 秒,應該加 20 秒到 50 秒。這也是該算法實現(xiàn)的一個特點,很多算法也都有 burst 的功能,即允許提前訪問。

45 秒又來了一個請求,盡管這個請求來時,我們也允許它提前訪問。但由于上一次最后訪問時間已經(jīng)是 50 秒了,而且在通過計算得出不到一個請求時,這一個請求也就被拒絕掉了,時間戳 last 還是 50 秒。

**漏桶算法核心的地方在于我們在實現(xiàn)的時候保存最后一次的通過時間,新請求來的時候,用當前的時候減去之前的時間,然后拿到可以允許通過的請求個數(shù)。如果能通過,就把最后一次請求時間改成當前的時間;不能通過,當前最后一次請求時間還是不變。**如果我們要添加 burst 的功能,即提前允許它訪問多少個請求的時候,last 時間可能不再是最后一次放過去的時間,而是相對于之前最后一次請求的時間,它增長了多少個請求的時間,而這個 last 時間可能會超過請求的時候,總的來看主要核心的變量就是 last 的時間戳和 burst。

漏桶/令牌桶算法開源庫

漏桶跟令牌桶的開源庫也是特別多,下列幾個庫非常經(jīng)典,各個語言和各個包都有實現(xiàn),再加上因為我從事的工作主要是對 lua 和 golang 比較熟悉,這里主要講他們:

  • nginx

https://www.nginx.com/blog/rate-limiting-nginx/

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

https://github.com/openresty/lua-resty-limit-traffic/blob/master/lib/resty/limit/req.lua

  • uber-go/ratelimit

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

Nginx 使用漏桶實現(xiàn)的,這個大家有興趣去看一看,我們稍后會講 Nginx 如何配置限制。Openresty 是基于 Nginx 之上使用 lua 編寫模塊的一個框架,它的實現(xiàn)里主要有兩個參數(shù),第一個參數(shù)是剛剛說的 rate,即每秒允許多少個請求;另一個參數(shù)是 burst ,指的是允許提前范圍多少個,比如說每秒鐘允許請求 5 個,這里還可以允許它提前放過去 5 個請求。

Uber 是 Uber 公司內(nèi)部用 go 語言實現(xiàn)的一個 rate 限制。與前面 lua 代碼不加鎖不同的是,這個算法加了一個自選鎖。我認為在高并發(fā)場景中,自選鎖是一個挺好的選擇,因為這會有一個 get 和 set 的操作,為了保證準確肯定要加鎖,大家也可以去看看。

Nginx 配置

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

以上提到的這些限制維度在 Nginx 里都能實現(xiàn),實現(xiàn)方式主要依賴于 Nginx 的兩個模塊:ngx_http_limit_req_module 和 ngx_http_limit_conn_module ,即限制請求數(shù)和限制連接數(shù)。

固定窗口(Fixed Windows)

固定窗口是最好理解的一個算法,應用在分布式限制場景中非常容易實現(xiàn),因為它不需要加鎖。

如圖是一個時間戳的窗口,我們現(xiàn)在規(guī)定每分鐘 50 個請求。30 秒來了第 1 個請求,40 秒的時候來了 49 個請求,現(xiàn)在一分鐘的時候來了 50 個請求,由于已經(jīng)達到每秒 50 個限制,當 50 秒再來一個請求時會被直接彈掉。等到下一分鐘時,即便一下子來了 50 個請求也會被放過,因為它已經(jīng)到了下一分鐘了。

通過分析,大家能看到固定窗口算法是真的非常簡單,你的程序只需要存儲著當前時間窗口內(nèi)已經(jīng)有了多少個請求。至于不加鎖,則是因為我們直接原子操作增加變量,增加完了以后需要注意有沒有超過 50,超過 50,請求被拒絕;沒有超過 50,請求會被接收,所以這里不會出現(xiàn) get 跟 set 的情況。

當然這個也有一個弊端,如上圖所示的 00:30 到 01:30 ,也算是一個 60 秒的時間范圍,但它有 100 個請求了,和我們限制的要求是不一樣的,會出現(xiàn)流量高峰的問題。因而這種算法只能保證在一個固定窗口請求不會超過 50 個,如果是隨機一個非固定窗口之內(nèi),它的請求就很有可能超過 50 個,針對這種情況又提出了滑動窗口的概念。

滑動窗口(Sliding Windows)

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

已知 01:00 到 01:15 有 18 個請求,但 00:15 到 01:00 這個時間段是多少個請求呢?我們現(xiàn)在知道的是 00:00 到 01:00 是有 42 個請求,而滑動窗口算法的特點在于按比例。可以將這一分鐘分成兩段時間,前 15 秒和 后 45 秒,它按這個比例計算 00:15 到 01:00 大約有多少個請求。按比例算不是很精準,因為它只記錄了總數(shù)。通過計算

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

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

通過上述操作可以發(fā)現(xiàn)滑動窗口通過比例來保證每個分鐘內(nèi)通過值和限制值相近。當然這種不準的情況可以通過減小窗口時間改進,例如現(xiàn)在窗口是 1 分鐘,你可以減小到 10 秒鐘,這樣發(fā)生錯誤的概率就會降低,不過減小到 10 秒窗口帶來的額外存儲成本就會很高。雖然這個算法有一些缺點,但是也有不少的公司在用。

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

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

  • 0.003% 的請求被錯誤地允許或限制了速率

  • 實際利率與近似利率之間的平均差異為 6%

  • 盡管產(chǎn)生了略高于閾值的流量(誤報),實際發(fā)生率比閾值率高出不到 15%

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

固定窗口/滑動窗口應用

剛剛我們已經(jīng)講到了滑動窗口限制算法不需要加鎖,使用原子操作即可,所以實現(xiàn)也非常簡單。

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

https://github.com/openresty/lua-resty-limit-traffic/blob/master/lib/resty/limit/count.lua

只有 100 的代碼,這里用了一個 increment 的原子操作,不需要加鎖,對多線程、多進程的實現(xiàn)比較友好,開銷非常少。

  • kong

https://github.com/Kong/kong/tree/master/kong/plugins/rate-limiting

這是 Kong 實現(xiàn)的滑動窗口應用,不過代碼比較多,大家有興趣的看看,同樣是 lua 的代碼,這個滑動窗口的實現(xiàn)比較全。

分布式 Rate Limit

很多時候網(wǎng)關(guān)可能不止一臺,有兩臺機器的時候就要執(zhí)行同步操作,例如在漏桶算法中要同步 last 值。同步的策略可以使用 DB 庫。不過 DB 庫同步適合請求量比較小的場景。面對請求量特別大的時候,可以使用 redis 這種高速的內(nèi)存庫,同步比較快。當然以上兩種都是限制比較精準的時候可以使用,如果不是特別精準,只需要防止服務(wù)不被沖垮,我覺得可以使用 local 限制。

local 的限制是什么呢?我們剛剛提到每分鐘限制 50 個請求,如果你有兩個 Node,可以平均分配每個 Node 25 個,這個方案是可行的。如果有權(quán)重是 10% 的流量往一邊走,90% 的流量往另一邊走,可以相對調(diào)大其中一個 Node 的權(quán)重,改成一個 Node 每分鐘限制 45 個,另一個每分鐘限制 5 個,這樣可以避免再接入一些 DB 的中間件。

面對這種分布式的業(yè)務(wù)場景,APISIX 實現(xiàn)的還不錯 (https://github.com/apache/incubator-apisix/blob/master/apisix/plugins/limit-count/limit-count-redis.lua),它是基于 openresty 做的一個庫,直接使用了 redis 作為同步,通過固定窗口方法實現(xiàn)。另一個不錯的是goredis(https://github.com/rwz/redis-gcra/blob/master/vendor/perform_gcra_ratelimit.lua),基于 golang 的庫實現(xiàn)了漏桶算法。goredis 的成本稍微高一些,如果是分布式的話,用固定窗口和滑動窗口的成本會低很多。

分布式 Rate Limit 性能優(yōu)化

前面提到每個請求過來時都要讀取 redis 或者是 DB 類的數(shù)據(jù),例如固定窗口讀取 count 值,去redis 把 count 減 1 會增加延遲,這種情況下就帶來一些多余的開銷。為解決此問題,一些開源的企業(yè)級方案推崇不實時同步數(shù)值的做法。假如現(xiàn)在每分鐘有兩個請求,Node1 接到一個請求時,并不是馬上執(zhí)行去 redis 執(zhí)行 last 減 1 的操作,而是等待一段時間,例如 1 秒鐘同步一次,然后同步到 redis ,這樣就減少了同步的次數(shù)。

但是這種操作也會帶來一個新的問題。假如現(xiàn)在的限制是每秒允許 2 兩個請求,Node1 和 Node2 在一秒內(nèi)同時來了兩個請求,因為還沒有到一秒,只是在本地計數(shù),所以這 4 個請求被放過。當?shù)搅?1 秒的時候,去 redis 減值的時候,才會發(fā)現(xiàn)已經(jīng)有 4 個請求被放過。不過這種可以通過限制它下一秒一個請求都不能被通過來補償。

當然這種情況要看你的容忍程度,這也算是一種解決方案,不過這種解決方案實現(xiàn)的還是比較少。Kong 算其中一個,它是基于 openresty 開發(fā)的一個網(wǎng)關(guān)產(chǎn)品,實現(xiàn)了我們講的定時同步,不需要實時同步 count 值的功能。還有一個是 Cloudflare,也是用滑動窗口去解決性能的問題,不過它沒有開源。

總結(jié)

  • 漏桶跟令牌桶非常經(jīng)典,大家可以找到自己熟悉的語言算法實現(xiàn),限制比較準確。

  • 固定窗口實現(xiàn)更簡單,無鎖,適于分布式,但是會有流量高峰的問題。在一些限制不需要那么平滑的的場景中可以使用,限制相對準確。

  • 滑動窗口實現(xiàn)簡單,也適用于分布式,且不會有高峰流量的問題,但是限制會有偏差,如果要使用需要容忍限制偏差的問題。

以上是陳卓在又拍云 Open Talk 公開課上的主要分享內(nèi)容,演講視頻和 PPT 詳見下方鏈接:

Open Talk 公開課

總結(jié)

以上是生活随笔為你收集整理的网关速率限制实践的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。