etcd
etcd 是一个分布式键值存储,提供一致的元数据读写、条件事务、变更监听和租约。它适合保存服务地址、配置和协调状态,应用再根据这些状态完成服务发现、任务调度等工作。
理解 etcd,要把三件事分开:集群已经提交什么,当前成员能够读到什么,客户端缓存已经处理到哪里。集群的一致性不能直接变成客户端缓存的即时一致,也不能替外部业务系统完成事务。本文以 etcd 3.5 API 和 3.5.21 实现为基线。
1. 面试核心
先记住这几条,再读正文- etcd 保存的是键值和协调状态。它可以为注册、配置和任务协调提供基础能力,但不会自行完成业务调度或健康判断。卡 · 综合题
revision是整个键值存储的逻辑修订号,version是单个键的版本计数。一次修改多个键的事务共用一个 revision,不能把 revision 当成某个键的修改次数。卡 · revision 与键版本- 比较事务把条件判断与选中分支放在一个原子操作中。先读取再无条件写入不能代替比较事务;事务也不会包含外部数据库或网络调用。卡 · 比较事务
- Watch 按修订顺序提供变化,应用仍要维护已处理进度,并在历史不可用时重新拉取快照。Watch 不是一次线性一致读。卡 · Watch 与客户端缓存
- Lease 将一组键与存活期限关联,过期或撤销后删除关联键。键被删除不等于旧工作进程已经停止,也不证明实例业务健康。卡 · Lease
- 默认读取要求线性一致性,
serializable读取可以使用成员本地状态。失去多数时,允许的本地读可能返回旧数据,写入和默认读则不能据此继续安全完成。卡 · Raft 与读取一致性 - compaction 清理的是历史版本,defragment 整理存储空间。历史压缩不是删除当前键,也不是替代备份。卡 · 历史压缩与恢复
- 选择注册中心时,需要比较一致性、租约、监听恢复与客户端缓存。不能只因为 etcd 和 Redis 都能存键值,就认为它们的注册语义相同。卡 · 综合题
2. 键值与协调状态
etcd 的键和值都是字节。应用可以用 /services/orders/instance-1 这样的键组织实例信息,但其中的 / 只是应用约定的前缀,不代表文件系统目录。
| 保存的内容 | 应用使用它做什么 | 仍由应用负责什么 |
|---|---|---|
| 服务实例地址与属性 | 发现可选实例 | 实际可达性、就绪状态、负载选择 |
| 配置值与修订信息 | 分发和观察配置变化 | 配置校验、应用生效、回滚规则 |
| 任务领取或协调记录 | 让执行者按条件竞争任务 | 任务执行、幂等、结果记录和失败恢复 |
例如,分布式定时任务可以在 etcd 中记录任务归属,并通过条件事务竞争领取资格。但真正执行脚本、更新业务数据库或发送请求的仍是工作进程。不能因为领取记录唯一,就声称任务对外部系统恰好执行一次。
etcd 适合这类需要协调的小规模关键元数据,不适合把大文件、任意海量日志或所有高频业务数据都搬进去。写入需要共识、持久化和复制;值越大、写入越频繁,协调系统承担的成本越高。
3. revision 与键版本
3.1 全局修订号与单键字段
revision 是 etcd 键值存储的逻辑修订号。修改键值的操作推进 revision,普通读取不会因为“读了一次”就产生新修订。它不是时间戳,也不等于 Raft 日志编号。
每个键还包含自己的版本信息:
| 字段 | 含义 |
|---|---|
create_revision | 这个键最近一次创建时的 revision |
mod_revision | 这个键最近一次修改时的 revision |
version | 当前这次存在期间的版本计数,创建为 1,后续修改递增 |
lease | 关联的租约 ID;没有关联时为 0 |
假设创建 /config/a 时全局 revision 为 10,它的 create_revision 和 mod_revision 都是 10,version 为 1。另一个键随后被修改,整体 revision 变成 11,/config/a 的字段并不会因此一起变化。
之后修改 /config/a,它的 mod_revision 变为这次操作的 revision,create_revision 保持不变,version 加一。删除后重新创建同名键,相当于新的存在阶段:create_revision 更新,version 从 1 开始。相同键名不代表还是同一份协调记录。
3.2 快照与历史读取
读取响应的 header 带有存储 revision,键本身的 mod_revision 则说明它最后何时变化。一个读取响应可以处于 revision 20,而返回的某个键仍在 revision 12 就已经写好,两者不矛盾。
下面使用 etcd 3.5.21 的 etcdctl 向本地实验集群写入并读取一个键:
export ETCDCTL_API=3
etcdctl --endpoints=http://127.0.0.1:2379 put /demo/name Alice
etcdctl --endpoints=http://127.0.0.1:2379 get /demo/name --write-out=json
第一条命令成功时返回 OK;第二条返回 JSON,其中键和值使用 Base64 表示,同时包含 header revision、create_revision、mod_revision 和 version。不要把示例环境中的某个 revision 数值写死为协议常量,它取决于此前发生的修改。
历史尚未被压缩时,可以按指定 revision 读取当时的键值状态。需要分页构建大一点的快照时,也应让各页使用同一个 revision,避免一页读到修改前、另一页读到修改后的混合状态。
4. 比较事务
比较事务先检查一组条件,再原子执行成功分支或失败分支。条件可以比较键的值、版本、创建修订或修改修订,而不是把判断和写入拆成两个独立请求。
下面约定 /demo/once 不存在时写入 first,已存在时读取原值:
etcdctl --endpoints=http://127.0.0.1:2379 txn <<'EOF'
version("/demo/once") = "0"
put /demo/once first
get /demo/once
EOF
首次执行时,条件成立,返回 SUCCESS 并写入;再次执行时,条件不成立,返回 FAILURE 并读取已有值。这里的 FAILURE 表示比较条件为假、执行了另一分支,不等于网络故障或整个请求执行失败。
多个客户端并发执行这个创建操作时,条件判断和写入由 etcd 原子处理,只有一个能从“不存在”进入创建成功。若客户端先 get,看到不存在后再普通 put,两个客户端就可能都执行写入,后一个覆盖前一个。
更新已有值时,可以比较之前读到的 mod_revision:只有键没有被其他人修改,才写入新值。相比只比较 value,修订条件还可以发现“值改过又改回来”的情况。若删除再创建,也不能把旧存在阶段的记录误当成未变化。
请求超时只说明客户端没有在期限内取得确认,不足以判断写入一定没有执行。恢复后应读取结果或用业务标识和比较条件核对,不能把超时直接当作安全重做全部外部操作的依据。
一次事务修改多个键时,这些修改使用同一个 revision。只有读取的分支不会因为执行了事务 API 就强行生成新的键值修订。事务的原子范围是 etcd 内的操作,业务数据库提交、外部 HTTP 调用等仍要通过幂等键和恢复流程协调。
5. Watch 与客户端缓存
5.1 快照与增量衔接
Watch 订阅一个键或范围内的变化事件,事件包含修改类型和对应的 revision。它适合用来更新本地配置或实例列表,但事件需要经过网络传输和客户端处理,不能把本地缓存当作与集群同步变化。
一个可恢复的缓存初始化流程是:
- 读取目标范围的完整快照,记下响应 header 的 revision
R。 - 从
R + 1开始 Watch 同一范围。 - 按事件 revision 更新缓存,并记录已经完整应用的进度。
如果在快照返回后、Watch 建立前发生了修改,只要对应历史仍在,指定起点的 Watch 就能补到这段变化。先读取后订阅“从现在开始”,却不携带起点,会留下漏事件的间隙。
Watch 的普通事件进度应依据已经应用的事件修订,不能随意把某个响应 header 的较新 revision 当作“所有对应变化都已处理”。专门的进度通知可以说明此前事件已经交付,但应用也要确保自己的处理已经完成。
5.2 顺序、断开与历史窗口
Watch 在保留历史范围内按 revision 交付事件。按默认不启用大修订分片的方式,同一事务在监听范围内产生的事件会作为完整修订交付;应用也应完整处理这一批,再推进持久进度。
连接断开后,从最后已经处理的 revision 之后重新订阅。若应用在处理完成前崩溃,只能从更早的持久进度恢复,仍可能重新处理以前见过的事件,因此业务消费逻辑应能识别重复。单条 Watch 的事件保证不等于应用对外部系统恰好执行一次。
Watch 不保证与任意另一个请求形成即时线性一致的观察。即使某次写入已经成功,监听客户端也可能尚未收到或应用事件。需要在当下读取权威状态时,应使用对应的一致性读取,或建立有依据的修订等待条件。
6. Lease
Lease(租约)为一组键提供有期限的存活关系。客户端申请租约,服务端返回租约 ID 和实际授予的 TTL;随后写键时关联该租约,并通过 KeepAlive 续约。
请求的 TTL 是期望值,应用应使用服务端返回的授予结果。租约过期或被撤销后,关联键被删除,监听者可以收到删除事件。一个租约可以关联多个键,一个键至多关联一个租约。
| 操作 | 改变什么 |
|---|---|
| Grant | 创建租约并返回 ID、授予的 TTL |
| Put 并指定 Lease | 把键与租约关联 |
| KeepAlive | 在有效条件下延长租约存活时间 |
| Revoke | 撤销租约,并删除关联键 |
| 到期处理 | 集群确认租约失效后删除关联键 |
续约和业务健康不同。实例的续约线程还能工作,不代表它的数据库连接池或业务线程池没有耗尽;反过来,续约中断也不代表进程已经停止。服务发现还应结合实际调用和就绪检查,见服务发现。
TTL 也不是外部观察者的精确定时器。网络延迟、集群状态、到期处理和 Watch 传递都会影响何时看到删除。应用不能因为本地经过了指定秒数,就推断所有观察者都已经看到租约失效。
如果用带租约的记录表示任务持有权,旧进程在长暂停后恢复时,可能仍以为自己拥有资格。即使 etcd 已删除旧记录并允许新进程领取,旧进程对外部资源的写入也不会自动被阻止。需要外部资源执行版本或 fencing 检查,原理见故障转移。
7. Raft 与读取一致性
7.1 多数派与提交
etcd 通过 Raft 复制状态变化,只有满足多数派提交等协议条件,才能将对应写入作为已完成结果。三节点集群通常需要两个投票节点配合;失去两个节点后,剩余节点不能独自继续确认正常写入。
这也是回答“etcd 会不会脑裂”时需要说明的范围:旧节点可能暂时仍认为自己是 Leader,但没有多数支持,不能按正常协议提交新的冲突写入。安全性来自共识规则,不是要求所有节点时时刻刻都对 Leader 身份有完全相同的看法。选举与提交的机制见 Raft。
成员处于同一个故障域、错误恢复操作或存储故障,仍需要单独处理。三个进程不等于三个独立的故障域,也不能用“用了 Raft”代替备份与恢复验证。
7.2 线性一致读与成员本地读
默认的 Range 读取要求线性一致性:如果一次写已经成功返回,随后开始的默认读不能返回比它更早的值。并发发生的读写则需要按它们在调用期间的有效顺序理解,不能理解为“响应到达客户端那一刻的所有最新变化都必须包含”。
客户端可以选择 serializable 读取,使用响应成员已经应用的本地状态,以减少对实时共识协调的依赖。这个模式允许相对集群已提交进度返回陈旧结果;接口中的名称不能被误读为“比默认读更严格”。
| 读取方式 | 需要的依据 | 适合怎样理解结果 |
|---|---|---|
| 默认线性一致读 | 确认安全读取位置,并等待本地应用进度达到要求 | 本次调用范围内的权威读取 |
serializable 成员本地读 | 当前成员已应用的本地状态 | 可能陈旧,需要业务允许 |
| 客户端缓存读 | 已拉取快照与已处理事件 | 陈旧程度还取决于连接、处理进度与缓存策略 |
有 Leader 也不代表只读它的内存就安全。旧 Leader 失联后可能已经失去资格;即便领导权有效,本地状态机也可能尚未应用到安全位置。对应原理见 Raft 的读取屏障。
在三个 etcd 3.5.21 成员的隔离实验中,先让所有成员保存 v1,再断开一个仍在运行的 Follower 与其他成员的网络连接。剩余两个成员提交 v2 后,隔离成员的 serializable 读仍返回 v1,默认读与写入在客户端期限内未获成功确认;重新连接后,默认读返回 v2。这里观察的是特定故障条件下的协议行为,不是吞吐或延迟保证。
8. 历史压缩与恢复
compaction 清理不再保留的历史修订,让历史读取和 Watch 的可恢复窗口有明确边界。当前仍存在的键会继续可读,不能把历史压缩理解为删除全部旧键。
如果 Watch 从已经被压缩的历史开始,服务会取消该监听并告知压缩信息。继续用同一个过期起点重试不会补回事件;应重新读取完整快照,获得新的 R,再从 R + 1 监听。
客户端重建期间,需要避免把未构建完成的缓存直接交给业务读取。可以先构建新的同步状态,再替换用于路由或配置读取的快照;是否允许暂时使用旧快照,要由业务的陈旧容忍策略决定。具体路由责任见服务发现。
defragment 整理成员的后端数据库文件,把可回收空间交还给文件系统。它和清理逻辑历史不同,还要考虑整理期间成员处理请求的影响。当前数据备份则用于恢复,不由 compaction 或 defragment 代替。面试中先解释这三者各自改变什么,无需把设备运维参数背成一张命令表。
9. 服务发现中的选型
etcd 可以用键前缀组织实例列表,用 Lease 表达续约关系,用事务保护条件更新,用 Watch 同步客户端缓存。这些能力能够组合成服务发现的基础,但调用方仍要决定实例选择、健康检查、陈旧缓存与连接排空。
与 Redis 比较时,应比较实际方案的契约。普通 Redis 主从采用异步复制,故障切换的写入确认与丢失边界不同;普通 Pub/Sub 通知也不能直接替代带修订历史恢复的 Watch。Redis 仍可被用于某种注册方案,但必须自行说明故障和恢复语义,而不能只列出 SET、TTL 和订阅命令就认为等价。
etcd 的共识写入有延迟与容量成本,不应因此让每个业务请求都同步访问注册中心。客户端可以使用有效的本地实例快照处理正常流量,同时持续同步变化和判断实际目标健康,机制见服务发现。Redis 复制与故障切换的具体边界见 Redis 高可用。
10 · 面试题卡片
本页题目与参考回答
10.1 etcd 可以怎样用于项目中的注册、配置或任务协调?
题型:综合题
考察重点:键值状态、事务、监听、租约与业务执行的分工。
参考回答
etcd 保存需要一致协调的元数据,再由应用使用这些状态。 注册场景可以用前缀保存实例,用 Lease 维护存活关系,用 Watch 更新缓存;任务场景可以用比较事务竞争领取记录。
任务执行、业务健康判断和外部数据库提交仍由应用负责。领取记录唯一不等于外部副作用恰好执行一次,失败后还需核对结果和幂等恢复。
10.2 revision、mod_revision 和 version 有什么区别?
考察重点:全局逻辑修订、单键修改与删除重建。
相关内容:revision 与键版本。
参考回答
revision 表示整个键值存储的修订进度,mod_revision 表示某个键最后修改的位置,version 统计这个键当前存在阶段的版本。 其他键被修改会推进全局 revision,但不会改变当前键的 mod_revision 和 version。
一次事务修改多个键,它们共用该事务的 revision。键删除后重建会得到新的 create_revision,version 从 1 开始;revision 也不能当作 Raft 日志编号或时间戳。
10.3 两个客户端都看到键不存在,怎样避免同时取得领取资格?
考察重点:条件判断与写入的原子性、比较失败和外部操作范围。
相关内容:比较事务。
参考回答
用一个比较事务检查键不存在,并在成功分支创建领取记录。 判断与写入由 etcd 原子执行,不能拆成 get 后普通 put。
比较失败会执行失败分支,不等于网络错误。更新已有记录时可以比较 mod_revision 防止覆盖别人的修改;这个事务只保护 etcd 内的状态,实际任务对外部系统的副作用仍需独立约束。
10.4 etcd Watch 怎样工作,客户端如何维护可恢复的缓存?
考察重点:修订顺序、快照与增量衔接、客户端消费进度。
相关内容:Watch 与客户端缓存。
参考回答
Watch 根据键或范围提供按 revision 排序的变化,客户端用快照加增量建立缓存。 先读取完整快照并取得 R,再从 R+1 监听,避免初始化间隙漏事件。
断开后从已完整处理的修订之后恢复,不能把尚未处理的响应当作完成进度。Watch 不是线性一致读,缓存可以落后;历史已压缩时重新拉取快照,不能无限重试过期起点。
10.5 etcd Lease 怎样关联键、续约和失效?
考察重点:TTL、关联键、续约与外部持有者的区别。
相关内容:Lease。
参考回答
客户端申请租约、给键关联 Lease,并通过 KeepAlive 续约;过期或 Revoke 后关联键被删除。 应使用服务端实际授予的 TTL,不能把本地经过若干秒等同于所有观察者都已收到删除。
键删除不证明旧工作进程停止,也不证明之前的续约代表业务健康。旧持有者恢复后仍可能写外部资源,必要时由资源端检查 fencing 或版本,拒绝过期持有者的写入。
10.6 etcd 如何保持数据一致,失去多数后还能读写吗?
考察重点:Raft 提交、旧 Leader、线性读取与陈旧本地读取。
相关内容:Raft 与读取一致性。
参考回答
etcd 通过 Raft 多数派提交等规则复制状态,失去多数后不能继续正常确认写入。 旧节点暂时自认为 Leader,也不能在没有多数的情况下安全提交新的冲突结果。
默认读需要确认安全读取位置及本地应用进度,缺少所需协调时也不能按成功线性读返回。显式选择 serializable 可以读取成员本地已有状态,但可能陈旧;客户端缓存还可能比这个状态更旧。
10.7 Watch 起点已经被 compact,为什么要重新拉取,compaction 会删除当前键吗?
考察重点:历史窗口、当前状态、重建与空间整理。
相关内容:历史压缩与恢复。
参考回答
旧历史已经不能提供完整增量,所以需要重新读快照,并从新修订之后监听。 反复使用同一个过期起点无法恢复已经清理的事件。
compaction 清理历史修订,不等于删除当前键。defragment 负责整理后端文件空间,备份负责恢复当前数据,这三者不能互相替代。
10.8 为什么项目选择 etcd 而不是 Redis 作为注册中心?
题型:综合题
考察重点:真实需求、一致性与发现缓存的恢复契约。
相关内容:服务发现中的选型、Watch 与客户端缓存、线性一致读与成员本地读。
参考回答
如果项目需要一致的元数据更新、比较事务、租约和带历史修订的监听,etcd 可以直接提供相应基础能力。 Redis 普通主从的异步复制和普通 Pub/Sub 的恢复语义不同,不能只凭键值、TTL 和订阅能力就认为等价。
选择仍要考虑容量、延迟、客户端生态和已有运维条件。etcd 也不能代替实例健康判断和路由缓存策略,通常不应让每次业务请求都同步访问注册中心。
11. 参考资料
- etcd 3.5:API
- etcd 3.5:API guarantees
- etcd 3.5:Maintenance
- etcd 3.5.21:KV、Watch 与 Lease 协议
- etcd 3.5.21:Watch 客户端
- etcd 3.5.21:Lease 客户端
- Redis:Pub/Sub 的交付语义
来源:Coding 101 原文 · 免费阅读,引用请保留出处。内容使用说明