ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Spring Cloud Gateway路由规则配置与排障实践指南

Spring Cloud Gateway路由规则配置与排障实践指南 做微服务网关的人大概率都有过被路由规则支配的经历。规则写错一个路径线上流量就跑到别的地方去了谓词顺序调一下某些请求就突然 404。我接触 Spring Cloud Gateway 大概是从 2.x 版本开始的中间经历过 Zuul 1.x 迁移到 Gateway 的过程也在生产环境里被各种路由配置坑过无数次。今天想把这段关于路由规则的实践经验完整整理出来从最基础的配置写法到谓词和过滤器的组合技巧再到动态路由的实现最后把 502 这类经典故障的排查思路一次讲透。这篇文章适合谁看正在用 Spring Cloud Gateway 做微服务网关的同学尤其是刚接手网关配置、被 YAML 里那堆 predicates 和 filters 绕晕的人准备把静态路由改成动态路由、让配置中心接管规则的人以及正在排查线上路由相关问题、想少踩几个坑的人。内容不涉及玄乎的高深理论但每一步都是我在实际项目里验证过的可以直接照着用。1. 路由规则核心概念与整体设计思路1.1 三个核心组件Route、Predicate、FilterSpring Cloud Gateway 的官方文档里一个路由Route由三部分组成id路由唯一标识、predicate断言决定请求是否匹配这条路由、filter过滤器对匹配上的请求做加工。这三者的关系我习惯用“门卫加安检通道”来类比。Route 就是一条通道告诉网关请求往哪转发。Predicate 是门卫按各种条件判断这个请求能不能走这条通道。Filter 是安检和服务人员在请求转发之前、拿到响应之后做处理。这个类比在排查问题时非常实用。比如一个请求没走到预期的服务我第一反应就是看 predicate 是不是没匹配上如果匹配上了但响应不对就继续追 filter 里动了什么手脚。Predicate 的匹配条件非常丰富官方内置了十多种Path路径、Method请求方法、Header请求头、Query查询参数、Cookie、Host、RemoteAddr、Weight 等等。这些谓词可以单个用也可以组合使用。默认情况下多个谓词之间是 AND 关系必须全部满足路由才会生效。这一点在配置时必须想清楚别指望“满足其中一个就行”。从设计哲学上看Gateway 把“匹配条件”和“处理动作”拆成两层而不是像老一代网关那样把逻辑写死在代码里就是为了让规则具备高度的可配置性。业务团队要接入一个新服务通常不用改代码改配置文件就能完成。这也是 Spring Cloud Gateway 能在微服务架构里快速普及的原因。1.2 为什么路由规则要先想清楚边界很多人上手 Gateway第一件事就是抄一段 YAML把服务路径写上能通就行。但真正上了生产你会发现路由规则的设计直接决定故障半径。比如你把/api/**一刀切转发到一个服务上后面另一个服务需要暴露接口时就只能再加一条更具体的路由。如果顺序没排好流量就被前面的宽泛路由劫持了。我个人的经验是动手写配置之前先回答三个问题。第一路径前缀怎么规划是统一带/api前缀还是每个服务自带一级路径这决定了后面StripPrefix该配几层。第二哪些服务需要暴露到网关外部哪些只做内部调用内部调用的服务就不该配置路由甚至要从注册中心层面做隔离。第三有没有跨服务、跨版本的特殊需求比如灰度发布要按 Header 分流这类需求从一开始就要在谓词设计上留好位置否则后面加规则会非常痛苦。这些问题想清楚再往下写条理会清晰很多。路由规则从来不只是“能转发就行”它本质上是你整个微服务系统对外边界的一份契约。边界越清晰线上出问题的概率越低。2. 路由规则的配置方式与关键参数2.1 YAML 配置最常用的静态路由写法Gateway 的路由规则最常见的落地方式就是写在 application.yml 里。一个最基础的路由配置长这样spring: cloud: gateway: routes: - id: order-service-route uri: lb://order-service predicates: - Path/api/order/** filters: - StripPrefix1这里有几个关键点逐个拆开说。id是路由的唯一标识虽然不参与转发逻辑但日志里会用到。排查问题时你能从日志里看到每条请求命中了哪条路由所以 id 的命名最好做到“见名知义”。我一般用“服务名-业务场景-序号”的格式比如order-service-v1-route。uri是目标地址。lb://order-service表示走注册中心负载均衡从服务列表里拿实例。如果服务没接入注册中心也可以直接写http://127.0.0.1:8080这种固定地址。很多朋友问的“网关配置路由转发固定链接地址”其实就是把 uri 写成完整 URL。predicates是匹配条件。Path/api/order/**表示路径以/api/order/开头的请求都会命中。这里的**值得多说一句它匹配任意层级包括多级路径而*只匹配单级路径。这两个的区别是初学阶段最容易搞混的地方。filters是对命中后的请求做处理。StripPrefix1表示剥离掉第一级路径前缀再转发。比如请求是/api/order/detail剥掉/api后后端服务实际收到的是/order/detail。这个参数值取决于你的路径规划。如果统一前缀是/api后端接口本身就带/order那 StripPrefix1 就对了如果后端接口路径里也带/api那就别配 StripPrefix直接转发。2.2 Java DSL 方式适合复杂动态场景YAML 能覆盖大多数静态场景但有几种情况我会改用 Java 的 RouteLocator 来写。一是路由多、规则复杂时YAML 可读性撑不住。二是有逻辑需要动态判断比如根据配置中心的下发内容决定启用哪条路由。三是在路由构建过程中要加定制逻辑比如动态拼接 URI 的路径参数。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(); }这里要提醒一个行为只要定义了 RouteLocator Bean它会和 YAML 里的路由合并生效而不是覆盖。这个特性既方便又容易出问题。如果你本来想“替换”某条 YAML 路由结果 Java 里新增的路由和它存在相同 predicate就可能出现路由冲突一个请求被其中一条抢先命中另一条永远收不到流量。2.3 路由规则的匹配优先级Gateway 的路由匹配顺序很多人以为是按配置文件里的书写顺序来的其实不完全准确。它内部走的是 RoutePredicateHandlerMapping核心逻辑是先找出所有能匹配当前请求的路由然后按谓词权重排序权重高的优先权重相同时再按配置顺序取第一条。所以这里藏着一个典型的坑你写了一条宽泛的路由/api/**转发到 A 服务又写了一条精确的路由/api/order/**转发到 B 服务。到底走哪条Gateway 对 Path 谓词有专门的权重计算逻辑精确路径的权重高于带通配符的路径也就是说/api/order/**会比/api/**优先匹配。这个设计多数时候符合直觉但如果你一直用“先写先匹配”的心智模型去理解 Gateway排查时会绕不少弯路。我把匹配优先级总结成三条经验谓词条件越精确匹配优先级越高不要在配置里依赖书写顺序。多个谓词组合时满足条件更多的路由优先级也更高。拿不准时用spring.cloud.gateway.routes[*].order字段显式指定顺序数字越小优先级越高。这三条尤其适合配置规模大、路由动不动几十条的中大型项目。手动指定 order 看起来多写几行但能避免很多“规则被劫持”的隐性故障。3. 核心谓词工厂与过滤器实操拆解3.1 高频 Predicate Factory 逐个讲光说不练假把式。我把用得最多的几个谓词逐个过一遍每个都给实际配置写法。Path 路由谓词Path/api/**是最常见写法。它支持模板变量比如/api/order/{orderId}匹配到的路径参数会被放到 ServerWebExchange 的属性里后面的 filter 可以通过exchange.getAttribute(URI_TEMPLATE_VARIABLES_ATTRIBUTE)拿到。这个特性在做日志记录和鉴权时非常有用比如从路径里直接取出订单号。Method 路由谓词限定请求方法写法是MethodGET,POST。注意多个方法用逗号分隔中间不要留空格。我见过同事写成MethodGET, POST结果 POST 一直匹配不上排查半天才发现是空格问题。这类配置错误不会报错只会让流量悄悄走错路由特别隐蔽。Header 路由谓词按请求头匹配常用来做灰度发布。写法predicates: - Headerversion, v2表示请求头version的值等于v2时才匹配。Header 的值支持正则比如Headerversion, v[0-9]。灰度分流我惯用的组合是 Header 加 Weight 一起用一个按用户维度区分一个按流量比例区分。Query 路由谓词按查询参数匹配常用于区分调用来源predicates: - Querysource, app匹配带sourceapp的请求。如果只写Querysource表示只要存在source参数就匹配不限制值。Cookie 路由谓词和 Header 类似匹配请求携带的 Cookie。适合做会话级别的分流比如把已登录用户导向新版页面。Host 路由谓词按域名匹配写法Host**.example.com。同一套网关服务多个域名时用 Host 做路由拆分比 Path 更干净。Weight 路由谓词灰度分流利器。比如 90% 流量到稳定版本10% 到灰度版本routes: - id: order-stable uri: lb://order-service-stable predicates: - Weightorder-group, 90 - id: order-gray uri: lb://order-service-gray predicates: - Weightorder-group, 10同一个 group 下所有路由的权重加起来尽量凑成 100分流比例一眼就能看懂。这个谓词底层是随机权重算法不是精确到请求级别的轮询。线上验证时样本量太小会看到比例明显偏离配置值这是正常的概率波动。3.2 高频 Filter Factory 逐个讲Predicate 决定哪些请求进来Filter 决定怎么处理这些请求。我按使用频率整理一下。StripPrefix剥离路径前缀前面已经讲过。它适合“网关统一加前缀、后端不带前缀”的规范场景。RewritePath路径重写比 StripPrefix 灵活得多。它用正则匹配原始路径然后替换成目标路径filters: - RewritePath/api/order/(?segment.*), /$\{segment}注意 YAML 里$要写成$\{...}转义否则会被当成占位符解析。这个细节坑过不少人配置看起来没问题但重写出来的路径全是乱码。AddRequestHeader / AddResponseHeader请求头和响应头加工。常用来注入网关侧标记比如请求来源、用户身份信息。我一般在全局过滤器里统一注入X-Gateway-Source下游服务可以拿它做来源统计和风控。RemoveRequestHeader转发前删除某些头防止内部信息泄露到下游。比如上游调用方传了一个内部认证头你不想让它在服务间继续传递就在这里删掉。SetPath直接把路径重写为固定值适合把多个外部 URL 映射到同一条内部路径的场景。RequestRateLimiter限流过滤器基于 Redis 加 Lua 脚本实现令牌桶。配置时需要配合KeyResolver的 Bean 一起使用按用户、按 IP、按接口维度限流都可以。这是网关最常用的保护机制之一但很多人只配置了默认值导致限流粒度完全不符合业务需求。Retry重试过滤器。默认重试的是 GET 这类幂等方法。如果你给 POST 接口也配了重试一定要确认下游有没有做幂等处理否则重复下单、重复扣款的锅网关至少要背一半。3.3 综合路由配置示例把常用的谓词和过滤器串起来我贴一份可以直接抄作业的配置spring: cloud: gateway: routes: - id: order-service-api-route uri: lb://order-service order: 1 predicates: - Path/api/order/** - MethodGET,POST filters: - StripPrefix1 - AddRequestHeaderX-Gateway-Source, gateway - RewritePath/api/order/(?segment.*), /$\{segment} - Retryretries: 2, statuses: BAD_GATEWAY, methods: GET这套配置覆盖了路径匹配、方法限制、前缀剥离、请求头注入、路径重写和重试可以在小型项目里直接套用。配完以后用 curl 验证curl -i -X GET http://localhost:8080/api/order/detail?orderId1001重点看两个地方响应头里有没有X-Gateway-Source: gateway网关日志里命中的路由 id 是不是order-service-api-route。两个都对说明链路正常。4. 动态路由把规则从配置文件里解放出来4.1 基于配置中心的动态路由实现静态配置文件方案最大的痛点是改一条路由就得重启网关。微服务架构下服务数量动辄几十个每次加服务都重启网关无论是运维效率还是可用性都不可接受。所以动态路由几乎是生产环境的刚需。业界最常用的方案是 Nacos 配置中心配合 Spring Cloud Gateway。核心思路是把路由规则从本地 YAML 挪到 Nacos 配置里用监听器监听配置变化动态更新网关的路由定义并触发路由刷新。我介绍一种我常用的做法基于 Nacos 配置监听加手动刷新路由。定义一个组件订阅 Nacos 里的路由配置解析成RouteDefinition列表后通过ApplicationEventPublisher发布RefreshRoutesEventComponent public class NacosDynamicRouteService implements ApplicationEventPublisherAware { private ApplicationEventPublisher publisher; public void updateRoutes(ListRouteDefinition routeDefinitions) { // 保存路由定义 for (RouteDefinition definition : routeDefinitions) { routeDefinitionWriter.save(Mono.just(definition)).subscribe(); } // 发布事件触发路由刷新 this.publisher.publishEvent(new RefreshRoutesEvent(this)); } }从 Spring Cloud Gateway 2.x 开始路由数据可以持久化到各种存储。官方提供了RouteDefinitionRepository接口默认实现是基于内存的InMemoryRouteDefinitionRepository。动态路由的本质就是在运行期替换这个仓库里的数据再触发一次路由刷新。理解了这个模型很多配置问题就能看透了。4.2 动态路由刷新机制与注意事项动态路由的核心机制不复杂但容易在细节上翻车。我直说几个踩过的坑。第一事件发布后路由是“全量刷新”而不是“增量更新”。如果你从 Nacos 配置里漏了一条路由刷新后这条路由就消失了。所以每次更新配置必须保证配置内容的完整性最好是拿配置中心里的全量配置去覆盖而不是做“追加一条”的操作。第二刷新过程中如果配置解析出错Gateway 很可能保留旧缓存新配置完全不生效而且日志不一定显眼。我建议在更新逻辑里加配置校验解析失败时直接拒绝更新并抛出明确异常。宁可让更新失败报出来也不要让流量默默走到旧规则上。第三多实例部署时每台网关实例都要监听同一个配置源。如果其中一台网络抖动导致监听失败就会出现部分实例路由是新规则、部分还是旧规则的情况。线上表现就是请求一会儿好一会儿坏特别难排查。所以要么把配置监听做成独立组件统一管理要么在发布流程里加一个“配置生效检查”步骤发布完以后主动调用/actuator/gateway/routes端点确认各实例的路由版本一致。5. 常见问题与排查技巧实录5.1 502 Bad Gateway 排查指南说到网关502 可能是出现频率最高的故障码了。在 Spring Cloud Gateway 语境里502 的本质是网关已经匹配到路由但往下游转发时出了问题拿不到合法响应。我把常见的 502 原因和排查方法整理成速查表表现特征根本原因排查方向日志提示 No provider available注册中心里没有可用实例检查服务是否下线、注册是否成功、负载均衡策略下游服务超时无响应服务处理太慢或线程池耗尽查看下游服务的慢日志、GC、线程池指标下游返回非法响应格式服务端响应不是合法 HTTP 格式用 curl 直连下游验证请求头过大被拒绝网关或下游对 header 大小有限制检查 Netty 的 maxHeaderSize 配置转发路径错误导致下游 404StripPrefix 或 RewritePath 配置不当去下游服务日志里看实际收到的路径排查 502 有一个非常实用的技巧临时打开 Gateway 的 DEBUG 日志重点看RoutePredicateHandlerMapping和NettyRoutingFilter的输出。前者确认路由是否匹配后者能看到转发的具体地址和失败原因。我在生产环境就是这么干的调高日志级别定位问题完事再恢复全程不动代码。5.2 路由不生效的几种典型原因路由不生效的坑归纳下来主要是这几种。第一谓词写错了但没被发现。比如 Path 值里带了空格或者通配符用错。这类问题在配置阶段很难暴露因为 YAML 本身合法只是解析出来的谓词跟你想的不一样。建议写完后用/actuator/gateway/routes端点查看实际解析出来的路由定义和预期对比。第二服务没注册或注册信息异常。lb://开头的 uri 依赖注册中心服务没注册、或者注册了但实例数为零网关就无路可转表现就是 502 或者找不到路由。第三路由顺序导致请求被“截胡”。前面讲过注意用order字段显式控制不要靠书写顺序赌人品。第四过滤器把路径改坏了。请求确实匹配上了但经过 RewritePath 之后下游收到一个完全错误的路径表现是下游 404 或 400。排查时不要只看网关日志要去下游服务的访问日志里看实际请求路径。这个习惯能帮你大幅缩短定位时间。第五配置中心的配置没刷新过来。用了动态路由的话确认配置监听是否正常Nacos 控制台里的配置版本是不是最新的。多实例环境还要确认每台实例是否都拿到了最新配置。5.3 性能与稳定性实战经验最后分享一些 Gateway 调优和稳定性建设上的心得。先说话线程模型。Spring Cloud Gateway 基于 WebFlux底层是 Netty设计目标是用少量线程处理海量请求。这种模型下最忌讳在过滤器链里写阻塞代码比如Thread.sleep、同步调用数据库、同步调用第三方 HTTP。一旦某个请求线程被阻塞整个事件循环都会受影响表现就是网关整体吞吐暴跌。我记得有一次线上事故就是团队在全局过滤器里加了一段同步查数据库的逻辑直接导致网关在高峰期大面积超时。这里必须强调过滤器里只做非阻塞操作需要查数据就用异步方式或者在进入网关之前处理好。再说路由规则的防御性设计。网关是流量的第一道关口规则要有兜底。建议配一条兜底路由把没有匹配到任何业务的路由统一响应一个明确错误比如 404 加一段提示文案不要默认返回 Spring 的错误页。另外对下游服务的超时时间做显式配置不要依赖默认值避免某个下游服务慢吞吞时长时间占住网关连接。最后说监控。Gateway 的 Actuator 提供了/actuator/gateway/routes、/actuator/gateway/globalfilters等端点可以查看运行时路由和过滤器的真实状态。我把这些端点限制在内网访问并为网关配置独立的监控告警。以下三个指标是必告警的路由刷新失败次数。下游服务不可用导致的 5xx 比例异常。网关自身的线程池活跃度或请求延迟 P99 突增。网关这东西平时感觉不到它的存在一旦出问题影响的是全部下游服务。规则写清楚一点日志看得仔细一点监控配得早一点线上就能安稳很多。总的来说路由规则看着是配置文件里几十行 YAML但背后牵扯的模块边界、匹配机制、刷新策略、故障排查每一项都不简单。以上内容都是我从实际项目里一点点踩出来的总结尤其是 502 排查和动态路由刷新机制如果你能从中少走点弯路这篇文章就没白写。最后再分享一个小建议不管项目多急上线前一定要把网关的关键路径走一遍完整联调同时准备一份路由排查手册。等出问题时再临时翻文档代价会大得多。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进