Nacos到底是AP还是CP?一文说清楚

引子

我之前写过几篇手写Raft的文章,陆陆续续讲了很多Raft的原理:

最近准备写开源JobFlow,又深入研究了Nacos:

借这个机会,把Raft做个总结吧。分成两篇:

  • 第一篇:Nacos到底是AP还是CP?一文说清楚(本篇)
  • 第二篇:深入JRaft:Nacos配置中心的性能优化实践

这一篇我们聚焦核心问题,用大白话把Nacos的设计逻辑讲清楚。


一、直接回答:Nacos既是AP也是CP

很多人问:"Nacos到底是AP还是CP?"

答案是:看你用哪个功能。

erlang 复制代码
Nacos的架构设计
├── 服务注册中心
│   ├── 临时实例(默认)→ AP模式
│   │   协议:Distro
│   │   特点:高可用,最终一致
│   │   占比:99%的使用场景
│   │
│   └── 持久化实例 → CP模式
│       协议:Raft
│       特点:强一致,可能不可用
│       占比:1%的特殊场景
│
└── 配置中心 → CP模式
    协议:Raft
    特点:强一致,数据不能错
    占比:100%

大部分人用Nacos做服务注册,用的都是临时实例(AP模式),根本没用到Raft。

只有配置中心和少量持久化实例才用Raft(CP模式)。

面试时怎么回答?

arduino 复制代码
面试官:Nacos是AP还是CP?

如果直接回答"CP"或"AP":20分

正确回答:
"Nacos的服务注册中心,默认使用临时实例,是AP模式,
通过Distro协议实现最终一致性,保证高可用。
配置中心使用Raft协议,是CP模式,保证强一致性。
另外也支持持久化实例,同样走Raft协议,
适用于数据库、消息队列等基础设施的注册。"

这样回答:80分

接下来我们深入讲讲为什么这么设计。


二、CAP理论:5分钟讲清楚

在讲Nacos之前,必须先理解CAP理论。

CAP是什么?

CAP是分布式系统的铁律,由三个单词首字母组成:

  • C (Consistency) :一致性
    • 所有节点在同一时间看到相同的数据
    • 读取操作总能读到最新写入的值
  • A (Availability) :可用性
    • 任何请求都能得到响应
    • 不管成功还是失败,总要给个答复
  • P (Partition tolerance) :分区容错
    • 网络分区时系统仍能工作
    • 部分节点之间通信中断,系统照常运行

CAP定理

分布式系统最多只能同时满足两个。

但实际上,网络分区(P)是一定会发生的,你没法避免。所以真正的选择是:

css 复制代码
当网络分区发生时,你选C还是A?

CP系统:宁可不可用,也不能数据错

diff 复制代码
典型场景:银行转账

发生网络分区:
- 杭州机房:有客户A的账户
- 北京机房:有客户B的账户
- 两个机房之间网络断了

如果选择C(一致性):
- 拒绝服务:"系统维护中,请稍后再试"
- 等网络恢复后再转账
- 保证不会出现"扣了A的钱,但B没收到"

代表技术:Raft、ZooKeeper、etcd

AP系统:宁可数据不一致,也要继续服务

diff 复制代码
典型场景:微博点赞

发生网络分区:
- 杭州机房:显示1000个赞
- 北京机房:显示1002个赞
- 两个机房之间网络断了

如果选择A(可用性):
- 继续服务,各机房独立计数
- 杭州的用户看到1000赞
- 北京的用户看到1002赞
- 等网络恢复后再同步(最终一致)

代表技术:Distro、Gossip、Cassandra

关键理解

markdown 复制代码
不是"AP系统不一致",而是"暂时不一致"。

AP系统保证:
1. 任何时候都能服务(可用)
2. 最终会一致(最终一致性)
3. 不一致的时间窗口很短(通常1-2秒)

CP系统保证:
1. 数据任何时候都一致(强一致)
2. 可能拒绝服务(不可用)
3. 不一致的时间窗口为0

三、微服务注册为什么选AP?

理解了CAP,我们看看为什么微服务注册要用AP模式。

微服务的场景

假设你的电商系统:

diff 复制代码
用户服务:10个实例
订单服务:20个实例
商品服务:15个实例

每天:
- 凌晨发版重启:45个实例重启
- 高峰期扩容:新增10个实例
- 低峰期缩容:下线5个实例
- 偶尔宕机:1-2个实例挂掉

平均每小时有几十次实例上下线

如果用CP模式会怎样?

markdown 复制代码
场景:订单服务的一个实例宕机

CP模式的处理流程:
1. Nacos检测到实例下线
2. 需要通过Raft协议更新注册信息
3. Leader节点发起日志复制
4. 等待过半节点确认(50-100ms)
5. 确认后,所有节点才更新注册表

问题:
- 慢:每次上下线都要走共识
- 阻塞:写操作会等待
- 风险:Nacos集群网络分区导致服务不可用

最严重的:
如果Nacos集群自己发生网络分区
→ 无法达成共识
→ 拒绝所有注册/心跳
→ 整个系统瘫痪

AP模式的处理方式

markdown 复制代码
场景:订单服务的一个实例宕机

AP模式的处理流程:
1. Nacos某个节点检测到(心跳超时)
2. 立即更新本地注册信息(1-2ms)
3. 异步通知其他Nacos节点(不等待)
4. 客户端在1-2秒内拉取到最新信息

优势:
- 快:直接写本地内存
- 不阻塞:不等其他节点响应
- 高可用:即使Nacos网络分区也能各自服务

最重要的:
微服务调用有重试机制
→ 即使短暂拿到过期的注册信息
→ 调用失败后会重试其他实例
→ 影响很小

临时实例的工作原理

sequenceDiagram participant S as 服务实例 participant N1 as Nacos节点1 participant N2 as Nacos节点2 participant N3 as Nacos节点3 participant C as 调用方 Note over S,N1: 1. 服务注册 S->>N1: 注册实例信息 N1->>N1: 写入本地内存(1-2ms) N1-->>S: 注册成功 par 异步同步 N1->>N2: 异步同步实例信息 N1->>N3: 异步同步实例信息 end Note over S,N1: 2. 心跳保活 loop 每5秒 S->>N1: 发送心跳 N1->>N1: 更新lastBeat时间 end Note over S,N1: 3. 实例下线 S->>S: 进程崩溃,停止心跳 Note over N1: 15秒后 N1->>N1: 检测到心跳超时 N1->>N1: 标记实例不健康 par 异步通知 N1->>N2: 通知实例下线 N1->>N3: 通知实例下线 end Note over C: 4. 调用方拉取 C->>N2: 查询服务列表 N2-->>C: 返回健康实例列表

临时实例的时间线

r 复制代码
T + 0秒:服务启动,注册到Nacos
T + 5秒:发送第一次心跳
T + 10秒:发送第二次心跳
T + 15秒:服务宕机,停止心跳

T + 30秒:Nacos检测到15秒没心跳
         标记实例为"不健康"
         但不删除(给恢复的机会)

T + 45秒:Nacos检测到30秒没心跳
         删除实例
         
关键时间点:
- 5秒:心跳间隔
- 15秒:标记不健康的阈值
- 30秒:删除实例的阈值

为什么微服务适合AP?

markdown 复制代码
微服务的特点:
1. 实例频繁上下线
   - 发版、重启、扩缩容
   - 一天几十次很正常

2. 调用有容错机制
   - 重试
   - 熔断
   - 降级

3. 短暂不一致可以接受
   - 拿到过期的实例信息
   - 调用失败重试就行
   - 1-2秒后数据就一致了

所以:
- 高可用 > 强一致
- 选择AP模式

四、配置中心为什么选CP?

配置和服务注册完全不同,配置错了是致命的。

配置不一致的灾难

diff 复制代码
场景:数据库连接池配置

如果使用AP模式,可能出现:

Nacos节点1的配置:
  datasource.url=jdbc:mysql://192.168.1.100:3306/db

Nacos节点2的配置:
  datasource.url=jdbc:mysql://192.168.1.200:3306/db

结果:
- 部署在杭州机房的应用实例
  从Nacos节点1拉取配置
  连到数据库A(192.168.1.100)

- 部署在北京机房的应用实例
  从Nacos节点2拉取配置
  连到数据库B(192.168.1.200)

后果:
- 同一个订单,杭州看到的状态是"待支付"
- 北京看到的状态是"已支付"
- 数据完全混乱
- 系统故障

这是不可接受的!

CP模式的配置写入

sequenceDiagram participant C as 客户端 participant L as Nacos Leader participant F1 as Nacos Follower1 participant F2 as Nacos Follower2 C->>L: 发布配置 Note over L: 1. 写入本地日志 L->>L: append log Note over L,F2: 2. 并行复制 par 复制到所有Follower L->>F1: 复制日志 L->>F2: 复制日志 end F1->>F1: 写入日志 F1-->>L: 确认 F2->>F2: 写入日志 F2-->>L: 确认 Note over L: 3. 过半确认,提交 L->>L: commit log Note over L: 4. 应用到配置数据 L->>L: apply to config store L-->>C: 发布成功 Note over L,F2: 5. 推送配置变更 par 通知所有应用实例 L->>F1: 推送新配置 L->>F2: 推送新配置 end

CP模式的处理流程

markdown 复制代码
写配置的完整流程:

1. 客户端提交配置变更
   POST /nacos/v1/cs/configs
   dataId: application.yml
   content: server.port=8080

2. Leader收到请求
   - 写入本地日志文件
   - 分配日志索引:index=12345

3. 并行复制给Follower
   - Leader → Follower1
   - Leader → Follower2
   - 异步发送,但要等响应

4. 等待过半确认(阻塞)
   - 3个节点需要2个确认
   - 可能等50-100ms
   - 超时5秒后失败

5. 过半确认后提交
   - 标记日志为已提交
   - 应用到配置数据库

6. 返回客户端成功

7. 推送配置给应用
   - 所有订阅该配置的应用
   - 实时收到变更通知

配置中心的特点

markdown 复制代码
配置的特点:
1. 变更不频繁
   - 一天可能就改几次
   - 不像服务实例一天上下线几十次

2. 可以接受写入慢一点
   - 50-100ms可以接受
   - 不需要像注册那样毫秒级

3. 绝对不能不一致
   - 宁可写入失败(503错误)
   - 也不能不同节点配置不一样

4. 需要可靠性
   - 配置要持久化
   - 重启不能丢
   - 历史可追溯

所以:
- 强一致 > 高可用
- 选择CP模式

五、Raft协议5分钟讲清楚

Nacos的配置中心用的是Raft协议。理解Raft是理解Nacos的基础。

Raft的核心思想

用一句话总结:

arduino 复制代码
通过"过半确认",保证所有节点按相同顺序执行相同操作

这句话有三个关键词:

  • 过半确认:多数派原则
  • 相同顺序:日志索引保证
  • 相同操作:日志内容一致

Raft的三大机制

1. Leader选举

diff 复制代码
Raft集群中任何时候只有一个Leader

Leader的职责:
- 接收客户端请求
- 负责日志复制
- 决定何时提交

选举规则:
- 日志最新的节点优先当选
- 必须获得过半选票
- 通过Term(任期号)区分新老Leader

为什么日志要最新?
→ 保证新Leader一定有所有已提交的数据
→ 否则已提交的数据可能丢失

选举过程:

graph TD A[Follower检测心跳超时] --> B[转为Candidate] B --> C[Term+1] C --> D[给自己投票] D --> E[向其他节点请求投票] E --> F{收到过半选票?} F -->|是| G[成为Leader] F -->|否| H[继续等待或重新选举] G --> I[发送心跳维持地位] E --> J{收到更高Term的消息?} J -->|是| K[转回Follower]

2. 日志复制

Leader收到写请求后:

markdown 复制代码
1. 写入本地日志
   LogEntry {
       index: 100      // 日志位置
       term: 5         // 任期号
       command: "PUT name=alice"  // 具体操作
   }

2. 并行复制给Follower
   - Leader发送AppendEntries RPC
   - 携带日志内容

3. 等待过半确认
   - 3节点需要2个确认
   - 5节点需要3个确认

4. 过半后提交
   - 标记日志为已提交
   - 应用到状态机

5. 返回客户端成功

关键的一致性检查:

yaml 复制代码
Leader发送日志时,会带上"前一条日志"的信息:

AppendEntriesRequest {
    prevLogIndex: 99
    prevLogTerm: 5
    entries: [
        {index: 100, term: 5, command: "..."}
    ]
}

Follower收到后:
1. 检查本地日志[99]
2. 如果term也是5 → 接受,追加日志[100]
3. 如果term不是5 → 拒绝,说明日志冲突

为什么这样检查?
→ 递归保证:如果99一致,说明1-98都一致
→ 如果100一致,说明1-99都一致
→ 保证整个日志序列一致

3. Term机制

ini 复制代码
Term(任期)是Raft的逻辑时钟

作用1:区分新老Leader
  旧Leader: term=5
  新Leader: term=6
  旧Leader看到term=6 → 自动降级为Follower

作用2:防止脑裂
  两个Candidate同时选举
  → term高的会赢
  → term低的会失败

作用3:拒绝过期请求
  收到term=5的请求
  当前term=6
  → 直接拒绝

规则:
- 每次选举term+1
- 收到更高term的消息 → 立即更新自己的term
- 拒绝处理更低term的请求

过半确认的数学

diff 复制代码
为什么要过半?

3节点集群:
- 需要2个节点确认(包括Leader自己)
- 容忍1个节点故障

5节点集群:
- 需要3个节点确认
- 容忍2个节点故障

N节点集群:
- 需要(N/2 + 1)个节点确认
- 容忍(N-1)/2个节点故障

为什么不是全部确认?
→ 任何一个节点故障就不可用了

为什么不是1/3确认?
→ 可能出现数据不一致

过半是最优解:
→ 容错能力和一致性的平衡点

Raft保证的安全性

markdown 复制代码
核心保证:已提交的数据永不丢失

怎么保证?

1. 选举限制
   - 只有日志最新的节点才能当选
   - 新Leader一定有所有已提交的数据

2. 提交规则
   - 只能提交当前term的日志
   - 旧term的日志通过新日志间接提交
   
3. 日志匹配
   - 通过prevLogIndex/prevLogTerm检查
   - 发现冲突就删除重建

结果:
- 所有节点最终日志完全一致
- 已提交的数据在任何节点都能读到
- 即使发生Leader切换也不会丢数据

六、Nacos的双模式对比

现在我们理解了AP和CP,也理解了Raft,来看看Nacos的两种模式。

临时实例 vs 持久化实例

java 复制代码
// 注册临时实例(默认)
Instance instance = new Instance();
instance.setIp("192.168.1.100");
instance.setPort(8080);
instance.setEphemeral(true);  // 临时实例
namingService.registerInstance("user-service", instance);

// 注册持久化实例
Instance instance = new Instance();
instance.setIp("192.168.1.100");
instance.setPort(3306);
instance.setEphemeral(false);  // 持久化实例
namingService.registerInstance("mysql-service", instance);

详细对比

维度 临时实例(AP) 持久化实例(CP)
协议 Distro Raft
写入速度 1-2ms 50-100ms
一致性 最终一致(1-2秒) 强一致(实时)
保活方式 客户端发心跳 Server主动检查
实例下线 心跳停止自动删除 需要显式删除
数据持久化 否,重启丢失 是,持久化到磁盘
网络分区 各自继续服务 少数派不可用
适用场景 微服务实例 数据库、消息队列
使用占比 99% 1%

什么时候用持久化实例?

markdown 复制代码
适合持久化实例的场景:

1. 数据库实例
   - MySQL、PostgreSQL、Redis
   - 实例不会频繁上下线
   - 需要准确的健康检查
   - 地址信息必须准确

2. 消息队列
   - RocketMQ、Kafka
   - Broker地址不能错
   - 需要持久化保存

3. 其他基础设施
   - Elasticsearch集群
   - 配置中心本身
   
特点:
- 实例稳定,很少变化
- 地址信息必须准确
- 重启后仍需要保留注册信息
markdown 复制代码
不适合持久化实例的场景:

1. 普通微服务
   - Spring Boot应用
   - 频繁发版重启
   - 实例动态扩缩容
   
2. 短生命周期应用
   - Kubernetes Pod
   - Serverless函数
   - 临时任务

特点:
- 实例频繁上下线
- 允许短暂不一致
- 调用方有重试机制

实际使用建议

diff 复制代码
经验法则:

默认用临时实例
- 99%的微服务场景
- 除非有特殊需求

只在这些情况用持久化实例:
- 数据库、缓存、MQ等基础设施
- 实例地址几乎不变
- 需要准确的健康检查
- 必须保证数据强一致

如果不确定用哪个:
→ 用临时实例就对了

七、常见面试问题

Q1:为什么Nacos不全用Raft?

markdown 复制代码
如果服务注册也用Raft:

缺点:
1. 写入慢
   - 每次注册/心跳都要过半确认
   - 原来1-2ms,现在50-100ms

2. 可用性差
   - Nacos集群网络分区时可能不可用
   - 影响所有微服务

3. 吞吐量低
   - Leader成为瓶颈
   - 无法支撑大规模集群

优点:
1. 强一致性
   - 所有节点数据完全一致

权衡:
- 微服务注册:不需要强一致,需要高可用 → AP
- 配置中心:必须强一致,可以牺牲部分可用 → CP

Q2:如果过半节点都宕机了怎么办?

markdown 复制代码
场景:3节点集群,2个节点宕机

Raft的处理:
- 拒绝所有写入(无法达成共识)
- 已有的配置可以读取
- 等待节点恢复

临时方案:
1. 紧急扩容
   - 快速启动新节点加入集群
   - 等新节点追上数据

2. 降级到单节点模式
   - 仅紧急情况
   - 有数据丢失风险
   - 需要专业人员操作

预防措施:
- 部署5节点(容忍2个故障)
- 跨机房部署
- 做好监控告警
- 备份配置数据

Q3:Distro协议和Raft协议的本质区别?

markdown 复制代码
Distro(AP):
- 设计目标:高可用
- 写入方式:直接写本地,异步同步
- 数据一致:最终一致,允许短暂不一致
- 故障处理:各自独立服务
- 性能:极快(1-2ms)

Raft(CP):
- 设计目标:强一致
- 写入方式:Leader复制,等过半确认
- 数据一致:强一致,任何时候都一致
- 故障处理:少数派拒绝服务
- 性能:较慢(50-100ms)

选择依据:
- 数据能容忍短暂不一致吗?
  - 能 → Distro
  - 不能 → Raft

Q4:为什么心跳超时是15秒?

diff 复制代码
Nacos临时实例的时间参数:

心跳间隔:5秒
不健康阈值:15秒(3次心跳)
删除阈值:30秒(6次心跳)

设计考虑:

15秒不健康:
- 太短(如5秒)→ 网络抖动就误判
- 太长(如60秒)→ 故障发现太慢
- 15秒是平衡点

30秒删除:
- 给实例恢复的时间
- 防止短暂重启被删除

实际生产可调整:
spring.cloud.nacos.discovery:
  heart-beat-interval: 5000
  heart-beat-timeout: 15000
  ip-delete-timeout: 30000

Q5:Leader宕机后需要多久选出新Leader?

markdown 复制代码
Raft选举时间线:

T+0秒:Leader宕机,停止发送心跳

T+5秒:某个Follower心跳超时
      转为Candidate,发起选举
      Term从5变为6

T+5.1秒:其他Follower收到投票请求
        检查Candidate的日志是否最新
        如果是,投票给它

T+5.2秒:Candidate收到过半选票
        成为新Leader
        开始发送心跳

T+5.3秒:其他Follower收到新Leader的心跳
        确认新Leader

总耗时:约5秒

实际可能更快:
- 网络好的情况下1-2秒
- 可以调小election_timeout加速

Q6:配置中心的写入性能如何?

markdown 复制代码
性能数据(3节点集群):

写入延迟:
- P50: 20-50ms
- P99: 100-200ms
- P999: 500ms

吞吐量:
- 单Leader:500-1000 QPS
- 对于配置中心足够了
  (配置变更频率很低)

性能瓶颈:
1. Leader单点
   - 所有写入走Leader
2. 磁盘IO
   - 每次写入要fsync
3. 网络延迟
   - 需要等Follower响应

优化方向:
- 批量写入
- 异步刷盘
- SSD硬盘

八、总结

核心要点

1. Nacos是AP还是CP?

diff 复制代码
不是二选一,而是针对不同场景:

服务注册(临时实例):AP
- Distro协议
- 高可用优先
- 最终一致性
- 适合微服务

配置中心:CP
- Raft协议
- 强一致优先
- 可能不可用
- 适合配置管理

持久化实例:CP
- Raft协议
- 强一致优先
- 适合基础设施

2. CAP理论的本质

arduino 复制代码
不是"能不能同时满足CAP"
而是"网络分区时,选C还是A"

CP:宁可不可用,也不能数据错
AP:宁可暂时不一致,也要继续服务

3. Raft的核心

diff 复制代码
一句话:通过过半确认,保证所有节点按相同顺序执行相同操作

三大机制:
- Leader选举:日志最新的当选
- 日志复制:过半确认后提交
- Term机制:防止脑裂

安全保证:已提交的数据永不丢失

4. 实际使用建议

diff 复制代码
默认用临时实例:
- 99%的微服务场景
- 高可用,性能好

只在必要时用持久化实例:
- 数据库、MQ等基础设施
- 需要强一致性

配置中心必须用Raft:
- 配置错了是致命的
- 不能接受不一致

写在最后

Nacos的设计很聪明:不追求"完美的一致性"或"绝对的可用性",而是根据业务特点选择合适的模式

这也是分布式系统设计的核心思想:没有银弹,只有权衡。

如果你想深入了解Raft的实现细节、JRaft的性能优化、以及生产环境的最佳实践,欢迎继续阅读下一篇:

《深入JRaft:Nacos配置中心的性能优化实践》


参考资料


本文完

相关推荐
踏浪无痕7 小时前
深入JRaft:Nacos配置中心的性能优化实践
分布式·后端·面试
我梦见我梦见我7 小时前
CentOS下安装RocketMQ
后端
Cache技术分享7 小时前
273. Java Stream API - Stream 中的中间操作:Mapping 操作详解
前端·后端
天天摸鱼的java工程师7 小时前
Docker+K8s 部署微服务:从搭建到运维的全流程指南(Java 老鸟实战版)
java·后端
Undoom7 小时前
Redis 数据库的服务器部署与 MCP 智能化交互深度实践指南
后端
法欧特斯卡雷特7 小时前
如何解决 Kotlin/Native 在 Windows 下 main 函数的 args 乱码?
后端·操作系统·编程语言
over6977 小时前
掌控 JavaScript 的 this:从迷失到精准控制
前端·javascript·面试
南囝coding7 小时前
《独立开发者精选工具》第 024 期
前端·后端
古城小栈7 小时前
性能边界:何时用 Go 何时用 Java 的技术选型指南
java·后端·golang