Kubernetes 把整个集群的状态都存在 etcd 里,Pod、Service、ConfigMap、Secret,每一个资源的增删改查都经过 etcd。CoreDNS、CoreOS 的 locksmith、Rook 的编排元数据也依赖它。etcd 的角色和 ZooKeeper 一样,是分布式协调服务:不存业务数据,只存那些”一致性要求极高但数据量不大”的元数据。但两者走的路子截然不同。ZooKeeper 自己设计了 ZAB 协议,etcd 直接用 Raft。Raft 算法的选举、日志复制、安全性保证这些理论细节不属于本文范围,这里只聊工程实现:etcd 怎么把 Raft 论文变成生产级代码,以及什么时候选 etcd 什么时候选 ZooKeeper。同目录的 ZooKeeper ZAB 与临时节点 讲 ZAB 那套机制,这篇是它的对照面。
一、etcd 是什么
1.1 协调服务定位
etcd 是一个强一致性的分布式键值存储,名字来自 “etc”(Unix 的配置目录)加 “d”(daemon),最初的定位就是存分布式系统的配置信息。后来 Kubernetes 把它选为状态存储后端,etcd 才从”配置中心”演变成”整个集群的单一可信来源”。
和 ZooKeeper 一样,etcd 不存业务数据。它的数据量通常在几百 MB 到几 GB 之间,每一次写入都经过 Raft 共识保证多数节点一致。区别在于 API 风格和数据模型。
| 维度 | etcd | ZooKeeper |
|---|---|---|
| 数据模型 | 扁平 KV,key 是字符串,value 是字节数组 | 树形 znode,路径层级结构 |
| API | gRPC | 自定义 TCP 协议 |
| 数据量上限 | 官方建议 ≤ 8 GiB | 单节点默认 1 MB/znode |
| 典型用途 | K8s 状态存储、服务发现、配置 | 选主、配置、锁、服务发现 |
| 客户端 | gRPC 多语言客户端 | 原生 Java,Curator 封装 |
1.2 gRPC API 与 MVCC
etcd 的 API 基于 gRPC,不是 REST 也不是自定义协议。这意味着客户端用 Go、Java、Python 都能通过 gRPC stub 直接调用,不需要手写协议解析。核心 API 就是 Put、Range(范围查询)、DeleteRange、Txn(事务)、Watch(监听变更)几个。
etcd 的存储引擎用 MVCC(多版本并发控制)。每次写操作不会覆盖旧值,而是生成一个新的 revision。revision 是全局单调递增的整数,每次事务提交加 1。底层持久化存储用 b+tree 存所有版本数据,内存里再维护一棵 btree 索引加速按 key 范围查询。同一个 key 的每次修改都对应一个 revision。
# 往 etcd 写一个 key,再修改两次,查看历史etcdctl put /config/db-host 10.0.0.1 # 假设这是集群首次写入,对应全局 revision 2etcdctl put /config/db-host 10.0.0.2 # revision 3etcdctl put /config/db-host 10.0.0.3 # revision 4
# 读取某个历史 revision 的值(每条 get 只返回单个 revision 的快照)# 要列出所有历史版本,需对每个 revision 分别调用,或用 watch --rev=<起始 revision>etcdctl get /config/db-host -w json --rev=3# header.revision 是当前集群 revision,kvs[].mod_revision 是该 key 最后修改时的 revisionMVCC 的好处是 Watch 能从任意历史 revision 开始回放,不会漏掉变更。客户端断线重连后告诉 etcd “我从 revision 100 开始看”,etcd 把 100 之后的变更流式推给它。这比 ZooKeeper 一次性 Watcher 的”重新注册窗口”问题干净得多。
历史版本不会永远留着。etcd 默认不自动压缩,需要手动触发或配置 --auto-compaction-retention 开启定期压缩。压缩后旧的 revision 数据不可恢复,Watch 只能从压缩点之后的 revision 开始。这是运维上需要定期关注的点,后面踩坑章节会展开。
二、etcd 的 Raft 实现
Raft 论文描述的是算法,etcd 的工程实现有一套自己的分层设计。理解这套设计,才能看懂 etcd 处理选举、日志复制、成员变更的具体行为。
2.1 纯算法库与上层驱动的分层
etcd-raft 库的设计思路是把 Raft 算法和 I/O 层彻底分开。算法库只管状态机流转,不管网络收发,不管磁盘读写。网络、存储、状态机应用全部由上层 etcdserver 驱动。
import "go.etcd.io/etcd/raft/v3"
// etcd-raft 库的核心接口conf := &raft.Config{ ID: uint64(1), ElectionTick: 10, HeartbeatTick: 1, Storage: raftStorage, MaxSizePerMsg: 1024 * 1024, MaxInflightMsgs: 256,}n := raft.StartNode(conf, peers)go.etcd.io/etcd/raft/v3 是 etcd v3.5 的 import 路径。etcd v3.6 把 raft 库拆到独立仓库,路径改为 go.etcd.io/raft/v3。生产环境多数还在用 v3.5,这里以 v3.5 为准。
etcd-raft 库对外暴露的是一个 Node 接口,上层通过 n.Tick() 驱动时钟、通过 n.Step(m) 投递收到的 RPC 消息、通过 n.Propose(ctx, data) 提交写请求。库内部维护 Raft 状态机(当前角色、任期、日志、投票记录),但库自己不发起网络请求、不写磁盘。
这种”纯算法库”设计的好处是可测试性。etcd 的测试可以直接构造消息序列喂给状态机,验证选举和复制的边界行为,不需要起整个集群。代价是上层 etcdserver 要做不少胶水工作:把 Node 产出的数据落盘、把 RPC 发出去、把 committed 日志应用到 KV 存储。
2.2 Ready 结构体驱动状态机流转
算法库和上层之间的交互核心是 Ready 结构体。上层循环调用 Node.Ready() 取出一个 Ready,里面有算法库”准备好”交给上层处理的所有内容。
// Ready 结构体的关键字段(简化示意)type Ready struct { // 当前软状态:Leader 是谁、集群有哪些节点(不持久化) SoftState *SoftState // 当前硬状态:任期、投票给谁、提交位置(需持久化) HardState pb.HardState // 待持久化的日志条目(写入 WAL) Entries []pb.Entry // 待发送的快照 Snapshot pb.Snapshot // 待发送给其他节点的消息(AppendEntries、RequestVote 等) Messages []pb.Message // 已提交待应用到状态机的日志条目 CommittedEntries []pb.Entry}上层的驱动循环大致是这样的结构。
上层取到 Ready 后的固定流程:先把 HardState 和 Entries 写进 WAL,再把 Messages 通过网络发给其他节点,最后把 CommittedEntries 应用到 MVCC 存储。处理完调 Node.Advance() 告知算法库可以推进到下一轮。
这个设计的精妙之处在于:算法库不知道磁盘和网络,但通过 Ready 的字段顺序暗示了处理优先级。HardState 和 Entries 要先落盘,因为崩溃后靠它们恢复状态。Messages 在落盘之后才发,保证发出去的消息对应的日志已经持久化,不会出现”消息到了对端但本地日志没写进去”的崩溃不一致。
2.3 SoftState 与 HardState
Ready 里的状态分软硬两类,对应 Raft 状态机的两类信息。
SoftState 不需要持久化,只存内存。核心字段是 Lead(当前 Leader 的节点 ID)和 RaftState(当前角色:Leader、Candidate、Follower)。节点重启后这些信息重新算出来,不依赖磁盘。上层靠 SoftState 的变化感知 Leader 切换,比如从 Follower 变成 Leader 时上层会更新自己的角色。
HardState 必须持久化,否则崩溃后无法恢复。三个字段:Term(当前任期)、Vote(给谁投了票)、Commit(已提交的日志索引)。这三个值每次变更都要写进 WAL,节点重启时从 WAL 重放出来。
软硬分离的设计和 Raft 论文一致。Raft 的安全性靠 HardState 保证,任期和投票记录丢了会导致同一任期重复投票,破坏安全性。而 Leader 是谁这种信息是运行时算出来的,不需要持久化。
三、WAL 与 Snapshot 持久化
etcd 的持久化分两层:WAL 预写日志保证 Raft 日志不丢,Snapshot 快照压缩历史日志。
3.1 WAL 预写日志
每次 Ready 里的 HardState 和 Entries,上层先写进 WAL 再做后续操作。WAL 是 append-only 文件,记录了所有日志条目和状态变更。etcd 启动时通过回放 WAL 恢复 Raft 状态机:重放出来的 HardState 告诉算法库当前任期和投票记录,重放出来的 Entries 告诉算法库日志内容。
# etcd 的 WAL 文件默认存放在 data-dir 下的 member/wal 目录ls /var/lib/etcd/default.etcd/member/wal/# 0000000000000000-0000000000000001.wal# 0000000000000001-0000000000000002.wal
# 每个 WAL 文件有大小上限(默认 64 MB),写满后滚动新文件WAL 的每次写操作都会 fsync,这是 etcd 对磁盘延迟敏感的根本原因。如果磁盘 I/O 延迟高,fsync 慢,Ready 处理循环被阻塞,Leader 发心跳的频率受影响,Follower 可能误判 Leader 挂了发起选举。生产环境 etcd 的数据目录必须用高性能磁盘,SSD 是底线,不要挂网络存储或普通云盘。
3.2 Snapshot 快照
WAL 无限增长会撑爆磁盘,etcd 定期做快照压缩。快照就是当前 KV 存储的完整状态,加上 Raft 的 HardState 和最后的日志索引,写成一个文件。快照之后,之前的 WAL 条目和日志条目可以删除。
快照和 Raft 论文里的 InstallSnapshot RPC 对应。当某个 Follower 落后太多,Leader 发现 nextIndex 指向的位置已经被快照覆盖了,就不再发日志条目,而是直接发整个快照。Follower 收到快照后用快照数据覆盖本地状态,把日志截断到快照点。
etcd 默认的快照阈值是日志条目数达到一定数量后触发。快照过程会阻塞写入,所以 etcd 不会频繁做快照,也不做太小粒度的快照。快照文件本身也走 WAL 机制保证原子性,不会出现写了一半崩溃导致快照不可用。
四、Linearizable Read 与 Lease
强一致性不只是写一致,读也要一致。etcd 的默认读是 linearizable(可线性化读),不会读到旧数据。
4.1 ReadIndex 机制
Linearizable 读的核心问题:Follower 本地可能有未追上 Leader 的数据,直接读 Follower 可能读到过期的值。etcd 用 ReadIndex 机制保证读一致。
Follower 收到 linearizable 读请求后,向 Leader 发一个 ReadIndex 请求,带上自己当前的 commitIndex。Leader 收到后先确认自己仍是 Leader(通过一轮心跳确认多数节点还认它),然后返回当前的 commitIndex。Follower 拿到这个值后等自己的 applyIndex 追上,再从本地 MVCC 存储读数据返回。
这套流程保证了读到的一定是 Leader 确认的最新已提交数据,不会读到旧值。代价是每次读都要一次 Leader 往返,延迟比直接读本地高。如果业务对延迟敏感且能容忍读到稍旧的值,可以用 serializable 读绕过 ReadIndex。
# 默认是 linearizable 读etcdctl get /config
# 显式指定 serializable 读(本地读,不经过 ReadIndex)etcdctl --consistency=s get /config4.2 Lease-based Leader Read
每次读都走 ReadIndex 的往返开销太大,etcd 做了优化:Leader 不需要每次读都发心跳确认,而是用 lease 机制。Leader 持有一个租约,租约在心跳有效期内认为 Leader 身份有效。Leader 直接处理发到自己的读请求,不需要额外往返。只有 Follower 的读请求才走 ReadIndex 向 Leader 确认。
这个优化基于一个前提:Leader 只要还能发出心跳被多数节点确认,就是合法的。lease 的时间不超过选举超时,保证 Leader 挂了之后 lease 失效之前不会有新 Leader 被选出。但极端情况下(时钟漂移),lease-based read 仍有理论上的脑裂窗口,etcd 默认配置下会权衡延迟和一致性。
4.3 Serializable 读
Serializable 读就是直接读本地存储,不向 Leader 确认。延迟低,但可能读到旧值。适用场景是读对实时性要求不高的配置类数据,或者明确知道本节点刚同步过。生产中大部分场景用默认的 linearizable 就好,只有延迟敏感且能接受短暂不一致时才考虑 serializable。
五、Learner 节点与成员变更
加新节点到 etcd 集群是个危险操作。新节点没有任何日志,Leader 要把全量日志同步给它,这个同步过程可能很久。如果直接把新节点当投票成员加进来,在它追上日志之前集群的容错能力下降,因为多数派里算上了这个空节点。
5.1 Learner 的定位
etcd v3.4 引入了 learner 节点(也叫 non-voting member)。learner 接收 Leader 复制的日志,应用到本地存储,但不参与投票。集群的多数派计算不算 learner,所以加一个 learner 不会降低现有集群的容错能力。
加节点的正确流程是先加 learner,等它把日志追上后,再通过成员变更把它提升为投票成员。这样新节点追赶日志的整段时间里,集群始终是原来的 3 节点多数派,容错能力不变。
# 添加 learner 节点(不参与投票)etcdctl member add node4 --peer-urls=http://10.0.0.4:2380 --learner
# 查看成员列表,learner 标记为 IsLearner: trueetcdctl member list
# 等新节点日志追上后,提升为投票成员etcdctl member promote <member-id>5.2 成员变更的安全保证
Raft 的成员变更有一致性风险:如果直接从旧配置切到新配置,不同节点切换时机不同,可能出现两个多数派重叠导致两个 Leader。Raft 论文用 Joint Consensus 方案两阶段解决,etcd 在工程上简化为”一次只变更一个节点”。
一次只加或删一个投票成员,保证多数派始终连续,不会出现两个不相交的多数派。如果要一次加多个节点,得串行操作,每次等前一个变更提交后再做下一个。learner 的引入把这个流程更安全化了:加节点分两步(先 learner 再 promote),删节点分两步(先移除再让对应实例退出),每步都不破坏现有多数派。
六、与 ZAB 对比
etcd 用 Raft,ZooKeeper 用 ZAB,两者都是”Leader 主导、过半写入”的共识协议,细节上有差异。ZooKeeper ZAB 与临时节点 里详细讲了 ZAB,这里放在一起对比。
6.1 选举与日志格式
| 维度 | etcd(Raft) | ZooKeeper(ZAB) |
|---|---|---|
| 任期号 | term,单调递增整数 | epoch,zxid 的高 32 位 |
| 日志格式 | 每条带 term + index,有 prevLogIndex/prevTerm | zxid 单调递增,无 prevTerm |
| 选举比较 | term 大的优先,其次日志索引长 | epoch 大的优先,其次 zxid 大 |
| 日志一致性 | 靠 prevLogIndex/prevTerm 匹配回退 | 靠 zxid 单调 + 同步补齐 |
Raft 的日志条目带 prevLogIndex 和 prevTerm,Leader 发 AppendEntries 时带上前一条的索引和任期,Follower 收到后检查本地日志是否匹配。不匹配就拒绝,Leader 回退 nextIndex 重发。这套日志匹配机制保证了日志的连续性。
ZAB 没有这套 prevTerm 机制,靠 zxid 全局单调递增和选举时选 zxid 大的节点当 Leader。zxid 大意味着见过的写入最多,新 Leader 上线就有全部已提交数据。两种思路都成立,Raft 更显式(靠匹配字段保证一致),ZAB 更隐式(靠单调序号和选主规则保证一致)。
6.2 提交与广播
| 维度 | etcd(Raft) | ZooKeeper(ZAB) |
|---|---|---|
| 提交方式 | Leader 在心跳中带 commitIndex | Leader 提交时广播 Commit 消息 |
| 客户端 API | gRPC,扁平 KV | 自定义 TCP 协议,树形 znode |
| 读一致性 | 默认 linearizable(ReadIndex) | 默认读到客户端上次见过视图之后 |
| 临时节点 | 无原生支持(靠 lease + TTL key 模拟) | 原生支持,与 Session 绑定 |
两个协议的提交都是过半即提交,差异在 Leader 怎么通知 Follower 提交。Raft 的 Leader 在后续的 AppendEntries 心跳里带上当前的 commitIndex,Follower 看到后推进本地 commitIndex。ZAB 的 Leader 提交时单独发一个 Commit 消息给所有 Follower。
API 层面的差异更影响使用体验。etcd 的扁平 KV 简单直接,没有目录层级概念,key 就是字符串。ZooKeeper 的树形 znode 有层级结构,适合需要组织路径的场景。etcd 没有原生的临时节点,但可以通过 lease 加 TTL key 实现”会话失效自动删除”的效果,功能上等价但语义上不如 ZooKeeper 的临时节点直观。
七、踩坑与运维
etcd 的运维围绕几个核心风险:磁盘延迟影响心跳、压缩不当导致历史堆积、成员变更操作不当导致多数派丢失。
7.1 磁盘 fsync 延迟导致选举抖动
WAL 每次写都 fsync,磁盘 I/O 延迟直接传导到 Ready 处理循环。如果磁盘慢,Leader 发心跳的频率被打断,Follower 在 election timeout 内没收到心跳就会发起选举。表现就是集群频繁切 Leader,客户端感知到的是间歇性超时。
排查方向是先看磁盘延迟。etcd 暴露的 wal_fsync_duration_seconds 指标直接反映 WAL 刷盘耗时,正常应该在毫秒级。如果这个指标飙升,先排查磁盘是不是共享了其他 I/O 负载,或者用了网络存储。
# 查看磁盘 fsync 延迟(etcd 暴露的 Prometheus 指标,走 /metrics 端点)curl http://etcd:2379/metrics | grep wal_fsync_duration_seconds
# etcd 官方建议的磁盘延迟基准:# fsync 99 分位 < 10ms 为健康解决办法是给 etcd 独占的快速磁盘。云上环境优先用本地 SSD 实例,或者给 etcd 单独挂一块 NVMe EBS。把其他 I/O 密集的服务(比如日志采集、监控 agent)挪走,别和 etcd 抢磁盘。
7.2 选举超时配置
etcd 的选举超时由 ElectionTick 和 HeartbeatTick 两个参数控制,单位是 tick。Leader 每 HeartbeatTick 个 tick 发一次心跳,Follower 在 ElectionTick 个 tick 内没收到心跳就发起选举。官方文档建议 ElectionTick 至少是 HeartbeatTick 的 10 倍,给网络抖动留余量。
# etcd 配置示例election-timeout: 1000 # 选举超时,默认 1000 毫秒heartbeat-interval: 100 # 心跳间隔,默认 100 毫秒etcd 默认 heartbeat-interval 是 100 毫秒,election-timeout 是 1000 毫秒,正好 10 倍关系。跨机房部署时网络往返延迟大,选举超时要按机房间 RTT 的多倍配置,否则 Follower 频繁误判 Leader 挂了。
ElectionTick 设太小,网络抖动一下就触发选举,集群抖动不断。设太大,Leader 真挂了要等很久才切换,影响可用性。3 节点同机房集群用默认值通常没问题,跨机房要专门调。
7.3 压缩与碎片整理
MVCC 保留历史版本,不压缩的话磁盘会被旧版本撑爆。etcd 默认不自动压缩,需要配置 --auto-compaction-retention 开启定期压缩,或手动 compact。压缩只标记旧版本可删除,空间不会立刻还给文件系统。要回收磁盘空间需要额外做碎片整理(defrag)。
# 压缩:清除指定 revision 之前的旧版本etcdctl compact <revision>
# 碎片整理:回收已压缩版本占用的空间etcdctl defrag
# 查看当前 revision 和 DB 大小etcdctl endpoint status -w table# 输出列: ENDPOINT, ID, VERSION, DB SIZE, IS LEADER, IS LEARNER,# RAFT TERM, RAFT INDEX, RAFT APPLIED INDEX, ERRORS# 注意:endpoint status 不输出 compact revision 或配额信息# 压缩点需通过尝试读旧 revision 报错间接确认:# etcdctl get <key> --rev=<旧 revision># 报 "mvcc: required revision has been compacted" 即说明该 revision 已被压缩defrag 会阻塞节点处理请求,在 Leader 上执行会导致该节点短暂不可用。生产环境务必逐个节点 defrag,一个完成后接下一个,不要同时对整个集群 defrag。对 Leader 执行 defrag 前可以先移除 Leader 再操作,或者用 --command-timeout 设个超时防止卡死。
压缩和碎片整理的节奏要根据业务 Watch 的需求定。如果客户端依赖长时间回放历史,过早压缩会让 Watch 断流。一般定期压缩(比如每小时或每天),保留足够覆盖客户端断线重连窗口的历史。
7.4 配额与数据量上限
etcd 默认有存储配额(--quota-backend-bytes),硬限制默认 2 GiB。超过配额后 etcd 会停止接受写入,整个集群变成只读,直到手动压缩降下来。这是保护机制,防止业务把协调服务当数据库用。
etcd 官方建议数据量不要超过约 8 GiB。这里要区分两套机制:真正会停写的 NOSPACE 告警绑定的是存储配额,默认 2 GiB,运行时数据量超过配额即触发;8 GiB 是配额配置的推荐上限,启动时若把 --quota-backend-bytes 设成超过 8 GiB 会打一条告警日志,但这针对的是配置值过大,不是数据量到 8 GiB 自动告警。8 GiB 以上 WAL 重放慢,快照大,故障恢复时间长,Raft 复制开销大。如果业务真的需要存这么多数据,说明 etcd 的定位已经不合适了,该换一个专门的存储系统。这和 ZooKeeper 的定位一致,协调服务存的是元数据,不是业务数据。
# 查看当前 DB 大小(endpoint status 不输出配额值)etcdctl endpoint status -w table# DB SIZE 显示当前数据大小,配额需查配置或 /metrics 中的 etcd_server_quota_backend_bytes
# 查看是否有配额超限告警etcdctl alarm list
# 生产环境调大配额(谨慎)etcd --quota-backend-bytes=8589934592 # 8 GiB7.5 运维命令速查
| 命令 | 用途 |
|---|---|
etcdctl member list | 查看集群成员和角色 |
etcdctl endpoint health | 健康检查 |
etcdctl endpoint status -w table | 查看详细状态(DB 大小、revision、leader) |
etcdctl snapshot save backup.db | 备份快照 |
etcdctl snapshot restore backup.db | 从快照恢复 |
etcdctl compact <rev> | 压缩历史版本 |
etcdctl defrag | 碎片整理回收空间 |
etcdctl alarm list | 查看告警(如配额超限) |
# 定期备份快照(建议写入 cron)etcdctl snapshot save /backup/etcd-$(date +%Y%m%d).db \ --endpoints=https://etcd1:2379 \ --cacert=/etc/etcd/ca.crt \ --cert=/etc/etcd/peer.crt \ --key=/etc/etcd/peer.key
# 恢复流程(灾难恢复时用)# 1. 停止所有 etcd 节点# 2. 在每个节点从同一快照恢复etcdctl snapshot restore backup.db \ --name=etcd1 \ --initial-cluster=etcd1=https://etcd1:2380,etcd2=https://etcd2:2380 \ --initial-cluster-token=new-cluster \ --initial-advertise-peer-urls=https://etcd1:2380 \ --data-dir=/var/lib/etcd-new# 3. 启动所有节点八、etcd 与 ZooKeeper 选型
etcd 和 ZooKeeper 都是协调服务,选哪个取决于技术栈和场景。
8.1 技术栈匹配
etcd 是 Go 写的,API 是 gRPC,原生支持 Go、Java、Python 等多语言客户端。如果你的系统跑在 Kubernetes 上,etcd 已经在集群里了,直接用就行,不用额外搭一套协调服务。Kubernetes 生态里的工具(CoreDNS、Rook、Prometheus Operator)都围绕 etcd 构建。
ZooKeeper 是 Java 写的,生态偏 Java 和大数据。HBase、Kafka(早期版本)、Dubbo、Hadoop HA 都用 ZooKeeper 做协调。如果技术栈是 Java 为主,团队对 JVM 运维熟悉,ZooKeeper 是自然选择。
8.2 API 与数据模型
| 维度 | etcd | ZooKeeper |
|---|---|---|
| API 协议 | gRPC(多语言友好) | 自定义 TCP(Java 原生) |
| 数据模型 | 扁平 KV | 树形 znode |
| 事务 | 支持 Txn(If-Then-Else) | 支持多操作事务 |
| 监听机制 | Watch(revision 回放,不丢变更) | Watcher(3.6+ 持久 Watcher) |
| 临时节点 | lease + TTL key 模拟 | 原生临时节点 |
| 分布式锁 | 需自己实现或用 clientv3 的 concurrency 包 | 临时顺序节点原生支持 |
etcd 的扁平 KV 模型简单,但不擅长需要层级组织的场景。ZooKeeper 的树形结构天然适合表示”应用的配置树""服务的实例列表”这类有层级关系的数据。etcd 要实现类似效果得在 key 前缀上做约定,比如 /service/inventory/10.0.0.1,没有原生的子节点列表语义。
监听机制上 etcd 更干净。Watch 基于 revision 回放,客户端断线重连后从上次看到的 revision 继续看,不会漏变更。ZooKeeper 的经典 Watcher 是一次性的,重新注册窗口内会丢事件,3.6+ 的持久 Watcher 才追上。
8.3 规模与性能
| 维度 | etcd | ZooKeeper |
|---|---|---|
| 建议数据量 | ≤ 8 GiB | 单 znode ≤ 1 MB,总量几 MB 到几十 MB |
| 读性能 | linearizable 读有 Leader 往返开销 | 默认读本地,延迟低 |
| 写性能 | Raft 复制 + WAL fsync | ZAB 广播 + WAL fsync |
| 适合场景 | K8s 状态存储、配置 | 选主、锁、服务发现 |
两个系统都强调”协调服务不是数据库”。数据量大了别用任何一个。具体选择看生态匹配度:K8s 体系用 etcd,Java 大数据体系用 ZooKeeper,这样能复用现有基础设施和团队经验。
8.4 选型建议
以下是几个典型场景的选择参考。
| 场景 | 推荐 | 理由 |
|---|---|---|
| Kubernetes 集群 | etcd | K8s 原生依赖,生态绑定 |
| 服务发现(Go 技术栈) | etcd | gRPC 客户端,语言匹配 |
| 服务发现(Java 技术栈) | ZooKeeper 或 Nacos | Curator 封装成熟,Dubbo 原生支持 |
| 分布式锁 | 均可 | ZooKeeper 临时顺序节点开箱即用;etcd 用 concurrency 包 |
| 配置中心 | 均可 | etcd Watch 干净;ZooKeeper 持久 Watcher 也够用 |
| 大数据组件协调 | ZooKeeper | HBase、Kafka、Hadoop 生态默认选择 |
两个系统在功能上高度重叠,真正的差异在生态和工程细节。团队熟悉的、系统里已经有的,就是该用的。
参考资料
- etcd Official Documentation - etcd 官方文档,涵盖架构、配置与运维
- etcd Tuning Guide - 选举超时、心跳间隔等时间参数调优
- etcd Hardware Recommendations - 磁盘与网络硬件建议,含 fsync 延迟基准
- etcd Disaster Recovery - 快照备份与恢复流程
- The Raft Paper - Diego Ongaro, John Ousterhout, In Search of an Understandable Consensus Algorithm, 2014,Raft 算法原始论文
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






