第
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.go、quota.go、apply.go(quotaApplierV3)、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 只更新 ModRevision 与
Version。客户端 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 估算:
- Put:
256 + len(key) + len(value)(kvOverhead) - Txn:success/failure 分支中较大一侧的 cost 之和
- LeaseGrant:
leaseOverhead(64)
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,返回
ErrNoSpace(mvcc: database space exceeded)。applyEntryNormal
捕获首次 ErrNoSpace 且尚无 NOSPACE alarm
时:
- 异步 Propose
AlarmRequest{ACTIVATE, NOSPACE}; - 仍
Trigger客户端 wait,返回错误。
alarm 激活后(AlarmType_NOSPACE):
- 写 Put/Txn/LeaseGrant 在 RPC 层被挡;
- 读 Range、delete、compaction 仍可进行;
/health报告ALARM NOSPACE(可?exclude=NOSPACE忽略,见health.go)。
解除路径: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 对象失败时,错误链可能是:
ErrTooManyRequests— etcd apply 积压,常伴随 apiserver 超时;mvcc: database space exceeded— 元数据盘满,需 defrag / 扩容 quota / 清理事件;- Raft propose 超时 — 第 2–3 篇轴,不是本篇 quota 门。
resourceVersion 对应 MVCC mod
revision(对单个 object key 通常取
ModRevision);list/watch 的
resourceVersion=0 语义与读一致性见第 8、13
篇。
参考资料
规范 / 源码(A)
etcd-io/etcdtagv3.5.33:server/etcdserver/quota.go、apply.go(quotaApplierV3)、server/etcdserver/v3_server.goserver/mvcc/kvstore_txn.go(ModRevision 赋值)- etcd v3.5 · Maintenance / alarm
站内对照
- 第 6 篇 · Apply 管道
- distributed/50 · etcd(quota 段落)
争论 / 开放问题
- quota 用物理 Size 而非 SizeInUse 的运维含义(defrag 必要性)
- 2GiB 默认对大规模 K8s 是否足够(工程实践因集群而异,无统一公式)
下一篇:读路径与一致性
读完这篇,下一步读什么
优先读同系列或同问题的下一篇,把单篇消费变成主题集群。
【etcd】Compaction、Defrag 与容量:quota、alarm 与 SLO
钉 etcd v3.5.33 MVCC compaction 与 bbolt defrag 的分工、auto-compaction 保留窗口、quota-backend-bytes 与 NOSPACE alarm 写入门,以及 ErrCompacted 如何写进 Watch SLO。
【etcd】bbolt 后端:mmap、batch 与单写者约束
钉清 etcd v3.5.33 的 bbolt 后端:bucket 布局、mmap 预映射、100ms/10000 条 batch commit、ConcurrentReadTx 与单写者如何约束 MVCC 读写并发。
【etcd】treeIndex 与 Apply 管道:propose → commit → apply
走读 etcd v3.5.33 从 Raft commit 到 MVCC apply 的串行管道:treeIndex 与 bbolt 分工、consistent index、watchableStore 通知触发点,以及 committed/applied 分列排障。
【etcd】读路径与一致性:ReadIndex、Serializable 与 Lease read
分列 etcd v3.5.33 三种读语义:默认线性一致读的 ReadIndex 循环、Serializable 本地读、Raft ReadOnlyLeaseBased 与不经 Raft 的 Lease 路径;follower 读风险与 K8s 期望。