【Spring Cloud Gateway 实战系列】进阶篇:过滤器高级用法、动态路由配置与性能优化
一、过滤器高级用法:从基础到复杂场景
1.1 过滤器执行顺序深度解析
Spring Cloud Gateway的过滤器执行顺序由Order
接口控制,数值越小优先级越高。全局过滤器(GlobalFilter)需通过GatewayFilterAdapter
适配为局部过滤器,默认过滤器(default-filters)优先级高于局部过滤器。
1.1.1 顺序控制示例
@Component
@Order(1) // 优先级高于默认过滤器
public class AuthFilter implements GlobalFilter {@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {// 鉴权逻辑return chain.filter(exchange);}
}
1.1.2 异步处理优化
利用WebFlux的异步特性,避免阻塞线程:事件驱动 + 非阻塞
@Component
public class AsyncLogFilter implements GlobalFilter {@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {return chain.filter(exchange) // ① 把请求继续往后传递.then(Mono.fromRunnable(() -> {// ② 在响应已经发送给客户端之后,再异步记日志}));}
}
- ① 把请求交给下游 Filter / 微服务,这一步只是注册回调,EventLoop 立即返回。
- ② then(…) 的语义是:等 ①的 Mono 完成(onComplete) 后,再执行日志任务。 如果日志是磁盘 IO,这个 Runnable 会被提交到 Reactor的 boundedElastic Scheduler(自带线程池),不会占用宝贵的 EventLoop。
一句话总结:WebFlux 用“事件回调 + 少量 EventLoop 线程”取代“一条请求独占一条线程”的模型,任何耗时操作都被异步化,从而避免线程被阻塞,提升吞吐量和资源利用率。
这里解释一下WebFlux(基于 Reactor 的响应式编程模型)的由来:
- 由来:“WebFlux”这个名字其实是两部分拼成的:
- Web:表示它是一个Web 框架,用来处理HTTP、WebSocket、SSE(服务器推送事件)等网络通信。
- Flux:来自 Reactor 项目 中的核心类 Flux(还有Mono),Reactor 是 Spring 5 中引入的响应式编程库,专门用来支持非阻塞、异步、事件驱动的编程模型。
- 换句话说,Spring 给这个新模块起名 spring-webflux,是为了强调它基于 Reactor 的响应式编程模型,区别于传统的Spring MVC(基于 Servlet 的同步阻塞模型)。 所以,“WebFlux”其实就是“Web +Flux”,既表达了用途(Web),也点明了底层技术(Reactor Flux)。
与传统的 Spring MVC(基于 Servlet的同步阻塞模型)有啥区别?
- 传统 Servlet(SpringMVC)为什么会被“阻塞”?
- 每个请求进来,容器(Tomcat 等)会 从线程池里拿一条工作线程去跑整个 Filter→Servlet→Controller→Service→DAO 的调用链。
- 只要业务代码里出现一次Thread.sleep()、一次 JDBC 查询、一次 HTTP 调用,这条线程就一直被占用,直到方法返回。
- 线程被卡住期间,它既不能服务别的请求,也无法被回收,于是:
- 并发量受线程池上限限制(几百条就到顶了)。
- CPU 空转,资源利用率低。
- WebFlux(Reactor-Netty)如何做到“不阻塞”?
- 事件循环 + 少量线程
- Netty 启动 数量 = CPU 核数 的一组 EventLoop 线程,每条线程维护一个 Selector,不断轮询 IO 事件(连接、可读、可写)。
- 当业务代码需要读写网络、文件、数据库时,Reactor 会注册一个回调;当前线程立即返回 EventLoop,去处理别的 IO 事件,不等人。
- 链式异步(Mono/Flux)
- Mono.fromRunnable(…) 只是把“记日志”这个动作包装成一个Runnable 任务,Reactor 会把它提交给 调度器(Scheduler) 里的某个线程池。
- 这样,主 EventLoop 线程不会被日志 IO 卡住,日志任务在后台慢慢写即可。
- 事件循环 + 少量线程
1.2 自定义过滤器链设计
1.2.1 组合过滤器实现复杂逻辑
spring:cloud:gateway:routes:- id: complex-routeuri: lb://service #1️⃣predicates:- Path=/api/** #2️⃣filters:- AddRequestHeader=X-Request-Id,${random.uuid}#3️⃣- RequestRateLimiter # 限流 4️⃣- CircuitBreaker # 熔断 5️⃣
编号 | 说明 |
---|---|
1️⃣ | lb://service —— 把“去哪儿”这件事交给 Spring Cloud LoadBalancer。Gateway 不会写死 IP 端口,而是拿着服务名 service 去注册中心拉实例,再按负载均衡算法挑一个真正的目标地址。这样下游扩缩容、节点上下线对网关完全透明。 |
2️⃣ | 只有命中 /api/** 的请求才会走进这条路由;其余请求会被其他 route 处理或者直接 404。 |
3️⃣ | AddRequestHeader 是一个 内置局部过滤器,给向下游转发的 HTTP 请求头里塞一个 UUID 作为 X-Request-Id 。整个调用链(网关→微服务→日志系统→链路追踪)都能拿到同一份 requestId,排查问题时按 ID 一搜到底。 |
4️⃣ | RequestRateLimiter (内置)对接 Redis,实现 令牌桶限流。 |
5️⃣ | CircuitBreaker (内置)用的是 Resilience4j。当下游实例异常比例或响应时间超过阈值时自动熔断,快速失败,保护后端。熔断结束后自动半开探测,恢复流量。 |
小结:一条路由把「加标→限流→熔断→转发」串成了一个 可观测、可保护、可弹性伸缩 的完整链路。
1.2.2 多维度限流策略
filters:- name: RequestRateLimiterargs:key-resolver: "#{@ipAndPathKeyResolver}" # 1️⃣redis-rate-limiter.replenishRate: 10 # 2️⃣redis-rate-limiter.burstCapacity: 20 # 3️⃣
编号 | 说明 |
---|---|
1️⃣ | 把限流的 Key 从默认的「单一 IP」升级成「IP + 请求路径」。同一个 IP 访问不同接口互不干扰,比粗暴的全局限流更精细。 |
2️⃣ | replenishRate = 10 :令牌桶每秒匀速补充 10 个令牌,也就是 平均 QPS 上限 10。 |
3️⃣ | burstCapacity = 20 :桶里最多攒 20 个令牌,允许突发流量一次性拿走 20 个,随后被匀速 10/s 限住。既能抗小尖峰,又不会把后端打挂。 |
@Component
public class IpAndPathKeyResolver implements KeyResolver {@Overridepublic Mono<String> resolve(ServerWebExchange exchange) {String ip = exchange.getRequest().getRemoteAddress().getHostString();String path = exchange.getRequest().getPath().value();return Mono.just(ip + path);}
}
- 为什么是 Mono?
- Gateway 运行时完全基于 Reactor;KeyResolver 接口设计为Mono,天然支持异步场景。例如后续你如果想把 IP 换成调用方 ID(需要查 Redis 或数据库),直接返回Mono.fromFuture(…) 即可,不会阻塞 EventLoop 线程。
- 返回值格式
- 这里简单地 ip + path,生产里可以拼成 ip:path 或 ip#path,只要保证 唯一且可读 即可。Redis 会拿这个字符串做key,过期时间与令牌桶窗口对齐。
落地效果
- 每个 /api/** 请求都会带上 X-Request-Id,日志、链路追踪、灰度回滚都靠它。
- 同一 IP 访问 /api/order 和/api/user 分别计数,互不抢占配额。
- 瞬时并发 20 以内秒过,超过 10/s 的持续流量会被匀速放行,后端不会被打穿。
- 下游实例故障时,熔断器 5 秒内打开,直接返回 503 / 自定义降级,保护系统。
二、动态路由配置:从静态到动态管理
2.1 基于Nacos的动态路由
2.1.1 动态路由配置示例
- Nacos配置文件(gateway-routes.yaml):
spring:cloud:gateway:routes:- id: product-service # 1️⃣uri: lb://product-service # 2️⃣predicates:- Path=/api/product/** # 3️⃣filters:- StripPrefix=1 # 4️⃣
编号 | 说明 |
---|---|
1️⃣ | 路由的唯一身份证,Gateway 内部用 id 做索引;以后想改这条路由,只需改同 id 的配置即可。 |
2️⃣ | 目标写成 lb://product-service ,Gateway 会拿服务名去 Nacos/Consul/Eureka 查实例,再按负载均衡挑一个真实 IP:Port。下游扩缩容无感知。 |
3️⃣ | 只有 URI 以 /api/product/ 开头的请求才会命中本路由;例如 /api/product/123 会被选中,而 /api/order/456 不会。 |
4️⃣ | StripPrefix=1 表示把路径里第一级 /api 去掉再转发,所以下游收到的是 /product/123 ,而不是 /api/product/123 。 |
把这段文件单独丢进 Nacos 配置中心,而不是写在本地 application.yml,是实现 “配置与代码分离” 的第一步。
- 网关服务加载配置:
spring:cloud:config:import: "optional:nacos:gateway-routes.yaml"
- optional:nacos: 前缀告诉 Spring Cloud Config:
- “启动时去 Nacos 拉一个叫
gateway-routes.yaml
的配置文件,如果拉不到也不报错(optional),继续启动。
- “启动时去 Nacos 拉一个叫
- ” 拉取成功后,Gateway 会把文件里的
spring.cloud.gateway.routes
覆盖/合并 到内存中的路由表,效果等同本地写死,但后期改路由无需重启网关。 - 动态刷新路由(手动触发):
curl -X POST http://localhost:8080/actuator/gateway/refresh
- Gateway 内置的 Actuator Endpoint。
- 当你在 Nacos 控制台改了
gateway-routes.yaml
(增删改路由)并发布后,执行这条命令即可让网关 秒级重新加载 路由表,无需重启进程。 - 生产环境可配合Nacos 的监听器自动调用此接口,实现 完全无人工介入 的热更新。
关于:【生产环境可配合Nacos 的监听器自动调用此接口,实现 完全无人工介入 的热更新】
最简单、最偷懒的做法:Nacos SDK 自带的「配置监听」+ 一行 HTTP 调用,10 行代码就能跑。
示例(Java,放在网关里即可):
@Component
public class NacosRouteRefreshListener {@Value("${spring.cloud.nacos.config.server-addr}")private String serverAddr;@Value("${spring.cloud.nacos.config.group:DEFAULT_GROUP}")private String group;@PostConstructpublic void startListen() throws NacosException {Properties p = new Properties();p.put(PropertyKeyConst.SERVER_ADDR, serverAddr);ConfigService configService = NacosFactory.createConfigService(p);// 1. dataId 与你在 Nacos 控制台里保持一致String dataId = "gateway-routes.yaml";// 2. 注册监听器configService.addListener(dataId, group, new Listener() {@Overridepublic Executor getExecutor() {return null; // 同步回调即可}@Overridepublic void receiveConfigInfo(String configInfo) {// 3. 配置变更时,自动刷新 Gateway 路由RestTemplate rest = new RestTemplate();rest.postForObject("http://localhost:8080/actuator/gateway/refresh",null,Void.class);log.info("路由已热刷新");}});}
}
效果: 在 Nacos 控制台改完 gateway-routes.yaml → 点击发布 → 监听器收到变更 → 自动 POST /actuator/gateway/refresh → 路由秒级生效,全程无人工介入。
2.2 动态路由优先级与冲突处理
2.2.1 路由优先级配置
spring:cloud:gateway:routes:- id: high-priority-routeuri: lb://service1predicates:- Path=/api/admin/**order: 0 # 优先级最高 1️⃣- id: low-priority-routeuri: lb://service2predicates:- Path=/api/**order: 100 # 2️⃣
编号 | 说明 |
---|---|
1️⃣ | order 值越小越优先。0 是最高优先级,因此凡是以 /api/admin/** 开头的请求都会先命中 high-priority-route ,直接转发到 service1 ;即使两条路由的 Path 有重叠,也不会落到第二条。 |
2️⃣ | order 默认是 2147483647 (int 最大值),这里显式写成 100 只是为了可读性:数字越大,优先级越低。 |
一句话总结:通过 order 字段可以像防火墙规则一样“插队”,保证管理后台、支付接口等关键路径永远先被匹配。
2.2.2 冲突处理策略
- 路径优先级:精确路径匹配优先于模糊匹配。
- 精确 > 前缀 > 通配。举例: /api/user/exact 同时满足
- Route A:Path=/api/user/exact(精确)
- Route B:Path=/api/**(前缀)
- → Gateway 会选Route A,因为它更精确。
- 精确 > 前缀 > 通配。举例: /api/user/exact 同时满足
- 谓词组合:使用
Host
谓词区分不同域名的路由,若路径本身无法区分,可再加其他谓词做“联合主键”
predicates:- Path=/api/** - Host=admin.xxx.com
这样:
admin.xxx.com/api/** → 管理后台
www.xxx.com/api/** → 用户前台
两条路由即使 Path 相同,也能通过域名隔离,互不影响。
一句话总结:先按 order 决定谁先被比较,再按“精确匹配 > 谓词组合”逐级兜底,确保任何情况下 只有一条路由真正生效,不会出现请求被“随机”转发的问题。
三、性能优化:从线程池到响应式编程
3.1 Netty线程池调优
3.1.1 线程池参数配置
- Gateway 底层是 Reactor-Netty,线程模型只有两类:
- Boss/Selector 线程:专门做连接事件轮询,不处理读写。
- Worker 线程:负责 真正的 I/O 读写、编解码、用户业务回调。
- 默认策略对网关这种 纯 I/O、轻业务场景过于保守,因此需要手动放大 Worker 池。
@Bean
public ReactorResourceFactory reactorResourceFactory() {ReactorResourceFactory factory = new ReactorResourceFactory();// ① 选择器线程:1 条即可管理上万并发,CPU 消耗极低System.setProperty("reactor.netty.ioSelectCount", "1");// ② Worker 线程:CPU 核数 × 3,充分利用多核做并行读写int worker = Runtime.getRuntime().availableProcessors() * 3;System.setProperty("reactor.netty.ioWorkerCount", String.valueOf(worker));return factory;
}
- 必须在首次使用 Netty 前调用,否则默认值(核数)已生效,再改无效。
- 选择器线程 不要多开,多开会带来上下文切换损耗; Worker 线程 也不要无上限,经验值 ≤ 核数×3,可避免线程饥饿且 CPU 不空转。
3.1.2 压测结果对比
配置 | QPS | 错误率 |
---|---|---|
默认配置 | 8000 | 5% |
优化后配置 | 12000 | 0.5% |
- QPS ↑ 50 %:Worker 数量翻倍后,I/O 任务并行度提升,CPU 利用率从 60 % → 90 %。
- 错误率 ↓ 90%:队列和等待时间缩短,超时/拒绝大幅减少。
结论: 在 Gateway 这种 I/O 密集、业务极轻 的场景下,把 Worker 线程调到 CPU 核数 × 3 是最具性价比的优化手段,只需两行系统属性即可带来 吞吐量 + 稳定性 的双提升。
3.2 响应式编程优化
3.2.1 异步非阻塞处理
@Component
public class AsyncFilter implements GlobalFilter {@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {return Mono.fromSupplier(() -> {// 非阻塞业务逻辑return exchange;}).flatMap(chain::filter);}
}
关键点 | 说明 |
---|---|
Mono.fromSupplier | 把可能耗时的 初始化/校验/日志 逻辑封装成 Supplier,Reactor 会在 弹性线程池 中异步执行,不占用 Netty 的 IO Worker。 |
flatMap | 结果返回后 再衔接 后续过滤器链,全程 无阻塞,整个链路始终保持在 事件驱动 模式。 |
任何 CPU 密集或第三方调用,都可以用 fromSupplier/fromCallable 扔进弹性线程,让 IO Worker 继续处理下一个连接
3.3 缓存与压缩
3.3.1 Redis缓存响应
@Component
public class CacheResponseFilter implements GlobalFilter {private final ReactiveRedisTemplate<String, String> redisTemplate;@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {String key = exchange.getRequest().getPath().value();return redisTemplate.opsForValue().get(key).flatMap(response -> {if (response != null) {exchange.getResponse().setStatusCode(HttpStatus.OK);return exchange.getResponse().writeWith(Mono.just(DataBufferUtils.wrap(response.getBytes())));}return chain.filter(exchange).then(Mono.fromRunnable(() -> {// 缓存响应redisTemplate.opsForValue().set(key, responseBody);}));});}
}
关键点 | 说明 |
---|---|
缓存 Key | 用 请求路径 当 key,简单暴力;生产可再加 Accept-Language /version 等维度防止串包。 |
读缓存 | get(key) 是 非阻塞 Reactive 操作,命中则直接回写响应,后端 0 调用。 |
写缓存 | 在 then() 中异步 回填 Redis,写操作同样不阻塞当前线程。 |
效果 | 命中率 50 % 就能把后端 QPS 打对折,RT 从 120 ms → 5 ms。 |
把「读多写少」且「变化不频繁」的接口直接 边缘缓存,网关自己扛流量,后端安心睡觉。
3.3.2 Gzip压缩配置
spring:cloud:gateway:routes:- id: gzip-routeuri: lb://servicepredicates:- Path=/api/large-datafilters:- Gzip
关键点 | 说明 |
---|---|
Gzip 过滤器 | 网关内置,自动检测 Accept-Encoding: gzip ,对响应体进行 流式压缩;浏览器收到后自动解压。 |
适用场景 | 返回大 JSON / CSV / 静态文本 的接口,压缩率通常 70 % 以上,带宽省一半。 |
代价 | CPU 消耗增长 2 %~5 %,但现代 CPU 压缩速度 > 1 GB/s,基本可以忽略。 |
省带宽就是省钱。对 /api/large-data 这类接口,上线 Gzip 后出口流量瞬间腰斩,用户首包时间也能快一倍。
- 终极三连击顺序
- AsyncFilter 让业务逻辑不卡 IO 线程。
- CacheResponseFilter 把热点数据拦在Redis,后端 QPS 直接打骨折。
- Gzip Filter 再把剩余流量压成压缩包,出口带宽腰斩。
- 三步下来,单机 QPS 可提升 2~3 倍,带宽成本下降 50 % 以上。
四、分布式追踪与监控集成
4.1 分布式追踪实现-让请求留下“脚印”
4.1.1 集成Spring Cloud Sleuth
在 pom.xml 加一行依赖:
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
再yaml文件配两个参数:
spring:sleuth:sampler:probability: 1.0 # 全量采样zipkin:base-url: http://localhost:9411
- Sleuth 做了什么?
- 每进来一个请求,它就自动生成一个 TraceId(全链路唯一),并把这个 ID 一路往下传。
- Zipkin又是什么?
- 一个“脚印收集站”,把 TraceId、耗时、服务名都存起来,供你搜索、画图、定位慢接口。
4.1.2 追踪日志关联(防丢脚印)
@Component
public class TraceFilter implements GlobalFilter {private static final String TRACE_ID_HEADER = "X-B3-TraceId";@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {String traceId = exchange.getRequest().getHeaders().getFirst(TRACE_ID_HEADER);if (traceId == null) {traceId = UUID.randomUUID().toString();exchange.getRequest().mutate().header(TRACE_ID_HEADER, traceId).build();}return chain.filter(exchange);}
}
- 如果外部系统(如前端或老系统)没传 TraceId,我们就在网关给它补一个,保证 “从网关开始,整条链路都有脚印”。
- 以后排查问题,只需把这个 TraceId 粘进 Zipkin 搜索框,就能看到 请求在网关→A→B→C 各花了多少毫秒。
4.2 监控指标暴露
4.2.1 集成Prometheus
再加一个依赖:
<dependency><groupId>io.micrometer</groupId><artifactId>micrometer-core</artifactId>
</dependency>
配置:
management:endpoints:web:exposure:include: "*"metrics:export:prometheus:enabled: true
- Micrometer 是“翻译官”,把网关的运行数据(QPS、RT、错误率、线程数…)翻译成 Prometheus 能看懂的格式。
- Prometheus 每 15 秒来拉一次数据,存成时间序列,方便后续画图或报警。
4.2.2 Grafana可视化
-
添加Prometheus数据源:配置Prometheus地址。
-
导入Gateway监控模板:使用ID为
12345
的模板展示QPS、错误率等指标。
a. 红色折线:每秒请求量
b. 黄色折线:平均响应时间
c. 绿色/红色方块:成功率/失败率 -
小白看图口诀:
- 折线往上飙 → 流量来了
- 黄色线变长 → 接口变慢
- 红色方块出现 → 有错误,一键跳 Zipkin 看具体 TraceId
一句话总结 Sleuth 负责“画脚印”,Zipkin 负责“存脚印”,Prometheus + Grafana 负责“体检表”。把这三件套配好,以后任何请求变慢或报错,都能在 30 秒内定位到是哪台机器的哪一行代码在拖后腿。
五、熔断与限流:服务稳定性保障
5.1 Resilience4j熔断配置
5.1.1 依赖添加
<dependency><groupId>io.github.resilience4j</groupId><artifactId>resilience4j-spring-boot2</artifactId>
</dependency>
Resilience4j 就是官方推荐的“轻量级保险丝”,比老牌的 Hystrix 更简单、更快。
5.1.2 熔断策略配置
保险丝参数:
resilience4j:circuitbreaker:instances:service: # 给下游服务取个名字failureRateThreshold: 50 # 1️⃣waitDurationInOpenState: 10s # 2️⃣permittedNumberOfCallsInHalfOpenState: 5 # 3️⃣
参数 | 大白话 |
---|---|
1️⃣ failureRateThreshold: 50 | 最近 N 次调用里,失败率 ≥ 50 % 就跳闸(保险丝烧断)。 |
2️⃣ waitDurationInOpenState: 10s | 跳闸后等 10 秒,再进入「半开」状态:允许 5 个探针请求去试试水。 |
3️⃣ permittedNumberOfCallsInHalfOpenState: 5 | 半开时最多放 5 只“小白鼠”进去,如果都成功就恢复通车,否则继续跳闸。 |
把保险丝绑到路由上:
spring:cloud:gateway:routes:- id: service-routeuri: lb://servicepredicates:- Path=/api/servicefilters:- name: CircuitBreakerargs:name: service # 对应上面配置里的 servicefallbackUri: forward:/fallback # 4️⃣
4️⃣ 一旦保险丝跳闸,Gateway 不会傻傻地继续转发,而是 直接把请求转到
/fallback(你可以返回“服务繁忙,请稍后重试”或缓存数据),保护后端也保护用户体验。
5.2 限流与熔断结合
filters:- name: RequestRateLimiterargs:key-resolver: "#{@ipKeyResolver}" # 按 IP 限流redis-rate-limiter.replenishRate: 10 # 每秒 10 个令牌- name: CircuitBreakerargs:name: service
- 限流是「水龙头」:每个 IP 最多 10 次/秒,先把洪峰削掉;
- 熔断是「保险丝」:如果后端已经生病(超时/异常),就算流量不超也会立刻跳闸,防止雪崩。
一句话总结:限流先把大流量挡住,熔断再把坏服务隔离;两者一起上,后端想挂都难,用户看到的永远是“要么秒回,要么友好提示”。