次版本要一级一级地升
一句话总结
Kubernetes 升级不是“安装最新版本”,而是把次版本一格一格地往上升,并把组件之间的版本偏差(skew)控制在允许范围之内。k3s 也不例外。
为什么需要它
k3s 只需一行命令就能安装完成,所以很容易以为升级也只要一行。重新运行安装脚本,就会下载新的二进制文件并重新启动服务。于是人们常常这样做:“现在 stable 通道是什么版本?就升到它吧。”
这里有一个陷阱。通道指向的只是那一刻的最新推荐版本,它并不知道你的集群现在是什么版本。如果把一直停留在 1.34 的集群升级到 stable 通道,而在 stable 指向 1.36 的那天,就会一次跳过两个次版本。安装脚本不会阻止这一点。
然而 Kubernetes 项目的策略很明确。版本偏差策略写道,kube-apiserver 升级时不能跳过次版本,即使集群只有一个实例也一样。k3s 的手动升级文档也说适用同样的策略,并要求不要跳过中间的次版本。原因在于,存储中对象的转换、已弃用 API 的移除、控制器行为的变化,只会按一个版本为单位进行测试。如果一次跳过两格,你就成了第一个走上那条无人测试过的路径的人。
工作原理
版本差异的允许范围
skew 策略为每个组件规定了“可以与 API 服务器相差几个版本”。核心是任何组件都不能比 API 服务器更新。
구성요소 API 서버가 1.35 일 때 허용되는 판
kube-apiserver (HA) 서로 한 마이너 이내
controller-manager·scheduler 1.35, 1.34 (한 판 낮게까지)
kubelet·kube-proxy 1.35, 1.34, 1.33, 1.32 (세 판 낮게까지)
kubectl 1.36, 1.35, 1.34 (위아래 한 판)
该代码块中的韩文说明依次为:当 API 服务器为 1.35 时各组件所允许的版本。多个 kube-apiserver(HA)之间相差不超过一个次版本;controller-manager 和 scheduler 可以是 1.35、1.34(最多低一个版本);kubelet 和 kube-proxy 可以是 1.35、1.34、1.33、1.32(最多低三个版本);kubectl 可以是 1.36、1.35、1.34(上下各一个版本)。
因此,升级的顺序也就确定了:先升级控制平面(API 服务器),后升级 kubelet。如果反过来,就会出现 kubelet 比 API 服务器更新的时刻。这就是 k3s 文档要求按先逐台升级服务器节点,然后再升级 agent 节点的顺序进行的原因。另外,由于 kubelet 最多可以落后三个版本,在把控制平面一格一格地升几次的过程中,工作节点不必每次都跟着升级。
k3s 中实际发生的事
k3s 的 API 服务器、kubelet 和控制器是同一个二进制文件。用 INSTALL_K3S_VERSION 固定版本并重新运行安装脚本,就会下载该版本的二进制文件并重启服务。在单个节点上,控制平面和 kubelet 会一起升级,所以 skew 只会出现在节点之间。
有两点需要注意。
第一,安装时通过环境变量或参数给出的配置,重新运行时如果不再次给出就会丢失。 这是 k3s 文档明确写出的行为。因此,把配置放在 /etc/rancher/k3s/config.yaml 中更安全,因为它与安装脚本无关,会一直保留。
第二,即使停止 k3s,Pod 的容器也会继续运行。 所以即使 API 短暂中断,服务通常也还活着;但如果是在 API 中断期间会出问题的工作负载,文档建议先 drain。
这里也记录一下实测中看到的情况。在与本实验相同的 VM 上,用安装脚本把 v1.34.11+k3s1 升级到 v1.35.8+k3s1 用了 11 秒,预先启动的两个 nginx Pod,其 Pod UID 和容器 ID 都没有变化,重启次数也是 0。而 kubeadm 升级文档写道,容器规格哈希会发生变化,升级之后所有容器都会重启,并且在升级次版本的 kubelet 之前必须先 drain。同样是“Kubernetes 升级”,工作负载所经历的事情会因发行版和版本组合而不同,所以不要把文档中的一句话当作假设,而要亲自测量。
drain 能做什么,不能做什么
kubectl drain 会先对节点执行 cordon(禁止调度新 Pod),然后通过 eviction API 把 Pod 驱逐出去。eviction 会遵守 PodDisruptionBudget。违反预算的 eviction 会被拒绝,而 drain 会不断重试被拒绝的 Pod。
这意味着如果没有可去的地方,drain 就不会结束。如果只有一个节点,被驱逐的 Pod 的替代 Pod 无法调度到已经 cordon 的那个节点上,会变成 Pending,就绪的 Pod 数量降到预算以下,下一次 eviction 就被阻止。实测中,drain 每隔 5 秒重复八次 Cannot evict pod as it would violate the pod's disruption budget,最后因触及 --timeout=40s 而结束。在本实验中你将亲眼看到这一幕。
备份与已弃用 API 检查
k3s 的默认存储是 SQLite。备份与恢复文档要求,除了 /var/lib/rancher/k3s/server/db/ 之外,一定要同时保管令牌文件(/var/lib/rancher/k3s/server/token)。令牌用于加密存储中的机密数据,所以只有 DB 而没有令牌,就无法恢复。
已弃用的 API,要在 API 迁移指南中按版本确认哪些会被移除,再通过 API 服务器的 apiserver_requested_deprecated_apis 指标确认实际上是谁在调用这些 API。只看文档,会让人相信“我们没有用到”;而看指标,就会发现旧的 Helm Chart 或脚本仍在调用旧版本。
在现场相遇的样子
常见的情况是,分散在边缘的几十台 k3s 被搁置了一段时间,又因为安全公告而仓促升级。这时如果想着“反正都要升到最新”,就把通道设为 stable,由于每个节点的起始版本不同,有的节点跳过一格,有的节点跳过三格。即使升级成功了,依赖旧 API 创建的对象或已被移除功能的工作负载,也会在之后悄无声息地出问题。
自动化时使用 Rancher 的 system-upgrade-controller。在 Plan 对象中写入目标版本(version)或通道(channel),并通过 concurrency、cordon、nodeSelector 决定一次升级几台、怎么升级。这里如果设置通道,也会出现同样的陷阱。而且实测发现,Plan CRD(v0.20.1)在服务器端校验时,会原样接受既没有 version 也没有 channel 的 Plan。应用成功,并不意味着版本是对的。固定版本,并一格一格地修改 Plan,才符合 skew 策略。
实际工作中真正重要的事
- 先记录起始版本。 升级之后,在任何地方都再也看不到之前的版本了。必须记录版本、节点 UID 和工作负载 UID,才能证明“同一个集群升级了”。
- 目标版本要固定为确切的版本,而不是通道。 用数字确认次版本只相差一格。
- 备份要同时备份 DB 和令牌。 缺少令牌的备份无法恢复。
- drain 只有在有地方可去时才会结束。 如果只有一个节点,drain 被 PDB 阻止是正常的,此时要结合 k3s 会让容器继续存活的特性以及 API 中断的影响来做决定。
- 升级之后要与记录对照。 要确认节点 UID 是否相同、工作负载是否以同一个对象继续存活、配置(例如关闭的 traefik)是否保持,这样才算结束。
下一项实验要做什么
从 VM 中一台 k3s 1.34 开始。记录起始版本,部署带有 PDB 的工作负载,然后通过已弃用 API 指标和 SQLite 备份做好准备。观察 drain 为什么在单节点上会停住,用安装脚本向上升一格到 1.35,再对照工作负载和节点是否以同一个对象继续存活。最后为下一格编写 system-upgrade-controller Plan,并计算如果一直跟随 stable 通道,会跳过几格。