etcd 分布式键值存储的核心机制

FreeGuideOnline 最新 2026-07-09

/services/api-server/address /services/api-server/port /config/db/host


配合 `--prefix` 查询可以列举某个前缀下的所有键值,实现类似目录的批量操作。

---

## Raft 共识算法

etcd 实现一致性、容错和选举的核心是 Raft 算法。集群中的所有写操作都必须经过 leader 提交。

### 角色与任期

- **Follower(跟随者)**:初始状态,被动接收 leader 的心跳和日志复制。
- **Candidate(候选人)**:当 follower 在规定时间内未收到心跳,会发起选举,切换为 candidate 并给自己投票。
- **Leader(领导者)**:赢得选举的节点,负责处理所有客户端写请求,管理日志复制。

整个过程以 **term(任期)** 为周期,每个 term 最多只有一个 leader。

### 日志复制流程

1. 客户端向 leader 发送写请求。
2. Leader 将操作追加到本地日志(为 `Entry`),并标记为 uncommitted。
3. Leader 并行向所有 follower 发送 `AppendEntries` RPC,携带日志条目。
4. 当 **多数派**(超过半数节点)确认收到条目后,leader 将其标记为 committed,并应用到状态机(也即实际更新键值存储)。
5. Leader 在下一次心跳或 `AppendEntries` 中告知 follower 提交位置,follower 也将其应用。
6. 最后 leader 返回成功给客户端。

### 安全性保证

- **选举安全性**:每个 term 最多一个 leader,通过随机选举超时和投票规则确保。
- **日志匹配特性**:如果两个日志在同一个 index 和 term 上的条目相同,则它们之前所有条目完全相同。
- **Leader 完整性**:leader 必须包含所有已提交的条目,不允许覆盖已提交日志。

### 快照与日志压缩

随着操作增多,Raft 日志会不断增长。etcd 周期性地创建 **快照**,将当前状态机的状态(即键值对集合)序列化保存,并截断快照之前的日志,避免磁盘和内存膨胀。在集群成员变动或新节点加入时,可以通过快照+增量日志快速同步。

---

## 核心 API 与操作模式

etcd 提供 v2 和 v3 两套 API,v3 是当前主流,基于 gRPC 协议。

### 基本键值操作

- `Range`:获取指定 key 或 key 前缀范围内的键值对。
- `Put`:写入一个 key-value,支持设置租约(TTL)。
- `DeleteRange`:删除指定 key 或前缀范围内的数据。
- `Txn`:事务接口,支持原子地判断多个条件并执行一组操作。

### Watch 机制

Watch 是 etcd 的杀手级特性,允许客户端订阅 key 的变化事件。

- 客户端建立一个 gRPC 流,通过 `Watch` 请求指定要监听的 key 或前缀,以及开始的 revision。
- etcd 服务器端会持续推送该范围内任何 key 的创建、更新、删除事件。
- Watch 基于 revision 进行,保证不丢失事件:客户端可以从上次接收到的 revision 继续监听,实现断线重连后的事件重放。
- 配合 MVCC,可以获取“从一个历史版本开始至今的所有变动”,这对实现事件溯源和响应式配置非常有用。

### 租约(Lease)

租约机制提供了一种分布式 TTL(Time To Live)的能力。

- 客户端通过 `LeaseGrant` 创建租约,指定 TTL 秒数。
- 将 key 与租约绑定(`Put` 时指定 `lease` 字段),当租约过期后,etcd 会自动删除与之关联的所有 key。
- 客户端需要周期性地调用 `LeaseKeepAlive` 续约,保持 key 存活。
- 租约是集群共识的一部分,过期操作由 leader 按时间顺序安全触发。这比单个节点的定时器更可靠。

### 事务(Transaction)

etcd 的事务支持 **“if-then-else”** 语义:

- `Compare`:比较目标 key 的版本(ModRevision、CreateRevision 等)或值是否满足条件。
- 成功时执行一组操作,失败时执行另一组(可选)。
- 所有操作在同一个 revision 中原子提交,保证读写隔离性。

实例——实现分布式锁:
```protobuf
// 如果 key 不存在则创建,相当于获取锁
If: key("lock") revision == 0
Then: put("lock", "holder1", lease=...)
Else: get("lock")