Spring Cloud Gateway:微服务网关的路由、过滤、限流与容错实践

在微服务架构中,API Gateway 不只是一个请求转发器,而是外部流量进入内部服务体系的统一边界。Spring Cloud Gateway 将路由匹配、服务发现、负载均衡、过滤器链、鉴权、跨域、限流、超时、重试、熔断和监控等能力集中到网关层。本文从 Gateway 的核心模型和请求处理链路出发,结合 Nacos、Redis 与 Resilience4J 给出一套面向 Spring Boot 3 / Spring Cloud Gateway 4.x 的完整实践,并说明 Gateway 5.x 下需要关注的版本变化。

为什么微服务架构需要网关

在单体应用中,系统通常只有一个对外入口:

1
2
3
4
Client
|
v
Application

当系统拆成多个微服务以后,情况开始发生变化:

1
2
3
4
5
auth-service    : 8081
order-service : 8082
file-service : 8083
user-service : 8084
payment-service : 8085

如果客户端直接访问每一个微服务,就会出现很多问题。

客户端必须知道所有服务地址;服务拆分、合并或迁移时客户端也要跟着修改;每个服务都需要重复处理认证、跨域、访问控制、日志等公共逻辑;内部微服务直接暴露到公网还会扩大攻击面。

API Gateway 的价值就在于把这些分散的入口收敛为一个统一入口:

1
https://api.example.com

所有外部请求首先进入 Gateway,再根据请求路径、Host、Header、Method 等信息路由到真正的服务。鉴权、限流、日志和故障保护等横切逻辑也可以在这里统一完成。Spring Cloud Gateway 官方将自身定位为 Spring 生态中的 API Gateway,用于完成 API 路由,并处理安全、监控指标和弹性治理等横切关注点。

一个比较典型的微服务入口架构如下:

flowchart LR
    C["Client / Browser / APP"]
    E["CDN / WAF / Nginx / Load Balancer"]
    G["Spring Cloud Gateway"]

    A["auth-service"]
    U["user-service"]
    O["order-service"]
    F["file-service"]

    N["Nacos<br/>服务发现"]
    R["Redis<br/>限流 / 会话辅助"]

    C --> E
    E --> G

    G --> A
    G --> U
    G --> O
    G --> F

    N -. 服务实例 .-> G
    R -. 状态与令牌桶 .-> G

这里需要区分两个容易混淆的概念:流量网关和业务/API 网关并不一定互相替代。

能力 Nginx / LB / WAF Spring Cloud Gateway
主要位置 网络入口、边缘层 微服务应用入口
静态资源 很适合 通常不是主要用途
TLS 终止 很适合 可以,但通常放在前层
四层/七层负载均衡 主要关注应用服务路由
服务发现 通常需要额外方案 可直接接入 Spring DiscoveryClient
API 路由 可以 核心能力
Java 业务规则 不适合复杂逻辑 很适合
鉴权 基础能力 可与 Spring Security 等深度集成
限流 支持 支持按用户、路径等业务维度限流
熔断/重试 能力因产品而异 与 Spring Cloud 体系整合
请求/响应改写 支持 GatewayFilter 体系
业务上下文 较弱 较强

因此生产环境中经常不是:

1
Nginx OR Gateway

而是:

1
2
3
4
5
6
7
8
9
Internet
|
CDN / WAF
|
Nginx / Cloud Load Balancer
|
Spring Cloud Gateway
|
Microservices

Nginx 负责更靠近网络边缘的 TLS、静态资源、连接管理和负载均衡,Gateway 则负责更贴近微服务业务语义的路由、鉴权、服务发现、限流和故障治理。

先说版本:不要把旧教程直接复制到新项目

Spring Cloud Gateway 从早期 Spring Cloud 2.x 到今天已经经历了明显变化。

早期 Gateway 教程大量基于 Spring Cloud 2.1.x.RELEASE,运行于 Spring Boot 2、Spring Framework 5 和早期 Project Reactor 体系。很多核心思想今天依然成立,例如 Route、Predicate、Filter 和 pre/post Filter Chain,但是部分依赖、配置和组件已经发生变化。最典型的就是过去经常看到的 Hystrix 熔断方案。

截至 2026 年 8 月,Spring 官方文档列出的稳定版本包括 Gateway 5.0.24.3.54.2.74.1.9。其中 Gateway 4.3.5 属于 Spring Framework 6 / Spring Boot 3 技术栈,而 Gateway 5.0.2 已进入 Spring Framework 7 / Spring Boot 4 技术栈。

本文为了兼顾当前大量 Spring Boot 3 微服务项目,主要采用:

1
2
3
4
5
6
7
Spring Boot 3
Spring Cloud Gateway 4.3.x
Spring WebFlux
Nacos
Spring Cloud LoadBalancer
Redis
Spring Cloud CircuitBreaker + Resilience4J

进行讲解。

如果项目已经升级到 Spring Boot 4 / Gateway 5.x,核心模型没有推倒重来,但一定要按照对应版本官方文档核对 starter、属性路径和 API。

还有一个需要特别修正的老教程写法。

过去经常使用:

1
<artifactId>spring-cloud-starter-gateway</artifactId>

当前 Gateway 4.3.5 和 5.0.2 官方文档针对 Server WebFlux 版本推荐的 starter 已经是:

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway-server-webflux</artifactId>
</dependency>

Server WebFlux Gateway 基于 Spring WebFlux、Project Reactor 和 Netty,不是传统 Servlet/Tomcat 模式,也不应该再按照普通 Spring MVC 项目的思路处理请求线程。

这也是为什么 Gateway 项目中不要随手塞入:

1
spring-boot-starter-web

尤其不要因为某个公共模块间接引入 MVC,就把 Gateway 变成一个 WebMVC、WebFlux 混杂的项目。

Spring Cloud Gateway 的三个核心概念

理解 Gateway,最重要的不是记住几十个配置项,而是先建立三个核心模型:

1
2
3
Route
Predicate
Filter

它们基本描述了 Gateway 的全部路由逻辑。

可以简单理解成:

1
Route = Predicate + URI + Filters + Metadata

即:

什么请求需要匹配,匹配后发到哪里,转发前后还需要做什么处理。

Route:一条完整的路由规则

Route(路由)代表一条完整的转发规则。

例如:

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: order-route
uri: lb://order-service
predicates:
- Path=/api/orders/**
filters:
- StripPrefix=1

这里表示:

1
2
3
4
5
6
7
8
9
10
11
请求:
/api/orders/123

满足:
Path=/api/orders/**

目标:
lb://order-service

过滤:
StripPrefix=1

id 是路由唯一标识。

uri 是目标地址。

predicates 决定请求是否匹配。

filters 决定匹配后如何修改请求或响应。

Gateway 同时支持简写方式和完整参数方式配置 Predicate、Filter。

Predicate:这个请求是不是我的

Predicate(路由断言)本质上是一个条件判断。

例如:

1
2
predicates:
- Path=/api/orders/**

意思是:

1
2
如果 path 满足 /api/orders/**
那么命中这条 Route

除了 Path,还可以根据很多请求属性进行判断,例如:

1
2
3
4
5
6
7
8
Path
Host
Header
Cookie
Method
Query
RemoteAddr
时间

一个请求可以配置多个 Predicate。

例如:

1
2
3
4
predicates:
- Path=/api/admin/**
- Method=GET
- Header=X-Client-Type,internal

可以理解成:

1
2
3
Path 匹配
AND Method = GET
AND Header 匹配

多个 Predicate 共同决定请求是否属于这条 Route。

Filter:匹配之后做什么

Filter 是 Gateway 真正扩展能力最强的部分。

例如:

1
2
filters:
- StripPrefix=1

可以修改请求 Path。

除此之外还可以:

1
2
3
4
5
6
7
8
9
10
11
12
添加 Header
删除 Header
修改 Header
重写 Path
修改请求参数
修改响应
限流
熔断
重试
Token Relay
请求体处理
响应体处理

Spring Cloud Gateway 4.3.5 自带大量 GatewayFilter Factory,包括 CircuitBreakerRequestRateLimiterRewritePathStripPrefixRetryTokenRelay 等。

因此 Gateway 的本质可以浓缩为:

1
2
3
4
5
6
7
8
Predicate 决定:
这个请求走哪条 Route?

Filter 决定:
走这条 Route 时需要做什么?

URI 决定:
最终发到哪里?

Gateway 一次请求到底经历了什么

理解 Filter 之前,需要先理解整个 Gateway 请求生命周期。

官方描述的核心流程是:

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
Client
|
v
Gateway Handler Mapping
|
| 查找匹配 Route
v
Gateway Web Handler
|
v
Filter Chain
|
| pre filters
v
Proxy Request
|
v
Downstream Service
|
v
Filter Chain
|
| post filters
v
Client

Gateway Handler Mapping 首先判断请求是否能够匹配某个 Route。匹配之后交给 Gateway Web Handler,后者构建对应的过滤器链。过滤器既可以在代理请求发出之前执行,也可以在下游响应返回之后执行。

用时序图表示更加直观:

sequenceDiagram
    participant C as Client
    participant HM as Gateway Handler Mapping
    participant GW as Gateway Web Handler
    participant F as Filter Chain
    participant LB as LoadBalancer
    participant S as Downstream Service

    C->>HM: HTTP Request
    HM->>HM: 匹配 Route / Predicate
    HM->>GW: Route
    GW->>F: 构建过滤器链
    F->>F: Pre Filters
    F->>LB: 处理 lb://service-name
    LB->>S: Proxy Request
    S-->>LB: HTTP Response
    LB-->>F: Response
    F->>F: Post Filters
    F-->>GW: Response
    GW-->>C: HTTP Response

这一点非常重要,因为很多网关功能其实都只是 Filter 的不同形态:

1
2
3
4
5
6
7
鉴权        -> pre
限流 -> pre
Header 注入 -> pre
URL 改写 -> pre
访问日志 -> pre + post
响应统计 -> post
响应 Header -> post

GatewayFilter 与 GlobalFilter

Spring Cloud Gateway 中经常遇到两个过滤器概念:

1
2
GatewayFilter
GlobalFilter

GatewayFilter

GatewayFilter 通常跟某一条 Route 绑定。

例如:

1
2
3
4
5
6
7
routes:
- id: order-route
uri: lb://order-service
predicates:
- Path=/api/orders/**
filters:
- StripPrefix=1

这里的 StripPrefix 只影响 order-route

适合处理:

1
2
3
4
5
某服务独有的 URL Rewrite
某接口限流
某服务重试
某服务熔断
某条 Route Header 修改

GlobalFilter

GlobalFilter 会参与所有匹配路由的过滤器链,因此非常适合处理:

1
2
3
4
5
6
7
统一鉴权
TraceId
访问日志
租户信息
统一安全 Header
黑白名单
统一请求检查

当请求成功匹配 Route 后,Gateway 会把 GlobalFilter 和当前 Route 对应的 GatewayFilter 合并,再根据 Ordered 排序。高优先级 Filter 在 pre 阶段更早执行,在 post 阶段反而更晚退出,因此整个执行结构实际上类似一个嵌套调用栈。

假设:

1
2
3
Filter A order = -100
Filter B order = 0
Filter C order = 100

大致执行过程是:

1
2
3
4
5
6
7
A pre
B pre
C pre
downstream
C post
B post
A post

所以 Filter 顺序不能随便写。

例如比较合理的顺序可能是:

1
2
3
4
5
Trace Filter       -200
Security Filter -100
Tenant Filter -90
Logging Filter -50
业务 Filter 0

当然,具体 order 应根据项目已有 Filter 与 Gateway 内部 Filter 一起设计,而不是看到“数字越小越早”就一路写成 Integer.MIN_VALUE

Gateway 项目依赖

以 Spring Boot 3 + Gateway 4.3.x Server WebFlux 为例,可以准备以下核心依赖。

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
32
33
34
35
36
37
38
39
<dependencies>

<!-- Spring Cloud Gateway Server WebFlux -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway-server-webflux</artifactId>
</dependency>

<!-- Spring Cloud LoadBalancer -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

<!-- Nacos 服务发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

<!-- Redis Reactive:RequestRateLimiter 需要 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>

<!-- CircuitBreaker + Resilience4J -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
</dependency>

<!-- 监控 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

</dependencies>

具体版本应交给对应 Spring Boot、Spring Cloud 和 Spring Cloud Alibaba BOM 管理,不建议分别给每一个 Spring Cloud 组件手工指定版本。

特别注意:

1
2
3
4
Spring Boot
Spring Cloud
Spring Cloud Alibaba
Nacos

是存在版本兼容关系的。

不要采用这种拼装式升级:

1
2
3
4
Boot 升最新
Cloud 随便找一个
Alibaba 再随便找一个
Nacos Server 也升最新

然后期待 Maven 用爱发电。

Nacos + Gateway 的基本架构

采用 Nacos 后,服务启动时向注册中心注册自己的实例:

1
2
3
4
order-service
├── 10.0.0.11:8080
├── 10.0.0.12:8080
└── 10.0.0.13:8080

Gateway 不再需要把目标地址写死:

1
uri: http://10.0.0.11:8080

而是:

1
uri: lb://order-service

lb:// 表示该目标通过 Spring Cloud LoadBalancer 解析,Gateway 根据服务名获取服务实例,然后选择一个实例完成请求转发。DiscoveryClient Route Locator 也可以根据注册中心中的服务自动创建 lb://service-name 路由。

架构关系如下:

flowchart TD
    O1["order-service<br/>10.0.0.11:8080"]
    O2["order-service<br/>10.0.0.12:8080"]
    O3["order-service<br/>10.0.0.13:8080"]

    N["Nacos"]

    G1["Gateway-1"]
    G2["Gateway-2"]

    C["Client"]
    LB["Nginx / Load Balancer"]

    O1 --> N
    O2 --> N
    O3 --> N

    N -. ServiceInstance .-> G1
    N -. ServiceInstance .-> G2

    C --> LB
    LB --> G1
    LB --> G2

    G1 --> O1
    G1 --> O2
    G1 --> O3

    G2 --> O1
    G2 --> O2
    G2 --> O3

配置一套路由

假设系统中存在:

1
2
3
auth-service
file-service
main-service

可以配置:

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
server:
port: 1000

spring:
application:
name: gateway

cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848

gateway:
routes:
- id: auth-route
uri: lb://auth-service
predicates:
- Path=/a/**

- id: file-route
uri: lb://file-service
predicates:
- Path=/f/**

- id: main-route
uri: lb://main-service
predicates:
- Path=/m/**

这样客户端不再需要访问:

1
2
3
http://127.0.0.1:8081/a/login
http://127.0.0.1:8082/f/upload
http://127.0.0.1:8083/m/users

而可以统一访问:

1
2
3
http://gateway:1000/a/login
http://gateway:1000/f/upload
http://gateway:1000/m/users

这就是网关最基本的统一入口与路由功能。类似的 Nacos + lb://service-name 路由方式也是实际微服务项目中最常见的 Gateway 组合之一。

是否应该开启 Discovery Locator

还可以配置:

1
2
3
4
5
6
spring:
cloud:
gateway:
discovery:
locator:
enabled: true

开启以后 Gateway 可以根据注册中心中的服务自动创建路由。

默认情况下,通过 DiscoveryClient 创建的路由采用:

1
/serviceId/**

这样的 Path,并将 serviceId 从真正发送给下游的 Path 中剥离。目标 URI 使用:

1
lb://service-name

官方同时说明,这种模式需要 Spring Cloud LoadBalancer。

例如注册中心存在:

1
order-service

可能形成类似:

1
2
3
4
5
6
7
/order-service/orders/1
|
v
order-service
|
v
/orders/1

它很方便,但生产环境需要慎重。

如果注册中心中存在:

1
2
3
4
payment-internal
admin-center
job-service
config-service

而你无条件把所有服务都自动暴露为网关路由,那么原本只应该在内部网络中访问的服务也可能拥有外部入口。

因此生产环境更推荐:

注册中心负责发现服务,Route 负责显式定义哪些 API 可以被外部访问。

也就是:

1
uri: lb://order-service

照样使用服务发现,但 Route 白名单由 Gateway 配置控制。

Path 路由与路径改写

实际系统通常不希望把后端服务真实路径直接暴露出去。

外部可能希望:

1
/api/order/123

后端 Controller 却是:

1
/order/123

可以使用:

1
2
filters:
- StripPrefix=1

假设:

1
/api/order/123

执行一次 StripPrefix 后会变成:

1
/order/123

也可以使用 RewritePath 完成更加灵活的路径转换。

例如:

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: order-route
uri: lb://order-service
predicates:
- Path=/api/order/**
filters:
- RewritePath=/api/order/?(?<segment>.*), /order/${segment}

这样:

1
/api/order/10001

会转成:

1
/order/10001

网关对外 API 路径与内部服务 Controller 路径不必完全一致。

这是 Gateway 很重要的价值之一:内部服务可以重构,而外部 API Contract 尽量保持稳定。

跨域为什么适合放在 Gateway

对于前后端分离系统,如果所有浏览器 API 都经过 Gateway,那么 CORS(Cross-Origin Resource Sharing)策略也适合在 Gateway 统一处理。

Gateway 支持全局 CORS,也支持针对 Route 单独定义 CORS。官方还提供 add-to-simple-url-handler-mapping,用来处理 OPTIONS 预检请求没有命中某些 Route Predicate 的情况。

例如:

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
spring:
cloud:
gateway:
globalcors:
add-to-simple-url-handler-mapping: true

cors-configurations:
'[/**]':
allowedOriginPatterns:
- "https://*.example.com"

allowedMethods:
- GET
- POST
- PUT
- PATCH
- DELETE
- OPTIONS

allowedHeaders:
- "*"

exposedHeaders:
- Authorization
- X-Request-Id

allowCredentials: true

maxAge: 3600

开发阶段可能经常看到:

1
allowedOriginPatterns: "*"

但生产环境不建议无脑放开全部 Origin,尤其是允许 Cookie 或其他 Credential 的系统。

更合理的是:

1
2
3
https://www.example.com
https://admin.example.com
https://*.example.com

并明确控制:

1
2
3
4
5
allowedOrigins / allowedOriginPatterns
allowedMethods
allowedHeaders
exposedHeaders
allowCredentials

还要记住:

CORS 是浏览器的跨域访问机制,不是系统的身份认证机制。

允许某个 Origin 请求 API,不代表这个 Origin 拥有对应业务权限。

统一鉴权应该如何做

网关是统一入口,所以非常适合承担第一层身份校验。

一种最简单的模型是:

1
2
3
4
5
6
7
8
9
10
11
Client
|
Authorization: Bearer xxx
|
Gateway
|
校验 Token
|
解析 userId / tenantId / role
|
Downstream Service

可以通过 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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@Component
public class AuthenticationFilter implements GlobalFilter, Ordered {

private final PathMatcher pathMatcher = new AntPathMatcher();

private static final List<String> WHITE_LIST = List.of(
"/auth/login",
"/auth/register",
"/actuator/health"
);

@Override
public Mono<Void> filter(ServerWebExchange exchange,
GatewayFilterChain chain) {

String path = exchange.getRequest()
.getURI()
.getPath();

boolean ignored = WHITE_LIST.stream()
.anyMatch(pattern -> pathMatcher.match(pattern, path));

if (ignored) {
return chain.filter(exchange);
}

String authorization = exchange.getRequest()
.getHeaders()
.getFirst(HttpHeaders.AUTHORIZATION);

if (authorization == null ||
!authorization.startsWith("Bearer ")) {

exchange.getResponse()
.setStatusCode(HttpStatus.UNAUTHORIZED);

return exchange.getResponse().setComplete();
}

// 在这里接入项目实际的 Token / JWT / OAuth2 校验逻辑。
// 校验通过后继续执行 Filter Chain。

return chain.filter(exchange);
}

@Override
public int getOrder() {
return -100;
}
}

这段代码展示的是 Filter 结构,而不是完整安全方案。

一些早期工程示例会采用:

1
2
3
4
5
6
userId + token
|
v
Redis
|
比较 token 是否一致

这种方法可以用于理解网关统一鉴权的基本思路,但企业系统最好直接接入成熟认证体系,比如:

1
2
3
4
5
OAuth 2.0
OpenID Connect
JWT
Spring Security Resource Server
统一身份中心

而不是长期维护自己手写的 Token 协议。早期实践资料中也明确说明,Redis Token 对比只是为了演示 Gateway 过滤器能力。

401 和 403 不要混用

认证失败:

1
401 Unauthorized

表示:

1
2
我不知道你是谁
或者你的身份凭证无效

授权失败:

1
403 Forbidden

表示:

1
2
我知道你是谁
但你没有权限执行这个操作

不要为了统一错误响应,把鉴权失败全部返回:

1
500 Internal Server Error

500 应该表示服务器内部发生了非预期故障,而不是“用户没有登录”。

不要信任客户端自己传来的身份 Header

假设内部服务约定:

1
2
3
X-User-Id
X-Tenant-Id
X-Role-Id

Gateway 解析 Token 后写入这些 Header。

那么 Gateway 接收到外部请求时应该先删除客户端伪造的同名 Header,再写入经过认证得到的可信值。

逻辑应该是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
外部:

X-User-Id: admin

|
v

Gateway 删除

|
Token -> userId=10086
|
Gateway 写入

X-User-Id: 10086

|
v

内部服务

否则攻击者可以直接:

1
2
X-User-Id: 1
X-Role: ADMIN

然后愉快地给自己升职。

白名单应该精确匹配

登录、验证码、健康检查等接口通常需要跳过登录校验。

可以把它们配置成:

1
2
3
4
5
6
7
gateway:
security:
permit-paths:
- /auth/login
- /auth/register
- /auth/sms/**
- /actuator/health

但是不要简单写:

1
startsWith("/auth")

因为:

1
/auth/login

和:

1
/auth-admin/delete-user

可能被错误地放到同一类。

应使用明确的 Path Pattern 语义,并且把:

1
2
3
4
认证白名单
权限白名单
运维白名单
内部接口

分开管理。

Gateway 限流到底在限制什么

限流不是简单的:

1
每秒最多 100 个请求

真正需要先决定的是:

1
限制谁?

可能是:

1
2
3
4
5
6
7
8
9
整个 Gateway
某 Route
某 API
某 IP
某用户
某租户
某 AppKey
某 API Key
某业务操作

例如:

1
2
3
4
5
6
7
8
9
10
11
/login
每 IP 5 次/分钟

/search
每用户 20 次/秒

/export
每租户 2 个并发任务

/open-api
每 AppKey 100 QPS

这才是业务网关与简单网络限流之间比较明显的区别。

漏桶与令牌桶

限流领域经常遇到两个算法:

1
2
Leaky Bucket
Token Bucket

漏桶

把请求想象成进入一个桶。

1
2
3
4
5
6
7
8
9
10
请求
↓↓↓↓↓↓↓
+---------+
| |
| Bucket |
| |
+----+----+
|
| 固定速率
v

请求可以快速进入,但按照固定速率流出。

如果请求进入速度长期大于输出速度,桶最终会满,新请求被拒绝。

优点是:

1
输出非常平滑

适合希望严格平滑下游流量的场景。

令牌桶

令牌桶反过来:

1
2
3
4
5
6
7
8
9
10
11
12
Token Generator
|
| replenishRate
v
+-------------+
| Token Token |
| Token Token |
+-------------+
|
| Request consume token
v
Request

系统按照固定速率向桶里放 Token。

请求到来时必须拿到 Token:

1
2
有 Token -> 放行
无 Token -> 拒绝

如果之前一段时间流量很低,桶里可以积累 Token,于是短时间内允许一定程度的突发流量。

因此:

算法 主要特征
漏桶 强调平滑输出
令牌桶 控制平均速率,同时允许一定突发
Gateway Redis RateLimiter Token Bucket

使用 RequestRateLimiter + Redis 限流

Spring Cloud Gateway 提供 RequestRateLimiter GatewayFilter。

请求超过限流策略后,默认返回:

1
429 Too Many Requests

Redis RateLimiter 使用令牌桶算法,并依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>

官方 Redis RateLimiter 中几个最重要的参数是:

1
2
3
replenishRate
burstCapacity
requestedTokens

含义分别为:

1
2
3
4
5
6
7
8
replenishRate
每秒向桶中补充多少 Token

burstCapacity
桶最多能装多少 Token

requestedTokens
一次请求需要消耗多少 Token,默认 1

假设:

1
2
3
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
redis-rate-limiter.requestedTokens: 1

含义是:

1
2
3
稳定速率:10 request/s
最大桶容量:20
每次请求消耗:1 token

如果系统经过一段低流量时间,桶中积累了 20 个 Token,就可以允许一次短暂的 20 请求突发。

根据 Path 限流

先提供 KeyResolver

1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class RateLimitConfiguration {

@Bean
public KeyResolver pathKeyResolver() {
return exchange ->
Mono.just(exchange.getRequest()
.getPath()
.value());
}
}

Route:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
spring:
cloud:
gateway:
routes:
- id: api-route
uri: lb://api-service

predicates:
- Path=/api/**

filters:
- name: RequestRateLimiter
args:
key-resolver: "#{@pathKeyResolver}"
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
redis-rate-limiter.requestedTokens: 1

这样不同 Path 会拥有不同限流 Key。

根据用户限流

更常见的做法是认证完成之后使用用户身份:

1
2
3
4
5
6
@Bean
public KeyResolver userKeyResolver() {
return exchange ->
exchange.getPrincipal()
.map(Principal::getName);
}

这样可以实现:

1
2
3
user:10001
user:10002
user:10003

分别计数。

实际上 Gateway 默认 KeyResolver 就是基于 Principal 名称的 PrincipalNameKeyResolver

IP 限流最容易踩坑

简单 Demo 经常写:

1
2
exchange.getRequest()
.getRemoteAddress()

如果 Gateway 前面没有代理,这通常可以拿到直接连接方地址。

但真实生产架构可能是:

1
2
3
4
5
6
7
8
9
Client
|
CDN
|
WAF
|
Nginx
|
Gateway

Gateway 的 RemoteAddress 可能只是:

1
Nginx IP

于是所有用户都变成同一个 IP。

另一个极端是无条件相信:

1
X-Forwarded-For

这也危险,因为客户端可能自行伪造。

正确思路是:

1
2
3
4
5
6
7
只信任受控代理层写入的 Forwarded Header
|
v
明确可信代理链
|
v
解析真实 Client IP

不要把“从 Header 读字符串”误认为“获取真实 IP”。

为什么不推荐自己用 Redis INCR 手撸限流

一个非常直观的限流方案是:

1
2
3
4
5
6
7
Redis INCR gateway:ip:xxx

第一次:
SET TTL 20s

超过 3 次:
写入黑名单 30s

这种实现用于学习非常容易理解,也可以实现一些特殊的封禁逻辑。实际工程资料中就采用过这种 GlobalFilter + Redis Counter 的做法。

但是如果目的只是普通 API Rate Limit,优先考虑 Gateway 自带 RateLimiter。

因为自己实现以后马上会遇到:

1
2
3
4
5
6
7
8
9
INCR 与 EXPIRE 原子性
TTL 丢失
集群并发
窗口边界
滑动窗口
突发流量
Redis 故障时 fail-open 还是 fail-close
不同接口权重
不同租户额度

最后你会发现自己正在重新开发半个限流组件。

自定义 Filter 更适合:

1
2
3
4
5
持续恶意 IP 封禁
风控名单
特殊业务额度
多级限流
动态策略

普通令牌桶限流优先交给成熟实现。

超时不是可选配置

如果网关完全不控制超时,就可能出现:

1
2
3
4
5
6
7
Client
|
Gateway
|
| 一直等待
v
Downstream

当下游发生网络故障或处理时间异常,连接长期占用,最终可能拖垮 Gateway 自身。

Gateway 支持两类核心 HTTP Timeout:

1
2
connect-timeout
response-timeout

其中:

1
2
3
4
5
connect-timeout
建立连接最多等待多久

response-timeout
建立连接后等待响应最多多久

可以全局配置:

1
2
3
4
5
6
spring:
cloud:
gateway:
httpclient:
connect-timeout: 1000
response-timeout: 5s

在 Gateway 4.3.5 中,全局 connect-timeout 使用毫秒,response-timeout 使用 java.time.Duration 格式。

还可以针对 Route 覆盖:

1
2
3
4
5
6
7
8
9
10
11
12
13
spring:
cloud:
gateway:
routes:
- id: report-route
uri: lb://report-service

predicates:
- Path=/reports/**

metadata:
connect-timeout: 2000
response-timeout: 10000

Route 级别的这两个值均以毫秒表示。

为什么需要 Route 级超时?

因为:

1
2
3
4
5
/login             2s
/query 3s
/file/upload 30s
/report/export 60s
AI streaming 特殊策略

业务性质完全不同。

如果全部设置:

1
response-timeout = 60s

普通查询接口发生故障后也会挂 60 秒。

如果全部设置:

1
response-timeout = 2s

正常的大文件和长任务又会被误杀。

所以 Timeout 是业务 SLA 的一部分,不是随手复制的配置数字。

重试不是“失败就再试几次”

Gateway 提供 Retry 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
26
27
28
spring:
cloud:
gateway:
routes:
- id: query-route
uri: lb://query-service

predicates:
- Path=/query/**

filters:
- name: Retry
args:
retries: 2

statuses:
- BAD_GATEWAY
- SERVICE_UNAVAILABLE
- GATEWAY_TIMEOUT

methods:
- GET

backoff:
firstBackoff: 100ms
maxBackoff: 1s
factor: 2
basedOnPreviousValue: false

Gateway 4.3.5 中 Retry 支持:

1
2
3
4
5
6
7
8
retries
statuses
methods
series
exceptions
backoff
jitter
timeout

如果启用 Filter 但不覆盖默认参数,官方默认值包括:

1
2
3
4
retries = 3
series = 5XX
methods = GET
exceptions = IOException / TimeoutException

为什么默认 Method 是 GET?

因为:

1
GET /orders/100

重复请求通常不会改变业务状态。

而:

1
POST /payment

如果第一次:

1
2
服务器已经扣款
但是响应丢了

Gateway 再重试一次,就有可能变成:

1
扣款 × 2

因此:

重试必须建立在接口幂等性之上。

对于写操作,只有在业务已经实现:

1
2
3
4
5
Idempotency-Key
业务唯一号
数据库唯一约束
幂等表
状态机

等机制之后,才应该考虑自动重试。

还有一个经常被忽视的问题:Gateway 如果重试带 Request Body 的请求,需要缓存请求体,会增加内存压力;而且 Retry Filter 后面的 Filter 也会被再次执行。官方文档对此有明确提醒。

所以重试绝不是:

1
retries: 10

然后祈祷第十一次世界突然恢复正常。

熔断解决的不是一次失败,而是持续失败

假设 order-service 已经不可用。

如果 Gateway 仍然不断把请求打过去:

1
2
3
4
5
6
Gateway
|--------> order-service timeout
|--------> order-service timeout
|--------> order-service timeout
|--------> order-service timeout
|--------> order-service timeout

最终:

1
2
3
4
请求大量堆积
线程/连接/内存资源被消耗
Gateway 自己也开始异常
其他正常服务受到影响

Circuit Breaker 的目的就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
发现下游持续异常
|
v
打开熔断器
|
v
后续请求快速失败
|
v
等待恢复窗口
|
v
试探下游
|
+----+----+
| |
恢复 继续异常
| |
关闭 再次打开

核心思想是:

失败不可怕,持续等待一个已经明显不可用的依赖才可怕。

Hystrix 方案为什么不要再照搬

很多早期 Gateway 教程会写:

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

然后:

1
2
filters:
- name: Hystrix

这是早期 Spring Cloud Gateway 时代的实现方式,在旧资料中非常常见。

当前 Gateway 应使用 Spring Cloud CircuitBreaker API。

Gateway 4.3.5 官方文档明确支持:

1
2
3
Spring Cloud CircuitBreaker
+
Resilience4J

并要求在使用 Resilience4J Reactor 实现时加入:

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
</dependency>

Route 可以配置:

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-service

predicates:
- Path=/orders/**

filters:
- name: CircuitBreaker
args:
name: orderCircuitBreaker
fallbackUri: forward:/fallback/order

当熔断触发后,请求转发到 Gateway 内部:

1
/fallback/order

官方当前 fallbackUri 支持 forward: URI。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@RestController
@RequestMapping("/fallback")
public class FallbackController {

@GetMapping("/order")
public Mono<ResponseEntity<Map<String, Object>>> orderFallback() {

Map<String, Object> body = Map.of(
"code", "SERVICE_UNAVAILABLE",
"message", "订单服务暂时不可用"
);

return Mono.just(
ResponseEntity
.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(body)
);
}
}

不过生产系统不要把 fallback 设计成:

1
2
3
4
5
{
"code": 200,
"message": "success",
"data": null
}

下游已经失败却还返回业务成功,会把真正的问题藏起来。

Retry、Timeout 和 CircuitBreaker 是一套组合拳

这三个机制不能孤立理解。

1
2
3
4
5
6
7
8
9
10
11
Timeout
决定:
一次调用最多等多久

Retry
决定:
失败以后还要不要再试

CircuitBreaker
决定:
当依赖持续失败时是不是继续调用

例如:

1
2
3
connect timeout = 1s
response timeout = 3s
retry = 2

最坏情况下,如果设计不当,请求时间可能远大于单次的 3 秒。

如果再叠加:

1
2
3
4
5
6
Frontend Retry
Nginx Retry
Gateway Retry
Feign Retry
Service Retry
Database Retry

一个客户端请求可以在后端复制成一串请求风暴。

所以必须从调用链整体设计:

1
2
3
4
5
6
7
8
9
10
11
12
13
Client
|
| Retry?
v
Gateway
|
| Retry?
v
Service A
|
| Retry?
v
Service B

重试层级越多,越容易发生 Retry Storm。

比较合理的原则是:

能在最接近失败源的位置做可靠恢复,就不要让调用链每一层都独立重试。

黑名单和访问控制

Gateway 还可以承担一些基础访问控制。

例如:

1
2
3
4
5
6
7
8
9
gateway:
security:
path-whitelist:
- /auth/**
- /actuator/health

ip-blacklist:
- 192.0.2.100
- 192.0.2.101

然后通过 GlobalFilter:

1
2
3
4
5
6
7
8
9
10
11
Request
|
检查可信 Client IP
|
黑名单?
|
+--+--+
| |
是 否
| |
403 continue

但是黑名单并不是完整安全体系。

对于:

1
2
3
4
5
6
DDoS
海量恶意 IP
Bot
CC 攻击
SQL Injection
Web 攻击

更前置的:

1
2
3
4
5
CDN
WAF
Cloud Firewall
Nginx
云负载均衡

通常比 Java Gateway 更适合承担第一层防御。

让一台 JVM 先接住几十万恶意连接,然后再在 Filter 里优雅地返回 403,多少有一点“门已经被撞飞了,再检查访客证”的味道。

GlobalFilter 中不要执行阻塞操作

这是 Gateway 项目最重要的工程规则之一。

Server WebFlux Gateway 基于:

1
2
3
Spring WebFlux
Project Reactor
Netty

官方也特别指出,WebFlux Gateway 与传统同步 Servlet 编程模型存在明显区别。

因此 Filter 中不要直接做:

1
2
3
4
5
jdbcTemplate.query(...);
repository.findById(...);
Thread.sleep(...);
blockingHttpClient.execute(...);
future.get();

如果一个 Netty EventLoop 被阻塞:

1
2
3
4
5
EventLoop-1
|
| JDBC 500ms
|
+---- 这 500ms 其他请求也可能被拖延

大量请求同时发生之后:

1
2
3
4
5
延迟上涨
吞吐下降
请求堆积
超时
雪崩

所以 Gateway 中的数据访问和网络调用优先采用:

1
2
3
4
Reactive Redis
Reactive HTTP Client
Reactive Security
异步机制

更重要的是:

不要把业务逻辑搬进 Gateway。

Gateway 应保持轻量。

错误方向:

1
2
3
4
5
6
Gateway
├── 查询订单数据库
├── 计算价格
├── 判断库存
├── 查询用户积分
└── 组装业务结果

正确方向:

1
2
3
4
5
6
7
8
9
Gateway
├── Route
├── Authentication
├── Authorization Edge Check
├── Rate Limit
├── Traffic Policy
├── Header / Path Transform
├── Observability
└── Proxy

否则 Gateway 最终会从“网关”成长为第二个单体应用。

请求链路中的服务发现和负载均衡

以:

1
uri: lb://order-service

为例,可以把内部处理理解成:

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
1. Route Predicate 匹配

/api/orders/**
|
v

2. 得到 Route

uri = lb://order-service
|
v

3. LoadBalancer 根据 serviceName 查找实例

order-service
|
+--> 10.0.0.11:8080
+--> 10.0.0.12:8080
+--> 10.0.0.13:8080

|
v

4. 选择 ServiceInstance

10.0.0.12:8080

|
v

5. Reactor Netty 发出 HTTP 请求

DiscoveryClient Route Locator 官方默认也采用 lb://service-name 创建可负载均衡的 Route。

因此:

1
Nacos

负责:

1
我有哪些实例

而:

1
Spring Cloud LoadBalancer

负责:

1
这次请求选哪个实例

Gateway 则负责:

1
这个请求应该进入哪个服务

不要把服务发现、路由和负载均衡三个概念混成一个东西。

Gateway 不是只做请求前处理

利用 pre/post 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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Component
public class RequestCostFilter implements GlobalFilter, Ordered {

private static final String START_TIME =
RequestCostFilter.class.getName() + ".START_TIME";

@Override
public Mono<Void> filter(ServerWebExchange exchange,
GatewayFilterChain chain) {

long start = System.nanoTime();

exchange.getAttributes()
.put(START_TIME, start);

return chain.filter(exchange)
.doFinally(signalType -> {

Long begin = exchange.getAttribute(START_TIME);

if (begin == null) {
return;
}

long costMs =
(System.nanoTime() - begin) / 1_000_000;

String path = exchange.getRequest()
.getURI()
.getPath();

HttpStatusCode status =
exchange.getResponse()
.getStatusCode();

// 记录 path、routeId、status、costMs 等信息
});
}

@Override
public int getOrder() {
return -50;
}
}

逻辑就是:

1
2
3
4
5
6
7
8
9
10
11
12
Pre:
记录开始时间

|
v
Downstream

|
v

Post:
结束时间 - 开始时间

这也是理解 Reactive Filter Chain 很好的一个例子。

Gateway 监控应该看哪些指标

网关是整个微服务入口,因此监控优先级非常高。

至少应该关注:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
QPS
响应时间 P50/P95/P99
4xx
5xx
429
连接失败
超时数量
熔断次数
重试次数
每 Route 请求量
每 Route 错误率
下游服务错误分布
Gateway CPU
Gateway Memory
Direct Memory
EventLoop 延迟
Redis RateLimiter 异常

加入:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

后,Gateway Metrics Filter 可以提供:

1
spring.cloud.gateway.requests

指标,并包含:

1
2
3
4
5
6
routeId
routeUri
outcome
status
httpStatusCode
httpMethod

等标签,还可以与 Prometheus / Grafana 组合使用。

特别建议围绕 Route 构建 Dashboard:

1
2
3
4
5
6
7
8
order-route

QPS 1280
P95 120ms
P99 450ms
5xx 0.08%
429 0.01%
timeout 12/min

因为 Gateway 自己可能是健康的:

1
2
CPU 20%
Memory 45%

但某个 Route 已经:

1
2
P99 = 8s
5xx = 15%

如果只看 Gateway JVM 指标,就会产生“系统很健康”的错觉。

Gateway 日志应该记录什么

网关访问日志建议至少包含:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
timestamp
traceId
requestId
clientIp
method
path
query
routeId
targetService
status
costMs
userId
tenantId
userAgent

敏感字段不要直接记录:

1
2
3
4
5
6
7
8
Authorization
Cookie
Password
AccessToken
RefreshToken
银行卡
身份证
完整手机号

尤其不要直接:

1
log.info("headers={}", request.getHeaders());

因为 Header 里很可能存在:

1
2
Authorization: Bearer eyJ...
Cookie: SESSION=...

调试方便五分钟,安全事故调查半年。

TraceId 应该在 Gateway 尽早生成

如果请求没有 TraceId,可以在靠前的 Filter 中生成:

1
2
3
4
5
6
7
8
9
10
11
12
Client
|
v
Gateway
|
生成 traceId
|
+---- order-service
|
+---- inventory-service
|
+---- payment-service

从而把一次请求的日志串起来。

统一入口天然就是:

1
Trace Context

的最佳起点之一。

需要避免这种做法:

1
2
3
4
客户端说:
X-Trace-Id=abc

Gateway 无条件相信

外部 TraceId 可以作为参考,但内部链路追踪字段是否允许由客户端直接控制,应由安全策略明确规定。

Filter 顺序要形成规范

当 Gateway 功能越来越多,最终可能出现:

1
2
3
4
5
6
7
8
9
10
TraceFilter
CorsFilter
AuthenticationFilter
AuthorizationFilter
TenantFilter
RateLimitFilter
RequestLogFilter
GrayFilter
SignFilter
HeaderFilter

如果每个人都随手:

1
return 0;

Filter 顺序会迅速失控。

建议制定统一区间:

Order 范围 用途
-1000 ~ -900 Trace / Request Context
-900 ~ -800 安全 Header 清洗
-800 ~ -700 Authentication
-700 ~ -600 Tenant / App Context
-600 ~ -500 Authorization
-500 ~ -400 风控 / 黑名单
-400 ~ -300 Logging
-300 ~ -200 自定义治理
其他 根据框架内部 Filter 调整

这张表不是 Spring Cloud Gateway 的固定规定,而是一种工程治理方式。

重点不是具体数字,而是:

Filter Order 必须有统一设计,不应该成为“谁写得早谁占一个数字”的公共停车场。

动态路由应该怎么理解

很多系统希望:

1
2
3
4
修改 Nacos 配置
|
v
Gateway 自动更新 Route

从架构上看,动态 Route 通常需要解决:

1
2
3
4
5
6
Route 存哪里?
怎么加载?
怎么刷新?
怎么验证?
多台 Gateway 如何保持一致?
错误配置如何回滚?

这和:

1
2
3
discovery:
locator:
enabled: true

不是同一个概念。

Discovery Locator 的重点是:

1
根据服务注册信息自动生成 Route

而真正的动态路由管理系统通常还包括:

1
2
3
4
5
6
7
8
路由配置中心
版本
发布
校验
审计
灰度
回滚
集群刷新

对于规模较小的系统:

1
2
3
4
5
Git 管理 YAML
+
配置中心
+
应用刷新

已经足够。

对于大量 API 的企业网关,则应该把 Route 当成配置资产,而不是散落在几十个 application.yml 中。

一个更完整的 Gateway 配置示例

下面把前面的能力组合起来。

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
server:
port: 1000

spring:
application:
name: gateway

data:
redis:
host: 127.0.0.1
port: 6379

cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848

gateway:

httpclient:
connect-timeout: 1000
response-timeout: 5s

globalcors:
add-to-simple-url-handler-mapping: true

cors-configurations:
'[/**]':
allowedOriginPatterns:
- "https://*.example.com"

allowedMethods:
- GET
- POST
- PUT
- PATCH
- DELETE
- OPTIONS

allowedHeaders:
- "*"

allowCredentials: true

maxAge: 3600

routes:

- id: auth-route
uri: lb://auth-service

predicates:
- Path=/api/auth/**

filters:
- StripPrefix=1


- id: order-query-route
uri: lb://order-service

predicates:
- Path=/api/orders/**

metadata:
connect-timeout: 1000
response-timeout: 3000

filters:

- StripPrefix=1

- name: RequestRateLimiter
args:
key-resolver: "#{@userKeyResolver}"
redis-rate-limiter.replenishRate: 20
redis-rate-limiter.burstCapacity: 40
redis-rate-limiter.requestedTokens: 1

- name: Retry
args:
retries: 2
statuses:
- BAD_GATEWAY
- SERVICE_UNAVAILABLE
- GATEWAY_TIMEOUT
methods:
- GET
backoff:
firstBackoff: 100ms
maxBackoff: 1s
factor: 2
basedOnPreviousValue: false

- name: CircuitBreaker
args:
name: orderCircuitBreaker
fallbackUri: forward:/fallback/order


- id: file-route
uri: lb://file-service

predicates:
- Path=/api/files/**

metadata:
connect-timeout: 2000
response-timeout: 30000

filters:
- StripPrefix=1

这份配置已经体现了比较完整的 Gateway 思路:

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
统一入口
|
v
CORS
|
v
Global Authentication
|
v
Route Predicate
|
v
Rate Limit
|
v
Timeout
|
v
Retry
|
v
Circuit Breaker
|
v
LoadBalancer
|
v
Microservice

其中具体的 QPS、Timeout、Retry 次数必须根据业务 SLA 和压测结果制定,不能直接把示例数字当作生产参数。

生产环境最容易出现的几个问题

Gateway 能访问,但是 lb:// 服务找不到

检查:

1
2
3
4
5
6
7
Nacos 是否注册成功
Gateway 是否接入同一个 Namespace
Group 是否一致
服务名是否完全一致
LoadBalancer starter 是否存在
Gateway 是否能够访问 Nacos
服务实例是否健康

例如:

1
uri: lb://order-service

而注册中心实际是:

1
order-server

Gateway 不会猜“它俩看起来挺像”。

404 到底是谁返回的

Gateway 出现 404,需要区分两个阶段。

第一种:

1
没有 Route 命中

例如:

1
Path=/api/order/**

请求:

1
/api/orders/1

就没有匹配。

第二种:

1
2
Route 已命中
但改写后的 Path 在下游不存在

例如:

1
2
3
4
5
6
7
8
9
10
11
Gateway:
StripPrefix=2

原始:
/api/order/123

下游最终:
/123

Controller 实际:
/order/123

所以排查 404 时要看:

1
2
3
4
5
6
原始 URI
RouteId
Predicate
改写后 URI
目标 ServiceInstance
下游 Controller Path

不要看到 404 就直接去重启 Nacos。

OPTIONS 请求莫名其妙 404

浏览器正式发送跨域请求前可能先发:

1
OPTIONS /api/orders

如果 Route Predicate 又限定:

1
Method=POST

OPTIONS 就可能不命中。

Gateway 官方提供:

1
2
3
4
5
spring:
cloud:
gateway:
globalcors:
add-to-simple-url-handler-mapping: true

专门帮助处理这种预检场景。

限流后所有用户一起被限

如果 KeyResolver 返回:

1
/api/order

那就是整个接口共享一个桶。

如果想按用户:

1
userId

如果想按租户:

1
tenantId

如果想:

1
tenant + route

可以构造:

1
tenant:10001:order-route

限流策略正确不正确,80% 取决于 Key 怎么设计,而不仅仅是 replenishRate 写多少。

Filter 中调用数据库导致 Gateway 延迟越来越高

如果 GlobalFilter 中使用阻塞 JDBC:

1
2
3
每请求查一次用户
每请求查一次权限
每请求查一次租户

高峰时 Gateway 很容易成为瓶颈。

可考虑:

1
2
3
4
5
6
JWT 本地解析
Redis Reactive
本地短缓存
权限版本号
异步刷新
批量策略

把网关每次请求必须访问的外部依赖尽量压缩。

重试把故障放大

假设:

1
2
1000 QPS
Retry = 3

下游彻底故障时,理论请求压力可能迅速从:

1
1000

膨胀成远高于原始流量的调用尝试。

正确组合应该是:

1
2
3
4
5
6
7
8
9
小次数 Retry
+
Backoff
+
Jitter
+
Timeout
+
CircuitBreaker

不是:

1
2
服务挂了?
那就更用力地请求它。

Gateway 成为单点

既然所有请求都经过 Gateway,那么:

1
1 个 Gateway 实例

几乎等于主动创造一个 Single Point of Failure。

生产环境至少应该:

1
2
3
                  +--> Gateway-1
Client -> LB -----+--> Gateway-2
+--> Gateway-3

Gateway 实例应该尽量无状态。

共享状态放到:

1
2
3
4
Redis
Nacos
统一配置中心
外部身份系统

不要存进某台 Gateway JVM 的本地 Session,然后期待负载均衡器读心。

推荐的职责边界

最终还需要回答一个很重要的问题:

什么逻辑应该放 Gateway,什么逻辑不应该?

比较适合 Gateway:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
路由
服务发现
负载均衡
统一认证入口
粗粒度授权
API Key 校验
签名校验
黑白名单
限流
超时
重试
熔断
灰度路由
协议/Header 转换
TraceId
访问日志
基础审计
CORS

不适合 Gateway:

1
2
3
4
5
6
7
8
9
订单状态计算
库存扣减
结算规则
复杂权限业务
数据库聚合
核心领域逻辑
复杂事务
大量同步 RPC
复杂报表

尤其不要认为:

1
2
Gateway 已经做了权限校验
所以微服务内部不需要任何安全边界

Gateway 是外部入口的一道重要安全边界,但内部服务仍然需要根据系统的 Zero Trust、服务间认证以及权限模型决定是否继续验证调用身份。

一套比较合理的企业网关分层

把整套架构放在一起,可以形成:

flowchart TB
    C["Web / APP / OpenAPI Client"]

    EDGE["CDN / WAF / Nginx / Cloud LB"]

    G["Spring Cloud Gateway"]

    subgraph gateway["Gateway Cross-Cutting Concerns"]
        TRACE["Trace / RequestId"]
        SEC["Authentication"]
        ACL["Access Control"]
        RL["Rate Limit"]
        CB["Circuit Breaker"]
        RETRY["Retry / Timeout"]
        OBS["Logging / Metrics"]
    end

    NACOS["Nacos"]
    REDIS["Redis"]

    subgraph services["Microservices"]
        AUTH["Auth Service"]
        USER["User Service"]
        ORDER["Order Service"]
        FILE["File Service"]
    end

    C --> EDGE
    EDGE --> G

    G --> TRACE
    TRACE --> SEC
    SEC --> ACL
    ACL --> RL
    RL --> CB
    CB --> RETRY
    RETRY --> OBS

    OBS --> AUTH
    OBS --> USER
    OBS --> ORDER
    OBS --> FILE

    NACOS -. Discovery .-> G
    REDIS -. RateLimit / Shared State .-> G

职责可以概括为:

1
2
3
4
5
6
7
8
9
10
11
Edge Gateway
解决网络边界问题

API Gateway
解决微服务入口治理问题

Microservice
解决领域业务问题

Service Mesh / RPC Governance
解决服务之间通信治理问题

不要试图让一个 Gateway 把四层职责全部包办。

从请求视角重新理解 Gateway

学习 Spring Cloud Gateway 最容易陷入的方式,是按配置项背:

1
2
3
4
5
6
7
8
Path
Header
StripPrefix
RewritePath
RequestRateLimiter
Retry
CircuitBreaker
...

更容易建立长期理解的方式,是从一个请求看整个过程:

1
2
GET /api/orders/10001
Authorization: Bearer xxx

进入系统以后:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
1. Edge Layer

Client
|
CDN / WAF / Nginx
|
Gateway


2. Gateway Route Matching

Host ?
Path ?
Method ?
Header ?

|
v

order-route


3. Pre Filter

TraceId
Client IP
Authentication
Tenant Context
Rate Limit
Header Sanitization
Path Rewrite


4. Service Discovery

lb://order-service
|
v
Nacos
|
v
10.0.0.12:8080


5. Resilience

Connect Timeout
Response Timeout
Retry
CircuitBreaker


6. Proxy

Reactor Netty
|
v
order-service


7. Response

order-service
|
v
Post Filters
|
Logging / Metrics / Headers
|
v
Client

这条链路理解了,Gateway 大部分配置都会自然地找到自己的位置。

总结

Spring Cloud Gateway 真正解决的不是“把 /a 转发到 A 服务、把 /b 转发到 B 服务”这么简单的问题,而是为微服务建立一个可治理的统一 API 边界。

它最核心的模型仍然非常简单:

1
2
3
4
5
6
7
Route
=
Predicate
+
URI
+
Filter

但围绕这个模型,可以进一步构建:

1
2
3
4
5
6
7
8
9
10
11
12
13
服务发现
负载均衡
路径改写
统一鉴权
跨域处理
访问控制
Redis 令牌桶限流
Timeout
Retry
Circuit Breaker
Trace
Metrics
Logging

Spring Cloud Gateway 的核心请求模型从早期 Spring Cloud 2.x 到今天依然保持连续:请求先匹配 Route,再由 Gateway Web Handler 执行对应 Filter Chain,Filter 可以在代理请求前后分别工作。变化更多发生在 Spring Boot、Starter、配置层级以及熔断组件等外围技术上。当前 Gateway 4.3.5 面向 Spring Boot 3 体系,而 Gateway 5.0.2 已进入 Spring Boot 4 / Spring Framework 7 体系,因此维护旧项目时尤其需要避免把旧版本 Hystrix、旧 starter 和新版本配置机械混用。

真正落到生产环境时,可以记住几条原则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Gateway 保持轻量,不承载领域业务。

显式控制外部 Route,不要因为接入注册中心就暴露所有服务。

鉴权失败使用正确的 401 / 403,不要伪装成 500。

Rate Limit 的关键是限流 Key,而不仅仅是 QPS 数字。

Retry 必须建立在幂等性之上。

Timeout、Retry、CircuitBreaker 要作为整体设计。

WebFlux Filter 中避免阻塞调用。

不要信任客户端自行传递的身份 Header 和代理 Header。

Gateway 自己必须高可用。

监控要下钻到 Route,而不仅仅看 Gateway JVM。

版本升级时重新核对 Gateway 官方文档,不要复制五年前的配置。

当这些边界建立起来之后,Spring Cloud Gateway 才真正从一个“反向代理 Java 版”变成微服务架构中的统一流量入口和 API 治理层。


Spring Cloud Gateway:微服务网关的路由、过滤、限流与容错实践
https://allendericdalexander.github.io/2026/08/16/java/spring/Spring-Cloud-Gateway/
作者
AtLuoFu
发布于
2026年8月16日
许可协议