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

【etcd】写入路径深读:Txn、mod revision 与 quota/alarm

文章导航

分类入口
distributedkubernetes
标签入口
#etcd#txn#mod-revision#quota#alarm#nospace#v3.5

目录

第 6 篇 把 committed entry 如何 apply 到 MVCC 讲清;写入路径从客户端视角补全:gRPC → Propose → quota 检查 → apply → ModRevision 回写,以及 database space exceeded / NOSPACE alarm 如何切断写流量而读与 delete 仍可继续。

本篇是 K8s 控制面「apiserver 写 etcd 失败」时的 MVCC/容量轴 入口。

本篇在系列中的位置

篇目 核心内容
第 6 篇 · Apply 管道 propose → commit → apply
第 7 篇 · 写入路径 Txn、mod revision、quota、alarm
第 8 篇 · 读路径与一致性 ReadIndex、Serializable
系列目录 关键问题、阅读路径

版本锚定:etcd v3.5.33(tag v3.5.33)。机制对齐 server/etcdserver/v3_server.goquota.goapply.goquotaApplierV3)、server/mvcc/kvstore_txn.go无真实集群则不粘贴伪造 quota 触发时的 etcdctl 输出。


一、Put 与 Txn:谁走 Raft

RPC Leader 路径 是否写 Raft 日志
Put / DeleteRange raftRequest → Propose
Txn(含 compare + success/failure 写) raftRequest
Txn(只读 compare + Range) linearizableReadNotify + 本地 applyV3Base.Txn
Compaction processInternalRaftRequestOnce

写 Txn 与 Put 共享 raftRequestOnce:分配 RequestHeader.ID,Propose InternalRaftRequest,阻塞至 apply 完成并 wait.Trigger 返回响应。只读 Txn 在 第 8 篇 与 Range 共用线性读或 Serializable 门。

flowchart LR
  grpc["gRPC Put or Txn write"] --> propose["Propose WAL"]
  propose --> commit["Raft commit"]
  commit --> quota["quotaApplierV3"]
  quota --> mvcc["MVCC Put or TxnWrite"]
  mvcc --> watch["watchableStore.notify"]
  mvcc --> resp["PutResponse or TxnResponse"]

二、ModRevision 与 Txn 内 sub revision

每次改变 MVCC 状态的 apply 在 storeTxnWrite.End() 递增 currentRev,记为本次 txn 的 main revision。txn 内第 \(i\) 个变更(从 0 计)写入 bbolt 时使用:

\[\text{idxRev} = (\text{main} = \text{beginRev}+1,\; \text{sub} = i)\]

ModRevision 字段取 main;同一 Txn 内多条 Put 共享同一 ModRevision,以 sub 区分物理行(kvstore_txn.go)。

CreateRevision 在 key 首次创建时固定为当时 main;后续 Put 只更新 ModRevisionVersion。客户端 compare 常用:

Compare 目标 典型用途
MOD / ModRevision CAS 更新
CREATE / CreateRevision 仅当 key 未变时写
VERSION 乐观并发
LEASE 绑定租约变更

Txn 在 apply 阶段原子执行 compare → success 或 failure 分支;分支内的多个写共享一个 main rev。Jepsen 与 strict-serializable 边界见 distributed/53 与本系列第 11 篇(不在此重述)。


三、proposal 背压:ErrTooManyRequests

Leader 在 Propose 前检查(v3_server.go processInternalRaftRequestOnce):

if ci > ai+maxGapBetweenApplyAndCommitIndex {
    return nil, ErrTooManyRequests
}

committed index 领先 applied index 过多,新 proposal 被拒绝——防止 apply 队列无限堆积。这与 quota 无关,属于 apply 滞后 症状:应查 backend batch、大事务、snapshot,而非先 defrag。


四、backend quota 机制

默认配额(quota.go):

常量 说明
DefaultQuotaBytes 2 GiB --quota-backend-bytes=0 时使用
MaxQuotaBytes 8 GiB 官方建议上限;更大可能降性能

配额度量的是 Backend().Size()——bbolt 文件物理分配字节,含未回收空洞;不是逻辑 key 个数。Cost 估算:

Available 判断:Size() + cost < maxBackendBytes。compare-only 或 delete 的 cost 为 0,delete 不释放 quota——须 compaction + defrag(第 12 篇)。

gRPC 层 quotaKVServer 在 Propose 再查一次 quota,不足则 activate NOSPACE alarm 并拒绝(api/v3rpc/quota.go)。


五、NOSPACE alarm 与只读模式

apply 路径上 quotaApplierV3 在 Put/Txn/LeaseGrant 成功后!Available,返回 ErrNoSpacemvcc: database space exceeded)。applyEntryNormal 捕获首次 ErrNoSpace 且尚无 NOSPACE alarm 时:

  1. 异步 Propose AlarmRequest{ACTIVATE, NOSPACE}
  2. Trigger 客户端 wait,返回错误。

alarm 激活后(AlarmType_NOSPACE):

解除路径:etcdctl alarm disarm + defrag 回收空间 + 确保 Size() 低于 quota——只 disarm 不 defrag 会再次触顶。

stateDiagram-v2
  [*] --> Normal
  Normal --> QuotaExceeded: Size plus cost ge quota
  QuotaExceeded --> AlarmNOSPACE: activate alarm
  AlarmNOSPACE --> Normal: disarm plus defrag
  AlarmNOSPACE --> AlarmNOSPACE: write rejected

六、与 K8s 控制面的衔接(指针)

apiserver 写 Pod/Node 对象失败时,错误链可能是:

  1. ErrTooManyRequests — etcd apply 积压,常伴随 apiserver 超时;
  2. mvcc: database space exceeded — 元数据盘满,需 defrag / 扩容 quota / 清理事件;
  3. Raft propose 超时 — 第 2–3 篇轴,不是本篇 quota 门。

resourceVersion 对应 MVCC mod revision(对单个 object key 通常取 ModRevision);list/watch 的 resourceVersion=0 语义与读一致性见第 8、13 篇。


参考资料

规范 / 源码(A)

站内对照

争论 / 开放问题


上一篇treeIndex 与 Apply 管道

下一篇读路径与一致性

读完这篇,下一步读什么

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


By .