场景

公司产品迭代,架构升级了,从 vertx 换成了 spring Cloud 全家桶,最开始微服务网关用的是 ZUUL 后来换成了 spring官网的 springcloud getway 网关,在按照官网配置限流配置文件和代码的时候,碰到点问题,把解决过程记录一下

最开始限流配置文件是这样写的

 spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true
      default-filters:
        # 添加限流机器,基于令牌桶算法,当服务超出限流约束,则直接拒决请求:RequestRateLimiter(默认),CustomRequestRateLimiter(自定义)
        - name: RequestRateLimiter
          args:
            redis-rate-limiter.replenishRate: 1 #允许用户每秒处理多少个请求
            redis-rate-limiter.burstCapacity: 1 #令牌桶的容量,允许在一秒钟内完成的最大请求数
            redis-rate-limiter.requestedTokens: 1
            key-resolver: "#{@ipKeyResolver}"
        - DedupeResponseHeader=Access-Control-Allow-Origin, RETAIN_UNIQUE

配置文件里,按照官网的例子,配置成了 RequestRateLimiter 限流器

记一次Spring Gateway 限流遇到的问题-LMLPHP

具体的java代码大体如下

/**
 * @ClassName RequestRateLimiterConfig
 * @Deacription 限流
 * @Author xuzhou
 * @Date 2020/9/1 18:17
 * @Version 1.0
 **/
@Configuration
public class RequestRateLimiterConfig {

    @Bean("apiKeyResolver")
    @Primary
    KeyResolver apiKeyResolver() {
        //按URL限流,即以每秒内请求数按URL分组统计,超出限流的url请求都将返回429状态
        return exchange -> Mono.just(exchange.getRequest().getPath().toString());
    }

    @Bean("userKeyResolver")
    KeyResolver userKeyResolver() {
        //按用户限流
        return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));
    }

    @Bean("ipKeyResolver")
    KeyResolver ipKeyResolver() {
        //按IP来限流
        return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
    }
}


测试

配置好之后,启动程序测试,限流配置和代码生效了,但是限流之后,返回的结果不友好 记一次Spring Gateway 限流遇到的问题-LMLPHP

使用Postman测试,限流之后,HTTP状态码变成了 429,但是返回体里面没有详细信息 后端为了统一处理错误,返回友好的提示给前端,程序都会处理,但是在官方提供的 RequestRateLimiterGatewayFilterFactory 过滤器里面,是直接返回了,没有任何回调接口提供程序做处理。 记一次Spring Gateway 限流遇到的问题-LMLPHP

解决

开始想的比较简单,自己 实现 GlobalFilter 全局过滤器里面的方法,在 Response的时候做拦截,获取到http 429 code,重新封装返回值,但是实践之后发现不起作用,翻阅资料发现:局部过滤器,优先于全局过滤,所以自己实现的拦截不起作用。 后来经过社区的网友提点,自己继承了 AbstractGatewayFilterFactory 然后复制 RequestRateLimiterGatewayFilterFactory 的代码,重写 apply 方法,自己实现了一份限流过滤器,然后在里面做代码处理

 /*
 * Copyright 2013-2019 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import com.alibaba.fastjson.JSONObject;
import com.rayeye.springgetaway.util.RespObj;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.cloud.gateway.filter.ratelimit.RateLimiter;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

import java.util.Map;

/**
 * User Request Rate Limiter filter. See https://stripe.com/blog/rate-limiters and
 * 重写 RequestRateLimiterGatewayFilterFactory
 */
@ConfigurationProperties("spring.cloud.gateway.filter.request-rate-limiter")
@Component("CustomRequestRateLimiter")
public class CustomRequestRateLimiterGatewayFilterFactory extends AbstractGatewayFilterFactory<CustomRequestRateLimiterGatewayFilterFactory.Config> {

    private static Logger logger = LoggerFactory.getLogger(CustomRequestRateLimiterGatewayFilterFactory.class);
    /**
     * Key-Resolver key.
     */
    public static final String KEY_RESOLVER_KEY = "keyResolver";

    private static final String EMPTY_KEY = "____EMPTY_KEY__";

    private final RateLimiter defaultRateLimiter;

    private final KeyResolver defaultKeyResolver;

    /**
     * Switch to deny requests if the Key Resolver returns an empty key, defaults to true.
     */
    private boolean denyEmptyKey = true;

    /**
     * HttpStatus to return when denyEmptyKey is true, defaults to FORBIDDEN.
     */
    private String emptyKeyStatusCode = HttpStatus.FORBIDDEN.name();

    public CustomRequestRateLimiterGatewayFilterFactory(RateLimiter defaultRateLimiter,
                                                        KeyResolver defaultKeyResolver) {
        super(Config.class);
        this.defaultRateLimiter = defaultRateLimiter;
        this.defaultKeyResolver = defaultKeyResolver;
    }

    public KeyResolver getDefaultKeyResolver() {
        return defaultKeyResolver;
    }

    public RateLimiter getDefaultRateLimiter() {
        return defaultRateLimiter;
    }

    public boolean isDenyEmptyKey() {
        return denyEmptyKey;
    }

    public void setDenyEmptyKey(boolean denyEmptyKey) {
        this.denyEmptyKey = denyEmptyKey;
    }

    public String getEmptyKeyStatusCode() {
        return emptyKeyStatusCode;
    }

    public void setEmptyKeyStatusCode(String emptyKeyStatusCode) {
        this.emptyKeyStatusCode = emptyKeyStatusCode;
    }

    @SuppressWarnings("unchecked")
    @Override
    public GatewayFilter apply(Config config) {
        KeyResolver resolver = (config.keyResolver == null) ? defaultKeyResolver : config.keyResolver;
        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 ->
                    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);
                        }

                        ServerHttpResponse httpResponse = exchange.getResponse();
                        httpResponse.setStatusCode(HttpStatus.OK);
                        logger.error("访问已限流,请稍候再请求");
                        RespObj respObj = RespObj.fail("访问已限流,请稍候再请求", HttpStatus.INTERNAL_SERVER_ERROR.value());
                        DataBuffer buffer = httpResponse.bufferFactory().wrap(JSONObject.toJSONBytes(respObj));
                        return httpResponse.writeWith(Mono.just(buffer));
                    }));
        };
    }

    private <T> T getOrDefault(T configValue, T defaultValue) {
        return (configValue != null) ? configValue : defaultValue;
    }

    public static class Config {

        private KeyResolver keyResolver;

        private RateLimiter rateLimiter;

        private HttpStatus statusCode = HttpStatus.TOO_MANY_REQUESTS;

        private Boolean denyEmptyKey;

        private String emptyKeyStatus;

        public KeyResolver getKeyResolver() {
            return keyResolver;
        }

        public Config setKeyResolver(KeyResolver keyResolver) {
            this.keyResolver = keyResolver;
            return this;
        }

        public RateLimiter getRateLimiter() {
            return rateLimiter;
        }

        public Config setRateLimiter(RateLimiter rateLimiter) {
            this.rateLimiter = rateLimiter;
            return this;
        }

        public HttpStatus getStatusCode() {
            return statusCode;
        }

        public Config setStatusCode(HttpStatus statusCode) {
            this.statusCode = statusCode;
            return this;
        }

        public Boolean getDenyEmptyKey() {
            return denyEmptyKey;
        }

        public Config setDenyEmptyKey(Boolean denyEmptyKey) {
            this.denyEmptyKey = denyEmptyKey;
            return this;
        }

        public String getEmptyKeyStatus() {
            return emptyKeyStatus;
        }

        public Config setEmptyKeyStatus(String emptyKeyStatus) {
            this.emptyKeyStatus = emptyKeyStatus;
            return this;
        }

    }

}


重写配置文件

自定义 CustomRequestRateLimiter 拦截器

spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true
      default-filters:
        # 添加限流机器,基于令牌桶算法,当服务超出限流约束,则直接拒决请求:RequestRateLimiter(默认),CustomRequestRateLimiter(自定义)
        - name: CustomRequestRateLimiter
          args:
            redis-rate-limiter.replenishRate: 1 #允许用户每秒处理多少个请求
            redis-rate-limiter.burstCapacity: 1 #令牌桶的容量,允许在一秒钟内完成的最大请求数
            redis-rate-limiter.requestedTokens: 1
            key-resolver: "#{@ipKeyResolver}"
        - DedupeResponseHeader=Access-Control-Allow-Origin, RETAIN_UNIQUE

最终运行结果如下

记一次Spring Gateway 限流遇到的问题-LMLPHP

可以看到,限流起作用了,并且返回值也是自定义的。

03-30 22:06