限流的簡單使用及學習
前言
最近系統剛做了一次大的重構,以及下游子服務都做了升級改造。
整個系統間的呼叫都是採用spring cloud這一套去實現的。我所負責的為業務服務端,專門為web端和pc端提供介面呼叫。在服務剛上線的一段時間,出現了一次雪崩的事件,整個呼叫鏈路如下:
呼叫鏈路很簡單,因為文字匹配服務 需要分詞,匹配,已經從ES獲取匹配後的術語語料等資料,所以會有請求擠壓,一段時間類服務就崩潰了。為了緊急處理這種情況,所以需要再業務方加上限流機制(後續優化下游的匹配演算法)。剛好也針對於這種情況,自己來學習下幾種限流的方式。
限流演算法分類
參見的限流演算法有:令牌桶,漏桶,計數器。
計數器限流演算法
計數器是最簡單也是最粗暴的一種限流演算法,同時也是比較常用的,主要用來限制總併發數,比如資料庫連線池大小、執行緒池大小、程式訪問併發數等都是使用計數器演算法。
- 使用Redis的限流做法:
/** * 限流方法,通過redis進行方法級別的限流措施。 */ @Service @Transactional @Slf4j public class MethodThrottleService { @Autowired private RedisTemplate<String, String> redisTemplate; /** * 通過指定key值獲取是否是合法請求,如果在規定快取時間內仍然存在該key值,說明該請求不合法 * * @param key 請求key值 * @param expireTime 過期時間 * @param timeUnit 過期時間單位 * @return 是否過期 true || false */ public Boolean validateKeyRequest(String key, int expireTime, TimeUnit timeUnit) { ValueOperations<String, String> ops = redisTemplate.opsForValue(); String result = ops.get(key); if (StringUtils.isNotBlank(result)) { return false; } ops.set(key, key, expireTime, timeUnit); return true; } /** * 通過指定使用者和方法名判斷請求是否合法請求,如果在規定快取時間內仍然存在該key值,說明該請求不合法 * * @param methodName 方法名 * @param perCount 規定時間請求的次數 * @param iolId 使用者名稱 * @return 是否過期 true || false */ public Boolean validateUserRequest(String methodName, int perCount, String iolId, int expireTime, TimeUnit timeUnit) { ValueOperations<String, String> ops = redisTemplate.opsForValue(); String cacheKey = getCacheKey(iolId, methodName); Long requestCount = ops.increment(cacheKey, 1); log.info("requestCount = {}", requestCount); redisTemplate.expire(cacheKey,expireTime, timeUnit ); if (requestCount >= perCount) { log.info("MethodThrottle exceed weight limit! iolId = {}, methodName = {}, requestCount = {}", iolId, methodName, requestCount); return false; } return true; } /** * 獲取快取的key值 * @param targetName 目標名稱 * @param methodName 方法名稱 * @return 快取key */ private String getCacheKey(String targetName, String methodName) { StringBuilder sb = new StringBuilder(""); sb.append("limitRate.").append(targetName).append(".").append(methodName); return sb.toString(); } }
使用redis限流,可以針對於使用者+方法名進行精準限流。同時可以根據請求key值進行限流,目的是限定規定時間類同樣引數的請求次數。
但是redis 限流會有很大的效能瓶頸,頻繁的寫入,讀取,過期會對redis效能損耗比較大。不建議此種方法。
另外計數器還可以使用AtomicInteger
和 Semaphore
,具體就不在這列出程式碼了,具體可以參考:Java限流策略-簡書
令牌桶演算法
令牌桶演算法是一個存放固定容量的令牌的桶,按照固定速率往桶裡新增令牌。令牌桶演算法的描述如下:(參考開濤:億級流量網站架構核心技術 中第4章部分內容)
如下:
- 假設限制2r/s,則按照500毫秒的固定速率往桶中新增令牌;
- 桶中最多存放b個令牌,當桶滿時,新新增的令牌被丟棄或拒絕;
-當一個n個位元組大小的資料包到達,將從桶中刪除n個令牌,接著資料包被髮送到網路上;
-如果桶中的令牌不足n個,則不會刪除令牌,且該資料包將被限流(要麼丟棄,要麼緩衝區等待)。
備註(10r/s: 一秒鐘10令牌放入桶中)
對於令牌桶限流,我們可以使用Guava
開源得到RateLimiter
來做,具體可以參考如下程式碼:
//每秒只發出10個令牌
RateLimiter rateLimiter = RateLimiter.create(10);
/**
* 嘗試獲取令牌
*
* @return 獲取令牌是否成功 true || false
*/
public boolean tryAcquire() {
return rateLimiter.tryAcquire();
}
漏桶演算法
漏桶作為計量工具(The Leaky Bucket Algorithm as a Meter)時,可以用於流量整形(Traffic Shaping)和流量控制(TrafficPolicing),漏桶演算法的描述如下:
- 一個固定容量的漏桶,按照常量固定速率流出水滴;
- 如果桶是空的,則不需流出水滴;
- 可以以任意速率流入水滴到漏桶;
- 如果流入水滴超出了桶的容量,則流入的水滴溢位了(被丟棄),而漏桶容量是不變的。
令牌桶和漏桶對比:
- 令牌桶是按照固定速率往桶中新增令牌,請求是否被處理需要看桶中令牌是否足夠,當令牌數減為零時則拒絕新的請求;
- 漏桶則是按照常量固定速率流出請求,流入請求速率任意,當流入的請求數累積到漏桶容量時,則新流入的請求被拒絕;
- 令牌桶限制的是平均流入速率(允許突發請求,只要有令牌就可以處理,支援一次拿3個令牌,4個令牌),並允許一定程度突發流量;
- 漏桶限制的是常量流出速率(即流出速率是一個固定常量值,比如都是1的速率流出,而不能一次是1,下次又是2),從而平滑突發流入速率;
- 令牌桶允許一定程度的突發,而漏桶主要目的是平滑流入速率;
- 兩個演算法實現可以一樣,但是方向是相反的,對於相同的引數得到的限流效果是一樣的。