4. 网关层设计:Spring Cloud Gateway 路由、限流、熔断、降级
好,咱们进入第四章。网关层。
说实话,很多团队做微服务,网关就是随便配个路由就上线了。结果呢?流量一上来,网关先挂了。我见过最夸张的一次,网关成了整个系统的瓶颈,下游服务一个没倒,网关自己先被冲垮了。嗯,这其实是个很尴尬的事。
所以这一章,我会把 Spring Cloud Gateway 的核心能力拆开揉碎。路由、限流、熔断、降级,这四个能力缺一不可。我个人习惯是把网关当成系统的「第一道防线」,而不是简单的「请求转发器」。
4.1 路由配置:别只写静态路由
路由是网关最基础的能力。但很多人只会写死路由,比如:
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/user/**
这样写没问题,但不够灵活。我在项目中遇到过一个问题:某个服务需要灰度发布,新版本和老版本的路由规则不一样。如果全写死,每次改路由都要重启网关,这谁受得了?
我的做法是:路由配置动态化。把路由信息存到 Nacos 或 Redis 里,网关启动时加载,运行时监听变更。Spring Cloud Gateway 提供了 RouteDefinitionRepository 接口,你只需要实现它:
public class NacosRouteDefinitionRepository implements RouteDefinitionRepository {
@Override
public Flux<RouteDefinition> getRouteDefinitions() {
// 从 Nacos 读取路由配置
return Flux.fromIterable(loadRoutesFromNacos());
}
@Override
public Mono<Void> save(Mono<RouteDefinition> route) {
// 保存到 Nacos
return route.flatMap(r -> saveToNacos(r));
}
@Override
public Mono<Void> delete(Mono<String> routeId) {
// 从 Nacos 删除
return routeId.flatMap(id -> deleteFromNacos(id));
}
}
这样,路由变更只需要改 Nacos 配置,网关自动刷新。你想想看,是不是省心多了?
4.2 限流:别让流量冲垮你的网关
限流是网关的核心能力。没有限流,一个突发流量就能让你的系统瘫痪。
Spring Cloud Gateway 内置了 RequestRateLimiter 过滤器,基于 Redis 和令牌桶算法。配置起来很简单:
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/user/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
key-resolver: "#{@userKeyResolver}"
这里 replenishRate 是每秒填充的令牌数,burstCapacity 是最大突发容量。说白了,就是每秒最多处理 10 个请求,突发情况下可以到 20 个。
但这里有个坑。我曾经遇到过:限流粒度太粗,导致某个恶意用户刷接口,把整个服务的额度都耗光了。怎么办?
按用户维度限流。 实现 KeyResolver 接口,根据用户 ID 或 IP 生成限流 key:
@Bean
public KeyResolver userKeyResolver() {
return exchange -> {
String userId = exchange.getRequest().getHeaders().getFirst("X-User-Id");
if (userId == null) {
userId = exchange.getRequest().getRemoteAddress().getAddress().getHostAddress();
}
return Mono.just(userId);
};
}
这样,每个用户都有自己的令牌桶。一个用户刷接口,只影响他自己,不影响别人。
4.3 熔断:别让故障蔓延
熔断是防止雪崩的关键。下游服务挂了,网关不能一直重试,否则会把整个系统拖垮。
Spring Cloud Gateway 集成了 Resilience4j 的熔断器。配置方式如下:
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/user/**
filters:
- name: CircuitBreaker
args:
name: userServiceCircuitBreaker
fallbackUri: forward:/fallback/user
然后在 application.yml 里配置熔断参数:
resilience4j:
circuitbreaker:
configs:
default:
slidingWindowSize: 10
minimumNumberOfCalls: 5
failureRateThreshold: 50
waitDurationInOpenState: 10000
permittedNumberOfCallsInHalfOpenState: 3
这些参数什么意思?我解释一下:
- slidingWindowSize:滑动窗口大小,统计最近 10 次请求
- minimumNumberOfCalls:最少请求数,少于这个数不触发熔断
- failureRateThreshold:失败率阈值,超过 50% 就熔断
- waitDurationInOpenState:熔断持续时间,10 秒后进入半开状态
- permittedNumberOfCallsInHalfOpenState:半开状态下允许的请求数
我曾经犯过一个错误:把 waitDurationInOpenState 设得太短,比如 1 秒。结果下游服务还没恢复,熔断器就频繁开闭,反而加重了系统负担。后来我改成 10 秒,效果好了很多。
核心原则: 熔断不是越快恢复越好。要给下游服务留出足够的恢复时间。我一般建议至少 5 秒以上。
4.4 降级:给用户一个体面的回应
熔断之后,不能直接返回 500 错误。用户看到「服务器内部错误」,体验很差。降级就是给用户一个体面的回应。
Spring Cloud Gateway 的降级很简单,配置一个 fallback 路由:
@RestController
public class FallbackController {
@GetMapping("/fallback/user")
public Mono<Map<String, Object>> userFallback() {
Map<String, Object> result = new HashMap<>();
result.put("code", 503);
result.put("message", "服务暂时不可用,请稍后重试");
return Mono.just(result);
}
}
但降级不只是返回一个提示。我建议做 分级降级:
| 降级级别 | 触发条件 | 返回内容 |
|---|---|---|
| 一级降级 | 单个接口熔断 | 返回缓存数据或默认值 |
| 二级降级 | 整个服务熔断 | 返回友好提示,引导用户重试 |
| 三级降级 | 网关自身过载 | 返回 429,提示请求过多 |
举个例子。用户查询订单列表,如果订单服务熔断了,一级降级可以返回最近 5 分钟的缓存数据。用户看到的是旧数据,但至少不是空白页。你想想看,哪个体验更好?
4.5 组合使用:一个完整的网关配置
最后,我把路由、限流、熔断、降级组合起来,给一个完整的配置示例:
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/user/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
key-resolver: "#{@userKeyResolver}"
- name: CircuitBreaker
args:
name: userServiceCircuitBreaker
fallbackUri: forward:/fallback/user
- name: Retry
args:
retries: 2
statuses: BAD_GATEWAY, SERVICE_UNAVAILABLE
methods: GET
这里我还加了一个 Retry 过滤器。注意,重试次数不能太多,我一般设 2 次。重试太多,反而会加重下游负担。
嗯,到这里网关层的核心设计就讲完了。总结一下:
- 路由要动态化,别写死
- 限流要按用户维度,别一刀切
- 熔断要给恢复留时间,别频繁开闭
- 降级要分级处理,别只返回 500
下一章,我们会进入服务间通信层。到时候聊聊 Feign 的超时配置、重试策略,还有线程隔离。这些都是我在线上踩过的坑,到时候一一分享给你。