nginx 、springMvc(非分布式)相應的限流、消峰


互聯網服務賴以生存的根本是流量, 產品和運營會經常通過各種方式來為應用倒流,比如淘寶的雙十一等,如何讓系統在處理高並發的同時還是保證自身系統的穩定,

通常在最短時間內提高並發的做法就是加機器, 但是如果機器不夠怎么辦? 那就需要做業務降級或系統限流。

流量控制中用的比較多的三個算法就是令牌桶、漏桶、計數器。

一、令牌桶限流(TokenBucket)
令牌桶算法的基本過程如下:

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

     

 

二、漏桶限流(LeakBucket)

漏桶算法強制一個常量的輸出速率而不管輸入數據流的突發性

(1)當輸入空閑時,該算法不執行任何動作.就像用一個底部開了個洞的漏桶接水一樣, 水進入到漏桶里, 桶里的水通過下面的孔以固定的速率流出

(2)當水流入速度過大會直接溢出,可以看出漏桶算法能強行限制數據的傳輸速率.如下圖所示:


 

漏桶和令牌桶比較

“漏桶算法”能夠強行限制數據的傳輸速率,而“令牌桶算法”在能夠限制數據的平均傳輸數據外,還允許某種程度的突發傳輸。

在“令牌桶算法”中,只要令牌桶中存在令牌,那么就允許突發地傳輸數據直到達到用戶配置的上限,因此它適合於具有突發特性的流量。

三、計數器限流

有時我們還會使用計數器來進行限流,主要用來限制一定時間內的總並發數。計數器限流方法可以通過緩存實現計數器,假如以秒為單位進行限流,

過期時間為1秒,每次請求計數加1,超過每秒允許的最大請求數請求數閥值將被丟棄。

 

四、nginx 實現漏桶

    #以用戶二進制IP地址,定義三個漏桶,滴落速率1-3req/sec,桶空間1m,1M能保持大約16000個(IP)狀態
    limit_req_zone  $binary_remote_addr  zone=qps1:1m   rate=1r/s;
    limit_req_zone  $binary_remote_addr  zone=qps2:1m   rate=2r/s;
    limit_req_zone  $binary_remote_addr  zone=qps3:1m   rate=3r/s;
     
    server {
     
        #速率qps=1,峰值burst=5,延遲請求
        #嚴格按照漏桶速率qps=1處理每秒請求
        #在峰值burst=5以內的並發請求,會被掛起,延遲處理
        #超出請求數限制則直接返回503
        #客戶端只要控制並發在峰值[burst]內,就不會觸發limit_req_error_log
        # 例1:發起一個並發請求=6,拒絕1個,處理1個,進入延遲隊列4個:
        #time    request    refuse    sucess    delay
        #00:01        6        1        1            4
        #00:02        0        0        1            3
        #00:03        0        0        1            2
        #00:04        0        0        1            1
        #00:05        0        0        1            0
        location /delay {
            limit_req   zone=qps1  burst=5;
        }
         
        #速率qps=1,峰值burst=5,不延遲請求
        #加了nodelay之后,漏桶控制一段時長內的平均qps = 漏桶速率,允許瞬時的峰值qps > 漏桶qps
        #所以峰值時的最高qps=(brust+qps-1)=5
        #請求不會被delay,要么處理,要么直接返回503
        #客戶端需要控制qps每秒請求數,才不會觸發limit_req_error_log
        # 例2:每隔5秒發起一次達到峰值的並發請求,由於時間段內平均qps=1 所以仍然符合漏桶速率:
        #time    request     refuse    sucess
        #00:01         5         0          5
        #00:05         5         0          5
        #00:10         5         0          5
        # 例3:連續每秒發起並發請求=5,由於時間段內平均qps>>1,超出的請求被拒絕:
        #time    request     refuse     sucess
        #00:01         5         0           5
        #00:02         5         4           1
        #00:03         5         4           1
         
        location /nodelay {
            limit_req   zone=qps1  burst=5 nodelay;
        }
     
    }

 

 limit_req_module

五、redis

local key = KEYS[1] --限流KEY(一秒一個)
local limit = tonumber(ARGV[1])        --限流大小
local current = tonumber(redis.call('get', key) or "0")
if current + 1 > limit then --如果超出限流大小
    redis.call("INCRBY", key,"1") -- 如果不需要統計真是訪問量可以不加這行
    return 0
else  --請求數+1,並設置2秒過期
    redis.call("INCRBY", key,"1")
    if tonumber(ARGV[2]) > -1 then
        redis.call("expire", key,tonumber(ARGV[2])) --時間窗口最大時間后銷毀鍵
    end
    return 1
end

lua腳本返回值比較奇怪,用java客戶端接受返回值,只能使用Long,沒有去深究。這個腳本只需要傳入key(url+時間戳/預設時間窗口大小),便可以實現限流。 

java調用lua腳本

/**
 * Created by xujingfeng on 2017/3/13.
 * <p>
 * 基於redis lua腳本的線程安全的計數器限流方案
 * </p>
 */
public class RedisRateLimiter {

    /**
     * 限流訪問的url
     */
    private String url;

    /**
     * 單位時間的大小,最大值為 Long.MAX_VALUE - 1,以秒為單位
     */
    final Long timeUnit;

    /**
     * 單位時間窗口內允許的訪問次數
     */
    final Integer limit;

    /**
     * 需要傳入一個lua script,莫名其妙redisTemplate返回值永遠是個Long
     */
    private RedisScript<Long> redisScript;

    private RedisTemplate redisTemplate;

    /**
     * 配置鍵是否會過期,
     * true:可以用來做接口流量統計,用定時器去刪除
     * false:過期自動刪除,時間窗口過小的話會導致鍵過多
     */
    private boolean isDurable = false;

    public void setRedisScript(RedisScript<Long> redisScript) {
        this.redisScript = redisScript;
    }

    public void setRedisTemplate(RedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public boolean isDurable() {
        return isDurable;
    }

    public void setDurable(boolean durable) {
        isDurable = durable;
    }

    public RedisRateLimiter(Integer limit, Long timeUnit) {
        this.timeUnit = timeUnit;
        Assert.isTrue(timeUnit < Long.MAX_VALUE - 1);
        this.limit = limit;
    }

    public RedisRateLimiter(Integer limit, Long timeUnit, boolean isDurable) {
        this(limit, timeUnit);
        this.isDurable = isDurable;
    }

    public boolean acquire() {
        return this.acquire(this.url);
    }

    public boolean acquire(String url) {
        StringBuffer key = new StringBuffer();
        key.append("rateLimiter").append(":")
                .append(url).append(":")
                .append(System.currentTimeMillis() / 1000 / timeUnit);
        Integer expire = limit + 1;
        String convertExpire = isDurable ? "-1" : expire.toString();
        return redisTemplate.execute(redisScript, Arrays.asList(key.toString()), limit.toString(), convertExpire).equals(1l);
    }

}

 

5.spring mvc

RateLimiter

我們可以使用 Guava 的 RateLimiter 來實現基於令牌桶的流量控制。RateLimiter 令牌桶算法的單桶實現,RateLimiter 對簡單的令牌桶算法做了一

些工程上的優化,具體的實現是 SmoothBursty。需要注意的是,RateLimiter 的另一個實現 SmoothWarmingUp,就不是令牌桶了,而是漏桶算法。

SmoothBursty 有一個可以放 N 個時間窗口產生的令牌的桶,系統空閑的時候令牌就一直攢着,最好情況下可以扛 N 倍於限流值的高峰而不影響

后續請求,就像三峽大壩一樣能扛千年一遇的洪水.

 

 

 



作者:Lewe
鏈接:http://www.jianshu.com/p/7170edcd9239
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM