ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Spring Cloud Gateway 路由规则实战:从核心概念到排障调优

Spring Cloud Gateway 路由规则实战:从核心概念到排障调优 Spring Cloud Gateway 的路由规则这块我前前后后折腾了不短时间踩过的坑比写过的配置还多。网上讲路由的文章不少但大多数只告诉你Path/api/**怎么配至于为什么这么配、失配时发生了什么、多个谓词怎么协作讲得很少。这次我把实际项目里沉淀下来的路由规则体系完整梳理一遍从核心概念到生产环境排障尽量写透希望对正在用或者准备用 Spring Cloud Gateway 的朋友有点帮助。先说清楚这东西是什么。Spring Cloud Gateway 是 Spring Cloud 生态里的 API 网关组件基于 Spring WebFlux 和 Project Reactor 实现底层走的是 Netty 响应式模型跟传统的 Zuul 1.x 那种每请求一个线程的 Servlet 模型完全是两码事。对于微服务架构来说网关是流量进入后端的第一道关卡路由规则就是这道关卡的核心逻辑决定某个请求该去哪个服务、经过哪些处理、以什么形式转发。理解并掌握路由规则是玩转 Spring Cloud Gateway 的基石。如果你是刚接触网关的初级开发这篇能帮你把路由规则的全貌拉通少走弯路如果你已经配置过一些路由但遇到过“配置不生效”“断言匹配不到”“转发路径不对”这类问题那这篇的排查思路和踩坑实录应该能直接帮你定位问题。1. 路由规则的核心设计一分为三再组合1.1 Route、Predicate、Filter 三者的分工逻辑Spring Cloud Gateway 的路由规则本质上是由三个核心组件叠加组成的。我在给团队做技术分享的时候经常把这三者的关系比喻成“门卫 登记表 服务人员”Route路由定义了一个完整的转发单元包含路由 ID、目标 URI、一组断言Predicate和一组过滤器Filter。相当于门卫手上的登记表一个表记录一条规则。Predicate断言判断请求是否匹配当前路由的条件。相当于门卫的检查动作看时间、看来访者的身份、看从哪里来、看带了什么通行证。如果断言全部满足这条路由才会被选中。Filter过滤器请求被路由选中后在转发前后执行的处理逻辑。相当于门卫确认放行后引导访客到指定会议室甚至提前把会议室布置好。这个拆分逻辑最大的好处是关注点分离。断言负责“能不能走这条路”过滤器负责“走上这条路之后要做哪些事”路由则是把两者绑在一起的载体。你可以独立地增删断言条件也可以单独修改过滤器链互不干扰。1.2 Predicate Factory路由的“准入条件”是怎么生效的Spring Cloud Gateway 内置了十几个 Predicate Factory每一个都对应一种匹配规则。配置层面它们长得很像但底层实现各有各的解析逻辑。我挑几个高频且容易误用的详细讲Path Route Predicate Factory匹配请求路径。常见写法是Path/api/user/**。注意这里的**代表匹配任意层级的子路径而*只能匹配一层。如果写成Path/api/user/*那/api/user/123能匹配到/api/user/123/address就会失配。Method Route Predicate Factory匹配 HTTP 方法。MethodGET,POST表示只接受 GET 和 POST 请求。我见过有人只配了 Path 忘了配 Method结果 POST 接口被 GET 请求打到后端后台直接抛 405。Header Route Predicate Factory匹配请求头。HeaderX-Request-Id, \d表示请求头里必须存在X-Request-Id且值要满足后面的正则表达式。它在做灰度发布、租户隔离时特别有用比如通过自定义请求头把流量引导到灰度版本的服务。Query Route Predicate Factory匹配查询参数。Queryversion, v\d表示 URL 上必须带version参数且值匹配v\d。这个常用于区分 API 版本。RemoteAddr Route Predicate Factory按客户端 IP 匹配支持 CIDR 格式比如RemoteAddr192.168.1.0/24。不过要注意如果网关前面还有负载均衡器或 CDN拿到的是前置设备的 IP需要配合 X-Forwarded-For 解析真实客户端 IP否则这个断言基本失准。这些谓词可以在一个路由里同时配置多个Spring Cloud Gateway 底层默认用and逻辑组合它们所有谓词都返回 true路由才匹配成功。这一点非常重要——不是“满足其中一个”而是“全部满足”。我在项目中就踩过这个坑当时以为配置多个 Path 条件是“或”的关系结果请求被错误路由了排查了半小时才意识到。1.3 Filter Factory路由的“加工流水线”Filter 是路由规则里最灵活也最容易被低估的部分。它分为两种GatewayFilter作用于单个路由和GlobalFilter作用于所有路由全局生效。常用的 GatewayFilter 包括StripPrefix转发给后端服务时去掉路径前缀。这个特别关键。假设前端请求/api/order/list路由配置Path/api/order/**的同时设置了StripPrefix1那转发到下游的路径就变成/order/list。如果忘了配 StripPrefix下游服务就得被迫接收带/api/order前缀的请求很容易出现 404。AddRequestHeader / AddResponseHeader在转发请求时添加或重写请求头/响应头。我做身份认证时就经常用它把解析后的用户信息放到请求头传递下去。RewritePath正则重写路径。它的灵活性比 StripPrefix 更高因为可以用正则表达式做任意组合的路径变换。Retry失败重试。可以配置重试次数、重试的状态码和异常类型但要注意幂等性场景否则重试会把数据写重复。GlobalFilter 的典型例子是自定义鉴权逻辑。我通常会在网关层实现一个 GlobalFilter统一解析 Token、校验权限而不是在每个下游服务里重复写拦截器。这样职责边界就清楚了网关管准入下游管业务。我个人的体会是搞清楚 Predicate 和 Filter 的分工比背配置项重要得多。遇到路由不生效的问题先问自己是断言没匹配上还是过滤器把路径改坏了又或者是转发目标本身有问题定位范围一下就缩小了。2. 从配置到落地路由规则的完整写法与选型逻辑2.1 YAML 配置的核心结构与匹配逻辑Spring Cloud Gateway 的路由配置在application.yml里核心结构如下spring: cloud: gateway: routes: - id: user-service-route uri: lb://user-service predicates: - Path/api/user/** - MethodGET,POST filters: - StripPrefix1 - AddRequestHeaderX-User-From-Gateway, true这里有几个点需要展开说明id 是路由的唯一标识多个路由的 id 不能重复。它的用途不仅是在日志里定位路由还用于动态路由管理的标识。生产环境里我习惯按“目标服务名-用途”的格式命名比如user-service-public、order-service-internal一眼就能看出路由的作用域。uri 定义了转发的目的地支持两种常见形式lb://user-service走注册中心按服务名负载均衡。这是微服务架构下的标准做法网关向服务注册中心查询user-service对应的实例列表然后按负载均衡策略选择一个实例转发。http://192.168.1.10:8080直接转发到固定地址。适用于没有接入注册中心的遗留系统或者某些外部服务。选型逻辑上服务间调用我推荐一律用lb://形式这样下游服务扩缩容时网关无需感知。只有当某个接口消费的是外部固定地址的 HTTP 服务才用直连地址。匹配顺序上Spring Cloud Gateway 会按配置顺序依次判断路由。也就是说第一个路由的谓词如果匹配成功请求就被转发到该路由的 uri后续路由不再检查。这个“先到先得”的机制特别容易踩坑。我举个例子如果先配置了Path/api/**的兜底路由再配置Path/api/user/**的精确路由那个精确路由永远不生效。正确做法是把更具体的路由放在前面。2.2 常用 Predicate 组合的实战场景单个谓词的匹配范围是有限的真正适合生产环境的是多谓词组合。我根据实际项目整理了几个经典场景场景路由IDPredicates 组合说明用户服务查询接口user-queryPath/api/user/**MethodGET只放行 GET 查询请求用户服务写接口user-writePath/api/user/**MethodPOST,PUT,DELETE写操作单独管理可附加权限过滤公众号回调入口wechat-callbackPath/callback/wechat/**HeaderX-Signature, [a-f0-9]{32}校验签名头防止非法回调灰度版本路由user-grayPath/api/user/**HeaderX-Version, gray灰度流量转发到指定服务版本内网直连路由internal-onlyPath/internal/**RemoteAddr10.0.0.0/8限制内网 IP 访问禁止外部请求这里要特别强调Header 谓词里正则表达式的写法。配置项里用Header名称, 正则时如果正则包含逗号或空格务必用引号包裹否则 YAML 解析会出问题。我早期就被这个搞过看起来配置没问题实际运行时谓词一直匹配失败最后发现是正则里有个空格把参数拆成了两个。另一个容易忽视的是Cookie 谓词CookiesessionId, abc。它匹配的是请求 Cookie 中是否存在名为sessionId且值为abc的项。但注意Cookie 的值往往包含特殊字符建议配合正则使用。比如CookiesessionId, [a-zA-Z0-9]。2.3 过滤器链的配置技巧与执行顺序过滤器在一个路由内是一个链式结构涉及顺序问题时有两个层面要注意配置层面的顺序Spring Cloud Gateway 内置的过滤器都实现了Ordered接口默认有固定的执行顺序。比如AddRequestHeader和AddResponseHeader这类过滤器顺序值相同按配置顺序执行。如果你在同一个路由里配置了多个过滤器它们的执行顺序等于配置顺序。但如果你同时使用了全局过滤器全局过滤器与路由级过滤器的合并顺序是由各自的order值决定的——order 值越小执行优先级越高。逻辑层面的注意点过滤器链的执行是分两阶段的。请求到达后先执行所有过滤器的“前置”逻辑即 pre 部分再转发到下游服务下游响应返回后再逆序执行所有过滤器的“后置”逻辑即 post 部分。简单说前置阶段顺序执行后置阶段逆序执行。举个例子假设路由配置了AddRequestHeaderX-Trace-Id, 12345和StripPrefix1它们同时作用于转发前。如果我再加一个修改请求体的过滤器比如ModifyRequestBody那它和AddRequestHeader的执行顺序会影响最终转发出去的请求长什么样。经验法则是路径相关的过滤器StripPrefix、RewritePath往前放Header/Body 修改的过滤器往后放因为路径改写后下游服务是根据改写后的路径来路由的而 Header/Body 只是附加信息不影响路径解析。3. 进阶实操自定义扩展路由规则内置的谓词和过滤器虽然多但遇到复杂业务场景时还是会觉得不够用。这时候就该自己动手写扩展了。3.1 自定义 Predicate 的完整实现Spring Cloud Gateway 提供了AbstractRoutePredicateFactory让我们可以扩展自定义谓词。我举一个实际业务场景需要根据请求路径中的某个段做路由匹配但内置的Path谓词不支持“从路径中提取变量并与数据库配置进行比对”这种动态逻辑于是我在网关层自定义了一个VersionRoutePredicateFactory用于解析请求头中的版本号并与配置的版本白名单做比对。自定义谓词的步骤如下创建一个配置类定义需要接收的配置参数public class VersionRoutePredicateFactory extends AbstractRoutePredicateFactoryVersionRoutePredicateFactory.Config { public VersionRoutePredicateFactory() { super(Config.class); } Override public ListString shortcutFieldOrder() { return Collections.singletonList(versions); } Override public PredicateServerWebExchange apply(Config config) { return exchange - { String version exchange.getRequest().getHeaders().getFirst(X-Version); if (StringUtils.isEmpty(version)) { return false; } return config.getVersions().contains(version); }; } public static class Config { private ListString versions; // getter / setter } }配置时启用spring: cloud: gateway: routes: - id: version-route uri: lb://user-service predicates: - name: Version args: versions: v1,v2这里我补充几个要点shortcutFieldOrder方法定义了配置项的“简写”顺序这样才支持Versionv1,v2这种简短写法。不重写这个方法的话配置里必须用完整的name args形式。谓词返回false不代表请求被拒绝而是代表这条路由“不匹配”网关会继续检查下一条路由。千万别在谓词里直接抛异常这会导致请求直接返回 500而不是继续路由匹配。自定义谓词通常要注册成 Spring BeanGateway 启动时才会加载它。3.2 动态刷新路由的几种方案生产环境里有一个刚需路由规则变更时不想重启网关进程。Spring Cloud Gateway 本身支持通过spring.cloud.gateway.routes配置的变更动态刷新但这种方式对 Spring Cloud 配置中心比如 Nacos、Apollo的依赖比较深。我工作中的做法是另外接一套管理接口把路由配置持久化到数据库再通过事件机制更新RouteDefinition。简单的实现思路是实现一个RouteDefinitionRepository从数据库读取路由定义。通过ApplicationEventPublisher发布RefreshRoutesEvent强制网关重新加载路由。提供一个管理接口比如/admin/route/refresh在配置更新后触发刷新。这样做的优势是路由规则可以做成后台可配置化产品和运维可以自助调整不用每次改 YAML 发版。但也有代价就是需要自己维护一套路由管理页面和接口适合路由规则较多的团队。如果只是偶尔微调我建议先用 Nacos 配置中心 监听刷新成本低得多。等路由规则确实“长”到需要界面化管理了再考虑自研管理端。3.3 与注册中心联动的服务发现lb://前缀背后的实现机制是ReactiveLoadBalancer由 Spring Cloud LoadBalancer 负责。网关启动时RouteDefinitionRouteLocator会把lb://开头的 uri 解析成服务实例的访问地址。这中间有两点要特别注意服务名的大小写注册中心的服务名如果包含大写字母lb://解析时可能需要额外配置因为一些注册中心比如 Nacos 默认分组对服务名敏感。遇到“路由配置对但转发失败”的情况先检查服务名是否和注册中心里的完全一致。实例下线与摘除负载均衡器会缓存服务实例列表。如果某个实例下线但缓存还没刷新转发到该实例的请求就会超时或报连接拒绝。遇到这种情况可以调整 LoadBalancer 的缓存刷新时间或者在网关层配合重试过滤器Retry做容错。我实际项目中遇到过一个问题某个下游服务的高峰期流量很大偶尔出现“Connection refused”就是因为服务实例滚动发布时网关的负载均衡缓存还没感知到实例已经下线。后来我加了Retry过滤器并设置较短的缓存刷新时间问题基本解决。4. 常见问题与排查技巧实录4.1 路由不生效的排查路径这个是我被问得最多的一个问题配置明明写了为什么请求打过来就是 404我的排查思路是固定的步骤操作目的1查看网关日志中是否有Routing filters相关记录确认请求是否被某个路由接收2检查 Predicate 的顺序和条件确认业务语义上是否匹配3检查 uri 的配置lb 还是直连确认目标服务地址是否可达4检查 Filter 的路径处理确认转发路径是否被正确重写5用 curl 直接请求网关地址复现问题方便调试这里有一个非常重要的排查技巧开启网关的调试日志。logging: level: org.springframework.cloud.gateway: TRACE开启后网关会打印每个请求经过路由匹配的详细过程包括匹配了哪个路由、哪些谓词通过了、哪些失败了。做路由排查时效率提升立竿见影。有一次排查了一个多小时的问题开了 TRACE 日志后 5 分钟就定位到是 Path 谓词的正则没有匹配上。4.2 谓词匹配与 StripPrefix 的经典坑StripPrefix 的坑我拿出来单独说。很多新人会把StripPrefix1和Path/api/**误以为是组合后“自动去掉前面 api 前缀”其实 StripPrefix 的规则是转发时去掉路径前 N 段内容。N 是段数不是字符个数。举个例子请求路径/api/order/list Path/api/order/** StripPrefix1 转发路径/order/list但如果我把StripPrefix设置成 2转发路径就变成/list。N 值设错就会导致下游接口 404。这个问题的排查方法很简单开 TRACE 日志看转发的实际路径但最好不要等到线上出了问题才排查写路由的时候就应该算清楚段数。另外注意RewritePath的优先级比StripPrefix更可控。如果路径重构逻辑复杂比如要从/api/v1/order变成/order/v1/list这样的跨段映射用RewritePath比用 StripPrefix 加多个过滤器组合要直观得多filters: - RewritePath/api/v1/order/(?segment.*), /order/v1/$\{segment}4.3 性能与调优的实战经验路由规则会影响网关的吞吐量和时延这块的调优经验我觉得有必要分享谓词数量与性能的关系每个新增谓词都会在请求匹配时多一层判断。虽然响应式框架下的开销比想象中小得多但在秒级数万的流量下过长的谓词链依然会产生可感知的 CPU 开销。我建议单个路由的谓词数量控制在 5 个以内。超出这个范围先审视一下业务场景是否真的需要这么复杂的路由条件。Filter 的耗时控制过滤器链中的每个过滤器都会增加请求的处理时间。特别是自定义 GlobalFilter 里的 IO 操作比如查数据库、调远程接口一定要加超时和缓存策略否则会直接拖垮网关。我早年写过一个鉴权过滤器每次都同步查一次数据库压测时发现网关吞吐量直接砍半。后来改成 Redis 缓存 异步刷新性能才恢复。DDL 与响应式线程模型的匹配Spring Cloud Gateway 的线程模型是事件循环驱动的处理线程数量有限。如果在过滤器里做了阻塞调用比如 JDBC 同步查询会阻塞整个事件循环线程导致网关整体瘫痪。这一点务必重视自定义过滤器中禁止出现阻塞 IO必须用 WebClient 或 Reactor 的异步 API。我实测过一个场景早期网关的 P99 延迟在 80ms 左右后来排查发现是一个过滤器里用了同步的 HttpURLConnection 调用外部接口把事件循环堵住了。改成 WebClient 之后P99 直接降到了 30ms 以下。调优这件事很多时候瓶颈不在框架本身而在使用方式上。回到开头那个问题路由规则的难点不在于怎么配而在于理解它背后的匹配模型。只有把 Route、Predicate、Filter 三者之间的关系理清楚把谓词的“与”逻辑和过滤器的执行顺序吃透把自定义扩展开销和响应式模型的红线记在心里才能真正写出稳、快、可维护的网关路由配置。我个人的观察是Spring Cloud Gateway 的路由规则并不难难的是在复杂业务场景下依然保持路由逻辑的清晰和可排查性。所以如果你现在正在设计网关层的路由架构我的建议是先把路由规则拆成“基础转发”和“能力扩展”两层基础转发保持纯粹能力扩展尽量用过滤器解耦。多年以后你会感谢当初那个肯花时间把规则理清楚的自己。
返回列表