这篇文章记录一次真实的 Kubernetes CNI 迁移:把现有集群的网络插件从 Flannel 切换到 Calico。

这不是只执行两条命令就结束的事情。CNI 切换后,旧 Pod 可能还挂着旧网络;如果集群里有 Longhorn、PostgreSQL、Redis、Ingress、Prometheus 这些组件,还要按顺序恢复存储、数据库、业务和监控。

这次最后的完成标准是:

所有 Pod 都 Ready
所有 Deployment 都 Available
metrics-server 可以正常返回 kubectl top nodes
new-api 和 Grafana 入口可以访问
Longhorn 业务卷恢复 healthy

最终验证结果:

kubectl wait --for=condition=Ready pod --all -A --timeout=120s
kubectl wait --for=condition=Available deployment --all -A --timeout=120s
kubectl top nodes

输出里 93 个 Pod 全部 Ready,所有 Deployment 都 Available,kubectl top nodes 正常返回。

业务入口也恢复:

https://k8s-ai.jihw.top    HTTP/2 200
https://grafana.jihw.top   HTTP/2 302 -> /login

当前集群

当前集群是三主节点 kubeadm 集群:

k8s-master1  192.168.3.214
k8s-master2  192.168.3.215
k8s-master3  192.168.3.216
API VIP      192.168.3.217

kubeadm 初始化时使用的 Pod CIDR 是:

10.244.0.0/16

这个网段很重要。Flannel 常见默认 Pod 网段就是 10.244.0.0/16。已有集群迁移时,Calico 的 IPPool 要继续使用这个 CIDR,不要随手改成 192.168.0.0/16 或其他新网段。

当前集群里还有这些关键组件:

Longhorn
CloudNativePG PostgreSQL
Redis HA + redis-master-proxy
ingress-nginx
MetalLB
cert-manager
kube-prometheus-stack
Headlamp
new-api

这也决定了迁移完成后不能只看 kubectl get nodes,还要看所有命名空间的 Pod。

为什么换 Calico

Flannel 的优点是简单,适合快速把 Pod 网络跑起来。它主要解决 Pod 跨节点通信问题。

Calico 更适合后续生产化:

  • 支持 Kubernetes NetworkPolicy,可以控制 Pod 之间的访问。
  • 支持 VXLAN、IPIP、BGP 等更多网络模式。
  • 有更完整的网络排错和可观测能力。
  • 后面可以继续探索 eBPF、全局网络策略等能力。

如果只是学习 Kubernetes,Flannel 够用。如果想继续做安全隔离和网络治理,Calico 更值得切换。

这里也顺手纠正一下:fannel 应该写作 flannel

最重要的结论

全新集群最简单:kubeadm init 后直接安装 Calico,不安装 Flannel。

已有集群也能迁移,但必须安排维护窗口,因为 CNI 切换会影响 Pod 网络。尤其是使用 Longhorn 时,迁移完成后要优先恢复 Longhorn 自身组件,否则 PostgreSQL、Redis 这类依赖 PVC 的业务会继续不可用。

这次真实故障链路是:

new-api 无法访问
  |
  v
new-api Service 没有 endpoints
  |
  v
new-api Pod CrashLoopBackOff
  |
  v
new-api 连接 PostgreSQL / DNS 超时
  |
  v
PostgreSQL / Redis NotReady
  |
  v
Longhorn CSI 和卷状态异常
  |
  v
Longhorn instance-manager 还保留旧 Pod IP
  |
  v
Longhorn engine / replica 之间 no route to host

所以恢复顺序不能反过来。不要一上来反复重启 new-api。应该先确认 Calico,再恢复 Longhorn,再恢复数据库和 Redis,最后恢复业务和监控。

全新集群怎么做

如果集群还没正式跑业务,建议直接重建。

初始化 Kubernetes 时继续指定 Pod CIDR:

sudo kubeadm init \
  --control-plane-endpoint "192.168.3.217:8443" \
  --apiserver-advertise-address=192.168.3.214 \
  --pod-network-cidr=10.244.0.0/16 \
  --upload-certs

不要再安装 Flannel:

# 不再执行这个
kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml

安装 Tigera Operator:

kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.32.0/manifests/tigera-operator.yaml

下载 Calico 自定义资源:

curl -O https://raw.githubusercontent.com/projectcalico/calico/v3.32.0/manifests/custom-resources.yaml

custom-resources.yaml 里的 IP 池改成当前 Pod CIDR:

apiVersion: operator.tigera.io/v1
kind: Installation
metadata:
  name: default
spec:
  calicoNetwork:
    ipPools:
      - blockSize: 26
        cidr: 10.244.0.0/16
        encapsulation: VXLANCrossSubnet
        natOutgoing: Enabled
        nodeSelector: all()

应用:

kubectl create -f custom-resources.yaml

等待 Calico 就绪:

kubectl -n calico-system get pods -o wide
kubectl get tigerastatus
kubectl get nodes

已有集群迁移总流程

这次已有集群迁移按这个顺序收口:

1. 备份资源和 etcd
2. 确认 Pod CIDR 和 NetworkPolicy
3. 删除 Flannel
4. 清理每台节点上的 Flannel CNI 残留
5. 安装 Calico
6. 验证 Calico 和 DNS
7. 恢复 Longhorn
8. 恢复 PostgreSQL / Redis
9. 恢复 new-api 和 Ingress
10. 恢复 metrics-server / cert-manager / Prometheus operator 等旧 Pod
11. 全集群 Pod Ready 检查

不要在业务高峰期做。

迁移前备份

先导出资源清单:

mkdir -p ~/k8s-backup/flannel-to-calico

kubectl get all -A -o yaml > ~/k8s-backup/flannel-to-calico/all.yaml
kubectl get cm,secret,ingress,svc,pvc,pv -A -o yaml > ~/k8s-backup/flannel-to-calico/common-resources.yaml
kubectl get nodes -o wide > ~/k8s-backup/flannel-to-calico/nodes.txt
kubectl get pods -A -o wide > ~/k8s-backup/flannel-to-calico/pods.txt

备份 Flannel 资源:

kubectl -n kube-flannel get all -o yaml > ~/k8s-backup/flannel-to-calico/flannel.yaml
kubectl -n kube-flannel get cm -o yaml >> ~/k8s-backup/flannel-to-calico/flannel.yaml

备份 NetworkPolicy:

kubectl get networkpolicy -A -o yaml > ~/k8s-backup/flannel-to-calico/networkpolicy.yaml

这一点很关键。Flannel 本身不实现 Kubernetes NetworkPolicy,Calico 会实现。也就是说,如果集群里以前已经创建过 NetworkPolicy,但因为 Flannel 没有执行,所以没有产生实际限制;换成 Calico 以后,这些策略可能会突然生效。

备份 etcd

kubeadm 默认把 etcd 跑成静态 Pod,宿主机上不一定安装了 etcdctl。如果直接执行:

sudo ETCDCTL_API=3 etcdctl snapshot save /root/etcd-snapshot-before-calico.db \
  --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

可能会报:

sudo: etcdctl:找不到命令

说明只是宿主机没有 etcdctl,不代表 etcd 有问题。

更推荐直接使用 etcd 容器里自带的 etcdctl。注意不要写 sh -c,有些 etcd 镜像非常精简,容器里没有 sh,会报:

exec: "sh": executable file not found in $PATH

直接执行 etcdctl

ETCD_CONTAINER_ID=$(sudo crictl ps --name etcd -q | head -n 1)

sudo crictl exec "$ETCD_CONTAINER_ID" etcdctl snapshot save /var/lib/etcd/etcd-snapshot-before-calico.db \
  --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

sudo cp /var/lib/etcd/etcd-snapshot-before-calico.db /root/etcd-snapshot-before-calico.db
sudo ls -lh /root/etcd-snapshot-before-calico.db

如果提示找不到 etcdctl,再尝试完整路径:

sudo crictl exec "$ETCD_CONTAINER_ID" /usr/local/bin/etcdctl snapshot save /var/lib/etcd/etcd-snapshot-before-calico.db \
  --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

如果 crictl 也不可用,先确认容器运行时:

sudo crictl ps | grep etcd
sudo ctr -n k8s.io containers list | grep etcd

也可以安装客户端:

sudo apt update
sudo apt install -y etcd-client

迁移前检查

确认 Flannel:

kubectl get ns | grep flannel
kubectl -n kube-flannel get pods -o wide
kubectl -n kube-flannel get ds

确认节点和 Pod:

kubectl get nodes -o wide
kubectl get pods -A -o wide

确认 kubeadm 配置:

kubectl -n kube-system get cm kubeadm-config -o yaml

确认当前 NetworkPolicy:

kubectl get networkpolicy -A

删除 Flannel

如果当初用官方 manifest 安装 Flannel,可以先尝试:

kubectl delete -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml

也可以删除当前集群里的 Flannel 资源:

kubectl -n kube-flannel delete ds kube-flannel-ds
kubectl delete ns kube-flannel

确认 Flannel Pod 消失:

kubectl get pods -A | grep -i flannel

清理 Flannel CNI 残留

下面操作要在每台节点执行:

sudo systemctl stop kubelet

sudo rm -f /etc/cni/net.d/*flannel*
sudo rm -f /etc/cni/net.d/10-flannel.conflist

sudo ip link delete flannel.1 2>/dev/null || true
sudo ip link delete cni0 2>/dev/null || true

sudo rm -rf /var/lib/cni/networks/cni0
sudo rm -rf /var/lib/cni/results

sudo systemctl start kubelet

如果节点上还有其他 CNI 配置文件,先看清楚再删:

ls -l /etc/cni/net.d/

安装 Calico

安装 Tigera Operator:

kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.32.0/manifests/tigera-operator.yaml

下载自定义资源:

curl -O https://raw.githubusercontent.com/projectcalico/calico/v3.32.0/manifests/custom-resources.yaml

修改 custom-resources.yaml

apiVersion: operator.tigera.io/v1
kind: Installation
metadata:
  name: default
spec:
  calicoNetwork:
    ipPools:
      - blockSize: 26
        cidr: 10.244.0.0/16
        encapsulation: VXLANCrossSubnet
        natOutgoing: Enabled
        nodeSelector: all()

应用:

kubectl create -f custom-resources.yaml

等待 Calico:

kubectl -n calico-system get pods -o wide
kubectl get tigerastatus

这次看到的 Calico 状态是:

apiserver   True
calico      True
goldmane    True
ippools     True
tiers       True
whisker     True

如果 tigerastatus 都是 Available,说明 Calico 这层基本正常。

先验证新 Pod DNS

切换后先创建临时 Pod 验证 DNS,不要马上判断业务问题都是 Calico 问题。

kubectl run dns-test --image=busybox:1.36 --restart=Never -- sleep 3600
kubectl wait --for=condition=Ready pod/dns-test --timeout=60s

kubectl exec dns-test -- nslookup kubernetes.default.svc.cluster.local
kubectl exec dns-test -- nslookup newapi-postgres-rw.new-api.svc.cluster.local

kubectl delete pod dns-test --wait=false

这次新建的临时 Pod DNS 是正常的,可以解析:

kubernetes.default.svc.cluster.local
newapi-postgres-rw.new-api.svc.cluster.local

所以后面 new-api 里的 DNS 超时,不是 Calico 整体坏了,而是旧 Pod 和 Longhorn 状态还没恢复。

真实故障:new-api 不能访问

迁移后重启了 new-api:

kubectl -n new-api rollout restart deployment/new-api

但是访问仍然失败。

先看资源:

kubectl -n new-api get pods,svc,endpoints,ingress -o wide

当时看到:

new-api Pod CrashLoopBackOff
new-api Service endpoints 为空
newapi-postgres-rw endpoints 为空
redis-ha endpoints 为空

这说明入口不是第一现场。Ingress 可以转发,但 Service 后面没有 Ready Pod。

看 new-api 日志:

kubectl -n new-api logs deploy/new-api --tail=120 --all-containers=true

关键错误:

failed to connect to user=newapi database=newapi
lookup newapi-postgres-rw.new-api.svc.cluster.local on 10.96.0.10:53: i/o timeout

继续看 PostgreSQL、Redis 和 Longhorn:

kubectl -n new-api get pods -o wide
kubectl -n longhorn-system get pods -o wide
kubectl get csidriver

当时事件里看到:

driver name driver.longhorn.io not found in the list of registered CSI drivers
volume is not ready for workloads
Multi-Attach error
MountVolume.MountDevice failed

这说明真正卡住的是存储层。

恢复 Longhorn

先看 Longhorn 组件:

kubectl -n longhorn-system get pods -o wide
kubectl -n longhorn-system get ds,deploy -o wide
kubectl -n longhorn-system get volumes.longhorn.io -o wide

当时 longhorn-managerlonghorn-csi-plugin 都不健康,业务卷卡在:

detaching
unknown
not ready for workloads

先重启 Longhorn 控制组件:

kubectl -n longhorn-system rollout restart \
  daemonset/longhorn-manager \
  daemonset/longhorn-csi-plugin \
  deployment/longhorn-driver-deployer \
  deployment/csi-attacher \
  deployment/csi-provisioner \
  deployment/csi-resizer \
  deployment/csi-snapshotter

等待恢复:

kubectl -n longhorn-system rollout status daemonset/longhorn-manager --timeout=180s
kubectl -n longhorn-system rollout status daemonset/longhorn-csi-plugin --timeout=180s
kubectl -n longhorn-system rollout status deployment/longhorn-driver-deployer --timeout=180s

然后继续看 instance-manager:

kubectl -n longhorn-system get pods -l longhorn.io/component=instance-manager -o wide

这次关键问题是:instance-manager 还保留旧 Pod IP,例如:

10.244.1.169
10.244.2.152

Longhorn engine 和 replica 互相访问时报:

no route to host
read: connection timed out

删除旧 instance-manager Pod,让 Longhorn 重新创建:

kubectl -n longhorn-system delete pod instance-manager-旧Pod名 --wait=false

等新 Pod 拿到 Calico 新 IP:

kubectl -n longhorn-system get pods -l longhorn.io/component=instance-manager -o wide

这次新 IP 类似:

10.244.159.x
10.244.224.x
10.244.135.x

再看卷:

kubectl -n longhorn-system get volumes.longhorn.io -o wide

目标是业务卷变成:

attached   healthy

恢复 PostgreSQL 和 Redis

Longhorn 恢复后,再处理依赖 PVC 的业务。不要在 Longhorn 还卡着时反复删 Pod。

查看状态:

kubectl -n new-api get pods,svc,endpoints -o wide
kubectl -n new-api get clusters.postgresql.cnpg.io -o wide

这次在 Longhorn 恢复后,重新创建 PostgreSQL 和 Redis Pod:

kubectl -n new-api delete pod newapi-postgres-1 newapi-postgres-2 newapi-postgres-3 --wait=false
kubectl -n new-api delete pod redis-ha-node-0 redis-ha-node-1 redis-ha-node-2 --wait=false

然后等待:

kubectl -n new-api get pods -o wide
kubectl -n new-api get endpoints
kubectl -n new-api get clusters.postgresql.cnpg.io -o wide

恢复后的目标:

newapi-postgres-1   1/1 Running
newapi-postgres-2   1/1 Running
newapi-postgres-3   1/1 Running
redis-ha-node-0     2/2 Running
redis-ha-node-1     2/2 Running
redis-ha-node-2     2/2 Running

CloudNativePG 最终状态:

Cluster in healthy state

PostgreSQL 和 Redis 的 endpoints 也要恢复:

newapi-postgres-rw   10.244.x.x:5432
redis-ha             10.244.x.x:6379,26379

恢复 new-api

数据库和 Redis 恢复后,再重启代理和业务:

kubectl -n new-api rollout restart deployment/redis-master-proxy deployment/new-api

等待:

kubectl -n new-api rollout status deployment/redis-master-proxy --timeout=180s
kubectl -n new-api rollout status deployment/new-api --timeout=240s

看 endpoints:

kubectl -n new-api get pods,svc,endpoints,ingress -o wide

恢复后的目标:

new-api 3 个 Pod 都是 1/1 Running
new-api endpoints 有 3 个 Pod IP
redis-master-proxy endpoints 有 2 个 Pod IP
newapi-postgres-rw endpoints 有 primary Pod IP

验证入口:

curl -kI --connect-timeout 5 https://k8s-ai.jihw.top
curl -kI --connect-timeout 5 -H 'Host: k8s-ai.jihw.top' https://192.168.3.230

这次恢复后返回:

HTTP/2 200
x-new-api-version: v1.0.0-rc.10

恢复 Ingress

这次 new-api 恢复后,Ingress 还有旧 Pod 不 Ready,所以重启 ingress-nginx:

kubectl -n ingress-nginx rollout restart deployment/ingress-nginx-controller
kubectl -n ingress-nginx rollout status deployment/ingress-nginx-controller --timeout=180s
kubectl -n ingress-nginx get pods,endpoints -o wide

目标:

ingress-nginx-controller 3/3 Running
ingress-nginx-controller endpoints 有 3 个 Pod

恢复监控和证书组件

业务恢复不代表迁移完成。后面检查全集群 Pod 时,又发现这些旧 Pod 还在 CrashLoop:

metrics-server
cert-manager
kube-prometheus-stack-operator
kube-prometheus-stack-kube-state-metrics

它们也都是切 CNI 前创建的旧 Pod,保留旧 Pod IP 后会出现探针超时、连接 API Server 超时等问题。

滚动重启:

kubectl -n kube-system rollout restart deployment/metrics-server
kubectl -n cert-manager rollout restart deployment/cert-manager
kubectl -n monitoring rollout restart deployment/kube-prometheus-stack-operator
kubectl -n monitoring rollout restart deployment/kube-prometheus-stack-kube-state-metrics

等待:

kubectl -n kube-system rollout status deployment/metrics-server --timeout=180s
kubectl -n cert-manager rollout status deployment/cert-manager --timeout=180s
kubectl -n monitoring rollout status deployment/kube-prometheus-stack-operator --timeout=180s
kubectl -n monitoring rollout status deployment/kube-prometheus-stack-kube-state-metrics --timeout=180s

metrics-server 恢复后,验证:

kubectl top nodes

这次返回:

NAME          CPU(cores)   CPU(%)   MEMORY(bytes)   MEMORY(%)
k8s-master1   611m         15%      5250Mi          67%
k8s-master2   443m         11%      3831Mi          49%
k8s-master3   447m         11%      4419Mi          56%

清理 Longhorn 其他旧 Pod

为了避免 Longhorn 后续继续被旧 IP 绊住,这次又重启了剩余 Longhorn 组件:

kubectl -n longhorn-system rollout restart daemonset/engine-image-ei-c9fa6d45
kubectl -n longhorn-system rollout restart deployment/longhorn-ui

等待:

kubectl -n longhorn-system rollout status daemonset/engine-image-ei-c9fa6d45 --timeout=180s
kubectl -n longhorn-system rollout status deployment/longhorn-ui --timeout=180s

确认所有 Longhorn Pod 都是新 IP:

kubectl -n longhorn-system get pods -o wide

最终验证

最终不要只看某个业务能不能访问,要用全集群检查收口。

所有 Pod Ready:

kubectl wait --for=condition=Ready pod --all -A --timeout=120s

所有 Deployment Available:

kubectl wait --for=condition=Available deployment --all -A --timeout=120s

检查 Deployment、StatefulSet、DaemonSet:

kubectl get deploy,sts,ds -A

检查 Longhorn 卷:

kubectl -n longhorn-system get volumes.longhorn.io -o wide

目标是业务相关卷都 healthy:

attached   healthy

检查 metrics-server:

kubectl top nodes

检查业务入口:

curl -kI --connect-timeout 5 https://k8s-ai.jihw.top
curl -kI --connect-timeout 5 https://grafana.jihw.top

这次最终结果:

ALL_PODS_READY
ALL_DEPLOYMENTS_AVAILABLE
new-api: HTTP/2 200
Grafana: HTTP/2 302 -> /login

到这里才算从 Flannel 正式切到了 Calico。

常见问题

节点一直 NotReady

先看 kubelet:

sudo journalctl -u kubelet -n 100 --no-pager

再看 CNI 配置:

ls -l /etc/cni/net.d/

正常情况下应该能看到 Calico 配置,例如:

10-calico.conflist

Calico Pod 起不来

看 operator 和 Calico 组件:

kubectl -n tigera-operator get pods
kubectl -n tigera-operator logs -l k8s-app=tigera-operator --tail=100
kubectl -n calico-system get pods -o wide
kubectl -n calico-system describe pod Pod名称
kubectl get tigerastatus

新 Pod DNS 正常,旧 Pod DNS 超时

这次就遇到了这种情况。

新创建的 dns-test 可以正常解析 Service,但旧业务 Pod 仍然 DNS 超时。这通常说明不是 CoreDNS 整体坏了,而是旧 Pod、旧 CNI 网络命名空间、Longhorn instance-manager 或依赖组件没有重建干净。

处理方式是按层重启:

CoreDNS
Longhorn
数据库和 Redis
业务 Deployment
监控和证书组件

Service 没有 endpoints

先看后端 Pod 是否 Ready:

kubectl -n 命名空间 get pods,svc,endpoints -o wide
kubectl -n 命名空间 describe pod Pod名称
kubectl -n 命名空间 logs Pod名称 --tail=100

Service 没有 endpoints 通常不是 Service 自己坏了,而是 selector 匹配的 Pod 没有 Ready。

Longhorn volume 卡在 detaching

先看 Longhorn:

kubectl -n longhorn-system get volumes.longhorn.io -o wide
kubectl -n longhorn-system get pods -l longhorn.io/component=instance-manager -o wide
kubectl -n longhorn-system logs -l app=longhorn-manager -c longhorn-manager --tail=200

如果日志里有:

no route to host
read: connection timed out

并且 instance-manager 还在旧 IP,删除旧 instance-manager Pod,让 Longhorn 重建。

回滚到 Flannel

如果 Calico 安装失败,并且短时间内无法修好,可以回滚。

先删除 Calico:

kubectl delete -f custom-resources.yaml
kubectl delete -f https://raw.githubusercontent.com/projectcalico/calico/v3.32.0/manifests/tigera-operator.yaml

每台节点清理 Calico CNI 残留:

sudo systemctl stop kubelet

sudo rm -f /etc/cni/net.d/*calico*
sudo ip link delete tunl0 2>/dev/null || true
sudo ip link delete vxlan.calico 2>/dev/null || true
sudo rm -rf /var/lib/cni/networks/k8s-pod-network
sudo rm -rf /var/lib/cni/results

sudo systemctl start kubelet

重新安装 Flannel:

kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml

然后按同样思路重启 CoreDNS、Longhorn、业务和监控组件。

这次经验

如果只是刚搭好的实验集群,我更倾向于重建:

kubeadm reset
重新 kubeadm init
直接安装 Calico
重新加入其他节点
重新部署业务

如果集群里已经有 Longhorn、PostgreSQL、Redis、Ingress、Prometheus、业务服务,就不要把 CNI 迁移理解成“删 Flannel、装 Calico”。真正的工作是迁移后的恢复顺序:

Calico Ready
CoreDNS 正常
Longhorn healthy
数据库和 Redis Ready
业务 endpoints 恢复
Ingress 可访问
metrics-server 和 Prometheus 组件恢复
所有 Pod Ready

这些都验证通过,迁移才算完成。