互聯網服務賴以生存的根本是流量, 產品和運營會經常通過各種方式來為應用倒流,比如淘寶的雙十一等,如何讓系統在處理高並發的同時還是保證自身系統的穩定,
通常在最短時間內提高並發的做法就是加機器, 但是如果機器不夠怎么辦? 那就需要做業務降級或系統限流。
流量控制中用的比較多的三個算法就是令牌桶、漏桶、計數器。
一、令牌桶限流(TokenBucket)
令牌桶算法的基本過程如下:
- 每秒會有 r 個令牌放入桶中,或者說,每過 1/r 秒桶中增加一個令牌。
- 桶中最多存放 b 個令牌,如果桶滿了,新放入的令牌會被丟棄。
- 當一個 n 字節的數據包到達時,消耗 n 個令牌,然后發送該數據包。
- 如果桶中可用令牌小於 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; } }
五、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
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。