从上个月开始,我负责的这套 K8S 1.28 集群就开始不断出现节点 NotReady 告警。一开始我们怀疑是机房网络抖动,排了一圈又一圈,直到某个运维老哥在周会上甩出一句:“CentOS 7 已经 EOL 了,我们是不是该动了?”会议室瞬间安静。说实话,从 CentOS 7 迁移到 Rocky Linux 9.4 这个想法我们拖了大半年,总觉得不到最后一刻不用慌。但当你真正面对 K8S 1.28 集群里几十个节点、一堆本地数据盘、还有跑在 GPU 节点上的训练任务时,就会发现这不是“重装个系统再 kubeadm join”那么简单。
这套集群承载了线上核心 API、日志采集、部分 AI 推理服务,业务方只关心别停,不关心你用的是什么发行版。于是我花了两个月时间,用滚动替换的方式把 K8S 1.28 集群从 CentOS 7 平滑迁到了 Rocky Linux 9.4,整个过程中业务没有出现过超过 5 分钟的连续中断。这篇文档不是什么官方迁移教程,而是我在实际操作中整理出来的完整实施手册,适合和我一样需要“在不重建集群、不迁移业务的前提下更换节点操作系统”的团队参考。
1. 迁移前要搞清的三个问题:风险、兼容性、目标状态
网上很多文章会把“系统迁移”写成一堆命令的堆砌,但真正动手前,必须先把三个问题想清楚:为什么要迁、用什么方案迁、迁完之后要得到什么。这三个问题没捋明白,后续每一步都会踩坑。
1.1 CentOS 7 的 EOL 到底影响了什么
CentOS 7 停止维护后,最直接的问题就是 yum 源和漏洞补丁不再更新。尤其是 K8S 1.28 这种对 kubelet、containerd 运行时有较新依赖的版本,底层操作系统如果长期停留在 3.10 内核,很多新特性根本用不上。比如 cgroups v2、io_uring、更完善的 nftables 机制,这些在 CentOS 7 上要么不支持,要么需要额外打补丁。
Rocky Linux 9.4 作为 RHEL 9 的兼容发行版,内核版本通常在 5.14 以上,且默认启用 cgroups v2。Kubernetes 从 1.25 开始就已经支持 cgroups v2,1.28 版本在 Rocky 9.4 上运行是完全没有问题的。但要注意,容器运行时如果还用 Docker 的话,需要评估 cri-dockerd 的兼容性,我这边一开始就是 containerd,反而省了运行时迁移这一步。
1.2 迁移方案的取舍:原地升级、新建集群、滚动替换
我在社区里看到不少人推荐“原地升级”,也就是在 CentOS 7 节点上直接替换 yum 源,然后通过升级安装包的方式把系统变成 Rocky Linux。这个思路听起来省事,但实际上风险极高。CentOS 7 和 Rocky Linux 9.4 的 glibc、systemd、内核模块加载机制差异太大,跨大版本原地升级非常容易把 kubelet、containerd 甚至 SSH 服务搞挂。一旦中途失败,系统处于半坏状态,回滚成本极高。
新建集群再迁移业务是很多正规企业会选的方式,但前提是业务允许重新发布、重新挂载存储,而且你有充足的时间做数据同步。我这边业务方明确要求不能重建 Service 和 PVC,所以这条路也被否掉了。
最终我选择的是“滚动替换节点”方案:保持现有控制面和 API Server 不变,新装一批 Rocky Linux 9.4 节点,通过 kubeadm 方式加入现有集群,逐批把旧节点上的业务排空后下线。这样集群的 etcd 数据、Service、PVC 全部得以保留,只是承载这些对象的节点发生了替换。整个过程中,集群视角始终存在,不会出现“集群整体不可用”的窗口。
1.3 迁移前必须摸清的盘点清单
这个环节很多人会省略,但恰恰是迁移成败的关键。你需要对集群现有状态做一次彻底盘点,把这四类信息记录在案:
- 控制平面架构:是单 Master 还是高可用?etcd 是内嵌还是外部部署?如果内嵌 etcd,节点替换时要额外处理 etcd member 的增删。
- CNI 类型及 CIDR:Calico 还是 Flannel?Pod CIDR、Service CIDR、IPIP 模式还是 VXLAN 模式?
- 存储方案:用了 Local PV 还是 NFS?如果是 Local PV,节点替换后原节点数据无法跟随,必须在迁移前确认哪些 PVC 可以重建,哪些必须提前备份。
- 工作负载分布:哪些节点有专用标签、污点或节点亲和性?GPU 节点有多少个业务 Pod 在跑?
我当时的排查命令大概是这样的:
bash复制# 查看所有节点及角色
kubectl get nodes -o wide
# 查看节点上的标签和污点
kubectl get nodes --show-labels
kubectl get node <node-name> -o yaml | grep -A5 taints
# 查看 CNI 配置
kubectl get pods -n kube-system -o wide | grep -i calico
# 或者
kubectl get ds -n kube-system calico-node -o yaml
# 查看 StorageClass
kubectl get sc
这些信息在后面滚动替换的时候会反复用到。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 迁移前的备份与红线的设立
备份做得好不好,决定了你出问题时是“回滚”还是“回天乏术”。K8S 集群迁移过程中,最核心的数据是 etcd,其次是 PVC 数据、kubeadm 生成的配置、以及自定义的 CRD 资源。
2.1 etcd 快照:唯一能救命的底牌
不管集群规模多大,迁移前一定要做 etcd 快照。K8S 1.28 如果用 kubeadm 部署的集群,etcd 通常以静态 Pod 方式运行在控制平面节点上,直接进入 etcd 容器执行快照命令即可:
bash复制# 进入 etcd 容器
kubectl exec -n kube-system etcd-master01 -- sh -c \
"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/lib/etcd/snapshot-$(date +%F).db"
# 将快照拷贝到迁移服务器或对象存储
kubectl cp kube-system/etcd-master01:/var/lib/etcd/snapshot-2025-01-20.db /backup/
快照文件不要只存在本地,建议同时上传到异地位置,比如内网备份服务器和对象存储各一份。我之前见过一个事故,etcd 快照和原集群在同一台存储上,存储坏了快照也跟着没了,那才是真正的灾难现场。
2.2 kubeadm 配置与 RBAC 资源备份
kubeadm 的配置文件和证书路径也需要提前备份。虽然滚动替换节点不会轻易改动控制面,但万一新节点加入时证书不匹配,就需要用原始 kubeadm-config 重新生成。
bash复制# 导出 kubeadm 配置
kubectl -n kube-system get configmap kubeadm-config -o yaml > kubeadm-config.yaml
# 备份控制平面的 pki 证书目录
tar czf kube-backup-$(date +%F).tar.gz /etc/kubernetes/pki
2.3 业务数据的备份策略
如果是无状态应用,迁移 Worker 节点只需要重新调度 Pod 即可,数据不涉及持久化。但如果有状态应用使用了 Local PV,情况就比较复杂。Local PV 的数据绑定在节点本地磁盘上,节点下线后数据无法自动迁移。所以在迁移前,我建议先把使用 Local PV 的工作负载梳理清楚,能改成 NFS/共享存储的就尽早改,不能改的要提前做数据备份,并在业务低峰期执行迁移。
操作上我会给这批 PVC 打上自定义注解,方便迁移时识别:
bash复制kubectl get pvc -A -o custom-columns=NAMESPACE:.metadata.namespace,NAME:.metadata.name,STORAGECLASS:.spec.storageClassName | grep local
3. Rocky Linux 9.4 节点初始化实操:每一个坑都值得细说
新节点的操作系统初始化是整个迁移的基石。很多人以为装完系统、配置好 IP、装上 kubeadm 和 containerd 就完事了,但实际跑起来你会发现,各种小细节会导致 kubelet 无法启动、CNI 插件异常、Pod 网络不通。
3.1 系统安装阶段最容易忽略的两个点
Rocky Linux 9.4 的 ISO 可以从国内镜像源下载,安装时记得选择“Server with GUI”以外的 Minimal 安装,避免多余服务增加攻击面。分区方面,我统一采用了 LVM 方案,给根目录留了 100GB,/var/lib/containerd 独立分区至少 200GB。如果节点需要承载大量镜像或容器日志,容量可以再放大,因为容器镜像占用空间往往比预期增长快。
第二个容易忽略的是网卡命名。CentOS 7 时代很多服务器网卡叫 eth0,Rocky Linux 9.4 默认使用一致网络设备命名,网卡名可能是 ens192、enp1s0 等。如果集群里某些监控或节点发现机制强行依赖 eth0,这个变化会让它们认不出新节点。我的做法是在安装完成后,通过 udev 规则把网卡名统一改成 eth0,保证与旧节点一致:
bash复制# 编辑 /etc/default/grub,在 GRUB_CMDLINE_LINUX 中追加 net.ifnames=0 biosdevname=0
GRUB_CMDLINE_LINUX="... net.ifnames=0 biosdevname=0"
# 重新生成 grub 配置
grub2-mkconfig -o /boot/grub2/grub.cfg
# 修改 /etc/sysconfig/network-scripts/ifcfg-eth0 或使用 NetworkManager 重新生成配置
reboot
3.2 内核模块与 sysctl 配置
K8S 1.28 节点需要加载 overlay、br_netfilter、nf_conntrack 等内核模块,这些在 CentOS 7 上通常已经存在,但在 Rocky 9.4 上如果没手动加载,kubelet 能起来,但网络通信会出各种诡异问题。我直接放到系统启动自动加载:
bash复制cat > /etc/modules-load.d/containerd.conf <<EOF
overlay
br_netfilter
nf_conntrack
EOF
systemctl enable --now systemd-modules-load.service
lsmod | grep -E "overlay|br_netfilter|nf_conntrack"
sysctl 方面,网桥转发和 IPv4 转发必须提前打开,否则 kubelet 启动后 Pod 内无法访问 Service:
bash复制cat > /etc/sysctl.d/99-kubernetes.conf <<EOF
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
net.ipv4.conf.all.rp_filter = 0
net.ipv4.conf.default.rp_filter = 0
vm.overcommit_memory = 1
vm.panic_on_oom = 0
fs.inotify.max_user_watches = 1048576
EOF
sysctl --system
3.3 用官方 yum 源安装 containerd 和 kubeadm
Rocky Linux 9.4 自带的网络源里有 containerd,但我更推荐使用 Docker 官方源或阿里云镜像源安装 containerd,这样版本可控,后续排障也更顺手。安装完后必须修改 containerd 默认配置,把 cgroup 驱动改成 systemd,否则 K8S 1.28 会一直报 kubelet 与运行时 cgroup 驱动不一致:
bash复制yum install -y yum-utils device-mapper-persistent-data lvm2
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
yum install -y containerd.io
mkdir -p /etc/containerd
containerd config default | sudo tee /etc/containerd/config.toml
# 修改 SystemdCgroup 为 true
sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
# 设置镜像加速和 pause 镜像地址(可选)
systemctl enable --now containerd
K8S 组件这里我只装三件套:kubelet、kubeadm、kubectl,版本锁定在 1.28.x。这里有个非常关键的点:如果安装源默认是最新版本,kubeadm join 时版本不匹配会导致加入失败。我采用了阿里云镜像源,同时加 exclude 防止意外升级:
bash复制cat > /etc/yum.repos.d/kubernetes.repo <<EOF
[kubernetes]
name=Kubernetes
baseurl=https://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64/
enabled=1
gpgcheck=0
EOF
yum install -y kubelet-1.28.2 kubeadm-1.28.2 kubectl-1.28.2
systemctl enable --now kubelet
注意这里的 repo 路径用的是 kubernetes-el7-x86_64,因为 K8S 官方针对 el7/el8 的包在 Rocky 9 上同样可用。如果你直接用 el9 的源,某些版本可能还没有同步,反而会找不到包。这一点网上很少有人提到,我刚开始也卡了很久。
3.4 节点时间同步与主机名规划
K8S 节点之间对时间偏差非常敏感,尤其是 etcd 和 kubelet 依赖时间一致性。Rocky Linux 9.4 默认安装了 chrony,配置内网 NTP server 后要确保服务正常启动。主机名规划建议沿用旧节点主机名加后缀的方式,比如 worker01-new、master01-rocky,这样在迁移过程中可以从节点名称一眼看出系统版本。
4. 控制平面节点滚动替换:etcd member 的增删比想象中繁琐
控制平面替换一定是整场迁移中风险最高的部分。如果控制平面架构是高可用模式,比如 3 个 Master,那么滚动替换的时候要保证任意时刻至少两个 Master 在线,否则 etcd 可能会因为失去 quorum 而进入只读状态。
4.1 新 Master 加入集群的手工操作
这里我先说结论:新 Master 不要直接用 kubeadm join,直接把旧 Master 的证书复制过去是最稳妥的方式。虽然 kubeadm 支持 --control-plane --certificate-key 自动同步证书,但在生产环境里,证书同步失败的概率不小,而且如果中途网络抖动,会导致新节点处于半加入状态,清理起来非常麻烦。
我的操作流程如下:
bash复制# 在旧 Master 上打包证书
tar czf control-plane-cert.tar.gz /etc/kubernetes/pki
# 将证书包拷贝到新 Master 对应目录
scp control-plane-cert.tar.gz rocky-master01:/tmp/
mkdir -p /etc/kubernetes/pki
tar xzf /tmp/control-plane-cert.tar.gz -C /etc/kubernetes/pki --strip-components=3
注意 strip-components 参数需要根据 tar 包里的路径层级调整,建议先 tar -tzf 看一下结构。证书放好后,再执行 kubeadm join:
bash复制kubeadm join <APIServer-VIP>:6443 --token xxx \
--discovery-token-ca-cert-hash sha256:xxx \
--control-plane
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
如果集群使用了 Keepalived + HAProxy 或云负载均衡,需要保证 APIServer-VIP 能指向新 Master,且 HAProxy 后端列表里把新 Master 加进来。大部分场景下 VIP 不变,所以这里无需修改 kubelet 配置。
4.2 验证新控制平面各组件状态
join 完成后,不要急着下线旧 Master,先验证新控制面的组件和 etcd 健康状态:
bash复制kubectl get nodes
kubectl get pods -n kube-system -o wide | grep -E "etcd|kube-apiserver|kube-controller|kube-scheduler"
# 在任意控制平面节点检查 etcd 健康
kubectl exec -n kube-system etcd-rocky-master01 -- sh -c \
"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 \
endpoint health --cluster"
如果新 Master 的 etcd 成员已经加入,且 KubeAPIServer 正常,就可以进行下一步。
4.3 剔除旧 Master:etcd member 清理不能忘
执行 kubectl drain 和 kubectl delete node old-master 只是把节点从 K8S 层面移除,但内嵌 etcd 的成员关系还残留在集群里。如果不手动移除旧 etcd member,集群里会出现一个状态为 unhealthy 的成员,时间长了会影响 etcd 健康检查和性能。
bash复制# 列出 etcd 成员
kubectl exec -n kube-system etcd-rocky-master01 -- sh -c \
"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 \
member list"
# 找到旧 Master 对应的 member ID,移除
kubectl exec -n kube-system etcd-rocky-master01 -- sh -c \
"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 \
member remove <OLD-MEMBER-ID>"
移除结束后,手动把旧 Master 上的 kubelet 和 etcd 停掉,然后清理网络接口和挂载点。如果有条件,旧 Master 保留 7 天再重装,方便随时回滚。
5. Worker 节点迁移:把业务压力从旧节点平稳卸到新节点
Worker 节点的滚动替换虽然不像控制平面那么吓人,但几十个节点逐个排空、新节点逐个加入,如果没做好节奏管理,很容易出现资源不足或者 Pod 长时间 Pending。
5.1 单批节点的消解策略:先加后减
我当时的策略是“先加后减”,也就是先加入一台 Rocky worker 节点,确认它能正常承载业务后,再排空一台旧节点。这样集群中可调度资源始终是增加的,不会因为迁移造成资源黑洞。
新 Worker 节点加入后,需要打上标签和污点,尽量让调度器按照业务需求分配 Pod:
bash复制kubectl label node worker01-rocky node-role.kubernetes.io/worker=
kubectl label node worker01-rocky infra=rockylinux9
# 如果某些业务要求节点亲和性,需要同步修改 deployment 或使用 nodeSelector
5.2 drain 旧节点:注意 PDB 和本地存储
排空节点是迁移中最容易出问题的操作。很多团队直接执行 kubectl drain node 然后干等,结果发现大量 Pod 被驱逐后无法在新节点启动,或者一直呈 Evicted 状态。
正确做法是先检查节点上有哪些工作负载,是否有 PodDisruptionBudget,是否使用了 EmptyDir 和 Local PV:
bash复制kubectl get nodes ${OLD_NODE} -o name | xargs -I {} kubectl get pods -A --field-selector spec.nodeName=${OLD_NODE}
然后执行带 --ignore-daemonsets 和 --delete-emptydir-data 的 drain 命令。这两个参数非常关键:DaemonSet 类 Pod 会被忽略,因为新节点会自动创建新的 DaemonSet Pod;EmptyDir 数据如果不删除,drain 会一直卡住,等待用户确认。
bash复制kubectl drain ${OLD_NODE} --ignore-daemonsets --delete-emptydir-data --force
如果个别 Pod 因为 PDB 限制迟迟不结束,可以通过 kubectl get pdb -A 检查,必要时临时调大 PDB 的 maxUnavailable。
5.3 业务断言与慢启动处理
drain 完成后,旧节点上的 Pod 会先变成 Terminating,再由调度器分配到新节点。这里有个细节很容易被忽略:如果业务 Pod 使用了节点亲和性且只绑定旧节点标签,迁移后它就永远调度不上来。
所以在上线新节点前,我统一检查了所有 Deployment 的 nodeSelector/affinity,把原来指向旧节点 IP 或者旧标签的规则改成泛化标签,或者恢复为所有节点可调度。有条件的话,可以在迁移期间临时放开某些业务 Pod 的调度限制,等迁移完成后再收紧策略。
5.4 旧 Worker 的清场操作
旧节点完成排空后,执行:
bash复制kubectl delete node ${OLD_NODE}
然后到旧节点上执行 kubeadm reset --force,清理 kubelet、containerd 的残留数据。如果这台机器后续不再使用,直接关停即可。如果还打算重装成 Rocky Linux 继续复用,需要把 /var/lib/containerd、/var/lib/kubelet、/etc/cni/net.d 这几个目录一并清干净,避免下次加入集群发生冲突。
6. 网络、存储、GPU 这些容易漏掉的检查点
系统层面迁移完成后,很多团队会直接宣布大功告成。但真正让业务稳定跑起来的,反而是网络、存储、GPU 这些“看不见的细节”。我这里总结了几个在迁移过程中踩过或观察过别人踩过的坑。
6.1 CNI 插件的踏勘:Calico 与 nftables 的适配
Rocky Linux 9.4 默认使用 nftables 作为防火墙后端,而 CentOS 7 时代很多 CNI 插件默认操作 iptables。Calico 从较新版本开始已经支持 nftables,但如果你集群里的 Calico 版本比较老,替换节点后可能出现新节点上的 calico-node Pod 状态正常,但 Pod 间网络不通的情况。
解决方案是在迁移前升级 Calico 到较新版本,或者在 Calico 的 FelixConfiguration 中显式设置 iptablesBackend: Nft。如果不想改动全局配置,也可以先在单节点上测试,确认路由和转发规则正确后再批量替换。
另外,旧节点下线后,Calico 中如果还残留旧的 IP 池分配记录,新节点加入时可能会得到相同 IP 但路由规则冲突。所以删除旧节点后,建议用 calicoctl 清理一下节点资源:
bash复制calicoctl get nodes
calicoctl delete node ${OLD_NODE}
如果 Calico 使用 Kubernetes datastore,执行 kubectl delete node 后通常会自动清理,但保险起见仍然值得检查。
6.2 存储节点迁移:Local PV、NFS 和基于内核的 CSI
存储往往是迁移中“最重”的部分。如果集群使用 Local PV,旧 Worker 节点下线前务必要把 PVC 对应的宿主机目录备份出来,并在新节点上恢复或重建。如果使用 NFS 动态供给,则比较简单,PVC 绑定的 PV 不依赖具体节点,Pod 被重新调度到新节点后,挂载信息会自动适配。
但如果使用了 Rook-Ceph 这类基于内核的 CSI,新节点必须提前加载 rbd、nbd 等内核模块。Rocky Linux 9.4 默认不一定加载这些模块,而且不同内核版本的 nbd 模块行为有差异。我建议在节点初始化脚本里直接加入:
bash复制modprobe rbd
modprobe nbd
echo "rbd" > /etc/modules-load.d/rook.conf
echo "nbd" >> /etc/modules-load.d/rook.conf
否则等 CSI 创建 Pod 时才报 rbd: failed to load rbd kernel module,排障会非常痛苦。
6.3 GPU 节点的特殊处理:驱动、container runtime、调度标签
我这边有少量 GPU 节点,承载 CUDA 相关推理服务。CentOS 7 上装 NVIDIA 驱动的套路在 Rocky 9.4 上基本不可用,需要重新安装适配新版内核的驱动,并且在 containerd 中配置 nvidia-container-runtime。如果你也遇到类似问题,建议先确认以下环境变量和配置文件是否就绪:
bash复制# containerd 配置中增加 nvidia 运行时
# /etc/containerd/config.toml
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.nvidia]
runtime_type = "io.containerd.runc.v2"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.nvidia.options]
BinaryName = "/usr/bin/nvidia-container-runtime"
# 通过 nvidia-device-plugin 的 DaemonSet 自动发现 GPU
kubectl get pods -n kube-system | grep nvidia
如果新节点 GPU 数量、型号与旧节点不一致,旧节点上原本设置的 nvidia.com/gpu 资源数量可能对不上,需要手动给节点打注解和标签,让调度器知道新节点有多少可用 GPU:
bash复制kubectl label node gpu-node-rocky nvidia.com/gpu.present=true
kubectl annotate node gpu-node-rocky nvidia.com/gpu.count=2
6.4 防火墙与安全组:Rocky 9.4 的 firewalld 默认策略
CentOS 7 和 Rocky Linux 9.4 都默认启用 firewalld,但 Rocky 9.4 的默认 zone 策略可能更严格。如果集群节点之前开放了某些端口,迁移后需要在新节点上重新放行。尤其是 containerd 默认监听 127.0.0.1,不需要额外开放,但 kubelet 的 10250 端口、NodePort 端口段都需要确保没被 firewalld 拦掉。这个看起来很简单,实际遇到 kubelet 无法注册到集群时,十次里有五次是防火墙拦截。
7. 回滚方案与真实踩坑记录
哪怕准备再充分,生产环境迁移还是有可能出问题。所以我把回滚方案放在和正向迁移同等重要的位置。
7.1 回滚节点到旧系统的决策点
我的原则很简单:新节点加入后,如果 2 小时内没有出现稳定性问题,才允许对旧节点执行 kubeadm reset。在这之前,旧节点只是被 drain,并没有从集群中完全剥离,随时可以恢复调度。
如果新节点出了问题,回滚操作也很直接:
bash复制# 把新节点标记为不可调度并排空
kubectl cordon <new-node>
kubectl drain <new-node> --ignore-daemonsets --delete-emptydir-data --force
# 把旧节点重新设为可调度
kubectl uncordon <old-node>
这里有一个前提:旧节点的 kubelet、containerd 服务都还在,且节点没有执行过 kubeadm reset。所以迁移时不要急着清场,至少保留旧节点 7 天以上。
7.2 kubelet 启动失败:证书、os-release 和 hostname 的坑
我在迁移中遇到最经典的问题是,新节点 kubelet 无法启动,systemctl 日志里报证书错误或节点地址不合法。排查过程基本沿着三条线:
第一条线是证书。新节点的 /etc/kubernetes/pki 如果缺少部分文件,kubelet 无法向 API Server 完成 TLS 引导。可以查看 /var/log/messages 或 journalctl -u kubelet -f,如果有 certificate signed by unknown authority,大概率是证书复制不完整。
第二条线是 hostname 冲突。如果新节点的主机名和旧节点完全相同,而且旧节点还没有删除,kubelet 可能被拒绝注册,因为 API Server 认为该节点已经存在。所以我在迁移中规定新节点必须使用不同主机名,比如加 -rocky 后缀。
第三条线是系统版本识别。很多第三方脚本或 kubelet 插件会读取 /etc/os-release,如果脚本里写死了 CentOS Linux 7,Rocky 9.4 会被拒绝。这种情况下需要检查脚本逻辑并适配,而不是硬着头皮伪装 os-release。
7.3 Pod 长时间 Pending:资源、污点、StorageClass 的联合排查
迁移后最让人焦虑的就是 Pod Pending。我的排查顺序是这样的:
bash复制# 看一下具体原因
kubectl describe pod <pod-name> -n <namespace>
常见原因有三种:节点资源不足、节点污点不匹配、PVC 无法挂载。资源不足通常是因为新旧节点规格不一致,比如旧节点内存 64GB,新节点只有 32GB,导致部分 Pod 超大请求无法调度。污点不匹配则是因为新节点没有继承旧节点的 taint,原本容忍该污点的 Pod 反而其他节点不愿意收。PVC 无法挂载则需要在事件里看报错,比如 Local PV 的路径不存在,或者 NFS 挂载超时。
8. 验证清单与个人心得
迁移全部完成后,不要马上把旧节点清光,先跑一轮完整验证。这里给出我每次迁移后都会执行的验证清单,你可以直接拿去用。
8.1 集群健康状态检查
bash复制# 节点状态全部 Ready
kubectl get nodes
# 控制面组件状态
kubectl get pods -n kube-system
# CoreDNS 解析
kubectl run -it --rm test-dns --image=busybox:1.28 --restart=Never -- nslookup kubernetes.default.svc.cluster.local
# 业务代表性 Pod 的分布
kubectl get pods -A -o wide | awk '{print $8}' | sort | uniq -c
还要在迁移节点上触发一次 Pod 重建,观察调度是否正常,避免出现“应用平时没事,一发版就出问题”的情况。
8.2 数据一致性抽查
如果迁移涉及 Local PV 或 NFS,抽查几个关键应用的日志和数据库数据,确认没有因为 Pod 重启而丢失最近写入。我通常会把业务方拉进一个验证群,让 QA 在迁移期间跑一轮核心用例,第一时间发现异常。
8.3 监控告警与日志采集的适配
不要忘记检查 Prometheus、Loki、ELK 这类基础组件是否能够自动发现新节点。特别是 kubelet 指标采集需要依赖节点证书和 IP,如果 node_exporter 或者 kube-state-metrics 的配置写死了旧节点 IP,迁移后就会丢失监控数据。检查方式很简单:
bash复制# 确认 metrics 接口可达
curl -k https://<new-node-ip>:10250/metrics -H "Authorization: Bearer $TOKEN"
日志采集端如果是 fluent-bit/fluentd 以 DaemonSet 部署,通常会自动适配新节点,但如果采集配置里包含主机名或节点标签,也需要调整。
8.4 最后一点个人体会
整个迁移做完,我有一个很深的感受:从一个发行版迁到另一个发行版,本质不是“操作系统升级”,而是“节点生命周期管理”。K8S 已经在设计上把节点看成可替换资源,真正要做的是把排水、加入、验证、清理这套流程打磨熟练。生产环境里永远不要把希望寄托在“原地升级不失败”上,而是要让每个节点都能在 30 分钟内被替换掉。这样以后哪怕再遇到系统版本切换,你也会觉得只是又一次例行的滚动发布而已。
如果你也正要开始做类似的事,建议第一批先挑两个非核心 Worker 节点跑通整个流程,把新节点镜像、初始化脚本、验证清单全部固化下来,再做控制平面和高危节点。不要一上来就动 etcd,那是我用自己的肝换来的一条经验。
