bigywater.tech
Kubernetes · 12

resourceVersion:乐观并发和 409 冲突

409 Conflict 是 Kubernetes 最常见的并发报错之一,它背后的 resourceVersion 到底是什么?这篇让两个写入者真的互相冲突一次,再对照 etcd 里的 revision,并用 resourceVersion 参数读过去和未来的状态。

409 Conflict
带着旧的 resourceVersion=4859 写回时的结果:the object has been modified
4863 = 4863
对象的 resourceVersion 与 etcd 里该 key 的 mod_revision 完全相同
43.1 秒
读一个比当前大 100 万的 resourceVersion,apiserver 等了这么久才返回 Timeout

谁后写谁冲突

实测 · 本机执行输出
  写入者 A(带着 resourceVersion=4859)replace:退出码 0;configmap/cm1 replaced;现在 resourceVersion=4860
  写入者 B(带着同样的 resourceVersion=4859)replace:退出码 1;Error from server (Conflict): error when replacing "STDIN": Operation cannot be fulfilled on configmaps "cm1": the object has been modified; please apply your changes to the latest version and try again
  cm1 现在:data={"owner":"A"} resourceVersion=4860(B 的修改没有写进去)

A 和 B 同时读到 cm1(resourceVersion=4859),A 先 replace 成功,resourceVersion 变成 4860。B 再带着 4859 写回:Conflict … the object has been modified; please apply your changes to the latest version and try again,cm1 里留下的是 A 写的内容。这就是乐观并发:不加锁,提交时比对版本,版本不一致就让你重新读、重新改。

什么情况下不会冲突

实测 · 本机执行输出
  merge patch 里带着旧的 resourceVersion=4859:退出码 1;Error from server (Conflict): Operation cannot be fulfilled on configmaps "cm1": the object has been modified; please apply your changes to the latest version and try again
  merge patch 不带 resourceVersion:退出码 0;configmap/cm1 patched;data={"owner":"D"}
  replace 的 YAML 里去掉 resourceVersion:退出码 0;configmap/cm1 replaced;data={"owner":"E"}(ConfigMap 允许无条件更新)
  kubectl apply(它自己先读最新的再算 patch,不带 resourceVersion):退出码 0;没有冲突;data={"owner":"F"}

冲突只发生在请求里带了 resourceVersion 的时候。merge patch 里带旧版本会冲突,不带就直接覆盖(最后写入者获胜);replace 的 YAML 里去掉 resourceVersion,ConfigMap 允许这样无条件更新;kubectl apply 自己先读最新对象再算 patch,不带 resourceVersion,所以不会因为别人改过而失败——它的风险是悄悄覆盖别人的修改(字段所有权下一篇说)。

resourceVersion 是什么

实测 · 本机执行输出
  cm1:metadata.resourceVersion=4863;etcd 里这个 key 的 mod_revision=4863,create_revision=4859(创建时的 resourceVersion 是 4859)
  打标签 x=1:4863 → 4864;再打一次一模一样的 x=1:configmap/cm1 not labeled,resourceVersion 4864 → 4864(没有变化)
  改成 x=2:4864 → 4865
  之后创建 cm2、cm3:resourceVersion 分别是 4866、4867——所有对象共用 etcd 的同一个递增序列,数字大小可以比较先后(但官方说它是不透明的字符串)

它就是 etcd 里这个 key 最后一次修改的 revision:cm1 的 resourceVersion 是 4863,etcd 里同一个 key 的 mod_revision 也是 4863,create_revision 4859 就是创建时的版本。所有对象共用 etcd 的同一个递增序列(创建 cm2、cm3 得到 4866、4867)。没有实际改动的更新(重复打同一个标签)不会写 etcd,版本不变。官方把它定义为不透明字符串,不要自己解析或做算术,这里只是观察。

按版本读

实测 · 本机执行输出
  list?resourceVersion=4859&resourceVersionMatch=Exact(cm1 创建时的版本):cm1 的 data={'owner': 'nobody'}(现在是 {"owner":"F"});list 的 resourceVersion=4859
  resourceVersion=1(早已被压缩掉)Exact:Error from server (Expired): The resourceVersion for the provided list is too old.
  list?resourceVersion=1004868(当前是 4868):43.1 秒后返回;Error from server (Timeout): Timeout: Too large resource version: 1004868, current: 4936
  list?resourceVersion=0(任意版本,可以走缓存):items=4 个

resourceVersionMatch=Exact 配合创建时的版本,读回了 cm1 创建时的内容(owner: nobody),相当于读到过去;版本太旧(etcd 已压缩)返回 Expired;读一个比当前大得多的版本,apiserver 会等它出现,这次等了 43.1 秒才返回 Timeout: Too large resource version;resourceVersion=0 表示任意版本都行,可以走缓存。

分步演示

resourceVersion
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / kind(Docker 里的单节点或多节点真实集群,节点是 Docker Desktop 虚拟机里的容器)/ kubectl;用空的 kubeconfig 之外的独立 kubeconfig,不碰用户自己的集群;计时用 date +%s%N / kubectl 事件时间戳,数字每次运行会有小幅抖动。三节点 kind 集群 kx-main(Kubernetes v1.37.0);所有对象都是 k6 命名空间里的 ConfigMap;两个写入者用同一份 kubectl get -o yaml 的结果模拟,A、B 各自 kubectl replace;mod_revision 与 create_revision 用 etcd 静态 Pod 里的 etcdctl get -w json 读出;按版本读用 kubectl get --raw 加 resourceVersion / resourceVersionMatch 参数;每种情形各一次,版本号每次运行不同
    • 没有验证:Pod 等不允许无条件更新的资源(缺 resourceVersion 时的行为);watch 时的 resourceVersion 语义(下一篇);etcd 压缩的具体周期;future 版本等待 43.1 秒的具体来源(只观察到结果);分页 list(limit/continue)时 resourceVersion 的一致性;客户端 retry.RetryOnConflict 的写法
    京ICP备2026067502号-1京公网安备11010502063487号