主题
集群与一致性
一句话: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 的升级顺序
- 备份 MySQL,并在测试环境验证注册、配置、推送三条链路。
- 升 JDK 17;3.x 不再支持 JDK 8。
- 补鉴权配置:
nacos.core.auth.plugin.nacos.token.secret.key、server.identity.key/value所有节点一致,否则启动失败或节点间通信失败。 - 改端口与探针:控制台从 8848/nacos 变为 8080 且无
/nacos前缀;客户端仍需 9848/9849。 - 处理 API 与命名空间:v1/v2 API 在 3.2 起剥离到
api-legacy-adapter,后续版本可能移除;默认 Namespace 从空字符串变为public,按官方迁移说明处理。 - 逐节点滚动,每升一个节点观察集群状态与客户端连接,再升下一个;最后升客户端。
- 不要混合版本回滚:3.2.4 起 JRaft 鉴权进入强制模式,回滚到不支持该鉴权的版本会破坏节点间通信。
实战场景
一、三台“集群”其实是三个单机
现象:按 3 节点部署 Nacos,压测通过后上线。运行一周后出现问题:同一服务在不同调用方看到的实例列表不一致;控制台里某个服务只有 1 个实例,另一个节点上看是 3 个。运维脚本往其中一个节点注册的数据,其他节点完全看不到。
排查:集群是否真的成立,有唯一的判据。
| 定位层 | 取证据 | 判定 |
|---|---|---|
| 启动模式 | 是否显式 MODE=cluster 或 -m cluster | standalone 起三份不会组成集群 |
| 节点清单 | 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:8848bash
# 启动后必须校验“成员数 = 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,绝对值应远小于 100mstext
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 五道关,逐节点滚动,不要混合版本回滚。