Eh,能把“Gateway路由的配置方式”这个词翻来覆去琢磨的人,多半已经在网上搜了一堆零零散散的资料。有人卡在Spring Cloud Gateway的Route定义上,有人被502搞到怀疑人生,还有人拿着家用路由器的“网关地址”概念往微服务网关上套,结果越套越晕。这篇文章想把Gateway路由这摊子事从头到尾捋清楚:它解决什么问题、三种主流配置方式怎么写、断言规则怎么组合、生产环境怎么动态刷新不重启,最后再把高频报错和排查思路整理成速查表。全程用工程实践的口吻写,不堆概念术语,给的都是能直接抄作业的配置和踩坑总结。
先说清楚一个容易混淆的点:我们讨论的Gateway路由,不是你家路由器里那个“默认网关192.168.1.1”,而是微服务架构里的API网关路由,以Spring Cloud Gateway为主。这是目前Java生态里用得最广的方案,很多团队从Zuul迁移到它就是冲着性能和非阻塞模型去的。你在搜索热词里看到的“Spring Cloud Gateway”“Gateway的作用”“gateway配置”基本都指向这个领域。它和硬件网关最大的区别是:硬件网关转发的是IP报文,API网关转发的是HTTP请求,并且能在转发前后做鉴权、限流、改写、熔断等一系列动作。理解了这层差异,后面看路由配置才能心里有数。
1. Gateway路由是什么,为什么单独把配置方式拎出来讲
路由在Gateway中的角色,简单说就是“请求的交通指挥”:客户端请求到达网关后,网关根据URL路径、请求头、查询参数等条件,决定把这个请求转发给哪个下游服务。例如请求/api/order/create过来,网关一看前缀是/api/order,就知道该转到订单服务,这事儿就是路由干的。
那么“配置方式”有什么好讲的?因为路由是整个网关最核心的灵活性所在。业务在迭代、服务在拆拆合合、灰度在分批放量,这些变化都需要在路由层面快速响应。而Gateway的路由配置不是死板的单一方式——你可以写在YAML里,也可以写成Java代码,还可以在运行时从注册中心和配置中心拉取动态刷新。每种方式各有优劣,选错了对后续维护就是灾难。
Spring Cloud Gateway官方文档把路由拆成三个核心概念:Route(路由)、Predicate(断言)、Filter(过滤器)。我习惯用一个快递站来理解:
- Route就是“分拣规则表”,告诉你什么特征的包裹走哪条传送带。
- Predicate是“包裹的识别条件”,比如“收件地址在杭州”“体积小于50cm”,只有条件满足才匹配到这条路由。
- Filter是“包裹处理工序”,比如贴标签、加固包装、扫码入库,在转发前后对请求响应做加工。
路由配置就是把这三个东西组合起来。理解了这套模型,Gateway的配置就基本拿下了一大半。接下来按配置方式逐个拆解,从最常用的YAML静态配置开始。
2. 三种主流的Gateway路由配置方式
2.1 基于YAML文件的静态配置(最常用、最直观)
如果你所在的项目规模不大、路由规则相对稳定,YAML静态配置是第一选择。它的优点是简单、声明式、上手快,团队里任何成员扫一眼配置文件就能知道网关把什么路径转发到什么服务。
先看一个最基础的单路由配置:
spring: cloud: gateway: routes: - id: order-service-route uri: lb://order-service predicates: - Path=/api/order/** filters: - StripPrefix=2这段配置的效果是:当网关收到路径以/api/order/开头的请求,就将请求负载均衡转发到名为order-service的服务实例上,同时剥离掉前两段路径——也就是把/api/order/create变成/create再发给下游。
这里有几个关键点必须解释清楚:
第一,uri的写法分两种。一种是lb://service-name,表示走注册中心的服务发现,网关会通过LoadBalancerClient从Nacos或Eureka里拉取实例列表做负载均衡。另一种是http://192.168.1.100:8080,直接写死某个具体地址,适合下游服务没有接入注册中心的场景。实测中很多新手把lb://漏掉了,直接写服务名,结果网关报UnknownHostException,就是这一步的问题。
第二,StripPrefix为什么是2而不是1。这是网上问烂了但大家还是会栽跟头的问题。如果路由的Predicate匹配的是/api/order/**,而下游服务Controller的映射是/create,那请求进来时路径是/api/order/create,必须把/api和/order两段都剥掉才能对得上,所以是StripPrefix=2。一句话判断:看你要从请求路径里去几段“网关用来定位服务的前缀”。
第三,路由id必须唯一。这个问题更隐蔽,尤其在配置文件中手动复制路由段落时。两个路由配了相同的id,Gateway启动时不会报错,但上下文中后一个会覆盖前一个,转发行为变得难以预测。我曾经排查过一起“路由时而生效时而不生效”的诡异故障,最后发现就是配置里两条路由id写重了。
2.2 基于Java DSL的代码配置(适合复杂条件和动态逻辑)
当路由规则参数化程度高、需要根据运行环境或业务数据做判断时,YAML配置就会显得僵硬。比如你要根据请求头中的某个业务字段决定转发到不同环境,或是要引用配置中心动态下发的参数,这时候用Java DSL更顺手。
基于代码配置的最小示例:
@Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { return builder.routes() .route("order-route", r -> r .path("/api/order/**") .filters(f -> f.stripPrefix(2)) .uri("lb://order-service")) .route("pay-route", r -> r .header("X-Version", "v2") .and() .path("/api/pay/**") .filters(f -> f.addRequestHeader("X-Env", "gray")) .uri("lb://pay-service")) .build(); }这个例子演示了两件事。一是RouteLocatorBuilder可以链式定义多条路由,每一条都清晰表达“什么条件下转发到哪里”。二是第二条第路由同时用了header和path两个断言,用and()组合,表示两个条件都要满足才匹配。这种“多条件联合判断”在Java DSL里特别方便,而YAML里同样能做到,但嵌套结构一多就容易看不清楚。
在实践中我个人的体会是:Java DSL更适合做“路由规则模板”。什么意思?比如你有灰度环境、预发环境、生产环境三套部署,每套环境的服务名不同,你可以把服务名前缀抽成配置项,在Java代码中拼接。YAML配置虽然也能做到,但拼接逻辑放在代码里更可控、更好做单元测试。
不过Java DSL也有明显的局限:修改路由需要重新编译、打包、发布。这个代价在生产环境可不算小。所以现在的项目里,Java DSL更多被用来写基础兜底路由,而频繁变动的业务路由,往往交给动态配置的方式。
2.3 基于注册中心与配置中心的动态路由(生产环境必用)
这是目前生产环境里用得最多、也最符合“运维友好”思路的方式。核心思想是:路由配置不写死在项目里,而是存在Nacos、Apollo这类配置中心,网关启动时加载,配置变更时通过监听机制自动刷新,无需重启网关进程。
实现思路分成两条路径:
路径一:Spring Cloud Gateway + Nacos配置中心自动刷新。利用spring-cloud-starter-alibaba-nacos-config,把spring.cloud.gateway.routes配置项放到Nacos配置文件中,再配合@RefreshScope或Spring Cloud Gateway自带的路由刷新事件来做。
核心配置:
spring: cloud: nacos: config: server-addr: 127.0.0.1:8848 file-extension: yaml group: DEFAULT_GROUP name: gateway-routes.yaml然后在启动类或配置类里注入RouteDefinitionWriter和ApplicationEventPublisher,监听Nacos配置变更。一旦路由文件有修改,网关读取最新配置,把变更后的RouteDefinition重新加载到路由表中。
路径二:自行扩展动态路由加载接口。很多团队自定义一套管理后台,将路由规则存储到数据库,网关通过定时任务或消息通知动态拉取。这种方式灵活度最高,适合多团队共用一个网关、每条业务线各自维护路由的场景。但工程复杂度也高,需要自己实现路由更新的并发控制,避免在流量高峰期频繁刷新导致短暂路由不可用。
我个人的建议是分阶段演进:项目初期直接YAML静态配置就够了,路由不超过20条时完全没问题;服务数量涨上来之后,迁移到Nacos实现配置化下发;只有当出现“运营需要在后台改路由规则,且不能依托发版”的诉求时,再考虑自研管理后台动态刷新。一上来就追求动态路由,往往是为过度设计买单。
3. Predicate断言规则详解:路由匹配的命门
Predicate翻译成“断言”有点抽象,本质上就是一个返回布尔值的条件函数。请求来了,把请求数据塞进断言函数里,返回true就走这条路由,返回false就找下一条。Spring Cloud Gateway内置了一堆断言工厂,熟练掌握排列组合,就能覆盖绝大多数路由场景。
3.1 最常用的五种断言
Path断言:按请求路径匹配,支持通配符**和*。/api/order/**匹配/api/order/create也匹配/api/order/list/detail;/api/order/*只匹配一级路径,/api/order/create能匹配上,但/api/order/list/detail不行。这个差异在配置时很容易被忽略,导致某些深层路径请求匹配不上,网上不少“路由不生效”的问题就是这原因。
Method断言:按HTTP方法匹配。Method=GET,POST表示只对GET和POST请求生效。实际场景中常用于将读写路由拆分开来,比如/api/query/**只允许GET和POST,而/api/manage/**仅允许POST、PUT、DELETE。
Header断言:按请求头匹配,语法Header=请求头名称, 正则表达式。比如Header=X-Request-Version, \d+,含义是请求头X-Request-Version的值必须匹配“纯数字”这个正则。这个断言在做灰度发布时极其好用——带特定版本号头的请求走新集群,不带的走老集群。
Query断言:按查询参数匹配,语法Query=参数名, 参数值正则。比如Query=userId, \d+,要求请求必须携带userId参数且为数字。适用于某些需要指定用户维度的AB测试配置。
Host断言:按域名匹配,语法Host=**.example.com,适合一个网关同时代理多个域名的场景。例如api.example.com和admin.example.com通过不同的Host断言路由到不同服务,要比在代码里判断请求头更干净。
3.2 时间相关的三种断言
Spring Cloud Gateway内置了After、Before、Between三个时间断言,可以精确控制路由在某个时间段内生效。语法固定是ZonedDateTime格式,例如:
predicates: - Between=2024-01-01T00:00:00+08:00[Asia/Shanghai], 2024-12-31T23:59:59+08:00[Asia/Shanghai]这个特性适合什么场景?比如促销活动期间,把流量路由到活动专用服务上;活动结束后配置自动失效,避免人工忘记关闭。虽然是冷门功能,但用对了能省不少运维操心事。
3.3 Weight断言:按权重分配流量
Weight断言用于灰度发布和按比例分流,语法是Weight=分组名, 权重值。多个路由使用相同的分组名时,网关会按权重计算概率:
spring: cloud: gateway: routes: - id: order-v1 uri: lb://order-service-v1 predicates: - Path=/api/order/** - Weight=order-group, 80 - id: order-v2 uri: lb://order-service-v2 predicates: - Path=/api/order/** - Weight=order-group, 20这段配置会把/api/order/**的请求按8:2的比例分配到v1和v2两个服务版本。注意两个路由必须使用相同的order-group分组名,权重值相加不一定要等于100,网关内部会按比例归一化处理。
这里有个藏得很深的坑:Weight断言不会在网关日志里直接显示“本次请求走了哪条路由”。排查灰度分流问题时,一定要在Filter里主动透传版本标识到下游,或者在响应头加X-Route-Id,否则流量分布对不上、问题定位很痛苦。
4. Filter过滤器配置:光有路由还不够
路由决定“往哪走”,Filter决定“怎么处理”。Spring Cloud Gateway的Filter分为GlobalFilter(全局过滤器)和GatewayFilter(局部过滤器)。全局过滤器对所有路由生效,比如LoadBalancerClientFilter负责负载均衡,NettyRoutingFilter负责发起转发请求。局部过滤器只对声明它的路由生效,我们在配置里写的StripPrefix、AddRequestHeader都属于这类。
4.1 高频使用的内置Filter
StripPrefix:刚才已经说过,掐掉路径前N段。
RewritePath:更灵活的路径改写,基于正则替换。语法示例:
filters: - RewritePath=/api/order/(?<segment>.*), /$\{segment}注意这里有个大坑:YAML文件里${segment}必须写成$\{segment},否则会被YAML解析器当成占位符引用,运行时直接报错或生成错误路径。这个报错很经典,很多新手看官方文档没仔细,照抄过来就是404。
AddRequestHeader / AddRequestParameter / AddResponseHeader:分别往请求头、查询参数、响应头添加固定值。常用于网关层注入内部标识,比如请求来源、经过网关的时间戳。
Retry:重试过滤器,默认情况下如果下游短暂故障,网关会直接返回502;配置Retry后可以自动重试。配置示例:
filters: - name: Retry args: retries: 3 statuses: BAD_GATEWAY methods: GET要特别留意的是:不是所有请求都适合重试。POST、PUT这类非幂等请求如果下游已经处理成功,但因为响应超时触发了重试,会导致业务重复执行。所以Retry的methods参数务必明确限定,甚至只对GET开启。
RequestRateLimiter:限流过滤器,基于Redis + Token Bucket算法实现。这是网关挡住突发流量的关键组件。最简配置:
filters: - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 10 redis-rate-limiter.burstCapacity: 20 key-resolver: "#{@userKeyResolver}"replenishRate是每秒向桶里补充的令牌数,burstCapacity是桶的最大容量。如果你理解不了这两个参数的含义,就用一个生活化类比:桶就是售票窗口的排队区,令牌就是服务员的接待能力。replenishRate决定服务员每秒能接待几个,burstCapacity决定排队区最多能站几个人,站不下的直接被拒绝。
4.2 自定义Filter的思路
内置过滤器覆盖不了所有场景,比如统一签名校验、链路追踪ID注入、灰度标签透传,这些都得自己写。实现一个Filter的骨架如下:
@Component public class CustomAuthFilter implements GlobalFilter, Ordered { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String token = exchange.getRequest().getHeaders().getFirst("Authorization"); if (null == token || !token.startsWith("Bearer ")) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } return chain.filter(exchange); } @Override public int getOrder() { return -100; } }getOrder()返回的值越小,过滤器执行顺序越靠前。写自定义过滤器时最容易犯的错是“阻塞了线程却不结束请求”——要么没调用setComplete(),要么没走chain.filter()。一旦出现,表现就是请求在网关层卡死,直到超时。
我个人的经验是:全局Filter不要写太多业务逻辑,保持精简;需要复杂业务处理的,在Filter里只做校验和透传,把真正逻辑丢给下游服务,别让网关变成一个巨型业务处理器。
5. 路由注册、刷新与执行顺序的底层逻辑
很多人在配置路由时容易忽略“路由是怎么被Gateway加载的”“改完配置为什么没生效”这类问题。这里把底层流程说透,对排查线上问题极有帮助。
5.1 路由加载的三个阶段
Spring Cloud Gateway的路由生命周期分三阶段:RouteDefinition加载 → RouteDefinition转换成Route → Route写入路由表供请求匹配。
RouteDefinition是配置的原始描述形式,可以来自YAML、Java Bean或动态接口。框架通过RouteDefinitionLocator读取这些描述,再经RoutePredicateFactory和GatewayFilterFactory将描述转换为可执行的Route对象。最终每个Route包含一个predicate和一组filter,Route被保存到RouteCache里。
请求进来时,RoutePredicateHandlerMapping遍历路由表,找到第一个匹配的Route,然后构造过滤器链并按Ordered排序执行,最后把请求转发到目标URI。理解了这条链路,你就能明白为什么说“Predicate匹配是为了组装Filter链”,而不是简单地把请求转发出去。
5.2 配置修改后为什么有时不生效
这是排查中最高频的场景。如果用的YAML静态配置,修改后必须重启网关应用,只有重启后RouteDefinitionLocator才会重新读取配置。如果你用了Nacos动态配置但没生效,多半是这几个原因:
- 没有引入
spring-cloud-starter-alibaba-nacos-config依赖,只引入了nacos-discovery。 - 配置文件的
dataId和group没匹配上,网关没读到对应配置。 - 没有加
@RefreshScope或者没有触发RefreshRoutesEvent事件,路由定义虽然刷新了,但路由表没更新。 - 修改的是Nacos配置,但网关部署环境连接的不是同一个Nacos集群。
排查时可以临时打开Gateway的Debug日志,看启动时和配置变更时是否打印了路由加载记录。日志里能明确看到RouteDefinition的名称和对应的Predicate、Filter列表,比一头扎进代码里调试高效得多。
6. 常见报错和排查实录:从502到404一次说清
6.1 502 Bad Gateway:下游服务不可达
热词里频繁出现502 Bad Gateway,这是网关场景里最常见的报错,但触发原因各不相同。最直接的可能是下游服务实例挂了或注册中心里没有可用实例。这时候先从注册中心看一眼服务列表,确认lb://service-name中的服务名拼写对不对。
第二个高频原因是“网关和下服务的网络不通”。如果两个服务部署在不同Kubernetes集群或不同VPC,需要检查网络策略、安全组、防火墙规则是否放行。有时候网关本地能telnet通,但容器环境里就不通,需要进入网关容器里测试。
第三个原因是超时。默认情况下Spring Cloud Gateway的响应超时时间较长,但如果你在下游服务的场景中配置了spring.cloud.gateway.httpclient.response-timeout,超时时间设得太短,下游处理时间超过阈值就直接返回502。
6.2 Unexpected status 502:CC Switch和路由转发异常
有个热词是unexpected status 502 bad gateway: cc switch local proxy failed while handli...,这类报错常见于网关和其他代理组件(比如某些过滤组件、SwitchProxy)联调时。核心含义是网关将请求转发给一个本地代理组件,但代理组件处理失败。
排查思路从两个方面推进:一是查看本地代理组件的健康状态和监听端口是否正常,比如127.0.0.1:15721这个地址对应的进程是否存活;二是查看网关的HTTP Client配置,确认超时时间和连接池设置是否合理。这类问题通常是组件间版本不兼容或系统资源不足导致,不是路由配置本身的语法问题。
有报错502 bad gateway: unknown error, url: http://127.0.0.1:15721/v1/responses时,优先检查本地代理进程是否被守护进程拉起,或者是否存在负载过高导致进程僵死。这类排查的一个好习惯是:在网关和后端组件之间加一层访问日志,至少能看到请求在哪一步断了。
6.3 404 Not Found:Predicate匹配成功但路径没改对
网关返回404和返回502是完全不同的排查路径。404往往意味着请求已经到达网关并匹配到了路由,但转发给下游时下游处理不了,或者路径没改对。最常见的是StripPrefix参数错误:
比如Predicate是Path=/api/order/**,如果下游Controller的映射是@RequestMapping("/api/order"),那么StripPrefix=1就够了;但如果下游映射是@RequestMapping("/order"),那么必须StripPrefix=2。这个参数纯粹取决于下游服务的Controller定义,没有标准答案,配置前必须确认。
另一种可能:路由匹配了,但匹配到了一条“不存在下游服务”的路由。比如uri写的是lb://non-exist-service,注册中心查不到实例,Gateway会抛出ServiceInstanceListSupplier相关异常,表现也是502或404。这个时候看日志比看响应码更有价值。
6.4 路由404排查速查表
| 症状 | 可能原因 | 排查步骤 |
|---|---|---|
| 所有请求404 | Predicate条件没匹配到任何路由 | 检查请求路径、Header、Query是否满足任一Route的Predicate |
| 部分请求404 | StripPrefix值与下游Controller地址不匹配 | 在Filter中加日志打印转发后的URI |
| 某个接口404,其他正常 | 路由优先级配置错误 | 调整Route顺序,LocalWeight等断言注意重叠规则 |
| 动态路由不生效 | 刷新事件未触发或路由表Cache未更新 | 查看编排日志,手动调用Gateway的actuator刷新接口 |
热词里还提到了“洛谷提交失败无法解析路由对象”和“vue路由参数”这类纯前端场景,撇开具体平台,如果是前端路由在“刷新页面后404”,通常是服务端没有做history模式回退配置;如果是“路由参数变了但组件不渲染”,基本是忘记监听路由变化或组件复用了实例。这些问题和Gateway本身是两个领域,但能理解“路由=根据条件决定去向”这个思想的话,解决问题时也能触类旁通。
6.5 502排查速查表
| 症状 | 可能原因 | 排查步骤 |
|---|---|---|
| 502+无法连接下游 | 注册中心无可用实例 | Nacos/Eureka控制台检查服务列表,curl服务地址验证 |
| 502+下游有响应 | 网关与下游网络隔离 | 进入网关容器ping/curl下游地址 |
| 502+偶尔出现 | 连接池耗尽或下游慢 | 看网关日志,排查HTTP Client的连接池参数 |
| 502+固定接口出现 | 下游接口运行时报错 | 查看下游应用日志,注意网关只是传话人 |
7. 生产环境路由配置的几条经验总结
写到这里,分享几个项目中沉淀下来的实操心得。
第一,路由配置一定要纳入代码仓库和版本管理。即使是Nacos动态配置,也要把配置文件保存到Git/GitLab一份,不然哪天误操作改坏了配置,想回滚都不知道上一版长什么样。
第二,给路由配置加独立的日志输出。在Gateway的logback配置里,单独给org.springframework.cloud.gateway包设置一个DEBUG级别的独立日志文件,能观察到每一次请求的路由匹配结果和过滤链执行情况,排查问题效率高一个档次。
第三,生产环境改路由要像发版一样走审批。路由错误的影响面是整个入口流量,一次配置错误可能把流量全部打到死服务上。建议配置中心加上变更记录和审计日志,稍微大一点的团队甚至可以在管理后台做“配置发布”和“配置回滚”两个按钮。
第四,不要把所有服务都放进网关路由。网关只暴露对外的必要路由,内部服务之间的调用尽量走注册中心直连,不要让HTTP请求在网关层绕一大圈。路由表越膨胀,排查问题和故障定位的难度就越大,性能也会受影响。
关于Gateway路由的配置方式,基本就这些核心内容。从我自己的实践来看,最容易出问题的不是语法不会写,而是对匹配链路的理解不够。把Route、Predicate、Filter三者拆开了揉碎了想清楚,再配合日志排查,大部分路由问题都能在五分钟内定位。如果你在实际配置中遇到什么特别刁钻的报错,卡了很久的话,不妨先按这个排查思路走一遍,大概率能找到突破口。