Forráskód Böngészése

update gateway rate limiter

reghao 8 hónapja
szülő
commit
6e59d8c045

+ 11 - 0
gateway/pom.xml

@@ -68,6 +68,17 @@
             <artifactId>commons-lang</artifactId>
             <version>2.5</version>
         </dependency>
+
+        <!--<dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-actuator</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
+            <version>2.2.10.RELEASE</version>
+        </dependency>-->
     </dependencies>
 
     <profiles>

+ 0 - 47
gateway/src/main/java/cn/reghao/tnb/gateway/limiter/GlobalLimiterFilter.java

@@ -1,47 +0,0 @@
-package cn.reghao.tnb.gateway.limiter;
-
-import org.springframework.cloud.gateway.filter.GatewayFilterChain;
-import org.springframework.cloud.gateway.filter.GlobalFilter;
-import org.springframework.core.Ordered;
-import org.springframework.data.redis.core.RedisTemplate;
-import org.springframework.web.server.ServerWebExchange;
-import reactor.core.publisher.Mono;
-
-
-/**
- * @author reghao
- * @date 2024-11-21 17:10:03
- */
-public class GlobalLimiterFilter implements GlobalFilter, Ordered {
-    private final RedisTemplate<String, String> redisTemplate;
-    private MyRateLimiter myRateLimiter;
-
-    public GlobalLimiterFilter(RedisTemplate<String, String> redisTemplate, MyRateLimiter myRateLimiter) {
-        this.redisTemplate = redisTemplate;
-        this.myRateLimiter = myRateLimiter;
-    }
-
-    @Override
-    public int getOrder() {
-        return -1;
-    }
-
-    @Override
-    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
-        //获取到调用客户端的IP地址
-        String ip = exchange.getRequest().getRemoteAddress().getHostName();
-        //从数据库中获取到该IP地址对应的限流参数
-        /*IpRate ipRate = ipRateService.get(ip);
-        String path = exchange.getRequest().getPath().value();
-
-        //如果允许同行,没有超过该ip的流量限制
-        if(myRateLimiter.isAllowed(ip, path)){
-            return chain.filter(exchange);
-        }else{
-            exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
-            return exchange.getResponse().setComplete();
-        }*/
-
-        return chain.filter(exchange);
-    }
-}

+ 79 - 0
gateway/src/main/java/cn/reghao/tnb/gateway/limiter/LimiterConfig.java

@@ -0,0 +1,79 @@
+package cn.reghao.tnb.gateway.limiter;
+
+import org.springframework.cloud.gateway.route.RouteLocator;
+import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.web.reactive.function.server.RequestPredicates;
+import org.springframework.web.reactive.function.server.RouterFunction;
+import org.springframework.web.reactive.function.server.RouterFunctions;
+import org.springframework.web.reactive.function.server.ServerResponse;
+import reactor.core.publisher.Mono;
+
+/**
+ * @author reghao
+ * @date 2025-07-16 10:02:14
+ */
+//@Configuration
+public class LimiterConfig {
+    private final MetricsRateLimiterFilter metricsRateLimiterFilter;
+
+    public LimiterConfig(MetricsRateLimiterFilter metricsRateLimiterFilter) {
+        this.metricsRateLimiterFilter = metricsRateLimiterFilter;
+    }
+
+    /**
+     * 实现 RequestRateLimiterGatewayFilterFactory 和 RateLimiter 来限流
+     *
+     * @param
+     * @return
+     * @date 2025-07-17 15:33:36
+     */
+    //@Bean
+    public RouteLocator routeLocator(RouteLocatorBuilder builder) {
+        return builder.routes()
+                .route("rate-limiter", r -> r.path("/api/content/**")
+                        .filters(filter -> filter.requestRateLimiter()
+                                .rateLimiter(MyRateLimiter.class, limiter -> limiter.setRequestedTokens(10))
+                                .and()
+                        )
+                        .uri("lb://content-service")
+                )
+                .build();
+    }
+
+    /**
+     * 实现 GatewayFilter 来限流
+     *
+     * @param
+     * @return
+     * @date 2025-07-17 15:32:51
+     */
+    //@Bean
+    public RouteLocator customerRouteLocator(RouteLocatorBuilder builder) {
+        return builder.routes()
+                .route(r -> r.path("/apit/content/**")
+                        .filters(f -> f.stripPrefix(0).filter(metricsRateLimiterFilter))
+                        .uri("lb://content-service")
+                )
+                .build();
+    }
+
+    /**
+     * hystrix 熔断接口(需要 spring-cloud-starter-netflix-hystrix 依赖)
+     *
+     * @param
+     * @return
+     * @date 2025-07-17 14:38:13
+     */
+    //@Bean
+    public RouterFunction<ServerResponse> fallbackRouter() {
+        return RouterFunctions
+                .route(RequestPredicates.GET("/fallback"), request -> ServerResponse
+                        .status(HttpStatus.TOO_MANY_REQUESTS)
+                        .contentType(MediaType.TEXT_PLAIN)
+                        .body(Mono.just("timeout limiter"), String.class));
+    }
+}

+ 61 - 0
gateway/src/main/java/cn/reghao/tnb/gateway/limiter/MetricsRateLimiterFilter.java

@@ -0,0 +1,61 @@
+package cn.reghao.tnb.gateway.limiter;
+
+import lombok.extern.slf4j.Slf4j;
+//import org.springframework.boot.actuate.metrics.MetricsEndpoint;
+import org.springframework.cloud.gateway.filter.GatewayFilter;
+import org.springframework.cloud.gateway.filter.GatewayFilterChain;
+import org.springframework.core.Ordered;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Component;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+
+import java.util.Objects;
+
+/**
+ * 根据 CPU 使用率限流(需要 spring-boot-starter-actuator 依赖)
+ *
+ * @author reghao
+ * @date 2025-07-16 10:02:14
+ */
+@Slf4j
+@Component
+public class MetricsRateLimiterFilter implements GatewayFilter, Ordered {
+    private static final String METRIC_NAME = "system.cpu.usage";
+    private static final double MAX_USAGE = 0.50D;
+    /*private final MetricsEndpoint metricsEndpoint;
+
+    public MetricsRateLimiterFilter(MetricsEndpoint metricsEndpoint) {
+        this.metricsEndpoint = metricsEndpoint;
+    }*/
+
+    @Override
+    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
+        double systemCpuUsage = getCpuUsage();
+        boolean ok = systemCpuUsage < MAX_USAGE;
+        log.debug("system.cpu.usage: " + systemCpuUsage + " ok: " + ok);
+        if (!ok) {
+            exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
+            return exchange.getResponse().setComplete();
+        } else {
+            return chain.filter(exchange);
+        }
+    }
+
+    private double getCpuUsage() {
+        /*double usage = metricsEndpoint.metric(METRIC_NAME, null)
+                .getMeasurements()
+                .stream()
+                .filter(Objects::nonNull)
+                .findFirst()
+                .map(MetricsEndpoint.Sample::getValue)
+                .filter(Double::isFinite)
+                .orElse(0.0D);*/
+        return 0.0D;
+    }
+
+    @Override
+    public int getOrder() {
+        return 0;
+    }
+}

+ 0 - 37
gateway/src/main/java/cn/reghao/tnb/gateway/limiter/MyKeyResolver.java

@@ -1,37 +0,0 @@
-package cn.reghao.tnb.gateway.limiter;
-
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
-import org.springframework.cloud.gateway.route.Route;
-import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.server.reactive.ServerHttpRequest;
-import org.springframework.stereotype.Component;
-import org.springframework.web.server.ServerWebExchange;
-import reactor.core.publisher.Mono;
-
-import java.util.Optional;
-
-/**
- * @author reghao
- * @date 2023-09-14 11:51:58
- */
-@Slf4j
-//@Component
-public class MyKeyResolver implements KeyResolver {
-    @Override
-    public Mono<String> resolve(ServerWebExchange exchange) {
-        HttpHeaders httpHeaders = exchange.getRequest().getHeaders();
-        String loginId = httpHeaders.get("x-login-id").get(0);
-        String userId = httpHeaders.get("x-user-id").get(0);
-        String remoteAddress = exchange.getRequest().getRemoteAddress().getHostString();
-        String requestUrl = exchange.getRequest().getPath().value();
-        return Mono.just(String.format("%s:%s", userId, requestUrl));
-
-        /*Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
-        ServerHttpRequest request = exchange.getRequest();
-        String uri = request.getURI().getPath();
-        log.info("限流的 URI: {}", uri);
-        return Mono.just(Optional.ofNullable(route).map(Route::getId).orElse("") + "/" + uri);*/
-    }
-}

+ 69 - 0
gateway/src/main/java/cn/reghao/tnb/gateway/limiter/MyKeyResolverConfig.java

@@ -0,0 +1,69 @@
+package cn.reghao.tnb.gateway.limiter;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+
+import java.util.Objects;
+
+/**
+ * @author reghao
+ * @date 2025-07-17 10:24:14
+ */
+@Slf4j
+@Configuration
+public class MyKeyResolverConfig {
+    /**
+     * 根据请求 IP 限流
+     *
+     * @param
+     * @return
+     * @date 2025-07-16 09:55:42
+     */
+    @Bean("keyResolver")
+    public KeyResolver ipKeyResolver() {
+        return exchange -> Mono.just(Objects.requireNonNull(Objects.requireNonNull(
+                exchange.getRequest().getRemoteAddress()).getAddress().getHostAddress()));
+    }
+
+    /**
+     * 根据请求路径限流
+     *
+     * @param
+     * @return
+     * @date 2025-07-16 09:56:45
+     */
+    //@Bean("keyResolver")
+    public KeyResolver pathKeyResolver() {
+        return new KeyResolver() {
+            @Override
+            public Mono<String> resolve(ServerWebExchange exchange) {
+                ServerHttpRequest request = exchange.getRequest();
+                String path = request.getURI().getPath();
+                String remoteAddr = Objects.requireNonNull(request.getRemoteAddress()).getHostString();
+                log.info("{} -> {}", path, remoteAddr);
+                return Mono.just(path);
+            }
+        };
+    }
+
+    /**
+     * 根据请求参数限流(这里根据 userId 参数)
+     *
+     * @param
+     * @return
+     * @date 2025-07-16 09:55:59
+     */
+    //@Bean(name = "keyResolver")
+    public KeyResolver userKeyResolver() {
+        String paramName = "userId";
+        return exchange -> Mono.just(Objects.requireNonNull(exchange.getRequest()
+                .getQueryParams()
+                .getFirst(paramName))
+        );
+    }
+}

+ 0 - 23
gateway/src/main/java/cn/reghao/tnb/gateway/limiter/MyLimiterConfig.java

@@ -1,23 +0,0 @@
-package cn.reghao.tnb.gateway.limiter;
-
-import org.springframework.cloud.gateway.route.RouteLocator;
-import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-
-/**
- * @author reghao
- * @date 2023-09-14 13:21:48
- */
-//@Configuration
-public class MyLimiterConfig {
-    @Bean
-    public RouteLocator routeLocator(RouteLocatorBuilder builder) {
-        return builder.routes()
-                .route("rate-limiter", r -> r.path("/api/content/**")
-                        .filters(filter -> filter.requestRateLimiter().rateLimiter(MyRateLimiter.class, limiter -> limiter.setRequestedTokens(10)).and())
-                        .uri("lb://content-service")
-                )
-                .build();
-    }
-}

+ 9 - 8
gateway/src/main/java/cn/reghao/tnb/gateway/limiter/MyRateLimiter.java

@@ -6,6 +6,7 @@ import lombok.Getter;
 import lombok.Setter;
 import lombok.ToString;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.context.properties.ConfigurationProperties;
 import org.springframework.cloud.gateway.filter.ratelimit.AbstractRateLimiter;
 import org.springframework.cloud.gateway.support.ConfigurationService;
 import org.springframework.context.annotation.Primary;
@@ -13,18 +14,18 @@ import org.springframework.stereotype.Component;
 import reactor.core.publisher.Mono;
 
 /**
+ * 参照 org.springframework.cloud.gateway.filter.ratelimit.RedisRateLimiter
+ *
+ * 已经存在一个默认实现 RedisRateLimiter, 若要使用自定义实现则需要使用 @Primary 注解
+ *
  * @author reghao
- * @date 2023-09-14 11:52:52
+ * @date 2025-07-16 10:02:14
  */
 @Slf4j
-//@Primary
-//@Component
+@Primary
+@Component
 public class MyRateLimiter extends AbstractRateLimiter<MyRateLimiter.Config> {
-    /**
-     * 和配置文件中的配置属性相对应
-     */
-    private static final String CONFIGURATION_PROPERTY_NAME = "default-gateway-rate-limiter";
-
+    private static final String CONFIGURATION_PROPERTY_NAME = "my-rate-limiter";
     private RateLimiter rateLimiter = RateLimiter.create(10);
 
     protected MyRateLimiter(ConfigurationService configurationService) {

+ 12 - 15
gateway/src/main/java/cn/reghao/tnb/gateway/limiter/MyLimiterFilterFactory.java → gateway/src/main/java/cn/reghao/tnb/gateway/limiter/MyRequestRateLimiterGatewayFilterFactory.java

@@ -2,7 +2,6 @@ package cn.reghao.tnb.gateway.limiter;
 
 import java.util.Map;
 
-import cn.reghao.tnb.gateway.log.GatewayLogService;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import lombok.extern.slf4j.Slf4j;
@@ -23,33 +22,32 @@ import reactor.core.publisher.Mono;
 import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.setResponseStatus;
 
 /**
+ * 继承 org.springframework.cloud.gateway.filter.factory.RequestRateLimiterGatewayFilterFactory
+ *
  * @author reghao
- * @date 2024-11-21 14:44:23
+ * @date 2025-07-16 10:02:14
  */
 @Slf4j
-//@Primary
-//@Component
-public class MyLimiterFilterFactory extends RequestRateLimiterGatewayFilterFactory {
+@Primary
+@Component
+public class MyRequestRateLimiterGatewayFilterFactory extends RequestRateLimiterGatewayFilterFactory {
     private static final String EMPTY_KEY = "____EMPTY_KEY__";
     private final ObjectMapper objectMapper;
-    private final GatewayLogService gatewayLogService;
 
-    public MyLimiterFilterFactory(RateLimiter defaultRateLimiter, KeyResolver defaultKeyResolver,
-                                  ObjectMapper objectMapper, GatewayLogService gatewayLogService) {
+    public MyRequestRateLimiterGatewayFilterFactory(RateLimiter defaultRateLimiter,
+                                                    KeyResolver defaultKeyResolver,
+                                                    ObjectMapper objectMapper) {
         super(defaultRateLimiter, defaultKeyResolver);
         this.objectMapper = objectMapper;
-        this.gatewayLogService = gatewayLogService;
     }
 
     @Override
     public GatewayFilter apply(RequestRateLimiterGatewayFilterFactory.Config config) {
         KeyResolver resolver = getOrDefault(config.getKeyResolver(), super.getDefaultKeyResolver());
-        @SuppressWarnings("unchecked")
         RateLimiter<Object> limiter = getOrDefault(config.getRateLimiter(), super.getDefaultRateLimiter());
         boolean denyEmpty = getOrDefault(config.getDenyEmptyKey(), super.isDenyEmptyKey());
-        HttpStatusHolder emptyKeyStatus = HttpStatusHolder
-                .parse(getOrDefault(config.getEmptyKeyStatus(), super.getEmptyKeyStatusCode()));
-
+        HttpStatusHolder emptyKeyStatus = HttpStatusHolder.parse(getOrDefault(config.getEmptyKeyStatus(), super.getEmptyKeyStatusCode()));
+        
         return (exchange, chain) -> resolver.resolve(exchange).defaultIfEmpty(EMPTY_KEY).flatMap(key -> {
             if (EMPTY_KEY.equals(key)) {
                 if (denyEmpty) {
@@ -64,11 +62,11 @@ public class MyLimiterFilterFactory extends RequestRateLimiterGatewayFilterFacto
                 assert route != null;
                 routeId = route.getId();
             }
+
             return limiter.isAllowed(routeId, 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);
                 }
@@ -79,7 +77,6 @@ public class MyLimiterFilterFactory extends RequestRateLimiterGatewayFilterFacto
                     log.warn("Unable to set status code to " + serverHttpResponse + ". Response already committed.");
                 }
                 serverHttpResponse.getHeaders().setContentType(MediaType.APPLICATION_JSON);
-                gatewayLogService.setGatewayLog(exchange);
                 return serverHttpResponse.writeWith(Mono.create(monoSink -> {
                     try {
                         String body = "REQUEST_RATE_LIMIT";

+ 18 - 0
gateway/src/main/resources/application.yml

@@ -10,6 +10,24 @@ spring:
       ribbon:
         enabled: false
     gateway:
+      # 默认过滤器(对所有 route 均生效)
+      default-filters:
+        # 限流配置
+        - name: MyRequestRateLimiter
+          args:
+            # 如果 keyResolver 返回空 key 则拒绝请求返回 403, 默认 true 表示拒绝, false 表示允许访问
+            deny-empty-key: false
+            # 自定义的 KeyResolver(从请求 exchange 解析 id 来区分独立的限流单元, id 可以是 userId, remoteAddr, sessionId 等)
+            key-resolver: "#{@keyResolver}"
+#            rate-limiter: "#{@myRateLimiter}"
+#            my-rate-limiter.requestedTokens: 1
+            rate-limiter: "#{@redisRateLimiter}"
+            # 令牌桶算法每秒补充的 token 数量(每秒的请求数量)
+            redis-rate-limiter.replenishRate: 100
+            # 令牌桶算法的 token 最大数量(每秒的最大请求数量)
+            redis-rate-limiter.burstCapacity: 150
+            # 单次请求消费的 token 数量
+            redis-rate-limiter.requestedTokens: 1
       routes:
         - id: account-router
           # spring-cloud-starter-zookeeper-discovery 提供服务发现功能