本文来自一次真实节点迁移。主机名、地址、账号和安装源均已替换为占位符;不要把示例中的网络与认证配置直接用于生产。

一台物理机重装 Rocky Linux、安装 containerd 和 kubeadm join 后,表面上一切正常:系统能登录、容器运行时已启动、证书也已下发。但节点始终不是 Ready。

真正的问题不在 CNI,也不是 join token 失效,而是重启后系统自动启用了 swap。Kubernetes 1.36 的 kubelet 默认不允许带 swap 运行,因此它拒绝正常工作。

重装前先留下可复核的节点基线

重装或纳管前,先确认目标机器、磁盘与网络都没有认错。尤其是带多块盘的物理机,/dev/sda、NVMe 名称和挂载点不能只靠经验判断。

hostnamectl
ip -br address
ip route
lsblk -o NAME,SIZE,FSTYPE,MOUNTPOINTS,MODEL,SERIAL
findmnt -rno SOURCE,TARGET,FSTYPE /
timedatectl status

# 保存旧节点的集群侧事实,便于重装后还原标签与污点。
kubectl get node <node-name> --show-labels
kubectl describe node <node-name> | sed -n '/Taints:/,/Unschedulable:/p'

真正执行 kubeadm reset、清盘或重装前,先 kubectl drain 并确认业务有可承接副本。单副本数据库、local PV 或未迁走的构建缓存不能直接 drain:

kubectl cordon <node-name>
kubectl drain <node-name> --ignore-daemonsets --delete-emptydir-data \
  --pod-selector='app.kubernetes.io/part-of=<migrated-workload>'

上面故意使用了 selector。不要在未确认 local PV、单副本服务和业务窗口之前,对整台节点直接执行带 --force 的全量 drain。

先看 kubelet,而不是先重装

kubectl get node <node-name>
ssh <node-name> 'systemctl --no-pager status kubelet'
ssh <node-name> 'journalctl -u kubelet -b --no-pager | tail -80'

典型证据类似:

running with swap on is not supported

接着确认 swap 的真实来源:

swapon --show
free -h
grep -n swap /etc/fstab || true
systemctl list-units --type=swap --all

这里最容易踩坑:/etc/fstab 已经注释掉 swap,并不代表重启后不会再出现。使用 GPT 自动发现的系统可能由 systemd 在启动时识别 swap 分区并重新激活。

同时确认 kubelet 和 containerd 的 cgroup 驱动一致。这个问题和 swap 一样,常被误判为 CNI 或 join 失败:

sudo grep -n 'SystemdCgroup' /etc/containerd/config.toml
sudo grep -n 'cgroupDriver' /var/lib/kubelet/config.yaml
sudo systemctl is-active containerd kubelet

修复分两层:立即生效和重启后持续生效

先让当前节点恢复:

sudo swapoff -a
sudo systemctl restart kubelet

再禁止内核启动阶段重新发现 swap:

sudo grubby --update-kernel=ALL --args='systemd.swap=0'
sudo reboot

重启后必须重新验收,不能只相信命令返回成功:

swapon --show                 # 应无输出
systemctl is-active kubelet   # 应为 active
kubectl get node <node-name>  # 应为 Ready

若 swapon --show 重启后仍有输出,进一步查出是谁启用了该设备,而不是重复执行 swapoff:

systemctl list-unit-files | grep -i swap || true
systemctl status dev-disk-by\x2duuid-<swap-uuid>.swap --no-pager || true
lsblk -f
cat /proc/cmdline

确认内核参数已经写入后,可用下面命令查看全部启动项,不要只看当前内核:

grubby --info=ALL | grep -E 'index=|args=.*systemd.swap'

加入集群前的最小节点清单

把下面检查固化成初始化脚本,比故障后逐台排查可靠得多:

swapoff -a
sed -ri '/\sswap\s/s/^#?/#/' /etc/fstab
grubby --update-kernel=ALL --args='systemd.swap=0'
systemctl enable --now containerd kubelet

此外还应确认 containerd 与 kubelet 都使用 systemd cgroup,并在节点真正 Ready 后再施加业务污点:

kubectl taint node <node-name> workload-pool=build:NoSchedule
kubectl label node <node-name> workload-pool=build

先打污点、后验证 kubelet,常会让调度失败和节点故障混在一起,增加排障成本。

Join、CNI 与调度的验收命令

Join token、CA hash 和 API Server 地址属于短期敏感信息,必须从受控终端生成和传递,不能写入文档、脚本仓库或 Shell 历史。节点侧命令形态如下:

sudo kubeadm join <api-server>:6443 \
  --token <redacted-token> \
  --discovery-token-ca-cert-hash sha256:<redacted-ca-hash>

# 由集群管理端观察节点从注册到 Ready 的全过程。
kubectl get node <node-name> -w
kubectl -n kube-system get pod -o wide \
  --field-selector spec.nodeName=<node-name>
kubectl -n kube-system get events --sort-by=.lastTimestamp | tail -50

只有 Node 为 Ready、该节点的 CNI 与 kube-proxy DaemonSet 都就绪后,才允许让业务 Pod 调度上去:

kubectl wait --for=condition=Ready node/<node-name> --timeout=10m
kubectl -n kube-system get ds -o wide
kubectl label node <node-name> workload-pool=build --overwrite
kubectl taint node <node-name> workload-pool=build:NoSchedule --overwrite

标签和污点中的 build 只是示例,应替换成已批准的节点池名称。若加入后需要验证调度,使用一个有资源上限、明确 toleration 的临时 Pod;验证完成立即删除。

这次复盘留下的规则

系统重装完成不等于 Kubernetes 节点可用。一个节点至少要依次通过:操作系统与网络、containerd、kubelet、CNI、Node Ready、目标 DaemonSet、污点与标签、真实 Pod 调度。任何一层没验证,都不应把它计入可用容量。

可安全回退的边界

节点无法就绪时,先 cordon 保持其不接收新工作负载,再采集 kubelet、containerd、CNI 日志。不要通过删除 Node 对象来“修好”节点;这只能移除集群记录,不能修复磁盘、swap 或网络。

kubectl cordon <node-name>
sudo journalctl -u kubelet -b --no-pager | tail -200
sudo journalctl -u containerd -b --no-pager | tail -200
kubectl -n kube-system describe pod <cni-pod-on-node>

修复后重新运行本文的 swap、cgroup、Node Ready 与 DaemonSet 验收命令;只有所有检查通过,才 uncordon 并恢复该节点池的工作负载。