接手网关项目那会儿,我最早啃的就是 Spring Cloud Gateway 的路由规则。说实话,路由规则写起来真不难,难的是写完以后线上突然给你抛一堆 502 Bad Gateway,你根本分不清是路由没匹配上、上游服务挂了,还是注册中心没实例。尤其是最近不少人反馈类似unexpected status 502 bad gateway: unknown error, url: http://127.0.0.1:15721/v1/responses这种报错,一看就是网关把请求转发到了一个本机端口,结果对端根本没服务在监听。
这篇东西我打算把 Spring Cloud Gateway 路由规则彻底拆开讲,从 Route、Predicate、Filter 三个核心概念讲起,再给一套能直接抄的配置方案,最后重点复盘 502、405、路由不生效这些高频问题。适合刚接手网关、被路由规则搞到头大、或者已经在线上被 502 折磨过的同学,把文章看完基本能自己定位大部分问题。
1. 路由规则的整体设计与配置思路
网关这东西,说白了就是所有流量的总入口。Spring Cloud Gateway 整条链路的核心就三个词:Route、Predicate、Filter。很多人一开始觉得路由规则复杂,其实是被这三个概念的组合方式绕晕了。
1.1 核心三件套:Route、Predicate、Filter
我习惯拿快递站做类比。
- Route(路由):一条快递线路。一个网关里可以配很多条 Route,每条路由都有唯一的
id,代表“什么请求走哪条线路”。 - Predicate(谓词/断言):快递面单上的收件条件。它负责判断“这个请求满不满足条件”,比如路径是不是
/api/user/**、请求方法是不是 GET、Header 里有没有指定 token。满足条件才会走这条路由。 - Filter(过滤器):运输途中的处理工序。请求匹配到路由后,会经过一系列过滤器,比如把前缀
/api剥掉、给请求头加东西、做鉴权、限流,都在这一层完成。
一次完整的请求流程是这样:客户端请求进来 -> 遍历所有 Route -> 用 Predicate 依次匹配 -> 匹配成功则走该 Route -> 执行 Filter 链 -> 转发到下游服务 -> 响应原路返回。
理解了这个流程,后面写路由就不会乱。你只需要明确三件事:这个请求长什么样(Predicate)、转发到哪里去(URI)、中间要做什么处理(Filter)。
1.2 为什么用 Spring Cloud Gateway 而不是 Zuul 1.x
很多人会纠结选型。Zuul 1.x 是基于 Servlet 的同步阻塞模型,每个请求占用一个线程,线程池一满,整个网关就卡死。Spring Cloud Gateway 基于 Spring WebFlux 和 Netty,是异步非阻塞模型,用少量线程撑住大量并发连接,性能上限高得多。
单从路由规则的角度看,Gateway 的配置也更灵活。它的 Predicate 和 Filter 都是独立的、可组合的组件,你想加一个“每周一上午 10 点且 Header 里带 version=v2 的请求才走灰度路由”,用几行配置就能拼出来。Zuul 1.x 做这种组合判断要写不少自定义代码。
另外 Spring Cloud Gateway 原生支持与注册中心(Nacos、Eureka)集成,URI 用lb://service-name就能按服务名负载均衡,不用手写服务发现逻辑,这对微服务架构来说太重要了。
注意:Spring Cloud Gateway 基于 WebFlux,项目里不要同时引入
spring-boot-starter-web,否则会冲突,启动时各种莫名其妙的报错。我见过不少同事在这里翻车。
1.3 静态配置与 Java DSL 两种方式怎么选
路由规则的配置方式主要有两种。
第一种是application.yml 静态配置,适合路由规则相对固定的场景。上线后如果改了路由,需要重启网关才能生效,优点是直观、好维护、新人上手快。
第二种是Java DSL 动态配置,通过RouteLocatorBuilder编写@Bean方法来定义路由。这种方式的好处是可以结合配置中心(Nacos Config、Apollo),把路由规则做成可动态刷新的配置,网关不用重启就能调整路由。适合路由规则频繁变化、或者需要按环境灵活调整的场景。
实际项目里我通常是两个混用:固定的、公共的路由走 yml 配置,业务迭代快的、需要灰度控制的走 Java DSL,配合配置中心做动态刷新。
@Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { return builder.routes() .route("order-service-route", r -> r .path("/api/order/**") .filters(f -> f.stripPrefix(1)) .uri("lb://order-service")) .build(); }这段代码的意思是:所有/api/order/**的请求,先剥掉第一级前缀/api,再负载均衡转发到order-service。
2. 核心细节解析:Predicate 与 Filter 的实操要点
配置语法好背,真正决定路由规则好不好用的是对 Predicate 和 Filter 细节的理解。下面这几个点是我用得最多、也最容易出错的。
2.1 高频路由谓词逐个拆解
先看一张我整理的常用谓词对照表,后面会有详细示例。
| Predicate | 作用 | 示例 |
|---|---|---|
| Path | 按请求路径匹配,支持通配符**和占位符{} | Path=/api/user/** |
| Method | 按 HTTP 方法匹配 | Method=GET,POST |
| Header | 按请求头匹配,支持正则 | Header=X-Request-Id, \d+ |
| Query | 按查询参数匹配,支持正则 | Query=page, \d+ |
| Cookie | 按 Cookie 匹配,支持正则 | Cookie=sessionId, [a-f0-9]+ |
| Host | 按 Host 请求头匹配 | Host=**.example.com |
| RemoteAddr | 按客户端 IP 匹配 | RemoteAddr=192.168.1.1/24 |
| After/Before/Between | 按时间匹配 | After=2025-01-01T00:00:00+08:00[Asia/Shanghai] |
| Weight | 按权重分流,用于灰度发布 | Weight=group1, 8 |
Path 谓词是最常用的。/api/user/**能匹配/api/user、/api/user/1、/api/user/100/orders,但匹配不了/api/users,因为**匹配的是零个或多个路径段,而不是模糊匹配任意字符串。这个细节很多人搞混。
Header 谓词常用于版本控制。比如我想让只有带X-Version: v2的请求走新服务:
predicates: - Path=/api/product/** - Header=X-Version, v2这里多个谓词之间是AND 关系,必须全部满足才会匹配这条路由。
Weight 谓词做灰度分流很好用。比如 80% 流量走稳定版,20% 走灰度版,两条路由用同一个 group,权重不同:
- id: product-route-v1 uri: lb://product-service-v1 predicates: - Path=/api/product/** - Weight=product-group, 8 - id: product-route-v2 uri: lb://product-service-v2 predicates: - Path=/api/product/** - Weight=product-group, 22.2 谓词组合顺序的坑,配错直接路由不生效
路由规则的匹配是从上到下按顺序执行的,第一个匹配到的 Route 生效,后面的就不再看了。这个特性既是优势也是坑。
举例说明,假设我配了两个路由:
spring: cloud: gateway: routes: - id: user-route uri: lb://user-service predicates: - Path=/api/user/** - id: user-detail-route uri: lb://user-detail-service predicates: - Path=/api/user/detail/**如果user-route写在前面,/api/user/detail这个请求会被user-route匹配到,转发到user-service,而你想走精细化接口的user-detail-route永远不会生效,因为请求根本走不到它。
所以写路由规则的铁律是:精确路径的 Route 放在前面,宽泛路径的 Route 放在后面。把/api/user/detail/**提到/api/user/**前面,才符合预期。
另外还要注意 yml 里路由的id必须唯一,重复id会导致启动时路由注册异常,或者后面配置覆盖前面的路由,这种问题排查起来特别隐蔽。
2.3 全局过滤器与局部过滤器怎么选
Filter 分为两种:GatewayFilter(局部过滤器)和GlobalFilter(全局过滤器)。
局部过滤器只对某一条路由生效,常见的有:
StripPrefix=1:转发前剥掉路径里的第一级前缀RewritePath:用正则重写路径AddRequestHeader:给请求头添加参数AddResponseHeader:给响应头添加参数Retry:下游失败时自动重试RequestRateLimiter:限流
全局过滤器对所有路由生效,适合做统一鉴权、日志记录、灰度标记。自定义一个全局过滤器很简单:
@Component public class AuthGlobalFilter implements GlobalFilter, Ordered { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String token = exchange.getRequest().getHeaders().getFirst("Authorization"); if (token == null || token.isEmpty()) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } return chain.filter(exchange); } @Override public int getOrder() { return -100; } }getOrder()返回值越小,过滤器越先执行。做鉴权的过滤器 Order 要设得比转发类过滤器小,确保请求在转发前就被拦截。这里返回 401 的场景,正好对应很多人遇到的 token missing / unauthorized 问题。
2.4 StripPrefix 与 RewritePath:路径转换的常规操作
写路由规则时最容易出错的就是路径前缀处理。比如前端请求/api/user/1,你希望网关剥掉/api,再把/user/1转发给 user-service,那就用StripPrefix=1。
filters: - StripPrefix=1StripPrefix=1表示去掉第一级路径段。如果请求是/api/v1/user/1,你想去掉/api/v1,就用StripPrefix=2。
RewritePath更灵活,可以按正则重写。比如把/api/user/1重写成/user/1:
filters: - RewritePath=/api/(?<segment>.*), /$\{segment}注意$\{segment}里的$要加反斜杠转义,否则 yml 会把它当变量解析,写错以后路径重写不生效,报错还特别难查。
我自己的习惯是:只要路径前缀规则统一,优先用StripPrefix;如果不同服务的路径规范不一致,再用RewritePath做精细化改写。两者不要混用,不然路由逻辑会变得很难维护。
3. 实操过程:从零配置一个带路由规则的网关
理论说完了,直接上实操。我用 Spring Boot 2.6.13 + Spring Cloud 2021.0.5 + Spring Cloud Alibaba 2021.0.5.0 + Nacos 2.x 这套组合演示。
3.1 版本选型与环境准备
版本匹配是网关项目的第一道坎。Spring Cloud Gateway 的版本跟着 Spring Cloud 走,Spring Boot 和 Spring Cloud 的版本必须对应,乱配版本会导致启动直接失败。
我这里给一个经过验证的组合:
| 组件 | 版本 |
|---|---|
| JDK | 1.8 以上 |
| Spring Boot | 2.6.13 |
| Spring Cloud | 2021.0.5 |
| Spring Cloud Alibaba | 2021.0.5.0 |
| Nacos Server | 2.2.0 以上 |
注意:这个组合只做参考,实际项目请根据你现有的技术栈统一规划。升级版本前先查官方版本对应关系,不要凭感觉升级。
3.2 核心依赖配置
创建项目后,pom.xml里核心依赖如下:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>关键点:不要加spring-boot-starter-web。Gateway 基于 WebFlux,加了 web starter WebApplicationType 会冲突,典型报错是Spring MVC found on classpath, which is incompatible with Spring Cloud Gateway。
需要动态刷路由的话,再加:
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency>3.3 一份可直接复制的完整路由配置
下面是application.yml核心片段。这个配置包含了服务发现、路由规则、过滤器和超时设置,能覆盖大多数项目的需求。
server: port: 8080 spring: application: name: api-gateway cloud: nacos: discovery: server-addr: 127.0.0.1:8848 gateway: routes: - id: user-service-route uri: lb://user-service predicates: - Path=/api/user/** filters: - StripPrefix=1 - id: order-service-route uri: lb://order-service predicates: - Path=/api/order/** filters: - StripPrefix=1 globalcors: cors-configurations: '[/**]': allowedOrigins: "*" allowedMethods: - GET - POST - PUT - DELETE allowedHeaders: "*" httpclient: connect-timeout: 3000 response-timeout: 5suri: lb://user-service是关键。它表示从注册中心按服务名user-service获取实例,再做负载均衡。如果你没有注册中心,直接用uri: http://127.0.0.1:8081这种写死地址的方式也行,但这样就没法享受服务发现的弹性扩缩容了。
httpclient超时配置也要重视,默认连接超时是 45 秒,对很多接口来说太长了。一旦下游服务假死,请求会长时间挂住,拖垮整个网关。我一般把连接超时和响应超时都调小,再配合重试策略。
3.4 与 Nacos 注册中心结合的路由规则
路由规则里lb://前缀是 Spring Cloud Gateway 内置的负载均衡协议。它会把服务名解析成注册中心里的实例列表,然后按负载均衡策略选择一个实例进行转发。
实际操作中要检查两件事:
- Nacos 服务列表里有没有目标服务,服务名必须和
lb://后面的名字完全一致,大小写敏感。 - 目标服务的实例状态是否健康,Nacos 控制台能直接看到实例的健康状态。
如果这两个条件不满足,路由规则写得再漂亮也没用,请求转发时必然报错,最常见的就是 503 Service Unavailable 或者 502 Bad Gateway。
3.5 启动验证与网关未启动的区分技巧
配置写完,启动网关后先用最简单的请求验证:
curl http://localhost:8080/api/user/1如果返回了 user-service 的数据,说明路由规则生效了。
这里要特别提醒:很多人遇到本机连不上网关,先别急着怀疑路由规则。先确认网关进程到底有没有起来:
curl http://localhost:8080/actuator/health如果连接被拒绝,那是网关没启动,不是路由问题。这是两个完全不同的排查方向,前者看进程,后者看配置。网上很多报错就是“gateway 未启动”或者“端口连接失败”,这种跟 502 完全不同,别混在一起查。
4. 502 Bad Gateway 排查实录:从路由规则到服务实例的完整链路
502 Bad Gateway 是网关场景里出现频率最高的报错,也是最容易让人误判的。我单独拿一章来讲,毕竟这个坑实在太多。
4.1 502 的本质:请求到网关了,但上游没接住
502 的语义是:网关已经收到了客户端的请求,也根据路由规则找到了目标地址,但在转发时没有从上游服务拿到有效响应。简单说,请求死在从网关到下游服务的那一段路上。
常见的报错形式主要有两种。一种是摘要式的unexpected status 502 bad gateway,另一种带具体转发地址,比如unknown error, url: http://127.0.0.1:15721/v1/responses。后一种其实已经指明了转发目标,排查范围直接缩小到这台机器和这个端口。
这时候最直接的验证手段是:
curl -v http://127.0.0.1:15721/v1/responses如果本机 curl 也报连接失败,说明下游端口根本没有服务监听;如果本机能通但网关转发 502,那就要查网络隔离、防火墙或者网关所在节点到目标节点的连通性。
4.2 最容易踩的坑:URI 写死 localhost 或固定 IP
我见过最多的 502 案例,根源都在 URI 配错。有人把uri写成了http://localhost:8081,这在网关部署在容器里时特别致命——容器里的 localhost 指向的是容器自己,不是宿主机,更不是下游服务所在的机器。
还有一种是把uri写成某个固定 IP,比如http://127.0.0.1:15721。如果下游服务是多实例部署,或者服务迁移过机器,这个 IP 就是错的。这也是为什么生产环境强烈建议用lb://service-name代替手写地址。
排查思路是:拿到报错里的 url,先确认这个地址是不是当前合理的服务地址,再确认这个地址从网关所在节点能否访问。三步走:
# 1. 看端口是否在监听 netstat -an | grep 15721 # 2. 测端口连通性 telnet 127.0.0.1 15721 # 3. 直接请求目标接口 curl -v http://127.0.0.1:15721/v1/responses4.3 服务发现链路导致的 502/503
如果用了lb://方式,502 的排查重点就转向服务发现链路。
常见问题有:下游服务没注册到 Nacos、服务名写错(注意大小写)、服务实例全部下线、注册中心网络隔离导致网关拉不到最新实例列表。
Nacos 控制台的“服务列表”页面能直接看到服务名和实例健康状态。另外要留意,如果下游服务启动时注册失败,但进程没退出,网关可能拿到一个不可用的实例地址,转发过去就是连接超时或拒绝连接,最终表现为 502。
还有一个隐蔽场景:服务注册了,但端口配错。服务实际监听 8081,注册到 Nacos 的端口却是 8082,网关拿到的地址就是错的。这种问题在多人协作、环境变量不一致时经常出现。
4.4 超时导致 502 的定位方法
超时也会表现为 502。如果下游服务处理很慢,超过网关配置的response-timeout,网关就会主动断开并返回 502。
判断是不是超时问题,看日志最直接。Gateway 的日志里通常会有TimeoutException: null或者Read timed out之类的字样。
解决办法分两层:
- 网关层调大超时时间,但这是治标不治本。
- 下游服务优化接口性能,这才是根本。
如果接口确实需要长时间处理,建议改成异步任务 + 轮询结果,而不是让网关干等。网关这个位置很特殊,长连接会占用线程和连接池资源,超时调得越大,资源被拖死的风险越高。
4.5 连接池耗尽导致的 502
Spring Cloud Gateway 使用 HttpWebClient 发起转发,底层是 Netty 连接池。高并发场景下,如果连接池被耗尽了,新的请求无法获取连接,也会报 502。
连接池耗尽的典型现象是:网关 CPU 和内存都不高,但大量请求 502,日志里有Connection pool exhausted或者Pool acquired timeout。
处理方法:
- 调大
spring.cloud.gateway.httpclient.pool.max-connections,默认是几百,可以根据压测结果调大。 - 调大
max-idle-time,让空闲连接存活更久,减少频繁建立连接的损耗。 - 优化下游服务的响应速度,这是最根本的。
4.6 502 排查清单速查表
这个表我打印出来贴在工位上过,排查时挨个查,效率很高。
| 排查项 | 命令/工具 | 判断标准 |
|---|---|---|
| 网关进程是否正常 | curl http://localhost:8080/actuator/health | 返回 JSON 且 status=UP |
| 路由是否匹配 | GET /actuator/gateway/routes | 能看到当前生效的所有 Route |
| 目标服务端口监听 | netstat -an | grep 15721 | 端口处于 LISTEN 状态 |
| 路径连通性 | curl -v http://127.0.0.1:15721/v1/responses | 能正常返回业务响应 |
| 注册中心状态 | Nacos 控制台服务列表 | 实例健康,服务名与 lb 后一致 |
| 超时配置 | application.yml 中 httpclient 配置 | 超时时间是否过小 |
| 连接池 | 网关日志 | 有无Connection pool exhausted |
| 防火墙/网络策略 | telnet <IP> <端口> | 连接能建立成功 |
5. 高频报错速查与路由不生效的排查技巧
除了 502,网关项目里还有几个报错频率极高,我也一起整理出来,尤其是 405 和路由不生效,遇到的人特别多。
5.1 405 Method Not Allowed:请求到了,但方法不对
405 的意思是请求已经到达了目标服务,但目标服务不允许这个 HTTP 方法。典型场景是下游接口只支持 POST,你用 GET 去调;或者网关把 GET 转发过去了,下游 Spring MVC 接口没有对应的 RequestMapping。
对接 SAP 网关时尤其常见。SAP 的服务接口对 HTTP 方法和 Content-Type 要求很严格,方法不对、请求头不对,直接返回 405。
排查方法分三步:
- 确认请求方法和下游接口方法定义是否一致。
- 确认加了
StripPrefix或RewritePath后,实际转发给下游的路径是否正确。方法没变但路径变了,可能打到一个根本不存在的接口,返回 404 或 405。 - 直接绕过网关,用 curl 请求下游服务原地址,看是否同样 405。如果直连正常、走网关才 405,问题在网关的转发逻辑或过滤器,比如某个过滤器改写了请求方法或 Content-Type。
5.2 路由不生效的 5 个常见原因
路由规则写了但请求没按预期走,这类问题我总结了 5 个原因。
第一,Route 顺序错误。宽泛路径放在精确路径前面,导致精确路由永远匹配不到。调整顺序即可。
第二,StripPrefix 参数不对。前端路径是/api/user/1,下游接口是/user/1,如果 StripPrefix 配成 0 或 2,转发路径就不对。
第三,YAML 缩进错误。Spring Cloud Gateway 的配置层级很深,routes要在spring.cloud.gateway下面,多一个空格或少一个空格都可能导致配置没被加载。判断方法是用/actuator/gateway/routes看实际生效的路由列表。
第四,配置缓存没刷新。尤其在 Nacos Config 动态配置场景下,改了配置但没触发刷新,网关还是一直用旧路由。
第五,谓词条件太严格。比如配置了Header=X-Version, v2,但请求里没带这个 Header,路由就匹配不上。可以用 curl 加上对应 Header 验证。
curl -H "X-Version: v2" http://localhost:8080/api/product/15.3 网关统一鉴权与 401/token missing 问题
前面全局过滤器里写过,如果网关做了统一鉴权,请求没有携带 token 或者 token 校验失败,网关会直接返回 401。很多人把这种报错归结为“路由问题”,其实方向偏了。
排查顺序应该是:
- 请求有没有带
Authorization头。 - 网关的鉴权过滤器有没有生效,它返回的响应体是不是你预期的 401。
- 如果直连下游服务正常、走网关 401,那 100% 是网关的全局过滤器在拦截。
这里要特别注意GlobalFilter的getOrder()。如果你的鉴权过滤器 Order 大于转发过滤器,可能先转发了,后面才校验,顺序就乱了。建议鉴权过滤器 Order 设为负数,优先级最高。
5.4 用 Actuator 端点快速排查网关状态
排查网关问题时,Actuator 是我最先用的工具。先在pom.xml引入依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>然后在application.yml开放 gateway 端点:
management: endpoints: web: exposure: include: gateway,health,info之后三个端点很关键:
# 查看当前所有路由规则 curl http://localhost:8080/actuator/gateway/routes # 查看全局过滤器 curl http://localhost:8080/actuator/gateway/globalfilters # 查看路由过滤器工厂列表 curl http://localhost:8080/actuator/gateway/routefilters/actuator/gateway/routes是排查路由不生效的第一利器。如果配置了但这里看不到,说明配置根本没加载;如果这里能看到但是转发错误,说明问题在后面的 URI 或 Filter。
5.5 几个实用排查命令
最后分享几个我日常用的命令,都是压箱底的。
# 查看网关日志中与路由相关的信息 tail -f gateway.log | grep "RouteToRequestUrlFilter" # 查看日志中实际转发的目标地址 tail -f gateway.log | grep "outbound" # 查看注册中心实例列表 curl "http://127.0.0.1:8848/nacos/v1/ns/instance/list?serviceName=user-service"日志里outbound开头的关键字会打印网关实际转发的地址,配合这个能快速确认转发目标是否符合预期。路由规则排障,我总结下来就是一句话:先确认路由有没有匹配到,再确认转发地址对不对,最后看下游服务接没接住,按这个顺序走,大部分问题十分钟内能定位。
我个人在实际操作中还有一个习惯:每个路由规则写完,都要用 curl 带相关请求头跑一遍,确认通路之后再上线。网关是流量的总闸口,一点马虎都可能酿成线上事故。把路由规则当成一门严谨的手艺来对待,你会在后续的排障中省下大量时间。