限流的原理以及常用算法
高並發的處理有三個比較常用的手段:緩存、限流和降級。
有了限流,就意味着在處理高並發的時候多了一種保護機制,不用擔心瞬間流量導致系統掛掉或雪崩,最終做到有損服務而不是不服務;但是限流需要評估好,不能亂用,否則一些正常流量出現一些奇怪的問題而導致用戶體驗很差造成用戶流失。
在一個分布式的高可用系統中,限流是必備的操作。這個流可以是:網絡流量,帶寬,每秒處理的事務數,每秒請求數,並發請求數,或者業務上的指標等。比如在參加一些秒殺活動的時候,我們可以看到,有時候會出現“系統繁忙,請稍后再試”或者 “請稍等”的提示,那這個系統就很可能是使用了限流的手段。
常見的限流方式
限制總並發數(如數據庫連接池、線程池)
限制瞬時並發數(nginx的limit_req_conn模塊,用來限制瞬時並發連接數)
限制時間窗口內的平均速率(如Guava的RateLimiter、nginx的limit_req_zone模塊,限制每秒的平均速率)
其他的還有限制遠程接口調用速率、限制MQ的消費速率。
另外還可以根據網絡連接數、網絡流量、CPU或內存負載等來限流。
限流算法
限流的實現主要是依靠限流算法,限流算法主要有下面4種。
1、固定時間窗口(計數器)
計數器固定時間窗口算法是最基礎也是最簡單的一種限流算法。
原理:對一段固定時間窗口內的請求進行計數,如果請求數超過了閾值,則拒絕該請求;如果沒有達到設定的閾值,則接受該請求,且計數加1。當時間窗口結束時,重置計數器為0。
優點:實現簡單,容易理解
缺點:
1)一段時間內服務不可用
2)限流策略過於粗略,不夠平滑,無法應對兩個時間窗口臨界時間內的突發流量。
舉個栗子:
假設這樣一個場景,我們限制用戶一分鍾下單不超過 10 萬次,現在在兩個時間窗口的交匯點,前后一秒鍾內,分別發送 10 萬次請求。也就是說,窗口切換的這兩秒鍾內,系統接收了 20 萬下單請求,這個峰值可能會超過系統閾值,影響服務穩定性。
參考代碼:
1 <?php 2 3 // 固定時間窗口限流 4 class CounterRateLimiter 5 { 6 private $redis; 7 8 public function __construct() 9 { 10 $this->redis = new \Redis(); 11 } 12 13 /** 14 * @param string $key 計數key 15 * @param int $window 窗口時間 16 * @param int $limit 次數限制 17 * @return bool 18 */ 19 public function accessLimit(string $key, int $window, int $limit) 20 { 21 if (!$key) { 22 return false; 23 } 24 $used = $this->redis->incr($key); 25 // redis異常則直接退出 26 if ($used == false) { 27 return true; 28 } 29 // 超過限制,則重置 30 if ($used > $limit) { 31 $ttl = $this->redis->ttl($key); 32 if ($ttl == -1) { 33 $this->redis->expire($key, $window); 34 } 35 return false; 36 } 37 if ($used == 1) { 38 $this->redis->expire($key, $window); 39 } 40 return true; 41 } 42 43 }
2、滑動時間窗口
滑動窗口算法在固定窗口的基礎上,將一個計時窗口分成了若干個小窗口,然后每個小窗口維護一個獨立的計數器。
原理:滑動時間窗口算法是對固定時間窗口算法的一種改進,通過切分更細維度的計數器來記錄一段時間窗口內請求數量,超過閾值拒絕請求。
優點:准確性更高。
缺點:沒有從根本上解決臨界問題。基於時間窗口的限流算法,不管是固定時間窗口還是滑動時間窗口,只能在選定的時間粒度上限流,對選定時間粒度內的更加細粒度的訪問頻率不做限制。
參考代碼:
1 <?php 2 3 // 滑動時間窗口限流 4 class SlideRateLimiter 5 { 6 private $redis; 7 8 public function __construct() 9 { 10 $this->redis = new \Redis(); 11 } 12 13 // 1分鍾限制60次,將1分鍾划分為10個窗口,$window=6s 14 public function accessLimit(string $key, int $window, int $limit) 15 { 16 $count = $this->redis->zCard($key); 17 if ($count >= $limit) { 18 return false; 19 } 20 // 若未超過,將窗口內的訪問數增加 21 $this->increment($key, $window); 22 return true; 23 } 24 25 // 滑動窗口計數增長 26 public function increment(string $key, int $window) 27 { 28 // 當前時間 29 $now = time(); 30 $start = $now - $window * 10; 31 // 清除窗口過期成員 32 $this->redis->zRemRangeByScore($key, 0, $start); 33 // 添加當前時間 34 $this->redis->zAdd($key, $now, $now); 35 // 設置key的過期時間 36 $this->redis->expire($key, $window); 37 } 38 }
3、漏桶算法
思路:水(請求)先進入到漏桶里,漏桶以一定速度向外出水。當水流入速度過大,桶會直接溢出。即請求進入一個固定容量的Queue,若Queue滿,則拒絕新的請求,可以阻塞,也可以拋異常。
這種模型其實非常類似MQ的思想,利用漏桶削峰填谷,使得Queue的下游具有一個穩定流量。
實現:生產者和消費者
優點:從出口處限制請求速率,並不存在上面計數器法的臨界問題,請求曲線始終是平滑的。
缺點:對請求的過濾太精准了,不允許任何的突發流量。比如我們限制每秒下單 10 萬次,那 第10 萬零 1 次的請求,就會被拒絕。大部分業務場景下,雖然限流了,但還是希望系統允許一定的突發流量,這時候就需要令牌桶算法。
4、令牌桶算法
思路:系統以一個恆定的速率往桶里放入令牌。桶的容量是一定的,如果桶已經滿了就不再繼續添加。若有請求需要處理,則從令牌桶里獲取令牌,當桶里沒有令牌,否則拒絕請求或者加入隊列進行排隊等等。
令牌桶算法並不能實際的控制速率。比如,10秒往桶里放入10000個令牌桶,即10秒內只能處理10000個請求,那么qps就是100。但這種模型可以出現1秒內把10000個令牌全部消費完,即qps為10000。所以令牌桶算法實際是限制的平均流速。具體控制的粒度以放令牌的間隔和每次的量來決定。若想要把流速控制的更加穩定,就要縮短間隔時間。
優點:允許一定程度流量突發,但不會超過設置閾值,對用戶友好同時有效保護系統。
缺點:請求異步處理,無法同步返回
漏桶算法VS令牌桶算法
1) 漏桶算法進水速率是不確定的,但是出水速率是一定的,當大量的請求到達時勢必會有很多請求被丟棄。
2) 令牌桶算法會根據限流大小,設置一定的速率往桶中加令牌,這個速率可以很方便的修改,如果我們要提高系統對突發流量的處理,我們可以適當的提高生成token的速率。
總結
1)計數器固定時間窗口算法實現簡單,容易理解。和漏桶算法相比,新來的請求也能夠被馬上處理到。但是流量曲線可能不夠平滑,有“突刺現象”,在窗口切換時可能會產生兩倍於閾值流量的請求。
2)計數器滑動窗口算法作為計數器固定窗口算法的一種改進,有效解決了窗口切換時可能會產生兩倍於閾值流量請求的問題。
3)漏桶算法能夠對流量起到整流的作用,讓隨機不穩定的流量以固定的速率流出,但是不能解決流量突發的問題。
4)令牌桶算法作為漏斗算法的一種改進,除了能夠起到平滑流量的作用,還允許一定程度的流量突發。
以上四種限流算法都有自身的特點,具體使用時還是要結合自身的場景進行選取,沒有最好的算法,只有最合適的算法。比如令牌桶算法一般用於保護自身的系統,對調用者進行限流,保護自身的系統不被突發的流量打垮。如果自身的系統實際的處理能力強於配置的流量限制時,可以允許一定程度的流量突發,使得實際的處理速率高於配置的速率,充分利用系統資源。而漏桶算法一般用於保護第三方的系統,比如自身的系統需要調用第三方的接口,為了保護第三方的系統不被自身的調用打垮,便可以通過漏桶算法進行限流,保證自身的流量平穩的打到第三方的接口上。
實際的場景中完全可以靈活運用,沒有最好的算法,只有最合適的算法。
限流組件
一般而言我們不需要自己實現限流算法來達到限流的目的,不管是接入層限流還是細粒度的接口限流其實都有現成的輪子使用,其實也是用了上述我們所說的限流算法。
- 比如Google Guava 提供的限流工具類 RateLimiter,是基於令牌桶實現的,並且擴展了算法,支持預熱功能。
- 阿里開源的限流框架 Sentinel 中的勻速排隊限流策略,就采用了漏桶算法。
- Nginx 中的限流模塊 limit_req_zone,采用了漏桶算法,還有 OpenResty 中的 resty.limit.req庫等等。
參考鏈接:
https://www.jianshu.com/p/6e4eb31128af
https://juejin.cn/post/6870396751178629127
https://www.cnblogs.com/shijiaqi1066/p/10508115.html