前言
最近系統剛做了一次大的重構,以及下游子服務都做了升級改造。
整個系統間的調用都是采用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),從而平滑突發流入速率;
- 令牌桶允許一定程度的突發,而漏桶主要目的是平滑流入速率;
- 兩個算法實現可以一樣,但是方向是相反的,對於相同的參數得到的限流效果是一樣的。