Kubernetes 升级与版本兼容:控制面、节点和 API 演进
节点已经显示新版本,是否就代表集群升级完成?如果 API Server 已经升级,kubelet 仍是旧版本,集群是否处在故障状态?升级后应用无法创建,问题又一定出在节点吗?
Kubernetes 的升级需要同时处理组件版本、API 和数据、扩展组件,以及工作负载的连续运行。版本号是检查入口,不能单独代表业务已经恢复。本文用 Kubernetes v1.35.1 到 v1.35.2 的隔离补丁升级观察这些层次,并用固定版本源码解释 kubeadm 实际修改了什么。
升级顺序受通信关系约束
kubelet 不直接读取 etcd,而是通过 API Server 观察分配到本节点的 Pod,并回写节点和 Pod 状态。Scheduler、Controller Manager 和 kube-proxy 也通过 API Server 获取对象。组件升级过程中,这些通信仍然会发生,所以需要保证新旧版本之间的协议和行为处在支持范围内。
按 Kubernetes v1.35 版本偏差策略,多个 API Server 之间最多相差一个小版本;kubelet 和 kube-proxy 不应比 API Server 更新,允许的向后偏差最多为三个小版本。Scheduler 和 Controller Manager 应与所访问的 API Server 同小版本,升级窗口允许落后一个小版本。kubectl 则允许比服务端新或旧一个小版本。部署工具和插件可能施加更严格的限制。
这里的“小版本”指 1.35 中的 35,不是补丁号。允许向后偏差不等于建议长期保留旧节点,也不能据此跳过 API Server 的中间小版本。在 HA 集群中,组件可能连接负载均衡后的任意 API Server,因此应按照它可能访问到的全部实例取兼容范围的交集。
这会约束升级顺序:先让 API Server 进入能接纳后续组件的版本,再推进其他控制面组件和节点。实际使用 kubeadm 时,应遵循对应目标版本的升级流程,不能从这几条原则自行拼出一套与部署工具冲突的命令。本文的补丁升级不构成跨小版本升级已经验证的证据。
kubeadm 检查的是集群与目标版本的组合
kubeadm upgrade apply 并不是简单替换一个镜像标签。在 v1.35.2 的 newCmdApply 中,命令通过工作流依次注册预检、控制面升级、集群配置上传、kubeconfig、kubelet 配置、插件和升级后处理阶段。前一阶段失败后,正常流程不会继续假定后续动作已经完成。
预检中的 runPreflight 读取实际集群信息,检查健康状态并调用版本策略。版本检查的输入包括目标版本、当前集群版本、执行命令的 kubeadm 版本,以及节点报告的 kubelet 版本。
关键分支位于 EnforceVersionPolicies:
该版本的 MaximumAllowedMinorVersionUpgradeSkew 为 1。策略把错误分为 Mandatory 和 Skippable;enforceVersionPolicies 遇到 Mandatory 错误会返回,--force 只影响允许跳过的那一类错误。它不会把任意跨版本组合变成受支持的升级路径。
kubeadm 必须先具备理解目标版本配置的能力,因此升级流程需要先准备匹配的 kubeadm,再由它执行计划和变更。upgrade plan 是根据当前状态形成的计划;它不是对业务流量、外部 Webhook 或云盘插件的完整兼容性认证。
首个控制面节点执行 kubeadm upgrade apply,其余控制面和工作节点执行 kubeadm upgrade node,不能把首节点命令复制到全部机器。newCmdNode 注册本节点预检、控制面、kubeconfig、kubelet 配置和插件等阶段,再根据节点角色决定适用动作;它不会重新执行首节点的全部集群级升级流程。执行变更前还应准备可校验的 etcd 快照、控制面配置和证书备份,并为业务卷另行准备恢复点;能恢复某个 manifest 不代表能恢复这些数据。
控制面升级靠 kubelet 落实静态 Pod 变化
在 kubeadm 管理的静态 Pod 控制面中,kubeadm 生成新 manifest,kubelet 观察节点上的 manifest 后,才通过运行时替换组件。API Server 的 Deployment 控制器不参与这些静态 Pod 的创建。
v1.35.2 的 upgradeComponent 先比较新旧 manifest,相同时可跳过该组件。需要变更时,它记录旧 manifest 的备份位置,将旧文件移出正式目录,再移入新文件。
代码随后等待 mirror Pod 的配置 hash 变化,并检查组件 Pod 的状态。这一步有具体原因:旧组件可能还在运行,如果只检查旧组件的健康端点,就可能把“旧进程仍然健康”误判成“新版本已经成功启动”。hash 变化让后续检查能够关联到这次配置替换。
sequenceDiagram
participant O as 操作者
participant A as kubeadm
participant F as 节点 manifest 文件
participant K as kubelet
participant R as 容器运行时
participant API as API Server
O->>A: upgrade apply 目标版本
A->>API: 读取集群状态并检查版本
A->>F: 备份旧 manifest 并放入新 manifest
K->>F: 观察静态 Pod 配置变化
K->>R: 协调旧容器终止与新容器启动
K->>API: 更新 mirror Pod 信息
A->>API: 等待配置 hash 变化及组件就绪
API-->>A: 返回观测状态
A-->>O: 当前控制面升级结果
图中 API Server 是被访问的 API 服务;升级 API Server 自身时,它会经历进程切换,检查可能需要重试。图上没有把 mirror Pod 更新画成新进程已经服务所有业务请求的统一完成屏障。
发生部分文件移动、证书续期或等待失败时,源码会尝试通过 rollbackOldManifests 恢复已记录的 manifest。但这个恢复动作针对当前升级步骤,不能推导出任意已完成的升级都能通过更换旧二进制安全降级。对象数据、插件状态、证书和外部系统变更不属于同一个原子事务。
节点维护先解决工作负载去向
cordon 让节点不再接收普通的新调度;它不会把已有 Pod 搬走。drain 继续枚举节点上的 Pod,过滤不能按普通方式处理的对象,并尝试驱逐可处理的 Pod。控制器随后观察副本缺口,创建新的 Pod,再由调度器选择节点。
在 DeleteOrEvictPods 中,客户端优先发现并使用 Eviction API;evictPods 遇到预算导致的 TooManyRequests 会重试,直到成功或达到超时。等待删除时还比较 UID,避免把同名新 Pod 当作旧 Pod 一直等待。
所以,维护前需要确认剩余节点上有可用容量、存储可以满足重新调度、约束允许迁移,以及 PDB 有足够预算。只有 PDB 并不能创造替代副本,也不能消除卷的拓扑约束。预算为零时绕开 Eviction 直接删除 Pod,会改变保护语义,不应作为默认的“解除升级阻塞”手段。具体保护范围见已有的 PDB 与驱逐文章。
DaemonSet、静态 Pod、无控制器的独立 Pod 和带本地数据的 Pod 需要分别处理。--ignore-daemonsets 允许 drain 继续处理其他对象,并不表示已经删除 DaemonSet Pod 或关闭该节点上的系统进程。
控制面完成后,节点版本可能仍然是旧值
kubeadm 升级控制面时可以更新 kubelet 的配置,却不会代替操作系统包管理器安装并重启所有节点的 kubelet。
这一点可以从 runKubeletConfigPhase 看清:核心操作是 WriteKubeletConfigFiles,将配置写入节点目录,并处理对应节点信息。这不是下载一个新 kubelet 可执行文件再启动它。
因此,需要分别核对 API Server 版本、节点报告的 kubelet 版本、kube-proxy 镜像和插件状态。kubectl get nodes 中的 VERSION 主要反映 kubelet,不能当作 API Server 已升级或未升级的唯一证据。包升级和进程重启后,还需要等待节点 Ready,再验证该节点真正能够创建工作负载。
API 兼容不是改一个 apiVersion
API 移除意味着旧端点不再提供服务;资源对象可能仍通过另一个稳定版本存在。清单、Helm 模板、GitOps 控制器和程序中的 typed client 如果继续请求旧端点,仍可能失败。
迁移时应同时核对字段、默认值和语义。例如,旧版本的字段可能已被替换,稳定版本也可能要求新的必填结构。只改 apiVersion 而不核对 schema,可能从“找不到端点”变成“对象校验失败”,也可能生成与原意不同的配置。具体移除版本与迁移字段应查阅官方 API 迁移指南。
CRD 还存在 served version、storage version 和转换逻辑的区别。停止提供旧版本前,需要确认客户端已经迁移,以及历史存储对象能够被当前转换链处理。修改 CRD 的存储版本声明,不等于已经迁移所有历史对象。
扩展组件也要进入升级检查:CNI 与内核数据面、CSI 与存储服务、Device Plugin 与驱动、Admission Webhook 与 AdmissionReview 版本及新字段。API Server 和节点全部 Ready,只能说明一部分基础条件成立;它们不会替这些扩展完成兼容性验证。
实验:先验证维护条件,再升级补丁版本
本轮通过 SSH 在 zj 创建专用 kind v0.33.0 集群 article-upgrade,拓扑为一个控制面、两个工作节点。起始 API Server、kubectl 和 kubelet 均为 Kubernetes v1.35.1,提交 8fea90b45245ef5c8ba54e7ae044d3e777c22500;目标为 v1.35.2,提交 fdc9d74cbf2da6754ebf81d56f80ae2948cd6425。没有修改 Kubernetes 源码,也没有升级 etcd 或执行证书续期。
实验文件与准备、运行、结果查看及清理说明见 k8sdev/article-upgrade。先按该目录 README 的固定提交准备步骤创建独立工作目录,再运行实验。
维护实验检查 PDB 拒绝、原 Pod UID 仍存在,以及成功 drain 后的副本位置,不能只以命令返回值代替这些检查。
先创建单副本 Deployment,并设置 PDB 的 minAvailable: 1。drain 将节点设为不可调度后,反复收到:
| |
脚本检查原 Pod UID 仍存在。这个结果说明维护被预算阻塞,不能把 drain 返回非零理解为节点仍然完全保持原状态:cordon 已经发生,需要明确决定后续是继续维护还是 uncordon。
恢复调度、扩为两副本并确认分散在两个工作节点后,再次执行 drain。该次操作成功,两个就绪副本最终都位于剩余工作节点;原来被驱逐的副本由新 UID 替代。它验证了控制器补副本与重新调度,没有测试真实 HTTP 业务的无损切流。工作负载使用软反亲和,以允许维护期间副本临时共处一台节点。
本轮还直接请求旧的 extensions/v1beta1 Deployment 端点,得到 NotFound,而 apps/v1 discovery 成功。这个对照说明旧 API 不可用时,应检查请求端点和清单版本,不能把错误归因于工作节点没有升级。它不代表已经验证任意旧清单的字段迁移。
kubeadm 首次预检发现 kind 容器内没有宿主内核的配置文件,因此失败。确认 zj 的 /boot 存在与实际运行内核匹配的配置后,脚本把该配置提供给本轮节点,再次执行预检;没有忽略 SystemVerification。这里验证的是容器节点环境的准备条件,不应让读者通过绕过预检照搬结果。
实际升级输出包含:
| |
在这条成功输出之后,API Server 已报告 v1.35.2,但三个节点的 kubelet 仍全部报告 v1.35.1。脚本保存了这个中间状态,随后逐节点完成维护、配置更新、二进制更新和 kubelet 重启。最终 API Server 与三个 kubelet 都为 v1.35.2,kube-proxy rollout 完成,两个实验副本重新 Ready。
kind 提供的是可抛弃的容器节点,本轮安装二进制的方法用于验证机制,不是生产节点包管理教程。跨小版本升级、etcd 升级、HA 滚动升级、插件兼容性和数据恢复都需要各自验证,不能用这次补丁升级的通过结果替代。
恢复方案要对应已经改变的状态
升级失败时,先确认停在哪一阶段:预检失败通常还没有替换控制面 manifest;控制面部分升级时,各组件可能已经处于不同版本;节点升级后,配置文件和二进制又可能分别处于新旧状态。只看最后一条报错,容易遗漏前面已经成功的动作。
恢复路径应对应这些实际变化。某个插件的新配置不兼容时,可能只需恢复插件配置;单个节点升级失败时,可以保留其他健康节点,修复或替换该节点。若需要从旧 etcd 快照恢复,就涉及对象状态回退和控制器重新协调,不能把它当作一个“撤销软件更新”的按钮;业务卷和外部资源也不会随 API 元数据自动回到同一时刻。
因此,升级完成的判断应落到实际能力上:能够读写 API、完成调度与调谐、在目标节点启动工作负载,并满足应用自己的连接、存储和恢复要求。版本号对齐是其中一项证据,业务验证仍需覆盖这次升级真正改变的路径。