API 网关深度实战——Spring Cloud Gateway 限流、熔断、灰度发布完整方案
API 网关深度实战——Spring Cloud Gateway 限流、熔断、灰度发布完整方案
适读人群:使用 Spring Cloud 做微服务网关的 Java 后端开发者 | 阅读时长:约18分钟 | 核心价值:掌握 Spring Cloud Gateway 核心功能的完整实现,从配置到生产实践
网关"被打穿"的那次教训
去年六月,我的同事阿成负责一个 C 端应用的网关,用的是 Spring Cloud Gateway。运营搞了一个抽奖活动,推广没做好预估,结果活动上线后流量是预期的 20 倍。
网关没有配置限流,直接把流量全部透传到后端。后端服务撑了 5 分钟就开始 OOM 重启,重启后流量依然来,服务再崩,陷入死循环。
最惨的是,因为服务不断重启,连带把 Nacos 上的服务注册信息搞乱了,导致本来正常的其他服务也无法路由,整个应用瘫痪了约 25 分钟。
复盘时,阿成总结出三个漏洞:没有限流、没有熔断、没有流量隔离。
这三个功能,Spring Cloud Gateway 都能做,但很多团队不会配置或者没有配置。今天把这三块完整讲透。
Spring Cloud Gateway 核心架构
Gateway 基于 Spring WebFlux(响应式编程),所有请求处理都是非阻塞的,这意味着:
- 不能用 ThreadLocal(每个请求的线程可能变化)
- 不能用 BIO(会阻塞 Reactor 线程池)
- 性能很好(少量线程处理大量并发)
基本配置:
spring:
cloud:
gateway:
routes:
- id: order-service-route
uri: lb://order-service # lb:// 表示通过负载均衡访问
predicates:
- Path=/api/order/**
filters:
- StripPrefix=2 # 转发时去掉前2个路径段限流实现:基于 Redis 的令牌桶
Spring Cloud Gateway 内置了 RequestRateLimiter 过滤器,基于 Redis 实现令牌桶算法:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>@Configuration
public class RateLimiterConfig {
/**
* 基于用户 ID 的限流 key(不同用户独立限流)
*/
@Bean
public KeyResolver userKeyResolver() {
return exchange -> {
// 从请求头中取用户 ID(由认证过滤器设置)
String userId = exchange.getRequest().getHeaders()
.getFirst("X-User-Id");
if (userId == null) {
// 未登录用户用 IP 限流
userId = exchange.getRequest().getRemoteAddress()
.getAddress().getHostAddress();
}
return Mono.just(userId);
};
}
/**
* 基于接口路径的限流(全局限流)
*/
@Bean
public KeyResolver apiKeyResolver() {
return exchange -> {
String path = exchange.getRequest().getPath().value();
return Mono.just(path);
};
}
}spring:
cloud:
gateway:
routes:
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/order/**
filters:
- name: RequestRateLimiter
args:
# 令牌生成速率:每秒 100 个令牌
redis-rate-limiter.replenishRate: 100
# 令牌桶容量:允许突发到 200(桶最多装 200 个令牌)
redis-rate-limiter.burstCapacity: 200
# 每个请求消耗多少令牌(默认1)
redis-rate-limiter.requestedTokens: 1
# 使用哪个 KeyResolver
key-resolver: "#{@userKeyResolver}"限流被拒后的自定义响应:
@Component
public class CustomRateLimitGatewayFilterFactory implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
ServerHttpResponse response = exchange.getResponse();
if (response.getStatusCode() == HttpStatus.TOO_MANY_REQUESTS) {
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
String body = "{\"code\":429,\"message\":\"请求过于频繁,请稍后重试\"}";
DataBuffer buffer = response.bufferFactory()
.wrap(body.getBytes(StandardCharsets.UTF_8));
response.writeWith(Mono.just(buffer)).subscribe();
}
}));
}
@Override
public int getOrder() {
return -1;
}
}熔断实现:基于 Resilience4j
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
</dependency>spring:
cloud:
gateway:
routes:
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/order/**
filters:
- name: CircuitBreaker
args:
name: orderServiceCircuitBreaker
# 熔断后的降级接口
fallbackUri: forward:/fallback/order
resilience4j:
circuitbreaker:
configs:
default:
# 熔断触发阈值:失败率超过50%
failureRateThreshold: 50
# 统计窗口大小(最近10次请求)
slidingWindowSize: 10
# 进入 HALF_OPEN 状态时允许的测试请求数
permittedNumberOfCallsInHalfOpenState: 5
# 等待多久后从 OPEN 转到 HALF_OPEN
waitDurationInOpenState: 10s
# 慢请求阈值(超过2秒算慢请求)
slowCallDurationThreshold: 2s
# 慢请求比例超过80%也触发熔断
slowCallRateThreshold: 80
instances:
orderServiceCircuitBreaker:
baseConfig: default// 降级接口
@RestController
@RequestMapping("/fallback")
public class FallbackController {
@GetMapping("/order")
public ResponseEntity<Map<String, Object>> orderFallback(
ServerWebExchange exchange) {
Map<String, Object> result = new HashMap<>();
result.put("code", 503);
result.put("message", "订单服务暂时不可用,请稍后重试");
result.put("timestamp", System.currentTimeMillis());
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(result);
}
}灰度发布:基于请求头的路由
灰度发布的核心是:让部分流量去新版本,其余流量去旧版本。
方案一:基于请求头的灰度
spring:
cloud:
gateway:
routes:
# 灰度路由(优先级高,放在前面)
- id: order-service-gray
uri: lb://order-service-gray # 灰度版本的服务名
predicates:
- Path=/api/order/**
# 请求头中有 X-Gray-User: true 的走灰度
- Header=X-Gray-User, true
order: 1
# 正常路由
- id: order-service-stable
uri: lb://order-service
predicates:
- Path=/api/order/**
order: 2方案二:基于权重的灰度(Spring Cloud Gateway 内置)
spring:
cloud:
gateway:
routes:
- id: order-service-v1
uri: lb://order-service-v1
predicates:
- Path=/api/order/**
- Weight=order-group, 90 # 90% 流量
- id: order-service-v2
uri: lb://order-service-v2
predicates:
- Path=/api/order/**
- Weight=order-group, 10 # 10% 流量方案三:动态灰度路由(实现热更新)
不用重启网关的动态灰度配置:
@Configuration
public class DynamicGrayRouteConfig {
@Autowired
private RouteDefinitionWriter routeDefinitionWriter;
@Autowired
private ApplicationEventPublisher eventPublisher;
/**
* 动态更新灰度比例(可通过配置中心下发)
*/
public void updateGrayWeight(String routeGroup, int stableWeight, int grayWeight) {
// 删除旧的路由规则
routeDefinitionWriter.delete(Mono.just("order-service-v1")).subscribe();
routeDefinitionWriter.delete(Mono.just("order-service-v2")).subscribe();
// 创建新的路由规则
RouteDefinition stable = buildRoute(
"order-service-v1", "lb://order-service-v1",
routeGroup, stableWeight);
RouteDefinition gray = buildRoute(
"order-service-v2", "lb://order-service-v2",
routeGroup, grayWeight);
routeDefinitionWriter.save(Mono.just(stable)).subscribe();
routeDefinitionWriter.save(Mono.just(gray)).subscribe();
// 发布路由刷新事件
eventPublisher.publishEvent(new RefreshRoutesEvent(this));
}
}认证鉴权:全局 JWT 过滤器
@Component
public class JwtAuthenticationFilter implements GlobalFilter, Ordered {
@Autowired
private JwtTokenService jwtTokenService;
// 白名单:不需要认证的接口
private static final List<String> WHITE_LIST = Arrays.asList(
"/api/user/login",
"/api/user/register",
"/api/health"
);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getURI().getPath();
// 白名单直接放行
if (WHITE_LIST.stream().anyMatch(path::startsWith)) {
return chain.filter(exchange);
}
// 获取 Authorization Header
String authHeader = exchange.getRequest().getHeaders()
.getFirst("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
return unauthorized(exchange, "缺少认证信息");
}
String token = authHeader.substring(7);
return jwtTokenService.validateToken(token)
.flatMap(userInfo -> {
// 将用户信息添加到请求头,传递给下游服务
ServerHttpRequest mutatedRequest = exchange.getRequest()
.mutate()
.header("X-User-Id", userInfo.getUserId())
.header("X-User-Name", userInfo.getUsername())
.header("X-User-Roles", String.join(",", userInfo.getRoles()))
.build();
return chain.filter(exchange.mutate().request(mutatedRequest).build());
})
.onErrorResume(e -> unauthorized(exchange, "Token 无效或已过期"));
}
private Mono<Void> unauthorized(ServerWebExchange exchange, String message) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
String body = String.format(
"{\"code\":401,\"message\":\"%s\"}", message);
DataBuffer buffer = response.bufferFactory()
.wrap(body.getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(buffer));
}
@Override
public int getOrder() {
return -100; // 最先执行
}
}三大踩坑实录
坑一:限流 Key 设计不当,误限正常用户
现象: 部署了基于 IP 的全局限流后,发现部分公司用户大量报 429(限流)错误,但实际上他们的请求频率并不高。
原因: 公司用户经过公司内网 NAT 出口,多台设备共用一个出口 IP。基于 IP 限流会把整个公司的请求当成一个用户,很快触发限流阈值。
解法: 对已登录用户用用户 ID 限流,对未登录用户才用 IP 限流。
坑二:熔断降级返回 200,导致客户端误判
现象: 熔断触发后,降级接口返回了 HTTP 200,但 body 里是错误信息。前端判断了 HTTP 状态码,认为请求成功,把错误信息直接展示给用户,显示了一堆 JSON。
原因: 降级接口没有正确设置 HTTP 状态码。
解法: 降级接口返回合适的 HTTP 状态码(服务不可用用 503,限流用 429),前端根据 HTTP 状态码处理,而不是判断 body 内容。
坑三:Gateway 作为 WebFlux 应用与 Spring MVC 冲突
现象: 想在 Gateway 中引入一些 Spring MVC 的工具类,结果启动报错:Spring MVC found on classpath, which is incompatible with Spring Cloud Gateway。
原因: Spring Cloud Gateway 基于 WebFlux,与 Spring MVC 不兼容,不能在同一个应用中共存。
解法: Gateway 必须保持"纯净",不要在 Gateway 里写业务逻辑。如果需要业务处理,通过过滤器调用独立的业务服务。同时确保 classpath 上没有 spring-boot-starter-web(Spring MVC),只有 spring-boot-starter-webflux。
生产配置模板
spring:
cloud:
gateway:
# 开启全局 CORS
globalcors:
cors-configurations:
'[/**]':
allowedOrigins: "*"
allowedMethods: "GET,POST,PUT,DELETE,OPTIONS"
allowedHeaders: "*"
maxAge: 3600
# 默认过滤器(对所有路由生效)
default-filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 1000
redis-rate-limiter.burstCapacity: 2000
key-resolver: "#{@apiKeyResolver}"
- name: Retry
args:
retries: 3
statuses: BAD_GATEWAY,SERVICE_UNAVAILABLE
methods: GET,HEAD
backoff:
firstBackoff: 10ms
maxBackoff: 50ms
factor: 2