Kubernetes 基础体系 · 第 63/83 篇。示例基于 Kubernetes 当前稳定 API;弃用、版本偏差、云厂商差异和生产风险会明确说明。

Kubernetes 集群升级:Version Skew、API 弃用、节点灰度和回滚

Kubernetes 升级不是简单地把所有节点上的二进制文件替换成新版本。一次升级同时改变了三类状态:

  1. 组件版本状态:API Server、Controller Manager、Scheduler、kubelet、kube-proxy、kubectl 以及云厂商控制面可能处于不同版本。
  2. API 契约状态:某些 API 可能仍能读取但已经弃用,随后在更高版本中被移除。
  3. 持久化数据状态:etcd 中保存的是对象及其 API 版本信息。升级后,旧对象是否能继续被读取、转换和更新,取决于 API Server、CRD 转换和存储版本。

因此,安全升级的目标不是“版本号全部一致”,而是验证以下条件同时成立:

升级可行=Version Skew 合法API 使用可迁移数据可恢复业务可承受节点变更\text{升级可行} = \text{Version Skew 合法} \land \text{API 使用可迁移} \land \text{数据可恢复} \land \text{业务可承受节点变更}

下文以从版本 NN 升级到 N+1N+1 为例。具体可升级路径、支持窗口和参数必须以目标 Kubernetes 版本的官方文档及云厂商文档为准。


一、先区分 Kubernetes 中的几个“版本”

1. Kubernetes 版本不是一个单独的版本

集群中至少存在以下版本:

  • kube-apiserver:提供 Kubernetes API,并负责认证、授权、准入、对象持久化和 watch。
  • kube-controller-manager:运行各种控制器,例如 Deployment、Node、Job 和 EndpointSlice 控制器。
  • kube-scheduler:为未绑定节点的 Pod 选择节点。
  • kubelet:运行在每个节点上,负责把 PodSpec 转换为容器运行时操作。
  • kube-proxy:通常负责 Service 流量规则;具体实现也可能由云厂商网络组件替代。
  • kubectl:客户端工具,不是集群组件。
  • 扩展组件:CNI、CSI、Ingress Controller、监控组件、Admission Webhook、Operator 和 CRD。

kubectl version 的结果不能代表所有组件版本。例如:

kubectl version

可能输出:

Client Version: v1.31.2
Server Version: v1.30.6

这只能说明客户端和 API Server 的版本,不能说明每个节点的 kubelet 版本。应分别检查:

kubectl get nodes \
  -o custom-columns=NAME:.metadata.name,KUBELET:.status.nodeInfo.kubeletVersion,OS:.status.nodeInfo.osImage,RUNTIME:.status.nodeInfo.containerRuntimeVersion

kubectl get --raw='/version'

预期结果类似:

NAME       KUBELET    OS                         RUNTIME
worker-a   v1.30.6    Ubuntu 24.04.1 LTS         containerd://1.7.x
worker-b   v1.30.6    Ubuntu 24.04.1 LTS         containerd://1.7.x

这里的 status.nodeInfo.kubeletVersion 是 kubelet 向 API Server 报告的版本;它不一定等于节点上所有 Kubernetes 相关软件包的版本。

2. 版本偏差 Version Skew 是什么

Version Skew 指同一个集群内不同 Kubernetes 组件之间存在版本差异。版本偏差本身并不一定是错误,因为 Kubernetes 的滚动升级依赖短时间的混合版本状态。

但“能够通信”不等于“受到官方支持”。必须同时满足目标版本对应的 Version Skew Policy。

常见规则可以概括为:

组件关系 通常要求
高可用集群中的多个 kube-apiserver 不应跨越超过一个 minor 版本
kube-controller-manager、kube-scheduler 通常不应比 kube-apiserver 更新,升级过程中允许有限的旧版本偏差
kubelet 通常不应比 kube-apiserver 更新;可在官方策略允许的范围内落后
kube-proxy 通常遵循与 kubelet 类似的偏差约束,但实现和支持范围需核对目标版本
kubectl 官方支持范围通常为 API Server 前后一个 minor 版本

这里的“通常”很重要:精确的最大偏差、补丁版本要求和特殊例外属于版本敏感规则,不能用一张永久不变的表替代目标版本的官方 Version Skew Policy。

3. 为什么 kubelet 不能随意先升级到更高版本

API Server 是整个控制面的 API 契约中心。若 kubelet 比 API Server 新,可能出现以下问题:

  1. 新 kubelet 发送旧 API Server 不认识的字段或行为。
  2. 新 kubelet 依赖 API Server 尚未提供的资源或语义。
  3. 节点状态、Lease、Pod status 的字段解释出现差异。
  4. 新版本默认行为改变,但旧控制器仍按旧语义作出决策。

所以常见的安全顺序是:

旧 kubelet
    ↓
升级 API Server / controller-manager / scheduler
    ↓
验证控制面稳定
    ↓
逐批升级 kubelet 和节点组件

这不是因为新 kubelet 永远不能连接旧 API Server,而是因为官方支持通常要求 kubelet 不得领先于 API Server。


二、升级顺序:先改变控制面契约,再改变节点执行环境

一次典型的自建集群升级路径如下:

flowchart TD
    A[冻结变更并确认目标版本] --> B[扫描弃用 API]
    B --> C[验证扩展组件与 CRD]
    C --> D[备份 etcd 并演练恢复]
    D --> E[升级第一个控制面实例]
    E --> F[升级其余控制面实例]
    F --> G[验证 API Server、控制器和调度器]
    G --> H[灰度升级一个节点池或一个节点]
    H --> I[验证业务、网络、存储和监控]
    I --> J{验证通过?}
    J -- 是 --> K[继续分批升级节点]
    J -- 否 --> L[停止扩散并恢复节点]
    K --> M[升级后清理与复盘]

在高可用控制面中,不应同时停止所有 API Server。控制面升级的实际操作取决于部署方式:

  • kubeadm 管理的集群有明确的升级流程和版本检查。
  • 托管 Kubernetes 的控制面由云厂商升级,用户通常只能选择版本、维护窗口和节点池策略。
  • 静态 Pod、systemd、二进制安装或发行版管理的集群,其控制面替换方式不同。

不要把 kubeadm 命令直接套到托管集群或自研发行版上。命令形式相似,不代表控制面生命周期相同。

1. 升级前检查控制面健康

kubectl get --raw='/readyz?verbose'
kubectl get nodes
kubectl get pods -A
kubectl get events -A --sort-by=.lastTimestamp | tail -n 100

/readyz?verbose 用来检查 API Server 的就绪检查。输出中应看到各项检查通过,例如:

[+]ping ok
[+]log ok
[+]etcd ok
[+]informer-sync ok
readyz check passed

如果 etcdinformer-sync 或认证相关检查已经失败,此时升级会把既有故障扩大为版本变更故障。

还要检查:

kubectl get --raw='/livez?verbose'
kubectl get nodes -o wide
kubectl get pdb -A

Ready 节点数量、不可调度节点、PodDisruptionBudget 和正在进行的发布都会直接影响后续排空。

2. 高可用控制面的并发限制

假设有三个 API Server:

api-1: N
api-2: N
api-3: N

升级期间可暂时变为:

api-1: N+1
api-2: N
api-3: N

但不应直接变成:

api-1: N+1
api-2: N+1
api-3: N-1

因为 API Server 之间的版本偏差可能超过支持范围,而且不同实例处理 discovery、watch、准入和对象读写时可能有不一致行为。升级一个实例后,应确认它能够:

  • 通过负载均衡接收请求;
  • 正常访问 etcd;
  • 完成认证、授权和准入;
  • 正常提供 discovery;
  • 支持控制器和客户端建立 watch。

三、API 弃用不是“看到 warning 再改 YAML”

1. API Group、Version 和 Kind 的关系

一个 Kubernetes 对象通常由以下信息确定:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web

这里:

  • apps 是 API Group;
  • v1 是 API Version;
  • Deployment 是 Kind。

客户端操作时还存在资源名,例如:

kubectl get deployments.apps

API 弃用通常表示某个 Group/Version 不再推荐使用;API 移除则表示 API Server 不再提供该版本的读写接口。

例如,当前稳定 API 中应使用:

apiVersion: networking.k8s.io/v1
kind: Ingress

而不是已经移除的旧版本:

apiVersion: extensions/v1beta1
kind: Ingress

apiVersion 机械替换为新值并不总是正确,因为字段结构和语义也可能变化。Ingress 从旧版本迁移到 networking.k8s.io/v1 时,后端结构、路径匹配和 pathType 都需要按目标版本规范调整。

2. 弃用 API 的风险路径

一个弃用 API 可能经历如下过程:

仍可用
  ↓
标记为 deprecated,调用产生 warning
  ↓
新版本不再提供该 API
  ↓
旧 YAML 无法创建或更新
  ↓
旧对象可能仍存在,但无法通过旧接口管理

最容易误判的是:“集群里现在已经有旧对象,所以升级一定没问题。”实际上对象可能在 etcd 中继续存在,但升级后的 API Server 不再暴露读取或更新它的旧版本接口。

还存在第二个问题:即使资源已经迁移到新 API,发布系统、Operator、Admission Webhook 或备份工具仍可能使用旧 API。升级检查必须覆盖运行时调用者,而不只是 Git 仓库中的 YAML。

3. 发现正在使用的弃用 API

可以先检查 API Server 的弃用指标:

kubectl get --raw='/metrics' \
  | grep 'apiserver_requested_deprecated_apis'

指标中通常会包含 group、version、resource 等标签。它反映的是 API Server 观测到的请求,不是整个历史周期的完整审计记录。因此至少应覆盖:

  • 正常工作时间;
  • 定时任务执行时间;
  • 发布窗口;
  • 备份和扫描窗口;
  • Operator 的 reconcile 周期。

如果集群启用了审计日志,还应按 group/version/resource 搜索审计事件。仅执行一次 kubectl get 不能证明没有弃用调用。

检查集群提供的 API:

kubectl api-versions
kubectl api-resources

kubectl api-versions 展示当前 API Server 提供的 Group/Version;它不能告诉你哪些版本即将在下次升级中移除。弃用和移除列表必须与目标版本发行说明、弃用策略和 API 文档交叉核对。

4. 迁移清单和运行时调用

例如旧的 Deployment 清单:

apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
      - name: web
        image: nginx:1.27

迁移为稳定 API:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
      - name: web
        image: nginx:1.27

apps/v1 要求 Deployment 的 .spec.selector 明确存在,并且必须与 Pod Template 的标签匹配。下面这个清单虽然 apiVersion 正确,但会被拒绝:

spec:
  selector:
    matchLabels:
      app: frontend
  template:
    metadata:
      labels:
        app: backend

原因是 Deployment 控制器无法根据 selector 正确管理预期 Pod。API 迁移必须验证字段约束和语义,而不是只验证 YAML 能否解析。

在应用前进行客户端校验:

kubectl apply --dry-run=server -f deployment.yaml

预期结果是:

deployment.apps/web configured (server dry run)

如果目标集群仍未升级到目标版本,这个检查只能验证当前 API Server 的能力,不能替代目标版本测试。因此应在隔离测试集群上使用目标版本进行同样的校验。

5. CRD 的特殊风险:served、storage 和 storedVersions

自定义资源定义 CRD 至少涉及三个概念:

  • served: true:API Server 是否提供该版本接口;
  • storage: true:新写入对象在 etcd 中使用哪个版本作为存储版本;
  • status.storedVersions:该 CRD 对象曾经以哪些版本存储过。

一个 CRD 可以暂时同时提供两个版本:

spec:
  versions:
  - name: v1beta1
    served: true
    storage: false
  - name: v1
    served: true
    storage: true

这并不自动完成所有对象的迁移。若需要停止提供 v1beta1,必须确认:

  1. 控制器和客户端已经改用 v1
  2. 转换 webhook 能处理所有旧对象;
  3. 现有对象已经被读取并重新写入,使其完成存储迁移;
  4. status.storedVersions 不再保留不允许移除的旧版本;
  5. 备份工具和恢复工具支持目标版本。

CRD 的版本转换不是简单字段重命名。若 v1beta1v1 的结构不同,必须提供正确的 conversion webhook,并测试双向转换是否丢失数据。特别要验证默认值、枚举、列表顺序、未知字段和状态字段。


四、节点灰度:Drain 改变的是调度状态,不是“立刻杀掉节点”

1. 节点升级的基本状态变化

节点灰度通常包含四步:

kubectl cordon worker-a
kubectl drain worker-a --ignore-daemonsets --delete-emptydir-data
# 在节点上升级 kubelet、kube-proxy 和容器运行时
kubectl uncordon worker-a

每一步的含义不同:

  • cordon 将节点标记为不可调度,阻止新的普通 Pod 被调度到该节点;
  • drain 通过 Eviction API 或删除操作驱逐现有 Pod;
  • 节点软件升级改变 kubelet、容器运行时或网络代理;
  • uncordon 恢复调度资格。

cordon 不会迁移已有 Pod,drain 也不保证所有 Pod 都能立即退出。

2. 为什么 drain 可能失败

下面的命令可能失败:

kubectl drain worker-a \
  --ignore-daemonsets \
  --delete-emptydir-data

常见失败原因包括:

  • Pod 被 local 数据或 emptyDir 保护;
  • Pod 没有被控制器管理;
  • PDB 不允许同时驱逐足够多的副本;
  • Pod 使用本地卷;
  • 容器长期不响应终止;
  • 目标节点上的 DaemonSet Pod 被刻意保留;
  • 节点处于 NotReady,Eviction 流程无法正常完成。

--force--grace-period=0--disable-eviction 等参数会改变安全边界,不能作为“命令失败后的固定补救”。例如绕过 PDB 可能导致服务副本低于可用阈值,立即产生容量或可用性事故。

先检查:

kubectl get pods -A -o wide --field-selector spec.nodeName=worker-a
kubectl get pdb -A
kubectl describe node worker-a

如果某个 PDB 为:

NAME       MIN AVAILABLE   ALLOWED DISRUPTIONS
web-pdb    2               0

而当前只有两个可用副本,则驱逐任何一个副本都会违反 minAvailable: 2。此时增加业务副本、临时调整 PDB 或延后升级,通常比强制删除更可控。

3. 灰度批次不是固定数字,而是容量约束

假设某服务需要至少 AA 个可用副本,当前有 RR 个副本,批次内最多同时不可用 BB 个,则至少需要满足:

RBAR - B \ge A

如果节点升级会同时影响 BB 个服务副本,那么节点批次还必须受拓扑分布约束。例如:

  • 服务有 6 个副本;
  • PDB 要求至少 5 个可用;
  • 则最多只能让 1 个副本因升级暂时不可用;
  • 如果两个副本恰好集中在同一节点,升级该节点就可能违反 PDB。

因此节点灰度至少要观察:

kubectl get pods -A -o wide
kubectl get deploy,statefulset -A
kubectl get nodes
kubectl get events -A --sort-by=.lastTimestamp

升级一个节点后,应验证:

  • 节点重新 Ready
  • kubelet 版本符合预期;
  • Pod 能创建、终止和重启;
  • Service、DNS、Ingress 和网络策略正常;
  • CSI 能挂载和卸载卷;
  • 关键工作负载的可用副本和延迟恢复正常。

示例:

kubectl get node worker-a \
  -o jsonpath='{.status.nodeInfo.kubeletVersion}{"\n"}'

kubectl wait --for=condition=Ready node/worker-a --timeout=5m

kubectl wait 成功只证明节点 Ready,不证明应用已经恢复。应用还应通过就绪探针、业务健康检查、错误率和依赖访问进行验证。

4. StatefulSet 和本地存储的额外边界

无状态 Deployment 通常可以依赖副本和 PDB 完成迁移,但 StatefulSet 可能绑定:

  • PersistentVolume;
  • 节点本地盘;
  • 固定网络身份;
  • 单实例数据库;
  • 严格的启动和停止顺序。

对这类工作负载,drain 成功不等于数据服务安全。必须确认卷的拓扑约束、存储插件版本和重新挂载行为。升级节点前,先在同类节点和同类卷上验证“卸载—迁移—重新挂载—应用恢复”完整路径。


五、etcd 备份:回滚的基础,但不是普通升级按钮

1. 为什么必须备份 etcd

etcd 保存 Kubernetes 的控制面状态,包括:

  • API 对象;
  • Secret;
  • ConfigMap;
  • Service 和 EndpointSlice;
  • CRD 和自定义资源;
  • RBAC;
  • Lease;
  • 部分集群配置。

节点磁盘快照不能等价替代 etcd 备份。一个节点故障恢复可能修复该节点,但不能恢复已经被错误 API 迁移、误删或批量更新的集群对象。

使用 etcdctl snapshot save 前,必须明确:

  • etcd 访问地址;
  • 客户端证书、私钥和 CA;
  • etcdctl 与 etcd 的兼容要求;
  • 快照文件的完整性;
  • 快照保存位置和权限。

示例:

export ETCDCTL_API=3

etcdctl \
  --endpoints=https://127.0.0.1:2379 \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/peer.crt \
  --key=/etc/kubernetes/pki/etcd/peer.key \
  snapshot save /backup/etcd-$(date +%Y%m%d%H%M%S).db

证书路径仅适用于某些 kubeadm 或自建布局,不能直接假设适用于云厂商控制面。

验证快照:

etcdutl snapshot status /backup/etcd-20250101010000.db

不同 Kubernetes/etcd 版本中使用的工具名称和参数可能不同,应以随 etcd 提供的工具为准。验证至少应确认快照可读取、文件大小合理、哈希或状态信息可记录。

2. Snapshot 恢复意味着什么

etcd snapshot 恢复通常是灾难恢复操作,而不是升级失败后的“撤销按钮”。假设快照时间为 T0T_0,升级后在 T1T_1T2T_2 之间产生了对象变更,那么恢复 T0T_0 会丢失:

丢失状态=所有在 (T0,T2] 写入 etcd 的变更\text{丢失状态} = \text{所有在 } (T_0, T_2] \text{ 写入 etcd 的变更}

这包括可能刚刚创建的 Secret、Job、Service、CRD 对象和业务配置。

恢复后还要处理:

  • API Server 证书和访问地址;
  • etcd 集群成员关系;
  • 数据目录;
  • 控制面静态 Pod 或 systemd 配置;
  • 云厂商托管控制面无法由用户自行恢复的问题;
  • 外部数据库、对象存储和消息队列与 Kubernetes 状态的不一致。

3. Kubernetes 版本降级的边界

升级过程中写入新版本字段或新资源后,直接把控制面二进制降回旧版本通常不受支持。即使旧 API Server 能启动,也可能无法正确解释新版本对象。

因此应区分:

  • 节点回滚:通常可通过节点镜像、软件包或实例组版本回退完成;
  • 控制面回滚:往往不是普通的二进制降级,而是恢复兼容版本的控制面和 etcd 数据;
  • API 移除后的回滚:如果升级前没有准备快照和旧版本环境,回滚难度显著增加。

生产环境的回滚计划必须写出“恢复到哪个时间点、会丢失什么数据、如何冻结写入、如何验证恢复结果”,不能只写“执行 downgrade”。


六、升级前的兼容性验证与门禁

1. 用目标版本测试真实对象

应在隔离测试集群中部署:

  • 生产使用的 CRD;
  • 所有 Operator;
  • Admission Webhook;
  • CNI 和 CSI;
  • Ingress Controller;
  • 监控与日志组件;
  • 生产中实际的 Helm Chart 和 Kustomize 输出;
  • 典型 StatefulSet、DaemonSet、Job 和 CronJob。

渲染清单后进行服务端校验:

helm template myapp ./chart -n production > rendered.yaml
kubectl apply --dry-run=server -f rendered.yaml

这一步可以发现目标 API Server 无法识别的资源、字段或校验规则,但无法发现所有运行时问题。例如 Webhook 超时、CSI 挂载失败、网络策略行为改变,都需要真实部署和业务测试。

2. 兼容性测试应覆盖“更新”而不只是“创建”

很多弃用问题在创建阶段不暴露,而在更新阶段出现。原因是控制器会:

  1. 读取现有对象;
  2. 修改某个字段;
  3. 以特定 API 版本重新提交;
  4. 触发资源版本冲突或转换。

因此测试应执行:

kubectl apply -f manifests/
kubectl rollout status deployment/web
kubectl scale deployment/web --replicas=3
kubectl patch deployment/web \
  --type='merge' \
  -p '{"spec":{"template":{"metadata":{"labels":{"revision":"test"}}}}}'

每一步都应检查对象是否仍能被控制器管理、Pod 是否能滚动更新、status 是否持续刷新。

3. Admission Webhook 是常见的隐藏故障点

Webhook 配置中常见字段包括:

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration

升级后,如果 Webhook 服务证书过期、Service 不可达、CA Bundle 错误或 failurePolicy: Fail,大量 API 写操作可能被拒绝:

failed calling webhook "...": context deadline exceeded

此时表现可能像“Deployment 不能创建”或“节点升级卡住”,但根因在扩展组件。诊断路径包括:

kubectl get validatingwebhookconfiguration,mutatingwebhookconfiguration
kubectl describe validatingwebhookconfiguration <name>
kubectl get svc,endpoints -n <webhook-namespace>
kubectl logs -n <webhook-namespace> <webhook-pod>

修改 failurePolicy 可以降低阻断风险,但也可能绕过安全策略或合规校验,不能在不了解策略含义的情况下全局改为 Ignore


七、升级过程中的观测和停止条件

升级不是只看 kubectl get nodes。应为每一批定义明确的停止条件。

1. 控制面停止条件

出现以下情况之一,应停止继续升级:

  • API Server /readyz 持续失败;
  • etcd 延迟、错误率或 leader 变化异常;
  • controller-manager 大量重启或队列持续堆积;
  • scheduler 无法为新 Pod 分配节点;
  • discovery 结果不完整;
  • API 请求出现大量 5xx 或 timeout;
  • 审计或弃用指标显示新版本仍在被旧客户端调用。

2. 节点批次停止条件

节点灰度后,应停止继续扩散的典型信号包括:

  • 新 kubelet 节点无法保持 Ready;
  • Pod sandbox 创建失败;
  • CNI 初始化失败;
  • CSI 挂载、卸载或扩容失败;
  • DNS、Service 或 Ingress 流量异常;
  • 关键业务错误率、延迟或可用副本超阈值;
  • PDB 频繁阻止驱逐,说明容量边界不足。

事件诊断:

kubectl get events -A \
  --field-selector type=Warning \
  --sort-by=.lastTimestamp

节点和 Pod 详细信息:

kubectl describe node worker-a
kubectl describe pod <pod-name> -n <namespace>

若出现 FailedCreatePodSandBox,应优先检查 CNI 和容器运行时;若出现 FailedMount,应检查 CSI 控制器、节点插件、卷拓扑和存储后端,而不是重复执行 drain


八、回滚设计:优先回退扩散,不要假设可以撤回全部变化

1. 节点回滚

若某一批节点升级后异常,首先停止后续节点升级,并避免把更多工作负载调度到故障节点:

kubectl cordon worker-a

如果节点仍能运行 kubelet,可尝试将业务迁移到健康节点:

kubectl drain worker-a \
  --ignore-daemonsets \
  --delete-emptydir-data

随后使用经过验证的节点镜像或软件包恢复 kubelet、kube-proxy、容器运行时和 CNI/CSI 节点组件。恢复后:

kubectl uncordon worker-a
kubectl wait --for=condition=Ready node/worker-a --timeout=5m

若问题来自业务镜像或配置,而不是节点版本,回退节点版本不会解决问题。必须通过事件、日志和指标区分故障层次。

2. 控制面回滚

控制面已经升级并写入新对象后,不能默认执行:

# 不应把它当作通用回滚方案
apt install kube-apiserver=<old-version>

正确做法取决于集群发行方式和目标版本支持的降级路径。若必须恢复,通常需要:

  1. 停止或隔离控制面写入;
  2. 确认恢复点和数据丢失范围;
  3. 使用兼容版本的 etcd 工具恢复快照;
  4. 恢复与该快照匹配的控制面配置和证书;
  5. 启动兼容版本的 API Server、Controller Manager 和 Scheduler;
  6. 验证 API、控制器、节点和关键业务;
  7. 重新处理恢复后与外部系统之间的状态差异。

这属于灾难恢复流程,应该在升级前演练,而不是在生产故障时首次尝试。

3. API 迁移失败时的处理

如果新版本已移除旧 API,通常不能通过把旧清单重新 apply 来恢复。应根据失败类型处理:

  • 旧客户端调用失败:升级客户端、Operator 或发布工具;
  • 旧清单字段不兼容:改写为目标 API 结构;
  • CRD 转换失败:恢复 conversion webhook、修复转换逻辑并重新测试;
  • 对象无法读取:检查 API Server 提供的版本、CRD 的 served/storage 配置和 storedVersions
  • 误删除或错误迁移:从 etcd 快照恢复,或从声明式配置和备份中重建。

API 兼容性问题与节点软件问题不能用同一种回滚动作解决。


九、云厂商 Kubernetes 的差异

托管 Kubernetes 通常把控制面升级、etcd 管理、证书轮换和高可用实现交给云厂商,但这并不消除用户责任:

  • 控制面版本支持窗口由云厂商定义;
  • 节点池可能支持滚动替换、蓝绿替换或原地升级;
  • CNI、CSI、Ingress 和负载均衡控制器有云厂商专属版本矩阵;
  • 云厂商可能自动升级控制面,但不会自动迁移用户镜像、Helm Chart 或 Operator;
  • 托管控制面通常不能由用户直接执行 etcd snapshot restore;
  • 云厂商提供的“回滚”可能只回滚节点池,不回滚控制面 API 和对象数据。

因此升级前必须明确四个边界:

谁升级控制面?
谁负责 etcd 备份与恢复?
谁替换节点?
谁验证扩展组件兼容性?

如果云平台只提供“升级到目标版本”,却没有控制面降级能力,那么回滚策略必须在升级前通过蓝绿集群、备份恢复或重建流程实现。


十、一个可执行的升级门禁示例

下面的门禁不是完整平台,只展示如何把关键事实自动化检查。它假设目标版本是 v1.31,实际使用时应替换为计划目标,并把版本策略与目标版本文档核对。

#!/usr/bin/env bash
set -Eeuo pipefail

target_minor="v1.31"

echo "== API Server =="
kubectl get --raw='/version'
kubectl get --raw='/readyz?verbose'

echo "== Nodes =="
kubectl get nodes
kubectl get nodes \
  -o custom-columns=NAME:.metadata.name,VERSION:.status.nodeInfo.kubeletVersion

echo "== PDB =="
kubectl get pdb -A

echo "== Warning events =="
kubectl get events -A \
  --field-selector type=Warning \
  --sort-by=.lastTimestamp | tail -n 50

echo "== Deprecated API metrics =="
deprecated="$(
  kubectl get --raw='/metrics' \
  | grep 'apiserver_requested_deprecated_apis' || true
)"
if [[ -n "$deprecated" ]]; then
  echo "$deprecated"
  echo "Deprecated API requests were observed; migration is required."
  exit 1
fi

echo "== Server-side dry run =="
kubectl apply --dry-run=server -f manifests/

echo "All automated gates passed for preliminary review."

这个脚本仍有明确边界:

  • 它不能判断目标版本上的 API 是否即将移除;
  • 它不能证明定时任务和冷门 Operator 没有调用弃用 API;
  • 它不能替代 etcd 恢复演练;
  • 它不能证明业务流量、存储和网络正常;
  • 它不能决定 PDB 是否足以承受某种节点批次。

脚本适合做门禁的一部分,而不是把复杂的升级判断压缩成一个成功退出码。


十一、常见误解与对应故障

误解一:kubectl 版本一致,集群就升级完成了

kubectl 只是客户端。真正决定 API 行为的是 API Server,真正决定节点能否运行 Pod 的是 kubelet、容器运行时和网络/存储插件。必须分别检查各类组件。

误解二:kubectl apply 成功,API 迁移就完成了

apply 成功只说明这次请求被接受。旧对象、后台控制器、Webhook、定时任务和第三方工具仍可能使用旧 API。必须观察完整运行周期,并扫描 API Server 指标和审计记录。

误解三:节点全部 Ready,业务就没有风险

节点 Ready 只代表 kubelet 报告节点满足基本条件。它不证明:

  • Service 流量规则正确;
  • DNS 正常;
  • PVC 已挂载;
  • Ingress 能转发;
  • 应用依赖可访问;
  • 业务延迟和错误率正常。

误解四:etcd snapshot 等于随时可回滚

快照只有在能够恢复、版本兼容、证书和成员配置匹配,并且团队知道数据丢失范围时才具有回滚价值。没有恢复演练的快照,只能称为“已生成备份文件”,不能称为可验证的恢复方案。

误解五:强制 drain 可以提高升级速度

强制 drain 只是绕过保护条件,不会消除容量、数据和业务依赖。它可能把一个可诊断的“无法安全驱逐”变成多个副本同时中断、数据丢失或卷无法重新挂载的问题。


十二、最终验收

升级完成后,至少应验证以下状态:

kubectl get nodes
kubectl get pods -A
kubectl get --raw='/readyz'
kubectl api-versions
kubectl get crd
kubectl get pdb -A
kubectl get events -A --field-selector type=Warning

还应验证声明式和运行时行为:

  1. 部署、更新、扩缩容和回滚一个代表性 Deployment;
  2. 创建和完成一个 Job;
  3. 执行一次 CronJob;
  4. 重启一个使用持久卷的工作负载;
  5. 验证 Service、DNS、Ingress 和网络策略;
  6. 检查 Webhook、Operator、CNI、CSI 和监控告警;
  7. 确认弃用 API 指标在观察窗口内没有新增请求;
  8. 保存升级前后版本、事件、配置和备份记录。

一次合格的 Kubernetes 升级应能回答四个问题:

  • 哪些组件暂时处于混合版本,是否符合 Version Skew Policy?
  • 哪些 API 已弃用,所有调用者和存储对象是否完成迁移?
  • 节点按什么批次灰度,PDB、容量、网络和存储是否承受得住?
  • 控制面或节点出现故障时,究竟是回退节点、停止扩散,还是执行 etcd 灾难恢复?

只有把这四个问题分别验证清楚,版本升级才不是一次高风险的批量替换,而是一个具有边界、观测和恢复路径的状态迁移。


系列导航与关联阅读

官方资料

本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。