AI时代的Kubernetes教程36招-第31招-Kubernetes怎么升级、控制Version Skew并安全回滚?

kubeadm 升级实战:1.36→1.37 与补丁升级的官方路径与回滚边界

升级顺序错了,回滚就只剩向前一条路

[读者] 自建 kubeadm 集群的运维与平台工程师
[痛点] 想升到 1.37,却怕 Version Skew 与不可逆回滚
[现在读] 升级窗口已经排期,方案还没定稿
[读完] 能按官方路径完成 minor 与 patch 升级并守住回滚边界
[旧方案] 全节点一起 apt 升 kubelet 再重启
    |
    v
[新需求] 不停机、可回滚、不跳 minor 升到 1.37
    |
    v
[冲突] kubelet 不得新于 apiserver,etcd 又不支持降级
    |
    v
[后果] 节点 NotReady,回滚变成只能向前

我是老李,在一家做数据服务的公司管自建 Kubernetes。集群是三台控制面加八台工作节点的 HA 结构,Ubuntu + containerd,etcd 由 kubeadm 以静态 Pod 托管,控制面停在 1.36.x。

这套集群是三年前我用 kubeadm 起起来的,中间升过两次,方式是下班后所有人 ssh 上去,apt 升级 kubelet,然后重启。那两次运气不错,业务只是晚高峰后抖了一下。

这次不一样。业务方要求控制面升到 1.37.y,理由是安全修复和新版本带来的调度侧能力;同时明确说,滚动期间 API 不能整体中断,有状态服务的 PVC 不能被强制 detach。任务是把这个三控制面八工作节点的集群安全推到 1.37.y,再完成一次 1.37.x 到 1.37.y 的补丁升级。

于是问题来了:先升谁?先升 kubelet,还是先升控制面?

如果顺序选错,你打算用什么把集群退回去?

如果它根本退不回去,你打算怎么跟业务方交代?

01、故事

被选中的第一个控制面叫 cp-01。第一次尝试,我按老经验做事:先把三台控制面的 kubelet 包升到 1.37.y,重启 kubelet,然后才打算升 kubeadm 和控制面。

十分钟内,kubectl get nodes 里三台控制面全部 NotReady。apiserver 还是 1.36.x,kubelet 已经是 1.37.y,kubelet 日志里是版本相关的注册失败。进程虽然还在跑,集群却已经失去自愈能力——任何一个静态 Pod 退出,都不会再被拉起来。

那一刻我才真正读懂官方文档的顺序:控制面先到新版本,节点再追;节点只能落后,不能超前。这不是建议,而是 Version Skew Policy 写死的契约。

顺序定生死
版本有高低
先动控制面
再谈节点事

02、问题

旧方案在四个点上失效。

第一是顺序违约。kubelet 新于 kube-apiserver,直接违反官方版本偏差规则,属于未定义行为,出问题没有官方支持路径。

第二是可用性。控制面同时重启会让 apiserver 出现无人应答窗口,有状态服务的 leader 选举和 PVC 挂载会一起抖动。

第三是可观测性。一次动全集群,出问题后无法判断是哪一步引入的,只能靠猜。

第四是不可逆。etcd 数据一旦被新版本写入,就不能回到旧版本,所谓回滚从这一刻起只是幻觉。

业务影响很直接:API 中断,控制器失联,业务 Pod 既不重启也不缩容,监控上的曲线是断崖而不是波动。

本篇的完成标准必须是可验证的,我给自己列了六条:所有节点的 VERSION 列等于目标 patch;控制面组件版本一致;etcd 每个成员 endpoint health 均为 healthy;升级期间任一时刻至少两台 apiserver 可服务;每个阶段都有 etcd 快照与 /etc/kubernetes/tmp/ 备份目录留档;升级后没有工作负载因驱逐进入 CrashLoopBackOff。

旧法失其效
新需逼人前
标准须可验
空谈不算成

03、原理

官方版本偏差政策里有几条硬规则。kube-apiserver 必须是集群里最新的组件,HA 集群中最新与最旧的 apiserver 之间只能差一个 minor。kubelet 不得新于 kube-apiserver,最多可落后三个 minor。kube-controller-manager、kube-scheduler、cloud-controller-manager 不得新于它们通信的 apiserver,最多落后一个 minor。kubectl 支持与 apiserver 相差一个 minor,新旧皆可。kube-proxy 不得新于 kube-apiserver,旧版本 kube-proxy 的可用范围以当前官方文档为准。

反直觉判断:这张表里最容易被忽略的不是能落后多少,而是不能超前。落后是允许的过渡状态,超前是未定义行为。所以升级方向天生单向,控制面永远走在前面。

再看 kubeadm 的职责边界。kubeadm 负责控制面静态 Pod 清单、集群级配置如 kubelet-config ConfigMap、证书续期和 addon;kubelet 是操作系统包管理器的事。所谓升级集群,实际是两条版本线的对齐,两条线之间靠 skew 规则约束。

硬约束一:一次只能升一个 minor。kubeadm 会拒绝一次性跨越多个 minor 的目标版本,跳版本不是胆子大,是根本没这条路。

硬约束二:kubeadm 托管的 etcd 版本由 kubeadm 决定,而 etcd 数据不支持降级到较低版本。

反直觉判断:/etc/kubernetes/tmp/ 下的备份是给升级失败自动恢复用的,它备份的是静态 Pod 清单与 etcd 清单,不是数据。真正决定你能退多远的,是 etcd 里的数据格式。

偏差是窗口
不是稳态图
升易降难处
数据最无情

04、架构

[输入] 目标版本 v1.37.y 与节点清单
    |
    v
[模块] kubeadm(plan / apply / node)
    |
    v
[数据/状态] /etc/kubernetes/manifests、etcd 数据、kubelet-config ConfigMap
    |
    v
[处理] 快照 → drain → 换 kubeadm 包 → apply/node → 换 kubelet 包 → uncordon
    |
    v
[输出] 目标版本的控制面与节点 + 可复核的备份点

边界:这套流程只覆盖 kubeadm 管理的自建集群。外部 etcd 不在 kubeadm 的升级范围内,需要单独规划并显式跳过 etcd 升级;托管控制面同样不适用。工作节点上的容器运行时、CNI、CSI 也不在 kubeadm 的升级职责里。

收益:顺序可控,每一步可验证,每步都能停。控制面先升保证 skew 单向,节点逐个收尾保证业务只感受到短暂的一次驱逐。

代价:时间明显变长,drain 与 uncordon 的次数等于节点数,需要业务侧配合 PDB 和维护窗口,还需要一个不会被误用的备份目录规范。

适用条件:kubeadm 部署、etcd 由 kubeadm 托管或已单独规划、节点规模在人工可控范围内、业务能接受节点级滚动。

入口是目标版
出口是新集群
中间有顺序
各段各有界

05、实战一次

环境与版本:Ubuntu 22.04、containerd、kubeadm 部署的 HA 集群,当前 v1.36.x,目标 v1.37.y。具体包版本号需按当前官方文档核验,下面的 1.37.y-1.1 只是占位形式。

依赖:kubeadm、kubelet、kubectl 三个包,etcdctl(或 etcdutl),apt-mark,以及一份写到集群外的快照目录。注意 etcd 3.6 之后快照子命令已迁移到 etcdutl,需按当前官方文档核验。

配置:备份留档目录用 /var/backups/k8s;kubeadm 自己的临时备份落在 /etc/kubernetes/tmp/,包含 kubeadm-backup-manifests- 与 kubeadm-backup-etcd- 两类目录,升级后要逐个确认存在。

核心实现,首个控制面:

NODE="$(hostname)"

# 0. 记录现场
kubectl get nodes -o wide
kubectl -n kube-system get pods -o wide

# 1. 备份 etcd,快照写到集群外
sudo ETCDCTL_API=3 etcdctl \
  --endpoints=https://127.0.0.1:2379 \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/server.crt \
  --key=/etc/kubernetes/pki/etcd/server.key \
  snapshot save /var/backups/k8s/etcd-v1.36.x.db

# 2. 隔离本节点
kubectl drain "${NODE}" --ignore-daemonsets

# 3. 升级 kubeadm 包
sudo apt-mark unhold kubeadm
sudo apt-get update
sudo apt-get install -y kubeadm='1.37.y-1.1'
sudo apt-mark hold kubeadm
kubeadm version

# 4. 先看计划,再执行
sudo kubeadm upgrade plan
sudo kubeadm upgrade apply v1.37.y

# 5. 升级 kubelet 与 kubectl
sudo apt-mark unhold kubelet kubectl
sudo apt-get install -y kubelet='1.37.y-1.1' kubectl='1.37.y-1.1'
sudo apt-mark hold kubelet kubectl
sudo systemctl daemon-reload
sudo systemctl restart kubelet

# 6. 放回调度
kubectl uncordon "${NODE}"

启动与请求:kubeadm upgrade plan 是只读步骤,用来确认目标版本、etcd 目标版本与 addon 目标版本;kubeadm upgrade apply 才会改写 /etc/kubernetes/manifests 下的静态 Pod 清单并触发 kubelet 重建控制面容器。

未在当前环境实测,以下为预期结果。示例输出:

$ sudo kubeadm upgrade plan
[preflight] Running pre-flight checks
[upgrade/version] You have chosen to change the cluster version to v1.37.y
[upgrade/versions] Cluster version: v1.36.x
[upgrade/versions] kubeadm version: v1.37.y
[upgrade/versions] Target version: v1.37.y
[upgrade/etcd] etcd upgrade target reported by kubeadm
[upgrade/addons] CoreDNS and kube-proxy upgrade target reported by kubeadm

$ sudo kubeadm upgrade apply v1.37.y
[upgrade/successful] SUCCESS! Your cluster was upgraded to v1.37.y.

验证:

kubectl get nodes -o wide
kubectl version
kubectl -n kube-system get pods -l tier=control-plane -o wide
kubectl -n kube-system exec etcd-<node> -- etcdctl endpoint health
ls -l /etc/kubernetes/tmp/

后续控制面与工作节点走同一段逻辑,只换一条命令:drain → 换 kubeadm 包 → sudo kubeadm upgrade node → 换 kubelet 与 kubectl 包 → daemon-reload 与重启 kubelet → uncordon。addon 的升级由首个控制面的 kubeadm upgrade apply 负责,kubeadm upgrade node 不重复处理。

一次只动一节点
一步只验一件事
先计划后执行
慢走方为快

06、排查

第一条诊断链。现象:某工作节点升级后 kubectl get nodes 显示 NotReady,节点 VERSION 列已经高于控制面。怀疑:网络插件、容器运行时或节点证书。检查:kubectl describe node、journalctl -u kubelet -n 200、kubelet --version、kubectl version。证据:kubelet 版本已到 1.37.y,而 apiserver 仍是 1.36.x,kubelet 日志里是版本相关的注册失败。根因:违反 kubelet 不得新于 kube-apiserver。修复:把 kubelet 包降回 1.36 系列,恢复顺序,从控制面重新开始。

错误尝试: 先 systemctl restart kubelet,再 kubectl delete node 让它重新 join。这是错的:重启不改变版本,而 delete node 会丢掉节点对象与本地状态,重新 join 仍然要面对同一套版本对齐要求,还会触发一轮无谓的 Pod 重调度,把一次版本问题放大成一次业务抖动。

第二条诊断链。现象:升级窗口里 kubeadm upgrade plan 或工作节点上的升级预检报版本不合规,或提示目标版本超出允许范围。怀疑:网络拉不到版本信息。检查:逐节点执行 kubelet --version、kubeadm version,做成一张节点版本表。证据:表里三台节点还停在 1.33。根因:历史遗留节点从未补齐 minor,而 kubeadm 不支持一步跨越多个 minor。修复:按 1.33 到 1.34、1.35、1.36 逐级补齐,或在业务允许时用新节点替换旧节点。

第三条诊断链。现象:首控制面 apply 完成后业务验证不通过,想退回 1.36.x。怀疑:/etc/kubernetes/tmp/ 里有备份,直接还原即可。检查:ls -l /etc/kubernetes/tmp/,看到 kubeadm-backup-manifests- 与 kubeadm-backup-etcd-。证据:备份里只有静态 Pod 清单与 etcd 清单;etcd 已经用新版本启动并写入了数据目录,官方明确 etcd 数据不支持降级。根因:回滚上限被数据格式锁死,而不是被文件锁死。修复:数据未写入前可以把清单和 kubeadm、kubelet 包一起退回旧版本;数据已经写入,就转为向前修复,或用 etcd 快照重建到新的集群。

现象先看清
怀疑要可查
证据定根因
修复需回正

07、优化

根因很明确:问题不在命令写错,而在缺少阶段边界与门禁。所以 V2 不改命令内容,改的是执行结构——把升级拆成三个可独立执行、独立验证、独立中止的阶段,每个阶段失败都停在原地,而不是把整个集群拖进去。

修改点有三个。第一,把集群级备份做成阶段前置条件,快照写到集群外,同时留档节点与 Pod 清单,出问题时至少有一份对照基线。第二,把首控制面、其余控制面、工作节点拆成三个入口,参数化目标版本,避免手敲命令时把 apply 和 node 用反。第三,把 kubeadm upgrade plan 固定为执行前的门禁步骤,计划不过就不许 apply。

新行为如下:

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

PKG="1.37.y-1.1"
TARGET="v1.37.y"
STAMP="$(date +%Y%m%d-%H%M%S)"
NODE="$(hostname)"
BACKUP="/var/backups/k8s"
mkdir -p "${BACKUP}"

backup_cluster() {
  ETCDCTL_API=3 etcdctl \
    --endpoints=https://127.0.0.1:2379 \
    --cacert=/etc/kubernetes/pki/etcd/ca.crt \
    --cert=/etc/kubernetes/pki/etcd/server.crt \
    --key=/etc/kubernetes/pki/etcd/server.key \
    snapshot save "${BACKUP}/etcd-${STAMP}.db"
  kubectl get nodes -o wide > "${BACKUP}/nodes-${STAMP}.txt"
  kubectl -n kube-system get pods -o wide > "${BACKUP}/pods-${STAMP}.txt"
}

hold_kubeadm() {
  sudo apt-mark unhold kubeadm
  sudo apt-get update
  sudo apt-get install -y "kubeadm=${PKG}"
  sudo apt-mark hold kubeadm
  kubeadm version
}

upgrade_kubelet() {
  sudo apt-mark unhold kubelet kubectl
  sudo apt-get install -y "kubelet=${PKG}" "kubectl=${PKG}"
  sudo apt-mark hold kubelet kubectl
  sudo systemctl daemon-reload
  sudo systemctl restart kubelet
}

gate_and_apply() {
  sudo kubeadm upgrade plan
  sudo kubeadm upgrade apply "${TARGET}"
}

case "${1:-}" in
  cp-first)
    backup_cluster
    kubectl drain "${NODE}" --ignore-daemonsets
    hold_kubeadm
    gate_and_apply
    upgrade_kubelet
    kubectl uncordon "${NODE}"
    ;;
  cp-other)
    kubectl drain "${NODE}" --ignore-daemonsets
    hold_kubeadm
    sudo kubeadm upgrade node
    upgrade_kubelet
    kubectl uncordon "${NODE}"
    ;;
  worker)
    kubectl drain "${NODE}" --ignore-daemonsets
    hold_kubeadm
    sudo kubeadm upgrade node
    upgrade_kubelet
    kubectl uncordon "${NODE}"
    ;;
  *)
    echo "usage: $0 {cp-first|cp-other|worker}"
    exit 1
    ;;
esac

echo "stage done: ${1} on ${NODE} at ${STAMP}"
ls -l /etc/kubernetes/tmp/

为什么这样改:把顺序从人的记忆变成脚本的分支,把可回滚性从文件变成快照加留档,把验证从事后变成事前门禁。

新行为:任一阶段失败,集群停在两种状态之一——要么本节点已 drain 未升级,要么本节点已升级未 uncordon,两种都能被人一眼看出来并手工收尾。

验证方式:每个阶段结束后跑一遍第 05 章那组检查命令,确认节点版本、控制面组件版本、etcd 健康,以及 /etc/kubernetes/tmp/ 下备份目录的存在。补丁升级 1.37.x 到 1.37.y 走的是同一套脚本,只是 PKG 与 TARGET 换成对应 patch,顺序与门禁一个字都不用改。

根因既已明
改动须最小
新行为可验
不做无据事

08、演进

[同一输入:三控制面八节点集群升到 v1.37.y]
    |
    +--[V1] 人工按文档顺序逐节点敲 / 顺序靠记忆,失败靠经验
    |
    +--[V2] 阶段化脚本 + 门禁 + 备份留档 / 流程变长,需业务配合 PDB
    |
[Trade-off]
得到可复核的升级证据与明确的回滚边界 / 失去一键式的速度 / 适用于 kubeadm 自建集群且窗口可控的场景

正确性上,V1 与 V2 执行的是同一套官方路径,V2 的优势在于把顺序固化成分支,把跳 minor 这种错误从可能变成不可能。

稳定性上,V1 依赖执行者当时的判断,V2 依赖门禁的通过与否;V2 把 kubeadm upgrade plan 提到 apply 之前,等于给每次升级加了一次免费的预检。

复杂度上,V2 明显更高:多了一份脚本要维护,多了一个备份目录要巡检,也多了一次参数写错的风险。这是真实的成本,不是可以忽略的细节。

成本上,两者都要占维护窗口,但 V2 把窗口内的操作从思考型变成动作型,实际占用时间更短,对业务的暴露更可控。

适用范围上,节点规模越大、控制面越多,V2 的收益越明显;如果是单节点学习集群,V1 的手工方式反而更快。

遗留问题有两个:一是补丁升级和 minor 升级的差异被脚本参数吸收了,执行者容易忘记补丁升级同样需要 drain;二是脚本本身没有处理跨 minor 补齐,遇到 1.33 这类遗留节点仍然要人工逐级升。

同一份输入
两条升级路
得在可控处
失在自动间

09、洞见

9.1 顺序不是建议,是契约

反直觉判断:版本偏差表看起来在讲宽容度,实际在讲方向。它允许 kubelet 落后三个 minor,却一个字都不允许 kubelet 超前。宽容是给滚动升级留窗口,方向才是必须遵守的契约。理解了这一层,先升谁这个问题就没有第二种答案。

9.2 备份不等于回滚能力

反直觉判断:/etc/kubernetes/tmp/ 里有备份,不等于你拥有回滚能力。它备份的是清单,是给升级中途失败时自动恢复用的;而真正决定能退多远的,是 etcd 数据格式。清单可以还原,数据不能降级。把这两件事混为一谈,就会在出事那天才发现自己没有退路。

9.3 排除法要先排除版本,再排除网络

节点 NotReady 时,人的第一反应是查 CNI、查容器运行时、查证书,因为那些问题见得最多。但当集群刚做过版本变更,最该先查的是版本对齐本身。一次 kubelet --version 加一次 kubectl version,比翻半小时网络日志便宜得多。

9.4 补丁升级不是 minor 升级的简化版

补丁升级不需要跨越 minor,但同样要 drain、同样要换包、同样要重启 kubelet、同样要 uncordon。它省下的只是对 etcd 与 addon 的重新评估,省不掉节点级滚动。把它当成热补丁,是在用业务的稳定性换自己的省事。

反直觉之处
常藏真边界
备份非回滚
顺序即契约

10、系统落地

原来我手里有什么:一份官方升级文档的链接,一个记在脑子里的大致顺序,一份只在出事时才想起来的 etcd 备份命令。

这一篇新增了三个东西:一份按阶段切分的升级脚本,参数化目标版本并区分首控制面、其余控制面与工作节点;一套阶段门禁,把 kubeadm upgrade plan 固定在每次 apply 之前;一个留档规范,把 etcd 快照、节点清单、Pod 清单和 /etc/kubernetes/tmp/ 备份目录一起归档到同一个时间戳下。

现在能做什么:能按官方路径完成 1.36 到 1.37 的 minor 升级,能完成 1.37.x 到 1.37.y 的补丁升级,能在每个阶段停下来,并且清楚自己停在哪里、能不能退、退到哪一步为止。

还缺什么:缺跨 minor 补齐的批量处理,遗留节点仍然要人工逐级升级;缺升级后的自动一致性校验,目前还靠人工比对节点版本表;缺把 etcd 快照推到异地的机制,快照还在同一台机器附近。

下一步演进方向很清楚:先把节点版本清单做成常态化巡检,把落后超过一个 minor 的节点提前暴露出来,让升级窗口里不再出现意外;再把升级后的校验项固化成一次可重复执行的检查,而不是每次临时想检查什么。

原有是手工
新增是门禁
能跑不足够
还要能收场

11、小结

Q1 → kubelet 不得新于 kube-apiserver,所以控制面必须先行
Q2 → kubeadm 一次只支持一个 minor,跨 minor 只能逐级补齐
Q3 → 清单可还原、组件包可降级,但 etcd 数据不可降级,回滚上限被数据锁死
状态 → 首控制面升级路径跑通,且拿到可复核的门禁与备份留档方式

这一篇交付的不是一段能跑的命令,而是一条知道自己停在哪里的升级路径。路径的价值不在于快,而在于每一步都能被验证,每一步都能被中止。

三问皆有答
一役有所得
能升能回退
方称可交付

12、作业

12.1 理解题:为什么升级必须从控制面开始,而不是从工作节点的 kubelet 开始?

参考答案:因为版本偏差规则是单向的。kubelet 允许落后于 kube-apiserver 最多三个 minor,但不允许超前。先升 kubelet 会制造出 kubelet 新于 apiserver 的状态,这个状态没有任何官方支持路径。控制面先升,节点再追,前后都落在允许窗口内。

12.2 实战题:请写出首个控制面从 1.36.x 升到 1.37.y 的完整步骤,并说明每一步的验证方式。

参考答案:记录现场并备份 etcd 快照到集群外;drain 本节点;apt-mark unhold kubeadm 后安装目标版本并 hold,用 kubeadm version 验证;执行 kubeadm upgrade plan 确认目标版本、etcd 与 addon 目标;执行 kubeadm upgrade apply v1.37.y;安装目标版本 kubelet 与 kubectl 并 hold,daemon-reload 与重启 kubelet;uncordon 本节点。验证方式分别是快照文件存在、节点显示 SchedulingDisabled、kubeadm version 输出目标版本、plan 无版本不合规报错、apply 返回成功、kubectl get nodes 显示节点版本、以及确认 /etc/kubernetes/tmp/ 下有备份目录。

12.3 排障题:升级后某工作节点 NotReady,kubelet 版本高于控制面,你打算怎么处理?

参考答案:不要重启 kubelet,也不要 delete node。先确认证据:kubelet --version 与 kubectl version 对比,journalctl -u kubelet 查看版本相关报错。根因是 kubelet 超前,修复手段是把 kubelet 包降回与控制面同 minor,恢复正确顺序后从控制面重新推进。delete node 会让节点对象与本地状态丢失,重新 join 也解决不了版本对齐问题,只会把版本问题放大成一次业务重调度。

12.4 架构判断题:如果业务方要求升级失败必须能回滚到旧版本,你能否承诺?

参考答案:只能在有条件的范围内承诺。控制面清单、kubeadm 与 kubelet 包在 etcd 数据被新版本写入之前可以退回旧版本,/etc/kubernetes/tmp/ 下的备份也能支撑升级中途失败的自动恢复。但 etcd 数据不支持降级,一旦新版本写入数据,回滚就不再是可行路径。此时正确的方案是向前修复,或用 etcd 快照重建集群,而不是承诺一个不存在的回滚。

题考理解力
亦考动手功
更考边界感
答完再上场

13、思考

回到最初那个问题:先升谁。这个问题的答案不在经验里,在版本偏差表里。表规定了方向,方向决定了顺序,顺序决定了你在出问题时是能停下来,还是只能往前走。

工程上真正稀缺的不是升级命令,而是对边界的判断力。知道什么可以落后、什么不能超前,知道清单能还原而数据不能降级,知道什么时候该停手而不是继续敲回车。

可复用的判断只有一条:凡是不可逆的资源,都要在动它之前先问清楚退路;问不清楚,就不要开始。

升级非重启
是一次契约
守住顺序感
输赢皆可控

posted @ 2026-10-12 09:37  李福春  阅读(3)  评论(0)    收藏  举报