想象公司大厅里有一块公告板。行政发通知、技术同步状态、部门协调任务,全都靠它。贴上去的每一张纸都盖着“第几号”的编号章,旧通知即使被替换也不会立刻撕掉,而是压在下面留档;有人会在板前“盯梢”,一有更新马上通知自己团队;有些便利贴写着“有效期至下午5点”,到点自动撕掉;谁要独占某个任务就贴上“我包了”;选值班组长时,谁第一个贴上“我是组长”谁当选。今天要讲的 etcd,就是分布式系统世界里那块公告板——一个分布式键值存储与协调服务。Kubernetes 用它保存集群状态,微服务用它做服务发现、配置下发、分布式锁和领导选举。它不难理解,只要抓住“公告板”这几个日常动作的逻辑,你就能掌握它的全部核心机制。

岗位关联

掌握 etcd 就掌握了分布式系统中“状态同步”与“协调”的底层逻辑,这是从后端开发走向分布式架构师的必经之路,也是看懂 Kubernetes 基础设施的关键钥匙。

目录
  1. 可视化演示
  2. 1. 数据模型与MVCC:编号不撕的公告板
  3. 2. 线性一致性读:先对表再回答
  4. 3. Watch变更监听:盯梢的人
  5. 4. Lease租约与TTL:便利贴的保质期
  6. 5. 分布式锁实现:贴上“我包了”
  7. 6. 领导选举机制:谁当值班组长
  8. 7. 典型应用实践:公告板的用武之地
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

通过Raft共识与日志复制、Watch变更监听推送、Lease租约与分布式锁三个模式,展示etcd分布式键值存储的核心协调机制。

本节配有交互动效(进入视口后自动加载,约 3KB)。

Raft共识与日志复制机制 Leader节点负责日志复制Follower确认后提交保证线性一致性 Raft将共识分解为领导者选举日志复制和安全性三个子问题 Leader -> AppendEntries -> Majority Ack -> Commit Raft共识保证etcd集群数据强一致性

1. 数据模型与MVCC:编号不撕的公告板

公告板上每一张通知都有一个醒目的编号。行政贴出第三号通知时,并不会把第一号和第二号撕掉重写,而是直接压在上面。这样一来,谁想查“上个月的规定”也能翻到旧纸。etcd 的数据模型也这样:所有数据以扁平的 key-value 形式存放,key 之间按字典序排列,就像一叠按编号装订的公告档案,天然支持范围查询和前缀遍历。

每次对 etcd 的写操作(Put 或 Delete),都会让全局计数器 revision 加一,并把新值连同这个版本号追加到该 key 的版本链末尾。旧版本不删除,这就是多版本并发控制(MVCC)。客户端读数据时可以指定版本号,实现“读历史快照”;Watch 机制也依赖版本号定位“从哪一号开始重放”。为了不让公告板堆成山,管理员会定期做压缩(compaction),丢弃某个版本之前的历史纸,但每个 key 至少保留最新一张。

想象你站在公告板前想查“第 50 号通知发布时值班表长什么样”,你只需要翻到第 50 号及之前最后贴出的那张纸。这就是 etcd 给分布式世界提供的“时空穿梭”能力:任何时刻的状态快照,都能按版本号精确还原。

Get(k,r)=max⁡revi≤r{⟨vi,revi⟩}\displaystyle \text{Get}(k, r) = \max_{\text{rev}_i \le r} \{ \langle v_i, \text{rev}_i \rangle \}
  1. 定位版本列表根据 key 找到它所有的历史版本记录,每个版本是 (revision, value) 的二元组。
    versions(k)=[⟨v1,r1⟩,⟨v2,r2⟩,… ]\displaystyle \text{versions}(k) = [\langle v_1, r_1 \rangle, \langle v_2, r_2 \rangle, \dots]
  2. 按版本号过滤只保留版本号不大于 r 的记录,排除掉“还没发生”的未来版本。
    candidates={⟨vi,ri⟩∣ri≤r}\displaystyle \text{candidates} = \{ \langle v_i, r_i \rangle \mid r_i \le r \}
  3. 取最新版本从候选中选择版本号最大的那一条作为读取结果。
    result=⟨vj,rj⟩,rj=max⁡ri\displaystyle \text{result} = \langle v_j, r_j \rangle, \quad r_j = \max r_i

2. 线性一致性读:先对表再回答

有人问你“今天下午放假吗”,你不能凭昨天或五分钟前的记忆回答,因为通知可能刚贴出来还没来得及看。正确的做法是:先跑到值班室确认“最新通知确实已经传达给所有楼层了”,再回来看公告板回答你。etcd 的线性一致性读就是这个逻辑:保证客户端读到的永远是“已经提交并被多数节点确认”的最新数据,绝不返回过期值。

etcd 基于 Raft 共识算法保证集群一致性:所有写请求必须经过 Leader,并复制到多数派节点才算提交成功。如果允许每个节点直接读本地副本,Follower 可能还落后几个版本,导致读到旧数据。为此 etcd 实现了 ReadIndex 机制:读请求先由 Leader 向多数派确认当前已提交的索引,等本地状态机应用到这个索引后,再返回本地读结果。这样既保证了线性一致性,又避免每次读都写一遍日志,性能比“读写全走 Raft 日志”高得多。

在这套机制下,即使你连接的是 Follower,得到的答案也与 Leader 完全一致。就像公告板各楼层都有一张贴了封条的复印件,只有确认“原件已盖章发布”后,复印件才会揭开封条给你看。对分布式系统来说,这种“绝不撒谎”的读比“读得快但偶尔过期”珍贵得多。

Q=⌊N2⌋+1\displaystyle Q = \left\lfloor \frac{N}{2} \right\rfloor + 1
  1. 多数派计算N 是集群节点总数,至少需要超过一半节点确认,才能保证没有两个“最新版本”并存。
    Q=⌊N2⌋+1\displaystyle Q = \left\lfloor \frac{N}{2} \right\rfloor + 1
  2. Leader 确认提交读请求携带当前任期信息发给多数派,确认后得到最新的提交索引。
    tread≥tcommit\displaystyle t_{\text{read}} \ge t_{\text{commit}}
  3. 应用后本地读等待本地状态机追到该索引,再从本地读取 value 返回给客户端。
    val=stateMachine(key)\displaystyle \text{val} = \text{stateMachine}(\text{key})

3. Watch变更监听:盯梢的人

总有几个“盯梢”的人站在公告板前,眼睛一眨不眨。一有新通知贴出来,他们立刻用传呼机告诉团队:“编号 52 出来了,内容如下!”etcd 的 Watch 机制就是这个盯梢者:客户端可以对一个 key 或一个前缀(如同一服务下的所有实例)发起监听,该 key 一旦发生写入或删除,etcd 会实时推送变更事件给客户端。

与轮询不同,Watch 是长连接推送,事件带着精确的 revision 编号,客户端可以指定从“某个历史版本”开始监听。这就是版本区间订阅:你可以说“从第 50 号开始,之后所有变化都推给我”。事件类型主要有 Put(写入/更新)和 Delete(删除)。更贴心的是断线恢复:客户端记住最后收到的 revision,重连后从这个位置继续 Watch,一条事件都不丢。

服务发现、配置热更新、分布式锁状态感知……全都依赖这个“盯梢者”。它让无数服务不再反复询问“有没有变化”,而是等通知上门。公告板前挤满了人,但每个人都安静而高效,只有新纸落下时才会忙碌起来。

Watch(k,r0)={⟨type,v,r⟩∣r>r0}\displaystyle \text{Watch}(k, r_0) = \{ \langle \text{type}, v, r \rangle \mid r > r_0 \}
  1. 记录起始版本客户端发起 Watch 时携带 r₀,表示“我只关心 r₀ 之后的变化”。
    r0=lastReceivedRev\displaystyle r_0 = \text{lastReceivedRev}
  2. 事件生成key 发生 Put 或 Delete 时,服务端生成带新版本号的事件对象。
    Put(k,v)⇒event=⟨Put,v,r0+1⟩\displaystyle \text{Put}(k, v) \Rightarrow \text{event} = \langle \text{Put}, v, r_0 + 1 \rangle
  3. 增量推送服务端把所有版本号大于 r₀ 的事件打包推送给客户端。
    events={e∣e.r>r0}\displaystyle \text{events} = \{ e \mid e.r > r_0 \}

4. Lease租约与TTL:便利贴的保质期

公告板上有一种便利贴,贴的时候主人用记号笔写了“本通知有效期至下午 5:00”。5 点一到,保洁阿姨准时把它撕掉,不需要任何人提醒。etcd 中的 Lease(租约) 就是这样一个小装置:它是一个独立对象,拥有唯一的租约 ID 和一个 TTL(生存时间)。

关键点在于:租约不是 per-key 的定时器,而是独立实体。你可以创建一个 TTL=30 秒的租约,然后把任意多个 key 都“绑定”到这个租约上。客户端必须周期性地发送 KeepAlive 心跳进行续期,每次心跳都会刷新“最后心跳时间”。一旦客户端宕机或网络中断,心跳停止,TTL 到期后这个租约绑定的所有 key 会被 etcd 自动删除。这完美模拟了“如果你不吱声,就当你死了”的分布式哲理。

想象一个值班表:每个值班小组领了一张“月卡”,每天打卡一次,月底没打卡就自动清退。多张便利贴(多个 key)可以共用同一张月卡(同一个租约),既减少了续期请求数量,又保证了“集体生效、集体失效”的一致性。Kubernetes 中 Pod 的心跳、分布式锁的自动释放,全靠这张月卡。

texpire=tlastKeepAlive+TTL\displaystyle t_{\text{expire}} = t_{\text{lastKeepAlive}} + \text{TTL}
  1. 创建租约调用 LeaseGrant 从服务端申请一个租约 ID 和 TTL 秒数。
    lease=⟨id,TTL⟩\displaystyle \text{lease} = \langle \text{id}, \text{TTL} \rangle
  2. 心跳续期客户端周期性调用 KeepAlive,服务端刷新该租约的最后心跳时间。
    tlastKeepAlive=now\displaystyle t_{\text{lastKeepAlive}} = \text{now}
  3. 过期回收当“当前时间”超过“最后心跳时间 + TTL”,租约判定过期,其绑定的 key 全部删除。
    now−tlastKeepAlive>TTL⇒清理绑定 key\displaystyle \text{now} - t_{\text{lastKeepAlive}} > \text{TTL} \Rightarrow \text{清理绑定 key}

5. 分布式锁实现:贴上“我包了”

仓库里有一台贵重的测试机,谁要用就在机器上贴一张“我包了,使用人:张三”,用完撕掉。张三没撕下来之前,李四看到标签就不去抢。etcd 的分布式锁就是这张“我包了”的标签:多个进程争抢同一把锁时,谁能成功创建那个锁 key,谁就获得锁。

实现方式非常优雅:利用 etcd “key 不存在时才写入成功”的原子性(基于 revision 判断)。客户端尝试 Put(lockKey, token),如果 key 已存在则写失败,表示锁被占用;写成功则代表加锁成功。锁的 value 是客户端生成的唯一 token,相当于使用人的签名。释放锁时不能盲目删除,必须比较 value 是否为我的 token,确认是本人才能 Delete——这就防止了“张三的锁被李四误删”的经典坑。此外锁可以绑定 Lease,客户端持锁期间持续续约,防止进程崩溃后锁永不释放。

回到那张“我包了”的标签:如果张三撕标签前先确认“标签上写的是不是我名字”,就不会误撕李四补的标签。这个看似简单的“先验明正身再动手”,正是 etcd 分布式锁在真实世界中可靠运行的关键。

acquire: Put(k,token) if ¬∃(k)\displaystyle \text{acquire: Put}(k, \text{token}) \ \text{if}\ \neg \exists(k)
release: Delete(k) if Get(k).value=token\displaystyle \text{release: Delete}(k) \ \text{if}\ \text{Get}(k).\text{value} = \text{token}
  1. 抢占锁利用“key 不存在才写入”的原子性,创建成功即获得锁,失败则说明被占。
    ∃k⇒失败;¬∃k⇒Put(k,token)\displaystyle \exists k \Rightarrow \text{失败}; \quad \neg \exists k \Rightarrow \text{Put}(k, \text{token})
  2. 身份标识token 是客户端生成的随机唯一字符串,相当于持锁者的指纹。
    token=randomUUID()\displaystyle \text{token} = \text{randomUUID}()
  3. 释放校验删除前先读取锁的 value,只有与自己的 token 匹配时才能删除。
    Get(k).value=token⇒Delete(k)\displaystyle \text{Get}(k).\text{value} = \text{token} \Rightarrow \text{Delete}(k)

6. 领导选举机制:谁当值班组长

公司每周一选值班组长:想当的人抢先到公告板贴上“我是组长:老王”。第一个贴上去的人当选,后来的人看到位置被占就放弃了。etcd 的领导选举就是这个流程:多个候选者竞争同一个选举 key,第一个成功创建该 key 的节点获胜,成为 leader。

胜出的判断标准是 CreateRevision(该 key 创建时的全局版本号):因为创建越早,revision 越小,所以 CreateRevision 最小的候选者就是 leader。当选后,leader 把自己的节点信息写入 key 的 value,并绑定 Lease 定期续约——这就是“在岗打卡”。一旦 leader 宕机,租约过期,etcd 自动删除选举 key,触发新一轮竞选,集群自动选出新 leader。这个机制天然防止脑裂:任何时刻最多只有一个节点能持有这把“组长”标签,因为 key 只有一个。

分布式数据库的“主从切换”、分布式任务调度器的“主节点选举”、AI 训练集群的“参数服务器选择”,底层都是这个“我先贴标签”的游戏。只要大家认同一块公告板,秩序就不会乱。

leader=arg⁡min⁡c∈candidatescreateRevision(c)\displaystyle \text{leader} = \arg\min_{c \in \text{candidates}} \text{createRevision}(c)
  1. 候选者竞选所有节点尝试在同一个 key 上执行写入,只有第一个真正成功。
    Campaign(k,id)=PutIfAbsent(k,id)\displaystyle \text{Campaign}(k, \text{id}) = \text{PutIfAbsent}(k, \text{id})
  2. 比较创建版本号每个成功创建的候选者都有唯一 createRevision,最小值即最先创建者。
    leader={c∣createRevision(c) 最小}\displaystyle \text{leader} = \{ c \mid \text{createRevision}(c) \text{ 最小} \}
  3. 任期续约与交接当选者绑定租约持续心跳,租约过期后 key 消失,触发重新选举。
    lease过期⇒Delete(k)⇒重新竞选\displaystyle \text{lease过期} \Rightarrow \text{Delete}(k) \Rightarrow \text{重新竞选}

7. 典型应用实践:公告板的用武之地

现在,这块公告板真正挂进了办公大楼。最著名的使用者是 Kubernetes:整个集群的状态,包括每个 Pod、Service、ConfigMap 的期望状态和实际状态,全部存放在 etcd 中。API Server 是唯一读写 etcd 的组件,其他组件通过 Watch 感知变化,这保证了 K8s 的“声明式”工作方式——你告诉它“我要 3 个副本”,它盯着 etcd 里的状态不断调整直到满足。

配置管理是另一个杀手级应用:把配置写入 etcd 的某个 key,所有服务通过 Watch 监听这个 key。配置一变,全集群毫秒级热更新,完全不用重启——取代了“改文件 + 重启 + 祈祷”的原始流程。服务发现则是这样运作的:每个服务实例启动时把自己的 IP 和端口写入 /services/订单服务/实例ID,并挂一个租约;调用方只需要读取或 Watch 这个目录前缀,就能拿到所有在线实例,实例挂掉后租约过期自动摘除。再加上分布式锁和领导选举,etcd 成为微服务时代名副其实的“协调中枢”。

从 Kubernetes 到微服务架构,etcd 就像大楼里那块没人注意却日夜运转的公告板:写的人随手一贴,看的人随时一瞥,听的人实时接收,到期自动清理。它会成为你架构工具箱里最可靠的那把通用钥匙。

addr=Get(prefix/svc/instance)\displaystyle \text{addr} = \text{Get}(\text{prefix}/\text{svc}/\text{instance})
  1. 服务注册实例启动时写 key 并绑定租约,表示“我上线了”。
    Put(prefix/i,ip:port,lease)\displaystyle \text{Put}(\text{prefix}/i, \text{ip:port}, \text{lease})
  2. 客户端发现调用方通过前缀 Range 读取全量实例,或 Watch 前缀获取增量变化。
    instances=GetPrefix(prefix)\displaystyle \text{instances} = \text{GetPrefix}(\text{prefix})
  3. 下线摘除正常退出调用 Delete,异常崩溃则等待租约过期自动清理。
    Delete(prefix/i) 或 租约过期清理\displaystyle \text{Delete}(\text{prefix}/i) \ \text{或} \ \text{租约过期清理}

AI 应用场景

1. 分布式 AI 训练的参数服务器选举

阶段:分布式训练协调

解决的问题:大规模分布式训练需要选出一个参数服务器(PS)负责梯度聚合。人工指定会形成单点故障,随机争抢会导致多个节点同时当 PS 造成冲突。用 etcd 领导选举,所有 worker 竞选同一个 key,先创建成功者成为 PS,PS 宕机后租约过期自动把位置让出来,30 秒内重新完成选举,训练不中断。

具体案例:某公司用 8 台 GPU 服务器训练千亿参数模型,每台启动时通过 etcd 竞选 ps-leader key,选出的 master 节点负责聚合梯度,其余 7 台作为 worker 向它传输梯度。某次 PS 机器崩溃,etcd 在两秒内感知租约过期,自动选出新 PS,训练任务自动恢复。

2. 模型特征配置中心

阶段:模型发布与在线推理配置

解决的问题:推荐模型上线后经常要调整特征开关、模型版本号、采样比例,传统做法是改配置文件再重启服务,既慢又打断请求。把开关和版本号写入 etcd,推理服务用 Watch 监听,一有变化立即热加载,全程零重启。

具体案例:某推荐系统的 200 个推理实例监听 etcd 中 /config/features 前缀。运营人员在后台修改特征权重,写入 etcd 后 200 个实例在 1 秒内全部感知并切换到新版特征组合,实验迭代周期从天级缩短到分钟级。

3. 多机推理实例的服务发现与路由

阶段:推理服务注册与路由

解决的问题:推理服务弹性扩缩容后,调用方不知道新实例的地址。实例启动时把自己的 IP:Port 写入 etcd 并挂租约,网关 Watch 前缀动态维护可用实例列表,实现自动负载均衡和无感知上下线。

具体案例:某语音识别服务有 30 个推理 Pod,Pod 启动时注册到 /speech-asr/ 目录,网关 Watch 该目录实现自动路由。高峰期扩容到 50 个 Pod,新增 Pod 的注册信息在秒级同步到网关;Pod 被销毁时租约过期,地址自动摘除,请求无缝切换。

Python 演示

python113 行
"""简易 etcd 核心机制模拟:MVCC 存储 + 分布式锁"""import timeimport threadingimport uuid  class MVCCStore:    """模拟 etcd 多版本并发控制(MVCC)的键值存储。"""     def __init__(self):        self._store = {}   # key -> [(rev, value), ...]        self._revision = 0        self._lock = threading.Lock()     def put(self, key, value):        with self._lock:            self._revision += 1            rev = self._revision            versions = self._store.setdefault(key, [])            versions.append((rev, value))            return rev     def delete(self, key):        with self._lock:            self._revision += 1            rev = self._revision            versions = self._store.setdefault(key, [])            versions.append((rev, None))            return rev     def get(self, key, revision=None):        with self._lock:            versions = self._store.get(key, [])            if not versions:                return None, None            if revision is None:                return versions[-1]            for rev, val in reversed(versions):                if rev <= revision:                    return rev, val            return None, None     def compact(self, keep_revision):        """压缩:丢弃小于等于 keep_revision 的历史版本(每个 key 至少保留最新)。"""        with self._lock:            for key in list(self._store.keys()):                versions = self._store[key]                kept = [v for v in versions if v[0] > keep_revision]                self._store[key] = kept if kept else [versions[-1]]  class DistributedLock:    """基于 etcd 的分布式锁(模拟版):创建 key 即抢锁,删除 key 即释放。"""     def __init__(self, store, key, ttl=10):        self._store = store        self._key = key        self._ttl = ttl        self._token = None        self._expire_at = 0     @staticmethod    def _expire_at_of(value):        try:            return float(value.rsplit(":", 1)[1])        except (ValueError, IndexError):            return 0     def acquire(self, timeout=5):        deadline = time.time() + timeout        while time.time() < deadline:            _, current = self._store.get(self._key)            if current is None or time.time() > self._expire_at_of(current):                token = str(uuid.uuid4())                expires = time.time() + self._ttl                self._store.put(self._key, token + ":" + str(expires))                self._token = token                self._expire_at = expires                return True            time.sleep(0.1)        return False     def release(self):        if self._token is None:            return False        _, current = self._store.get(self._key)        if current and current.startswith(self._token):            self._store.delete(self._key)            self._token = None            return True        return False  def main():    store = MVCCStore()    store.put("config/app", "v1")    store.put("config/app", "v2")    rev, val = store.get("config/app")    print("最新值:", val, "revision:", rev)    rev, val = store.get("config/app", 1)    print("读取 rev=1 的历史值:", val)     lock = DistributedLock(store, "/locks/db", ttl=10)    if lock.acquire(timeout=3):        print("服务器A 获取锁成功")        lock.release()        print("服务器A 释放锁")    else:        print("获取锁失败")  if __name__ == "__main__":    main()
目的:用 Python 从零模拟 etcd 的 MVCC 键值存储和分布式锁,让你直观看到 revision 版本链与“创建即抢锁、校验再释放”的核心逻辑。
  1. self._revision += 1模拟全局版本号,每次写入(put/delete)都自增一。
  2. versions.append((rev, value))不覆盖旧值而是追加新版本,这就是 MVCC 的本质。
  3. for rev, val in reversed(versions): if rev <= revision从新到旧找到第一个不超过指定版本的记录,实现历史快照读取。
  4. if current is None or time.time() > self._expire_at_of(current)锁没人持有或租约已过期时才能抢锁,等价于“贴标签前先看标签是否还在且未过期”。
  5. current.startswith(self._token)释放前校验 value 中的 token,防止误删别人的锁。

常见误区

误区etcd 支持 KV 读写,跟 MySQL 差不多,我把业务数据往里面存,反正它性能看起来不错。

正解etcd 是面向“小而关键”的分布式协调数据设计的,核心是 CP 系统(强一致、分区容忍),不是通用数据库。它把全部数据保存在内存索引中,官方建议数据量控制在几个 GB 以内,超大会导致性能骤降。正如公告板只贴重要通知,没人会把仓库账本全贴在大厅。

为何常见:etcd 名字带"database",API 又是简单的 KV 风格,很容易被误认为可以当通用存储使用。

误区客户端和 etcd 之间的监听连接断开了,中间产生的变更就丢掉了,重连后只能全量拉取一次数据来补救。

正解etcd 的每个事件都带全局 revision,客户端会记录最后收到的事件版本号。断线重连后,客户端带着这个版本号重新发起 Watch,etcd 会把该版本之后的所有事件重放给它,一条都不会少。就像盯梢的人被挤出去,重新回来后问“我看到第 50 号,后面还有吗”,对方从第 51 号继续念给他听。

为何常见:很多人把消息队列“离线消息可能丢失”的直觉套到 etcd 上,忽略了 revision 持久化这个设计。

误区我想给 key 设置过期时间,以为 etcd 跟 Redis 一样,每个 key 单独挂一个倒计时器。

正解etcd 中 Lease 是独立对象,一个租约可以绑定多个键值,客户端通过 KeepAlive 心跳续期。租约过期时,它绑定的所有 key 一起删除。可以理解为一张“7 月到期”的集体入场券,多人共用,票过期全员离场。这个设计大幅减少了续期请求的数量,也让“同生共死”变得简单。

为何常见:Redis 的 TTL 是 per-key 模式深入人心,先入为主地推断所有 KV 存储都这样实现。

误区只要成功创建了锁 key,我的业务代码执行多久都不担心别人抢到锁,锁会一直等我释放。

正解分布式锁通常绑定租约。如果业务执行时间超过 TTL 而进程又没有续约(比如发生长时间 GC 停顿或网络分区),锁会自动过期,其他客户端就会抢到锁,出现“两个客户端同时持锁”的情况。所以必须设置合理的 TTL、持续续约,并在业务设计上考虑“锁被抢走后”的兜底逻辑,比如版本号校验和幂等处理。

为何常见:本地编程中锁一旦获得就不会被系统夺走,把单机经验直接套到分布式场景是常见的思维惯性。

面试题

Q1. etcd 的 MVCC 机制本质是?

单选题 · easy

  • A.覆盖旧值仅保留当前
  • B.每次写入保留旧版本← 正确答案
  • C.写入前需要全体备份
  • D.写入时先删除旧版本

答案:B。MVCC 的核心是每次写操作追加新版本且保留旧版本,使得历史读取成为可能。A 是“只留最新”的普通覆盖写;C 是备份思维,与版本管理无关;D 是写前删除,都会丢失历史,均不符合 MVCC。

Q2. revision 在 etcd 中的含义是?

单选题 · easy

  • A.单个键的写入次数
  • B.全局单调递增版本号← 正确答案
  • C.节点的随机生成编号
  • D.服务器的启动次数

答案:B。revision 是 etcd 集群级别的全局计数器,每次写操作都会使其加一,所有 key 共享这同一个版本号。它记录了数据的全局时序,Watch 断点恢复和历史读取都依赖它,而不是单个 key 的计数。

Q3. 线性一致性读的核心保证是?

单选题 · mid

  • A.读到某副本的过期数据
  • B.读到已提交的最新数据← 正确答案
  • C.读到集群中所有数据
  • D.读到客户端缓存的数据

答案:B。线性一致性要求读到的数据是“已提交且已经生效”的最新值,不存在读到过期副本的情况。A 是典型的非一致性读结果;C 不是“读最新”;D 是本地缓存,可能陈旧。etcd 通过 ReadIndex 机制向多数派确认提交位置来保证这一点。

Q4. Watch 断线重连恢复的关键是?

单选题 · mid

  • A.从记录的最后 revision 继续← 正确答案
  • B.重新从头开始监听
  • C.直接放弃丢失的事件
  • D.等待客户端重新启动

答案:A。etcd 的事件都带全局 revision,客户端记录最后收到的版本号,重连后用这个版本号重新发起 Watch,etcd 会把之后的增量事件全部重放。从头监听代价高且不一定能指定起点;放弃事件会导致状态不一致,都不正确。

Q5. 关于 etcd 租约(Lease)正确的是?

多选题 · mid

  • A.租约是独立管理对象← 正确答案
  • B.多个键可以共享租约← 正确答案
  • C.租约过期自动删除绑定键← 正确答案
  • D.每个键必须独立设置 TTL

答案:ABC。租约是独立对象,提供 TTL 和心跳续期能力,一个租约可绑定多个 key,过期时所有绑定 key 被统一删除,因此 A、B、C 正确。D 错误:TTL 定义在租约上而非 key 上,一个租约下所有 key 共享同一 TTL。

Q6. etcd 分布式锁防误删的关键是?

单选题 · hard

  • A.锁值携带唯一持有者标识← 正确答案
  • B.锁的名称设置得足够长
  • C.锁定期自动被清理删除
  • D.锁使用随机生成的名称

答案:A。锁的 value 中存放客户端生成的唯一 token,删除前先比较 value 是否等于自己的 token,只有匹配才删除。这样即使锁被其他人持有,也不会被误删。锁名长短、随机命名、定期清理都无法解决“如何确认是我的锁”这个归属问题。

Q7. 基于 etcd 领导选举确定 leader 的机制是?

单选题 · hard

  • A.竞选 key 的创建 revision 最小← 正确答案
  • B.竞选 key 的 value 字典序最大
  • C.节点 ID 数值最小者优先获胜
  • D.节点启动时间最早者优先获胜

答案:A。多个候选者竞争同一个 key,创建成功者中 CreateRevision 最小的是第一个成功写入的节点,它就是 leader。value 内容、节点 ID、启动时间都和选举结果无关,关键是“谁先成功创建 key”这个分布式原子操作的结果。

Q8. 关于 etcd 线性一致性读优化正确的是?

多选题 · hard

  • A.读前需要向多数派确认提交位置← 正确答案
  • B.每次都写入日志获取最高提交索引
  • C.确认提交后可从本节点直接读取← 正确答案
  • D.从任何 Follower 读到数据都保证一致

答案:AC。ReadIndex 优化中,Leader 先向多数派确认最新提交索引,本地应用完成后从本地读取,避免每次读都写日志,因此 A、C 正确。B 说的是“每次写日志”的朴素方案,正是 ReadIndex 要避免的;D 中 Follower 未确认提交点,可能出现过期数据。

Q9. Kubernetes 用 etcd 主要保存?

单选题 · easy

  • A.容器镜像和调度日志
  • B.集群状态与元数据配置← 正确答案
  • C.用户的应用进程代码
  • D.Pod 产生的业务日志

答案:B。Kubernetes 把 Pod、Service、ConfigMap、Deployment 等所有对象的期望状态和实时状态都存放在 etcd,API Server 是唯一读写入口。镜像放在镜像仓库,进程代码在镜像里,业务日志输出到 stdout 或日志系统,三者都与 etcd 无关。

Q10. 下面哪些属于 etcd 协调服务的常见场景?

多选题 · mid

  • A.服务发现与配置下发← 正确答案
  • B.分布式锁与领导选举← 正确答案
  • C.海量日志的离线分析
  • D.大规模数据仓库存储

答案:AB。etcd 的典型场景包括服务注册发现、动态配置下发、分布式锁和领导选举,这些都需要强一致和实时监听。海量日志分析和数据仓库存储属于大数据领域,数据量大且对一致性要求低,etcd 的定位与它们不匹配。