Nacos作为Spring Cloud生态里最常见的服务注册中心,一直有一个让很多人困惑的设计:它同时支持AP模式和CP模式,而且不同模式下注册实例的行为差异很大。我最早在线上排查过一次“服务明明注册成功,调用方就是发现不了”的问题,追到最后才发现是AP模式的异步同步和CP模式的Raft提交在背后各管一摊。很多人翻了不少文档,知道Nacos有这两种模式,可真问到“内部怎么实现的”“切换到底改哪个配置”又讲不清楚。这篇文章就围绕AP、CP模式的实现机制展开,同时把切换方式和生产环境里容易踩的坑一起理清,适合正在用Nacos做服务注册发现、或者面试前想系统梳理这块知识的开发者。
1. 注册中心的一致性困局:AP和CP到底在争什么
1.1 CAP不是用来选边站的,注册中心两种诉求都真实存在
CAP理论讲的是在网络发生分区时,一致性和可用性只能保一头。这里说的“分区”不是业务上的分库分表,而是指分布式系统里节点之间网络断了、消息丢了,整个集群被切成几个互不相通的孤岛。在这种情况下,如果系统继续对外服务,就可能出现不同孤岛对同一份数据给出不同答案;如果坚持所有节点数据一致,就必须拒绝一部分请求,直到网络恢复。
很多技术文章喜欢把AP和CP说成“非黑即白的选择”,但在注册中心这个场景里,两种诉求都是切切实实存在的。举个生活化的例子:你打开导航软件查实时路况,偶尔晚几十秒看到前方拥堵,你可以接受,因为导航只要持续可用就比一两秒的准确更重要,这是典型的AP诉求。但你用扫码支付时,系统必须先确认账户里确实有钱、这笔交易没有被重复提交,如果确认不了,宁可让你稍后再试,也不能给你一个错误结果,这就是CP诉求。
服务注册中心也一样。普通微服务的上下线,允许有短暂延迟——一个新节点注册成功后,过一两秒才被其他调用方发现,这通常没太大问题;但你不能因为某个节点之间数据没同步完,就让整个服务发现不可用。可如果这个服务列表还承担着路由规则、主从状态、分布式锁这类强一致语义,那延迟几秒可能就会出大事故。Nacos面对的就是这种“两边都不能丢”的场景,于是它没有走单一模式,而是把两种一致性协议都内化到了系统里,靠实例类型来分流。
1.2 Nacos的分界点不是集群,而是实例类型
我先说一个很容易被误解的点:Nacos并不提供一个全局按钮,让你把整个集群从AP切成CP。它真正干的事情是,在单个实例注册时,根据ephemeral字段来决定走哪套一致性协议。
ephemeral=true的临时实例:走AP模式,对应Distro协议。ephemeral=false的持久实例:走CP模式,对应JRaft协议。
这种设计的直接好处是同一个Nacos集群里,可以同时跑着对一致性要求完全不同的服务。注册中心本身不需要因为少数几个强一致场景,就牺牲整个集群的可用性;反之,也不能因为大多数场景追求高可用,就让真正需要严格一致的数据也跟着“松”下来。
这两个模式在很多维度上的差异都不小,我整理了一张表,方便对照:
| 对比项 | 临时实例(AP) | 持久实例(CP) |
|---|---|---|
| 数据一致性 | 最终一致,写入成功后可容忍短暂不可见 | 强一致,多数派确认后才返回成功 |
| 底层协议 | Distro(自研,无主模型) | JRaft(基于Raft的选主模型) |
| 数据存储 | 节点内存,不落盘 | 节点磁盘+状态机,Raft日志复制 |
| 健康检查 | 客户端主动心跳续约,超时即被剔除 | 服务端主动探测,探测失败标记不健康 |
| 写入性能 | 高,本地写即返回 | 相对低,需要多数派复制确认 |
| 分区容忍性 | 分区期间各分区仍可独立服务 | 分区期间失去多数派时停止写入 |
| 典型使用场景 | 普通微服务注册发现 | 配置、选主、分布式协调等强一致场景 |
1.3 对“模式切换”最朴素的理解
既然分界点在实例的ephemeral属性上,那么所谓的“AP、CP模式切换”,在日常操作层面,就是把服务实例从临时实例切换成持久实例,或者反过来。
但这里有一个很多人问过的问题:同一个服务能不能一部分实例是临时实例、一部分是持久实例?技术上,Nacos允许两者在同一个服务名下共存,因为临时实例和持久实例走的是两套存储和同步模块,互不干扰。但从工程实践看,我强烈不建议这么做。一旦一个服务混用两种模式,你会面临两个不一致的视角:AP侧认为某实例已经下线了,CP侧还认为它存在;服务端主动健康检查的结果和客户端心跳的结果也可能出现矛盾。后面排查问题的时候,你根本分不清到底是网络问题还是模式混用导致的。所以,模式切换要切就整个服务一起切,不要搞“部分临时部分持久”的过渡态。
2. AP模式的实现:Distro协议是怎么让注册中心“一直可用”的
2.1 无主设计,每个节点都能扛写请求
AP模式在Nacos中的实现叫Distro协议,这是Nacos自研的分布式协议,最早吸收了阿里内部ConfigServer和EDAS的设计思路。它和Raft这类强一致协议最大的区别在于:Distro没有主节点,所有节点地位等同,任何一个节点都可以独立处理客户端的注册、心跳、注销请求。这个“无主”特性,保证了AP模式下注册中心不会因为某个节点挂了而失去写入能力。
它的数据同步方式是副本式的。每个节点都会在本地内存维护一份服务列表的副本,写入操作先在当前节点生效并返回客户端成功,然后通过异步任务把变更推送给集群里的其他节点,最终达到全局一致。新加入的节点启动时会先从已有节点拉取全量数据,保证本集群每个节点的服务列表起点一致。
这个设计里有个关键点:写入成功和“其他节点知道这条数据”不是同一时刻发生的。客户端感知到的“注册成功”,只代表当前节点接受了你的请求,不代表集群里所有节点都已经有了这条数据。这也是生产环境里“明明注册成功却偶尔发现不了服务”这一问题的根源。
2.2 临时实例从注册到被其他服务看见的完整链路
一个临时实例的完整生命周期可以拆成四步:
- 客户端发起注册请求。Nacos 2.x默认走gRPC长连接,底层端口是主端口+1000,比如8848对应的gRPC端口是9848;Nacos 1.x则走HTTP接口。
- 服务端节点解析请求后,把实例信息写入自己的本地内存,同时记录该实例的心跳超时时间。这一步完成后,客户端就会收到注册成功响应。
- 节点把这次写入放进Distro任务队列,异步推送给其他节点。任务推送有批量合并机制,不会每个请求都立刻同步一次,而是攒一批或定时同步。
- 其他节点收到同步数据后更新本地缓存。调用方通过任意节点查询服务列表时,返回的是该节点本地缓存中的结果。
从第2步到第4步之间的这个时间差,就是AP模式下的“不一致窗口”。正常情况下这个窗口只有几十到几百毫秒,但如果节点负载高、网络抖动、或者注册请求短时间内爆发,窗口可能被拉长到秒级。
2.3 AP模式下不得不接受的几个“怪异”现象
因为Distro协议是异步复制,所以下面这些现象在AP模式下都是正常的,不用大惊小怪:
- 同一时刻,从不同Nacos节点查询同一个服务的实例列表,结果可能不一样。
- 一个实例刚注册成功,立刻去查它,查不到是可能的。
- 一个实例已经注销/心跳超时被剔除,但其他节点可能还保留它一段时间,调用方仍可能短暂地拿到这个下线实例。
这里有一个容易被忽视的运维点:很多团队习惯在前端控制台看服务列表,发现“服务已经下掉了但控制台还有显示”,就以为Nacos出了问题。其实只要是在AP模式下,异步同步有延迟是预期行为。控制台看到的数据来自你当前访问的那个节点,并不代表整个集群的状态都是这样。看这种问题,要多节点对比,不能在单点上做判断。
2.4 源码层面AP模式的核心实现位置
如果你对源码感兴趣,Nacos中AP模式的核心一致性服务实现类是DistroConsistencyServiceImpl。它实现了ConsistencyService接口,这个接口定义了put、remove、get、listen等方法。put方法在做完本地写入后,会把任务交给DistroTaskEngine,由它负责构造延迟任务、批量任务,最终把数据同步到其他节点。任务失败会有重试,重试次数和间隔均有上限,超过之后会记录失败日志,必要时依赖下一次全量校验来兜底修复。
这个实现逻辑说白了就是“先本地成功,再异步扩散”。理解这一层,你就明白为什么AP模式的注册接口性能好、抗节点故障能力强,同时也要清楚它那不保证强一致的根源在哪里。
3. CP模式的实现:JRaft接管后,数据是怎么被“多数派确认”的
3.1 Nacos选JRaft不是拍脑袋决定的
如果一直用无主模型,虽然可用性高了,但“数据会不会丢、会不会重复、到底哪个节点说了算”这些问题会一直悬着。Nacos很早就意识到,服务注册中心里有一部分数据必须做到强一致,比如配置中心的数据、持久实例的元数据等。于是Nacos在CP模式上引入了Raft协议。
早期的Nacos 1.x使用自研的Raft实现,虽然能跑,但工程问题不少。主要痛点包括:Raft日志没有做持久化优化、快照机制不完善、选主和日志复制在高并发下性能不够稳定、缺少现成的监控和运维手段。后来Nacos改用蚂蚁集团开源的JRaft,这是一个工业级Raft实现,底层基于RocksDB做状态机存储,选主、日志复制、快照、集群成员变更这些能力都比较成熟。Nacos内部通过JRaftConsistencyServiceImpl对接JRaft,实现持久实例的写入和读取。
3.2 持久实例注册时,数据到底经历了什么
当一个客户端注册ephemeral=false的持久实例时,请求处理链路完全不同:
- 请求进入节点A,A作为Raft中的一个节点,判断自己是否是leader。如果不是leader,会把写请求转发给leader,或者直接把leader信息返回给客户端,让客户端重定向。
- leader节点收到写请求后,将操作封装成一条Raft日志,追加到本地日志文件,同时并行把日志复制给其他follower节点。
- follower收到日志后,写入本地日志,然后向leader返回确认。leader统计收到的确认数量,当确认节点数超过集群节点总数的一半(即多数派),就认为这条日志已经“提交”。
- 提交后的日志应用到状态机,也就是把持久实例数据写入底层存储。此时leader才向客户端返回注册成功。
所以,CP模式下“注册成功”这句响应,代表的不只是某个节点收到了,而是集群里已经有过半节点把这条数据记录下来了。这条数据从此不会因为某一个节点宕机而丢失,代价是写延迟显著高于AP模式。
还要注意一个细节:CP模式下的读操作也不是直接从本地内存读那么简单。如果leader在日志尚未提交时就把旧数据返回给客户端,会违反线性一致性。JRaft提供了ReadIndex和LeaseRead两种一致性读方案,目的就是确保读到的数据不会早于之前已经确认的写入。当然,实际读取路径可能根据配置略有差异,但大方向不会变——CP模式想拿到一个新实例的数据,通常要等它被多数派确认。
3.3 切到CP模式后,系统会付出哪些代价
很多团队把服务切到CP模式以后,发现性能明显下降、发布变慢,这是正常的。CP模式带来的代价集中在三块:
- 性能代价:一次写请求从AP的“内存写入返回”变成了“Raft日志复制+落盘+多数派确认”,RTT可能从毫秒级升到几十毫秒,甚至更高。频繁的注册注销操作会把Raft日志体量快速顶上去,需要及时做快照来压缩日志。
- 可用性代价:Raft要求多数派可用才能继续工作。假设集群有3个节点,挂掉1个还能继续;如果挂掉2个,剩余节点永远凑不齐多数派,整个CP数据面会停止写入。此时如果恰好赶上发布窗口,重新注册实例可能一直失败或超时。
- 运维代价:你需要关注leader节点是谁、leader是否频繁切换、Raft日志是否积压、快照是否成功。这些在AP模式下几乎不用关心,但在CP模式下都是日常监控项。
3.4 分区发生时,AP和CP的表现对比
用同一个场景对比:一个5节点的Nacos集群,网络被切成了两个分区,一边3个节点,一边2个节点。
在AP模式下,两边的节点都会继续接受注册和查询,服务不会中断,但两边的服务列表可能各说各话,等到网络恢复后再慢慢收敛。这个“各说各话”如果发生在服务上下线频繁的场景,可能出现上线请求打到左边、查询请求打到右边,短时间内服务列表缺失。
在CP模式下,分区发生后,包含最多节点的分区因为有3个节点,达到半数以上,所以可以继续写入;另一个分区只有2个节点,不满足多数派,会拒绝写入。这个设计保证了整个集群中只会有一个“正确的数据版本”,避免分区恢复后出现数据冲突,代价就是少数派那边的服务注册和更新会临时不可用。
4. AP/CP模式切换实操:配置、流程和验证
4.1 切换前必须想清楚的三件事
动手切换之前,先确认三点。第一,你自己的业务能不能接受CP模式的性能损失和分区不可用。如果只是普通微服务的服务发现,不建议切,保持默认AP就好。第二,确认Nacos版本。1.x和2.x在协议细节、客户端默认行为上有差异,下面的配置在2.x上验证过,1.x也大致可用,但建议以实际版本的官方文档为准。第三,确认集群节点数。CP模式强烈建议至少3个节点,2节点集群在CP模式下没有任何容错优势,不如不切。
还有一个很容易被忽略的问题:客户端版本和服务端版本要匹配。Nacos 2.x客户端和服务端之间走gRPC,如果你用一个很老的客户端去连2.x服务端,或者反过来,都可能出现注册成功但控制台看不到、配置不生效等兼容性问题。切换模式前,先把Nacos服务端和客户端的版本拉齐,再动ephemeral配置。
4.2 通过注册参数完成模式选择
先说默认情况。用Spring Cloud Alibaba接入Nacos时,注册的实例默认是临时实例,也就是默认走AP模式。如果什么都不改,你的服务列表就是AP模式下的最终一致数据。
如果你想注册成持久实例,也就是切到CP模式,有两种常见方式。
方式一:通过Spring Cloud配置
在application.yml里增加:
spring: application: name: order-service cloud: nacos: discovery: server-addr: 127.0.0.1:8848 ephemeral: false设置ephemeral: false后,服务启动时会以持久实例的身份注册到Nacos,后续注册请求会走JRaft的CP链路。
方式二:通过OpenAPI直接注册
不依赖Spring Cloud,你也能直接调用Nacos的HTTP接口来控制实例类型:
# 注册一个临时实例,走AP模式 curl -X POST 'http://127.0.0.1:8848/nacos/v1/ns/instance?serviceName=order-service&ip=10.0.0.10&port=8080&ephemeral=true' # 注册一个持久实例,走CP模式 curl -X POST 'http://127.0.0.1:8848/nacos/v1/ns/instance?serviceName=order-service&ip=10.0.0.11&port=8080&ephemeral=false'如果你在控制台手动新建服务,也会看到一个“临时实例”的开关选项,勾选就是AP,不勾选就是CP。
4.3 已有服务如何平滑切换
一个已经以临时实例身份运行的服务,如果直接改配置重启,会经历一个“旧实例注销、新实例注册”的过程。如果是临时实例,注销掉旧实例再注册新实例,对整体服务影响不大,顶多出现几秒的服务列表抖动。但如果你是从持久实例切到临时实例,或者反过来,我建议按以下步骤来:
- 先确认服务本身有多个副本在跑,避免切换过程中把可用实例数降到0。
- 分批操作,一次只重启少量实例。先改其中一台机器的配置并重启,观察Nacos控制台里这台实例是否以预期的模式出现在服务列表中。
- 确认无误后,再滚动更新剩余实例。
- 如果切换过程中发现调用方报“no instances available”,立刻暂停操作,回滚配置。
这里要特别提醒一点:不要试图通过Nacos控制台的“编辑”按钮直接把现有实例的临时属性改掉。实例的ephemeral属性在注册时确定,修改需要先注销、再用新属性重新注册。控制台编辑通常只修改元数据,不改变一致性归属,容易造成你以为切了、实际没切的错觉。
4.4 切换之后的验证手段
模式切完之后,不能只看控制台显示就收工,要做一遍实际验证:
- 控制台二次确认:进入服务详情页,查看“临时实例”的数量和实例列表。一个服务如果配置的是CP模式,它下面不应该出现临时实例。
- 命令行查询服务列表:用Catalog接口看服务和实例组,确认实例数量与预期一致:
curl 'http://127.0.0.1:8848/nacos/v1/ns/catalog/services?pageNo=1&pageSize=10&serviceNameParam=order-service'- 故障演练:如果是CP模式,找一个维护窗口,手动停掉当前leader节点,观察剩余节点是否能正常选主、服务是否出现短时写入超时。如果是AP模式,可以停掉一个非当前接入节点,观察客户端是否能迅速切换接入到其他节点,注册是否正常。
- 客户端日志观察:Nacos客户端在注册成功时会打印类似
nacos registry, order-service 10.0.0.11:8080 register finished的日志。切换后观察这些日志是否还正常输出,同时记录从注册到能被其他服务发现的时间差,对比AP和CP的差异。
5. 生产环境里我踩过的坑和完整排查链路
5.1 “服务注册成功但调用方就是看不见”的真相
这是一个非常经典的问题,我遇到过一次。当时我们的服务A有多个节点,其中一个新扩容的节点启动后,日志显示注册成功,但服务B通过Feign调用时,仍然一直报错no instances available。
排查思路是这样的:
第一步,先去Nacos控制台看服务列表。结果发现这个实例确实存在于服务A的名下,而且是健康状态。这就排除了注册根本没成功的可能。
第二步,从服务B所在机器上用curl直接访问Nacos节点1的查询接口,能返回这个新实例;但换成访问Nacos节点2的查询接口,列表里就没有这个新实例。问题出现在两个Nacos节点上。
第三步,看服务A注册时用的是临时实例还是持久实例。查了半天,发现新扩容节点和其他节点一样,都是临时实例,走AP模式。但问题在于,这台机器所在的网络到Nacos节点2的网络偶发丢包,Distro的异步同步任务失败了两次,导致节点2的服务列表一直没更新。
原因找到后,解决方式并不复杂:清理网络问题、让节点2重新从其他节点拉取一次全量数据就行。但这件事给我留下的教训是:AP模式下的服务列表天然存在“不同节点短暂不一致”的窗口,排查这类问题时,一定要从多节点Nacos去查,不能只看控制台某一个节点的结果。
5.2 临时实例和持久实例混用,缩容时出现幽灵节点
还有一次事故比上面这个更隐蔽。当时我们有一套老系统迁移到新架构,老节点是用持久实例方式注册的,新扩容的节点用Spring Cloud默认配置注册成了临时实例。同一个服务名下,两种实例混着跑。平时看起来没什么问题,问题出在一次缩容上。
当时我们要下线一批老节点,操作前先调用了Nacos的注销接口,也确认了返回成功。但随后发现,调用方仍然会持续向已经下线的老节点IP发起请求,超时率直线上升。查控制台发现,这批老节点还在服务列表里,状态是“不健康”。
根因在于:持久实例的移除逻辑和临时实例完全不同。临时实例依赖客户端心跳,心跳停了就会被自动剔除;持久实例更多依赖服务端主动健康检查和显式注销。你调用注销接口时,如果这个实例已被标记为不健康,某些版本下服务端可能不会把它从列表中立刻清理掉,需要等健康检查或重启节点才最终消失。这就导致了一个“幽灵节点”窗口,它已经不提供服务了,但注册中心还把它发布给调用方。
这个事故给我们的教训有三条。第一,同一种服务不要混用临时实例和持久实例,模式在服务维度上必须统一。第二,下线流程不能只依赖注册中心的注销接口,还要在负载均衡、网关或客户端缓存层面同步移除节点。第三,对持久实例的缩容要格外谨慎,提前确认实例的不健康状态和移除时机,不能想当然地以为注销接口返回成功就万事大吉。
5.3 CP模式选主期间的注册超时
还有一次,一个核心服务切到了CP模式(持久实例),发布时出现大批量注册超时,发布平台直接报错。当时我们的Nacos集群是3节点,部署在同一个机房,平时很稳定。排查后才发现,异常窗口正好对应一次Raft leader切换。
事情的经过是:新发布的服务会先注销旧实例、再注册新实例。在没有做频率控制的情况下,成百上千个注册请求同时打进来,正好赶上某个节点正在发起leader选举。新的leader还没产生,写请求无法进入多数派确认流程,于是客户端不断重试,最终表现为注册超时。虽然过了一会集群恢复,但发布平台已经判定发布失败。
这个问题的解决方式比较常规:发布会分批发布,降低瞬时注册并发;同时调整了Raft的选举超时参数,避免无意义的频繁选主;监控里增加了JRaft相关的指标,一旦发现leader切换次数异常,立刻发告警。说到底,CP模式不是让普通服务随便切着玩的,它要求你对集群的选举、日志复制、JVM停顿都保持敏感。
5.4 升级Nacos版本时,注意AP/CP实现的变化
如果你的Nacos还是1.x,并且计划升级到2.x,有一个地方需要特别留意。Nacos 2.x在整体上引入了gRPC,客户端端口从“只用8848”变成了“8848 + 9848”的组合,防火墙和安全组如果不放通9848端口,客户端连接就会出现时断时续,注册和心跳都会受影响。这个和AP/CP没有直接关系,但很多人在升级后同时切了实例模式,结果一压测就崩,最后定位到的却是端口不通。
另外,1.x和2.x在持久实例的Raft实现上有差异。2.x将自研Raft替换成了JRaft,数据格式和日志目录都可能有变化。升级前一定要备份数据,并且在预发环境先用完整链路验证一遍CP模式下的注册、注销、选主和故障恢复,确认没问题再上生产。
6. 选型复盘:什么场景坚持AP,什么场景必须CP
6.1 标准微服务注册发现:别折腾,就用AP
如果你的场景是典型的微服务调用,服务数量多、实例上下线频繁、每次发布都涉及大批节点重新注册,那我建议你老老实实用AP模式。原因有三个:
第一,服务发现的本质是“拿到一份可用的节点列表”,短暂的不一致通常是可以接受的。新节点晚几秒被看到,旧节点多存活几秒被调用,这个在绝大多数业务里不会造成实质性影响。第二,AP模式对节点故障的容忍度更高,发布窗口更稳。第三,AP模式的注册延迟更低,大批量发布时不容易因为Raft日志积压拖慢发布速度。
AP模式真正要防的是“把不健康的实例发给调用方”和“下线实例迟迟不发现”。这两个问题主要靠合理的心跳配置、健康检查以及调用方侧的熔断重试来解决,而不是靠切CP来根治。
6.2 真正需要CP的场景:牺牲一时可用性,换取绝对正确
哪些场景适合切CP?我总结下来有三类:
- 配置数据:配置错误会导致线上大面积故障,必须保证新的配置一旦发布,所有节点都能在一个全局确定性的顺序下看到,而不是各节点各说各话。
- 选主和分布式协调数据:比如集群中谁是主节点、哪些节点在参与某项互斥任务。这类数据如果出现两个分区各自认为自己是主,后果往往是双写、脑裂。
- 不可丢失的元数据:比如服务实例的关键路由信息、带有状态标识的持久化数据。如果这类数据因为节点宕机而丢失,会直接影响流量调度正确性。
但即便属于这三类,我也建议把这类数据放在Nacos配置中心或专门的强一致存储里,而不是把普通业务服务注册都切到CP。因为CP模式在分区时拒绝写入的代价,对频繁上下线的普通服务来说是很难承受的。
6.3 混合部署时的最后建议
如果你确实需要在同一个Nacos集群里同时支撑AP和CP两类数据,我建议从这几个角度做好隔离和规划:
- 服务维度统一模式:一个服务下的实例,要么全临时,要么全持久,不要在服务内部混用。
- 用命名空间隔离开:不同团队、不同一致性级别的服务放到不同命名空间,避免一个团队误改全局配置影响所有人。
- 给CP模式单独配监控:重点看JRaft选主次数、日志复制延迟、快照耗时、leader切换事件。AP模式则重点看Distro同步延迟、节点间数据量差异、心跳超时剔除量。
- 控制台和API的使用规范:所有实例注册都通过统一的平台或脚本完成,手动在控制台点出来的实例很容易留下隐患。
最后分享一个我个人实际操作中的体会:如果你只是想保证“服务上下线足够快”,优先去调客户端心跳参数和服务端健康检查参数,而不是把服务切成CP。我见过很多团队因为AP模式下服务下线不够及时,冲动地把核心服务全切到CP,结果发布窗口动不动就注册超时,最后又灰溜溜切回来。真正懂Nacos的人,会先搞清楚自己缺的到底是“一致性”还是“可用性”,然后对症下药。AP和CP只是手段,业务才是目的。