Spring Cloud Gateway实现限流
背景
zuul切换为spring cloud gateway时,需要重新实现限流逻辑。本文主要整理了spring cloud gateway中如何实现限流。
zuul中的限流
之前zuul的限流是通过guava提供的令牌桶算法实现的,通过一个全局的过滤器,对所有经过网关的请求,以IP地址作区分进行限流。
引入guava依赖:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
具体代码案例:
/**
* 自定义过滤器
*
* @author yuanzhihao
* @since 2022/4/27
*/
@Component
@Slf4j
public class RequestRateLimitFilter implements Filter {
private static final Cache<String, RateLimiter> RATE_LIMITER_CACHE = CacheBuilder
.newBuilder()
.maximumSize(1000)
.expireAfterAccess(1, TimeUnit.HOURS)
.build();
private static final double DEFAULT_PERMITS_PER_SECOND = 1; // 令牌桶每秒填充速率
@SneakyThrows
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
String remoteAddr = servletRequest.getRemoteAddr();
RateLimiter rateLimiter = RATE_LIMITER_CACHE.get(remoteAddr, () -> RateLimiter.create(DEFAULT_PERMITS_PER_SECOND));
if (rateLimiter.tryAcquire()) {
filterChain.doFilter(servletRequest, servletResponse);
} else {
((HttpServletResponse) servletResponse).setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
servletResponse.setContentType("application/json;charset=UTF-8");
servletResponse.getWriter().write("Too Many Request!!!");
}
}
}
Spring Cloud Gateway实现限流
编写自定义的限流过滤器
参考zuul中限流方法,可以很容易的编写一个全局过滤器来进行限流,具体代码:
/**
* 自定义过滤器
*
* @author yuanzhihao
* @since 2022/4/27
*/
@Component
@Slf4j
@Order(-1)
public class RequestRateLimitFilter implements GlobalFilter {
private static final Cache<String, RateLimiter> RATE_LIMITER_CACHE = CacheBuilder
.newBuilder()
.maximumSize(1000)
.expireAfterAccess(1, TimeUnit.HOURS)
.build();
private static final double DEFAULT_PERMITS_PER_SECOND = 1; // 令牌桶每秒填充速率
@SneakyThrows
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String remoteAddr = Objects.requireNonNull(exchange.getRequest().getRemoteAddress()).getAddress().getHostAddress();
RateLimiter rateLimiter = RATE_LIMITER_CACHE.get(remoteAddr, () -> RateLimiter.create(DEFAULT_PERMITS_PER_SECOND));
if (rateLimiter.tryAcquire()) {
return chain.filter(exchange);
}
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
DataBuffer dataBuffer = response.bufferFactory().wrap("Too Many Request!!!".getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(dataBuffer));
}
}
不过这种限流的粒度非常大,对于所有的请求都进行了限流,不能进行定制化的限流。之前博客里面整理过gatewayFilters局部过滤器的用法,这边可以参考进行限流过滤器的编写。
贴一下案例代码:
/**
* 自定义局部限流
*
* @author yuanzhihao
* @since 2022/4/27
*/
@Component
public class CustomRequestRateLimitGatewayFilterFactory extends AbstractGatewayFilterFactory<CustomRequestRateLimitGatewayFilterFactory.Config> {
public CustomRequestRateLimitGatewayFilterFactory() {
super(Config.class);
}
private static final Cache<String, RateLimiter> RATE_LIMITER_CACHE = CacheBuilder
.newBuilder()
.maximumSize(1000)
.expireAfterAccess(1, TimeUnit.HOURS)
.build();
@Override
public GatewayFilter apply(Config config) {
return new GatewayFilter() {
@SneakyThrows
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String remoteAddr = Objects.requireNonNull(exchange.getRequest().getRemoteAddress()).getAddress().getHostAddress();
RateLimiter rateLimiter = RATE_LIMITER_CACHE.get(remoteAddr, () ->
RateLimiter.create(Double.parseDouble(config.getPermitsPerSecond())));
if (rateLimiter.tryAcquire()) {
return chain.filter(exchange);
}
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
DataBuffer dataBuffer = response.bufferFactory().wrap("Too Many Request!!!".getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(dataBuffer));
}
};
}
@Override
public List<String> shortcutFieldOrder() {
return Collections.singletonList("permitsPerSecond");
}
@Data
public static class Config {
private String permitsPerSecond; // 令牌桶每秒填充速率
}
}
对应请求路由生效过滤器:
- id: server1
uri: lb://eureka-server1
predicates:
- Path=/server1/hello
filters:
- CustomRequestRateLimit=1
Spring Cloud Gateway自实现的限流过滤器
spring cloud gateway里面也提供了一个自实现的限流过滤器org.springframework.cloud.gateway.filter.factory.RequestRateLimiterGatewayFilterFactory,这个过滤器里面有两个参数,一个是KeyResolver,这个参数可以动态的指定限流的一些key(个人理解,这边还是详细参考下官方文档~~~),比如这个key可以是访问的IP。
还有一个是RateLimiter,这个参数是具体的限流策略,在spring cloud gateway里面,它的默认实现是RedisRateLimiter,它采用的也是令牌桶算法。
首先我们实现KeyResolver接口,指定限流的key是访问的IP地址:
/**
* 根据ip地址进行限流
*
* @author yuanzhihao
* @since 2022/4/27
*/
@Component
public class HostAddrKeyResolver implements KeyResolver {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
return Mono.just(Objects.requireNonNull(exchange.getRequest().getRemoteAddress()).getAddress().getHostAddress());
}
}
添加spring-boot-starter-data-redis-reactive依赖,使用RedisRateLimiter限流:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
配置文件中添加redis和限流的配置信息:
redis:
host: 127.0.0.1
port: 6379
- id: server2
uri: lb://eureka-server1
predicates:
- Path=/server1/twoDog
filters:
- name: RequestRateLimiter
args:
key-resolver: "#{@hostAddrKeyResolver}"
redis-rate-limiter.replenishRate: 1 # 令牌桶填充的速率 秒为单位
redis-rate-limiter.burstCapacity: 1 # 令牌桶总容量
redis-rate-limiter.requestedTokens: 1 # 每次请求获取的令牌数
这边的参数表示填充的速率是1/s,桶的总容量也为1,每次请求获取一个令牌。也就是一秒只允许一次请求。测试生效:
结语
最后还是倾向于使用自定义的限流,他不需要引入redis组件,而且也可以自己重写响应到页面,更加灵活一点。
参考地址:https://docs.spring.io/spring-cloud-gateway/docs/3.0.4/reference/html/#the-requestratelimiter-gatewayfilter-factory
代码地址:https://github.com/yzh19961031/SpringCloudDemo/tree/main/gateway