SpringCloud中怎么实现gateway限流

发布时间:2021-07-30 13:56:37 作者:Leah
来源:亿速云 阅读:336

SpringCloud中怎么实现gateway限流,相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。

路由过滤器允许以某种方式修改传入的HTTP请求或传出的HTTP响应,路径过滤器的范围限定为特定路径,Spring Cloud Gateway包含许多内置的GatewayFilter工厂。

Spring Cloud Gateway限流就是通过内置的RequestRateLimiterGateWayFilterFactory工厂来实现的。

当然,官方的肯定不能满足我们部分业务需求,因此可以自定义限流过滤器。

## yml如下配置,就可以为该路由添加此拦截器:

spring:
   cloud:
      gateway:
        routes:
          - id: test_route
            uri: localhost
            predicates:
            - Path=/host/address
            filters:
            - name: RequestRateLimiter
              args:
                ## 允许用户每秒执行多少请求,而不会丢弃任何请求。这是令牌桶填充的速率。
                redis-rate-limiter.replenishRate: 1
                ## 是一秒钟内允许用户执行的最大请求数。这是令牌桶可以容纳的令牌数。将此值设置为零将阻止所有请求。
                redis-rate-limiter.burstCapacity: 3
                ## KeyResolver是一个简单的获取用户请求参数 我这里以主机地址为key来作限流
                key-resolver: "#{@hostAddrKeyResolver}"

## RequestRateLimiterGateWayFilterFactory代码:

//AbstractGatewayFilterFactory实现GatewayFilterFactory接口,自定义的过滤工厂可以继承
//AbstractGatewayFilterFactory并编写apply方法
public class RequestRateLimiterGatewayFilterFactory extends AbstractGatewayFilterFactory<RequestRateLimiterGatewayFilterFactory.Config> {
 
	public static final String KEY_RESOLVER_KEY = "keyResolver";
 
	private final RateLimiter defaultRateLimiter;
	private final KeyResolver defaultKeyResolver;
 
	public RequestRateLimiterGatewayFilterFactory(RateLimiter defaultRateLimiter,KeyResolver defaultKeyResolver) {
		super(Config.class);
		this.defaultRateLimiter = defaultRateLimiter;
		this.defaultKeyResolver = defaultKeyResolver;
	}
 
	public KeyResolver getDefaultKeyResolver() {
		return defaultKeyResolver;
	}
 
	public RateLimiter getDefaultRateLimiter() {
		return defaultRateLimiter;
	}
 
	@SuppressWarnings("unchecked")
	@Override
	public GatewayFilter apply(Config config) {
                //yml中我们配置的hostAddrKeyResolver
		KeyResolver resolver = (config.keyResolver == null) ? defaultKeyResolver : config.keyResolver;
                //这个就是限流的具体实现,默认使用RedisRateLimiter
		RateLimiter<Object> limiter = (config.rateLimiter == null) ? defaultRateLimiter : config.rateLimiter;
 
		return (exchange, chain) -> {
			Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
 
			return resolver.resolve(exchange).flatMap(key ->
                                    //这里的isAllowed就是具体实现,输入参数为路由id和限流key(这里为主机地址hostAddress)
					// TODO: if key is empty?
					limiter.isAllowed(route.getId(), key).flatMap(response -> {
 
						for (Map.Entry<String, String> header : response.getHeaders().entrySet()) {
							exchange.getResponse().getHeaders().add(header.getKey(), header.getValue());
						}
                                        //如果为真,通过拦截
						if (response.isAllowed()) {
							return chain.filter(exchange);
						}
                                        //否则设置http码为429,too many request
						exchange.getResponse().setStatusCode(config.getStatusCode());
						return exchange.getResponse().setComplete();
					}));
		};
	}
 
 
}


分析:

1.加载KeyResolver,从配置文件中加载,此处我配置了hostAddrKeyResolver,即根据host地址来进行限流。如果为空,使用默认的PrincipalNameKeyResolver

2.加载RateLimiter,默认使用RedisRateLimiter。

3.执行RedisRateLimiter的isAllowed方法,得到response,如果isAllowed为true则通过拦截,否则返回429(isAllowed方法具体实现下文描述)。

## HostAddrKeyResolver:

@Slf4j
public class HostAddrKeyResolver implements KeyResolver {
    @Override
    public Mono<String> resolve(ServerWebExchange exchange) {
        log.info("HostAddrKeyResolver 限流");
        return Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
    }
}


在启动类中注入bean

@Bean
public HostAddrKeyResolver hostAddrKeyResolver() {
   return new HostAddrKeyResolver();
}

## RedisRateLimiter:

@Override
    @SuppressWarnings("unchecked")
    public Mono<Response> isAllowed(String routeId, String id) {
                //判断是否初始化
        if (!this.initialized.get()) {
            throw new IllegalStateException("RedisRateLimiter is not initialized");
        }
                //获取配置
        Config routeConfig = getConfig().getOrDefault(routeId, defaultConfig);
 
        if (routeConfig == null) {
            throw new IllegalArgumentException("No Configuration found for route " + routeId);
        }
 
                //令牌桶填充速率
        int replenishRate = routeConfig.getReplenishRate();
 
                //令牌桶可容纳令牌数
        int burstCapacity = routeConfig.getBurstCapacity();
 
        try {
                    //获取redis的key,执行lua脚本时传入
            List<String> keys = getKeys(id);
 
 
                    //获取参数,执行lua脚本时传入
            List<String> scriptArgs = Arrays.asList(replenishRate + "", burstCapacity + "",
                    Instant.now().getEpochSecond() + "", "1");
            Flux<List<Long>> flux = this.redisTemplate.execute(this.script, keys, scriptArgs);
                    // .log("redisratelimiter", Level.FINER);
            return flux.onErrorResume(throwable -> Flux.just(Arrays.asList(1L, -1L)))
                    .reduce(new ArrayList<Long>(), (longs, l) -> {
                        longs.addAll(l);
                        return longs;
                    }) .map(results -> {
                        boolean allowed = results.get(0) == 1L;
                        Long tokensLeft = results.get(1);
 
                        Response response = new Response(allowed, getHeaders(routeConfig, tokensLeft));
 
                        if (log.isDebugEnabled()) {
                            log.debug("response: " + response);
                        }
                        return response;
                    });
        }
        catch (Exception e) {
            /*
             * We don't want a hard dependency on Redis to allow traffic. Make sure to set
             * an alert so you know if this is happening too much. Stripe's observed
             * failure rate is 0.01%.
             */
            log.error("Error determining if user allowed from redis", e);
        }
        return Mono.just(new Response(true, getHeaders(routeConfig, -1L)));
    }
 
    @NotNull
    public HashMap<String, String> getHeaders(Config config, Long tokensLeft) {
        HashMap<String, String> headers = new HashMap<>();
        headers.put(this.remainingHeader, tokensLeft.toString());
        headers.put(this.replenishRateHeader, String.valueOf(config.getReplenishRate()));
        headers.put(this.burstCapacityHeader, String.valueOf(config.getBurstCapacity()));
        return headers;
    }
 
    static List<String> getKeys(String id) {
        // use {} around keys to use Redis Key hash tags
        // this allows for using redis cluster
 
        // Make a unique key per user.
        String prefix = "request_rate_limiter.{" + id;
                //令牌桶剩余令牌数
        String tokenKey = prefix + "}.tokens";
                //令牌桶最后填充令牌时间
        String timestampKey = prefix + "}.timestamp";
        return Arrays.asList(tokenKey, timestampKey);
    }


分析:

1.判断是否初始化,加载配置,获取令牌填充速率和令牌桶大小

2.根据路由id组合成两个redis中的key值,传入lua脚本

request_rate_limiter.{id}.tokens   令牌桶剩余令牌数

request_rate_limiter.{id}.timestamp  令牌桶最后填充令牌时间

3.把令牌填充速率,令牌桶大小,当前时间(单位:秒),消耗令牌数(默认为1)组合传入lua脚本

4.执行lua脚本

5.flux.onErrorResume(throwable -> Flux.just(Arrays.asList(1L, -1L)))  这个是对执行lua脚本过程中发生异常的处理,它会忽略异常,返回令牌。这样就能跟redis解耦,不对它强依赖。

该实现核心主要体现在lua脚本上,它使用的是令牌桶算法

详见spring-cloud-gateway-core下的request_rate_limiter.lua

## 获取剩余令牌数的redis key
local tokens_key = KEYS[1]
## 获取最后一次填充令牌的时间
local timestamp_key = KEYS[2]
--redis.log(redis.LOG_WARNING, "tokens_key " .. tokens_key)
 
## 令牌填充速率
local rate = tonumber(ARGV[1])
## 令牌桶大小
local capacity = tonumber(ARGV[2])
## 当前秒数
local now = tonumber(ARGV[3])
## 消耗令牌数,默认1
local requested = tonumber(ARGV[4])
 
## 计算令牌桶需要填充的时间
local fill_time = capacity/rate
## 计算key的存活时间
local ttl = math.floor(fill_time2)
 
--redis.log(redis.LOG_WARNING, "rate " .. ARGV[1])
--redis.log(redis.LOG_WARNING, "capacity " .. ARGV[2])
--redis.log(redis.LOG_WARNING, "now " .. ARGV[3])
--redis.log(redis.LOG_WARNING, "requested " .. ARGV[4])
--redis.log(redis.LOG_WARNING, "filltime " .. fill_time)
--redis.log(redis.LOG_WARNING, "ttl " .. ttl)
 
## 获取剩余的令牌数
local last_tokens = tonumber(redis.call("get", tokens_key))
if last_tokens == nil then
  last_tokens = capacity
end
--redis.log(redis.LOG_WARNING, "last_tokens " .. last_tokens)
 
## 获取令牌最后填充时间
local last_refreshed = tonumber(redis.call("get", timestamp_key))
if last_refreshed == nil then
  last_refreshed = 0
end
--redis.log(redis.LOG_WARNING, "last_refreshed " .. last_refreshed)
 
local delta = math.max(0, now-last_refreshed)
## 计算得到剩余的令牌数
local filled_tokens = math.min(capacity, last_tokens+(deltarate))
## 大于请求消耗令牌 allowed 设为true
local allowed = filled_tokens >= requested
local new_tokens = filled_tokens
local allowed_num = 0
if allowed then
  new_tokens = filled_tokens - requested
  allowed_num = 1
end
 
--redis.log(redis.LOG_WARNING, "delta " .. delta)
--redis.log(redis.LOG_WARNING, "filled_tokens " .. filled_tokens)
--redis.log(redis.LOG_WARNING, "allowed_num " .. allowed_num)
--redis.log(redis.LOG_WARNING, "new_tokens " .. new_tokens)
 
redis.call("setex", tokens_key, ttl, new_tokens)
redis.call("setex", timestamp_key, ttl, now)
 
return { allowed_num, new_tokens }

看完上述内容,你们掌握SpringCloud中怎么实现gateway限流的方法了吗?如果还想学到更多技能或想了解更多相关内容,欢迎关注亿速云行业资讯频道,感谢各位的阅读!

推荐阅读:
  1. Spring Cloud Gateway 之 限流
  2. Spring Cloud Gateway如何实现限流操作

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

springcloud gateway

上一篇:在Qt中如何设置窗体的背景图片

下一篇:DevExpress中如何使用Report-XRTable绑定数据

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》