Spring Cloud Gateway 从入门到实战
Spring Cloud Gateway 从入门到实战:微服务架构下的统一网关
本文基于 Spring Boot 3.x + Spring Cloud 2023.x(Gateway 4.x)+ Java 17 编写。如果你还在使用 Spring Boot 2.7 / Spring Cloud 2021.x(Gateway 3.x),文中绝大多数配置和代码完全兼容。
📌 版本配套速查
| Spring Boot | Spring Cloud | Gateway | 说明 |
|---|---|---|---|
| 2.7.x | 2021.0.x(Jubilee) | 3.1.x | 老项目常见组合,配置基本一致 |
| 3.2.x | 2023.0.x(Leyton) | 4.1.x | 本文基准 |
| 3.3.x ~ 3.4.x | 2024.0.x(Moorgate) | 4.2.x | 官方拆分出 Gateway WebMVC 版本 |
| 3.5.x | 2025.0.x(Northfields) | 4.3.x | 当前最新稳定线 |
升级前请以 Spring Cloud 官网 Projects 页面的 Learn 栏版本对照表为准。
📑 目录
基础篇
- 一、为什么需要 API 网关
- 二、Spring Cloud Gateway 是什么
- 三、三大核心概念
- 四、5 分钟上手
- 五、常用断言(Predicate)
- 六、常用过滤器(Filter)
- 七、集成注册中心:从写死地址到服务发现
- 八、自定义过滤器实战
- 九、限流:Redis 令牌桶
- 十、熔断降级:CircuitBreaker + Resilience4j
- 十一、跨域(CORS)统一处理
- 十二、生产环境避坑指南 ⭐
进阶篇
- 十三、自定义断言工厂
- 十四、动态路由(配置中心驱动)
- 十五、修改请求体 / 响应体
- 十六、灰度发布实战
- 十七、全链路 TraceId 过滤器
- 十八、监控与性能调优
- 十九、高频面试题速查
- 二十、写在最后
一、为什么需要 API 网关
在单体应用时代,前端直接调用后端接口就够了。但当系统拆分成几十个微服务后,客户端会面临一堆现实问题:
- 服务地址分散,客户端需要知道每个服务的 IP 和端口;
- 每个服务都要重复实现鉴权、限流、日志、跨域等横切逻辑;
- 内部服务结构直接暴露给外部,存在安全风险;
- 缺乏统一的流量入口,灰度发布、熔断降级无从下手。
API 网关就是为了解决这些问题而生的:它作为系统的统一入口,把"转发"这件小事和"鉴权、限流、监控"这些横切能力收敛到一处。
flowchart LR
C[客户端] --> G[API 网关<br/>Spring Cloud Gateway]
G --> A[用户服务]
G --> B[订单服务]
G --> D[商品服务]
网关的典型职责:路由转发、统一鉴权、限流熔断、日志监控、跨域处理、灰度分流。
二、Spring Cloud Gateway 是什么
Spring Cloud Gateway 是 Spring 官方推出的网关组件,基于 Spring WebFlux、Project Reactor 和 Netty 构建的高性能异步非阻塞网关。它的前身 Zuul 1.x 基于 Servlet 同步阻塞模型,每个请求占用一个线程,高并发下线程开销大;而 Gateway 采用响应式模型,用少量线程支撑大量并发连接,性能天然占优。
核心特性:
- 动态路由:支持按路径、方法、请求头等多种条件匹配,可随注册中心动态刷新;
- 内置丰富的断言(Predicate)和过滤器(Filter);
- 与 Spring Cloud 生态无缝集成(Nacos、Eureka、Resilience4j、Micrometer 等);
- 支持 WebSocket、HTTP/2,可集成 Redis 实现分布式限流。
💡 提示:Spring Cloud 2024.x 之后,官方还提供了
spring-cloud-starter-gateway-mvc(Gateway Server WebMVC),让网关可以跑在 Servlet 技术栈上,适合不想引入响应式编程的团队。本文仍以主流的 WebFlux 版本为例。
三、三大核心概念
理解这三个概念,就理解了 Gateway 的一切:
| 概念 | 说明 |
|---|---|
| Route(路由) | 网关的基本单元,由 ID、目标 URI、一组断言和一组过滤器组成。断言全部匹配才走这条路由 |
| Predicate(断言) | Java 8 的 Predicate,判断请求是否满足条件(路径、方法、Header、参数……) |
| Filter(过滤器) | 在请求转发前后对请求/响应进行修改,是 Gateway 的核心扩展点 |
请求的处理流程如下:
flowchart LR
A[客户端请求] --> B{Handler Mapping<br/>按断言匹配路由}
B -- 匹配成功 --> C[Gateway Web Handler]
C --> D[过滤器链 pre 逻辑]
D --> E[转发到目标服务]
E --> F[过滤器链 post 逻辑]
F --> G[返回响应]
B -- 无匹配 --> H[404]
关键点:过滤器在转发前(pre)修改请求,在转发后(post)修改响应,请求和响应都会按 Ordered 顺序穿过整条过滤器链。这与 Servlet 的 Filter Chain 思想一致,只是 API 变成了响应式。
四、5 分钟上手
1. 引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- actuator 可选,用于查看路由信息 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
⚠️ 划重点:Gateway 基于 WebFlux,千万不要引入
spring-boot-starter-web,否则启动直接报错。
2. 配置路由
server:
port: 9000
spring:
application:
name: gateway-server
cloud:
gateway:
routes:
- id: user-service-route # 路由唯一标识
uri: http://localhost:8081 # 目标地址
predicates:
- Path=/api/user/** # 断言:路径匹配
filters:
- StripPrefix=1 # 过滤器:去掉第一段路径前缀
- id: order-service-route
uri: http://localhost:8082
predicates:
- Path=/api/order/**
filters:
- StripPrefix=1
StripPrefix=1 的作用:把 /api/user/get/1 转发为 http://localhost:8081/user/get/1,避免下游服务看到网关加的前缀。
3. 启动并验证
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
访问 http://localhost:9000/api/user/get/1,请求会被转发到用户服务。一个最简网关就跑起来了。
五、常用断言(Predicate)
Gateway 内置了十几种断言,全部形如"名称 + 参数"写在配置里。常用的有:
| 断言 | 示例 | 说明 |
|---|---|---|
Path |
Path=/api/user/** |
路径匹配,最常用 |
Method |
Method=GET,POST |
匹配 HTTP 方法 |
Query |
Query=token, ^abc. |
匹配请求参数,可加正则 |
Header |
Header=X-Request-Id, \d+ |
匹配请求头 |
Cookie |
Cookie=channel, ^pc$ |
匹配 Cookie |
Host |
Host=**.example.org |
匹配主机名 |
After / Before / Between |
After=2026-01-01T00:00:00+08:00[Asia/Shanghai] |
按时间匹配,可用于限时活动 |
Weight |
Weight=gray-group, 8 |
按权重分流,灰度发布利器 |
RemoteAddr |
RemoteAddr=192.168.1.0/24 |
匹配来源 IP 段 |
多个断言是 AND 关系,同时满足才能命中。例如"灰度期间只允许 GET 请求走新版本服务":
- id: user-service-gray
uri: lb://user-service-gray
predicates:
- Path=/api/user/**
- Method=GET
- Weight=gray-group, 2 # 20% 流量进灰度
六、常用过滤器(Filter)
过滤器分为路由级(GatewayFilter)和全局级(GlobalFilter)。常用的路由级过滤器:
| 过滤器 | 示例 | 说明 |
|---|---|---|
StripPrefix |
StripPrefix=2 |
去掉路径前 N 段 |
PrefixPath |
PrefixPath=/api |
给路径加前缀 |
RewritePath |
RewritePath=/api/(?<seg>.*), /$\{seg} |
正则重写路径 |
AddRequestHeader |
AddRequestHeader=X-Source, gateway |
添加请求头 |
AddResponseHeader |
AddResponseHeader=X-Response-Time, 100 |
添加响应头 |
Retry |
见下文 | 失败重试 |
CircuitBreaker |
见下文 | 熔断降级 |
RequestRateLimiter |
见下文 | 请求限流 |
RewritePath 是 StripPrefix 的正则加强版,更灵活,推荐配合命名捕获组使用(注意 YAML 中 $ 需转义为 $\{seg})。
如果希望某些过滤器对所有路由生效,可以用默认过滤器:
spring:
cloud:
gateway:
default-filters:
- AddRequestHeader=X-Source, gateway
七、集成注册中心:从写死地址到服务发现
上面 uri: http://localhost:8081 写死了目标地址,服务一旦扩容或多实例就失效了。正确姿势是结合注册中心(以 Nacos 为例):
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
gateway:
routes:
- id: user-service
uri: lb://user-service # lb = LoadBalancer,按服务名负载均衡
predicates:
- Path=/api/user/**
filters:
- StripPrefix=1
lb://服务名 会通过 ReactiveLoadBalancer 从 Nacos 拉取实例列表并负载均衡转发,服务上下线自动感知。
Gateway 也支持根据注册中心自动生成路由(服务名即路径前缀),适合本地调试,生产环境一般不建议:
spring:
cloud:
gateway:
discovery:
locator:
enabled: true
lower-case-service-id: true # 允许 http://网关:9000/user-service/xxx 直接按服务名路由
八、自定义过滤器实战
GlobalFilter 是全局过滤器接口,与路由无关、对所有请求生效,是做鉴权、日志、链路追踪的标配扩展点。
先看一个最常用的:接口耗时统计。
@Component
public class TimingGlobalFilter implements GlobalFilter, Ordered {
private static final Logger log = LoggerFactory.getLogger(TimingGlobalFilter.class);
/** 用 exchange 的 attributes 在 pre/post 阶段之间传递数据,别用 ThreadLocal */
private static final String START_TIME = "requestStartTime";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
exchange.getAttributes().put(START_TIME, System.currentTimeMillis());
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
Long start = exchange.getAttribute(START_TIME);
if (start != null) {
long cost = System.currentTimeMillis() - start;
log.info("[网关] {} {} 耗时 {} ms",
exchange.getRequest().getMethod(),
exchange.getRequest().getURI().getPath(),
cost);
}
}));
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}
再看一个统一鉴权过滤器:白名单放行 + 无 Token 直接返回 401 JSON。
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
private static final List<String> WHITE_LIST = List.of(
"/api/auth/login", "/api/user/register"
);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getURI().getPath();
if (WHITE_LIST.stream().anyMatch(path::startsWith)) {
return chain.filter(exchange);
}
String token = exchange.getRequest().getHeaders().getFirst("Authorization");
if (token == null || token.isBlank()) {
return writeUnauthorized(exchange, "未登录或令牌缺失");
}
// 实际项目中在这里解析并校验 JWT,校验通过后可把用户信息放入请求头传给下游
return chain.filter(exchange);
}
private Mono<Void> writeUnauthorized(ServerWebExchange exchange, String message) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
String body = "{\"code\":401,\"message\":\"" + message + "\"}";
DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(buffer));
}
@Override
public int getOrder() {
return -100; // 数值越小优先级越高,鉴权要早于大多数业务过滤器执行
}
}
两个要点:
getOrder()决定过滤器执行顺序,鉴权、限流这类前置校验应设小值,让它们尽早执行;- 多个过滤器串在一条链上,请求按 order 升序穿过 pre 阶段,响应按降序穿过 post 阶段。
九、限流:Redis 令牌桶
Gateway 内置 RequestRateLimiter 过滤器,基于 Redis + Lua 实现的令牌桶算法,天然支持多实例共享限流数据。
第一步,引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
第二步,定义限流 key 的维度(按 IP、按用户、按接口都可以):
@Configuration
public class RateLimiterConfig {
@Bean
public KeyResolver ipKeyResolver() {
return exchange -> Mono.justOrEmpty(
Optional.ofNullable(exchange.getRequest().getRemoteAddress())
.map(addr -> addr.getAddress().getHostAddress())
.orElse("unknown"));
}
}
第三步,在路由上启用:
spring:
data:
redis:
host: 127.0.0.1
port: 6379
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/api/user/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10 # 每秒补充 10 个令牌 = 稳态 10 QPS
redis-rate-limiter.burstCapacity: 20 # 桶容量 20 = 允许突发 20 个请求
redis-rate-limiter.requestedTokens: 1 # 每次请求消耗 1 个令牌
key-resolver: "#{@ipKeyResolver}" # 按 IP 维度限流
超限的请求会收到 429 Too Many Requests。如果想给被限流的用户一个友好的降级响应,可以搭配下面的熔断降级做统一处理。
十、熔断降级:CircuitBreaker + Resilience4j
下游服务挂掉或变慢时,网关应该快速失败并返回兜底数据,避免故障蔓延。引入 Resilience4j:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
</dependency>
路由配置:
resilience4j:
circuitbreaker:
instances:
user-service-cb:
slidingWindowSize: 10 # 统计窗口内最近 10 次请求
failureRateThreshold: 50 # 失败率超 50% 熔断
waitDurationInOpenState: 10s # 熔断 10 秒后尝试半开恢复
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/api/user/**
filters:
- name: CircuitBreaker
args:
name: user-service-cb
fallbackUri: forward:/fallback/user
网关内提供 fallback 接口:
@RestController
public class FallbackController {
@RequestMapping("/fallback/user")
public Map<String, Object> userFallback() {
return Map.of("code", 503, "message", "用户服务繁忙,请稍后重试");
}
}
十一、跨域(CORS)统一处理
有了网关,跨域配置收敛到一处即可:
spring:
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]':
allowed-origin-patterns: "*"
allowed-methods: "*"
allowed-headers: "*"
allow-credentials: true
max-age: 3600
⚠️ 注意:如果下游服务自己也配了 CORS,浏览器会收到两个
Access-Control-Allow-Origin响应头而直接报错。跨域只在网关配置一处,下游服务删掉@CrossOrigin和 WebMvcConfigurer 里的 CORS 配置。
十二、生产环境避坑指南 ⭐
以下是 Gateway 使用中最高频的坑,每一条都值得细看:
1. 引入 spring-boot-starter-web 直接启动失败
报错:Spring MVC found on classpath, which is incompatible with Spring Cloud Gateway at this time.
Gateway 是 WebFlux 技术栈,与 MVC 互斥。检查依赖树 mvn dependency:tree,把传递引入的 web starter 排除掉。
2. 请求体(Body)只能读一次
WebFlux 的请求体是流,被一个过滤器消费后,下游就收不到 body 了。在过滤器中读取 body 的正确方式是用内置的 ReadBodyRoutePredicateFactory,或通过 DataBufferUtils.join 聚合并重新包装请求:
return DataBufferUtils.join(exchange.getRequest().getBody()).flatMap(dataBuffer -> {
byte[] bytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(bytes);
DataBufferUtils.release(dataBuffer);
// 缓存到 attributes 供后续使用,并重建请求体往下传
ServerHttpRequest newRequest = exchange.getRequest().mutate()
.body(Flux.just(exchange.getResponse().bufferFactory().wrap(bytes)))
.build();
return chain.filter(exchange.mutate().request(newRequest).build());
});
注意这会把整个 body 缓存在内存,大文件上传场景不适用。
3. 不要用 ThreadLocal 传递上下文
响应式模型下,一个请求的多个处理阶段可能在不同线程上执行,ThreadLocal 会神秘失效。统一使用 exchange.getAttributes().put/get 传递数据(见第八节耗时过滤器的写法)。
4. 过滤器里严禁阻塞调用
在过滤器里调 JDBC、RestTemplate、Thread.sleep,会卡死 Netty 的 EventLoop 线程,把整个网关拖垮。必须调用阻塞资源时,用 Schedulers.boundedElastic() 隔离,或换成响应式客户端(R2DBC、WebClient)。
5. 超时必须显式配置
默认不配响应超时,下游一旦 hang 住,网关线程和连接就会被慢慢占满:
spring:
cloud:
gateway:
httpclient:
connect-timeout: 1000 # 连接超时 1s
response-timeout: 5s # 响应超时 5s
个别慢接口可在路由的 metadata 里单独放宽(response-timeout: 30000)。
6. 转发到下游的 Header 注意点
Host头默认会改写为下游地址,如需保留原始 Host:spring.cloud.gateway.preserve-host-header=true;- 建议在全局过滤器里给下游追加
X-Request-Id、X-Real-IP等头,方便链路追踪。
7. actuator 路由端点是个安全隐患
GET /actuator/gateway/routes 能看到全部路由,而 POST /actuator/gateway/routes/{id} 甚至能动态修改路由定义。生产环境要么关闭,要么必须纳入鉴权体系:
management:
endpoint:
gateway:
enabled: true # 仅内网/受保护环境开启
endpoints:
web:
exposure:
include: health, gateway
8. 路由不生效时的排查三板斧
- 开 debug 日志:
logging.level.org.springframework.cloud.gateway=DEBUG; - 访问
GET /actuator/gateway/routes核对路由是否加载、断言是否如预期; - 检查多个路由的断言是否重叠——Gateway 按配置顺序匹配,顺序错了会被前面的路由"截胡"。
十三、进阶:自定义断言工厂
内置断言不满足需求时,可以自定义断言工厂。命名必须以 RoutePredicateFactory 结尾,Gateway 会通过命名约定自动发现它。例如实现"匹配指定 API 版本"的断言:
@Component
public class ApiVersionRoutePredicateFactory
extends AbstractRoutePredicateFactory<ApiVersionRoutePredicateFactory.Config> {
public ApiVersionRoutePredicateFactory() {
super(Config.class);
}
/** 定义 shortcut 写法中参数的顺序 */
@Override
public List<String> shortcutFieldOrder() {
return List.of("version");
}
@Override
public Predicate<ServerWebExchange> apply(Config config) {
return exchange -> {
String version = exchange.getRequest().getHeaders().getFirst("X-Api-Version");
return config.getVersion().equalsIgnoreCase(version);
};
}
@Validated
public static class Config {
@NotEmpty
private String version;
// getter / setter 省略
}
}
配置中就能像内置断言一样使用(shortcut 写法):
predicates:
- Path=/api/user/**
- ApiVersion=v2 # 只放行携带 X-Api-Version: v2 的请求
十四、进阶:动态路由(配置中心驱动)
RouteDefinitionWriter 是 Gateway 提供的路由定义读写接口,配合 Nacos 配置监听,就能做到改配置即改路由,不用重启网关。
第一步,封装路由的增删改:
@Component
public class DynamicRouteService {
private final RouteDefinitionWriter routeDefinitionWriter;
private final ApplicationEventPublisher publisher;
/** 本地缓存已保存的路由定义,便于删除 */
private final Map<String, RouteDefinition> cache = new ConcurrentHashMap<>();
public DynamicRouteService(RouteDefinitionWriter routeDefinitionWriter,
ApplicationEventPublisher publisher) {
this.routeDefinitionWriter = routeDefinitionWriter;
this.publisher = publisher;
}
/** 新增或更新路由 */
public String save(RouteDefinition definition) {
cache.put(definition.getId(), definition);
routeDefinitionWriter.save(Mono.just(definition)).subscribe();
publisher.publishEvent(new RefreshRoutesEvent(this)); // 触发路由表刷新
return "ok";
}
/** 删除路由 */
public String delete(String routeId) {
if (cache.remove(routeId) == null) {
return "路由不存在";
}
routeDefinitionWriter.delete(Mono.just(routeId)).subscribe();
publisher.publishEvent(new RefreshRoutesEvent(this));
return "ok";
}
/** 全量替换:先删旧、再存新 */
public void replaceAll(List<RouteDefinition> routes) {
cache.keySet().forEach(this::delete);
routes.forEach(this::save);
}
}
第二步,监听 Nacos 配置变化(需引入 spring-cloud-starter-alibaba-nacos-config):
@Component
public class NacosRouteEventListener implements ApplicationRunner {
private final DynamicRouteService dynamicRouteService;
private final NacosConfigManager nacosConfigManager;
public NacosRouteEventListener(DynamicRouteService dynamicRouteService,
NacosConfigManager nacosConfigManager) {
this.dynamicRouteService = dynamicRouteService;
this.nacosConfigManager = nacosConfigManager;
}
@Override
public void run(ApplicationArguments args) throws Exception {
nacosConfigManager.getConfigService().addListener(
"gateway-routes.json", "DEFAULT_GROUP",
new AbstractListener() {
@Override
public void receiveConfigInfo(String configInfo) {
// 配置内容就是 RouteDefinition 的 JSON 数组
ObjectMapper mapper = new ObjectMapper();
List<RouteDefinition> routes = mapper.readValue(configInfo,
new TypeReference<List<RouteDefinition>>() {});
dynamicRouteService.replaceAll(routes);
}
});
}
}
两个细节:
RefreshRoutesEvent发布后,Gateway 会重新把RouteDefinition编译成可执行的路由,这一步是必须的;- 内存里存的动态路由不持久化,网关重启后会丢失,所以启动时应主动
getConfig拉一次全量配置再注册监听。
十五、进阶:修改请求体 / 响应体
有些场景需要网关"加工"报文:入参补全、响应脱敏、协议适配。内置的 ModifyRequestBody / ModifyResponseBody 过滤器配合 RewriteFunction 即可实现,无需手写字节流处理。
@Component
public class MaskRewriteConfig {
/** 把响应 JSON 中的手机号做脱敏处理 */
@Bean
public RewriteFunction<String, String> maskMobileRewrite() {
return (exchange, body) -> Mono.just(
body.replaceAll("(\"mobile\":\\s*\"?)(\\d{3})\\d{4}(\\d{4})", "$1$2****$3"));
}
}
filters:
- name: ModifyResponseBody
args:
inClass: '#{T(java.lang.String)}'
outClass: '#{T(java.lang.String)}'
rewriteFunction: '#{@maskMobileRewrite}'
ModifyRequestBody 用法相同,只是作用于请求方向。注意:一旦涉及 body 读写,就会触发"请求体只能读一次"的问题(第十二节第 2 条),这两个内置过滤器已经替你处理了重发逻辑,但大报文会被完整载入内存,只适合 JSON 类小报文。
十六、进阶:灰度发布实战
利用 Weight 断言做按比例分流,两条路由加入同一个权重组:
spring:
cloud:
gateway:
routes:
- id: user-service-stable
uri: lb://user-service
predicates:
- Path=/api/user/**
- Weight=user-group, 90 # 90% 流量走稳定版
- id: user-service-gray
uri: lb://user-service-gray
predicates:
- Path=/api/user/**
- Weight=user-group, 10 # 10% 流量走灰度版
想支持"指定用户必进灰度",可以在前面加一条优先级更高的路由(Header / Cookie 命中即短路):
- id: user-service-gray-force
uri: lb://user-service-gray
predicates:
- Path=/api/user/**
- Cookie=gray, true # 特定用户加了灰度 Cookie,必定进灰度
十七、进阶:全链路 TraceId 过滤器
分布式排查问题第一步是对齐日志。在网关生成/透传 TraceId 是最低成本的做法:
@Component
public class TraceIdGlobalFilter implements GlobalFilter, Ordered {
public static final String TRACE_ID = "X-Request-Id";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String traceId = Optional
.ofNullable(exchange.getRequest().getHeaders().getFirst(TRACE_ID))
.orElse(UUID.randomUUID().toString().replace("-", ""));
ServerHttpRequest request = exchange.getRequest().mutate()
.header(TRACE_ID, traceId)
.build();
return chain.filter(exchange.mutate().request(request).build())
.then(Mono.fromRunnable(() ->
exchange.getResponse().getHeaders().set(TRACE_ID, traceId)));
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE; // 最早执行,让后续过滤器都能拿到 TraceId
}
}
请求头会带着 X-Request-Id 传给下游,下游日志框架(Logback MDC、Log4j2 等)只需在 pattern 中加入该字段;响应头返回给前端,用户反馈问题时一查便知全链路。生产级方案可以在此基础上接入 SkyWalking / OpenTelemetry。
十八、进阶:监控与性能调优
监控接入
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
management:
endpoints:
web:
exposure:
include: health,prometheus
metrics:
tags:
application: ${spring.application.name}
Prometheus 抓取 /actuator/prometheus,Grafana 配面板即可。重点关注的指标:
http.server.requests:接口 QPS、P99 延迟、状态码分布;spring.cloud.gateway.requests:Gateway 自带指标,按路由 ID、断言、状态码打标签,能直接看出哪条路由慢;- JVM 指标:重点关注堆外内存(Netty direct memory)使用量。
性能调优清单
spring:
cloud:
gateway:
httpclient:
pool:
max-connections: 2000 # 连接池上限(fixed 模式)
acquire-timeout: 3000 # 获取连接的等待上限
max-idle-time: 30s # 空闲连接回收,防止被 LB 中间件掐断
- 超时三件套必配:
connect-timeout、response-timeout、acquire-timeout,任何一环缺失都会导致请求堆积; - 过滤器要"早失败":鉴权、限流的 order 设小值,把无效请求挡在最前面,别让它们消耗下游资源;
- 别在过滤器打印大 body 日志:读 body 意味着内存复制,QPS 高时是肉眼可见的性能损耗;
- JVM 层面:JDK 17 + G1 默认即可;容器环境注意
-XX:MaxDirectMemorySize,Netty 大量使用堆外内存,配小了会OutOfDirectMemoryError。
十九、高频面试题速查
| 问题 | 要点 |
|---|---|
| Gateway 和 Zuul 的区别? | Zuul 1.x 基于 Servlet 同步阻塞,一请求一线程;Gateway 基于 WebFlux + Netty 异步非阻塞,线程少、吞吐高,且是 Spring 官方亲儿子 |
| 三大核心概念? | Route(路由)、Predicate(断言)、Filter(过滤器),断言定"谁能过",过滤器定"怎么处理" |
| GlobalFilter 和 GatewayFilter 的区别? | GlobalFilter 对所有路由生效,GatewayFilter 绑定具体路由;二者都按 Ordered 排序后在同一条链上执行 |
| 为什么请求体只能读一次? | WebFlux 中 body 是流式 DataBuffer,消费即释放;需要多次读必须缓存重建(ReadBodyRoutePredicateFactory / DataBufferUtils.join) |
| 网关怎么实现限流? | 内置 RequestRateLimiter:Redis + Lua 令牌桶,多实例共享计数;KeyResolver 决定限流维度(IP/用户/接口) |
| 路由如何动态刷新? | RouteDefinitionWriter 修改定义 + 发布 RefreshRoutesEvent;来源可以是注册中心、Nacos 配置监听或 actuator 端点 |
lb:// 的原理? |
ReactiveLoadBalancerClientFilter 把 lb://服务名 解析成实际实例地址,从注册中心拉列表并负载均衡 |
| 怎么做灰度发布? | Weight 断言按比例分流,配合 Header/Cookie 断言实现"指定用户进灰度" |
二十、写在最后
回顾整篇文章(基础 + 进阶):
- 骨架是 Route + Predicate + Filter,一切功能都围绕这三者展开;
- 基础四件套:注册中心负载均衡(lb://)+ 全局过滤器(鉴权/日志)+ Redis 限流 + Resilience4j 熔断;
- 进阶三板斧:动态路由让配置流动起来,灰度发布让上线更从容,TraceId 全链路让问题无处遁形;
- 最重要的心法:忘掉 Servlet 的思维,拥抱响应式——不用 ThreadLocal、不做阻塞调用、请求体只能读一次。
建议把 Spring Cloud Gateway 官方文档(Spring 官网 Docs 板块内搜索 “Spring Cloud Gateway”)当作工具书备查。
如果本文对你有帮助,欢迎点赞、收藏、转发三连~ 有问题欢迎在评论区交流。
- 点赞
- 收藏
- 关注作者
评论(0)