Gateway限流
Spring Cloud Gateway提供RequestRateLimiterGatewayFilterFactory这个类,适用Redis和lua脚本实现令牌桶方式。实现逻辑在RequestRateLimiterGatewayFilterFactory类中。
pom文件中引入gateway的起步依赖和redis的reactive依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis-reactive</artifactId> </dependency>
在配置文件的配置:
spring: application: name: gateway-client cloud: gateway: routes: - id: route_service_one uri: ${movie.uri} # uri以lb://开头(lb代表从注册中心获取服务),后面就是需要转发到的服务名称 filters: - name: RequestRateLimiter args: key-resolver: '#{@uriKeyResolver}' redis-rate-limiter.replenishRate: 1 redis-rate-limiter.burstCapacity: 3 - AddRequestHeader=X-Request-Foo, Bar RewritePath=/foo/(?<segment>.*), /$\{segment} predicates: - Path=/movie/** redis: host: localhost port: 6379 database: 0
过滤器配置三个参数:
burstCapacity,令牌桶总容量
replenishRate,令牌桶每秒填充平均速率
key-resolver,用于限流的键的解析器的 Bean 对象的名字。使用 SpEL 表达式根据#{@beanName}从 Spring 容器中获取 Bean 对象。
keyResolver 限流的维度。 默认实现类PrincipalNameKeyResolver,以用户名来作为维度,会从 ServerWebExchange检索 Principal并调用 Principal.getName()。因为Principal.getName()是空的,而denyEmptyKey =true,没有Principal的话,会返回forbidden403,就算设置denyEmptyKey =false,因为name是空,所以限流KeyResolver的键是空,限流会不生效
KeyResolver需要实现resolve方法,比如根据Hostname进行限流,则需要用hostAddress去判断
public class HostAddrKeyResolver implements KeyResolver { @Override public Mono<String> resolve(ServerWebExchange exchange) { return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()); } } @Bean public HostAddrKeyResolver hostAddrKeyResolver() { return new HostAddrKeyResolver(); }
根据uri去限流,这时KeyResolver代码如下:
public class UriKeyResolver implements KeyResolver { @Override public Mono<String> resolve(ServerWebExchange exchange) { return Mono.just(exchange.getRequest().getURI().getPath()); } } @Bean public UriKeyResolver uriKeyResolver() { return new UriKeyResolver(); }
以用户的维度去限流:
@Bean KeyResolver userKeyResolver() { return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user")); }
RequestRateLimiterGatewayFilterFactory核心源码
public GatewayFilter apply(Config config) { KeyResolver resolver = getOrDefault(config.keyResolver, defaultKeyResolver); RateLimiter<Object> limiter = getOrDefault(config.rateLimiter, defaultRateLimiter); boolean denyEmpty = getOrDefault(config.denyEmptyKey, this.denyEmptyKey); HttpStatusHolder emptyKeyStatus = HttpStatusHolder .parse(getOrDefault(config.emptyKeyStatus, this.emptyKeyStatusCode)); return (exchange, chain) -> { Route route = exchange .getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR); return resolver.resolve(exchange).defaultIfEmpty(EMPTY_KEY).flatMap(key -> { if (EMPTY_KEY.equals(key)) { if (denyEmpty) { setResponseStatus(exchange, emptyKeyStatus); return exchange.getResponse().setComplete(); } return chain.filter(exchange); } return 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); } // 被限流了,直接返回 setResponseStatus(exchange, config.getStatusCode()); return exchange.getResponse().setComplete(); }); }); }; }
RedisRateLimiter核心源码
public Mono<Response> isAllowed(String routeId, String id) { if (!this.initialized.get()) { throw new IllegalStateException("RedisRateLimiter is not initialized"); } Config routeConfig = loadConfiguration(routeId); // How many requests per second do you want a user to be allowed to do? int replenishRate = routeConfig.getReplenishRate(); // How much bursting do you want to allow? int burstCapacity = routeConfig.getBurstCapacity(); try { List<String> keys = getKeys(id); // The arguments to the LUA script. time() returns unixtime in seconds. List<String> scriptArgs = Arrays.asList(replenishRate + "", burstCapacity + "", Instant.now().getEpochSecond() + "", "1"); // allowed, tokens_left = redis.eval(SCRIPT, keys, args) 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 -> {
//是否获取令牌成功(1-成功,0-失败) boolean allowed = results.get(0) == 1L; Long tokensLeft = results.get(1);//result返回四个参数:剩余令牌数、每秒补充的令牌数、令牌容量、每个请求申请的令牌数 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))); //兜底,默认是返回成功,剩余令牌数是-1 }
立志如山 静心求实
浙公网安备 33010602011771号