CRD 同时提供两个版本时,etcd 里到底存哪个?strategy: None 和 Webhook 有什么区别?转换服务挂了会怎样?这篇自己写了转换 webhook,按调用日志和 apiserver 的转换计数器,一项项看转换发生在写入、读取还是 list,再去 etcd 里确认存的版本。
[gadgets] 用 v1beta1 创建:gadget.k4.example.com/a1 created ← 调用 webhook 3 次(v1beta1→v1 共 1 个对象;v1→v1beta1 共 1 个对象;v1→v1beta1 共 1 个对象)
用 v1beta1 读:apiVersion=k4.example.com/v1beta1 spec={"replicas": 3} ← 调用 webhook 1 次(v1→v1beta1 共 1 个对象)
用 v1 读: apiVersion=k4.example.com/v1 spec={"size": 3} ← 没有调用 webhook
etcd 里存的: apiVersion=k4.example.com/v1 spec={"size": 3}
[gizmos] 用 v1beta1 创建:gizmo.k4.example.com/a1 created
用 v1beta1 读:apiVersion=k4.example.com/v1beta1 spec={}
用 v1 读: apiVersion=k4.example.com/v1 spec={}
etcd 里存的: apiVersion=k4.example.com/v1 spec={}两个 CRD 都是 v1beta1 用 replicas、v1 用 size(v1 是存储版本)。用 v1beta1 写 replicas: 3:有 webhook 的 gadgets,etcd 里存的是 apiVersion: v1、{"size": 3},两个版本读出来各是各的字段名;strategy: None 的 gizmos 只会把 apiVersion 改成 v1,replicas 不在 v1 的 schema 里被剪掉,etcd 里是 {},两个版本读出来都是空——数据凭空消失,没有任何报错。
None 只适合版本之间字段完全相同的情形。
用 v1(存储版本)创建 a2——没有任何读:webhook 被调用 1 次(v1→v1beta1);apiserver 的转换计数器增加 1
用 v1 读 a2(GET):webhook 被调用 0 次(无);apiserver 的转换计数器增加 0
用 v1beta1 读 a2(GET):webhook 被调用 1 次(v1→v1beta1);apiserver 的转换计数器增加 1
用 v1beta1 list 全部 6 个对象:webhook 被调用 0 次(无);apiserver 的转换计数器增加 0
那次 list 返回的内容:
NAME VERSION REPLICAS
a1 k4.example.com/v1beta1 3
a2 k4.example.com/v1beta1 2
a3 k4.example.com/v1beta1 3
a4 k4.example.com/v1beta1 4
a5 k4.example.com/v1beta1 5
a6 k4.example.com/v1beta1 6数一数 webhook 日志和 apiserver 的转换计数器:只用 v1(存储版本)创建一个对象、没有任何读,webhook 就被调用 1 次(v1→v1beta1);用 v1 读不调用;用 v1beta1 读单个对象(GET)调用 1 次;用 v1beta1 list 6 个对象调用 0 次,返回的却已经是 v1beta1 的形态。这几个数字说明:写入时就会为其他版本的视图做一次转换,GET 按需转换,list 读的是已经转换好的内容,所以 list 不会一次把所有对象批量交给 webhook。
这是根据调用次数做出的推断,没有读 apiserver 源码确认内部机制。
用 v1 读 a2:退出码 0;0.07 s;gadget.k4.example.com/a2 用 v1beta1 读 a2:退出码 1;0.05 s;Error from server (InternalError): Internal error occurred: StorageError: corrupt object, Code: 7, Key: /k4.example.com/gadgets/k4/a2, ResourceVersion: 0, AdditionalErrorMsg: object not decodable revision=3915: conversion webhook 用 v1 创建 c2(存储版本,不需要转换):退出码 0;0.05 s;gadget.k4.example.com/c2 created 用 v1beta1 创建 c3(要先转成 v1):退出码 1;0.05 s;Error from server: error when creating "STDIN": conversion webhook for k4.example.com/v1beta1, Kind=Gadget failed: Post "https://conv.k4.svc:443/convert?timeout=30s": dial tcp 10.96.251.24:443: connect: connection refused 用 v1 list:退出码 0;0.04 s;gadget.k4.example.com/a1 用 v1beta1 list:退出码 1;3.05 s;Error from server (StorageReadError): failed to read one or more gadgets.k4.example.com from the storage: StorageError: corrupt object, Code: 7, Key: /registry/k4.example.com/gadgets/k4/a1, ResourceVersion: 0, AdditionalErrorMsg: webhook 恢复后用 v1beta1 list:退出码 0;0.05 s;gadget.k4.example.com/a1
存储版本是 v1 时:用 v1 读、写、list 都照常;用 v1beta1 就全不行——读单个对象报 StorageError … conversion webhook … failed,创建报 conversion webhook for k4.example.com/v1beta1, Kind=Gadget failed … connection refused,list 先等了 3.05 秒再报 StorageReadError。webhook 恢复后立刻恢复。所以转换 webhook 的可用性只影响非存储版本的客户端——控制器如果用的是旧版本,它就会跟着一起坏。
gadgets 当前 status.storedVersions=["v1"]
把 storage 改到 v1beta1:storedVersions=["v1","v1beta1"]
旧对象 a2 在 etcd 里:apiVersion=k4.example.com/v1(还是旧版本,没人重写它)
新建 a7 在 etcd 里:apiVersion=k4.example.com/v1beta1 spec={"replicas": 7}
kubectl replace a2 重写之后:apiVersion=k4.example.com/v1beta1
想从 CRD 里删掉 v1 版本:status.storedVersions[0]: Invalid value: "v1": missing from spec.versions; v1 was previously a storage version, and must remain in spec.versions until a storage migration ensures no data remains persisted in v1 and removes v1 from status.storedVersions把存储版本改到 v1beta1,status.storedVersions 变成 ["v1","v1beta1"]:它记录 etcd 里可能还存着的所有版本。已有对象不会被改写(a2 在 etcd 里仍是 v1),新建的是 v1beta1,用 kubectl replace 重写过的才变成新版本。想把 v1 从 CRD 里删掉会被拒绝:必须先做存储迁移,确保 etcd 里没有 v1 的数据,并把 v1 从 storedVersions 里去掉。