mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
6059 字
17 分钟
etcd Raft 实现与选型
2024-11-20

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 风格和数据模型。

维度etcdZooKeeper
数据模型扁平 KV,key 是字符串,value 是字节数组树形 znode,路径层级结构
APIgRPC自定义 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 就是 PutRange(范围查询)、DeleteRangeTxn(事务)、Watch(监听变更)几个。

etcd 的存储引擎用 MVCC(多版本并发控制)。每次写操作不会覆盖旧值,而是生成一个新的 revision。revision 是全局单调递增的整数,每次事务提交加 1。底层持久化存储用 b+tree 存所有版本数据,内存里再维护一棵 btree 索引加速按 key 范围查询。同一个 key 的每次修改都对应一个 revision。

# 往 etcd 写一个 key,再修改两次,查看历史
etcdctl put /config/db-host 10.0.0.1 # 假设这是集群首次写入,对应全局 revision 2
etcdctl put /config/db-host 10.0.0.2 # revision 3
etcdctl 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 最后修改时的 revision

MVCC 的好处是 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)
Note

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
}

上层的驱动循环大致是这样的结构。

sequenceDiagram participant App as etcdserver participant N as raft.Node participant W as WAL participant N2 as Network participant KV as MVCC Store App->>N: Tick() App->>N: Ready() N-->>App: Ready{Entries, Messages, CommittedEntries} App->>W: 写入待持久化日志 App->>N2: 发送 Messages 给其他节点 App->>KV: 应用 CommittedEntries 到 KV App->>N: Advance()

上层取到 Ready 后的固定流程:先把 HardStateEntries 写进 WAL,再把 Messages 通过网络发给其他节点,最后把 CommittedEntries 应用到 MVCC 存储。处理完调 Node.Advance() 告知算法库可以推进到下一轮。

这个设计的精妙之处在于:算法库不知道磁盘和网络,但通过 Ready 的字段顺序暗示了处理优先级。HardStateEntries 要先落盘,因为崩溃后靠它们恢复状态。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 里的 HardStateEntries,上层先写进 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 条目和日志条目可以删除。

flowchart TD A["Raft 日志不断增长"] --> B{"达到快照阈值?"} B -->|是| C["截取当前 KV 存储状态"] C --> D["写入快照文件<br/>包含 lastIncludedIndex/Term"] D --> E["删除快照点之前的 WAL 条目"] E --> F["Follower 落后太多时<br/>Leader 发送快照而非日志"] B -->|否| A

快照和 Raft 论文里的 InstallSnapshot RPC 对应。当某个 Follower 落后太多,Leader 发现 nextIndex 指向的位置已经被快照覆盖了,就不再发日志条目,而是直接发整个快照。Follower 收到快照后用快照数据覆盖本地状态,把日志截断到快照点。

etcd 默认的快照阈值是日志条目数达到一定数量后触发。快照过程会阻塞写入,所以 etcd 不会频繁做快照,也不做太小粒度的快照。快照文件本身也走 WAL 机制保证原子性,不会出现写了一半崩溃导致快照不可用。

四、Linearizable Read 与 Lease#

强一致性不只是写一致,读也要一致。etcd 的默认读是 linearizable(可线性化读),不会读到旧数据。

4.1 ReadIndex 机制#

Linearizable 读的核心问题:Follower 本地可能有未追上 Leader 的数据,直接读 Follower 可能读到过期的值。etcd 用 ReadIndex 机制保证读一致。

sequenceDiagram participant C as Client participant F as Follower participant L as Leader C->>F: Get /config (linearizable) F->>L: ReadIndex 请求(带当前 commitIndex) L->>L: 确认自己仍是 Leader<br/>(发心跳给多数节点) L-->>F: 返回当前 commitIndex F->>F: 等待本地 applyIndex 追上 commitIndex F-->>C: 返回数据

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 /config

4.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 不会降低现有集群的容错能力。

flowchart LR subgraph "现有集群(3 投票节点)" L["Leader"] --> F1["Follower"] L --> F2["Follower"] end L -->|复制日志(不投票)| LN["Learner<br/>新节点"]

加节点的正确流程是先加 learner,等它把日志追上后,再通过成员变更把它提升为投票成员。这样新节点追赶日志的整段时间里,集群始终是原来的 3 节点多数派,容错能力不变。

# 添加 learner 节点(不参与投票)
etcdctl member add node4 --peer-urls=http://10.0.0.4:2380 --learner
# 查看成员列表,learner 标记为 IsLearner: true
etcdctl 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/prevTermzxid 单调递增,无 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 在心跳中带 commitIndexLeader 提交时广播 Commit 消息
客户端 APIgRPC,扁平 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 的选举超时由 ElectionTickHeartbeatTick 两个参数控制,单位是 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 已被压缩
Warning

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 GiB

7.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 与数据模型#

维度etcdZooKeeper
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 规模与性能#

维度etcdZooKeeper
建议数据量≤ 8 GiB单 znode ≤ 1 MB,总量几 MB 到几十 MB
读性能linearizable 读有 Leader 往返开销默认读本地,延迟低
写性能Raft 复制 + WAL fsyncZAB 广播 + WAL fsync
适合场景K8s 状态存储、配置选主、锁、服务发现

两个系统都强调”协调服务不是数据库”。数据量大了别用任何一个。具体选择看生态匹配度:K8s 体系用 etcd,Java 大数据体系用 ZooKeeper,这样能复用现有基础设施和团队经验。

8.4 选型建议#

以下是几个典型场景的选择参考。

场景推荐理由
Kubernetes 集群etcdK8s 原生依赖,生态绑定
服务发现(Go 技术栈)etcdgRPC 客户端,语言匹配
服务发现(Java 技术栈)ZooKeeper 或 NacosCurator 封装成熟,Dubbo 原生支持
分布式锁均可ZooKeeper 临时顺序节点开箱即用;etcd 用 concurrency 包
配置中心均可etcd Watch 干净;ZooKeeper 持久 Watcher 也够用
大数据组件协调ZooKeeperHBase、Kafka、Hadoop 生态默认选择

两个系统在功能上高度重叠,真正的差异在生态和工程细节。团队熟悉的、系统里已经有的,就是该用的。

参考资料#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

etcd Raft 实现与选型
https://blog.souloss.cn/posts/middleware/coordination/etcd-raft-implementation-and-selection/
作者
Souloss
发布于
2024-11-20
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时