409 Conflict 是 Kubernetes 最常见的并发报错之一,它背后的 resourceVersion 到底是什么?这篇让两个写入者真的互相冲突一次,再对照 etcd 里的 revision,并用 resourceVersion 参数读过去和未来的状态。
写入者 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,所以不会因为别人改过而失败——它的风险是悄悄覆盖别人的修改(字段所有权下一篇说)。
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 表示任意版本都行,可以走缓存。