做微服务网关改造的时候,最让人头疼的一步就是把网关和注册中心打通。路由如果写死 IP,服务一扩容就得改配置重启;服务挂了,网关还在往旧地址转发,线上告警能吵到你怀疑人生。把 gateway 整合 nacos 之后,服务实例的上下线、扩容缩容,网关都能自动感知,路由跟着服务列表走,整套链路才算真正活起来。
这篇东西适合正在做微服务拆分、或者网关已经搭好但路由还在用静态配置的人。不管你是用 Spring Cloud 全家桶,还是只打算把网关层单独拎出来接注册中心,只要网关走的是 gateway 这类组件,注册中心用 Nacos,下面这套整合思路基本都能直接参考。我会从依赖选型、路由配置、动态刷新到问题排查一路讲下来,重点说那些配置以外的坑。
1. 为什么网关非要和注册中心联动
1.1 静态路由的痛点
先说一个最常见的场景:网关刚搭起来的时候,为了快点跑通,路由配置可能是这样的:
spring: cloud: gateway: routes: - id: order-service uri: http://192.168.1.10:8081 predicates: - Path=/api/order/**写在当时没啥毛病,跑起来也能转发。但用上一段时间你就明白了,这套配置在真实环境里基本撑不住。举个例子,订单服务原来只有一个实例,后来业务量上来要做水平扩容,加了三台机器。你想想接下来要做什么:改网关配置,把三个 IP 都写上,然后重启网关。如果线上有四五个服务、每个服务好几个实例,这个维护量直接爆炸。
更难受的是服务下线的情况。发布过程中某台机器先摘流量,网关如果还往那台机器的 IP 转发,请求就会超时或者报连接被拒。网关这种组件一错过故障通知,前端用户感受到的就是“系统挂了”,但实际上只是某个服务实例下线了。
静态路由还有一个隐性成本:多环境配置冲突。开发环境、测试环境、生产环境的服务 IP 完全不同,每套环境你得单独维护一套路由配置,命的是不同的 IP 列表。稍微改一个端口,整套配置就得跟着改一遍,非常容易出错。
1.2 Nacos 在链路里的角色
把网关接到 Nacos 之后,路由不再关心“服务在哪台机器上”,只需要关心“服务叫什么”。网关通过 Nacos 拿到服务实例列表,然后根据负载均衡策略选一个实例进行转发。这样上面说的扩容、下线、环境差异,全部被注册中心抽象掉了。
Nacos 在这里同时扮演两个角色。一个是注册中心:各个微服务启动的时候把自己的 IP 和端口上报到 Nacos,服务下线的时候再摘除。另一个是配置中心:网关的路由规则可以放到 Nacos 配置中心管理,修改配置后网关不用重启,配置中心把变更推给网关,网关内部触发路由刷新事件。
很多刚接触的人会把这两个角色混在一起,理解成“只要服务注册到 Nacos,网关就能转发”。这个说法对,但不完整。网关能发现服务靠的是注册中心,但“路由规则从哪来”则是另一件事——你可以把路由规则写在本地配置文件里,也可以放在配置中心里。这两个职责可以分开,也可以都用 Nacos 承载,取决于你对动态性的要求。
1.3 网关侧是怎么感知服务变化的
如果服务实例变化了,网关怎么知道?这个问题值得展开讲一下,因为不少人在排查问题时会漏掉“缓存”这一层。
网关里负责和注册中心交互的组件,会定时向 Nacos 拉取服务列表,并在本地维护一份缓存。服务注册、心跳续约、实例下线这些变更发生后,Nacos 服务端会通知各个订阅方,网关收到通知后刷新本地缓存。但这里有一个时间差:从实例真实下线到网关完成缓存刷新,中间可能有几秒到十几秒的窗口期。网关如果在这期间把请求转发到已经下线的实例,就会出现偶发的失败。
理解了这个机制,你就知道为什么有些小短暂报错用“重试”就能掩盖过去,而不是一上来就怀疑代码写错了。
2. 依赖选型与版本搭配
2.1 网关工程的依赖怎么加
把 gateway 整合 nacos,首先要在网关工程里引入两个核心依赖:Spring Cloud Gateway 的 starter,以及 Nacos 服务发现的 starter。我这里用 Maven 举例:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>有一点必须提醒:Gateway 底层是 WebFlux,不是 Spring MVC,所以在网关工程里不要引入 spring-boot-starter-web 或者 spring-boot-starter-webmvc。如果同时引入,启动时会直接冲突,报一堆莫名其妙的异常,最常见的是 “Spring MVC found on classpath, which is incompatible with Spring Cloud Gateway”。
另外,如果你只打算从注册中心拉服务实例,不打算用 Nacos 的配置管理功能,那上面两个依赖就够了。如果你想把路由规则也放到 Nacos 配置中心,还需要额外引入:
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency>2.2 版本组合怎么选
版本组合是整合工作里最容易被忽视的隐性地雷。很多人从网上随便复制一段 yml 配置,结果起不来,大概率不是配置写错了,而是版本对不上。
我这里给一个比较稳的组合参考:
Spring Boot 2.6.x Spring Cloud 2021.0.x Spring Cloud Alibaba 2021.0.5.0 Nacos Server 2.2.x这个组合在实战里跑得比较顺,接口变化少,网上资料也多。如果你用的版本比较新,比如 Spring Boot 3.x 配 Spring Cloud 2022.x,那上面的依赖坐标会不一样,配置项的前缀也可能有调整,不能直接照抄。
有一个非常关键的细节:spring-cloud-starter-alibaba-nacos-discovery 这个依赖的版本号,是跟着 Spring Cloud Alibaba 走的,不是跟着 Spring Boot 走的。你在引入依赖的时候,要么在父 pom 里显式声明 Spring Cloud Alibaba 的 BOM,要么声明具体版本号。不要只写 starter 不写版本,否则 Maven 会拉到一个和你 Spring Boot 完全不兼容的版本。
建议在父工程里做统一的版本管理:
<dependencyManagement> <dependencies> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2021.0.5.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>2.3 服务端 Nacos 的部署检查
网关工程本身不需要装什么额外的客户端,但 Nacos 服务端你得提前确认几个点。
首先确认 Nacos 服务端版本和客户端版本要兼容。Nacos 2.x 的客户端和 1.x 的客户端协议不一样,2.x 引入了 gRPC 长连接,同时保留了对 1.x 的兼容,但如果你用的是非常老的 1.x 客户端连接 2.x 服务端,部分功能可能异常。建议客户端和服务端都保持在 2.x 系列。
其次,如果你的 Nacos 部署在生产环境,记得确认两个端口都通了:主端口(默认 8848)和客户端 gRPC 端口(偏移量 +1000,即 9848)。如果只开了 8848,服务能注册上,但客户端和服务端的长连接会失败,表现为“服务列表偶尔拉不到,刷新后又有”。这个问题很难查,第一次遇到时我排查了很久,最后发现是防火墙只放行了 8848。
点一下这个细节,后面排查环节还会再说一次。
3. 核心配置:路由改造与服务发现
3.1 bootstrap 和 application 里的配置怎么分
如果你的网关工程只接了 Nacos 注册中心,不接配置中心,那所有配置都写在 application.yml 里就行。结构大概是这样:
server: port: 8080 spring: application: name: gateway-server cloud: nacos: discovery: server-addr: 127.0.0.1:8848 namespace: dev group: DEFAULT_GROUP gateway: discovery: locator: enabled: true这里有一个配置要特别说明一下:spring.cloud.gateway.discovery.locator.enabled。
如果把它设为 true,意味着网关会自动根据注册中心里所有服务名称生成路由。比如 Nacos 里有一个叫 order-service 的服务,网关自动就能通过http://网关地址/order-service/**转发到 order-service 的实例上。好处是不用手动写路由规则,服务多了以后省事。坏处也很明显:所有服务都直接暴露在网关下,路径和服务名强绑定,有时候并不符合预期的 URL 设计。
我个人的建议是:locator.enabled适合前期快速联调,但正式项目里还是手动维护路由规则比较好。原因很简单,网关是所有流量的统一入口,你需要在入口层就把路径规划好、管控好,而不是让服务名直接裸露。
如果你要用 Nacos 配置中心来管理路由规则,那 Nacos config 的地址得放到 bootstrap.yml 里。因为在 Spring Boot 2.4 之后,虽然 bootstrap 机制默认不再启用,但 Nacos Config 还是习惯走 bootstrap 来提前加载配置。你需要额外引入 spring-cloud-starter-bootstrap 依赖,并配置 bootstrap.yml:
spring: application: name: gateway-server cloud: nacos: config: server-addr: 127.0.0.1:8848 namespace: dev group: DEFAULT_GROUP file-extension: yaml有些新版本也可以用 spring.config.import 的方式加载 Nacos 配置,写法不太一样。如果你和我一样用 bootstrap 老方案,记得加 spring-cloud-starter-bootstrap 依赖,不然 bootstrap.yml 根本不生效,配置中心连接不上,路由也拉不下来。
3.2 把静态路由改造成 lb:// 路由
这是 gateway 整合 nacos 最核心的一步:路由的 uri 从具体 IP 改成lb://服务名。
lb://order-service表达的意思是:网关要转发到名为 order-service 的服务上,具体转发到哪个实例,由负载均衡组件从注册中心拿实例列表后选择。
spring: cloud: gateway: routes: - id: order-service-route uri: lb://order-service predicates: - Path=/api/order/** filters: - StripPrefix=1这里的StripPrefix=1是经常会用到的过滤器。它的作用是把路径前缀去掉一层。举个例子,外部请求的路径是/api/order/order/list,经过StripPrefix=1之后变成/order/order/list,然后转发到 order-service。
为什么要这样设计?这其实是把“对外暴露的路径”和服务提供方的“内部路径”解耦。外部用户看到的接口路径由网关统一规划,服务方内部可以随时调整自己的接口路径,只要网关这边的映射规则同步调整就行。这在微服务架构里是一个很常见的设计思路:网关负责路由收敛,服务方不需要关心外部路径长什么样。
如果你不写 StripPrefix,那请求的完整路径会原样转发到服务端。也就是说,order-service 的接口如果定义的是/order/list,但你通过/api/order/order/list去访问,服务端会收到/api/order/order/list,大概率 404。这个细节是新手最容易踩的坑。
3.3 负载均衡是怎么串起来的
有人会问:网关的负载均衡和 Nacos 有什么关系?答案在底层的调用链里。
当网关把路由的 uri 配置成lb://order-service时,它使用的是 Spring Cloud LoadBalancer 组件。这个组件在处理请求时,会去调用服务发现客户端,也就是 Nacos 服务发现客户端,来获取 order-service 的实例列表。然后根据负载均衡策略选出一个可用的实例,再把请求转发过去。
如果你没有配置任何负载均衡策略,默认走的是轮询。那网关怎么知道各个实例的权重呢?其实 Nacos 注册中心本身支持给实例设置权重,权重高的实例会被分配更多流量。很多人在 Nacos 控制台里看到实例的权重列,但不知道它到底怎么影响网关的流量分配。
这里要提醒一下:Nacos 控制台里配置的实例权重,是 Nacos 服务端在“返回客户端实例列表”时排序和筛选的依据之一。但网关最终用的是 Spring Cloud LoadBalancer 自己的策略,默认轮询,不会因为你把某个实例权重调成 10 就一定多分流量。要让权重生效,你得在网关里配置一个能识别权重的负载均衡策略,或者结合 Nacos 的实例元数据自己实现选择逻辑。
简单来说,网关和 Nacos 的负载联动有两层:一层是“实例发现”,由 Nacos 负责;另一层是“选择策略”,由 Spring Cloud LoadBalancer 负责。这两层要分清,不然你调整了 Nacos 的权重却发现流量分布没变化,就会一头雾水。
4. 路由动态刷新与配置中心集成
4.1 路由规则放进 Nacos 配置中心
路由动态刷新是这个整合方案里最值钱的能力。路由规则一旦写死在网关本地,任何调整都意味着重启。但放到 Nacos 配置中心之后,配置改了,网关能收到变更通知,然后刷新路由而不重启进程。
配置中心的思路是这样的:路由相关的配置不再写进网关工程的 application.yml,而是写到 Nacos 配置中心的一个 dataId 里。dataId 的命名通常遵循spring.application.name+-+profile+.+file-extension的规则,比如:
gateway-server-dev.yaml内容就是网关应用在 dev 环境下的完整配置,包括路由规则。为了让路由发生变更时触发刷新,还需要在网关主类上加上刷新路由的代码:
@Configuration public class GatewayRouteRefreshConfig { @Autowired private ApplicationEventPublisher eventPublisher; @RefreshScope @Bean public NacosWatch nacosWatch(NacosDiscoveryProperties nacosDiscoveryProperties, ObjectProvider<ApplicationEventPublisher> publisher) { return new NacosWatch(nacosDiscoveryProperties, publisher); } @Component public static class NacosRouteRefreshListener implements ApplicationListener<RefreshRoutesEvent> { @Override public void onApplicationEvent(RefreshRoutesEvent event) { // 路由由 Nacos 配置中心管理,Nacos 配置变更后,触发路由刷新 } } }上面的代码不是唯一写法,但它体现了关键点:路由刷新依赖RefreshRoutesEvent,而RefreshRoutesEvent需要配置变更事件去触发。Nacos 配置中心推送配置变更后,Spring Cloud 的NacosWatch会感知到变化,进而触发路由刷新。
还有一种更干净的做法:监听 Nacos 配置变更事件,手动调用routeDefinitionWriter重新加载路由。这个方案适合路由规则非常复杂,或者你需要在刷新前后做额外处理的场景。普通情况用上面的事件监听方案就够了。
4.2 配置变更到路由生效的完整链路
整个动态刷新链路是这样的:
修改 Nacos 配置中心的 YAML,保存。Nacos 服务端把配置变更推送给网关客户端。网关这边的监听器收到变更,重新加载配置。配置重新加载后,Spring Cloud Gateway 的路由定义会被更新。新的请求按照新路由规则路由,整个过程网关进程不重启。
这里有个细节很多人会忽略:Nacos 配置中心推送配置变更之后,网关内部的路由定义并不是立刻全部替换的。它会重新解析配置文件中 routes 段的全部内容,然后和已有的路由做比较,新增的加进去,删除的移除掉。所以如果你只想修改一条路由的路径,也要保持整个 routes 段格式正确,不然整个解析会失败,最坏的情况是路由全部丢失,线上直接大面积报错。
真正的线上环境里,我建议在 Nacos 配置中心修改路由后,先观察网关日志。网关在刷新路由的时候,会输出路由刷新相关的日志。你看到路由数量和预期一致,再放量。不要保存配置后立刻发压测,万一格式有问题,可能把所有路由都搞没了。
4.3 动态跑通的实例演示
给你看一个我实际用下来的配置组织方式。
网关工成本身的 application.yml 只保留 Nacos 的地址信息,路由不出现在这里:
server: port: 8080 spring: application: name: gateway-server cloud: nacos: discovery: server-addr: 127.0.0.1:8848 gateway: discovery: locator: enabled: false然后我在 Nacos 配置中心创建了一个 dataId 为gateway-server.yaml的配置:
spring: cloud: gateway: routes: - id: user-service-route uri: lb://user-service predicates: - Path=/api/user/** filters: - StripPrefix=1 - AddRequestHeader=X-Gateway-User, ${user}这样调整路由映射关系的时候,只需要打开 Nacos 控制台修改这一段 YAML,保存后网关会自动感知新路由,完全不用碰网关进程。这个方案在服务数量多、路由变化频繁的场景里非常实用。
线上环境还有一个更稳的玩法:把配置中心的路由 YAML 再配上校验和版本管理。Nacos 配置中心本身有配置历史版本功能,改坏了可以一键回滚,这点是本地配置文件完全比不了的。
4.4 业务服务不可用时网关怎么办
路由配好了,但服务实例挂了怎么办?前面说过,服务下线后,网关不会立刻感知,会有一小段窗口期。为了减少这个窗口期对用户的影响,你需要在超时和重试上做文章。
Gateway 的超时配置是指整个转发过程的处理超时,一般用全局配置:
spring: cloud: gateway: httpclient: connect-timeout: 3000 response-timeout: 5sconnect-timeout是建立连接的超时时间,response-timeout是等待响应的超时时间。服务实例挂掉时,通常表现为连接超时或者请求直接失败。调短超时时间,可以让网关更快地放弃一个不可用的实例,然后由负载均衡组件换一个实例重试。
重试过滤器可以加在路由上:
filters: - name: Retry args: retries: 3 statuses: BAD_GATEWAY methods: GET注意,我建议重试只对 GET 这类幂等请求开。POST、PUT 这种写操作一旦重试,可能导致业务数据重复创建或重复扣款。这个风险,分布式系统下尤其需要注意。
5. 常见问题与排查实录
5.1 服务能注册到 Nacos,但网关转发 404
这个场景我遇到过一次。现象是:你在 Nacos 控制台能看到服务实例,网关也能正常启动,但通过网关访问具体接口时,返回 404。
排查思路先从“网关有没有成功发现这个服务”开始。用网关的 actuator 端点查看路由:
GET http://网关地址/actuator/gateway/routes如果你能看到 lb://order-service 路由,但请求还是 404,那问题多半在路径上。最常见的原因就是前面的 StripPrefix 配置没写对。外部请求路径是/api/order/list,你只配置了Path=/api/order/**,没配置 StripPrefix,那么后端收到的路径就是/api/order/list。如果后端的真实接口是/order/list,自然 404。
还有个隐藏原因:网关从 Nacos 拿到的服务名,和你配置的 lb:// 服务名不一致。比如 Nacos 里注册的服务名是 order-server,但你路由写的是 lb://order-service,那网关根本找不到这个服务,表现也会是 404。
5.2 网关返回 503 而不是 404
503 的语义是“服务的实例不可用”。这个状态码比 404 更容易定位,它说明路由已经匹配成功了,但负载均衡组件找不到可用的服务实例。
发生 503,先查两件事。第一,服务是不是真的注册到了 Nacos。第二,网关的 Nacos 服务发现客户端配置和服务的配置是否匹配。
有个非常典型的问题:Nacos 的 namespace 不一致。服务注册在 dev 命名空间,网关连接的是默认的 public 命名空间,两边不是一个空间,服务自然不可见。在网关接入 Nacos 的时候,namespace 配置尤其要检查清楚:
spring: cloud: nacos: discovery: server-addr: 127.0.0.1:8848 namespace: dev如果服务端和客户端 namespace 不一致,你在 Nacos 控制台能看到服务列表,网关却拿不到服务实例,最终表现为 503。
5.3 服务上下线后,网关没有及时感知
服务在 Nacos 上下线了,但网关还是访问到旧的实例或者访问不了新的实例。这属于服务发现缓存问题。
Nacos 客户端维护了本地服务列表缓存,这个缓存的刷新周期可以通过配置调整:
spring: cloud: nacos: discovery: server-addr: 127.0.0.1:8848在 Nacos 2.x 中,服务端变更会通过 gRPC 长连接主动推送给客户端,网关能比较快地感知。但如果网络环境复杂,长连接断了,客户端会回退到轮询模式。这种情况下的感知速度就会慢很多。
建议是:发布服务的时候,先摘掉 Nacos 上的实例权重,或者直接调用 Nacos 的配置下线接口,等流量停掉之后再停机。这比依赖网关自动感知要可靠得多。网关的自动感知能力是一种兜底手段,但不能把它当成微服务发布流程里的唯一保障。
5.4 配置中心的路由修改了,但网关没有刷新
这个问题的常见原因有三个。
第一个,bootstrap.yml 没生效。前面提过,Spring Cloud 2020.0.3 之后,bootstrap 默认不再启用。如果你要用 bootstrap.yml 加载 Nacos 配置中心,必须引入 spring-cloud-starter-bootstrap 依赖。
第二个,RefreshScope相关的配置没加对。路由刷新需要对配置变更做出响应,如果只是把路由写在 Nacos 配置中心,但没有对应的刷新机制,改了也不会生效。
第三个,dataId 的命名和网关应用名不匹配。比如应用名是 gateway-server,dataId 却叫 gateway-server-dev.yaml,而 profile 和 group 对不上,Nacos 客户端匹配不到配置,自然不会加载,更不会刷新。
5.5 常见问题速查
| 现象 | 大概率原因 | 排查方向 |
|---|---|---|
| 网关返回 404 | 路由路径匹配后端接口失败;StripPrefix 配置错误 | 看网关 actuator 路由列表,检查路径前缀 |
| 网关返回 503 | 服务未注册;namespace/group 不一致 | 对比服务端注册配置,检查 nacos 发现配置 |
| 服务调用偶尔失败 | 服务下线到网关感知存在时间窗口 | 发布流程先摘流量,做好网关超时和重试 |
| 路由修改不生效 | bootstrap 未启用;刷新事件未触发 | 确认 bootstrap 依赖,检查监听器配置 |
| 启动创建 Bean 冲突 | Gateway 环境混入了 spring-boot-starter-web | 移除 web starter,保留 webflux |
6. 实操心得与建议
我实际操作中最大的感受是:网关整合 Nacos 这件事,配置写起来很快,真正花时间的是理解网关内部的路由模型和刷新机制。
先说路由模型。网关的路由由 id、uri、predicates、filters 四个要素组成。id 唯一标识一条路由,uri 决定转发目标,predicates 决定请求是否匹配,filters 对匹配的请求做处理。你在配置中心看到的 YAML,本质上就是在描述这四个要素。理解了这一点,你就不太会被各种文章里的示例配置搞懵,因为它们再怎么变,核心都是这四个要素的组合。
再说刷新机制。很多人以为“配置变了,网关就会自动把路由改好”,实际情况是,这种自动刷新需要完整的链路配合。配置中心的配置变更要先被客户端监听,然后触发路由定义的重建,最后是路由定位器的刷新。任何一个环节断了,动态刷新就失效。这也是为什么我强烈建议在线上验证一下“改一条路由,看是否实时生效”,而不是默认事情会自然发生。
最后分享一个我用下来很顺手的小技巧。正式环境网关路由调整时,先不要直接改线上配置中心的 YAML。可以把要调整的内容先加到一个测试分组或者备用 dataId 里,通过修改本地 profile 来加载验证,确认生效之后,再把配置落回正式分组。这样能减少直接把线上路由改坏的概率。
做网关这个东西,没有太多花哨的技巧,稳字当头。路由规则尽量精简,配置中心的内容尽量结构清晰,每个路由的 id 命名要有规律。等你在生产环境跑个半年,你会发现,当初花时间把网关和 Nacos 这套链路理顺,是完全值得的。