本文来自一次真实迁移复盘。节点、地址、域名、命名空间、业务数量和内部仓库信息均已脱敏或泛化,示例不能直接用于生产环境。
系列导航:Kubernetes 重建迁移教程系列。本篇的目标是从零构建一个健康的空集群。
这次升级没有在旧控制平面上连续执行多次 kubeadm upgrade,而是创建一套全新的 Kubernetes v1.36.3 集群,再分阶段迁移节点、数据、中间件、配置和业务。
下面不只是讲结论,而是按照实际执行顺序,从创建工作目录开始,一直写到业务切换完成。你可以把命令复制到自己的运维仓库,再替换所有 <...> 占位符。
选择重建而不是原地升级,主要因为旧环境同时存在操作系统老化、节点用途混乱、固定 Service IP、历史命名空间、网络规则不一致和有状态服务分散等问题。单纯升级控制平面,只会把这些问题原封不动地带到新版本。
最终证明,迁移最难的部分不是让 Pod 显示 Running,而是把下面这条链路全部打通:
节点
→ CNI / kube-proxy
→ PVC 与真实数据
→ Pod Ready
→ Service Endpoint
→ Service DNS / 服务注册
→ Gateway / Ingress
→ 登录与真实业务请求
→ 外部入口
迁移目标与最终形态
最终集群采用:
- Rocky Linux 10.2。
- Kubernetes v1.36.3。
- containerd,使用 systemd cgroup。
- 三节点控制平面和 etcd。
- kube-vip 提供 API 虚拟地址。
- Calico 提供 Pod 网络。
- NGINX Ingress 三副本。
- NFS 动态存储与少量本地持久卷。
- metrics-server 提供节点和 Pod 指标。
- Nacos 三节点集群。
- 按业务、数据、入口、监控、共享和预留用途划分节点池。
网络规划在创建控制平面之前冻结。Pod CIDR、Service CIDR、节点网段三者不能重叠,应用也不能混淆这三类地址。
flowchart LR
A["旧集群与虚拟化平台盘点"] --> B["etcd、资源和数据备份"]
B --> C["批量重装 Rocky Linux"]
C --> D["创建高可用控制平面"]
D --> E["Calico、Ingress、存储、指标"]
E --> F["资源以零副本预部署"]
F --> G["恢复数据服务"]
G --> H["Nacos 与 Service DNS 治理"]
H --> I["分批启动业务"]
I --> J["真实请求与入口切换"]
开始前:建立版本化工作目录
不要把下载文件、备份、临时 patch 和最终清单散落在运维机的下载目录。先建立一个独立目录:
mkdir -p cluster-migration/{inventory,backup,images,manifests,scripts,reports}
cd cluster-migration
git init
printf 'backup/\n*.key\n*.kubeconfig\n.env\n' > .gitignore
建议目录结构如下:
cluster-migration/
├── inventory/ # 节点、服务、存储和域名清单
├── backup/ # 不进入 Git 的备份及校验值
├── images/ # 镜像和 digest 清单
├── manifests/ # kubeadm、CNI、存储和业务清单
├── scripts/ # 可重复执行的初始化与验证脚本
└── reports/ # 每个阶段的验收结果
再创建一个不提交到 Git 的 .env:
cat > .env <<'EOF'
K8S_VERSION=v1.36.3
API_VIP=<api-vip>
POD_CIDR=<pod-cidr>
SERVICE_CIDR=<service-cidr>
CONTROL_PLANE_1=<control-plane-1>
CONTROL_PLANE_2=<control-plane-2>
CONTROL_PLANE_3=<control-plane-3>
EOF
chmod 600 .env
本文不会给出真实地址。对于公开文档,可以使用 192.0.2.0/24、198.51.100.0/24 等专用文档网段;真实执行必须替换成自己的规划。
第 1 步:把服务器清单变成可校验数据
不要只维护一张自由格式 Excel。至少额外导出一份 CSV:
hostname,ip,role,pool,cpu,memory_gib,disk_gib,local_data
cp-a,<node-ip>,control-plane,control-plane,8,32,100,false
worker-a,<node-ip>,worker,app-dev,8,32,100,false
data-a,<node-ip>,worker,data,16,64,500,true
先检查 IP、主机名是否重复:
cut -d, -f1 inventory/nodes.csv | tail -n +2 | sort | uniq -d
cut -d, -f2 inventory/nodes.csv | tail -n +2 | sort | uniq -d
两个命令都不应输出任何内容。再核对节点池数量:
cut -d, -f4 inventory/nodes.csv | tail -n +2 | sort | uniq -c
这一步虽然简单,却能提前发现一台节点被写入两个业务池、预留节点被误当数据节点等问题。
第 2 步:导出旧集群资源
先确认自己操作的是旧集群:
kubectl config current-context
kubectl cluster-info
kubectl get nodes -o wide
然后分别导出 namespaced 和 cluster-scoped 资源。生产环境建议用经过评审的脚本逐类导出;下面只展示结构:
kubectl get deploy,statefulset,daemonset,job,cronjob -A -o yaml \
> backup/workloads.yaml
kubectl get svc,ingress,configmap,pvc -A -o yaml \
> backup/application-resources.yaml
kubectl get pv,storageclass,clusterrole,clusterrolebinding,crd -o yaml \
> backup/cluster-resources.yaml
Secret 必须单独处理:
umask 077
kubectl get secret -A -o yaml > backup/secrets.yaml
backup/secrets.yaml 不得提交到 Git,也不得通过聊天工具或普通文件共享分发。完成后对备份计算校验值:
sha256sum backup/*.yaml > backup/SHA256SUMS
还要保存副本数,因为后续会把新集群中的工作负载统一改成零副本:
kubectl get deployment,statefulset -A \
-o custom-columns='KIND:.kind,NAMESPACE:.metadata.namespace,NAME:.metadata.name,REPLICAS:.spec.replicas' \
> inventory/workload-replicas.txt
第 3 步:创建并验证 etcd 快照
在控制平面节点上找到 etcd 证书和 endpoint,再执行:
ETCDCTL_API=3 etcdctl snapshot save /backup/etcd-snapshot.db \
--endpoints=https://127.0.0.1:2379 \
--cacert=<etcd-ca> \
--cert=<etcd-client-cert> \
--key=<etcd-client-key>
ETCDCTL_API=3 etcdctl snapshot status /backup/etcd-snapshot.db \
--write-out=table
把快照复制到独立存储,并在隔离主机上做一次 restore:
etcdutl snapshot restore /backup/etcd-snapshot.db \
--data-dir /var/lib/etcd-restore-test
恢复目录能成功创建、member 信息可读取,才说明快照至少具备基础可恢复性。正式演练还应启动隔离的 etcd 和 API Server 来验证对象。
第 4 步:制作统一节点初始化脚本
不要在每台节点上手敲命令。创建 scripts/prepare-node.sh,脚本至少完成:
#!/usr/bin/env bash
set -euo pipefail
swapoff -a
sed -ri '/\sswap\s/s/^#?/#/' /etc/fstab
cat >/etc/modules-load.d/kubernetes.conf <<'EOF'
overlay
br_netfilter
EOF
modprobe overlay
modprobe br_netfilter
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
EOF
sysctl --system
systemctl enable --now containerd
systemctl enable kubelet
containerd 配置生成后,确认:
containerd config dump | grep -E 'SystemdCgroup|sandbox_image'
crictl info
期望 SystemdCgroup=true,pause 镜像应来自你已经验证过可拉取的仓库。
第 5 步:在加入集群前完成防火墙
在所有节点放行规划好的 Pod CIDR:
firewall-cmd --permanent --zone=trusted --add-source=<pod-cidr>
firewall-cmd --reload
firewall-cmd --zone=trusted --query-source=<pod-cidr>
最后一个命令必须输出 yes。再把结果写入报告:
hostname
firewall-cmd --zone=trusted --list-sources
不要简单地长期关闭 firewalld。关闭防火墙看似能让网络快速恢复,却会丢失可审计的安全边界。
第 6 步:生成 kubeadm 配置并预拉镜像
创建 manifests/kubeadm.yaml:
apiVersion: kubeadm.k8s.io/v1beta4
kind: ClusterConfiguration
kubernetesVersion: v1.36.3
controlPlaneEndpoint: <api-vip>:6443
imageRepository: <kubernetes-image-mirror>
networking:
dnsDomain: cluster.local
podSubnet: <pod-cidr>
serviceSubnet: <service-cidr>
apiServer:
certSANs:
- <api-vip>
- <api-dns-name>
- <control-plane-1-ip>
- <control-plane-2-ip>
- <control-plane-3-ip>
在三台控制平面节点上预拉镜像:
kubeadm config images list --config manifests/kubeadm.yaml
kubeadm config images pull --config manifests/kubeadm.yaml
如果这里失败,先解决镜像仓库、DNS 和证书问题,不要继续执行 kubeadm init。
第 7 步:初始化第一台控制平面
在第一台控制平面执行:
sudo kubeadm init --config manifests/kubeadm.yaml --upload-certs \
| tee reports/kubeadm-init.log
初始化成功后,保存输出中的两条 kubeadm join 命令。它们分别用于控制平面和工作节点,包含短期有效的 token,不能提交到 Git。
为当前管理员配置 kubeconfig:
mkdir -p "$HOME/.kube"
sudo install -m 600 /etc/kubernetes/admin.conf "$HOME/.kube/config"
sudo chown "$(id -u):$(id -g)" "$HOME/.kube/config"
kubectl get nodes
kubectl get pods -A
此时第一台节点通常处于 NotReady,CoreDNS 也可能处于 Pending。这是因为 CNI 还没有安装,不要把它误判为 kubeadm 失败。
第 8 步:部署 API 高可用虚拟地址
三节点控制平面不能让客户端固定连接某一台节点。本文使用 kube-vip 的静态 Pod 模式提供 <api-vip>。
先在第一台控制平面节点上生成 manifest。镜像版本必须在迁移前固定,并完成镜像扫描:
export VIP=<api-vip>
export INTERFACE=<node-interface>
export KVVERSION=<approved-kube-vip-version>
sudo ctr image pull <kube-vip-image>:${KVVERSION}
sudo ctr run --rm --net-host <kube-vip-image>:${KVVERSION} vip /kube-vip manifest pod \
--interface "$INTERFACE" \
--address "$VIP" \
--controlplane \
--services \
--arp \
--leaderElection \
| sudo tee /etc/kubernetes/manifests/kube-vip.yaml >/dev/null
检查虚拟地址和 API:
ip address show dev <node-interface>
curl --cacert /etc/kubernetes/pki/ca.crt https://<api-vip>:6443/readyz
预期 /readyz 返回 ok。如果没有 VIP,先查 kubelet 日志和静态 Pod:
sudo crictl ps -a --name kube-vip
sudo journalctl -u kubelet -n 100 --no-pager
第 9 步:安装 Calico,让节点真正 Ready
把经过评审、版本固定的 Calico 清单保存到 manifests/calico.yaml。安装前确认其中的地址池与 kubeadm 的 <pod-cidr> 完全一致:
grep -n '<pod-cidr>' manifests/calico.yaml
kubectl apply -f manifests/calico.yaml
kubectl -n kube-system rollout status daemonset/calico-node --timeout=10m
kubectl get nodes
所有节点应逐步进入 Ready,CoreDNS 也应进入 Running。继续做一次 DNS 测试:
kubectl run dns-check --image=<approved-busybox-image> --restart=Never \
--command -- nslookup kubernetes.default.svc.cluster.local
kubectl logs dns-check
kubectl delete pod dns-check
只看到 Calico Pod 在运行还不够。至少再创建两个位于不同节点的测试 Pod,互相访问 Pod IP,并访问一个 ClusterIP Service,证明跨节点路由和 Service 转发都正常。
第 10 步:加入其余控制平面和工作节点
先加入另外两台控制平面节点,再加入工作节点。控制平面命令形如:
sudo kubeadm join <api-vip>:6443 \
--token <bootstrap-token> \
--discovery-token-ca-cert-hash sha256:<ca-hash> \
--control-plane \
--certificate-key <certificate-key>
工作节点不带最后两项:
sudo kubeadm join <api-vip>:6443 \
--token <bootstrap-token> \
--discovery-token-ca-cert-hash sha256:<ca-hash>
token 失效后,在现有控制平面节点上重新生成:
kubeadm token create --print-join-command
sudo kubeadm init phase upload-certs --upload-certs
全部加入后检查控制平面和 etcd 静态 Pod 是否均匀分布:
kubectl get nodes -o wide
kubectl -n kube-system get pods -o wide | grep -E 'kube-apiserver|etcd'
kubectl get --raw='/readyz?verbose'
第 11 步:安装指标、存储和 Ingress
基础组件的安装顺序建议固定为:
metrics-server → StorageClass → 动态制备器 → Ingress Controller
每安装一个组件就完成一次小验收,不要一次性 kubectl apply -f manifests/:
kubectl apply -f manifests/metrics-server.yaml
kubectl -n kube-system rollout status deployment/metrics-server --timeout=5m
kubectl top nodes
kubectl apply -f manifests/storage/
kubectl get storageclass
kubectl apply -f manifests/ingress-nginx/
kubectl -n ingress-nginx rollout status deployment/ingress-nginx-controller --timeout=10m
存储必须用临时 PVC 做读写测试,Ingress 必须用临时域名做 Host 路由测试。详细清单放在本系列的“基础组件篇”,不要仅凭 Pod 处于 Running 就进入业务迁移。
第 12 步:完成集群级验收
创建 reports/cluster-baseline.txt,记录以下命令输出:
kubectl get nodes -o wide
kubectl get pods -A -o wide
kubectl get storageclass
kubectl top nodes
kubectl get ingressclass
kubectl get --raw='/readyz?verbose'
到这里,新的空集群才算搭建完成。接下来按专题继续:
- 基础组件:Calico、metrics-server、Ingress 和 NFS 的逐项部署与验收。
- 节点调度:标签、污点、亲和性和数据服务反亲和。
- 业务迁移:零副本预部署、数据恢复、分批放量、入口切换和回滚。
- Nacos:三节点集群搭建、Service DNS 治理和平滑升级。
复盘:为什么第一步不是安装,而是盘点
迁移前先建立服务器与工作负载清单。每台节点至少记录:
- 虚拟化宿主机和虚拟机标识。
- IP、主机名、CPU、内存和磁盘。
- 是否承载本地盘、数据库、Ingress、代理或控制平面。
- 旧主机名到新主机名的映射。
- 目标节点池、标签、污点和容忍关系。
旧集群则要单独导出:
- Deployment、StatefulSet、DaemonSet、CronJob。
- Service、Ingress、Endpoint、ConfigMap、Secret。
- PVC、PV、StorageClass 和本地目录。
- RBAC、CRD 和集群级资源。
- 各工作负载当前副本数和 CronJob suspend 状态。
只保存 YAML 还不够。恢复时最容易丢失的是“运行状态”和“数据实际在哪里”。因此副本数、节点亲和性、存储位置和数据校验必须另行记录。
备份必须证明能恢复
重装节点前完成三类备份:
- etcd snapshot。
- Kubernetes 全量资源归档。
- 数据库、消息系统、NFS 和 hostPath 数据备份。
最低限度的校验包括:
etcdctl snapshot status /backup/etcd-snapshot.db --write-out=table
tar -tzf /backup/kubernetes-resources.tgz | head
sha256sum /backup/* > /backup/SHA256SUMS
真正可靠的做法是在隔离环境执行一次 etcd restore,并对数据库做抽样恢复。snapshot status 正常只能证明文件可读,不能证明恢复流程完整。
操作系统基线要先于 kubeadm
所有节点统一使用同一份版本化初始化脚本:
swapoff -a
modprobe overlay
modprobe br_netfilter
sysctl --system
systemctl enable --now containerd
systemctl enable kubelet
containerd 统一使用:
SystemdCgroup = true
同时启用桥接流量和 IPv4 转发:
net.bridge.bridge-nf-call-iptables=1
net.bridge.bridge-nf-call-ip6tables=1
net.ipv4.ip_forward=1
这次最典型的网络故障来自 firewalld:Nacos 返回的 Pod IP 健康,但跨节点客户端访问时出现 No route to host。原因不是 Nacos,而是节点没有信任 Calico Pod CIDR。
正确做法是在 kubeadm 之前把 <pod-cidr> 永久加入可信区域,并在 Calico 安装后用一次性 DaemonSet 对所有节点再校准。不能等业务启动后逐台补规则。
对于仍使用 hostNetwork 的数据服务,还要单独验证目标节点端口是否允许来自 Pod SNAT 后的流量。长期方案应是减少 hostNetwork,通过普通 Pod 网络和 Service 暴露服务。
控制平面与基础组件
高可用控制平面使用三个节点。kubeadm 配置中的版本、API VIP、Pod CIDR、Service CIDR 和 DNS 域一旦冻结,就不应在业务迁移过程中修改。
apiVersion: kubeadm.k8s.io/v1beta4
kind: ClusterConfiguration
kubernetesVersion: v1.36.3
controlPlaneEndpoint: <api-vip>:6443
networking:
dnsDomain: cluster.local
podSubnet: <pod-cidr>
serviceSubnet: <service-cidr>
控制平面完成后按顺序安装:
- Calico。
- NFS provisioner。
- metrics-server。
- NGINX Ingress。
- 日志、监控和必要系统组件。
运维机的 kubectl 应与 API Server 保持官方支持的版本偏差。Kubernetes 文档规定,kubectl 与 kube-apiserver 的版本差只能在一个 minor 版本以内。参见 Version Skew Policy。
节点池解决的不是整齐,而是故障边界
节点划分为控制平面、入口、平台基础服务、业务、数据、监控、共享和预留池。调度策略同时使用:
nodeSelector指定目标池。- 污点阻止未授权工作负载进入专用池。
- toleration 允许对应工作负载调度。
- required Pod Anti-Affinity 分散 Kafka、Zookeeper、Nacos 等副本。
一个常见错误是只加标签,不加污点。这样“指定了节点池”的业务看起来正常,但没有 nodeSelector 的其他 Pod 仍会落到专用节点。另一个错误是先加污点、后补 toleration,结果造成大批 Pod Pending。
标签、污点和 toleration 必须在 Git 中成对生成并提前 dry-run。
资源先落地,副本保持为零
旧对象不能原样导入。需要移除:
uidresourceVersionmanagedFieldscreationTimestampstatus- 自动生成的 ServiceAccount Token
- Helm release Secret
- 旧集群管理平台的标签和资源
Service、Ingress、ConfigMap、Secret、PVC 等先创建;Deployment 和 StatefulSet 初始副本设为 0;CronJob 先 suspend。
这一步可以提前发现:
- YAML 与新 API 不兼容。
- 固定 ClusterIP 冲突。
- PVC 无法绑定。
- 镜像不存在或拉取凭据缺失。
- Service selector 与 Pod label 不匹配。
- 节点标签和容忍配置不完整。
有状态服务必须逐层恢复
启动顺序固定为:
存储与 PVC
→ MySQL / MongoDB / Redis / RabbitMQ / Zookeeper
→ Kafka
→ PostgreSQL 与辅助服务
→ Nacos
→ 平台依赖
→ 业务服务
数据库不能用“创建了同名空库”代替迁移完成。至少要验证库表、权限、关键行数、索引和真实应用账号查询。
Redis 的排查则分成四层:
redis-cli PING
→ Pod Endpoint
→ ClusterIP
→ Service DNS
→ 真实应用接口
如果 Endpoint 正常而 ClusterIP 失败,优先检查请求源节点的 kube-proxy;重启业务 Pod 或修改 Nacos 都不会修复 Service 转发表。
Nacos:进程启动不等于注册中心可用
Nacos 使用三个 StatefulSet Pod。Headless Service 为每个成员提供稳定 DNS,普通 ClusterIP Service 为客户端提供统一入口。
nacos-0.nacos-hs.platform.svc.cluster.local
nacos-1.nacos-hs.platform.svc.cluster.local
nacos-2.nacos-hs.platform.svc.cluster.local
nacos-cs.platform.svc.cluster.local:8848
迁移时曾出现这样的问题:Nacos 日志显示启动成功,但客户端仍无法注册。最终发现运行模式开关与 gRPC 注册不兼容。修复后按三层验收:
- 三个 Pod Ready,三个成员全部健康。
- 三个节点运行模式一致。
- 同一个测试客户端在三个节点的实例列表中都可见。
只有这三层通过,才能继续启动 OAuth、网关和业务服务。
从固定 IP 迁移到 Service DNS
旧配置中大量内部服务使用 ClusterIP:
rbac.api.url=http://<cluster-ip>:8082
spring.data.redis.host=<cluster-ip>
迁移后改为:
rbac.api.url=http://rbac-api.app-test.svc.cluster.local:8082
spring.data.redis.host=redis-service.app-test.svc.cluster.local
但不能用正则把所有 IP 都替换为 DNS:
- 已删除的 Service 没有可替换目标。
- Kafka advertised listener 需要单独设计。
- 外部数据库、短信和专网地址不是 Kubernetes Service。
- 少量 Jenkins 或入口依赖可临时保留固定 ClusterIP,但必须进入白名单并写入 Git。
Pod Running 只是验收起点
业务按命名空间逐批启动。每批至少检查:
kubectl -n <namespace> get deployment,statefulset,pod
kubectl -n <namespace> get svc,endpointslice
kubectl -n <namespace> get event --sort-by=.lastTimestamp
然后执行真实请求:
- 登录与 Token 刷新。
- 用户、角色和权限。
- Redis Session。
- 文件上传下载。
- RabbitMQ 与 Kafka 生产消费。
- 数据库读写。
- WebSocket 建连。
- CI/CD 发布接口。
未登录接口返回“未登录”,只能证明入口到业务服务的链路可达,不能替代登录后的业务验证。
这次最有价值的故障模式
| 现象 | 根因 | 正确处理 |
|---|---|---|
| 服务注册为空 | Nacos 运行模式或客户端注册失败 | 查三个节点实例列表,不只看启动日志 |
| Pod IP 跨节点不可达 | firewalld 未信任 Pod CIDR | 全节点固化规则并做跨节点测试 |
| Endpoint 正常、ClusterIP 超时 | 单节点 kube-proxy 规则异常 | 只处理故障源节点的 kube-proxy |
| Redis 正常但应用超时 | Service 地址过期或 hostNetwork 端口被拒绝 | 分层验证 Endpoint、Service 和防火墙 |
| 指标为空 | metrics-server 无法访问 kubelet | 验证网络路径,必要时使用 hostNetwork |
| 集群内 200、公网失败 | 外层路由或 SNI 未配置 | 修改入口层,不反复重启业务 Pod |
| Job Pending | 仍绑定旧主机名或缺少 toleration | 同步修改 selector、PV affinity 和 toleration |
回滚必须在迁移前设计
回滚分为四层:
- 入口回滚:恢复旧路由、VIP 或 DNS。
- 对象回滚:从资源归档恢复 Service、Ingress、RBAC、PVC/PV。
- 配置回滚:按 Nacos 的 namespace、group、dataId 精确回滚。
- 数据回滚:使用数据库备份、binlog/WAL 和存储快照恢复。
一旦新环境开始接受写入,回滚就不再只是切换入口。必须明确增量数据如何处理,避免新旧环境双写或数据倒退。
最终结论
重建式升级的价值,不只是获得一个新版本集群,而是顺便清理旧系统积累的隐式依赖:固定 IP、未登记节点用途、不可恢复备份、混乱污点、失效路由和不可追踪的手工配置。
真正可复用的原则只有四条:
- 所有设计在切换前冻结。
- 所有有状态数据在重装前证明能恢复。
- 每个阶段都有不可跳过的验收门槛。
- 最终用真实业务请求定义完成,而不是用 Pod 状态定义完成。
DISCUSSION
讨论与反馈