土法炼钢 · 系统与基础设施

【etcd】读路径与一致性:ReadIndex、Serializable 与 Lease read

文章导航

分类入口
distributedkubernetes
标签入口
#etcd#readindex#linearizable#serializable#lease#consistency#v3.5

目录

写入路径(第 7 篇)走 Raft;在 etcd 上刻意做了分流:默认 线性一致 读不走日志,而用 ReadIndex 确认 Leader 并等待 applied indexSerializable 读完全本地;Lease 相关 RPC(KeepAlive、TimeToLive)在 Leader 上不经 Raft 更新/查询 lessor 状态。

混用三种语义是排障「读到旧 resourceVersion」「锁 Lease 已过期但 TTL 仍显示正数」的第一分叉。

本篇在系列中的位置

篇目 核心内容
第 7 篇 · 写入路径 Txn、quota、alarm
第 8 篇 · 读路径与一致性 ReadIndex、Serializable、Lease read
第 9 篇 · Watch watchableStore、ErrCompacted
系列目录 关键问题、阅读路径

版本锚定:etcd v3.5.33(tag v3.5.33)。机制对齐 server/etcdserver/v3_server.goRangelinearizableReadLoop)、api/etcdserverpb/rpc.protoraft/raft.goReadOnlyOption)。无真实集群则不粘贴 follower 与 leader 读对比的伪造延迟表。


一、Range 默认:线性一致读

RangeRequest.serializable 默认为 false。非 Serializable 的 Range 在 EtcdServer.Range 中先调用 linearizableReadNotify,再 applyV3Base.Range 读本地 MVCC:

// server/etcdserver/v3_server.go @ v3.5.33(节选)
if !r.Serializable {
    err = s.linearizableReadNotify(ctx)
    // ...
}
get := func() { resp, err = s.applyV3Base.Range(ctx, nil, r) }

客户端默认即线性一致(etcdctl get key,不加 --consistency=s)。语义:读到的是已 apply 且不超过 ReadIndex 确认 commit 点的状态——不是「最新 commit 但未 apply」的数据。


二、ReadIndex 循环

Dedicated goroutine linearizableReadLoop 批量服务线性读:

sequenceDiagram
  participant req as Range or linearizable Txn
  participant loop as linearizableReadLoop
  participant raft as Raft ReadIndex
  participant apply as applyWait
  participant mvcc as MVCC Range
  req->>loop: readwaitc signal
  loop->>raft: sendReadIndex
  raft-->>loop: readState.Index equals commit index
  loop->>apply: Wait until applied ge index
  loop->>req: readNotifier notify
  req->>mvcc: Range local

步骤(v3_server.go):

  1. 合并多个并发读请求到同一 readNotifier
  2. requestCurrentIndexr.ReadIndexReadOnlySafe 路径,见下节);
  3. appliedIndex < confirmedIndex,阻塞于 applyWait.Wait(confirmedIndex)
  4. notify 唤醒所有等待的 Range/Txn 读。

边界情况:

这与 distributed/13 的 ReadIndex 工程补丁衔接;本篇钉 etcdserver 如何等待 apply,不重复 Raft 状态机全书。


三、Serializable 读与 follower 风险

RangeRequest.serializable = trueetcdctl get --consistency=s跳过 linearizableReadNotify,直接在本地 member 上 Range。proto 注释写明:延迟更低、吞吐更高,可能 stale——因 follower 的 applied index 可落后于 Leader

模式 网络 一致性 典型场景
默认 linearizable ReadIndex RTT + apply wait 线性一致 控制面强一致读、Txn compare
Serializable 无额外 Raft 往返 成员本地序 缓存预热、可容忍 lag 的统计

工程判断:K8s apiserver 对 etcd 的 Get/List 默认应走 linearizable(通过最新 RV);自行用 clientv3 WithSerializable() 接 follower 端点做读扩展,等于 apiserver 语义。

只读 Txn:isTxnSerializable 要求 success/failure 分支全部为 Serializable Range 才跳过线性读;否则仍 linearizableReadNotify


四、Lease read:两类「租约读」

「Lease read」在 etcd 语境下需分列,避免与 KV Lease 字段混淆:

4.1 Raft ReadOnlyLeaseBased(库能力,非 etcd 默认)

go.etcd.io/raft/v3 提供两种 ReadOnlyOptionraft/raft.go):

选项 机制 etcd v3.5.33
ReadOnlySafe(默认) ReadIndex + quorum heartbeat ack 使用raft.Config 未改 ReadOnlyOption
ReadOnlyLeaseBased 依赖 Leader lease,跳过 quorum ack 未启用;需 CheckQuorum

ReadOnlyLeaseBased 在时钟漂移无界时不安全(raft 注释);etcd 生产路径走 ReadOnlySafe。讨论「Lease read 优化」时须标明是 Raft 层可选模式,不是 v3.5.33 默认行为。

4.2 Lease RPC:不经 Raft 的 lessor 读/续租

LeaseKeepAliveLeaseRenew)与 LeaseTimeToLiveLeader 上直接访问 lessor,不写 Raft 日志:

Follower 收到请求会 forward 到 Leader(gRPC proxy 路径)。语义:TTL 以 Leader 上 lessor 为准,不是 MVCC key 的 ModRevision;Leader 切换时存在 Promote/Demote 窗口(第 10 篇)。

KeepAlive 延长 Raft log,但依赖 Leader 身份;分区 Follower 返回 ErrNotPrimary,客户端应切换 endpoint。


五、读路径与 treeIndex / bbolt

线性读等待 apply 之后,Range 与 Serializable 在 MVCC 层相同treeIndex.Revisions + bbolt UnsafeRange(第 5–6 篇)。ConcurrentReadTx 合并写缓冲,delete 立即 commit 规则保证不会线性读看到「已 delete 仍可见」的违反。

历史读:RangeRequest.revision > 0 读指定 MVCC rev;若 < compactMainRev 返回 ErrCompacted(Watch 与 apiserver relist 见第 9、13 篇)。


六、选型口令与开放问题

需求 推荐
控制面当前状态 默认 Range / linearizable Txn
follower 读扩展 Serializable + 接受 stale
分布式锁 / CAS linearizable + ModRevision compare
Lease TTL 监控 TimeToLive 对 Leader;注意切换窗口

开放问题(研究台账):


参考资料

规范 / 源码(A)

站内对照

论文 / 争论


上一篇写入路径深读

下一篇Watch 机制

读完这篇,下一步读什么

优先读同系列或同问题的下一篇,把单篇消费变成主题集群。


By .