zuul閘道器限流
阿新 • • 發佈:2018-11-21
最近專案需要實現限流的功能,專案使用的是spring cloud框架,用zuul做網管模組。準備在網管層加上限流功能。
1、使用RateLimiter+filter做統一入口限流。適用單機
Guava中開源出來一個令牌桶演算法的工具類RateLimiter,使用簡單,cloud已經整合該模組,直接引入。
<dependency> <groupId>com.marcosbarbero.cloud</groupId> <artifactId>spring-cloud-zuul-ratelimit</artifactId> <version>1.7.1.RELEASE</version> </dependency>
直接新建一個zuulFilter,型別 pre.
@Component public class RateLimitZuulFilter extends ZuulFilter{ private static final Logger LOGGER = LoggerFactory.getLogger(RateLimitZuulFilter.class); //初始化 放入 1000令牌/s 時間視窗為 1s private final RateLimiter rateLimiter = RateLimiter.create(1000.0); @Override public boolean shouldFilter() { // 一直過濾 return true; } @Override public Object run() throws ZuulException { RequestContext ctx = RequestContext.getCurrentContext(); HttpServletResponse response = ctx.getResponse(); if(!rateLimiter.tryAcquire()) { response.setContentType(MediaType.TEXT_PLAIN_VALUE); response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value()); ctx.setSendZuulResponse(false);// 過濾該請求,不對其進行路由 try { response.getWriter().write("TOO MANY REQUESTS"); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } }else { ctx.setResponseStatusCode(200); LOGGER.info("OK !!!"); } return null; } @Override public String filterType() { return "pre"; } @Override public int filterOrder() { return -5; } }
2、使用zuul + RateLimiter 配置方式,在application.yml加簡單配置就行,適用分散式
zuul: add-host-header: true routes: servicewel: path: /getway/servicewel/** serviceId: service-wel servicehi: path: /getway/servicehi/** serviceId: service-hi ratelimit: enabled: true behind-proxy: true key-prefix: ilea-getway-key repository: Redis policies: servicewel: limit: 5 quota: 30 refresh-interval: 60 type: - URL - USER - ORIGIN servicehi: limit: 10 quota: 30 refresh-interval: 60 type: - URL - USER
- repository :是key值儲存方式,可以選Redis、Consul、Spring Data JPA等方式,這裡選擇的是 Redis,所以要新增redis依賴和配置。
- limit 單位時間內允許訪問的次數
- quota 單位時間內允許訪問的總時間(單位時間視窗期內,所有的請求的總時間不能超過這個時間限制)
- refresh-interval 單位時間設定
- type 限流型別:
- url型別的限流就是通過請求路徑區分
- origin是通過客戶端IP地址區分
- user是通過登入使用者名稱進行區分,也包括匿名使用者
通過使用者名稱進行限流可以自定義key策略
@Bean
public RateLimitKeyGenerator rateLimitKeyGenerator(final RateLimitProperties properties,final RateLimitUtils rateLimitUtils) {
//RateLimitPreFilter
return new DefaultRateLimitKeyGenerator(properties, rateLimitUtils) {
@Override
public String key(final HttpServletRequest request, final Route route, final Policy policy) {
String name = request.getParameter("name");
return super.key(request, route, policy)+":"+name;
}
};
}
3、redis計數器限流,利用redis.incrBy()方法實現計數器,最簡單的實現方法,無法實現平滑。適用於分散式
public synchronized boolean access() {
if(!redis.hasKey(COUNTER_KEY)) {
redis.set(COUNTER_KEY,1,(long)2, TimeUnit.SECONDS);//時間到就重新初始化
return true;
}
if(reids.hasKey(COUNTER_KEY)&&redsi.incrBy(COUNTER_KEY,(long)1) > (long)400) {
LOGGER.info("呼叫頻率過快");
return false;
}
return true;
}
// LUA
local key = KEYS[1] --count_key
local limit = tonumber(ARGV[1]) --limit
local current = tonumber(redis.call('get', key) or "0")
if current + 1 > limit then --如果超出限流大小
return 0
else --請求數+1,並設定2秒過期
redis.call("INCRBY", key,"1")
redis.call("expire", key,"2")
return 1
end
4、程式碼實現令牌桶演算法
參考https://zhuanlan.zhihu.com/p/20872901 ,大神講的很仔細
可以在 Bucket 中存放現在的 Token 數量,然後儲存上一次補充 Token 的時間戳,當用戶下一次請求獲取一個 Token 的時候, 根據此時的時間戳,計算從上一個時間戳開始,到現在的這個時間點所補充的所有 Token 數量,加入到 Bucket 當中。
public class RateLimiter {
private JedisPool jedisPool;
private long intervalInMills;
private long limit;
private double intervalPerPermit;
public RateLimiter() {
jedisPool = new JedisPool("127.0.0.1", 6379);
intervalInMills = 10000;
limit = 3;
intervalPerPermit = intervalInMills * 1.0 / limit;
}
// 單執行緒操作下才能保證正確性
// 需要這些操作原子性的話,最好使用 redis 的 lua script
public boolean access(String userId) {
String key = genKey(userId);
try (Jedis jedis = jedisPool.getResource()) {
// 取桶
Map<String, String> counter = jedis.hgetAll(key);
if (counter.size() == 0) {
TokenBucket tokenBucket = new TokenBucket(System.currentTimeMillis(), limit - 1);
jedis.hmset(key, tokenBucket.toHash());
return true;
} else {
TokenBucket tokenBucket = TokenBucket.fromHash(counter);
//取上次新增令牌時間,求與當前時間差值,計算是否加令牌,加多少
long lastRefillTime = tokenBucket.getLastRefillTime();
long refillTime = System.currentTimeMillis();
long intervalSinceLast = refillTime - lastRefillTime;
long currentTokensRemaining;
if (intervalSinceLast > intervalInMills) {
//差值大於 週期, 令牌設為最大
currentTokensRemaining = limit;
} else {
// 根據 新增令牌速率計算應該新增多少令牌
long grantedTokens = (long) (intervalSinceLast / intervalPerPermit);
currentTokensRemaining = Math.min(grantedTokens + tokenBucket.getTokensRemaining(), limit);
}
tokenBucket.setLastRefillTime(refillTime);
assert currentTokensRemaining >= 0;
if (currentTokensRemaining == 0) {
//無令牌可用
tokenBucket.setTokensRemaining(currentTokensRemaining);
jedis.hmset(key, tokenBucket.toHash());
return false;
} else {
//使用一個令牌
tokenBucket.setTokensRemaining(currentTokensRemaining - 1);
jedis.hmset(key, tokenBucket.toHash());
return true;
}
}
}
}
private String genKey(String userId) {
return "rate:limiter:" + intervalInMills + ":" + limit + ":" + userId;
}
public static class TokenBucket {
/*
* 上一次新增時間戳
*/
private long lastRefillTime;
/*
* 剩下的令牌數
*/
private long tokensRemaining;
public TokenBucket(long lastRefillTime, long tokensRemaining) {
this.lastRefillTime = lastRefillTime;
this.tokensRemaining = tokensRemaining;
}
public static TokenBucket fromHash(Map<String, String> hash) {
long lastRefillTime = Long.parseLong(hash.get("lastRefillTime"));
int tokensRemaining = Integer.parseInt(hash.get("tokensRemaining"));
return new TokenBucket(lastRefillTime, tokensRemaining);
}
public Map<String, String> toHash() {
Map<String, String> hash = new HashMap<>();
hash.put("lastRefillTime", String.valueOf(lastRefillTime));
hash.put("tokensRemaining", String.valueOf(tokensRemaining));
return hash;
}
}
}
5、LUA+redis
local key, intervalPerPermit, refillTime = KEYS[1], tonumber(ARGV[1]), tonumber(ARGV[2])
local limit, interval = tonumber(ARGV[3]), tonumber(ARGV[4])
local bucket = redis.call('hgetall', key)
local currentTokens
-- table.maxn(bucket) 不存在 key值為正數的值 = 0,即bucket不存在
if table.maxn(bucket) == 0 then
-- 設定令牌數為最大
currentTokens = limit
redis.call('hset', key, 'lastRefillTime', refillTime)
elseif table.maxn(bucket) == 4 then
-- 桶存在,先計算需要新增的令牌
local lastRefillTime, tokensRemaining = tonumber(bucket[2]), tonumber(bucket[4])
if refillTime > lastRefillTime then
-- 計算差值
-- 1.過了整個週期了,需要補到最大值
-- 2.如果到了至少補充一個的週期了,那麼需要補充部分,否則不補充
local intervalSinceLast = refillTime - lastRefillTime
if intervalSinceLast > interval then
currentTokens = limit
redis.call('hset', key, 'lastRefillTime', refillTime)
else
local grantedTokens = math.floor(intervalSinceLast / intervalPerPermit)
if grantedTokens > 0 then
-- ajust lastRefillTime, we want shift left the refill time.
local padMillis = math.fmod(intervalSinceLast, intervalPerPermit)
redis.call('hset', key, 'lastRefillTime', refillTime - padMillis)
end
currentTokens = math.min(grantedTokens + tokensRemaining, limit)
end
else
-- 有別的執行緒已新增過
currentTokens = tokensRemaining
end
end
assert(currentTokens >= 0)
if currentTokens == 0 then
-- 無令牌可用
redis.call('hset', key, 'tokensRemaining', currentTokens)
return 0
else
redis.call('hset', key, 'tokensRemaining', currentTokens - 1)
return 1
end
java中判斷
public boolean access(String userId) {
String key = genKey(userId);
/**
* keys[1] = key;
* arvg[1] = intervalPerPermit; 每個用多少秒
* arvg[2] = System.currentTimeMillis() 當前時間
* avrg[3] = 總令牌數
* avrg[4] = 週期
*/
long result = (long) jedis.evalsha(scriptSha1, 1, key,
String.valueOf(intervalPerPermit),
String.valueOf(System.currentTimeMillis()),
String.valueOf(limit),
String.valueOf(intervalInMills));
return result == 1L;
}
參照部落格:
https://zhuanlan.zhihu.com/p/20872901
https://blog.csdn.net/lsblsb/article/details/69486012
這兩篇部落格寫的非常棒,給我很大幫助。謝謝大神。