做AI算力平台的微服务化改造时,我见过最典型的翻车场景是这样的:训练任务调度中心因为某个GPU节点要维护,容器被调度器重新安置到新机器,原来写死在配置文件里的一组服务地址瞬间全部失效,调用方一个接一个地报连接超时,监控大屏上红成一片。那一刻你会非常清楚地意识到,在微服务架构下,服务注册与发现不是锦上添花的功能,而是像水电一样的基础设施。这篇文章记录了我带着Nacos进入Smoothcloud 润云 AI 算力平台之后,从选型、部署、配置、踩坑到上线运维的完整过程。如果你是负责平台基础设施的工程师、正在做微服务改造的技术负责人,或者只是想把“服务注册与发现”这个概念落到实处的开发者,这篇文章应该能帮你省掉不少弯路。
1. 为什么AI算力平台的微服务,比普通业务更依赖服务注册中心
1.1 当容器的IP变成“一次性用品”
普通业务系统的微服务,实例IP虽然是动态分配的,但通常一个服务绑定一台虚机,跑几个月不用变。AI算力平台不是这样。底层GPU节点是异构的,资源池随时按租户、按任务划拨,训练任务可能今天跑在这台A100节点上,明天就被调度器挪到另一台机器上。容器一旦被重新安置,原来的IP、端口全部作废,网络身份彻底改变。
更有意思的是,算力平台上的任务实例往往生命周期很短。一个数据预处理服务可能只运行几小时,分布式训练协调器可能随任务时而扩大、时而收缩。如果服务之间还靠写死的地址互相调用,每次扩缩容都要手工改配置,牵一发动全身,根本没法维持稳定。
我当时在润云平台上接手的系统就是这样:平台业务服务、训练任务服务、数据预处理服务之间互相调用,清单拉出来有二十多个微服务,没有一个服务能保证自己的IP是长期有效的。这种场景下,每个服务在启动时把自己的实际地址“报到”一个统一的地方,其他服务调用时从这里“查找”目标服务,就成了刚需。
1.2 服务注册与发现要解决的三件事
服务注册与发现本质上只做三件事:找到服务、判断健康、分配流量。
找到服务,就是服务启动时把自己实例的IP、端口、服务名登记到注册中心,消费者通过服务名反查地址列表,不再关心实例的真实网络位置。判断健康,是注册中心持续的探活机制,把失联、宕机、被调度器杀掉的实例从可用列表中摘除。分配流量,就是消费者拿到一批可用实例之后,按负载均衡策略选择具体的一个发起调用。
在AI算力平台上,第三件事最容易被人忽视。分布式训练任务经常出现单实例高负载、多实例并行的情况,如果注册中心里保留了大量已经不健康或者权重异常的实例,流量就会打到坏节点上,轻则这个训练任务减速,重则整个任务失败重来,浪费的GPU时间比业务系统故障昂贵得多。
1.3 为什么选Nacos而不是Eureka、Consul或者自研
我当时做选型对比的时候,候选方案有Eureka、Consul、Nacos,也考虑过自研一个简单的注册表服务。
Eureka的优点是简单,但它已经进入维护期,功能边界太窄,没有配置中心能力,1.x版本的心跳机制在实例规模上来之后对服务端压力很大。Consul做服务发现很成熟,但它强依赖Agent模式,在容器化环境里每个Pod都得跑一个Sidecar Agent,维护成本不低,而且和Spring Cloud Alibaba生态集成时总觉得隔了一层。自研的诱惑当然存在,但注册中心属于“看起来简单、做起来全是细节”的基础设施,光是来自不同客户端版本的兼容问题就能让人崩溃,不值得造轮子。
Nacos最终胜出的原因很实际:一是它天然支持Spring Cloud Alibaba,客户端接入成本极低;二是注册中心和控制台一体,运维团队可以直观看到服务列表、健康状态、权重配置;三是Nacos 2.x基于gRPC的长连接机制,相比1.x的大量HTTP心跳请求,对服务端压力大幅下降,更适合算力平台这种实例规模会快速增长的场景。再加上它还顺带解决配置管理问题,一套组件同时管住服务和配置,运维负担直接减半。
2. 落地前的规划:服务清单、命名空间与版本选型
2.1 先把“谁注册谁”理清楚
很多项目上来就部署Nacos,结果发现注册表里一片混乱,根本原因是没有先做服务清单梳理。在润云平台上,我先按调用关系把服务分成了三层,再决定哪些需要注册、哪些不需要。
算力资源层是真正的GPU节点和物理网络,不走服务注册,由专门的资源调度器管理。平台服务层包括API网关、任务调度中心、运行监控、用户权限、数据中台这些常驻服务,必须注册,它们面向外部调用和内部协作,实例一般2到4个。任务编排层包括训练入口、数据预处理、分布式训练协调器、结果回传等,这些实例跟着任务走,生命周期短、扩缩容频繁,更要注册,否则调度器每次挪动实例,调用方就彻底找不到它了。
这个梳理过程很关键,因为很多团队会把平台服务层和任务编排层混在一个命名空间里,训练任务的服务名和平台服务名冲突,实例健康状态互相干扰,查问题的时候一片浆糊。先按层次分开,后面做隔离和灰度都会轻松很多。
2.2 命名空间与分组:环境隔离还是租户隔离
Nacos的多级隔离机制是“命名空间 + 分组 + 服务名”的三段式结构。命名空间之间数据完全隔离,分组在同一命名空间内做逻辑隔离,服务名才是真正的实例标识。
我的经验是:命名空间用来切环境,分组用来切业务线。在润云平台上,我建了dev、test、prod三个命名空间,各自连接独立的数据库实例,开发环境随便折腾,不会影响生产。在每个命名空间内部,按业务线设置分组,比如platform表示平台业务,train表示训练任务,data表示数据处理。这样做的好处是,使用同一个Nacos集群时,不同环境之间的数据不会串,而同一环境里的不同业务线可以通过group参数快速区分。
如果未来要支持多个租户共用这个平台,可以直接为每个大租户再建一个命名空间,配合Nacos的权限控制做到彻底隔离。这个扩展路径在规划期就要留好,不然后面补隔离字段会非常痛苦。
2.3 版本选型的逻辑
版本选择上我最终选了Nacos 2.3.2。这个选择背后有几个考量。
Nacos 1.4.x虽然稳定,但客户端靠HTTP定时轮询上报心跳,实例多了以后服务端既要处理大量请求,还要维护延迟队列做健康检查,CPU和内存都会承压。2.x版本彻底换了通信模型,客户端和服务端之间建立gRPC双向流连接,心跳和状态同步都在长连接上跑,同时服务端还会主动探测客户端连接状态,服务端压力小得多,故障感知也更快。
为什么不追最新版?注册中心属于基础设施,稳定性优先级高于新功能。2.3.x是当前比较成熟的小版本系列,社区反馈的问题集中在个别边界场景,整体表现稳健。我建议生产环境选2.3.x小版本里最晚发布的那个补丁版本,同时保证客户端版本和服务端版本大版本对齐,避免出现gRPC协议兼容问题。
2.4 三节点起步的容量估算
集群规模方面,我给平台最少规划了三个Nacos节点。这不是拍脑袋,是按实例规模和连接数估算的。
当时平台服务实例加训练任务实例,高峰期预计在300到500个左右。每个gRPC长连接会维持一个连接对象,Nacos服务端还需要为主从节点之间的数据同步预留资源。理论上单节点能扛几千个连接,但算力平台经常有突发性的任务批量启动,比如某个大型训练任务一下子拉起几十个Worker,瞬间涌入大量并发注册请求。单节点扛住没问题,但会让客户端全部挤在一个节点上,一旦这个节点抖动,影响面太大。三节点可以做真正的集群选举和容错,也能利用负载均衡把不同客户端的连接分散开,降低单点压力。
3. 在Smoothcloud 润云平台上把Nacos集群真正跑起来
3.1 服务端部署:数据库、鉴权和集群配置
服务端部署这部分,网上教程很多,我只说几个算力平台上容易踩的细节。
第一个是数据库。Nacos把配置数据、服务实例元数据都存到数据库里,生产环境必须用独立的MySQL实例,不能沿用Nacos默认自带的内嵌数据库。我建了一个单独的库,账号只授权这个库的读写权限,避免和其他业务库混在一起。连接池配置要注意,默认连接数在实例多、读写频繁时会成为瓶颈,我把初始连接数调到了20。
第二个是鉴权。Nacos默认不开启鉴权,任何人都能通过8848端口读取你的服务注册列表,这在专业平台上完全不能接受。我设置了自定义密钥,并且把控制台的管理端口和客户端通信端口分开暴露,控制台只对运维网段开放。开启鉴权之后,客户端配置文件里就必须带上用户名和密码,否则服务注册会401报错。
第三个是集群配置。三节点要配置成互为主备的集群模式,而不是三个独立的单机实例。我在每个节点的application.properties里配置了集群节点地址列表,节点之间通过raft协议选主。选主这个机制比较重要,一旦主节点挂了,其他节点能快速接管,客户端连接可以自动切换到存活节点,不用人工干预。
3.2 客户端接入:Spring Cloud Alibaba的简化配置
服务端的坑填完之后,接入客户端就相对顺畅了。项目里用的是Spring Boot和Spring Cloud Alibaba,所以依赖加得很简单。我在平台公共的基础组件模块里统一引入了Nacos Discovery的starter,业务服务基本不用自己操心注册逻辑。
核心配置在application.yml里,重点说一下几个关键的字段:
spring: cloud: nacos: server-addr: nacos-svc.service.consul:8848 username: nacos password: xxxx discovery: namespace: prod group: train service: ${spring.application.name} ip: 10.20.30.40 register-enabled: true heart-beat-interval: 5000server-addr是Nacos集群对外暴露的地址,我这里用的是平台内部DNS名称,避免客户端直接绑定某一个节点IP。namespace是提前建好的生产环境命名空间ID。group填业务线。ip这个字段是重点,后面会单独展开。
注册到Nacos之后,消费者侧的调用方式也变了。原来代码里写死的HTTP地址全部改成了服务名调用,比如原来的http://10.9.8.7:8090/train/submit改成http://train-scheduler/train/submit,网关会根据服务名去注册中心拿到真实地址再转发。
3.3 多网卡环境下的注册IP处理
这是AI算力平台特有的问题,普通云环境下很少遇到。服务器上同时存在管理网卡、业务网卡、存储网卡,每个网卡都有自己的IP。Nacos客户端启动时会自动选择一个网卡地址作为实例IP,但它选的不一定是你希望对外暴露的那个地址。
我们第一次部署时就遇到这种怪现象:服务明明注册成功了,控制台里也能看到实例,但其他服务调用它就是不成功。查了半天,发现注册到Nacos的是管理网段的IP,而业务流量根本不走管理网段。
解决办法很直接:在客户端的spring.cloud.nacos.discovery.ip里显式指定业务网卡的IP。同时在服务器上配置好主机名映射,确保注册信息里记录的主机名能被其他节点解析。如果服务是容器化部署,IP要取自最终承担业务流量的那个网卡,不能图省事直接用Pod IP,除非你的网络方案保证Pod IP和宿主机网络互通。
3.4 用Open API快速确认注册是否成功
部署完服务端和客户端之后,不要急着写代码调试,先用Nacos的Open API做一次快速验证。Open API直接用HTTP调用就行,运维脚本里也经常用到。
# 查看某个命名空间下所有服务 curl -X GET "http://nacos-svc.service.consul:8848/nacos/v1/ns/service/list?pageNo=1&pageSize=10&namespaceId=prod&groupName=train" # 查看某个服务的所有实例详情 curl -X GET "http://nacos-svc.service.consul:8848/nacos/v1/ns/instance/list?serviceName=train-scheduler&namespaceId=prod&groupName=train"如果返回的实例列表里有你刚启动的服务,而且IP、端口、健康状态都正确,那说明注册链路已经通了。这一步验证到位之后,后面的调用问题基本都出在业务层而不是注册中心。
4. Nacos的心跳与健康检查机制:临时实例、持久化实例和gRPC连接
4.1 从1.x心跳包到2.x gRPC连接
Nacos的健康检测机制是理解整个服务发现行为的关键。1.x时期,客户端每5秒向服务端发送一次心跳包,服务端收到心跳后更新实例的最后心跳时间,这个机制简单但压力很大。
Nacos 2.x改成了gRPC双向流连接。连接建立之后,客户端和服务端保持一个长连接通道,状态同步在这个通道上实时传输。服务端默认每5秒检测一次客户端连接状态,如果连续15秒没有收到来自客户端的任何有效数据,就把实例标记为不健康,30秒后仍未恢复就彻底摘除。
对AI算力平台来说,这个机制最大的价值是服务端能主动感知连接断开。训练任务实例被调度器杀掉时,连接立即断开,服务端不用等心跳超时就能开始剔除流程,故障感知时间从几十秒级别缩短到秒级。
4.2 临时实例与持久化实例:AI训练任务该用哪种
Nacos把实例分成临时实例和持久化实例,这个选择直接影响健康检查的驱动方式。
临时实例由客户端主动上报心跳,服务端被动接收并更新状态。实例挂掉,心跳停止,服务端自动清除,整个过程无需人工介入。持久化实例则不一样,实例注册后不依赖客户端心跳,而是由服务端主动去探测实例的健康状况,探测失败会标记不健康,但不会自动删除。持久化实例主要面向那些不适合跑客户端的服务,比如数据库、缓存、路由器等。
在算力平台上,训练任务实例、Worker节点、数据预处理服务全部应该注册为临时实例。它们本质上都是短期存在的,生命周期由调度器控制,服务端必须自动感知上下线。平台常驻类服务用临时实例也没问题。只有数据库、Redis这类周边依赖才用持久化实例,让服务端主动探活,避免数据库短暂抖动导致注册信息被误删。
4.3 健康检查在算力平台上的调优思路
这里我要纠正一个常见误解:Nacos默认的临时实例健康检查,只看客户端连接是否存活和心跳是否上报,不看CPU、内存、负载这些指标。所以CPU被训练任务吃满,Nacos里的实例依然是健康状态,这是合理的设计,不是bug。
那问题在哪?真正需要关注的是进程活着但业务已经假死的场景。比如某个服务线程池满了,所有请求都排队,但心跳照常发送,消费者继续把流量打过来,结果就是请求大范围超时。
我的做法是把服务自身的健康检查端点暴露出来,让Nacos配合使用。Spring Boot应用启用Actuator的health端点之后,在自定义HealthIndicator里补充关键依赖的检查逻辑,比如队列积压量、数据库连接池状态、GPU显存占用率。消费者侧再配合定期拉取实例的健康状态,把“连接健康”和“业务健康”两层状态分开处理。这批服务和训练任务强相关,在我的平台上还额外检查了训练调度器的核心队列长度,队列积压太多就主动报告DOWN。
4.4 权重参数的正确用法
Nacos控制台里每个实例都能配置权重,这个值经常被误解成流量分配的百分比。它实际上只是一个数值标记,真正决定流量大小的是消费者侧的负载均衡算法。比如Ribbon的WeightedRandomRule会按权重占比分配请求,Nacos权重本身就是给这类负载均衡策略用的。
在算力平台上,权重最典型的用法是配合扩容和下线。新扩容的实例先把权重调低,比如10,观察稳定之后再调到100。要下线某个可能出现问题的实例,先把权重调成0,确认没有新请求进来再去做重启或者线程池调整。这个操作比直接摘除实例温和得多,也是服务注册与发现从“能发现”走向“会治理”的第一步。
5. 上线头两周踩到的四个坑:容器漂移、跨网段、健康误判和重连风暴
5.1 容器被重新调度后,注册的还是旧IP
第一个坑我印象最深。某次一个GPU节点要检修,几台训练Worker容器被调度到新机器,却发现调用方一直在往旧IP发请求。打开Nacos控制台看到实例列表还是旧IP,而且状态显示健康。
排查链路是这样的:先看客户端日志,发现服务启动时确实有过一次注册成功记录,但IP是容器在旧节点上的IP。再看客户端的网络配置,发现问题出在宿主机的网络代理层,容器内应用感知到的IP没有随调度更新。
根因是容器启动时把宿主机旧节点的主机名和IP写进了本地配置缓存,Nacos客户端在启动阶段解析并注册了这个地址。容器迁移之后,应用进程没有彻底重启,配置缓存还在,注册的实例自然还是旧IP。
解决方法是建立一个强制约束:所有需要注册到Nacos的服务,启动脚本里必须显式覆盖spring.cloud.nacos.discovery.ip,并且容器启动前清理宿主机名映射缓存。我当时还写了一个启动检查脚本,服务启动后自动调用Open API比对注册IP和当前实际IP,不一致直接通知告警。
5.2 管理网段和业务网段之间的跨网注册
第二个坑和网络架构有关。润云平台把管理网段和业务网段做了隔离,Nacos集群部署在管理网段,训练任务服务跑在业务网段。客户端能连上Nacos完成注册,但控制台里看到的实例IP是业务网段地址,从管理网段的监控平台去访问这个地址完全不通。
这个坑的典型症状是:服务注册没问题,实例状态健康,但是从运维平台发起测试调用失败。需要跨网段访问这些服务时,要么临时加路由规则,要么通过代理跳转。长期解决方案是在注册层的网络上做好规划。把Nacos集群同时接入核心交换机的两个网段,或者让注册信息里的IP使用一个全局可路由的浮动地址。
我在这个项目里的做法是,把业务服务注册的IP统一为业务网段地址,同时部署了一个分发网关作为跨网段调用的统一入口。消费者需要访问业务服务时,全部走网关,网关再从注册中心取实例地址发起调用,这样既不用打通所有网段,也让注册表保持干净。
5.3 进程活着但业务已经死了:自定义健康检查
第三个坑是服务假死导致的流量误分发。现象是有一个数据预处理服务,进程一直在跑,Nacos里也显示健康,但消费者调用它的接口时长时间无响应。查日志发现这个服务的线程池已经满了,所有工作线程都在等待一个训练数据集锁,根本没能力处理新请求。
这正是前面说的健康检查只看连接、不看业务的问题。我后来在服务里加了健康检查端点,把线程池活跃度、等待队列深度、最近一次任务完成时间纳入了判断逻辑。同时调整了消费者的调用策略,在从Nacos获取实例列表后,对每个实例额外做一次轻量级的探活请求,过滤掉业务不健康的实例。
这个改造不是把Nacos换掉,而是在它之上增加一层业务健康感知。注册中心负责管理网络层可用性,业务层可用性靠服务自身的健康端点来反馈,两层配合才是完整的健康体系。
5.4 gRPC重连风暴:网络抖动引发的“雪崩”
第四个坑出现在一次网络割接之后。割接过程中部分交换机发生短暂抖动,大量客户端和服务端之间的gRPC连接几乎同时断开。连接恢复后,所有客户端同时尝试重连Nacos集群,注册中心的CPU瞬间飙高,部分节点出现请求响应超时。
这就是重连风暴。统计下来当时有上百个客户端实例同时发起注册请求,加上重新拉取服务列表,控制面和数据面流量瞬间打满。好在Nacos的集群选主机制扛住了压力,没有全挂,但响应延迟明显变长。
后来我优化了两点:一是调整了客户端的重连参数,把断线重试的退避间隔调大,避免所有客户端同频重连;二是给Nacos服务端配置了更合理的线程池大小,提高高并发注册请求下的吞吐能力。好在这类割接窗口一般都在业务低峰期,真实影响被控制住了,但这个案例让我意识到,基础设施的抗抖动能力和容量评估同样重要。
6. 上线后的运维清单,以及向服务治理延伸的几步
6.1 巡检看什么:像体检一样给Nacos做检查
服务注册与发现跑起来之后,日常运维不能只看注册中心本身的进程状态。我整理了一份巡检清单,核心指标有四个。
第一个是实例数量变化。每天早上对比当前服务实例数和预期数量,发现异常减少要立刻排查是不是有任务批量失败。第二个是心跳成功率。Nacos控制台里能看到每个服务的健康实例比例,如果某个服务的健康实例比例长期低于80%,说明这个服务的稳定性有问题。第三个是配置变更记录。Nacos的配置管理功能有变更历史,定期检查有没有不合规的配置改动。第四个是存储状态。数据库磁盘使用率、Nacos节点日志大小、文件句柄数量都要盯。
另外我会写一个简单的自检脚本,用Open API定时查询所有核心服务的健康实例数,低于阈值直接触发告警。这个脚本比登录控制台手工看高效得多,也方便沉淀成平台运维工具的一部分。
6.2 容量、高可用和同城双活的一些经验
运行几个月之后,我有几个容量和可用性方面的心得。
连接数是容量扩容的第一参考指标。单节点维持上千个gRPC连接没有问题,但控制台、Open API、配置读写都会额外消耗资源,所以实际容量要打折扣。实例规模到1000左右时,三节点集群还能从容应对。再往上就需要考虑服务端参数调优和集群拆分。
高可用方面,Nacos集群本身具备故障转移能力,节点挂了客户端会自动连接其他节点。但要注意,客户端和服务端的版本一致性会影响切换的平滑度,尽量让所有客户端用同一个大版本。同城双活场景下,如果条件允许,可以把Nacos集群节点分散在两个机房,降低整体故障概率,前提是网络延迟要控制在可接受范围内。
6.3 从注册中心走向服务治理:标签路由、优雅上下线与灰度
服务发现稳定之后,下一步就是做服务治理了。我在平台上开了几项比较实用的能力。
标签路由:给实例打上环境标签、版本标签、GPU类型标签,比如gpuType=A100、version=latest。网关和调用方可以根据请求头里的标签条件,精准路由到对应实例。这个能力在做新旧算法对比测试时非常好用,新旧版本实例可以同时在注册中心共存,流量按标签分流。
优雅上下线:服务下线前先向注册中心发送注销请求,把实例状态置为下线,再等待存量请求处理完再真正停止进程。这个流程能避免训练任务正在执行到一半时被打断。Nacos控制台的连权重调整功能配合优雅下线,能做到零流量中断的发布。灰度发布场景我可以先调整路由权重,逐步把流量切到新版本实例,观察任务成功率再决定是否全量切换。
6.4 扩容后如何快速确认实例已上线
最后分享一个小技巧:用一条命令确认扩容实例是否被正确注册。算力平台经常在任务高峰期临时扩容,人工登录控制台确认既慢又容易漏。
我写的脚本思路是这样的:调用Open API拿到某个服务当前实例列表,再用jq解析出实例IP和状态,和预期列表比对。这个逻辑可以轻松集成到平台的自动化扩缩容流程里,扩容时自动确认实例注册,缩容时自动确认实例摘除。
curl -s "http://nacos-svc.service.consul:8848/nacos/v1/ns/instance/list?serviceName=train-scheduler&namespaceId=prod&groupName=train" \ | jq -r '.hosts[] | "\(.ip):\(.port) healthy=\(.healthy)"'这套东西用了一段时间后,我最大的感受是服务注册与发现的价值不只体现在故障时刻的快速恢复,更体现在日常变更的从容。每次训练任务批量启动、每次扩容缩容,Nacos都是我们唯一信任的“名册”。它不承担具体业务的复杂逻辑,但如果没有这张名册,AI算力平台上所有微服务之间的协作都会陷入混乱。对这个项目来说,前期规划阶段多花两小时把服务清单和命名空间理清楚,远比后期排查注册混乱要划算得多。