第一次把网关接入生产环境时,我第一反应是:这玩意不就是个转发层吗?配置几条路由,把请求扔到后端,完事。直到那个周五下午,一屏的“502 Bad Gateway”刷过来,我才发现自己对微服务网关的理解浅了。网关从来不是“多一层跳转”,它是整个微服务架构的收口点:路由、鉴权、限流、超时、熔断、可观测性,全在这里汇合。这篇文章我就从零开始,用 5 分钟搭一个最小的微服务网关,再把我后续踩过的坑——尤其是 502 这类最让人头大的问题——完整过一遍。适合正在学 Spring Cloud Gateway、或者已经上线却被网关问题折磨的开发和运维同学。
1. 先搞清楚:网关是给微服务“收口”用的,不是多此一举
1.1 没有网关的微服务,客户端直连真的很痛
你可以想象一个没有前台的公司:访客来了自己找工位,找人问路,什么登记、门禁、访客记录全部没有。微服务没有网关就是这样。假设你有 15 个服务,用户端要维护 15 个 baseURL;每次服务扩容、缩容、换 IP,客户端就得跟着改;登录鉴权业务在每个服务里抄一遍;限流各写各的;出了问题想排查,连一个统一入口日志都拿不出来。
实际项目里我见过更离谱的状况:前端为了调 3 个服务,代码里写死了一堆地址,结果后端某个服务迁移到另一台机器,前端页面白屏一整天。这不是前端同学的锅,是架构上缺了一个“统一入口”。网关把客户端和真实服务解耦——客户端只认识一个地址,后端怎么变,和客户端无关。
1.2 网关的核心职责:路由、过滤、限流、可观测
网关做的事情,说穿了四件事:
- 路由:根据请求的路径、方法、Header、Query 参数,决定转发到哪个下游服务。路径匹配是最基础的,比如
/api/user/**转到用户服务,/api/order/**转到订单服务。 - 过滤:在请求转发前、响应返回后做统一处理。最常见的场景是登录校验、Header 注入、请求日志、灰度标识。把这类逻辑从业务服务里抽出来,业务服务只管纯业务,这是网关最大的价值。
- 限流与容错:网关是流量的第一道闸门,非常适合做全局限流、超时控制、熔断降级。不在这里做,散落在每个服务里,标准很难统一。
- 可观测性:在网关生成 trace id、记录 access log、上报指标。这样排障的时候,从入口一路查到下游,链路是干净的。
我把网关比作前台门卫:所有访客先到门口登记,门卫确认身份、登记来访记录,再告诉你去哪栋楼几层。没有门卫,快递员也能进,但丢了东西你根本不知道是谁来过。
1.3 选型先想清楚:Spring Cloud Gateway、Envoy、APISIX 各自适合谁
网关方案很多,很多人一上来就纠结。我做过几个项目的对比选择,简单说下结论:
| 方案 | 典型适用场景 | 上手难度 | 我观察到的注意点 |
|---|---|---|---|
| Spring Cloud Gateway | Java 微服务技术栈统一,注册中心、配置中心都是 Spring 生态 | 中等 | 文档多、示例多,但 JVM 内存占用偏高,不适合极端低延迟要求 |
| APISIX | 想要低延迟、控制面与数据面分离、K8s Ingress 场景 | 中等偏易 | 基于高性能数据面,插件丰富,动态路由能力强 |
| Envoy | 云原生、服务网格、大流量边缘网关 | 高 | 配置模型复杂,需要理解 xDS 协议,通常是平台团队来维护 |
| Nginx | 简单转发、TLS 终结、固定路由 | 低 | 适合静态配置;但要做动态路由、灰度、复杂鉴权,会比较吃力 |
如果你们团队是 Java 为主,后续要接 Nacos、Spring Cloud,那直接选 Spring Cloud Gateway 最省心。如果你们更看重性能和云原生,愿意额外维护一套控制面,APISIX 或 Envoy 更合适。不要贪图“功能多”去选一个团队没人会的方案,网关这种基础设施,不会维护比不好用更可怕。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 五分钟跑通第一个网关:我的最小可用示例
2.1 创建最小工程,先避开一个要命的依赖坑
我用 Spring Cloud Gateway 来演示,因为它最容易复制。去 start.spring.io 生成一个空工程,Java 版本选 17 或 21,Spring Boot 选 3.x,依赖里加一个 Spring Cloud Gateway。如果你还想看健康状态,再加一个 Actuator。生成完的 pom 里会带类似这样的依赖:
xml复制<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
这里最大的坑:不要手动加 spring-boot-starter-web。Spring Cloud Gateway 底层是 WebFlux,也就是响应式非阻塞模型。你把 Spring MVC 也加进来,启动时会直接报错,提示两者不兼容。我见过好几个人因为“顺手加了个 web 依赖”,项目一直启动失败,卡了一下午。
2.2 配置两条路由,理解 Predicate 和 Filter
最小配置在我看来长这样。新建一个 application.yml:
yaml复制server:
port: 8080
spring:
application:
name: gateway-demo
cloud:
gateway:
routes:
- id: user-service
uri: http://localhost:8081
predicates:
- Path=/api/user/**
filters:
- StripPrefix=1
- id: order-service
uri: http://localhost:8082
predicates:
- Path=/api/order/**
- Method=GET,POST
filters:
- RewritePath=/api/order/(?<segment>.*), /$\{segment}
逐个拆开看:
id:这条路由的名字,自己起,主要用于日志排查。uri:转发目标地址。最简单的写法是直接写死http://localhost:8081。生产环境我推荐写lb://user-service,靠注册中心动态发现地址,后面会细说。predicates:断言条件。Path=/api/user/**表示匹配这个路径前缀;Method=GET,POST表示只匹配这两种 HTTP 方法。多个条件之间是“与”的关系,必须全部满足才走这条路由。filters:过滤器。StripPrefix=1表示转发前去掉第一个路径段。也就是说,外部请求/api/user/list,转发到用户服务时变成/user/list。如果你的用户服务接口本来就带/api/user前缀,那就不需要 StripPrefix,视情况而定。RewritePath:正则重写路径,比 StripPrefix 更灵活。上面示例把/api/order/xxx重写成/xxx。注意 YAML 里如果segment要作为正则分组引用,需要写成$\{segment},避免被 YAML 当变量解析。
你还需要一个真实的下游服务来做验证。最简单的办法是随便起一个 Spring Boot 项目,在 8081 端口提供一个 GET /user/list 接口。没有现成服务的话,用 Python 临时起一个也行。
2.3 启动、验证与最常碰到的启动失败
启动网关:
bash复制mvn clean spring-boot:run
然后请求网关:
bash复制curl -i http://localhost:8080/api/user/list
如果下游服务活着,你会看到响应正常返回。如果下游服务没起来,你会得到一个 503 或 502。先别急着骂网关,这个现象特别典型,下一节我会专门讲 502 的排查链路。
启动过程里常见三类报错:
| 现象 | 原因 | 处理 |
|---|---|---|
| Port 8080 was already in use | 端口被占用 | 换端口或找到占用进程杀掉 |
| Spring MVC found on classpath, which is incompatible with Spring Cloud Gateway | 误加了 spring-boot-starter-web |
移除该依赖 |
| YAML 解析失败或路由不生效 | 配置缩进错误、字段名写错 | 用 IDE 的 YAML 校验,确认 routes 是列表类型 |
顺带说一句,热词里有一条“gateway service install failed: error: windows cmd launcher script cannot be”,还有很多自带了 Gateway 服务的软件在 Windows 上安装会报 Error 1920。比如某些材料计算平台自带的 Gateway 服务,安装失败绝大多数是服务账户权限不够、端口被占用、旧版本残留或计划任务被禁用。Spring Cloud Gateway 本身不需要你注册成系统服务,用 spring-boot:run 或者打成 jar 跑就行。如果你在使用的是另一款把自己装成 Windows 服务的网关类软件,先检查任务计划程序是否被组策略禁用,再检查服务账户是否有登录权限。
3. 一定会遇到的 502 Bad Gateway:从现象到根因的排查链路
3.1 502 到底是谁报的,别让网关背锅
HTTP 502 的标准含义是“网关或转发层收到上游服务器的无效响应”。问题在于:大多数时候 502 是下游服务搞出来的,但大家第一反应都是“网关挂了”。所以我排障的第一条原则是:先搞清 502 是哪一层报的。
如果客户端请求的是 nginx -> 网关 -> 服务,那 nginx 可能报 502,网关也可能报 502,位置完全不同。判断方式很简单:用 curl -v 看响应头里的 Server 字段,再结合每一层的访问日志。502 只是一句“我没拿到有效响应”,真正的原因藏在日志里。
举个例子,热词里经常出现这种报错:
text复制unexpected status 502 bad gateway: unknown error, url: http://127.0.0.1:1572/v1/responses
很多人看到 unknown error 就懵了。“未知错误”其实只是调用方的 SDK 把 502 翻译成了文字,它并没有告诉你 502 是谁产生的。URL 指向 127.0.0.1:1572,那请求就是发给本机某个服务的。这时候优先怀疑,而不是怀疑网关。
3.2 完整排查链路:从端口探测到日志闭环
我按下面的步骤来,基本能覆盖大部分 502:
第一步:直接访问目标地址。 在和你报错相同的那台机器上执行:
bash复制curl -v http://127.0.0.1:1572/v1/responses
如果直接报 Connection refused,说明这个端口根本没有进程在监听,那 502 的根源是“目标服务没起来”。如果卡住直到超时,说明端口存在但服务不响应。如果返回的也是 502,那问题在服务自身,不在网关。
第二步:确认端口监听地址。 服务起了不代表能访问,还要确认监听的是不是 127.0.0.1。用命令查:
bash复制netstat -ano | findstr 1572
如果监听地址是 127.0.0.1,那只有本机能访问;如果网关或容器跑在另一个网络命名空间里,自然连不上。Docker 场景里特别容易踩这个坑:容器里的 127.0.0.1 是容器自己,不是宿主机。比如 Windows 上跑 Docker,容器里要访问宿主机的 1572 端口,得用 host.docker.internal 或宿主机 IP,而不是 127.0.0.1。
第三步:看转发层的日志。 如果网关日志里写的是 Connection refused,那是下游没起来。如果是 upstream connect error 或者 response timeout,那是下游起来但处理不了。如果是 connection reset by peer,大概率是下游进程在请求处理过程中崩溃重启了。日志的措辞不同,排查方向完全不同。
第四步:对下游服务做针对性验证。 如果 1572 端口后面是一个模型网关或者统一模型服务,502 常见原因还有:请求体太大、响应时间过长、并发打满、配置里引用了不存在的模型路由名称。尤其是“模型路由引用”这类配置错误,网关层经常只给一个笼统的 502/404,必须去查控制面的路由表和服务端日志才能看到真相。
我给你的忠告是:502 排障,永远从“上游是谁”出发,不要一上来就改网关配置。
3.3 工程化兜底:超时、重试、熔断、健康检查
502 不可能完全避免,但可以把影响范围压小。
最常见的配置是给网关到下游的 HTTP 客户端设超时。Spring Cloud Gateway 中可以这样配:
yaml复制spring:
cloud:
gateway:
httpclient:
connect-timeout: 1000
response-timeout: 5s
pool:
max-connections: 500
max-idle-time: 30s
connect-timeout 是建立连接的超时,response-timeout 是等待响应的超时。为什么不要设成 60 秒?因为网关是共享资源,如果每个请求都挂在下游 60 秒,连接池很快被打满,后面所有请求都进不来。快速失败、快速重试,才是网关该做的事。当然,如果你的场景是文件上传或者流式接口,需要单独调整。
还可以在默认过滤器里加重试:
yaml复制default-filters:
- name: Retry
args:
retries: 3
statuses: BAD_GATEWAY, SERVICE_UNAVAILABLE
methods: GET
看到 methods: GET 了吗?这是重点。对 GET 请求重试比较安全;对 POST、PUT 这类非幂等请求,盲目重试可能会导致重复扣款、重复下单。基础不稳的时候,宁可失败也不要乱重试。
再进阶一点就是熔断。Spring Cloud Gateway 配合 Resilience4j 可以做到:下游连续失败 N 次后,网关直接短路,不再把请求打到故障服务,而是返回 fallback。这属于高阶配置,本文不展开,但你心里要有数:网关层必须兜底,但不能替代下游服务的健康检查、自动重启和容量规划。
4. 单机跑通不算完:集群部署与高可用要这样设计
4.1 先回答“网关能做集群吗”:能,而且必须能
群里经常有人问“Spring Cloud Gateway 能做集群吗”。答案是能,而且生产环境必须这么干,否则网关就是单点故障。网关本身设计成无状态服务——它不保存业务数据,路由规则、限流计数、会话状态都不该放在本地内存。无状态意味着你可以随便起多个实例,前面挂负载均衡,请求分配到任意一个实例都等价。
但“能做集群”不等于“直接起两个实例就行”。有四个地方必须处理:会话一致性、限流一致性、配置一致性和优雅上下线。
4.2 多实例最容易翻车的地方:限流、会话、配置
限流一致性。 如果你在单机测试时用的限流器是基于本地内存的,那多实例后每个实例各自计数,总量会被放大一倍。正确做法是用 Redis 集中计数。Spring Cloud Gateway 自带 RequestRateLimiter,配合 RedisRateLimiter 很成熟:
yaml复制spring:
redis:
host: localhost
port: 6379
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/api/user/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
key-resolver: "#{@userKeyResolver}"
replenishRate 是每秒补充的令牌数,burstCapacity 是桶容量。key-resolver 指定按什么维度限流,比如按用户 ID、按客户端 IP、按接口。这里的关键是:限流状态必须存放在 Redis 上,否则实例一多就失控。
会话一致性。 如果你们还在用传统的 Session 会话,那多实例就要做 Session 共享。但我建议直接放弃服务端 Session,改用 JWT 或类似的无状态令牌。网关只负责校验签名,业务服务从令牌里解析用户信息。网关集群里任何一个实例都能独立校验,不需要会话同步。
配置一致性。 集群环境千万不要每个实例手工改一份本地 application.yml。正确做法是把路由配置放到 Nacos、Consul 这类配置中心。Spring Cloud Gateway 支持监听配置变更并动态刷新路由,改路由不用重启网关。否则你有 3 个网关实例,改一处漏一处,线上一定出岔子。
4.3 部署到 K8s 时,健康检查和优雅停机必须做对
网关死在 K8s 里也是常事。比较隐蔽的问题有两个。
第一个是异常下线。实例被 kill 的时候,如果还在处理存量请求,连接被掐断,客户端就会看到 502。解决办法是配置优雅停机:
yaml复制spring:
lifecycle:
timeout-per-shutdown-phase: 30s
同时要为网关配置 readiness 探针,指向 Actuator 的健康检查接口。K8s 在滚动更新时会等待旧实例处理完存量流量才摘除,前提是你给了它“体面退出”的能力。
第二个是滚动更新的顺序。尽量保证先启动新实例、确认健康,再缩容旧实例。如果新实例还没就绪就杀掉旧实例,流量就会断档。这些细节,线上事故十有八九都是这么来的。
5. 我踩过的坑,和现在一直在用的配置习惯
5.1 路由匹配顺序:为什么你写的路由总是不生效
Spring Cloud Gateway 的路由匹配是按配置顺序来的。请求到达后,会从第一条路由开始逐个判断;一旦某个路由的所有断言都满足,就立即使用这条路由,后面的不再看。所以如果你把 Path=/api/** 放在前面,把 Path=/api/user/** 放在后面,所有 /api/user 请求都会被前面那条吞掉,后面的精确路由永远不会生效。
我的习惯是:越是具体的路由越靠前,兜底路由放最后。改路由时一定要看整体顺序,而不是只看新增的那一段。
5.2 跨域、Header 大小与连接池:低频但致命的坑
跨域配置分散在每个服务里,是很多项目的灾难。网关更适合统一处理。Spring Cloud Gateway 在全局配置里配一次就够了:
yaml复制spring:
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]':
allowedOrigins: "https://your-frontend.com"
allowedMethods: "*"
allowedHeaders: "*"
还有一类隐藏问题是请求头或 URL 长度。如果客户端带了一个超长 Header,或者路径参数特别长,Netty 的默认限制可能会直接拒绝请求,表现成 431 或者异常,极端情况下会被上层误报成 502。遇到这种诡异问题,先看是不是 Header 太大。
连接池也很容易被忽略。spring.cloud.gateway.httpclient.pool.max-connections 控制的是网关到下游服务的连接池上限。并发量高的时候如果连接池撑满,新请求会长时间等待,最终以超时或异常收场。压力测试时一定要观察这个指标。
5.3 一套可以直接抄作业的基础配置
整理一套适合中小项目的配置框架,你直接改改就能用:
yaml复制server:
port: 8080
spring:
application:
name: gateway-demo
lifecycle:
timeout-per-shutdown-phase: 30s
cloud:
gateway:
httpclient:
connect-timeout: 1000
response-timeout: 5s
pool:
max-connections: 500
acquire-timeout: 3000
globalcors:
cors-configurations:
'[/**]':
allowedOrigins: "https://your-frontend.com"
allowedMethods: "*"
allowedHeaders: "*"
default-filters:
- name: Retry
args:
retries: 3
statuses: BAD_GATEWAY, SERVICE_UNAVAILABLE
methods: GET
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/api/user/**
filters:
- StripPrefix=1
注意 uri: lb://user-service 这种写法,是配合注册中心用的。生产环境我强烈建议用服务名而不是写死 IP 地址,这样服务缩扩容、迁移机器,网关配置不用跟着改。
还有一个我几乎每个项目都会放进去的自定义过滤器:生成并透传 X-Request-Id。这样无论是网关日志、下游服务日志,还是客户端返回的响应头,都能用同一个 ID 串起来,排障效率翻倍。
java复制@Component
public class RequestIdFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String requestId = exchange.getRequest().getHeaders().getFirst("X-Request-Id");
if (requestId == null || requestId.isBlank()) {
requestId = UUID.randomUUID().toString();
}
ServerWebExchange mutated = exchange.mutate()
.request(builder -> builder.header("X-Request-Id", requestId))
.response(response -> response.getHeaders().set("X-Request-Id", requestId))
.build();
return chain.filter(mutated);
}
@Override
public int getOrder() {
return -100;
}
}
5.4 几句实在的叮嘱
网关这类基础设施,上线之后改配置要像改代码一样谨慎。我现在的习惯是:改路由前先看 diff,改完先在测试环境压一遍,压测时故意把下游服务停掉,看看网关能不能正确返回 502/503,而不会把整个网关拖垮。这个动作看着简单,但真的能帮你提前发现一大半问题。网关的价值不在于功能多炫,而在于出了事的时候,你能从它这里拿到最干净的线索。
