Skip to content

Spring Cloud:网关、熔断与限流

微服务系统拆开之后,真正难的往往不是“服务能不能调通”,而是:

  1. 外部流量怎么统一接入
  2. 下游服务慢了怎么办
  3. 某个服务挂了会不会把整条调用链拖死
  4. 流量过大时怎么保护系统

这篇文章就集中讲 3 条常见治理主线:

  1. 网关
  2. 熔断与降级
  3. 限流与系统保护

1. 为什么微服务一定会遇到统一入口问题

在单体应用里,外部请求通常直接进入一个应用就够了。

但在微服务架构里,系统往往会拆成:

  1. 用户服务
  2. 订单服务
  3. 库存服务
  4. 支付服务
  5. 营销服务

如果外部请求直接对接每个服务,会带来很多问题:

  1. 路由分散
  2. 鉴权分散
  3. 限流分散
  4. 日志和监控分散
  5. 对外暴露面过多

所以系统通常需要一个统一入口。


2. 什么是网关

网关 可以理解成 系统外部流量进入内部服务之前的统一入口层

它解决的是:

  1. 请求先到哪里
  2. 根据什么规则路由到哪个服务
  3. 哪些通用逻辑应该在入口统一做

例如常见的统一逻辑包括:

  1. 路由转发
  2. 身份认证
  3. 权限校验
  4. 限流
  5. 日志和链路信息透传

3. Spring Cloud Gateway 主要解决什么问题

Spring Cloud Gateway 是 Spring Cloud 体系里很常见的网关方案。

它主要解决的是 如何在 Spring 生态里统一做路由、过滤和流量治理

它更适合被理解成:一个面向微服务入口治理的网关框架。


4. 网关到底在做什么

一条请求经过网关时,常见流程通常是:

  1. 请求先进入网关
  2. 网关根据路由规则判断目标服务
  3. 执行一组过滤逻辑
  4. 再把请求转发到下游服务
  5. 拿到响应后再统一返回

这里最重要的两个关键词是:

  1. 路由
  2. 过滤

4.1 路由

路由解决的是:这条请求应该被转发到哪个服务。

4.2 过滤

过滤解决的是:请求在进出系统时,要统一做哪些前置和后置处理。

例如:

  1. 加 token 校验
  2. 打日志
  3. 加链路追踪头
  4. 统一异常返回

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}

这条路由的含义可以直接按执行顺序理解:

  1. 外部请求命中 /api/orders/**
  2. 网关把它转发到 lb://order-service
  3. 转发前先去掉 /api 这层前缀
  4. 补一个统一请求头给下游服务识别来源
  5. 如果下游持续失败,就走熔断降级地址
  6. 如果同一个用户的请求速率超阈值,就直接在网关层被限流

如果还想把用户标识、链路信息之类的统一透传给下游服务,工程上通常会补一个全局过滤器:

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;
    }
}

执行结果可以这样理解:

  1. 路由规则统一决定“请求该去哪个服务”
  2. 过滤器统一决定“请求在转发前后要补什么治理动作”
  3. 下游订单服务不需要自己重复处理网关层已经做掉的公共逻辑

5. 为什么不能把所有治理逻辑都塞进业务服务

因为像鉴权、限流、黑白名单、统一路由这些能力,本质上都不是单个业务服务独有的问题。

如果每个服务都各自实现一遍,就会出现:

  1. 规则重复
  2. 行为不一致
  3. 维护成本很高
  4. 风险扩散到每个服务

所以更自然的思路是:入口层能统一治理的事情,尽量不要分散到所有服务里。


6. 什么是熔断

熔断 可以理解成 当系统发现某个下游服务持续失败或持续超时时,先主动切断对它的请求,避免故障继续放大

它解决的是 下游已经明显异常时,不要让上游继续无意义地把流量砸过去

这和电路里的保险丝思路有点像:

一旦故障持续,就先断开,避免全链路被拖坏。


7. 为什么熔断很重要

微服务调用链一长,故障传播速度会非常快。

例如:

  1. 网关调用订单服务
  2. 订单服务调用库存服务
  3. 库存服务调用商品服务

如果商品服务持续变慢,那么:

  1. 库存服务线程会被占住
  2. 订单服务调用也会被拖慢
  3. 网关请求堆积
  4. 最终整个系统都变慢

熔断就是在这个时候发挥作用:不要让一个局部故障,最终演变成全链路雪崩。


8. 熔断、降级、重试分别是什么关系

这几个词经常一起出现,所以要分开看。

8.1 熔断

重点是:发现下游持续失败后,先主动切断调用。

8.2 降级

重点是:当正常能力不可用时,给出一个更弱但可接受的兜底结果。

例如:

  1. 返回默认值
  2. 返回缓存值
  3. 暂时关闭非核心能力

8.3 重试

重点是:某次调用失败后,再试一次或几次。

但要特别注意:重试不是万能的。

如果下游已经过载,盲目重试反而可能把系统打得更重。


9. 什么是限流

限流 可以理解成 当请求量过大时,对进入系统或进入某个服务的流量做速率控制

它解决的是 不要让流量无限涌入,把系统资源瞬间打穿

限流的常见场景包括:

  1. 网关入口限流
  2. 热点接口限流
  3. 下游依赖保护
  4. 秒杀和营销活动保护

10. 为什么限流和熔断经常一起出现

因为它们都属于系统保护能力,但保护角度不同。

  1. 限流更偏“事前控制”
  2. 熔断更偏“事中止损”

把这两个动作拆开看:

  1. 限流:流量太多时,先卡入口
  2. 熔断:下游持续异常时,先切调用

两者结合,系统才更容易稳住。


11. 常见弹性治理思路怎么理解

在 Spring Cloud 语境里,常见弹性治理通常会围绕这些能力展开:

  1. 超时控制
  2. 重试
  3. 熔断
  4. 降级
  5. 限流
  6. 隔离

11.1 超时控制

它解决的是 一个请求不能无限等下游

11.2 隔离

它解决的是 某条调用链或某类资源出问题时,不要拖垮整个系统

11.3 降级

它解决的是 服务不稳定时,系统还能不能以更弱能力继续提供核心功能


12. Resilience4jSentinel 这类能力应该怎么理解

这些名字经常会和 Spring Cloud 一起出现。

更稳妥的理解是:它们主要是在补弹性治理和系统保护能力。

也就是说,它们关注的不是“服务发现”,而是:

  1. 熔断
  2. 限流
  3. 降级
  4. 系统保护

一句话区分:

  1. 注册中心解决“服务在哪”
  2. 网关解决“流量怎么进”
  3. 弹性治理组件解决“故障和流量冲击怎么控”

12.1 熔断降级和限流是怎么配合的

上面的路由只是把 CircuitBreakerRequestRateLimiter 挂上去了,真正运行时还需要补两个小部件:

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

把它们串起来看,请求链路通常是这样的:

  1. 正常情况下,请求通过网关并转发到订单服务
  2. 某个用户的请求速率超过阈值时,网关会优先在入口直接限流
  3. 即使通过了限流,如果订单服务持续超时或报错,熔断器会在失败率达到阈值后直接打开
  4. 熔断打开后,请求不再继续打到异常服务,而是快速返回 /fallback/orders 的降级结果

这也是为什么工程上常说:

  1. 限流更偏入口保护
  2. 熔断更偏故障止损
  3. 降级更偏用户侧兜底体验

13. 工程上最常见的几个误区

13.1 误区一:有网关就等于系统入口治理已经做好

网关只是入口载体。

真正关键的是:

  1. 路由规则是否清晰
  2. 鉴权是否统一
  3. 限流是否合理
  4. 观测是否到位

13.2 误区二:加了重试就更稳定

不一定。

如果下游已经过载,重试很可能是在放大流量。

13.3 误区三:熔断就是返回报错

熔断更重要的意义在于“快速失败和保护系统”,而不是单纯给用户看一个错误。

13.4 误区四:限流只在秒杀场景才需要

不是。

只要存在热点接口、突发流量、下游承载有限的情况,限流都可能有必要。


14. 一句话总结

网关解决的是“流量统一接入和治理”,熔断解决的是“故障扩散怎么止损”,限流解决的是“流量过大时怎么保护系统”,它们一起构成了 Spring Cloud 微服务治理里最关键的一层系统保护能力。

基于 VitePress 构建的个人技术笔记。