限流詳解

背景

在開發高併發系統時,有不少手段能夠用來保護系統,如:緩存、降級和限流等。在某些場景下並不能用緩存和降級來解決,好比稀缺資源(秒殺、搶購)、寫服務(如評論、下單)、頻繁的複雜查詢(評論的最後幾頁)等,所以須要有一種手段來限制這些場景下的併發/請求量,這種手段就是限流。html

限流的目的是經過對併發訪問、請求進行限流或者一個時間窗口內的請求進行限速來保護系統,一旦達到限制速率則能夠拒絕服務(定向到錯誤頁或告知資源沒有了)、排隊或者等待(好比秒殺、評論、下單)、降級(返回兜底數據或者默認值,如商品詳情默認有貨)。在壓測時,咱們能找到每一個系統的處理峯值,而後經過設定峯值閥值,來防止當系統過載時,經過拒絕過載的請求來保障系統可用。另外,也應根據系統的吞吐量,響應時間、可用率來動態調整限流閥值。java

限流算法

常見的限流算法有:令牌桶、漏桶。計數器也能夠用來進行粗暴的限流。node

令牌桶算法

在 Wikipedia 上,令牌桶算法是這麼描述的:git

  1. 每秒會有r個令牌放入桶中,或者說,每過 1/r 秒桶中增長一個令牌
  2. 桶中最多存放 b 個令牌,若是桶滿了,新放入的令牌會被丟棄
  3. 當一個n字節的數據包到達時,消耗n個令牌,而後發送該數據包
  4. 若是桶中可用令牌小於n,則該數據包將被緩存或丟棄

                                        

                                                        圖-令牌桶算法示意圖github

漏桶算法

  1. 一個固定容量的漏桶,按照常量固定速率流出水滴;
  2. 若是桶是空的,則不需流出水滴;
  3. 能夠以任意速率流入水滴到漏桶;
  4. 若是流入水滴超出了桶的容量,則流入的水滴溢出了(被丟棄),而漏桶容量是不變的。

                                                

                                                        圖-令牌桶限流算法算法

令牌桶限制的是平均流入速率(容許突發請求,只要有令牌就能夠處理,容許必定程度的突發流量)數據庫

漏桶限制的是常量流出速率,從而能夠平滑突發流入速率api

令牌桶容許必定程度的突發,而漏桶主要目的是平滑流入速率。緩存

計數器限流

                                    

                                                         圖-計數器限流示意圖服務器

它是限流算法中最簡單最容易的一種算法,好比咱們要求某一個接口,1分鐘內的請求不能超過10次,咱們能夠在開始時設置一個計數器,

每次請求,該計數器+1;若是該計數器的值大於10而且與第一次請求的時間間隔在1分鐘內,那麼說明請求過多;若是該請求與第一次請求的時間間隔大於1分鐘,而且該計數器的值還在限流範圍內,那麼重置該計數器。具體代碼以下:

public class CounterDemo {
    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;
        }
    }
}

不過,以上代碼有致命問題,當遇到惡意請求,在0:59時,瞬間請求100次,而且在1:00請求100次,那麼這個用戶在1秒內請求了200次,用戶能夠在重置節點突發請求,而瞬間超過咱們設置的速率限制,用戶可能經過算法漏洞擊垮咱們的應用。以下圖,如何解決呢,看下邊的滑動窗口算法。

                                    

                                                圖-計數器限流漏洞示意圖

滑動窗口

                                    

                                                    圖-滑動窗口限流示意圖

在上圖中,整個紅色矩形框是一個時間窗口,在咱們的例子中,一個時間窗口就是1分鐘,而後咱們將時間窗口進行劃分,如上圖咱們把滑動窗口劃分爲6格,因此每一格表明10秒,每超過10秒,咱們的時間窗口就會向右滑動一格,每一格都有本身獨立的計數器,例如:一個請求在0:35到達,那麼0:30到0:39的計數器會+1,那麼滑動窗口是怎麼解決臨界點的問題呢?如上圖,0:59到達的100個請求會在灰色區域格子中,而1:00到達的請求會在紅色格子中,窗口會向右滑動一格,那麼此時間窗口內的總請求數共200個,超過了限定的100,因此此時可以檢測出來觸發了限流。回頭看看計數器算法,會發現,其實計數器算法就是窗口滑動算法,只不過計數器算法沒有對時間窗口進行劃分,因此是一格。因而可知,當滑動窗口的格子劃分越多,限流的統計就會越精確。滑動窗口算法實現

應用級限流

限制總併發/鏈接/請求數

應用系統必定會存在極限併發/請求數,即總有一個TPS/QPS閥值。

Tomcat,Connector配置參數:acceptCount(超出排隊大小,則拒絕鏈接),maxConnections(瞬時最大鏈接數,超出的會排隊等待),maxThreads(處理請求的最大線程數)

MySQL的max_connections、Redis的tcp-backlog有相似限制鏈接數的參數配置。

限制總資源數

稀缺資源如:數據庫鏈接、線程可使用池化技術限制總資源數,如鏈接池、線程池。

限制某個接口的總併發/請求數

某些接口可能會有突發訪問狀況,爲避免訪問量太大形成崩潰,如搶購業務,那麼此時就須要限制這個接口的總併發/請求數了,超出限額,要麼讓用戶排隊,要麼告訴用戶沒貨了,這對用戶來講是能夠接受的。由於粒度比較細,能夠爲每一個接口都設置相應的閥值。可使用Java中的AtomicLong或者Semaphore進行限流。

限制某個接口的時間窗請求數

限制一個時間窗口內的請求數

平滑限流某個接口的請求數

某些場景下須要對突發請求進行整形,整形爲平均速率請求處理,這個時候有兩種算法知足咱們的場景,令牌桶和漏桶算法。Guava框架RateLimiter類提供了令牌桶算法實現,能夠直接拿來使用,可用於平滑突發限流(SmoothBursty)和平滑預熱限流(SmoothWarmingUp)實現。

SmoothBursty容許必定程度的突發,會有人擔憂若是容許這種突發,假設忽然來了很大的流量,那麼系統極可能扛不住這種突發。所以,須要一種平滑速率的限流工具,從而在系統啓動後慢慢趨於平均固定速率(即剛開始速率小一些,而後慢慢趨於咱們設置的固定速率),SmoothWarmingUp實現了這種需求,其能夠認爲是漏桶算法,可是在某些特殊場景下又不太同樣。參數warmupPeriod表示從冷啓動速率過渡到平均速率的時間間隔。

分佈式限流

分佈式限流算法的是指算法能夠分佈式部署在多臺機器上面,多臺機器協同提供限流功能,能夠對同一接口或者服務作限流。分佈式限流算法相較於單機的限流算法,最大的區別就是接口請求計數器須要中心化存儲

解決方案可使用Redis+Lua或者Nginx+Lua技術進行實現,經過這兩種技術能夠實現高併發和高性能。在採用Redis作分佈式限流時須要考慮的點有:

  • 數據一致性問題

接口限流過程包含讀取-判斷-寫入三步,在併發狀況下這3步CAS操做 (compare and swap) 存在race condition,但在分佈式環境下引入分佈式鎖代價較大,能夠考慮藉助Redis單線程工做模式+Lua腳本完美的支持上述操做的原子性。

  • 超時問題

若是Redis訪問超時,會嚴重影響接口的響應時間甚至致使接口響應超時,這個反作用是不能接受的。因此在咱們訪問 Redis 時須要設置合理的超時時間,一旦超時,斷定爲限流失效,繼續執行接口邏輯。Redis訪問超時時間的設置既不能太大也不能過小,太大可能會影響到接口的響應時間,過小可能會致使太多的限流失效。咱們能夠經過壓測或者線上監控,獲取到Redis訪問時間分佈狀況,再結合服務接口能夠容忍的限流延遲時間,權衡設置一個較合理的超時時間。

  • 性能問題

在應用分佈式限流算法時,必定要考量限流算法的性能是否知足應用場景,若是微服務接口的TPS已經超過了限流框架自己的TPS,則限流功能會成爲性能瓶頸影響接口自己的性能。分佈式限流算法的性能瓶頸主要在中心計數器Redis,在存在瓶頸的狀況下結合Redis sharding將分佈式限流進行分片,或者當併發量太大時降級爲應用級限流。

  • 其它問題

由於Redis的限制(Lua中有寫操做不能使用帶隨機性質的讀操做,如TIME)不能在Redis Lua中使用TIME獲取時間戳,所以只好從應用獲取而後傳入,在某些極端狀況下(機器時鐘不許的狀況下),限流會存在一些小問題。

如何進行分佈式環境下機器時間同步?

參考ntp時間服務器 時間同步分佈式系統中的時間(一)——時間的同步

接入層限流

接入層一般指請求流量的入口,該層的主要目的有:負載均衡、非法請求過濾、請求聚合、緩存、降級、限流等等。對於Nginx接入層限流可使用Nginx自帶了兩個模塊:鏈接數限流模塊ngx_http_limit_conn_module和漏桶算法實現的請求限流模塊ngx_http_limit_req_module。還可使用OpenResty提供的Lua限流模塊lua-resty-limit-traffic進行更復雜的限流場景。

limit_conn

limit_conn是對某個KEY對應的總的網絡鏈接數進行限流。能夠按照IP來限制IP維度的總鏈接數,或者按照服務域名來限制某個域名的總鏈接數。可是記住不是每個請求鏈接都會被計數器統計,只有那些被Nginx處理的且已經讀取了整個請求頭的請求鏈接纔會被計數器統計。

limit_req

limit_req是漏桶算法實現,用於對指定KEY對應的請求進行限流,好比按照IP維度限制請求速率,並有兩種用法:平滑模式(delay)和容許突發模式(nodelay)。

lua-resty-limit-traffic

OpenResty提供了lua限流模塊lua-resty-limit-traffic,經過它能夠按照更復雜的業務邏輯進行動態限流處理。

限流思考

選擇單機限流仍是分佈式限流

所謂單機限流是指:獨立的對集羣中的每臺實例進行接口限流,好比限制每臺實例接口訪問的頻率爲最大 1000 次 / 秒,單機限流通常使用單機限流算法;所謂的分佈式限流是指:提供服務級的限流限制對微服務集羣的訪問頻率,好比限制A調用方每分鐘最多請求1萬次「用戶服務」,分佈式限流既可使用單機限流算法也可使用分佈式限流算法。

單機限流的初衷是防止突發流量壓垮服務器,因此比較適合針對併發作限制分佈式限流適合作細粒度限流或者訪問配額,不一樣的調用方對不一樣的接口執行不一樣的限流規則,因此比較適合針對 hits per second 限流。從保證系統可用性的角度來講,單機限流更具優點,從防止某調用方過分競爭服務資源來講,分佈式限流更加適合。

分佈式限流與微服務之間如何部署

方式一,在接入層(api-gateway)集成限流功能,這種集成方式是在微服務架構下,有 api-gateway 的前提下,最合理的架構模式。若是 api-gateway 是單實例部署,使用單機限流算法便可。若是 api-gateway 是多實例部署,爲了作到服務級別的限流就必須使用分佈式限流算法。

方式二,把限流功能封裝爲RPC服務,這種部署架構,性能瓶頸會出如今微服務與限流服務之間的RPC通訊上,通常不建議採用。

方式三,限流功能集成在微服務系統內,這種架構模式不須要再獨立部署服務,減小了運維成本,但限流代碼會跟業務代碼有一些耦合,不過,能夠將限流功能集成在切面層,儘可能跟業務代碼解耦。若是作服務級的分佈式限流,必須使用分佈式限流算法,若是是針對每臺微服務實例進行單機限流,使用單機限流算法就能夠。

如何選擇限流算法

令牌桶和漏桶算法比較適合阻塞式限流(也能夠當即返回失敗,從而達到否決式限流的效果),好比一些後臺job類的限流,超過了最大訪問頻率以後,請求並不會被拒絕,而是會被阻塞到有令牌後再繼續執行,進行排隊請求。對於像微服務接口這種對響應時間比較敏感的限流場景,會比較適合選擇基於時間窗口的否決式限流算法,直接拒絕請求,好比返回HTTP code 429,其中滑動時間窗口限流算法空間複雜度較高,內存佔用會比較多,因此對比來看,儘管固定時間窗口算法處理臨界突發流量的能力較差,但實現簡單,而簡單帶來了好的性能和不容易出錯,因此固定時間窗口算法也不失是一個好的微服務接口限流算法。

如何配置合理的限流規則

限流規則包含三個部分:時間粒度,接口粒度,最大限流值。限流規則設置是否合理直接影響到限流是否合理有效。

對於限流時間粒度的選擇,咱們既能夠選擇 1 秒鐘不超過 1000 次,也能夠選擇 10 毫秒不超過 10 次,還能夠選擇 1 分鐘不超過 6 萬次,雖然看起這幾種限流規則都是等價的,但過大的時間粒度會達不到限流的效果,好比限制 1 分鐘不超過 6 萬次,就有可能 6 萬次請求都集中在某一秒內;相反,太小的時間粒度會削足適履致使誤殺不少本不該該限流的請求,由於接口訪問在細時間粒度上隨機性很大。因此,儘管越細的時間粒度限流整形效果越好,流量曲線越平滑,但也並非越細越合適。

對於訪問量巨大的接口限流,好比秒殺,雙十一,這些場景下流量可能都集中在幾秒內,TPS 會很是大,幾萬甚至幾十萬,須要選擇相對小的限流時間粒度。相反,若是接口 TPS 很小,建議使用大一點的時間粒度,好比限制 1 分鐘內接口的調用次數不超過 1000 次,若是換算成:一秒鐘不超過 16 次,這樣的限制就有點不合理,即使一秒內超過 16 次,也並無理由就拒絕接口請求,由於對於咱們系統的處理能力來講,16 次 / 秒的請求頻率太微不足道了。即使 1000 次請求都集中在 1 分鐘內的某一秒內,也並不會影響到系統的穩定性,因此 1 秒鐘 16 次的限制意義不大。

如何評判限流功能是否正確有效

如何測試限流功能正確有效呢?儘管能夠經過模擬流量或者線上流量回放等手段來測試,可是最有效的測試方法仍是:經過導流的方式將流量集中到一小組機器上作真實場景的測試。對於測試結果,咱們至少須要記錄每一個請求的以下信息:對應接口,請求時間點,限流結果 (經過仍是熔斷),而後根據記錄的數據作對比分析。

除了事先驗證以外,咱們還須要時刻監控限流的工做狀況,實時瞭解限流功能是否運行正常。一旦發生限流異常,可以在不重啓服務的狀況下,作到熱更新限流配置:包括開啓關閉限流功能,調整限流規則,更換限流算法等。

節流

有時候咱們想在特定時間窗口內對重複的相同事件最多隻處理一次,或者想限制多個連續相同事件最小執行時間間隔,那麼可以使用節流(Throttle)實現,其防止多個相同事件連續重複執行。節流主要有以下幾種用法:throttleFirst、throttleLast、throttleWithTimeout。能夠參考限流詳解之節流

參考資料

《億級流量網站架構核心技術——跟開濤學搭建高可用高併發系統》

微服務接口限流的設計與思考

分佈式限流

關於Guava中令牌桶算法RateLimiter的理解

相關文章
相關標籤/搜索