48.6 产品 (etcd / Consul / ZooKeeper / Nacos)
一句话
四个产品都能做「KV + watch + 租约」,差别在一致性语义、数据模型、多数据中心与生态;选型不是选功能最多的,而是选「故障模型与运维能力匹配」的那个。
一张总表
| 维度 | etcd | Consul | ZooKeeper | Nacos |
|---|---|---|---|---|
| 共识 | Raft | Raft | ZAB | 临时实例 Distro(AP);持久实例 Raft(CP) |
| 默认读一致性 | 线性一致(可切 serializable 本地读) | KV 读默认可能陈旧,consistent 才走 leader | 读可能陈旧(follower 本地返回) | 看实例类型/接口 |
| 数据模型 | 扁平 KV + revision | KV + catalog + health checks + intentions | znode 树 | namespace / group / service / config |
| Watch | 流式,按 revision 回放 | blocking query(index + wait) | 一次性 watch;3.6+ 持久 watch | 长轮询 + MD5 |
| 租约/会话 | lease + keepalive | session + TTL check | session + ephemeral | 心跳/临时实例 |
| 服务发现 | 需上层封装 | 原生 catalog / DNS | Curator 封装 | 原生 naming |
| 配置 | KV | KV | Curator | 原生 config |
| 多数据中心 | 无原生(mirror/learner 等方案) | 原生多 DC(WAN gossip) | observer / 手工 | 集群 + AP |
| 运维形态 | Go 单二进制,K8s 标配 | Go agent + gossip | JVM + 磁盘 + 会话 | Java + 控制台 |
| 主要生态 | Kubernetes | HashiCorp / Service Mesh | Hadoop / 旧 Kafka | Spring Cloud Alibaba |
⚠️ 这张表只能当「起点」:同一产品的不同 API、不同配置提供不同语义。比如 etcd 的
serializable读、Consul 的?consistent、ZooKeeper 的sync(),都会改变你拿到的一致性。
一致性:四个产品各自承诺什么
共识底座的细节见 21.5 Raft:选主、21.7 Raft:安全性证明;选型层面的算法对比见 21.12 共识算法对比。
| 产品 | 写 | 读 | 关键细节 |
|---|---|---|---|
| etcd | 线性一致(Raft 多数派提交) | 默认线性一致;serializable 读本地、可能陈旧 | watch 从 revision 回放;compaction 后旧 revision 不可读 |
| Consul | 线性一致(Raft) | 默认可能陈旧;?consistent 走 leader | blocking query 的 index 也可能落后;session 用于锁 |
| ZooKeeper | 线性一致(ZAB) | 可能陈旧(读本地副本) | 写后 sync() 再读才是新鲜;watch 一次性 |
| Nacos | 临时实例 AP、持久实例 CP | 取决于接口与实例类型 | 临时实例靠心跳,分区时可用;持久实例走 Raft |
🔑 最常见的一句话错误是「ZooKeeper 是强一致」。准确说法是:写线性一致 + 客户端 FIFO 顺序,读可能陈旧;需要新鲜读时先
sync()。
Watch 语义对比
| 产品 | 机制 | 断线续传 | 慢消费者 |
|---|---|---|---|
| etcd | 流式 watch,带 start_revision | 从 revision 回放(compaction 窗口内) | 慢 watcher 可能被断开或积压,客户端需重 LIST |
| Consul | blocking query(index + wait) | 带上次 index 继续 | 每个 watch 占一个连接;无服务端推送 |
| ZooKeeper | 标准 watch 一次性;3.6+ 持久 watch | 重新注册 + 重新 get | watch 数量受服务端内存限制 |
| Nacos | 长轮询(默认 30s 量级),MD5 比对 | 重新发起长轮询 | 服务端合并变更;客户端要处理超时重发 |
四种机制对应四种工程习惯:
- etcd:list + watch + revision,把 watch 当「增量提示」;
- Consul:blocking query,本质是「带 index 的长轮询」,没有推送;
- ZooKeeper:watch 触发后必须重新注册,注册间隙要重新读一次;
- Nacos:长轮询 + MD5,服务端 hold 住请求,变更才返回。
机制细节与「断线后从哪续」的通用模式见 48.5 Watch 与通知机制。
选型决策表
| 你的场景 | 选 | 理由 |
|---|---|---|
| 已经在 K8s 上,要配置/选主/锁 | etcd(或 K8s Lease) | K8s 已经依赖 etcd;少一套组件 |
| 多数据中心服务发现 + KV + 健康检查 | Consul | 原生多 DC、catalog、DNS 接口、mesh 生态 |
| Spring Cloud Alibaba、配置+注册一体、国内团队 | Nacos | 配置与命名一体、控制台友好、AP 注册 |
| Hadoop/旧 Kafka 生态、已有 Curator | ZooKeeper | 生态绑定;但 Kafka 新版本已转向 KRaft |
| 只想要一个强一致 KV + watch + lease | etcd | 模型最小、运维简单、K8s 验证充分 |
| 海量实例注册、能接受 AP | Nacos / Eureka | 心跳 + 本地缓存,分区可用 |
| 跨云/跨集群统一服务发现 | Consul / Nacos | 多 DC/多集群能力;etcd 需自己做 |
| 需要存大对象/高吞吐数据 | 数据库/对象存储 | 协调服务不是数据面 |
配置中心的能力与推拉权衡见 48.1 配置中心;注册、健康检查与摘除见 48.2 服务发现。
多数据中心:最容易低估的差异
| 产品 | 多 DC 能力 | 代价 |
|---|---|---|
| etcd | 无原生多 DC;常见做法是每 DC 一个集群 + 上层同步/mirror | 跨集群一致性要自己做 |
| Consul | 原生多 DC:每 DC 一组 server,WAN gossip 互联 | 跨 DC 查询延迟;WAN 故障要处理 |
| ZooKeeper | observer 跨 DC 读;写仍在主 DC | 写延迟;observer 读可能更陈旧 |
| Nacos | 集群 + AP 注册,跨集群同步 | 跨集群数据一致性弱;要接受 AP |
选多 DC 方案时先问:写要不要跨 DC? 要,就要接受 RTT;不要,就做「本地写 + 异步复制」,并接受冲突/陈旧。
容量与性能:量级,不是跑分
| 维度 | 影响因素 | 工程含义 |
|---|---|---|
| 写吞吐 | 共识落盘 + quorum RTT + 批量 | 协调服务写吞吐远低于业务数据库;不要拿它存流水 |
| 读吞吐 | 是否走 leader/quorum | 本地读快但可能陈旧;线性一致读要付 RTT |
| Watch 规模 | watcher 数 × 监听范围 × 事件速率 | 内存与网络的主要成本;慢 watcher 要能断开 |
| 会话/租约 | 实例数 × 心跳频率 | 心跳 QPS = 实例数 / 周期;TTL 决定误摘率 |
| 存储 | value 大小 × key 数 + 历史 revision | 小 value、少历史;大对象放对象存储 |
具体 QPS 取决于硬件、value 大小、批量与磁盘;不要用别人的跑分做容量规划,用自己的 value 大小和一致性要求压测。
运维与故障处理
| 事项 | etcd | Consul | ZooKeeper | Nacos |
|---|---|---|---|---|
| 备份 | snapshot | snapshot | snapshot + transaction log | 数据库备份(外置 MySQL) |
| 扩容 | 加成员/learner | 加 server/agent | 加 server/observer | 加节点 |
| 常见故障 | 磁盘慢导致 leader 抖动 | gossip/WAN 分区 | 会话超时、磁盘慢 | 数据库/长轮询堆积 |
| 关键监控 | leader 变更、fsync 延迟、watch 数 | serf/WAN、catalog 延迟 | 会话数、watch 数、磁盘 | 长轮询连接、配置推送延迟 |
上线配置:etcd 三节点
下面是三节点模板。本机用单节点 + TLS 跑通了
endpoint health、status、put/get与snapshot save;三节点只是initial-cluster不同。
# /etc/etcd/etcd.conf.yml(每个节点只改 name 与 IP)
name: etcd-1
data-dir: /var/lib/etcd
listen-client-urls: https://0.0.0.0:2379
advertise-client-urls: https://10.0.1.11:2379
listen-peer-urls: https://0.0.0.0:2380
initial-advertise-peer-urls: https://10.0.1.11:2380
initial-cluster: etcd-1=https://10.0.1.11:2380,etcd-2=https://10.0.1.12:2380,etcd-3=https://10.0.1.13:2380
initial-cluster-state: new
initial-cluster-token: prod-etcd-2026
client-transport-security:
cert-file: /etc/etcd/server.crt
key-file: /etc/etcd/server.key
client-cert-auth: true
trusted-ca-file: /etc/etcd/ca.crt
peer-transport-security:
cert-file: /etc/etcd/peer.crt
key-file: /etc/etcd/peer.key
client-cert-auth: true
trusted-ca-file: /etc/etcd/ca.crt
auto-compaction-retention: "1h"
quota-backend-bytes: 8589934592
heartbeat-interval: 100
election-timeout: 1000
| 参数 | 值 | 依据 | 配错的后果 |
|---|---|---|---|
| 成员数 | 3 或 5 | 容忍 (n-1)/2 台故障 | 2 节点:挂 1 台就不可写 |
initial-cluster-token | 每个集群唯一 | 防止新成员误加入旧集群 | 复用 token:两个集群串在一起 |
quota-backend-bytes | 8 GiB | 默认 2 GiB 量级,生产偏小;超限进入 NOSPACE 只读 | 不设/设太大:db 写满磁盘,全集群不可用 |
auto-compaction-retention | 1h | watch 断线可回放 1 小时;太长则 db 膨胀 | 不开:历史 revision 无限涨,终会撞 quota |
heartbeat-interval / election-timeout | 100 / 1000ms | 同 AZ 默认够;跨 AZ 要 ≥ 3×RTT | 跨 AZ 用默认:leader 反复重选,写超时 |
max-request-bytes | 默认 1.5 MiB | 单请求上限;大 value 直接拒绝 | 调大 = 用共识搬大对象,集群被拖慢 |
snapshot-count | 默认 10 万 | 每 10 万条做一次快照 | 太小:频繁 fork,影响写延迟 |
E="--endpoints=https://10.0.1.11:2379,https://10.0.1.12:2379,https://10.0.1.13:2379"
E="$E --cacert=/etc/etcd/ca.crt --cert=/etc/etcd/client.crt --key=/etc/etcd/client.key"
etcdctl $E endpoint health --cluster
etcdctl $E endpoint status --cluster -w table # DB SIZE / QUOTA / IS LEADER / RAFT TERM
etcdctl $E member list -w table
etcdctl $E alarm list # NOSPACE / CORRUPT
备份与恢复演练(恢复只能到空的 data-dir,且节点先停):
etcdctl $E snapshot save /backup/etcd-$(date +%F-%H%M).db
etcdutl snapshot status /backup/etcd-2026-10-26-1200.db -w table
etcdutl snapshot restore /backup/etcd-2026-10-26-1200.db --data-dir /var/lib/etcd-restore
| 场景 | 动作 | 不要做 |
|---|---|---|
| 单节点故障 | Raft 自动选主;客户端配置全 endpoint | 手工删 data-dir 重加 |
| 超过半数不可用 | 先把节点恢复,让它自己追 | --force-new-cluster(丢历史) |
| NOSPACE 告警 | 逐节点 defrag → alarm disarm → 调 quota 或清大 value | 只 disarm 不 defrag(很快再触发) |
| 误删 key | 只能快照恢复 | 指望 undo / 回收站 |
另一条不属于参数的纪律:etcd 只放最终态,变更历史放 Git。etcd 没有“回滚到上一版”的能力,快照恢复是整库回退,代价是回到备份时刻。
常见误区
| 误区 | 现实 |
|---|---|
| 「ZooKeeper 是强一致」 | 写线性一致,读可能陈旧;要新鲜读先 sync() |
| 「etcd 可以存大 value」 | 单 value 有上限,且大 value 会拖慢共识;大对象放对象存储 |
| 「Consul KV 默认强一致」 | 默认可能陈旧;需要时加 ?consistent |
| 「Nacos 只是配置中心」 | 它同时做服务发现(naming)与配置 |
| 「四个产品可以随意替换」 | 数据模型、watch 语义、会话/租约、多 DC 能力都不同,迁移有成本 |
| 「协调服务能当数据库/消息队列」 | 它是元数据/协调层,不是数据面 |
| 「Kafka 还依赖 ZooKeeper」 | 新版本已用 KRaft 取代 ZooKeeper |
反模式
| 反模式 | 后果 |
|---|---|
| 跨地域部署共识 quorum 还要求低延迟写 | 写延迟被 RTT 锁死,超时/抖动 |
| 用协调服务存业务流水/大对象 | 磁盘/共识被打满,集群不可用 |
| 不看 API 语义,默认读都是最新的 | 读到旧配置/旧实例列表 |
| watch 数量无上限、慢消费者不处理 | 内存上涨,服务端被拖垮 |
| 选主不做 fencing | 双主写(48.4) |
| 不做备份/恢复演练 | 元数据丢失无法重建 |
| 把 AP 注册中心用于强一致锁 | 分区期间双持有(48.3) |
自检
Q1 为什么说「ZooKeeper 是强一致」不准确?
▸ 答案:ZooKeeper 的写是线性一致的(ZAB 全序提交),但客户端可能连到 follower,读默认由本地副本返回,可能读到旧值;它保证的是「客户端 FIFO 顺序 + 写全序」,不是「任意读都最新」。需要新鲜读时先 sync() 再读。类似地,etcd 的 serializable 读、Consul 默认 KV 读也都可能陈旧。
Q2 etcd 和 Consul 都能做服务发现,选哪个?
▸ 答案:看是否需要原生多数据中心与服务发现开箱能力。Consul 原生多 DC、catalog、健康检查、DNS 接口,适合跨 DC/跨云的服务发现;etcd 只是一个强一致 KV + watch,服务发现要自己封装(或依赖 K8s),优势是与 K8s 同栈、组件少、运维简单。如果已经在 K8s 内,优先用 K8s Service/EndpointSlice;需要跨集群/非 K8s,再看 Consul/Nacos。
Q3 Nacos 的 AP/CP 是怎么切的?什么时候会踩坑?
▸ 答案:Nacos 的临时实例(ephemeral)走 Distro 协议(AP),靠心跳保活,分区时仍可注册/发现;持久实例(persistent)走 Raft(CP),强一致但分区时少数派不可用。踩坑场景:把需要强一致的锁/选主建立在临时实例上,或把高频变化的服务注册放到持久实例上——前者会在分区时双持有,后者会把 Raft 写打满。选之前先明确「这条数据要 AP 还是 CP」。
Q4 为什么不能用 etcd/Consul 的 KV 存业务大对象?
▸ 答案:协调服务的写要走共识(落盘 + 多数派),value 越大,每次写的网络/磁盘成本越高,还会拖慢整个集群的提交;同时 etcd 保留历史 revision、Consul 有 value 大小限制,大对象会迅速放大存储与快照成本。大对象应放对象存储/数据库,协调服务只存「指针/元数据」。
Q5 从 ZooKeeper 迁到 etcd,最容易忽略的语义差异是什么?
▸ 答案:① watch 模型:ZK 标准 watch 一次性,etcd 是流式 + revision 回放;迁移后要重写「重注册/补偿」逻辑。② 读一致性:ZK 读可能陈旧、需要 sync();etcd 默认线性一致读,延迟更高,要显式选 serializable。③ 临时节点 vs lease:ZK 会话超时 vs etcd lease keepalive,失效检测时间与客户端行为不同。④ 数据模型:树形 znode vs 扁平 KV + 前缀,路径/权限/通知范围都要重新设计。迁移前先做语义对照表,不要只做 API 映射。
Q6 etcd 报 NOSPACE 后为什么所有写都失败?正确的恢复顺序是什么?
▸ 答案:db 超过 quota-backend-bytes 后 etcd 进入只读告警,防止把磁盘写满导致崩溃——这是一种保护,不是 bug。恢复顺序:① 处理大 value / 清理无用 key;② 逐节点 etcdctl defrag(会阻塞该节点,滚动做);③ etcdctl alarm disarm 解除告警;④ 必要时调大 quota 或扩盘。只 disarm 不 defrag 会很快再次触发;根治是开 auto-compaction-retention 并把大对象移出 etcd。