https://www.cnblogs.com/linjiqin/p/9707713.html
https://www.cnblogs.com/my_life/articles/14870151.html
在開發高並發系統時,有三把利器用來保護系統:緩存、降級和限流。那么何為限流呢?顧名思義,限流就是限制流量,就像你寬帶包了1個G的流量,用完了就沒了。通過限流,我們可以很好地控制系統的qps,從而達到保護系統的目的。本篇文章將會介紹一下常用的限流算法以及他們各自的特點。
1、計數器算法
計數器算法是限流算法里最簡單也是最容易實現的一種算法。
比如我們規定,對於A接口來說,我們1分鍾的訪問次數不能超過100個。
那么我們可以這么做:在一開 始的時候,我們可以設置一個計數器counter,每當一個請求過來的時候,counter就加1,如果counter的值大於100並且該請求與第一個 請求的間隔時間還在1分鍾之內,那么說明請求數過多;
如果該請求與第一個請求的間隔時間大於1分鍾,且counter的值還在限流范圍內,那么就重置 counter,具體算法的示意圖如下:
具體的偽代碼如下:
public class CounterTest { public long timeStamp = getNowTime(); public int reqCount = 0; public final int limit = 100; // 時間窗口內最大請求數 public final long interval = 1000; // 時間窗口ms public boolean grant() { long now = getNowTime(); if (now < timeStamp + interval) { // 在時間窗口內 reqCount++; // 判斷當前時間窗口內是否超過最大請求控制數 return reqCount <= limit; } else { timeStamp = now; // 超時后重置 reqCount = 1; return true; } } public long getNowTime() { return System.currentTimeMillis(); } }
這個算法雖然簡單,但是有一個十分致命的問題,那就是臨界問題,我們看下圖:
從上圖中我們可以看到,假設有一個惡意用戶,他在0:59時,瞬間發送了100個請求,並且1:00又瞬間發送了100個請求,那么其實這個用戶在 1秒里面,瞬間發送了200個請求。
我們剛才規定的是1分鍾最多100個請求,也就是每秒鍾最多1.7個請求,用戶通過在時間窗口的重置節點處突發請求, 可以瞬間超過我們的速率限制。用戶有可能通過算法的這個漏洞,瞬間壓垮我們的應用。
聰明的朋友可能已經看出來了,剛才的問題其實是因為我們統計的精度太低。那么如何很好地處理這個問題呢?或者說,如何將臨界問題的影響降低呢?我們可以看下面的滑動窗口算法。
2. 滑動窗口
滑動窗口,又稱rolling window。為了解決這個問題,我們引入了滑動窗口算法。如果學過TCP網絡協議的話,那么一定對滑動窗口這個名詞不會陌生。下面這張圖,很好地解釋了滑動窗口算法:
在上圖中,整個紅色的矩形框表示一個時間窗口,在我們的例子中,一個時間窗口就是一分鍾。
然后我們將時間窗口進行划分,比如圖中,我們就將滑動窗口划成了6格,所以每格代表的是10秒鍾。每過10秒鍾,我們的時間窗口就會往右滑動一格。
每一個格子都有自己獨立的計數器counter,比如當一個請求 在0:35秒的時候到達,那么0:30~0:39對應的counter就會加1。
那么滑動窗口怎么解決剛才的臨界問題的呢?
我們可以看上圖,0:59到達的100個請求會落在灰色的格子中,而1:00到達的請求會落在橘黃色的格 子中。
當時間到達1:00時,我們的窗口會往右移動一格,那么此時時間窗口內的總請求數量一共是200個,超過了限定的100個,所以此時能夠檢測出來觸 發了限流。
我再來回顧一下剛才的計數器算法,我們可以發現,計數器算法其實就是滑動窗口算法。只是它沒有對時間窗口做進一步地划分,所以只有1格。
由此可見,當滑動窗口的格子划分的越多,那么滑動窗口的滾動就越平滑,限流的統計就會越精確。
2、令牌桶算法
令牌桶算法是比較常見的限流算法之一,大概描述如下:
1)、所有的請求在處理之前都需要拿到一個可用的令牌才會被處理;
2)、根據限流大小,設置按照一定的速率往桶里添加令牌;
3)、桶設置最大的放置令牌限制,當桶滿時、新添加的令牌就被丟棄或者拒絕;
4)、請求達到后首先要獲取令牌桶中的令牌,拿着令牌才可以進行其他的業務邏輯,處理完業務邏輯之后,將令牌直接刪除;
5)、令牌桶有最低限額,當桶中的令牌達到最低限額的時候,請求處理完之后將不會刪除令牌,以此保證足夠的限流;
3、漏桶算法
漏桶算法其實很簡單,可以粗略的認為就是注水漏水過程,往桶中以一定速率流出水,以任意速率流入水,當水超過桶流量則丟棄,因為桶容量是不變的,保證了整體的速率。
https://jishuin.proginn.com/p/763bfbd2c459
常見的限流算法主要有:計數器、固定窗口,滑動窗口、漏桶、令牌桶。接下來我們分別介紹下這幾種限流算法。
- <計數器限流>
計數器限流是最簡單粗暴的一種限流算法,例如系統能同時處理100個請求,那么可以在保存一個計數器,處理一個請求,計數器加一,一個請求處理完畢后計數器減一。每次請求進來的時候,先看一眼計數器的值,如果超過閥值則直接拒絕。
在具體實現時,如果該計數器是存在單機內存中,那么就實現了單機限流;而如果存在例如Redis中,集群中的所有節點依次為限流依據,那么就算實現了集群限流算法。
優點:實現簡單,單機例如諸如Java的Atomic等原子類就能實現,集群則通過Redis的incr操作就能快速實現。
缺點:計數器限流無法應對突發的流量增長。例如我們允許的閥值是1W,此時計數器的值是0,那么當1W個請求瞬間全部打進來的時候,很可能服務就頂不住了。這是因為流量的緩緩增加和一下子涌入,對系統所產生的壓力是不一樣的。
況且一般限流都是限制在指定時間間隔內的訪問量,而不是全時段服務的總體處理能力,所以計數器限流不太適合高並發場景下的限流實現。
- <固定窗口限流>
相對於計數器來說,固定窗口限流是以一段時間窗口內的訪問量作為限流的依據,計數器每過一個時間窗口就自動重置。其規則如下:
-
請求次數小於閥值,允許訪問,計數器加1;
-
請求次數大於閥值,拒絕訪問;
-
本時間窗口過了之后,計數器自動清零;
固定窗口限流雖然看起來挺完美,但是它有固定窗口臨界的問題。
例如系統每秒允許1000個請求,假如第一個時間窗口的間隔是0~1秒,但在第0.55秒處一下子涌入了1000個請求,過了1秒后計數清零,此時在1.05秒的時候又一下子涌入了1000個請求。
此時雖然在固定時間窗口內的計數沒有超過閥值,但在全局看來0.55秒~1.05秒這0.5秒內一下子卻涌入了2000個請求,而這對於閥值為1000/s的系統來說是不可承受的。如下圖所示:
而為了解決這個問題,衍生出了滑動窗口限流的算法!
- <滑動窗口限流>
滑動窗口限流解決了固定窗口臨界值的問題,可以保證任意時間窗口內都不會超過限流閥值。
相對於固定窗口,滑動窗口除了需要引入計數器外,還需要額外記錄時間窗口內每個請求到達的時間點。
以時間窗口為1秒為例,規則如下:
-
記錄每次請求的時間;
-
統計每次請求的時間向前推1秒這個時間窗口內的請求數,且1秒前的數據可以刪除;
-
統計的請求數小於閥值則記錄該請求的時間,並允許通過,反之則拒絕該請求;
雖然看起來很OK,但是滑動窗口也無法解決短時間之內集中流量的沖擊。
例如每秒限制1000個請求,但是有可能存在前5毫秒的時候,閥值就被打滿的情況,理想情況下每10毫秒來100個請求,那么系統對流量的處理就會更加平滑。
但在真實場景中是很難控制請求的頻率的。所以為了解決時間窗口類算法的痛點,又出現了漏桶算法。
- <漏桶限流>
漏桶算法的基本思想是,流量持續進入漏桶中,底部則定速處理請求,如果流量進入的速率高於底部請求被處理的速率,且當桶中的流量超過桶的大小時,流量就會被溢出。具體如下圖所示:
漏桶算法的特點是寬進嚴出,無論請求的速率有多大,底部的處理速度都勻速進行。這種算法的特點有點類似於消息隊列的處理機制,一般來說漏桶算法也是由隊列來實現的。
但漏桶算法的這種特點,實際上即是它的優點也是缺點。
有時候面對突發流量,我們往往會希望在保持系統穩定的同時,能更快地處理用戶請求以提升用戶體驗,而不是按部就班的佛系工作。
在這種情況下又出現了令牌桶這樣的限流算法,它在應對突發流量時,可以比漏桶算法更加激進。
- <令牌桶限流>
令牌桶與漏桶的原理類似,只是漏桶是底部勻速處理,而令牌桶則是定速的向桶里塞入令牌,然后請求只有拿到了令牌才會被服務器處理。
具體規則如下:
-
定速的向桶中放入令牌;
-
令牌數量超過桶的限制,則丟棄;
-
請求來了先向桶中索取令牌,索取成功則通過被處理,否則拒絕;
可以看出令牌桶在應對突發流量時,不會想漏桶那樣勻速的處理,而是在短時間內請求可以同時取走桶中的令牌,並及時的被服務器處理。所以在應對突發流量的場景下,令牌桶表現更強。
限流算法總結
經過上述的描述,好像漏桶、令牌桶比時間窗口類算法好多了,那么時間窗口類算法是不是就沒啥用了呢?
其實並不是,雖然漏桶、令牌桶對比時間窗口類算法對流量的整形效果更好,但是它們也有各自的缺點,
例如令牌桶,假如系統上線時沒有預熱,那么可能會出現由於此時桶中還沒有令牌,而導致請求被誤殺的情況;
而漏桶中由於請求是暫存在桶中的,所以請求什么時候能被處理,則是有延時的,這並不符合互聯網業務低延時的要求。
所以令牌桶、漏桶算法更適合阻塞式限流的場景,即后台任務類的限流。
而基於時間窗口的限流則更適合互聯網實施業務限流的場景,即能處理快速處理,不能處理及時響應調用方,避免請求出現過長的等待時間。
微服務限流組件
如果你有興趣實際上也是可以自己實現一個限流組件的,只不過這種輪子已經早有人造好了。
目前市面上比較流行的限流組件主要有:Google Guava提供的限流工具類“RateLimiter”、阿里開源的Sentinel。
其中Google Guava提供的限流工具類“RateLimiter”,是基於令牌桶實現的,並且擴展了算法,支持了預熱功能。
而阿里的Sentinel中的勻速限流策略,就是采用了漏桶算法。
有關Sentinel的使用方法可參考我之前寫的一篇文章《Spring Cloud微服務Sentinel+Apollo限流、熔斷實戰》。
—————END—————
參考資料:
https://mp.weixin.qq.com/s/511v1C6PmuiTokWYuvO0pQ
https://mp.weixin.qq.com/s/CChEb09EpVdGHEJKIagA4Q
<Alibaba Sentinel>
https://github.com/alibaba/Sentinel.git
Sentinel 是一個帶配置中心的分布式緩存,以 "資源名稱" 為統計點,提供了多種方式的限流方案,可以基於 QPS、線程數,甚至系統 load 進行集群規模的限流。Sentinel 在整個生態的位置是這樣的。
單機限流和分布式限流
本質上單機限流和分布式限流的區別其實就在於 “閾值” 存放的位置。
單機限流就上面所說的算法直接在單台服務器上實現就好了,而往往我們的服務是集群部署的。因此需要多台機器協同提供限流功能。
像上述的計數器或者時間窗口的算法,可以將計數器存放至 Tair 或 Redis 等分布式 K-V 存儲中。
例如滑動窗口的每個請求的時間記錄可以利用 Redis 的 zset
存儲,利用ZREMRANGEBYSCORE
刪除時間窗口之外的數據,再用 ZCARD
計數。
像令牌桶也可以將令牌數量放到 Redis 中。
不過這樣的方式等於每一個請求我們都需要去Redis
判斷一下能不能通過,在性能上有一定的損耗,所以有個優化點就是 「批量」。例如每次取令牌不是一個一取,而是取一批,不夠了再去取一批。這樣可以減少對 Redis 的請求。
不過要注意一點,批量獲取會導致一定范圍內的限流誤差。比如你取了 10 個此時不用,等下一秒再用,那同一時刻集群機器總處理量可能會超過閾值。
其實「批量」這個優化點太常見了,不論是 MySQL 的批量刷盤,還是 Kafka 消息的批量發送還是分布式 ID 的高性能發號,都包含了「批量」的思想。
當然分布式限流還有一種思想是平分,假設之前單機限流 500,現在集群部署了 5 台,那就讓每台繼續限流 500 唄,即在總的入口做總的限流限制,然后每台機子再自己實現限流。
限流的難點
可以看到每個限流都有個閾值,這個閾值如何定是個難點。
定大了服務器可能頂不住,定小了就“誤殺”了,沒有資源利用最大化,對用戶體驗不好。
我能想到的就是限流上線之后先預估個大概的閾值,然后不執行真正的限流操作,而是采取日志記錄方式,對日志進行分析查看限流的效果,然后調整閾值,推算出集群總的處理能力,和每台機子的處理能力(方便擴縮容)。
然后將線上的流量進行重放,測試真正的限流效果,最終閾值確定,然后上線。
我之前還看過一篇耗子叔的文章,講述了在自動化伸縮的情況下,我們要動態地調整限流的閾值很難,於是基於TCP擁塞控制的思想,
根據請求響應在一個時間段的響應時間P90或者P99值來確定此時服務器的健康狀況,來進行動態限流。
在他的 Ease Gateway 產品中實現了這套算法,有興趣的同學可以自行搜索。
其實真實的業務場景很復雜,需要限流的條件和資源很多,每個資源限流要求還不一樣。所以我上面就是嘴強王者
。
限流組件
一般而言我們不需要自己實現限流算法來達到限流的目的,不管是接入層限流還是細粒度的接口限流其實都有現成的輪子使用,其實現也是用了上述我們所說的限流算法。
比如Google Guava
提供的限流工具類 RateLimiter
,是基於令牌桶實現的,並且擴展了算法,支持預熱功能。
阿里開源的限流框架Sentinel
中的勻速排隊限流策略,就采用了漏桶算法。
Nginx 中的限流模塊 limit_req_zone
,采用了漏桶算法,還有 OpenResty 中的 resty.limit.req
庫等等。
具體的使用還是很簡單的,有興趣的同學可以自行搜索,對內部實現感興趣的同學可以下個源碼看看,學習下生產級別的限流是如何實現的。