Appearance
Spring Cloud:网关、熔断与限流
微服务系统拆开之后,真正难的往往不是“服务能不能调通”,而是:
- 外部流量怎么统一接入
- 下游服务慢了怎么办
- 某个服务挂了会不会把整条调用链拖死
- 流量过大时怎么保护系统
这篇文章就集中讲 3 条常见治理主线:
- 网关
- 熔断与降级
- 限流与系统保护
1. 为什么微服务一定会遇到统一入口问题
在单体应用里,外部请求通常直接进入一个应用就够了。
但在微服务架构里,系统往往会拆成:
- 用户服务
- 订单服务
- 库存服务
- 支付服务
- 营销服务
如果外部请求直接对接每个服务,会带来很多问题:
- 路由分散
- 鉴权分散
- 限流分散
- 日志和监控分散
- 对外暴露面过多
所以系统通常需要一个统一入口。
2. 什么是网关
网关 可以理解成 系统外部流量进入内部服务之前的统一入口层。
它解决的是:
- 请求先到哪里
- 根据什么规则路由到哪个服务
- 哪些通用逻辑应该在入口统一做
例如常见的统一逻辑包括:
- 路由转发
- 身份认证
- 权限校验
- 限流
- 日志和链路信息透传
3. Spring Cloud Gateway 主要解决什么问题
Spring Cloud Gateway 是 Spring Cloud 体系里很常见的网关方案。
它主要解决的是 如何在 Spring 生态里统一做路由、过滤和流量治理。
它更适合被理解成:一个面向微服务入口治理的网关框架。
4. 网关到底在做什么
一条请求经过网关时,常见流程通常是:
- 请求先进入网关
- 网关根据路由规则判断目标服务
- 执行一组过滤逻辑
- 再把请求转发到下游服务
- 拿到响应后再统一返回
这里最重要的两个关键词是:
- 路由
- 过滤
4.1 路由
路由解决的是:这条请求应该被转发到哪个服务。
4.2 过滤
过滤解决的是:请求在进出系统时,要统一做哪些前置和后置处理。
例如:
- 加 token 校验
- 打日志
- 加链路追踪头
- 统一异常返回
4.3 一条订单路由经过网关时会发生什么
看一个比较典型的 Spring Cloud Gateway 配置:
yaml
spring:
cloud:
gateway:
routes:
- id: order-service-route
# lb 表示通过注册中心按服务名转发,而不是写死某台机器地址
uri: lb://order-service
predicates:
# 只有命中这个路径规则的请求,才会进入当前路由
- Path=/api/orders/**
filters:
# /api/orders/create -> /orders/create,避免下游服务重复兼容网关前缀
- StripPrefix=1
# 给下游服务补一个统一请求头,方便日志和来源识别
- AddRequestHeader=X-Gateway, order-gateway
# 订单服务连续失败时,直接走降级地址,不再继续放大故障
- CircuitBreaker=name=orderCircuitBreaker,fallbackUri=forward:/fallback/orders
# 按用户维度做限流,避免某个用户把入口流量瞬间打满
- RequestRateLimiter=redis-rate-limiter.replenishRate=20,redis-rate-limiter.burstCapacity=40,key-resolver=#{userKeyResolver}这条路由的含义可以直接按执行顺序理解:
- 外部请求命中
/api/orders/** - 网关把它转发到
lb://order-service - 转发前先去掉
/api这层前缀 - 补一个统一请求头给下游服务识别来源
- 如果下游持续失败,就走熔断降级地址
- 如果同一个用户的请求速率超阈值,就直接在网关层被限流
如果还想把用户标识、链路信息之类的统一透传给下游服务,工程上通常会补一个全局过滤器:
java
/**
* 网关全局过滤器,负责补充统一的用户和链路透传信息。
*/
@Component
public class TraceUserGlobalFilter implements GlobalFilter, Ordered {
/**
* 在请求转发给下游前补全 `traceId` 和 `userId` 请求头。
*
* @param exchange 当前请求上下文
* @param chain 网关过滤器链
* @return 继续执行后的响应链路
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 用户标识优先取上游已经解析好的请求头
String userId = exchange.getRequest().getHeaders().getFirst("X-User-Id");
// 如果调用方没带 traceId,就由网关统一补一个
String traceId = Optional.ofNullable(exchange.getRequest().getHeaders().getFirst("X-Trace-Id"))
.orElse(UUID.randomUUID().toString());
// 统一把链路信息和用户信息透传给下游服务
ServerHttpRequest mutatedRequest = exchange.getRequest().mutate()
.header("X-Trace-Id", traceId)
.header("X-User-Id", userId == null ? "anonymous" : userId)
.build();
return chain.filter(exchange.mutate().request(mutatedRequest).build());
}
/**
* 指定过滤器执行顺序,值越小越早执行。
*
* @return 当前过滤器的顺序值
*/
@Override
public int getOrder() {
return -100;
}
}执行结果可以这样理解:
- 路由规则统一决定“请求该去哪个服务”
- 过滤器统一决定“请求在转发前后要补什么治理动作”
- 下游订单服务不需要自己重复处理网关层已经做掉的公共逻辑
5. 为什么不能把所有治理逻辑都塞进业务服务
因为像鉴权、限流、黑白名单、统一路由这些能力,本质上都不是单个业务服务独有的问题。
如果每个服务都各自实现一遍,就会出现:
- 规则重复
- 行为不一致
- 维护成本很高
- 风险扩散到每个服务
所以更自然的思路是:入口层能统一治理的事情,尽量不要分散到所有服务里。
6. 什么是熔断
熔断 可以理解成 当系统发现某个下游服务持续失败或持续超时时,先主动切断对它的请求,避免故障继续放大。
它解决的是 下游已经明显异常时,不要让上游继续无意义地把流量砸过去。
这和电路里的保险丝思路有点像:
一旦故障持续,就先断开,避免全链路被拖坏。
7. 为什么熔断很重要
微服务调用链一长,故障传播速度会非常快。
例如:
- 网关调用订单服务
- 订单服务调用库存服务
- 库存服务调用商品服务
如果商品服务持续变慢,那么:
- 库存服务线程会被占住
- 订单服务调用也会被拖慢
- 网关请求堆积
- 最终整个系统都变慢
熔断就是在这个时候发挥作用:不要让一个局部故障,最终演变成全链路雪崩。
8. 熔断、降级、重试分别是什么关系
这几个词经常一起出现,所以要分开看。
8.1 熔断
重点是:发现下游持续失败后,先主动切断调用。
8.2 降级
重点是:当正常能力不可用时,给出一个更弱但可接受的兜底结果。
例如:
- 返回默认值
- 返回缓存值
- 暂时关闭非核心能力
8.3 重试
重点是:某次调用失败后,再试一次或几次。
但要特别注意:重试不是万能的。
如果下游已经过载,盲目重试反而可能把系统打得更重。
9. 什么是限流
限流 可以理解成 当请求量过大时,对进入系统或进入某个服务的流量做速率控制。
它解决的是 不要让流量无限涌入,把系统资源瞬间打穿。
限流的常见场景包括:
- 网关入口限流
- 热点接口限流
- 下游依赖保护
- 秒杀和营销活动保护
10. 为什么限流和熔断经常一起出现
因为它们都属于系统保护能力,但保护角度不同。
- 限流更偏“事前控制”
- 熔断更偏“事中止损”
把这两个动作拆开看:
- 限流:流量太多时,先卡入口
- 熔断:下游持续异常时,先切调用
两者结合,系统才更容易稳住。
11. 常见弹性治理思路怎么理解
在 Spring Cloud 语境里,常见弹性治理通常会围绕这些能力展开:
- 超时控制
- 重试
- 熔断
- 降级
- 限流
- 隔离
11.1 超时控制
它解决的是 一个请求不能无限等下游。
11.2 隔离
它解决的是 某条调用链或某类资源出问题时,不要拖垮整个系统。
11.3 降级
它解决的是 服务不稳定时,系统还能不能以更弱能力继续提供核心功能。
12. Resilience4j、Sentinel 这类能力应该怎么理解
这些名字经常会和 Spring Cloud 一起出现。
更稳妥的理解是:它们主要是在补弹性治理和系统保护能力。
也就是说,它们关注的不是“服务发现”,而是:
- 熔断
- 限流
- 降级
- 系统保护
一句话区分:
- 注册中心解决“服务在哪”
- 网关解决“流量怎么进”
- 弹性治理组件解决“故障和流量冲击怎么控”
12.1 熔断降级和限流是怎么配合的
上面的路由只是把 CircuitBreaker 和 RequestRateLimiter 挂上去了,真正运行时还需要补两个小部件:
java
/**
* 网关侧的限流相关配置。
*/
@Configuration
public class GatewayRateLimitConfig {
/**
* 生成限流维度的 Key。
*
* @return 优先按用户 ID,缺失时退化为按来源 IP 限流的 `KeyResolver`
*/
@Bean
public KeyResolver userKeyResolver() {
// 优先按用户维度限流;如果没有登录态,就退化成按来源 IP 限流
return exchange -> Mono.just(
Optional.ofNullable(exchange.getRequest().getHeaders().getFirst("X-User-Id"))
.orElse(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress())
);
}
}
/**
* 熔断打开后的降级返回控制器。
*/
@RestController
public class GatewayFallbackController {
/**
* 返回订单服务不可用时的统一降级结果。
*
* @return 标识当前请求已进入降级链路的响应体
*/
@GetMapping("/fallback/orders")
public Map<String, Object> orderFallback() {
// 熔断打开后,网关直接返回一个可预期的降级结果
return Map.of(
"code", 503,
"message", "订单服务暂时不可用,请稍后重试",
"degraded", true
);
}
}如果要把熔断器的阈值也落到配置里,通常还会这样配:
yaml
resilience4j:
circuitbreaker:
instances:
orderCircuitBreaker:
# 最近 20 次调用作为统计窗口
slidingWindowSize: 20
# 至少收集到 10 次调用后,才开始判断是否需要熔断
minimumNumberOfCalls: 10
# 失败率达到 50% 就打开熔断器
failureRateThreshold: 50
# 熔断打开 10 秒后,再尝试放少量流量探测服务是否恢复
waitDurationInOpenState: 10s把它们串起来看,请求链路通常是这样的:
- 正常情况下,请求通过网关并转发到订单服务
- 某个用户的请求速率超过阈值时,网关会优先在入口直接限流
- 即使通过了限流,如果订单服务持续超时或报错,熔断器会在失败率达到阈值后直接打开
- 熔断打开后,请求不再继续打到异常服务,而是快速返回
/fallback/orders的降级结果
这也是为什么工程上常说:
- 限流更偏入口保护
- 熔断更偏故障止损
- 降级更偏用户侧兜底体验
13. 工程上最常见的几个误区
13.1 误区一:有网关就等于系统入口治理已经做好
网关只是入口载体。
真正关键的是:
- 路由规则是否清晰
- 鉴权是否统一
- 限流是否合理
- 观测是否到位
13.2 误区二:加了重试就更稳定
不一定。
如果下游已经过载,重试很可能是在放大流量。
13.3 误区三:熔断就是返回报错
熔断更重要的意义在于“快速失败和保护系统”,而不是单纯给用户看一个错误。
13.4 误区四:限流只在秒杀场景才需要
不是。
只要存在热点接口、突发流量、下游承载有限的情况,限流都可能有必要。
14. 一句话总结
网关解决的是“流量统一接入和治理”,熔断解决的是“故障扩散怎么止损”,限流解决的是“流量过大时怎么保护系统”,它们一起构成了 Spring Cloud 微服务治理里最关键的一层系统保护能力。