开始学习

Stage 04 · 生产 · 第 7 / 8

Day-2 运维:升级、扩容与 etcd 维护

版本升级评估、节点增删、控制平面扩容、etcd 备份与碎片整理。

50 分钟|全程第 30 / 37

集群部署上线是 Day-1,之后的几年都是 Day-2:Kubernetes 每 4 个月左右发布一个小版本,每个版本只维护约 14 个月,不升级就会失去安全修复;业务增长要加节点,硬件故障要下节点;etcd 的数据库在不停长大;证书默认一年过期。这些事每一件都不难,但每一件做错都可能让整个集群停摆。

这一课以 Kubespray 部署的集群为例,讲清楚:升级前怎样做影响评估、版本能不能跳、升级命令怎么跑;节点维护时 cordon/drain 和 PodDisruptionBudget 怎么配合;工作节点和控制平面节点的增删;etcd 的备份、恢复、压缩和碎片整理;证书续期。

Day-2 操作全景

操作频率工具主要风险
小版本升级每 4~12 个月Kubespray upgrade-cluster.yml破坏性变更、etcd 大版本、组件不兼容
节点维护(内核、驱动、硬件)随时kubectl drain + PDB服务中断、drain 卡住
扩容工作节点按需scale.yml新节点配置漂移
下线节点按需remove-node.yml有状态数据未迁移
扩容控制平面少见cluster.yml / recover-control-plane.ymletcd 成员变更失败
etcd 备份每天etcdctl snapshot save备份不可用却没人发现
etcd 压缩与碎片整理按告警etcdctl compact / defrag整理期间成员阻塞
证书续期每年(或自动)kubeadm certs renew忘记续期导致集群不可用

升级

版本跳跃规则

升级前先搞清楚"能不能一步到位"。三层规则同时生效:

  1. Kubernetes 本身不能跳小版本。控制平面必须 1.33 → 1.34 → 1.35 逐个升级。版本偏差策略(Version Skew Policy)规定 kubelet 最多可以比 apiserver 旧 3 个小版本,但不能比它新,所以永远先升控制平面,再升工作节点
  2. Kubespray 不能跳版本。官方要求按 release tag 逐个升级,例如 2.29 → 2.30 → 2.31,每个 Kubespray 版本只保证从上一个版本升级过来的路径。
  3. 组件自己的升级路径。最典型的是 etcd:3.5 升 3.6 要求所有成员先到 3.5.26 及以上。
text
Kubespray 2.29 ──▶ 2.30 ──────────▶ 2.31
K8s        1.33 ──▶ 1.34 ──────────▶ 1.35
etcd       3.5.x ─▶ 3.5.26(必须停留)─▶ 3.6.10(大版本,回退需按官方流程)

升级影响评估

每次升级前,团队都会写一份影响评估:把 Kubespray 的发布说明、Kubernetes 的 CHANGELOG 和各组件的发布说明读一遍,按"对我们集群的影响"排序。以下是 2.30 → 2.31(Kubernetes 1.34 → 1.35)评估的核心结论,可以当作模板:

变更项影响应对
cgroup v1 默认拒绝启动(K8s 1.35 FailCgroupV1=true最高:cgroup v1 节点 kubelet 直接起不来逐节点确认 stat -fc %T /sys/fs/cgroup 输出 cgroup2fs
ingress-nginx addon 被移除(上游项目已归档)规划迁移到 Gateway API、Istio 或其他控制器
etcd 3.5 → 3.6 大版本分两步升级,升级前必须快照备份
Dashboard addon 移除改用 Headlamp
移除 kubelet --pod-infra-container-image 标志检查是否手动加过
IPVS kube-proxy 模式废弃kube_proxy_mode: ipvs 的集群评估迁移到 nftables
StorageVersionMigration v1alpha1 移除升级前删除该类资源,否则升级阻塞
变量重命名 ssh_bastion_confing__namessh_bastion_config_name覆盖过旧变量的,改名,否则静默失效

cgroup v1 这一条值得展开,它是"升级后节点全挂"级别的风险:

bash
# 1. 确认 cgroup 版本:cgroup2fs 为 v2(正常),tmpfs 为 v1
stat -fc %T /sys/fs/cgroup

# 2. 是否曾被手动切回 v1
grep -rE "unified_cgroup_hierarchy" /etc/default/grub /etc/default/grub.d/*.cfg 2>/dev/null

Ubuntu 22.04/24.04、RHEL 9 默认都是 cgroup v2,RHEL 8 默认 v1。另外 cgroup v2 下还要确认工作负载兼容:OpenJDK 需要 8u372+/11.0.16+/17+,Node.js 需要 20.3.0+,否则程序读不到正确的内存限制;cgroup v2 的 OOM 默认杀整个 cgroup,而不是单个进程。

执行升级

升级前的准备:

  1. 备份 etcd(见后文),并把快照拷到集群外。
  2. 下载新版本 Kubespray,把旧 inventory 拷过去,用 diff 对比新版本 inventory/sample/group_vars 的变化,合并新增和改名的变量。
  3. 在同样拓扑的测试集群上先跑一遍。

升级命令:

bash
source .venv/bin/activate

# 整体升级(Kubespray 内部会先升 etcd 和控制平面,再分批升工作节点)
ansible-playbook -i inventory/mycluster/inventory.ini -b upgrade-cluster.yml \
  -e kube_version=1.35.4

生产上更推荐分阶段执行,每一步确认健康再继续:

bash
# 第一步:只升 etcd 和控制平面
ansible-playbook -i inventory/mycluster/inventory.ini -b upgrade-cluster.yml \
  --limit "kube_control_plane:etcd"

# 第二步:分批升工作节点,例如先升一批非关键节点
ansible-playbook -i inventory/mycluster/inventory.ini -b upgrade-cluster.yml \
  --limit "gn-192-168-1-1*:gn-192-168-1-2*"

几个控制升级节奏的变量:

变量作用
serial工作节点每批并行升级的数量或比例,默认 20%,生产可设为 1
drain_nodes升级前是否 drain 节点,默认开启
drain_timeout / drain_grace_perioddrain 的超时与 Pod 优雅终止时间
upgrade_node_confirm每个节点升级前等待人工确认
upgrade_node_pause_seconds每个节点升级后暂停若干秒,便于观察

升级完成后逐项验证:kubectl get nodes 版本一致且 Ready;kube-system 下 Pod 全部 Running;etcd endpoint health --cluster 正常;监控面板上 apiserver 错误率和延迟没有异常。

节点维护:cordon、drain 与 PDB

给节点升级内核、换 GPU 驱动、修硬件,都要先把上面的 Pod 安全挪走:

bash
# 标记不可调度,已有 Pod 不受影响
kubectl cordon gn-192-168-1-11

# 驱逐 Pod:DaemonSet 的 Pod 驱逐了也会马上回来,所以跳过;
# 使用 emptyDir 的 Pod 数据会丢,需要显式确认
kubectl drain gn-192-168-1-11 --ignore-daemonsets --delete-emptydir-data --timeout=10m

# ……维护……

# 恢复调度
kubectl uncordon gn-192-168-1-11

drain 用的是 Eviction API,会尊重 PodDisruptionBudget(PDB):PDB 声明"这个应用在自愿中断时至少保留多少个可用副本",驱逐会让它低于这个值时,驱逐请求被拒绝,drain 就会等待重试。

web-pdb.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: web
spec:
  minAvailable: 2          # 或 maxUnavailable: 1
  selector:
    matchLabels:
      app: web

扩容与缩容

添加工作节点

  1. 新节点按 生产集群规划 的规范完成 OS 初始化,确保安装节点能免密 SSH。
  2. 把节点加入 inventory.ini[kube_node] 组。
  3. 先刷新所有节点的 facts,再只对新节点执行 scale.yml
bash
ansible-playbook -i inventory/mycluster/inventory.ini -b playbooks/facts.yml
ansible-playbook -i inventory/mycluster/inventory.ini -b scale.yml \
  --limit=gn-192-168-1-21,gn-192-168-1-22

先跑 facts.yml 是因为 --limit 只会收集被限定节点的 facts,而配置模板(例如 nginx-proxy、/etc/hosts)需要引用其他节点的信息。

下线节点

bash
# 正常下线:会 drain、停止服务、清理节点并从集群删除
ansible-playbook -i inventory/mycluster/inventory.ini -b remove-node.yml \
  -e node=gn-192-168-1-21

# 节点已经宕机无法 SSH:跳过 reset,强制移除
ansible-playbook -i inventory/mycluster/inventory.ini -b remove-node.yml \
  -e node=gn-192-168-1-21 -e reset_nodes=false -e allow_ungraceful_removal=true

执行成功后再把节点从 inventory.ini 删掉。顺序反了,playbook 就找不到要删的节点。

扩容控制平面

控制平面扩容涉及 etcd 成员变更,是风险最高的扩容操作。团队曾把一个单控制平面集群(Kubespray 2.28)扩成 3 节点 HA,采用的是官方文档"恢复控制平面"的思路:把新节点当作"坏掉的节点"加入 broken_* 组,让 playbook 以恢复的逻辑安全地添加 etcd 成员。

inventory.ini(扩容期间)
[kube_control_plane]
mn-192-168-3-21 ansible_host=192.168.3.21 etcd_member_name=etcd1
mn-192-168-3-22 ansible_host=192.168.3.22 etcd_member_name=etcd2
mn-192-168-3-23 ansible_host=192.168.3.23 etcd_member_name=etcd3

[etcd:children]
kube_control_plane

[broken_etcd]
mn-192-168-3-22 ansible_host=192.168.3.22 etcd_member_name=etcd2
mn-192-168-3-23 ansible_host=192.168.3.23 etcd_member_name=etcd3

[broken_kube_control_plane]
mn-192-168-3-22 ansible_host=192.168.3.22
mn-192-168-3-23 ansible_host=192.168.3.23
bash
ansible-playbook -i inventory/mycluster/inventory.ini -b playbooks/facts.yml
ansible-playbook -i inventory/mycluster/inventory.ini -b recover-control-plane.yml \
  --limit=etcd,kube_control_plane -e etcd_retries=10

成功后立即删除 broken_* 两个组,避免以后误触发恢复逻辑;然后用 etcdctl member list 确认 3 个成员都是 started

etcd 维护

etcd 保存了集群的全部状态,丢了 etcd 就等于丢了集群。Kubespray 默认以 systemd 服务(etcd_deployment_type: host)运行 etcd,连接参数写在 /etc/etcd.env,下面的命令都先加载它:

bash
set -a && source /etc/etcd.env && set +a   # 导出 ETCDCTL_ENDPOINTS / CACERT / CERT / KEY
etcdctl endpoint status --cluster -w table
etcdctl endpoint health --cluster

快照备份

bash
etcdctl snapshot save /backup/etcd-$(date +%Y%m%d-%H%M).db
etcdutl snapshot status /backup/etcd-20260923-0300.db -w table

快照只需在一个健康成员上做。生产要求:

  • 每天定时备份(systemd timer 或 CronJob),拷贝到集群外的存储,保留多份。
  • 每次升级、控制平面变更前额外备份一次。
  • 定期在测试环境做恢复演练。没恢复过的备份不算备份。

从快照恢复

恢复是灾难场景下的最后手段,会把整个集群状态回退到快照时刻。大致流程:

  1. 停掉所有控制平面节点的 kube-apiserver 和所有 etcd 成员。
  2. 每个成员上用同一份快照恢复出新的数据目录:
    bash
    etcdutl snapshot restore /backup/etcd.db \
      --name etcd1 \
      --initial-cluster etcd1=https://192.168.3.21:2380,etcd2=https://192.168.3.22:2380,etcd3=https://192.168.3.23:2380 \
      --initial-advertise-peer-urls https://192.168.3.21:2380 \
      --data-dir /var/lib/etcd-restore
  3. 把原数据目录挪走备份,用新目录替换,启动 etcd,确认集群健康后再启动 apiserver。

Kubespray 在 docs/operations/recover-control-plane.md 中也提供了基于 playbook 的恢复流程,适合部分控制平面节点损坏、仍有健康成员的场景。

压缩、碎片整理与配额

etcd 用 MVCC 保存每个 key 的历史版本,每次对象更新都产生新的 revision。旧版本需要**压缩(compact)才能被标记为可回收,而压缩后的空闲空间还要经过碎片整理(defrag)**才会真正从数据库文件里还给磁盘。

后端数据库有配额 --quota-backend-bytes,默认 2GB,官方建议不超过 8GB。超过配额后 etcd 触发 NOSPACE 告警,进入只读和删除模式,拒绝一切写入。

几点补充:

  • kube-apiserver 默认每 5 分钟自动 compact 一次(--etcd-compaction-interval),所以日常数据库变大通常是碎片而不是历史版本堆积,定期 defrag 比手动 compact 更重要。
  • defrag 期间该成员会阻塞读写。逐个成员执行,先整理 follower,最后整理 leader,业务低峰期操作。
  • 数据库异常增长往往有源头:频繁更新的大对象(巨大的 ConfigMap、CRD 状态)、失控的控制器反复写入、海量 Event。只做 defrag 治标不治本,要结合 可观测性 里的 etcd 容量告警尽早发现。

证书续期

kubeadm 签发的组件证书(apiserver、controller-manager、scheduler 的客户端证书等)有效期 1 年,CA 证书 10 年。证书过期后 kubectl 报 x509: certificate has expired,组件之间无法通信。

bash
# 在控制平面节点上查看到期时间
kubeadm certs check-expiration

# 手动续期全部证书,之后需要重启控制平面静态 Pod 使其加载新证书
kubeadm certs renew all

Kubespray 设置 auto_renew_certificates: true 后,会在控制平面节点上安装一个 systemd 定时器(默认每月第一个周一凌晨),自动续期证书并重启控制平面组件。可以用 systemctl list-timers | grep k8s-certs-renew 确认。

动手练习

以下练习使用一个 1 控制平面 + 2 工作节点的 kind 集群。

  1. PDB 与 drain:部署 3 副本的 nginx Deployment,创建 minAvailable: 3 的 PDB,然后 kubectl drain kind-worker --ignore-daemonsets --timeout=60s,观察 drain 为什么失败。把 PDB 改成 minAvailable: 2 再试,最后 uncordon
  2. etcd 快照:kind 的 etcd 以静态 Pod 运行,证书在 /etc/kubernetes/pki/etcd/
    bash
    ectl() {
      kubectl -n kube-system exec etcd-kind-control-plane -- 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 "$@"
    }
    ectl endpoint status -w table
    ectl snapshot save /var/lib/etcd/snap.db
    kubectl -n kube-system exec etcd-kind-control-plane -- etcdutl snapshot status /var/lib/etcd/snap.db -w table
    docker exec kind-control-plane ls -lh /var/lib/etcd/snap.db
  3. 碎片整理:用一个循环创建再删除 500 个 ConfigMap,前后对比 endpoint status 里的 DB SIZE;执行 ectl defrag 后再看一次,理解 compact 和 defrag 分别解决什么问题。
  4. 证书检查:执行 docker exec kind-control-plane kubeadm certs check-expiration,找出最早过期的证书和 CA 的有效期。
  5. 写一份升级评估:假设你的集群要从当前 Kubernetes 版本升级到下一个小版本,阅读官方发布博客的"弃用与移除"部分,按"变更 / 是否命中 / 检查命令 / 处理方式"四列写出至少 3 条。

自测

集群当前是 Kubernetes 1.33,能否直接升级到 1.35?Kubespray 2.29 能否直接升级到 2.31?

都不能。Kubernetes 控制平面必须逐个小版本升级(1.33 → 1.34 → 1.35);Kubespray 也要求按 release 逐个升级(2.29 → 2.30 → 2.31)。另外还要关注组件自身的路径,例如 etcd 3.5 升 3.6 需先到 3.5.26+。

为什么升级时要先升控制平面,再升工作节点?

版本偏差策略规定 kubelet 不能比 kube-apiserver 新,但可以旧最多 3 个小版本。先升控制平面,整个过程中始终满足"apiserver ≥ kubelet"的约束。

kubectl drain 一直卡住并反复打印 "Cannot evict pod as it would violate the pod's disruption budget",该怎么排查?

kubectl get pdb -A 找到对应 PDB,看 ALLOWED DISRUPTIONS 是否为 0。常见原因:minAvailable 等于副本数、单副本应用配了 PDB、应用本身有 Pod 不健康导致可用数已经不够。修正 PDB 或先修复不健康的 Pod,而不是用 --disable-eviction 强行绕过。

etcd 报 database space exceeded 后,compact、defrag、alarm disarm 各起什么作用?顺序能不能换?

compact 丢弃指定 revision 之前的历史版本(集群级,执行一次);defrag 把空闲空间真正还给磁盘,缩小数据库文件(按成员逐个执行);alarm disarm 解除 NOSPACE 告警恢复写入。必须先释放空间再解除告警,否则数据库仍超配额,很快会再次触发告警。

用 remove-node.yml 下线一台已经断电的节点,需要加什么参数?inventory 什么时候修改?

-e reset_nodes=false -e allow_ungraceful_removal=true,跳过对该节点的 SSH 清理。playbook 执行成功后,再把该节点从 inventory 中删除。

参考资料

学完了吗?

进度保存在本机浏览器中,可在学习路线页查看。