一、服务调通了,外部访问还是乱的
Nacos 和 Feign 解决了服务间调用,外部访问却还是乱的:cart-service 跑在 8082,item-service 在 8081,trade-service 在 8083,order-service 又是另一个端口。调试时每测一个功能换一个地址,测购物车用 8082,测商品切回 8081,测订单再换一个。
端口散落只是表面,后面还跟着三个问题:
- 地址暴露:内部服务的端口直接给浏览器用,内部拓扑一览无余,扩容、迁移时前端也得跟着改
- 跨域各配一遍:每个服务都要配 CORS,漏配一个接口就在浏览器里报跨域错误
- 公共逻辑重复:鉴权、限流、访问日志这些横切逻辑,要么每个服务抄一遍,要么漏写
鉴权逻辑本来写一次就够,实际可能散落在五个服务里,版本还会不一致。
问题在于外部客户端不该直接面对一群内部服务。需要一个统一入口,所有外部请求先到这里,经过校验和过滤,再由它转发到真正的服务------这一层就是网关。
二、网关把什么挡在了外面
Spring Cloud Gateway 在微服务里的位置可以用一个词概括:守门人。外部请求先到它这里,它决定请求该去哪个内部服务。
它挡在外面的主要是三类东西:
地址暴露。 前端只认网关的地址,内部有几个服务、分别跑在什么端口,前端不知道也不需要知道。扩容、迁移、换实例,只要路由里的服务名不变,外部无感。
横切逻辑。 鉴权、限流、访问日志、跨域处理这些事,每个服务都要做,每个服务做一遍就是重复。放到网关,写一次,所有请求都经过它,统一生效。
协议与路径的差异。 内部服务的接口路径可能各不相同,外部只需要一套统一的路径规则。网关把外部路径映射到内部路径,两边可以各自演化。
和 Nginx 放在一起看,两者守的位置不一样。Nginx 站在更外层,处理静态资源、TLS 终止、最基础的转发;Gateway 站在应用层,能读 JWT、查缓存、写 Java 逻辑。生产环境里常见的是两者组合------Nginx 承担流量杂活,Gateway 处理需要懂业务的那部分。
至于 Spring Cloud 为什么选 WebFlux 而不是 Spring MVC:网关的请求轻、并发高,每个请求停留时间很短,非阻塞模型更合适。知道这一点即可,不影响日常使用。
三、路由:请求进门之后,往哪走
网关收到请求后,第一个问题是"这个请求该给谁"。答案在路由配置里。
路由的三要素
yaml
spring:
cloud:
gateway:
routes:
- id: item-route # 路由唯一标识
uri: lb://item-service # 目标:走负载均衡找 item-service
predicates: # 匹配条件
- Path=/items/**
id:这条路由的唯一标识uri:目标地址。写lb://item-service表示不写死 IP,交给负载均衡去解析predicates:匹配条件,请求满足条件才会走这条路由
lb:// 需要说明一下:lb 就是 load balance,后面跟的是服务名。底层还是 Nacos + LoadBalancer 那条链路------Nacos 篇讲的"服务名 → 真实 IP"在这里原样复用,网关自己不发明解析规则。
内置断言里,Path 匹配路径、Method 匹配 GET/POST,Header、Query、Cookie 也都有对应实现。日常九成场景用 Path 就够,其余的知道存在、用到再查。
多条路由并列
实际项目里一般是多条路由放在一起:
yaml
routes:
- id: item-route
uri: lb://item-service
predicates:
- Path=/items/**
- id: cart-route
uri: lb://cart-service
predicates:
- Path=/carts/**
- id: trade-route
uri: lb://trade-service
predicates:
- Path=/orders/**
匹配按配置顺序进行,命中第一条就停。这一点是后面 404 坑的一个伏笔:路径写岔,请求要么落到不该去的地方,要么哪条都不命中。
一次请求的完整路径
text
浏览器 GET http://localhost:8080/items/1
│
▼
Gateway 遍历所有路由,逐条用 predicates 匹配
│ Path=/items/** 命中 item-route
▼
uri lb://item-service → LoadBalancer 选一个实例
│ item-service → 192.168.10.3:8081
▼
转发 GET http://192.168.10.3:8081/items/1
│
▼
item-service 处理,响应原路返回浏览器
四、三次 404:没配路由、路径写错、路径形态不对
网关启动成功、Nacos 也注册上了,但访问 http://localhost:8080/carts 返回:
json
{
"timestamp": "2026-10-09T07:37:36.947+00:00",
"path": "/carts",
"status": 404,
"error": "Not Found"
}
404,而 Nacos 控制台里 cart-service 明明在线。
第一次:根本没配路由
排查分三步:先看网关日志,Netty 在 8080 启动正常、注册到 Nacos 也成功,排除网关进程问题;再看日志里有没有转发记录,从头到尾没有任何一行提到路由匹配------请求进来之后网关压根没找到匹配的路由;最后翻配置文件,routes 根本没配。网关起来了,但一条路由都没有,任何请求进来它都不知道该发给谁。
补上路由后重启,/carts 通了:
yaml
spring:
cloud:
gateway:
routes:
- id: item-service
uri: lb://item-service
predicates:
- Path=/items/**
- id: cart-service
uri: lb://cart-service
predicates:
- Path=/carts/**
第二次:配了路由,路径写错
yaml
- Path=/cart/**,/search/**
/cart 少了 s------cart-service 的接口路径是 /carts,写成 /cart 永远匹配不到;/search/** 本应是搜索服务的路径,为省一条配置塞进了 item-service 的路由里,把两个不相关的服务混在一条规则下。访问 /carts 依然 404。
第三次:路径写对了,转发出去还是 404
外部请求是 GET /api/carts/1,网关匹配 Path=/api/carts/** 之后把整个路径原样转给了 cart-service,而 cart-service 的接口映射是 /carts/1------带着 /api 前缀过去,下游自然找不到。解法是加一个过滤器做路径重写:
yaml
- id: cart-service
uri: lb://cart-service
predicates:
- Path=/api/carts/**
filters:
- StripPrefix=1
StripPrefix=1 表示转发前剥掉路径的第一段(/api),下游收到的才是 /carts/1。前两次的坑出在"匹配",这一次出在"转发时路径变不变"------路由匹配只决定请求归谁,过滤器决定请求以什么形态交给下游,这是两件分开的事,也是 filters 藏在配置里容易被忽略的原因。
怎么分清是网关的锅还是下游的锅
三次 404 表面相同,实际是三个层次的问题:没配路由、路径写错、路径形态不对。共同点是 404 不代表"服务挂了"------服务挂了通常是 502 或 503,网关这里的 404 只有两种来源:路由表里没有匹配项,或者匹配后转过去的路径下游不认。
排查这类问题,第一步固定为看网关日志里有没有转发记录:
- 有转发记录、下游返回 404 → 请求到下游了,是下游服务自己的路径问题
- 没有转发记录、网关直接返回 404 → 网关路由表里没有匹配项,是路由配置问题
这一步直接分清是网关的锅还是下游的锅。第一次排查时挨个确认了下游服务的状态,方向判断晚了一步;后来把"先看转发记录"固定为处理网关问题的第一动作,两边乱找的情况就没有再出现。
五、从三篇串联看网关的位置
服务名怎么变成 IP:三篇回答的是同一个问题
Nacos、Feign、Gateway 三次都碰到了"服务名怎么变成真实 IP",但需要三套机制,是因为解决的问题不同:
- Nacos :手动查
DiscoveryClient,把底层"服务名 → 实例"的解析看清,它是地基 - Feign :用动态代理封装这条链路,解决服务与服务之间的调用(对内),产出是编译期的接口契约
- Gateway :用路由 +
lb://复用同一条链路,解决外部请求进来的第一跳(对外),产出是运行时可配的转发规则
服务发现只做一次,三者都踩在这块地基上,各自按场景包装:Feign 要的是"调用方写起来像本地方法",Gateway 要的是"请求进门先过一道关"。这也是微服务分层的常见形态------底层能力收敛,上层按用途封装。
网关是单点,为什么还敢把流量都交给它
所有外部流量都过网关,它一挂,整个系统对外不可用。这个担心在架构上是成立的,所以生产环境网关从来都是按高可用组件对待的:多实例部署,前面还有 Nginx 或云负载均衡做流量分发和健康摘除,单实例故障对用户无感。
即便如此,网关仍然是故障影响面最大的一环------内部服务挂一个只影响对应功能,网关挂了是全部功能。这决定了网关的代码必须保持"薄":只做路由、鉴权、限流这类必须统一的事,业务逻辑一律不进网关。网关越重,稳定性风险越大,这也是各家网关层长期克制的原因。
断言和过滤器为什么设计成链
路由配置里,predicates 和 filters 结构上都是一个列表:请求先过一串断言,全部命中才进入这条路由;再过一串过滤器,逐个执行。这就是责任链模式。
这个模式在这几篇里反复出现:Feign 的 RequestInterceptor、Gateway 的 GlobalFilter、Sentinel 的 SlotChain,都是"一组处理环节按序执行,每环做一件事"。认清这一点之后,学新组件时看的就不再是新 API,而是这条链上有哪几环、分别解决什么问题------会一个,一类都会。
网关这个位置,是架构上的必然
责任链是模式层面的共性,网关这个位置本身也是架构层面的共性:它不是 Spring Cloud 的发明。任何技术栈的微服务架构,只要对多个服务提供统一外部入口,都会演化出这一层。Spring Cloud Gateway 只是这个位置上的一种实现,Nginx、Kong、Apisix 各自是另一种实现。配置可以查文档,知道网关守的是哪条边界、替外部挡掉了什么,比记住 routes 的写法更重要。
转发只是网关最基础的能力。请求进门之后,谁有权访问、userId 怎么带进集群、流量超了怎么办------这些留给下篇:统一鉴权怎么做、user-info 怎么透传,以及和 Feign 拦截器怎么配合接力。