跳到正文

集群与一致性 ​

一句话:Nacos 集群不是「三个节点复制同一份数据」,而是三套机制并存:临时实例走 Distro 的 AP 分片复制,持久实例走 Raft 的 CP 多数派,配置以 MySQL 为真相、节点只做本地缓存与通知;搞混这三套,就会问出「为什么配置没走 Raft」这种问题。

适用版本:Nacos 3.2.4;2.5.4 的差异单独标注。3.x 要求 Java 17,集群至少 3 节点并外置 MySQL。具体表结构与配置项以发行包里的 mysql-schema.sql、application.properties 示例为准。

为什么需要 ​

单节点 Nacos 能跑,但它是明显的单点:进程挂了注册和配置一起不可用;磁盘坏了数据可能丢;实例数一多,单机连接数和推送都扛不住。

集群要解决的是「节点故障时服务发现和配置读取还能继续」,不是「所有数据强一致」。这两个目标在 CAP 下不能同时满足,Nacos 的做法是按数据类型分开选:临时实例要可用性,持久实例要一致性,配置要持久化真相。

核心机制 ​

三套机制的分工 ​

图里只有持久实例那条线要求多数派确认;临时实例可以写到任意节点,再异步复制;配置则直接以 MySQL 为准,节点之间同步的是缓存和变更通知,不是 Raft 日志。所以「Nacos 集群挂了少数派还能不能注册」要分实例类型回答,不能一句话概括。

集群最小配置 ​

conf/cluster.conf 列出所有节点的 ip:port(主端口 8848,不是 9848):

text
192.168.1.11:8848
192.168.1.12:8848
192.168.1.13:8848

外置 MySQL 的关键配置(字段名以发行包示例为准):

properties
spring.sql.init.platform=mysql
db.num=1
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useSSL=false
db.user=nacos
db.password=***

先导入 conf/mysql-schema.sql,再启动三个节点。MODE=cluster(Docker)或 -m cluster(脚本)必须显式指定;用 standalone 起三个节点,它们不会组成集群。

Distro:临时实例的分片与补偿 ​

Distro 把临时实例按「服务 + 集群」分片,每个节点负责一部分,写入落在负责节点,再异步复制到其他节点。客户端可以从任意节点读,所以读到旧数据是允许的。

  • 冲突解决:带时间戳的后写覆盖(Last-Write-Wins)。
  • 故障接管:节点下线后,其他节点接管它的分片;客户端重连并重新注册,期间可能有短暂缺口。
  • 补偿:节点之间定时比对校验和,发现差异后做全量/增量同步。通知丢了最终会被补偿纠正,但纠正之前就是旧视图。

这就是 AP 的代价:分区期间两边都可能接受注册,恢复后按时间戳收敛。对无状态服务可以接受;对「不能调错实例」的路由,要在业务层做幂等和校验。

Raft:持久实例的写路径 ​

持久实例(ephemeral=false)走 JRaft:

  • 少数派节点不能写,只能读;所以「两个节点挂了,第三个还能注册持久实例」不成立。
  • 选举期间写会短暂不可用;具体恢复时间取决于网络与配置,不要承诺秒级。
  • 日志复制保证已提交的写不丢;未提交的写可能回滚。

配置:MySQL 是真相,节点只是缓存 ​

配置链路和命名链路完全不同:

  • 读:客户端读的是节点本地缓存,不是每次查 MySQL;所以 MySQL 挂了,已加载的配置仍能读。
  • 写:必须写 MySQL;MySQL 不可用时发布失败,但不会影响已有配置的读取。
  • 一致性:节点之间靠通知 + 定时 dump 收敛,通知丢失会有一段时间的旧值;这不是 Raft,也不是强一致。
  • 备份:备份 MySQL 就是备份配置真相;只备份节点本地文件不够。

分区时会发生什么 ​

场景临时实例(Distro)持久实例(Raft)配置
网络分区两边都可能接受写,LWW 收敛少数派不可写,多数派继续取决于 MySQL 主库是否可写
分区恢复校验和补偿,旧实例可能短暂存在日志追平,未提交写回滚通知/定时 dump 追平
主要风险读到旧实例列表注册短暂不可用短时间读到旧配置

脑裂不是单一问题:Distro 侧接受「双写后收敛」,Raft 侧靠多数派避免双主,配置侧则把问题交给 MySQL 的高可用方案。三者要分别设计,不能指望 Nacos 一个开关解决。

2.x → 3.x 的升级顺序 ​

  1. 备份 MySQL,并在测试环境验证注册、配置、推送三条链路。
  2. 升 JDK 17;3.x 不再支持 JDK 8。
  3. 补鉴权配置:nacos.core.auth.plugin.nacos.token.secret.key、server.identity.key/value 所有节点一致,否则启动失败或节点间通信失败。
  4. 改端口与探针:控制台从 8848/nacos 变为 8080 且无 /nacos 前缀;客户端仍需 9848/9849。
  5. 处理 API 与命名空间:v1/v2 API 在 3.2 起剥离到 api-legacy-adapter,后续版本可能移除;默认 Namespace 从空字符串变为 public,按官方迁移说明处理。
  6. 逐节点滚动,每升一个节点观察集群状态与客户端连接,再升下一个;最后升客户端。
  7. 不要混合版本回滚:3.2.4 起 JRaft 鉴权进入强制模式,回滚到不支持该鉴权的版本会破坏节点间通信。

实战场景 ​

一、三台“集群”其实是三个单机 ​

现象:按 3 节点部署 Nacos,压测通过后上线。运行一周后出现问题:同一服务在不同调用方看到的实例列表不一致;控制台里某个服务只有 1 个实例,另一个节点上看是 3 个。运维脚本往其中一个节点注册的数据,其他节点完全看不到。

排查:集群是否真的成立,有唯一的判据。

定位层取证据判定
启动模式是否显式 MODE=cluster 或 -m clusterstandalone 起三份不会组成集群
节点清单conf/cluster.conf 内容是否为三行 ip:8848(主端口,不是 9848)写 9848 或只写本机 → 无法组网
集群状态接口查询集群节点列表接口,看其他节点是否出现在成员表只有自己 → 未组集群
节点间连通9849(集群 gRPC)与 7848(JRaft)是否互通只放 8848 时 HTTP 查询正常,集群通信失败
分片归属临时实例的负责节点是否分散三个节点各自独立保存,读取结果自然不一致
配置侧三节点是否连同一个 MySQL连不同库时配置内容完全不同

本例是 Docker 部署时给三个容器分别设了 MODE=standalone,cluster.conf 没挂载进去;三节点各自工作,客户端通过 VIP 轮询连到哪个节点就读到哪份数据。

对症做法:

properties
# conf/cluster.conf:主端口 8848,三行,所有节点一致
192.168.1.11:8848
192.168.1.12:8848
192.168.1.13:8848
bash
# 启动后必须校验“成员数 = 3”,把这条做成上线检查
curl -s "http://127.0.0.1:8848/nacos/v1/ns/operator/servers" | grep -c '"ip"'
# 期望 3;返回 1 说明只是单机在跑
text
1. 三节点共连同一套外置 MySQL,配置真相只在库里;节点本地只是缓存与快照。
2. 端口清单核对:8848(HTTP)、9848(客户端 gRPC)、9849(集群 gRPC)、7848(JRaft)。
3. 部署产物里把 cluster.conf 与 MODE 作为受版本管理的配置项,避免“手工挂载忘记”。

复盘要点:判据是“成员表数量等于预期节点数”,而且要在每个节点上都查一遍——只在入口 VIP 上验证会漏掉部分节点未组网的情况。“Nacos 集群挂了少数派还能不能注册”必须分实例类型回答,前提是先确认它是一个集群。

二、2 节点集群挂一台,注册全线失败 ​

现象:为了“高可用”部署了 2 个 Nacos 节点。某次一台重启维护,持久实例(数据库代理登记)立刻无法注册和删除,报 leader 不可用;临时实例仍然正常。团队最初的结论是“Nacos 高可用不行”。

排查:两类实例对节点数的要求完全不同。

场景临时实例(Distro)持久实例(Raft)配置
2 节点挂 1每节点可写,继续工作多数派需 2 台,挂 1 剩 1 → 无法提交取决于 MySQL 主库是否可写
3 节点挂 1正常多数派 2/3,可继续正常读写
网络分区两边都可能接受写,恢复后 LWW 收敛少数派不可写,多数派继续与 MySQL 高可用方案绑定
定位层取证据判定
失败的是哪类写报错的注册请求里 ephemeral 取值只有 false 失败 → CP 路径多数派不足
leader 状态JRaft 节点状态与 leader 切换日志无 leader → 持久实例写不可用
节点间连通9849/7848 探测分区也会造成同样现象
临时实例是否受影响同期注册/查询成功率不受影响,正是 AP 的设计结果

“2 节点不比 1 节点稳”在这里成立:Raft 需要多数派,2 节点挂 1 个就没有提交能力。

对症做法:

text
1. 节点数取奇数且 ≥ 3:3 节点容忍 1 台故障,5 节点容忍 2 台,代价是写延迟与运维成本。
2. 三节点不要放在同一台物理机或同一可用区,否则“3 节点”在故障域上等于 1 节点。
3. 重新评估哪些服务真的需要持久实例:
   普通无状态服务用临时实例(AP),不会因为多数派问题掉可用性;
   持久实例换来的是“不能丢”,代价是“少数派不可写”。
4. 配置侧的高可用与 MySQL 绑定:主从 + 半同步或 MHA,
   并把“MySQL 挂了配置写不了”写进容量与故障预案。
properties
# 监控三类切换与延迟:leader 切换、Distro 同步延迟、MySQL 慢查询与连接数
# leader 切换次数在维护窗口外的增长通常意味着网络或负载异常

复盘要点:部署评审要把“容错几台”写成一个数字,而不是“有冗余”。“挂 1 台是否仍可注册持久实例”“挂 1 台是否仍可发布配置”这两问要能在演练里复现,答案由节点数与 MySQL 方案决定,不是 Nacos 的某个开关。

三、分区恢复后旧实例“复活”:LWW 依赖时钟 ​

现象:一次跨机架网络抖动持续 4 分钟。恢复后,一个已经下线的实例在服务列表里出现了约 90 秒,调用方把请求打到已停进程上,报大量连接拒绝;同时该实例的元数据权重被旧值覆盖。

排查:Distro 的收敛机制与时间戳语义决定了这类现象。

定位层取证据判定
分区两侧是否都接受过写两批节点上的该服务实例记录与时间戳分区期各自可写 → 恢复后需 LWW 收敛
收敛方式节点间定时校验和比对日志补偿前的窗口就是旧视图期
冲突结果带时间戳的后写覆盖:谁的写入时间戳更大时钟偏大的旧记录会“赢”
节点时钟各节点 node_timex_offset_seconds、chronyd 日志偏差 500ms 足以让顺序颠倒
客户端侧LB 缓存 TTL(35s)与收到推送的时间窗口长度 ≈ 补偿周期 + 缓存 TTL

对症做法:

bash
# 1. 时钟统一到同一 NTP 源,校时走 slew(逐步调整),禁止大幅 step
#    Distro 的 LWW 依赖时间戳,节点时间偏差会直接放大冲突概率
chronyc sources -v
chronyc tracking          # 观察 System time offset,绝对值应远小于 100ms
text
2. 分区风险要按类型分别设计,不能指望一个开关解决:
   Distro 侧接受“双写后收敛”,所以调用方要对“列表里存在但连不上”有兜底;
   Raft 侧靠多数派避免双主,代价是少数派不可写;
   配置侧把一致性问题交给 MySQL 的高可用方案。
3. 调用方兜底:连接被拒/超时允许换实例重试(仅幂等读),并配熔断;
   关键路由(支付、交易)用持久实例或业务层幂等校验,不接受短暂旧视图。
4. 不能把“补偿周期”调成 0 来消除窗口:校验和比对是集群内持续开销,
   缩短它等于用常态成本换极端场景。

复盘要点:分区恢复后的旧视图是 AP 的既定代价,验收口径是“窗口时长”和“窗口内的失败是否被兜住”,而不是“是否发生”。演练判据:制造 4 分钟分区,断言恢复后 60~120 秒内列表收敛,且调用方连接拒绝率不超过熔断阈值。多数据中心场景 Nacos 没有原生复制,跨机房要本地集群 + 客户端路由或外部同步方案,见 高可用与容量规划。

最小上手 ​

准备三台机器(或三个容器)、一个 MySQL,导入 schema,按上面的 cluster.conf 和 application.properties 启动。验证三件事:

bash
# 每个节点返回 readiness;-H 里的 token 来自 01 的登录
for ip in 192.168.1.11 192.168.1.12 192.168.1.13; do
  curl -s "http://$ip:8848/nacos/v3/admin/core/state/readiness" -H "accessToken: $ACCESS_TOKEN"; echo
done

# 节点列表里看 state 与 leader
curl -s "http://192.168.1.11:8848/nacos/v3/admin/core/cluster/node/list" -H "accessToken: $ACCESS_TOKEN"

三个节点都应返回健康、节点列表里三个都是 UP 且 Raft 只有一个 leader;再注册一个临时实例和一个持久实例,停掉一个节点,观察临时实例是否被接管、持久实例是否仍可读。这个实验能同时验证三套机制,不要只看「三个节点都起来了」。

生产动作 ​

  • 3 节点起步,奇数节点:2 节点不比 1 节点稳,Raft 多数派在 2 节点下没有容错空间;5 节点用于更高容错,代价是写延迟和运维成本。
  • MySQL 外置且做主从:Nacos 节点无状态,MySQL 才是有状态的部分;备份、慢查询、连接数都要按核心库对待。
  • 节点不要同机架:三个节点在同一台物理机/同一可用区,等于没做容灾。
  • NTP 同步:Distro 的 LWW 依赖时间戳,节点时间偏差会放大冲突。
  • 升级按手册:先备份、再测试、逐节点滚动;不要跳过大版本,不要混合版本回滚。
  • 监控:节点存活、Raft leader 切换、Distro 同步延迟、MySQL 慢查询与连接数、配置 dump 失败。

边界 ​

  • 没有原生跨数据中心复制:多机房要么本地集群 + 客户端路由,要么用 Nacos-Sync 一类外部方案,冲突与延迟要自己承担。
  • MySQL 是单点:Nacos 集群的高可用不等于配置存储的高可用;MySQL 挂了,配置写不了。
  • 2 节点不是高可用:Raft 需要多数派,2 节点挂 1 个就无法提交。
  • standalone 不能当集群:三个 standalone 节点互不通信,控制台看起来都健康,数据却各写各的。
  • 3.x 升级不是小版本:JDK、控制台端口、鉴权、API、Namespace 都变了,按小版本升级的心态操作会踩坑。

讲清楚 ​

Nacos 集群里同时跑着三套一致性机制:临时实例走 Distro,按服务分片、异步复制、时间戳后写覆盖,所以分区时两边都可能接受注册,恢复后收敛;持久实例走 Raft,写要多数派确认,少数派不可写,选举期间注册短暂不可用;配置则以 MySQL 为真相,节点只保留本地缓存,发布写库后靠通知和定时 dump 收敛,既不是 Raft 也不是强一致。集群至少 3 个奇数节点、外置 MySQL,cluster.conf 填 8848 主端口。分区时不要问「Nacos 会不会脑裂」,要分开问:Distro 接受双写后收敛,Raft 靠多数派避免双主,配置把问题交给 MySQL 高可用。升级 3.x 要过 JDK 17、鉴权、8080 控制台、API 与 Namespace 五道关,逐节点滚动,不要混合版本回滚。

把一门技术讲透到既懂也会用