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

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐