Featured image of post Spring Cloud Gateway:微服务流量的\

Spring Cloud Gateway:微服务流量的"中央调度官"

Spring Cloud Gateway:微服务流量的"中央调度官"

Java Web 微服务系列 · 第 6 篇 · Spring Cloud Gateway 阅读时长:约 26 分钟 本文写于 2026 年 6 月

引子:一次失败的发布让我们重新认识网关

2024 年下半年,我所在的团队做了一次"看似无害"的微服务重构——把原本单体应用里的 4 个核心业务模块拆成独立的 Spring Boot 服务,部署到 K8s 集群。架构图画得漂亮,CI/CD 流水线拉通,灰度发布、回滚机制一应俱全。

上线第一周就出事了。

凌晨 3 点,监控告警群炸了:所有下游服务的连接数(connection count)瞬间打满。我们 4 个服务,每个 50 个 Pod,一共有 200 个 Pod 收到了 20 倍的正常流量。日志翻出来一看——有个调用方在重试机制里写错了,所有失败的请求都被无限重试,而且重试时没有回退到服务名,而是打到了具体 Pod IP。

更糟糕的是:我们在网关层只做了"路径转发",没有任何限流、熔断、重试隔离。结果 200 个 Pod 直接雪崩,全站不可用 23 分钟。

事后复盘,CTO 在会上问了一个让我记忆深刻的问题:

“我们的网关到底在做什么?”

我答不上来。

那天晚上我回家翻 Spring Cloud Gateway 官方文档,从头读到尾。我意识到我们之前对网关的理解是错的——网关不是"路径转发器",网关是"微服务的中央调度官"。它该做的事情至少包括:统一鉴权、流量限流、熔断降级、灰度发布、日志追踪、监控告警。

第二天开始动手把网关从"路径转发器"升级成"流量调度官"——限流、熔断、灰度、鉴权、日志、监控一应俱全。这次我把它整理成一篇博客,从技术选型到核心类,从常用模块到实战配置,一次讲透

下面是我踩过的所有坑。

那天之后我成了团队里的"网关布道师",每次新项目立项都先过网关设计。我意识到:网关不是"装上去就行了"的基础设施,是要持续投入运维的核心组件。规则、路由、限流、熔断,都要随业务演进动态调整——网关是活的系统,不是死的配置


一、技术选型:为什么是 Spring Cloud Gateway

1.1 答案(拿分版)

微服务网关的选型,核心是看四件事:性能、协议支持、生态集成、团队熟悉度。 在 Java 技术栈里,Spring Cloud Gateway 是当下最优解——它基于 Reactor Netty 实现响应式编程,性能是 Zuul 1.x 的 1.6 倍;深度集成 Spring Cloud 生态(Nacos / Sentinel / Sleuth / Config 都能无缝对接);Java 团队几乎零学习成本,会写 Spring Boot 就上手。

1.2 横向对比:六大网关候选

网关编程模型性能协议支持生态适用场景
Spring Cloud GatewayReactor(响应式)★★★★★HTTP/HTTPS/WebSocket/gRPCSpring 全家桶Java 微服务首选
Zuul 1.xServlet(阻塞)★★★HTTPNetflix OSS维护期,不推荐新项目
Zuul 2.xNetty(异步)★★★★HTTPNetflix OSS文档少,Spring 集成弱
Nginx + OpenRestyLua/事件循环★★★★★HTTP/TCP/UDP第三方模块边缘网关 / 静态资源
KongOpenResty + Lua★★★★★HTTP/gRPC插件生态多语言混合架构
APISIXOpenResty + Lua★★★★★HTTP/gRPC/物联网Apache 顶级云原生 / 多协议

结论:

  • 纯 Java 微服务 → Spring Cloud Gateway(生态、性能、团队成本最优)
  • 多语言混合(Java + Go + Python)→ Kong 或 APISIX(中立网关,插件丰富)
  • 边缘入口(SSL 卸载、静态资源、CDN 协同)→ Nginx / OpenResty
  • 遗留 Zuul 1.x → 升级到 Spring Cloud Gateway

1.3 Zuul → Gateway:Spring 为何换帅

2018 年 Netflix 宣布 Zuul 1.x 进入维护期,Spring Cloud 团队开始自研网关。核心原因有三点:

  1. 编程模型代际差:Zuul 1.x 基于 Servlet,每请求一线程,并发 1000 就要 1000 个线程,线程上下文切换开销巨大。Spring Cloud Gateway 基于 Reactor Netty,少量线程通过事件循环处理数万并发
  2. 生态集成:Zuul 是 Netflix OSS 的一员,和 Spring Cloud 其他组件(Nacos / Sentinel / Config)整合需要胶水代码。Spring Cloud Gateway 是 Spring 亲儿子,开箱即用
  3. 动态路由:Zuul 改路由要重启,Spring Cloud Gateway 支持运行时动态刷新(结合 Nacos)。

时间线:

1
2
3
4
5
6
7
2014  Zuul 1.x 诞生(Netflix OSS 标杆)
2018  Netflix 宣布 Zuul 1.x 维护,推荐 Resilience4j
2017  Spring Cloud Gateway 立项
2019  Spring Cloud Gateway 2.1 GA(生产可用)
2022  Spring Cloud Gateway 3.1(基于 Spring Boot 3.0,WebFlux + Reactor Netty)
2024  Spring Cloud Gateway 4.0(响应式 API 全面稳定)
2026  Spring Cloud Gateway 主流选择(Z

💡 原理:响应式 vs 阻塞模型

Zuul 1.x 基于 Servlet,每个请求独占一个线程。并发 1000 请求 = 1000 个线程,每个线程默认占 1MB 栈空间,1GB 内存被线程栈吃光

Spring Cloud Gateway 基于 Reactor Netty,少量线程(默认 = CPU 核数 × 2)通过事件循环处理数万并发连接。单线程可处理 10000+ 并发,内存占用降一个数量级

类比:阻塞模型 = 1 个服务员服务 1 桌客人;响应式 = 1 个服务员服务 N 桌客人(客人点完单他就去服务下一桌)。

1.4 Spring Cloud Gateway 的三大核心特性

  1. Route(路由):定义"什么请求 → 转发到哪"
  2. Predicate(断言):判断"这个请求匹不匹配这条路由"
  3. Filter(过滤器):在转发前后做"鉴权、限流、修改请求/响应"

三剑客的关系:Predicate 决定请求走哪条路由,Filter 决定请求在路由上做什么处理。

1.5 一个反直觉的事实:网关不是越复杂越好

很多团队第一次上 Gateway 容易"过度设计"——把所有鉴权、限流、灰度、监控全堆在 Gateway,结果 Gateway 成了新的单点瓶颈

正确分层

关注点例子
边缘网关(Nginx)SSL 卸载、CC 攻击、静态资源100 万 QPS 也能扛
业务网关(Spring Cloud Gateway)鉴权、限流、路由5 万 QPS 以内
业务服务业务逻辑1 万 QPS 以内
数据层读写分离、分库分表

经验法则:Gateway 只做"横向能力"(鉴权、限流、灰度),不做"业务逻辑"(订单处理、用户管理)。业务逻辑下沉到下游服务,Gateway 保持轻量。

📌 实践:什么时候用 Spring Cloud Gateway?

  • Java 微服务架构,服务数 ≥ 5 个
  • ✅ 需要 统一鉴权 / 限流 / 灰度
  • ✅ 需要 WebSocket / gRPC 协议透传
  • ✅ 团队熟悉 Spring 生态
  • ❌ 单一服务、单一入口 → 上 Nginx 就够了
  • ❌ 多语言架构 → 上 Kong / APISIX

二、核心类:把"三剑客"扒到底层

2.1 答案(拿分版)

Spring Cloud Gateway 的内核只有三个核心接口:RouteLocatorPredicateGatewayFilter 所有高级特性(动态路由、限流、熔断)都是在这三个接口上的实现。理解了这三个接口,就理解了 Gateway 80% 的设计思想。剩下的 20% 是 RouteDefinitionLocator(路由定义定位器),用于动态路由场景

2.2 三大核心接口

2.2.1 Route —— 路由定义

1
2
3
4
5
6
7
8
public class Route implements Ordered {
    private final String id;
    private final URI uri;                  // 目标地址
    private final int order;
    private final AsyncPredicate<ServerWebExchange> predicate;  // 断言
    private final List<GatewayFilter> filters;  // 过滤器
    private final Map<String, Object> metadata;
}

关键字段:

  • id:路由唯一标识
  • uri:目标地址(http:// 固定地址 / lb://service-name 服务发现 / ws:// WebSocket)
  • predicate:匹配条件(注意是 AsyncPredicate 不是 Predicate——支持异步匹配
  • filters:过滤器链

2.2.2 Predicate —— 断言匹配

Gateway 内置 11 个 Predicate 工厂PathRoutePredicateFactoryMethodRoutePredicateFactory 等),全部实现 RoutePredicateFactory<C> 接口。源码在 org.springframework.cloud.gateway.handler.predicate 包。

1
2
3
4
5
@FunctionalInterface
public interface RoutePredicateFactory<C> extends ShortcutConfigurable, Configurable<C> {
    Predicate<ServerWebExchange> apply(C config);
    default String name() { return ... }
}

谓词的本质:返回一个 Predicate<ServerWebExchange>,对每个请求做 test() 判断,true → 命中路由。

为什么有"工厂"模式?因为每个 Predicate 需要带配置参数(如 Path=/api/order/** 里的路径)。工厂模式让"配置类 + 应用逻辑"解耦。

2.2.3 GatewayFilter —— 过滤器

1
2
3
public interface GatewayFilter {
    Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
}

两种过滤器:

  • GatewayFilterFactory:路由级别的过滤器(写在 yml 的 filters 下,只对当前路由生效)
  • GlobalFilter:全局过滤器(实现 GlobalFilter 接口,对所有路由生效)

执行顺序:

1
2
请求进入 → GlobalFilter.order(升序) → GatewayFilter → 路由到下游
         ← GatewayFilter ← GlobalFilter.order(降序) ← 下游响应

chain.filter(exchange) 之前的代码 = pre-filter(请求处理前) chain.filter(exchange) 之后的代码 = post-filter(响应处理后)

2.3 路由定位器:RouteDefinitionLocator 全家桶

1
2
3
public interface RouteDefinitionLocator {
    Flux<RouteDefinition> getRouteDefinitions();
}

4 个核心实现(用于动态路由):

实现类作用场景
InMemoryRouteDefinitionRepository内存存储默认实现,重启丢路由
PropertiesRouteDefinitionLocator读取 application.yml静态路由
DiscoveryClientRouteDefinitionLocator从注册中心读取集成 Nacos / Eureka
NacosRouteDefinitionRepository自定义 写 Nacos 持久化生产推荐:动态路由

RouteDefinitionRoute 的转换RouteDefinitionRouteLocator 完成。这条转换链是动态路由的核心——只要能拉取到 RouteDefinition,Gateway 就能转成 Route 生效。

💡 原理:动态路由的关键

Spring Cloud Gateway 启动时通过 RouteDefinitionRouteLocatorRouteDefinition 转成 Route 对象。动态路由的本质是定时从外部存储(DB / Nacos / Redis)拉取 RouteDefinition 列表,刷新内存中的路由缓存。这就是为什么生产环境一定要用 NacosRouteDefinitionRepository 而不是 yml。

2.4 三个核心接口的协作流程

1
2
3
4
5
6
7
8
9
HTTP Request
[RoutePredicateHandlerMapping]  ← 用 Predicate 匹配 Route
[FilteringWebHandler]           ← 装载 GatewayFilter + GlobalFilter
[NettyRoutingFilter]            ← Reactor Netty 转发到下游
HTTP Response

关键设计:响应式全链路。从 Predicate 到 Filter 到转发,全程 Mono/Flux没有任何阻塞调用

2.5 高级接口:GatewayFilterFactory 的扩展点

AbstractGatewayFilterFactory<C> 是所有内置 Filter 的父类。自定义 Filter 只需要继承它

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Component
public class CustomHeaderGatewayFilterFactory
        extends AbstractGatewayFilterFactory<CustomHeaderGatewayFilterFactory.Config> {

    public CustomHeaderGatewayFilterFactory() {
        super(Config.class);
    }

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            // pre 逻辑
            ServerHttpRequest mutated = exchange.getRequest().mutate()
                .header(config.getName(), config.getValue())
                .build();
            return chain.filter(exchange.mutate().request(mutated).build());
        };
    }

    public static class Config {
        private String name;
        private String value;
        // getter/setter
    }
}

使用方式

1
2
3
4
5
filters:
  - name: CustomHeader
    args:
      name: X-Source
      value: my-gateway

🎯 避坑:自定义 Filter 时的 3 个易错点

  1. 不调用 chain.filter(exchange) → 请求卡住,超时
  2. 修改 ServerHttpRequest 后没 mutate() → 改了原对象(ServerHttpRequest 不可变)
  3. 在 Filter 里写阻塞代码 → 整个事件循环卡死

2.6 Route 的 order 字段:路由优先级

Route 还有一个常被忽略的字段 order,控制多条路由同时匹配时哪条优先

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
spring:
  cloud:
    gateway:
      routes:
        - id: order_route_v2_gray
          uri: lb://order-svc-v2
          order: 0           # 数字越小越优先
          predicates:
            - Path=/api/order/**
            - Header=X-Gray-Tag, beta
        - id: order_route_default
          uri: lb://order-svc-v1
          order: 100
          predicates:
            - Path=/api/order/**

执行顺序:v2_gray 优先匹配(order=0),命中灰度 Header 走 v2;其他走 v1(order=100)。

生产经验:灰度路由 order 设小,兜底路由 order 设大;两者差值至少 50,方便后续插入中间版本。

2.7 Predicate 短路优化

Gateway 的 Predicate 是短路求值——第一个不满足就不继续。比如:

1
2
3
4
predicates:
  - Path=/api/order/**       # 先匹配路径(便宜)
  - Method=GET,POST          # 再匹配方法(便宜)
  - Header=X-Token, \w+      # 最后匹配头(需要正则)

调优技巧:便宜的断言放前面,贵的(如正则)放后面。Path 是最快的(字符串比较),Header 需要正则解析,最慢。

💡 原理:Predicate 的执行顺序

Gateway 内部把 Route 的所有 Predicate 包装成 Predicate<ServerWebExchange> 链。命中顺序是按 yml 配置顺序短路求值——所以你写的第一个 Predicate 一定先执行。


三、常用模块:11 个 Predicate + 30+ Filter 一网打尽

3.1 答案(拿分版)

Gateway 的"常用模块"分四类:路由断言、过滤器工厂、全局过滤器、生态集成。 生产环境必装的是 Path + Method + Header 三个断言、StripPrefix + AddRequestHeader 两个过滤器、以及一个 GlobalFilter 做统一鉴权。掌握这 6 个配置项,就能覆盖 80% 的业务场景。剩下的 20% 来自 Sentinel 限流、Hystrix/Resilience4j 熔断、Prometheus 监控这三个生态集成

3.2 11 个内置 Predicate 工厂

Predicate用途示例
Path路径匹配Path=/api/order/**
MethodHTTP 方法Method=GET,POST
Header请求头Header=X-Token, \d+
Query查询参数Query=token, \w+
Host域名Host=**.example.com
CookieCookieCookie=session, \w+
After / Before / Between时间窗口After=2026-01-01T00:00:00+08:00
RemoteAddrIP 段RemoteAddr=192.168.1.0/24
Weight权重路由Weight=order-svc, 80
CloudFoundryRouteCF 平台(云平台专用)

Predicate 组合是 AND 关系——一条路由的所有 Predicate 都为 true,请求才命中。

示例:组合 Predicate

1
2
3
4
5
6
predicates:
  - Path=/api/order/**
  - Method=GET,POST
  - Header=X-Tenant, [a-zA-Z0-9_]+
  - After=2026-01-01T00:00:00+08:00
  - RemoteAddr=192.168.0.0/16

含义/api/order/** 的 GET/POST 请求,必须带 X-Tenant 头(字母数字下划线),从 2026-01-01 起,且来自内网 IP。

3.3 30+ 内置 GatewayFilter 工厂

按功能分 6 大类:

(1)路径处理

Filter作用示例
StripPrefix=1去掉路径前缀 1 段/api/order/list/list
PrefixPath=/api加前缀/user/list/api/user/list
SetPath=/order/{segment}重写路径/api/o/123/order/123

(2)请求头 / 响应头

Filter作用
AddRequestHeader=X-Source, gateway加请求头
AddResponseHeader=X-Trace-Id, ...加响应头
RemoveRequestHeader=Cookie删请求头

(3)参数处理

Filter作用
AddRequestParameter=source, web加查询参数
SetQueryString=foo=bar重写查询串

(4)重定向 / 状态码

Filter作用
RedirectTo=302, https://...重定向
SetStatus=401改状态码

(5)限流 / 熔断(需额外依赖)

Filter作用
RequestRateLimiterRedis 令牌桶限流
CircuitBreaker集成 Hystrix / Sentinel / Resilience4j

(6)其他

Filter作用
Retry=3重试 3 次
Hystrix熔断(已不推荐)
SaveSessionWebFlux Session
SecureHeaders安全响应头
PreserveHostHeader保留原始 Host
DedupeResponseHeader去重响应头

3.4 GlobalFilter:4 个最常用实现

GlobalFilter 对所有路由生效,用 @Component 注册即可。

GlobalFilter作用适用场景
自定义鉴权校验 JWT / Token所有需要登录的接口
统一日志记录 request/response全链路追踪
跨域处理CORS 配置前后端分离
限流Redis 令牌桶防止突发流量

🎯 避坑:GlobalFilter 注册顺序

@Order 数字越小,越早执行。鉴权 GlobalFilter 必须在限流/日志之前,否则会先限流再鉴权,导致未鉴权用户触发限流统计的脏数据。

推荐顺序

  1. SecurityFilter 鉴权(@Order(-100)
  2. RequestLogFilter 日志(@Order(-50)
  3. RateLimitFilter 限流(@Order(0)
  4. 业务 GlobalFilter

3.5 生态集成:从单网关到微服务流量平台

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
        ┌──────────────────────────────────────┐
        │  Spring Cloud Gateway                 │
        │  ┌──────────┐  ┌──────────────────┐   │
        │  │ 路由动态 │  │ 限流/熔断/降级   │   │
        │  │ Nacos    │  │ Sentinel / Hystrix│  │
        │  └──────────┘  └──────────────────┘   │
        │  ┌──────────┐  ┌──────────────────┐   │
        │  │ 服务发现 │  │ 链路追踪          │   │
        │  │ Nacos    │  │ Sleuth + Zipkin   │   │
        │  └──────────┘  └──────────────────┘   │
        │  ┌──────────┐  ┌──────────────────┐   │
        │  │ 配置中心 │  │ 监控告警          │   │
        │  │ Nacos    │  │ Actuator + Prom   │   │
        │  └──────────┘  └──────────────────┘   │
        └──────────────────────────────────────┘

生产推荐组合:

  • 服务发现:Nacos(性能优于 Eureka,自带配置中心)
  • 限流熔断:Sentinel(阿里出品,规则可动态推送,优于 Hystrix)
  • 链路追踪:Micrometer Tracing + Zipkin(Sleuth 已弃用)
  • 配置中心:Nacos(路由规则 / 限流规则 持久化)
  • 监控告警:Actuator + Prometheus + Grafana

📌 实践:为什么 Sentinel 替代 Hystrix?

Hystrix 2018 年宣布停更,官方推荐用 Resilience4j 或 Sentinel。Sentinel 的核心优势是规则可动态推送——网关限流阈值调整不用重启服务,Nacos 一推即生效。Hystrix 只能改配置重启。

此外 Sentinel 还有:

  • 流量染色:按用户/接口打标,灰度路由
  • 系统自适应限流:根据 CPU / Load 自动降级
  • 热点参数限流:识别高频参数(如秒杀商品 ID)单独限流

3.6 11 个 Predicate 详细使用场景

生产中最常用的 5 个断言(覆盖 90% 场景):

Predicate真实场景示例
Path路由分流/api/order/** → order-svc
Method读写分离Method=GET,POST(部分服务只接 GET)
Header灰度发布Header=X-Gray-Tag, beta
Query鉴权 token 透传Query=token, \w+
RemoteAddr内网/外网分流RemoteAddr=10.0.0.0/8(仅内网)

不常用但有特殊价值的 3 个断言

Predicate场景
After / Before限时活动(“双 11 凌晨 0 点开始”)
Cookie灰度按用户固定分组(带 cookie 不变)
WeightA/B 测试(按流量比例分流)

示例:限时活动 Predicate

1
2
3
4
predicates:
  - Path=/api/promo/**
  - After=2026-11-11T00:00:00+08:00   # 11.11 凌晨 0 点后生效
  - Before=2026-11-11T23:59:59+08:00  # 11.11 当天 24 点失效

3.7 GatewayFilter 工厂的"链式调用"模式

Gateway 的多个 Filter 按 yml 顺序组成链式调用重要:每个 Filter 的 chain.filter(exchange) 之前是 pre、之后是 post

1
2
3
4
5
6
7
filters:
  - StripPrefix=1                        # pre: 去掉路径前缀
  - AddRequestHeader=X-Trace, abc123     # pre: 加请求头
  - name: Retry
    args: { retries: 3 }                 # pre: 准备重试
  - name: CircuitBreaker                 # pre: 熔断器包装
    args: { name: orderCB }

执行流程

1
2
请求进入 → StripPrefix(pre) → AddRequestHeader(pre) → Retry(pre) → CircuitBreaker(pre) → 下游服务
        ← StripPrefix(post) ← AddRequestHeader(post) ← Retry(post) ← CircuitBreaker(post) ← 下游响应

调优原则:pre 阶段做"请求处理"(鉴权、加头、限流);post 阶段做"响应处理"(加响应头、统计耗时)。

🎯 避坑:Filter 顺序的 2 个常见错误

  1. StripPrefix 放在 AddRequestHeader 之后 → AddRequestHeader 引用了 strip 前的路径,调试时找不到头在哪加的
  2. Retry 放在 RequestRateLimiter 之前 → 限流计数时 Retry 重试的请求会算多次,限流阈值要相应调大

四、实战:从 yml 到 Java DSL,从单体 Filter 到灰度发布

4.1 答案(拿分版)

Gateway 的实战分四步:yml 写简单路由 → Java DSL 写复杂路由 → 自定义 GlobalFilter 做鉴权 → 加灰度发布 Header 走灰度。 掌握这 4 步,从 demo 到生产只用 2 小时核心是:路由配置用 yml 起步、业务逻辑用 GlobalFilter 注入、动态配置用 Nacos 持久化

4.2 第一步:yml 简单路由

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
spring:
  cloud:
    gateway:
      routes:
        - id: order_route
          uri: lb://order-svc
          predicates:
            - Path=/api/order/**
            - Method=GET,POST
          filters:
            - StripPrefix=2  # /api/order/list -> /list
            - AddRequestHeader=X-Source, gateway

含义/api/order/** 的 GET/POST 请求,去掉前 2 段路径(/api/order),转发到 order-svc 服务,并加 X-Source: gateway 请求头。

lb:// 前缀:表示走服务发现(LoadBalancer),从 Nacos 拉取 order-svc 的实例列表,做负载均衡。

4.3 第二步:Java DSL 编程式路由

复杂路由用 yml 写不直观,改用 Java DSL:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
    return builder.routes()
        .route("user_route", r -> r
            .path("/api/user/**")
            .and().method("GET")
            .and().header("X-Tenant", "[a-z]+")
            .filters(f -> f
                .stripPrefix(2)
                .addRequestHeader("X-Source", "gateway")
                .retry(3)
            )
            .uri("lb://user-svc"))
        .build();
}

Java DSL 的优势:可加判断逻辑(从 DB / 配置中心读路由参数),适合动态路由场景。

实战场景:根据租户 ID 决定转发到不同集群。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@Bean
public RouteLocator tenantRouteLocator(RouteLocatorBuilder builder,
                                       TenantConfigService tenantConfig) {
    return builder.routes()
        .route(r -> r
            .path("/api/**")
            .filters(f -> f
                .filter((exchange, chain) -> {
                    String tenant = exchange.getRequest().getHeaders()
                        .getFirst("X-Tenant");
                    String prefix = tenantConfig.getPrefix(tenant);
                    // 动态修改 uri
                    return chain.filter(exchange.mutate()
                        .request(exchange.getRequest().mutate()
                            .header("X-Cluster", prefix).build())
                        .build());
                }))
            .uri("lb://" + tenantConfig.getDefaultCluster()))
        .build();
}

4.4 第三步:自定义 GlobalFilter(登录校验示例)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Component
@Order(1)  // 数字越小越早执行
public class AuthGlobalFilter implements GlobalFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String path = request.getURI().getPath();

        // 1. 白名单直接放行
        if (path.startsWith("/api/public/")) {
            return chain.filter(exchange);
        }

        // 2. 校验 Token
        String token = request.getHeaders().getFirst("Authorization");
        if (token == null || !token.startsWith("Bearer ")) {
            ServerHttpResponse response = exchange.getResponse();
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.writeWith(Mono.just(
                response.bufferFactory().wrap("{\"code\":401,\"msg\":\"未登录\"}".getBytes())
            ));
        }

        // 3. 解析 Token,将 userId 透传到下游
        String userId = JwtUtil.parseUserId(token);
        ServerHttpRequest mutated = request.mutate()
            .header("X-User-Id", userId)
            .build();
        return chain.filter(exchange.mutate().request(mutated).build());
    }
}

关键点:

  • exchange.mutate() 修改后必须 chain.filter() 透传
  • 状态码/响应体在 GlobalFilter 里直接 writeWith 即可中断链路
  • @Order(1) 数字越小越早执行,鉴权必须最先

4.5 第四步:灰度发布(基于 Header 权重路由)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
spring:
  cloud:
    gateway:
      routes:
        - id: order_route_v1
          uri: lb://order-svc-v1
          predicates:
            - Path=/api/order/**
            - Weight=order-svc, 90
        - id: order_route_v2
          uri: lb://order-svc-v2
          predicates:
            - Path=/api/order/**
            - Weight=order-svc, 10

升级灰度:v1 占 90% 流量,v2 占 10%。逐步调高 v2 权重到 100%,灰度完成。

更精细的灰度:基于 Header 指定版本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
spring:
  cloud:
    gateway:
      routes:
        - id: order_route_gray
          uri: lb://order-svc-v2
          predicates:
            - Path=/api/order/**
            - Header=X-Gray-Tag, beta
        - id: order_route_default
          uri: lb://order-svc-v1
          predicates:
            - Path=/api/order/**

测试人员带 X-Gray-Tag: beta 头走 v2,普通用户走 v1

🛑 误区:灰度发布的 3 个常见错误

  1. 用 Cookie / Session 做灰度 → 用户关浏览器重新打开就走错版本
  2. 灰度路由不带版本号 → 紧急回滚时不知道切哪个
  3. 灰度期间不监控错误率 → 上线 5% 流量已经报错,迟迟没发现

正确做法:用 Header=X-Gray-Tag, beta 做灰度,灰度路由命名带 _gray 后缀,监控 5xx 错误率超过阈值自动回滚。

4.6 第五步:全局异常处理(ErrorWebExceptionHandler

Gateway 抛异常时(如路由找不到、限流拒绝),默认返回 500 + 空白页面。生产环境必须自定义错误响应

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Component
@Order(-1)
public class JsonErrorWebExceptionHandler implements ErrorWebExceptionHandler {

    @Override
    public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(HttpStatus.OK);  // 统一 200
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);

        Map<String, Object> body = new HashMap<>();
        body.put("code", 500);
        body.put("msg", ex.getMessage());
        body.put("path", exchange.getRequest().getPath().value());

        try {
            byte[] bytes = new ObjectMapper().writeValueAsBytes(body);
            return response.writeWith(Mono.just(response.bufferFactory().wrap(bytes)));
        } catch (Exception e) {
            return Mono.error(e);
        }
    }
}

效果:所有异常返回 JSON {code, msg, path},前端可解析。

4.7 自定义 Predicate 实战(按租户分流)

内置 Predicate 不够用时,自定义 Predicate 是终极武器。实现 RoutePredicateFactory<MyConfig> 接口

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Component
public class TenantRoutePredicateFactory
        extends AbstractRoutePredicateFactory<TenantRoutePredicateFactory.Config> {

    public TenantRoutePredicateFactory() {
        super(Config.class);
    }

    @Override
    public Predicate<ServerWebExchange> apply(Config config) {
        return exchange -> {
            String tenant = exchange.getRequest().getHeaders()
                .getFirst(config.getHeaderName());
            // 命中租户白名单
            return config.getAllowedTenants().contains(tenant);
        };
    }

    @Data
    public static class Config {
        private String headerName = "X-Tenant";
        private List<String> allowedTenants;
    }
}

使用方式(yml):

1
2
3
4
5
6
7
predicates:
  - name: Tenant
    args:
      headerName: X-Tenant
      allowedTenants:
        - tenant-a
        - tenant-b

实战场景:多租户 SaaS 平台,按租户 ID 决定请求走哪个集群。

4.8 网关压力测试(用 wrk)

生产前必跑压力测试wrk 是 Linux 高性能 HTTP 压测工具:

1
2
3
4
5
# 安装 wrk(Ubuntu)
sudo apt install wrk

# 压测网关:100 并发,持续 30 秒
wrk -t4 -c100 -d30s --latency http://gateway:8080/api/order/list

关键指标

指标含义健康阈值
Latency Distribution 50%中位数延迟< 50ms
Latency Distribution 99%99 分位延迟< 200ms
Requests/secQPS≥ 预期峰值 1.5 倍
Socket errors套接字错误0

生产经验:网关压测必须用真实下游服务 mock(如 wiremock),不能直接压空 Gateway——否则延迟全在下游。

📌 实践:网关压测 3 个关键点

  1. 冷启动要算——前 1000 个请求延迟偏高(Netty 启动、类加载),生产要预热
  2. 限流要测——故意打超限流阈值,看 Gateway 是否正确返回 429
  3. 熔断要测——下游 mock 故意 5xx,看 Gateway 是否触发熔断

阿里云 AHAS 提供了专业的网关压测服务(ahas-console),生产级项目推荐。


五、配置:从 CORS 到限流,从超时到监控

5.1 答案(拿分版)

Gateway 的"必配"配置有 5 项:CORS 跨域、连接超时、限流、熔断、监控。 漏配任何一项都可能引发生产事故——CORS 不配,前端调不通;超时不配,慢调用拖垮网关;限流不配,雪崩;熔断不配,单服务故障扩散全站;监控不配,出了事不知道。

5.2 CORS 跨域完整配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
spring:
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          '[/**]':
            allowedOriginPatterns: "*"
            allowedMethods: "*"
            allowedHeaders: "*"
            allowCredentials: true
            maxAge: 3600

🎯 避坑:allowCredentials: true + allowedOrigins: * 会报错

必须改成 allowedOriginPatterns: "*"(Spring Boot 2.4+ 才支持)。这是初学者最常踩的坑。

5.3 超时与重试

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
spring:
  cloud:
    gateway:
      httpclient:
        connect-timeout: 1000    # 连接超时 1s
        response-timeout: 5s     # 响应超时 5s
      routes:
        - id: order_route
          uri: lb://order-svc
          predicates:
            - Path=/api/order/**
          filters:
            - name: Retry
              args:
                retries: 3
                statuses: BAD_GATEWAY,SERVICE_UNAVAILABLE
                methods: GET,POST
                backoff:
                  firstBackoff: 50ms
                  maxBackoff: 500ms
                  factor: 2

生产经验:

  • connect-timeout: 1sresponse-timeout: 5s 是大多数场景的安全值
  • 重试只对幂等接口开(GET),POST 重试可能产生重复订单
  • 重试配合 backoff 指数退避,避免雪崩

5.4 限流(内置 Redis 令牌桶)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
spring:
  cloud:
    gateway:
      routes:
        - id: order_route
          uri: lb://order-svc
          predicates:
            - Path=/api/order/**
          filters:
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 100   # 每秒 100 个令牌
                redis-rate-limiter.burstCapacity: 200  # 桶容量 200
                redis-rate-limiter.requestedTokens: 1
                key-resolver: "#{@userKeyResolver}"    # SpEL 表达式

KeyResolver 实现(按用户限流):

1
2
3
4
5
6
@Bean
public KeyResolver userKeyResolver() {
    return exchange -> Mono.just(
        exchange.getRequest().getHeaders().getFirst("X-User-Id")
    );
}

📌 实践:限流算法选型

算法优点缺点适用
令牌桶平滑限流、允许突发配置稍复杂通用首选
漏桶强制匀速突发流量被截断严格流量整形
滑动窗口精确控制时间窗内存开销大短时间窗口限流

生产推荐令牌桶replenishRate=平均QPS, burstCapacity=峰值QPS)。

5.5 熔断(集成 Sentinel)

Sentinel 是阿里出品的流量治理组件,比 Hystrix 强在规则可动态推送

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
spring:
  cloud:
    sentinel:
      transport:
        dashboard: localhost:8080  # Sentinel 控制台
      scg:
        fallback:
          mode: response
          response-status: 429
          response-body: '{"code":429,"msg":"服务降级中"}'

Sentinel 控制台配置

  • 资源名:order-svc:/api/order/list
  • 熔断策略:慢调用比例(RT > 1s 的请求占比 > 50% 触发熔断)
  • 熔断时长:10s
  • 半开恢复:探测请求数 5

5.6 监控(Actuator + Prometheus)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
management:
  endpoints:
    web:
      exposure:
        include: health,info,gateway,prometheus
  metrics:
    tags:
      application: ${spring.application.name}
  prometheus:
    metrics:
      export:
        enabled: true

Gateway 关键指标:

指标含义
gateway.requests总请求数(带 routeId 标签)
gateway.route.predicate.matched路由命中数
http.client.requests下游 HTTP 调用(带 uri, status, outcome
jvm.memory.usedJVM 内存使用

Grafana 仪表盘推荐

  • Spring Cloud Gateway 官方仪表盘 ID: 11591
  • 自定义 4 个核心面板:QPS、P99 延迟、5xx 错误率、限流次数

5.7 动态路由(生产推荐)

痛点:yml 路由改一次,要重启网关。10 个路由的微服务集群,每次业务变更都要重启网关 = 不可接受

解决方案:自定义 RouteDefinitionRepository,从 Nacos 读取路由。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Component
public class NacosRouteDefinitionRepository implements RouteDefinitionRepository {
    @Autowired
    private NacosConfigService nacosConfigService;

    @Override
    public Flux<RouteDefinition> getRouteDefinitions() {
        // 从 Nacos 拉取路由配置
        List<RouteDefinition> routes = nacosConfigService.getConfig(
            "gateway-routes", "DEFAULT_GROUP", 5000
        );
        return Flux.fromIterable(routes);
    }
}

Nacos 控制台配置

  • Data ID: gateway-routes
  • Group: DEFAULT_GROUP
  • 配置内容:JSON 格式的 RouteDefinition 列表

改路由:在 Nacos 控制台改 JSON,网关 5 秒内自动刷新,无需重启

🛑 误区:动态路由的 3 个坑

  1. 不加缓存 → 每次请求都读 Nacos,性能崩盘。必须本地缓存 + 监听 Nacos 变更事件
  2. 路由变更不通知 → 改完 Nacos 没刷内存。必须监听 NacosConfigServiceaddListener
  3. 动态路由不校验 → 写错的 JSON 让 Gateway 启动失败。必须有合法 yml 兜底,动态路由加载失败时回退

5.8 完整生产配置模板

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
server:
  port: 8080
  netty:
    connection-timeout: 30s
spring:
  application:
    name: api-gateway
  cloud:
    nacos:
      discovery:
        server-addr: nacos:8848
    gateway:
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true
      globalcors:
        cors-configurations:
          '[/**]':
            allowedOriginPatterns: "*"
            allowedMethods: "*"
            allowedHeaders: "*"
            allowCredentials: true
      httpclient:
        connect-timeout: 1000
        response-timeout: 5s
management:
  endpoints:
    web:
      exposure:
        include: health,info,gateway,prometheus

5.9 网关安全配置清单

生产环境必配 6 项

配置作用推荐值
X-Frame-Options防点击劫持DENY
X-Content-Type-Options防 MIME 嗅探nosniff
Strict-Transport-Security强制 HTTPSmax-age=31536000
Content-Security-Policy防 XSSdefault-src 'self'
Referrer-Policy控制 Refererno-referrer
X-XSS-Protection浏览器 XSS 防护1; mode=block

Gateway 自带 SecureHeaders Filter默认开启这 6 个安全头。禁用方式(不推荐):

1
2
3
4
5
6
spring:
  cloud:
    gateway:
      filter:
        secure-headers:
          disable: "*"  # 禁用所有

5.10 网关高可用部署

单点网关是灾难——网关挂了,所有流量进不来。生产必做:网关至少 2 个实例 + 负载均衡 + 健康检查。

架构

1
2
3
4
5
6
7
8
9
        ┌──────────┐
        │   Nginx  │  ← VIP / Keepalived
        └──────────┘
   ┌────────┴────────┐
   │                 │
┌──┴──┐           ┌──┴──┐
│GW-1 │           │GW-2 │  ← 至少 2 个 Spring Cloud Gateway 实例
└─────┘           └─────┘

K8s 部署(推荐):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
apiVersion: apps/v1
kind: Deployment
metadata:
  name: gateway
spec:
  replicas: 2  # 至少 2 副本
  template:
    spec:
      containers:
        - name: gateway
          image: gateway:1.0
          ports:
            - containerPort: 8080
          readinessProbe:  # 就绪探针
            httpGet:
              path: /actuator/health
              port: 8080
            initialDelaySeconds: 30
            periodSeconds: 10

健康检查:Spring Cloud Gateway 默认带 /actuator/health,K8s 探针每 10s 查一次,不健康实例自动从 Service 摘除


总结:一张图 + 一句话

一张图:Gateway 在微服务架构中的位置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
   客户端(Web/App/小程序)
   ┌─────────────┐
   │   Nginx     │  ← SSL 卸载、静态资源、CC 攻击拦截
   └─────────────┘
   ┌─────────────────────────────┐
   │ Spring Cloud Gateway       │  ← 鉴权、限流、灰度、路由
   │ ┌────┐ ┌────┐ ┌────┐      │
   │ │Pred│→│Filt│→│Load│      │
   │ └────┘ └────┘ └────┘      │
   └─────────────────────────────┘
        ↓        ↓        ↓
   ┌────────┐ ┌────────┐ ┌────────┐
   │user-svc│ │order-  │ │product-│
   │        │ │svc     │ │svc     │
   └────────┘ └────────┘ └────────┘

一句话总结

Spring Cloud Gateway = “路由 + 断言 + 过滤器” 三个核心接口的精妙组合。用对这三个接口,再配上限流、熔断、监控,就能在 Java 微服务架构里撑起"流量中央调度官"的角色。

选型决策树

1
2
3
4
5
需要 Java 微服务网关吗?
├─ 是 → 团队熟悉 Spring? → Spring Cloud Gateway
│       └─ 团队熟悉 Lua?   → APISIX / Kong
├─ 边缘入口(SSL/CDN)     → Nginx / OpenResty
└─ 单服务单入口             → 上 Nginx 就够了

系列下一篇预告

下一篇会写 Nacos 配置中心 + 注册中心——Gateway 的动态路由、限流规则都依赖它。

速查表:5 大场景的核心配置

场景核心配置文件位置
统一鉴权GlobalFilter + JWTAuthGlobalFilter.java
限流RequestRateLimiter + Redisapplication.yml
熔断Sentinel 资源规则Sentinel 控制台
灰度发布Header=X-Gray-Tag + Weightapplication.yml
动态路由Nacos + 自定义 RouteDefinitionRepositoryNacos 控制台

生产清单:把这 5 件事都做了,网关才真的"中央调度官"——缺一项都是假网关。


参考资料


💡 延伸学习:响应式编程为什么快?

Spring Cloud Gateway 基于 Reactor Netty,本质是 NIO + 事件循环

  • 阻塞模型(Zuul 1.x):每请求一线程,线程数 = 并发数。1000 并发 = 1000 线程,每个线程默认占 1MB 栈空间 = 1GB 内存
  • 响应式模型(Reactor):少量线程(= CPU 核数 × 2)通过事件循环处理数万连接,单线程可处理 10000+ 并发
  • 类比:阻塞模型 = 1 个服务员服务 1 桌客人;响应式 = 1 个服务员服务 N 桌客人(客人点完单他就去服务下一桌)

缺点是调试复杂——堆栈不直观、阻塞代码会让整个事件循环卡死。生产中禁止在 Gateway 里写阻塞代码(JDBC / 同步 HTTP / Thread.sleep)。

本系列共 16 篇,本文为第 8 篇 · 查看全部
使用 Hugo 构建
主题 StackJimmy 设计