1. 程式人生 > >zuul閘道器限流

zuul閘道器限流

最近專案需要實現限流的功能,專案使用的是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

這兩篇部落格寫的非常棒,給我很大幫助。謝謝大神。