bigywater.tech
Kubernetes · 10

CRD 版本转换:v1 与 v1beta 并存时数据怎么存

CRD 同时提供两个版本时,etcd 里到底存哪个?strategy: None 和 Webhook 有什么区别?转换服务挂了会怎样?这篇自己写了转换 webhook,按调用日志和 apiserver 的转换计数器,一项项看转换发生在写入、读取还是 list,再去 etcd 里确认存的版本。

spec={}
strategy: None 时,用 v1beta1 写入的 replicas=3 存进 etcd 之后剩下的内容
1 / 0
只写不读时 webhook 的调用次数 / v1beta1 list 6 个对象时的调用次数
["v1","v1beta1"]
把存储版本改到 v1beta1 之后的 status.storedVersions

Webhook 与 None

实测 · 本机执行输出
  [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 源码确认内部机制。

webhook 挂了

实测 · 本机执行输出
  用 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 里去掉。

分步演示

CRD 版本转换
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / kind(Docker 里的单节点或多节点真实集群,节点是 Docker Desktop 虚拟机里的容器)/ kubectl;用空的 kubeconfig 之外的独立 kubeconfig,不碰用户自己的集群;计时用 date +%s%N / kubectl 事件时间戳,数字每次运行会有小幅抖动。三节点 kind 集群 kx-main(Kubernetes v1.37.0);两个 CRD gadgets(conversion.strategy=Webhook)与 gizmos(strategy=None),都有 v1beta1(spec.replicas)和 v1(spec.size)两个版本;转换 webhook 是 python:3.12-alpine 里的标准库 HTTPS 服务(把 replicas 与 size 互相改名,每个转换请求打一行 JSON 日志);转换计数器取自 apiserver /metrics 的 apiserver_crd_conversion_webhook_duration_seconds_count;etcd 里的对象用 etcdctl get --print-value-only 读出;webhook 停掉用缩容到 0 副本并等 Pod 消失;每种情形各一次,实验后删除了 CRD
    • 没有验证:转换在 apiserver 内部具体怎么缓存(只验证了调用次数,没有读源码);storage version migrator 和 kube-storage-version-migrator 的迁移流程;Webhook 返回转换失败的错误格式;conversionReviewVersions 的协商;极大对象的批量转换;watch 请求在转换时的行为
    京ICP备2026067502号-1京公网安备11010502063487号