etcd从零到生产级:Raft、MVCC、Revision、Watch、Lease与故障恢复
etcd 是面向分布式协调和控制面元数据的强一致键值存储。它使用 Raft 复制有序日志,以MVCC保存键值历史,通过全局revision、事务比较、Watch和Lease支撑Kubernetes状态、服务发现、配置、选主和锁等场景。
etcd不是“高性能业务缓存”,也不是把Redis命令换成gRPC。它的正确使用依赖多数派、线性一致读、历史压缩、租约失效、外部Fencing、低延迟磁盘、成员变更和灾难恢复。任何一项没设计好,控制面会出现Watch永久断流、Lease误过期、后端配额耗尽或恢复后缓存不收敛。
一、学习目标
- 区分Raft日志、MVCC revision和单个Key version。
- 解释写事务如何经Leader和多数派提交。
- 区分默认线性一致读和Serializable读。
- 使用Range、Put、Delete、Txn和Compare实现原子状态转换。
- 从List快照的header revision启动Watch,并处理Compacted错误。
- 解释Lease、KeepAlive、TTL、Revoke和失效窗口。
- 设计服务发现、分布式锁、选主和Fencing Token。
- 区分Compaction和Defrag,处理NOSPACE告警。
- 执行成员变更、Snapshot、Restore和生产Runbook。
二、etcd适合做什么,不适合做什么
| 适合 | 原因 |
|---|---|
| Kubernetes控制面状态 | 需要强一致元数据、Watch和版本并发控制 |
| 服务实例与成员发现 | Prefix + Lease + Watch |
| 小型动态配置 | MVCC、Txn、Watch |
| 业务Leader选举 | Lease、Txn、顺序revision |
| 分布式协调锁 | Txn、Lease、Watch;仍需Fencing |
| 控制器期望状态 | List-Watch增量收敛 |
不适合:
- 高频订单、流水和日志明细。
- 大对象、图片和文件。
- 低价值热点计数器。
- 需要复杂SQL和多表分析的业务。
- 以为Lease到期可以代替精确定时任务。
- 以为etcd事务能和MySQL、Redis、MQ原子提交。
三、组件和数据路径
flowchart TD
A["Client通过gRPC访问Endpoint"] --> B["接收请求的etcd Member"]
B --> C{"操作是否需要Raft写提交"}
C -- "写Txn" --> D["转交当前Raft Leader排序"]
D --> E["WAL追加并复制给Follower"]
E --> F["多数派确认后提交"]
F --> G["Apply到MVCC和Backend"]
G --> H["生成revision并通知Watch"]
C -- "读" --> I["按一致性选项执行Range"]| 对象 | 含义 |
|---|---|
| Member | 一个etcd Server进程及持久数据 |
| Endpoint | 客户端可连接地址 |
| Leader | 当前Raft日志排序者 |
| Follower | 复制日志并可服务部分读请求 |
| Learner | 非投票学习成员,便于追平后再晋升;版本有边界 |
| Cluster ID | 集群身份,防止错误成员混入 |
| Member ID | 成员身份 |
连接任意Endpoint不代表它就是Leader。客户端库通常维护多个Endpoint、重试和负载;需要Leader协调的操作由集群内部转发或协调。
四、一次写事务完整过程
flowchart TD
A["Client提交Put/Delete/Txn"] --> B["Member认证、限额和请求校验"]
B --> C["Leader把命令追加为Raft日志"]
C --> D["复制给Follower"]
D --> E{"满足多数派提交规则"}
E -- "否" --> F["等待、超时或失去Leader"]
E -- "是" --> G["推进commit index"]
G --> H["各Member按序Apply"]
H --> I["MVCC事务产生新的全局revision"]
I --> J["响应Client并投递Watch事件"]Leader本地追加不等于提交。3个投票成员需要多数2,5个需要多数3。少数派旧Leader即使进程仍活着,也不能继续安全提交。
Client超时或连接断开仍可能发生在“已经提交、响应丢失”之后;因此写结果可能UNKNOWN,必须用幂等业务键和后续Range/Txn确认。
五、Raft日志与MVCC不是同一层
| 层 | 解决问题 | 主要标识 |
|---|---|---|
| Raft | 多Member对命令顺序和提交达成一致 | term、log index、commit index |
| Apply状态机 | 按已提交顺序执行命令 | applied index |
| MVCC | 保存Key在不同revision下的值和元数据 | revision、create_revision、mod_revision |
| Backend | 持久化MVCC索引和值 | DB大小、in-use大小、quota |
不能把etcd revision直接叫做Raft log index。二者通常相关地向前推进,但属于不同抽象,恢复、只读事务和内部维护可能使简单一一对应假设失效。
六、MVCC和全局revision
etcd每次修改键空间的已提交事务产生新的全局revision。一个Txn同时修改多个Key,这些事件可以共享同一revision:
revision 100:
PUT /services/order/a
PUT /services/order/b
DELETE /services/order/oldrevision是集群键空间的逻辑版本,不是毫秒时间戳,也不是单个Key被修改的次数。
七、每个Key的四个关键元数据
| 字段 | 含义 |
|---|---|
create_revision | 当前Key生命周期首次创建的全局revision |
mod_revision | 最近修改该Key的全局revision |
version | 当前Key生命周期内修改次数;删除后重建会开始新生命周期 |
lease | 当前绑定的Lease ID,没有则为0 |
示例:
rev=10 PUT /config/order=v1
create_revision=10, mod_revision=10, version=1
rev=15 PUT /config/order=v2
create_revision=10, mod_revision=15, version=2
rev=20 DELETE /config/order
rev=30 PUT /config/order=v3
create_revision=30, mod_revision=30, version=1所以:
- 判断“不存在”常比较
version == 0。 - 防并发覆盖常比较
mod_revision == expected。 - 排队锁可利用创建revision表达先后。
- 不能把Key version当全局revision。
八、Range和Prefix读取
etcd v3 KV读取本质是Range:
- 精确Key:
key到该Key范围。 - Prefix:计算前缀的结束边界。
- 范围:
[key, range_end)。 - 可指定历史revision读取旧快照,只要该revision未被Compaction。
- 可设置limit、排序、只取keys/count等选项。
大前缀不能无限一次性返回。生产客户端应分页并记录整次List的快照revision,避免边分页边读取不同revision形成不一致集合。
九、默认线性一致读为什么更贵
etcd v3 Range默认提供线性一致语义。接收请求的Member需要确认当前Leader仍有多数派领导权,并确保本地状态机应用到安全读位置,常涉及ReadIndex/Leader协调,而不是随便读取Follower本地Backend。
线性一致读保证该读可放进全局实时顺序,不会返回已经被一个明确完成写覆盖的旧状态。
flowchart TD
A["Client发起线性一致Range"] --> B["Member向Leader确认安全读位置"]
B --> C["Leader确认仍拥有Quorum领导权"]
C --> D["等待本地applied index达到安全位置"]
D --> E["从MVCC读取并返回"]十、Serializable读是什么
设置Serializable选项后,Member可以从本地状态读取,不必完成线性一致读的Leader确认,延迟更低、可扩展性更好,但可能返回落后值。
这里的Serializable不是数据库“可串行化事务隔离级别”的同义词。它表示结果符合某个已提交串行历史,但可能不是最新线性一致状态。
适用:
- 容忍短暂陈旧的监控展示。
- 大规模只读缓存预热。
- 明确不用于所有权裁决的查询。
不适用:
- 获取锁后确认当前持有者。
- Leader选举裁决。
- 权限或配置发布的关键前置检查。
- 资金、唯一状态转换。
十一、Txn比较—成功—失败模型
etcd Txn由三部分组成:
IF compares全部成立
THEN 执行success operations
ELSE 执行failure operations可比较:
version。create_revision。mod_revision。- value。
- lease。
可执行Range、Put、Delete和受版本限制的组合操作,具体嵌套/操作数量受API和Server限制。
11.1 原子创建
IF version(/jobs/2026-07-18/order) == 0
THEN PUT owner=request-1001
ELSE GET existing owner只有一个并发请求进入success分支,其他请求直接读取已有结果,避免“先GET不存在、再PUT”的竞态。
11.2 防并发覆盖
IF mod_revision(/config/order) == 100
THEN PUT new-config
ELSE GET current-config失败分支应返回当前值和revision,让调用方合并或提示冲突;不能退化成无条件Put。
11.3 etcd Txn不能包含什么
Txn只覆盖一个etcd集群内的KV操作,不能原子包含MySQL、Kafka、HTTP或Redis。跨系统仍需要Outbox、幂等、状态机和补偿。
十二、Watch事件流原理
Watch订阅Key或Range从某revision之后的变化。事件通常包含:
- PUT或DELETE类型。
- Key和新值。
- create/mod revision、version、lease。
- 可选前值
prev_kv。 - Watch响应Header中的集群revision。
同一事务修改多个Key时会产生同revision的多个事件。消费者应按revision和事务语义处理,不能假设每个事件都有独立revision。
十三、正确的List-Watch闭环
如果直接先List再从“当前时间”Watch,List和Watch之间可能漏变化。正确流程:
flowchart TD
A["Range/List读取完整前缀快照"] --> B["保存响应Header Revision=R"]
B --> C["原子发布本地快照"]
C --> D["从R+1建立Watch"]
D --> E["按revision应用PUT/DELETE"]
E --> F{"Watch是否断开或被Compaction"}
F -- "普通断开" --> D
F -- "Compacted" --> A分页List必须保持同一历史revision:第一页响应给出R,后续页指定revision=R;否则翻页期间新增/删除Key会造成重复、遗漏或集合混合。
十四、Watch断开怎样恢复
客户端保存最后完整应用的revision lastAppliedRevision:
- 流断开后,从
lastAppliedRevision + 1重新创建Watch。 - 事件处理和revision持久化必须有正确原子边界。
- 重复事件按revision/业务键幂等吸收。
- 若Server返回requested revision已compacted,不能无限重连同一revision,必须重新List全量快照。
Watch进度通知可以帮助客户端知道流仍存活和当前revision,但不是业务变更事件。
十五、Compaction做什么
MVCC保存历史版本会持续增长。Compaction声明某revision及之前的旧历史不再可读/Watch恢复,仅保留当前状态和较新历史。
历史revision: 1 ... 1000 ... 5000
compact at 4000
可恢复Watch: > 4000
请求revision 3000: ErrCompactedCompaction是逻辑历史压缩,不保证Backend文件立刻缩小。运行过慢、长时间离线的Watch客户端必须具备“ErrCompacted → 全量List → 从新R+1 Watch”路径。
15.1 自动Compaction
etcd支持按时间窗口或revision保留策略自动压缩,具体配置和解释随版本。保留过短会让暂时离线控制器频繁全量List;保留过长会扩大Backend、备份和维护成本。
选择依据:
- 最长允许离线时间。
- Watch消费者恢复能力。
- 写入revision增长速率。
- Backend配额和Snapshot窗口。
十六、Defrag为什么和Compaction不同
| 操作 | 作用 | 是否回收物理空间 |
|---|---|---|
| Compaction | 删除旧revision的逻辑可访问历史 | 不一定立即缩小DB文件 |
| Defrag | 重建/整理Backend空闲页 | 可降低物理DB占用 |
Defrag针对单个Member执行,可能阻塞该Member的读写或造成明显延迟,具体行为随版本。生产应逐Member进行,先处理Follower/非Leader,观察健康和追平后再继续,避免一次对所有Endpoint并发Defrag。
十七、Lease与TTL全过程
flowchart TD
A["Client请求Grant TTL"] --> B["etcd返回Lease ID和实际TTL"]
B --> C["Put Key并attach Lease"]
C --> D["Client通过KeepAlive流续租"]
D --> E{"续租是否持续成功"}
E -- "是" --> D
E -- "否且超过TTL" --> F["Leader判定Lease过期"]
F --> G["删除该Lease附着的Keys"]
G --> H["产生DELETE Watch事件"]一个Lease可以绑定多个Key。主动Revoke会撤销Lease并删除附着Key。Key可以重新绑定Lease,具体Put选项要明确。
17.1 Lease不是精确定时器
- Grant返回的TTL可能经过Server处理,不应只信请求值。
- KeepAlive响应证明续租在某个时点成功,不保证未来网络正常。
- Leader变化和调度会影响过期处理时刻。
- Client进程暂停但KeepAlive线程也暂停,会导致Lease过期。
- Key删除通知有提交和Watch传播延迟。
支付关单和订单超时应使用数据库截止时间与扫描/延迟消息,Lease仅适合协调性存活标记。
十八、用Prefix、Lease、Watch做服务发现
数据模型:
/services/stock/instance-a -> {host,port,zone,protocol}
/services/stock/instance-b -> {host,port,zone,protocol}每个实例Key绑定Lease并KeepAlive;Consumer执行List-Watch:
- Prefix Range取得完整实例快照和revision R。
- 从R+1 Watch PUT/DELETE。
- 原子更新本地不可变地址快照。
- Lease过期产生DELETE后移除实例。
- Watch compacted则重新List。
KeepAlive只能证明注册客户端仍能续租,不证明业务端口、数据库和线程池健康。Provider应把readiness与租约注册结合,Consumer仍需超时、熔断和被动失败统计。
十九、etcd分布式锁原理
etcd官方并发Recipe通常使用Session Lease、锁前缀、事务和创建revision建立排队:
/locks/inventory/<lease-or-generated-key>概念过程:
- 创建Session和Lease。
- 在锁前缀下Txn创建候选Key。
- 以create_revision排序。
- 最小revision持锁。
- 其他竞争者Watch比自己更早的候选删除。
- Session结束或Lease过期删除Key。
19.1 为什么仍需要Fencing Token
Client A取得锁revision 101后发生长暂停,Lease过期;Client B取得新锁revision 102并写数据库;A恢复后仍可能继续旧代码。
外部资源必须原子拒绝旧Token:
UPDATE resource_state
SET value = :value,
fencing_token = :token
WHERE resource_id = :id
AND fencing_token < :token;锁Key的create revision可作为候选Token,但必须保证同一集群、同一所有权语义,并由外部资源校验。相同Token的重复请求仍需要业务幂等。
二十、业务选主
Election Recipe与锁相似:候选Key绑定Lease,create revision最小者成为Leader,Leader值可被观察。Leader回调不保证任务Exactly Once:旧Leader可能执行一半后Lease失效,新Leader接管后重复执行。
任务需要:
- 稳定业务任务键。
- PENDING/RUNNING/SUCCEEDED状态机。
- Fencing Token。
- 接管前事实扫描。
- 幂等和对账。
二十一、Java客户端版本边界
etcd v3 API基于gRPC。Java常用jetcd等客户端,但不同客户端版本对JDK、Netty、gRPC、TLS和etcd Server版本的要求不同:
- 存量JDK 8项目应锁定明确兼容版本并做依赖收敛。
- Java 17+项目应选择仍维护且与框架兼容的版本。
- 不要手工覆盖Netty/gRPC版本破坏客户端BOM。
- Spring Boot 2与3项目还要检查javax/jakarta无关但Netty/SLF4J相关的二进制兼容。
概念代码:
try (Client client = Client.builder()
.endpoints("https://etcd-1:2379", "https://etcd-2:2379", "https://etcd-3:2379")
.build()) {
KV kv = client.getKVClient();
ByteSequence key = ByteSequence.from("/config/order", StandardCharsets.UTF_8);
GetResponse response = kv.get(key).get(3, TimeUnit.SECONDS);
long revision = response.getHeader().getRevision();
System.out.println("revision=" + revision);
}生产必须补TLS证书、认证、Deadline、Endpoint故障切换和受控Executor;示例依赖版本应按项目JDK矩阵选择。
二十二、JDK 8 Demo:全局revision与Txn CAS
下面用无依赖模型演示:一次Txn修改两个Key只推进一次全局revision,而Key version分别变化。
import java.util.LinkedHashMap;
import java.util.Map;
public class EtcdMvccTxnDemo {
static final class Entry {
final String value;
final long createRevision;
final long modRevision;
final long version;
Entry(String value, long createRevision, long modRevision, long version) {
this.value = value;
this.createRevision = createRevision;
this.modRevision = modRevision;
this.version = version;
}
}
static final class Store {
private final Map<String, Entry> data = new LinkedHashMap<String, Entry>();
private long revision;
synchronized boolean txnPutIfModRevision(String compareKey,
long expectedModRevision,
Map<String, String> updates) {
Entry compared = data.get(compareKey);
long actual = compared == null ? 0L : compared.modRevision;
if (actual != expectedModRevision) return false;
long nextRevision = ++revision;
for (Map.Entry<String, String> update : updates.entrySet()) {
Entry old = data.get(update.getKey());
long create = old == null ? nextRevision : old.createRevision;
long version = old == null ? 1L : old.version + 1L;
data.put(update.getKey(),
new Entry(update.getValue(), create, nextRevision, version));
}
return true;
}
}
public static void main(String[] args) {
Store store = new Store();
Map<String, String> first = new LinkedHashMap<String, String>();
first.put("/config/order", "timeout=1000");
first.put("/audit/order", "created");
System.out.println("first=" + store.txnPutIfModRevision("/config/order", 0L, first));
Entry config = store.data.get("/config/order");
Entry audit = store.data.get("/audit/order");
System.out.println("sameTxnRevision=" + (config.modRevision == audit.modRevision));
Map<String, String> stale = new LinkedHashMap<String, String>();
stale.put("/config/order", "timeout=3000");
System.out.println("staleCompare=" + store.txnPutIfModRevision("/config/order", 0L, stale));
System.out.println("configVersion=" + config.version + ", globalRevision=" + config.modRevision);
}
}预期输出:
first=true
sameTxnRevision=true
staleCompare=false
configVersion=1, globalRevision=1二十三、etcdctl最小实验:真实观察Revision、Txn、Watch和Lease
所有生产命令都应显式指定受信任Endpoint、CA、客户端证书和Deadline。下面使用本地实验环境简写;不要把无TLS写法直接复制到生产。
23.1 查看Endpoint与Leader
export ETCDCTL_API=3
etcdctl --endpoints=http://127.0.0.1:2379 endpoint status --write-out=table
etcdctl --endpoints=http://127.0.0.1:2379 endpoint health生产TLS形式:
etcdctl \
--endpoints=https://etcd-1:2379,https://etcd-2:2379,https://etcd-3:2379 \
--cacert=/etc/etcd/pki/ca.crt \
--cert=/etc/etcd/pki/client.crt \
--key=/etc/etcd/pki/client.key \
endpoint status --cluster --write-out=tableendpoint health成功不等于业务Prefix、Lease和Watch全部正常,还要检查DB/quota、alarm和目标数据。
23.2 Put/Get并观察Revision
etcdctl put /config/order 'timeout=1000'
etcdctl get /config/order --write-out=json
etcdctl put /config/order 'timeout=1500'
etcdctl get /config/order --write-out=json比较响应中的Header revision、KV create_revision、mod_revision和version。第二次Put通常保持create_revision,推进mod_revision和version。
23.3 线性读与Serializable读
# 默认线性一致Range
etcdctl get /config/order
# 允许从本地已提交状态读取,可能陈旧
etcdctl get /config/order --consistency=s命令参数形式可能随etcdctl版本变化,使用前执行 etcdctl get --help。不要仅凭延迟不同判断读取来自Leader还是Follower,要结合Endpoint和指标。
23.4 Txn原子创建
交互输入示意:
etcdctl txn依次输入:
version("/jobs/2026-07-18/order") = "0"
put /jobs/2026-07-18/order request-1001
get /jobs/2026-07-18/order含义是:Key不存在则创建,否则读取已有值。空行用于结束Compare、Success和Failure各段;具体交互格式以当前版本帮助为准。
23.5 从Revision建立Watch
先取得完整快照及Header revision R:
etcdctl get /services/stock/ --prefix --write-out=json再从R+1开始:
etcdctl watch /services/stock/ --prefix --rev=101另一个终端写入:
etcdctl put /services/stock/instance-a '10.0.0.11:20880'
etcdctl del /services/stock/instance-a生产程序必须自动保存并恢复revision;命令中的101只是教学示例。
23.6 Lease注册和续租
etcdctl lease grant 30
etcdctl put /services/stock/instance-a '10.0.0.11:20880' --lease=<LEASE_ID>
etcdctl lease timetolive <LEASE_ID> --keys
etcdctl lease keep-alive <LEASE_ID>停止KeepAlive并等待Lease过期后,Key会被删除并产生Watch DELETE事件。不要把实验观察到的删除秒数当作永久精确定时承诺。
23.7 Compaction、Defrag和Alarm
etcdctl compact <SAFE_REVISION>
etcdctl endpoint status --cluster --write-out=table
etcdctl defrag --endpoints=https://etcd-follower-1:2379
etcdctl alarm list必须先确定安全revision,Defrag逐Member执行并使用TLS参数。不能在未备份、不了解Watch恢复窗口时复制一个revision直接Compact。
二十四、Backend配额与NOSPACE告警
Backend物理DB达到 quota-backend-bytes 附近时,etcd会触发NOSPACE告警并限制会扩大状态的写入,以保护集群。
恢复不能只扩大磁盘:
- 检查endpoint status中的DB size和in-use size。
- 确认写入增长来源和Compaction策略。
- 对安全revision执行Compaction。
- 逐Member Defrag回收物理空间。
- 确认DB降到配额内。
- 再解除NOSPACE alarm。
- 验证Watch、Lease和业务控制器恢复。
在未回收空间前反复disarm alarm,只会让告警再次出现。
二十五、成员变更与Learner
成员变更是Raft配置变化,应一次操作一个成员并保持多数派。
推荐思想:
flowchart TD
A["确认Cluster健康和Snapshot"] --> B["新增Learner"]
B --> C["等待Learner追平Leader"]
C --> D["Promote为Voting Member"]
D --> E["确认Quorum和延迟稳定"]
E --> F["再移除旧Member"]Learner支持和数量限制随etcd版本变化。它不参与投票,避免尚未追平的新节点立即改变Quorum。不能一次替换多数成员,也不能把复制data-dir当作成员加入。
二十六、Snapshot备份
使用受支持的 etcdctl snapshot save 或客户端维护API从健康Endpoint获取Snapshot,并用对应工具检查状态。备份还应保存:
- etcd精确版本。
- 成员和Endpoint拓扑。
- TLS证书、CA和认证配置。
- 启动参数与配额。
- Kubernetes场景下的兼容信息和加密配置。
定期Snapshot不是完成灾备;必须在隔离环境执行Restore演练并测量RTO。
二十七、Restore为什么会创建新逻辑集群
Restore从Snapshot生成新的Member数据目录、Cluster/Member身份和启动配置,不能直接覆盖正在运行的旧data-dir后让节点“自动继续”。
Kubernetes等大量使用Watch缓存的系统恢复旧Snapshot后,revision可能回退,控制器本地缓存却记得更高resourceVersion。较新etcd恢复流程支持revision bump和标记compact等机制来迫使Watch消费者重建;具体命令和要求必须按etcd/Kubernetes版本官方灾备文档执行。
恢复后要验证:
- 所有Member形成新Quorum。
- revision/compact策略满足消费者恢复。
- API Server、控制器和Watch重新List。
- Lease和临时注册按业务重新建立。
- 不把旧集群Member误连到新集群。
二十八、安全边界
- Client端启用TLS并校验Server证书。
- Peer通信启用双向TLS,防止伪造Member。
- 启用用户/RBAC,按Prefix最小授权。
- 证书轮换要滚动验证,避免同时过期失去Quorum。
- etcd值默认不能被假定为磁盘加密;使用磁盘加密或上层信封加密。
- Kubernetes Secret的Base64不是加密,需配置API Server静态加密/外部KMS。
- 2379/2380等端口只在控制网络开放。
- 日志、命令和指标不输出密钥值和认证Token。
二十九、关键监控指标
Raft和集群
- has leader、leader changes。
- proposals committed/applied/pending/failed。
- Member applied index与Leader差距。
- peer round-trip time。
磁盘与Backend
- WAL fsync duration。
- backend commit duration。
- DB physical size和in-use size。
- quota使用率、NOSPACE alarm。
- Snapshot和Defrag耗时。
API与业务
- gRPC request duration和错误码。
- Range/Txn/Watch/Lease QPS。
- Watch stream数量、慢Watcher、compacted恢复次数。
- Lease grant/keepalive/expired数量。
- Client重试、DeadlineExceeded和Unavailable。
WAL fsync和Backend commit的P99通常比平均值更能解释控制面尾延迟。
三十、Runbook:没有Leader或Leader频繁变化
- 检查投票成员可达数是否达到Quorum。
- 检查Peer TLS证书、时间有效期和SAN。
- 检查2380网络RTT、丢包和防火墙。
- 查看WAL磁盘fsync、磁盘满和IO错误。
- 检查CPU throttling、长GC和宿主机停顿。
- 检查是否同时重启或移除多数Member。
- 不要清空data-dir或强制创建多个独立单节点集群。
三十一、Runbook:请求慢和apply request took too long
沿链路拆解:
Client排队/网络
+ Leader ReadIndex或Raft提议
+ WAL fsync和多数派复制
+ Apply等待
+ Backend commit
+ 大Range序列化检查WAL和Backend P99、peer RTT、proposals pending、applied lag、大Txn、大Prefix Range、Watch扇出、Compaction/Defrag和宿主机资源。增加客户端超时不会消除存储瓶颈。
三十二、Runbook:Watch报required revision has been compacted
- 停止从旧revision无限重连。
- 对目标Prefix执行分页List,固定同一最新revision R。
- 原子替换本地快照。
- 从R+1重新Watch。
- 监控全量重建耗时和内存。
- 若频繁发生,检查消费者离线时间、处理积压和Compaction保留窗口。
三十三、Runbook:Lease意外过期
- 查KeepAlive最后成功时间和响应TTL。
- 检查客户端进程暂停、GC、Executor阻塞。
- 检查Client到所有Endpoint网络和TLS。
- 查看Leader变化和集群请求延迟。
- 确认Key确实绑定目标Lease。
- 新Session重建Key前先撤销旧业务所有权并取得新Token。
三十四、Runbook:DB碎片率高
当physical DB远大于in-use:
- 确认已经对安全历史执行Compaction。
- 备份并确认Cluster健康。
- 逐个Member执行Defrag。
- 每次等待Member恢复和applied index追平。
- Leader最后处理或按版本推荐流程切换。
- 观察延迟和NOSPACE状态。
三十五、Runbook:成员数据损坏或Hash不一致
使用endpoint status/health和hash检查定位异常Member;保留日志和数据副本,不要把损坏data-dir复制到其他节点。若仍有健康Quorum,按官方流程移除异常Member、以Learner重新加入并追平。多数数据都损坏时进入Snapshot灾备,不能凭单个旧节点任意成为新事实源。
三十六、etcd、ZooKeeper、Consul、Redis和Nacos比较
| 组件 | 核心模型 | 典型优势 | 主要边界 |
|---|---|---|---|
| etcd | Raft + MVCC KV + revision/Watch/Lease | 云原生控制面、强一致Txn | Backend配额、Compaction、磁盘要求 |
| ZooKeeper | ZAB + znode + Session/Watch | 成熟协调Recipe、传统Dubbo | 内存树、Watcher和Session语义 |
| Consul | Raft Catalog/KV + Agent + Gossip | 服务发现、健康检查、多DC | Gossip与Raft职责需区分 |
| Redis | 内存数据结构 + 复制/Cluster | 高吞吐缓存和原子脚本 | 普通复制不是强一致共识 |
| Nacos | 服务发现 + 配置 | Spring Cloud Alibaba集成 | 临时/持久实例和一致性不能粗暴一概而论 |
三十七、etcd在Kubernetes中的边界
正常组件通过API Server访问Kubernetes对象,不直接读写etcd,因为API Server负责:
- 认证和授权。
- Admission。
- 默认值和校验。
- API版本转换。
- 审计。
- 对象并发控制。
- Secret加密转换。
Kubernetes resourceVersion与底层存储revision有关,但API语义将其视为不透明并发/Watch标识。业务代码不应把不同资源的resourceVersion当普通整数做自定义全局排序,也不应绕过API Server修改etcd。
三十八、常见错误及后果
| 错误 | 后果 |
|---|---|
| revision等于Key version | CAS和Watch恢复错误 |
| revision等于Raft log index | 混淆共识与MVCC层 |
| Serializable读等于数据库可串行化 | 用陈旧值做所有权裁决 |
| Watch断开总从旧revision重连 | Compaction后永久失败循环 |
| Compaction会自动缩小DB文件 | NOSPACE仍然存在 |
| 并发Defrag所有Member | 集群请求停顿和失去可用性风险 |
| Lease是精确定时器 | 订单关闭延迟或错误 |
| KeepAlive成功等于业务健康 | 注册存在但Provider线程池已死 |
| etcd锁不需要Fencing | 暂停旧客户端覆盖新持有者 |
| 直接改Kubernetes的etcd | 绕过API校验并破坏控制器假设 |
三十九、面试标准回答
etcd使用Raft让多数Member对写命令顺序达成一致,提交后Apply到MVCC并生成全局revision;一次Txn修改多个Key可共享一个revision。每个Key还有create_revision、mod_revision、version和lease,不能混用。Range默认线性一致,需要确认Leader/安全读位置;Serializable读可本地返回但可能陈旧。正确Watch先List得到header revision R,再从R+1订阅;若旧revision已Compaction,必须重新List,不能无限重连。Compaction删除旧MVCC历史的逻辑可见性,Defrag才回收Backend物理空页。Lease通过KeepAlive维护协调性存活,但过期不精确,锁和选主仍需外部Fencing与业务幂等。生产重点监控Leader、Raft pending/applied、WAL fsync、Backend commit、DB/quota、Watch恢复和Lease过期,并通过Snapshot与逐成员恢复保障灾备。
四十、关联知识点
本章小结
etcd的完整链路是“Raft提交命令 → Apply到MVCC → 全局revision推进 → Txn原子裁决 → Watch增量传播 → Lease管理协调性生命周期”。生产系统还必须处理Compaction后的全量重建、Defrag、Quota、成员追平、Snapshot恢复和旧持有者Fencing。只会Put/Get或说“etcd是Kubernetes数据库”,还不能解释它为什么可靠、怎样失败和如何恢复。
