你知道四種常見的限流算法以及怎麼用嗎

java旭陽 發佈 2023-01-19T08:03:38.819525+00:00

簡介限流顧名思義是對流量大小進行限制,防止請求數量超過系統的負載能力,導致系統崩潰,起到保護作用。現實生活中限流也隨處可見,節假日出門旅行的人數會劇增,對於旅遊景點來說往往會不堪重負,如果不進行人數控制,對整個景點的壓力會非常大,遊客的體驗也會非常差,還容易出現安全事故等危險。

簡介

限流顧名思義是對流量大小進行限制,防止請求數量超過系統的負載能力,導致系統崩潰,起到保護作用。
現實生活中限流也隨處可見,節假日出門旅行的人數會劇增,對於旅遊景點來說往往會不堪重負,如果不進行人數控制,對整個景點的壓力會非常大,遊客的體驗也會非常差,還容易出現安全事故等危險。


具體到程序,限流可以有以下幾種場景

  • 限制某個接口每秒最多訪問多少次
  • 限制某個ip每秒最多訪問多少次
  • 限制某個用戶或某個來源每秒最多訪問多少次
  • 限制某些用戶下載速度每秒最多多少kb
  • 禁止某些用戶或ip的訪問

限流起到了保護作用,那麼如何限呢?如果限制得太嚴,保護是保護到了,但是系統的處理能力下降了,體驗會很差;如果限制得太松,就會被一些突增流量衝擊到,或者被黑客利用進行安全攻擊。如何限流需要根據系統的負載來評估,系統的負載和處理能力是動態的,例如平時的qps是1000,雙11一般會進行擴容,也就是加服務節點,qps可能就是5000,這個時候系統處理能力變強的,限流策略也應該相應的調整。還有一種是出於安全的限流,例如同一個客戶端ip 1s內對系統發出上萬次請求,這種可以確定就是安全攻擊,很可能是有人惡意破壞,或者是一些爬蟲,這種可以限制請求數,超出的就直接拒絕。

固定窗口

固定窗口的思想和實現非常簡單,就是統計每個固定每個時間窗口的請求數,超過則拒絕。


如圖我們定義了窗口大小為1s,最大請求數100,窗口內超過100的請求數將被拒絕。實現上也非常簡單,利用redis的incr自增計數,當前時間秒作為緩存key,每次自增後判斷是否超過指定大小即可。
固定窗口的問題是容易出現「突刺現象」,例如在1s內,100個請求都是在前100ms過來的,那麼後面的900ms的請求都會被拒絕,而系統此時是空閒的。另外還有「臨界問題」,如果100個請求是在後100ms過來的,而下一個1s的100個請求在前100ms過來,此時系統在這200ms內就需要處理200個請求,跟我們想要的不符合。到這裡我們很容易想到,1s這個範圍太大了,縮小一些就更好了,這種把固定窗口拆成更多個小窗口的做法就是滑動窗口算法了。

滑動窗口

滑動窗口的思想是將固定窗口拆成更多個小窗口,隨著時間的推移,窗口不斷的滑動,統計也在不斷的變化。窗口拆分的越多,滑動就會越平滑,統計就會越精確,所消耗的資源就會越多。滑動窗口如果只拆為1個窗口,就退化為固定窗口。
滑動窗口算法可以解決上面固定窗口的問題,像hystrix和sentinel中都使用該算法進行數據統計,用於限流熔斷等策略處理。如hystrix中圖所示,在一個窗口內拆分了10個桶(bucket),隨著時間的推移,會創建新的桶也會丟棄過期的桶,當然窗口的大小和拆分桶的數量都是可配置的。

漏桶

漏桶算法的思想是將請求先放到一個桶中,然後像滴水一樣不斷的從中取出請求執行,桶滿則溢,後面的請求會被拒絕。

漏桶算法的特點是流入速度不確定,但是流出速度是確定的,漏桶可以很平滑,均衡的處理請求,但是無法應對短暫的突發流量。

令牌桶

令牌桶算法的思想是不斷的生成令牌放到一個桶中,請求到來時到桶中申請令牌,申請得到就執行,申請不到就拒絕。如果桶中的令牌滿了,新生成的令牌也會丟棄。

與漏桶不同的是,令牌桶是流入速度確定(生成令牌的速度),流出速度不確定,所以它不想漏桶一樣可以均衡的處理請求,但是由於有令牌桶這個緩衝,一旦有突增的流量,令牌桶里已有的令牌可以短暫的應對突發流量,由於流出速度是不限制的,此時桶中已有的令牌都可以被申請到,請求一下子就會到我們的服務,給系統帶來一定的壓力,所以桶的大小需要合適,不宜過大。舉個栗子:令牌桶的大小是1000,每秒放100個令牌,經過一段時間後,請求有空閒時,桶里的令牌就會積壓,最終保存了滿1000個令牌,由於某刻流量突增,有1000個請求到來,此時能申請到1000個令牌,所有請求都會放行,最終達到我們的系統,如果令牌桶過大,系統可能會承受不了這波請求。

應用

guava RateLimiter

guava限流實現的是桶算法,通過RateLimiter.create創建,可以創建兩種類型的限流器,SmoothBursty和SmoothWarmingUp,前者定時生成令牌,後者有一個預熱的過程。

        RateLimiter rateLimiter = RateLimiter.create(2);
                new Timer().schedule(new TimerTask() {
                        @Override
                        public void run() {
                                if (rateLimiter.tryAcquire()) {
                                        System.out.println("success");
                                } else {
                                        System.out.println("fail");
                                }
                        }
                }, 0, 200);

既然是桶,那麼桶的大小是多少呢?SmoothBursty里最大令牌數由maxPermits欄位表示,該欄位等於maxBurstSeconds * permitsPerSecond,permitsPerSecond是每秒要生成的令牌數,maxBurstSeconds默認是1。
另外還可以創建SmoothWarmingUp帶有預熱功能的限流器,預熱的作用是通過一個過程才達到permitsPerSecond,相當於讓系統有個熱身的時間。

             RateLimiter rateLimiter = RateLimiter.create(5, 10, TimeUnit.MILLISECONDS);
                new Timer().schedule(new TimerTask() {
                        @Override
                        public void run() {
                                log.info("" + rateLimiter.acquire());                   
                        }
                }, 0, 200);

rateLimiter.acquire()返回的是獲取打令牌的時間,運行程序可以看到開始並不是每秒都能產生5個令牌,也就是不是能立刻獲取到令牌,獲取令牌需要的時間會越來越小,直到預熱期過後就能立馬獲取到令牌了。
guava的限流只能提供單機版的實現,對於集群就無能為力了,並且它通常作為一個工具存在,使用還需要自己封裝,集成到服務,並不能開箱即用。

bucket4j

bucket4j是一個java實現,基於令牌桶算法的限流組件。它支持分布式限流,支持多種存儲,可以方便與各種框架和監控集成。github上start 1.2K,但是issues數量少,國內估計使用的人也不多,並且官方的實現存儲不支持最常用的redis,它專注於限流,如果是自研或者二次開發,是一個很好的參考。

Resilience4j

之前我們介紹過它的熔斷功能,Resilience4j也提供了限流的實現,可以參考這裡。相比guava,Resilience4j是框架級別的,可以很方便的使用。但Resilience4j也是單機版的實現,無法支持集群功能。
Resilience4j限流實現的是令牌桶,如下配置,每1s會生成10個令牌。

resilience4j.ratelimiter:
    instances:
        backendA:
            limitForPeriod: 10
            limitRefreshPeriod: 1s
            timeoutDuration: 0
            registerHealthIndicator: true
            eventConsumerBufferSize: 100

sentinel

流量控制是sentinel最重要的一個功能,sentinel屬於後起之秀,文檔齊全,支持的場景更加豐富。sentinel支持集群限流,也可以像guava一樣預熱,還可以基於調用鏈路進行限流。
sentinel還提供了控制台功能,支持多種數據源的持久化,使用spring cloud的話可以通過spring cloud alibaba引入sentinel。
開源版的sentinel有一些限制,並且使用起來並不是那麼方便,例如Resilience4j可以配置一個default針對所有的請求生效,但sentinel需要單個單個url去配置,顯得非常麻煩,包括熔斷feign接口的配置也是,這個給spring cloud alibaba提了feature,也許在下一個版本就會提供支持。

nginx

上面講到的都是應用級別的限流,nginx通常作為網絡請求的入口,從運維的角度來說,在這裡做限流再合適不過,nginx本身也提供了限流的支持。
nginx比較適合對外的限流,但是我們內部不同系統間的調用一遍不經過nginx,會直接訪問到對方的網關,所以兩者並不矛盾。
nginx限流通過limit_req和limit_conn兩個模塊支持,分別對應請求限制和連結限制(一個連結可以有多個請求)。

http {  
    limit_req_zone zone=name:10m rate=100r/s;  
    server {  
        location /app/ {
            limit_req zone=name burst=500 nodelay;
        }
}

如上,定義了一個name zone,訪問速率最高是100個每秒,/app路徑應用了這個規則。busrt表示爆發的意思,是一個緩衝隊列,用於存儲突增的請求,這些請求會被緩存不會拒絕,如果超過了burst,nodelay表示不等待直接拒絕。
前面我們說到有些惡意攻擊可能每秒發送上萬個請求,導致服務崩潰,如果多個應用系統共用一個nginx,那麼可以統一在nginx配置處理,不需要每個系統自己去實現。

limit_conn_zone $binary_remote_addr zone=name:10m;

server {    
    limit_conn name 50;    
}

如上,定義了一個name zone,$binary_remote_addr表示遠端地址,也就是ip,10m表示存儲空間,10m大概可以存儲16w的ip地址,我們在server節點應用這個規則,50表示最多50個,超過就會拒絕。

關鍵字: