一台跑着 Docker 和 Pigsty 的机器,怎么加进现有 Kubernetes 集群
手里有三台 150 GB 磁盘的机器。node4(控制面)和 node5 已经组成了一个 kubeadm 集群;node3 是另一个时代的产物:Docker 跑着 10 个业务容器,宿主机上还有一套 Pigsty 单机 PostgreSQL 和它带的整套观测组件。现在想让 node3 也进集群当 worker。
正常做法是把 node3 重装。但我想先确认一件事:不重装、不停业务,能不能直接加进去? 如果能,就有了一个从 Docker 迁到 Kubernetes 的过渡窗口——旧容器和新 Pod 在同一台机器上同时跑,业务切完再删 Docker。
这篇文章记录实际做下来遇到的每个冲突点和处理方式。所有结论都来自 node3 上的真实输出,不是推测。
先说结论:能共存,而且比想象中简单,核心原因是 Docker 和 kubelet 可以共用同一个 containerd。但共存只适合做过渡,不适合长期运行——后面会说为什么。
迁移前的环境
集群侧(node4 / node5,Ubuntu 26.04):
- Kubernetes v1.36.4,kubeadm 部署,
skipPhases: addon/kube-proxy - Cilium 1.20.1,
kubeProxyReplacement: true,Native Routing,netkit + TCX - Pod CIDR
10.244.0.0/16,Service CIDR10.96.0.0/12 - containerd 2.3.4(安装器从二进制装的
/usr/local/bin/containerd) - OpenEBS:LVM LocalPV 用 60 GB loop 设备建的
openebs-vg,默认 StorageClassopenebs-lvm;另有 hostpath 的openebs-hostpath - Spegel 做节点间 P2P 镜像分发,certs.d 里每个 registry 先走本机
:30021再走镜像站
node3(Ubuntu 26.04,同样 150 GB 磁盘):
sda3 148G → ubuntu-vg(VFree 0)├── ubuntu-lv 108G ext4 → / 已用 17G└── data-lv 40G xfs → /data 已用 30G(Docker Root Dir 在 /data/docker)- Docker 29.7.2(apt 装的
docker-ce),依赖containerd.io 2.3.4 /etc/containerd/config.toml里disabled_plugins = ["cri"]- 10 个容器:healthchecks、bugsink、两个 gatus、Debezium CDC(Kafka Connect + Elasticsearch)、LyraPass 两个服务及其 postgres:16、otel-collector
- Docker 网络:
docker0和 4 个 compose 网桥,占172.17.0.0/16到172.21.0.0/16 - Pigsty 4.5:patroni / PostgreSQL 18 / pgbouncer / haproxy / etcd / nginx(80,443) / grafana(3000) / VictoriaMetrics、Logs、Traces / alertmanager / vector / dnsmasq(53) / kafka / redis / minio,外加一堆 exporter
- ufw 开着:
INPUT DROP、FORWARD DROP,放行10.0.0.0/8、172.16.0.0/12、192.168.0.0/16和 22/80/443
一句话:这台机器的 containerd 是 Docker 的私有品,网络栈和防火墙也都是围着 Docker 配的。
为什么不能直接跑集群的安装脚本
我的集群安装器(bootstrap/start.sh --worker)对"已有 containerd"的策略是:版本接近就备份配置后用自己的二进制覆盖,差异大就停掉、删配置、重装。这个逻辑对干净机器和残留机器都对,但对 node3 是灾难——它会重写 config.toml,Docker 的 data-root、moby namespace 的运行时状态都可能被打乱。
所以 node3 不走通用脚本,单独写了一份只做必要动作的脚本。原则是:不碰 Docker 的任何东西,只给 containerd 补上 Kubernetes 需要的能力。
冲突点一:containerd 是谁的
这是整件事能成的关键。Docker 从 1.11 起就是 dockerd → containerd → runc 的结构,apt 装 docker-ce 会顺带装 containerd.io,系统里跑的是 /usr/bin/containerd。kubelet 需要的也是 containerd,通过 CRI 接口。两者可以是同一个进程,前提是 CRI 插件开着。
node3 的问题就在这里:
disabled_plugins = ["cri"]这是 containerd.io 包的默认配置——Docker 不需要 CRI,就把它关了。kubeadm join 的 preflight 直接报:
[ERROR CRI]: validate CRI v1 runtime API for endpoint "unix:///var/run/containerd/containerd.sock":rpc error: code = Unimplemented desc = unknown service runtime.v1.RuntimeService处理:删掉那一行,systemctl restart containerd。重启后 Docker 的 10 个容器一个没少——containerd 的 shim 进程是独立的,containerd 主进程重启不影响已运行的容器(这和 Docker 的 live-restore 是两回事,是 containerd 自身的设计)。
之后两边就在同一个 containerd 里各占一个 namespace,互相不可见:
$ ctr namespaces lsNAME LABELSk8s.iomoby
$ ctr -n moby containers ls | wc -l # Docker 的10$ ctr -n k8s.io containers ls | wc -l # kubelet 的26还有一个要对齐的是 cgroup 驱动。kubelet 从集群下发的配置是 cgroupDriver: systemd,node3 的 Docker 也是 Cgroup Driver: systemd,containerd 的 runc 配置里 SystemdCgroup 与之一致。如果 Docker 用的是 cgroupfs(老机器常见),这里就要改 containerd 配置并重启 Docker,业务容器会重启一次。
冲突点二:网络
三个层次要分开看。
网段。Docker 的 5 个网桥占 172.17–21.0.0/16,集群的 Pod 网 10.244.0.0/16、Service 网 10.96.0.0/12、节点网 10.10.21.0/24——没有重叠。这一条要在加入前查,重叠了 Cilium 起不来或者路由乱掉,而且事后很难改。查法就是 ip route 对着 kubeadm 的 podSubnet/serviceSubnet 看一遍。
转发规则。node3 的 ufw 默认 FORWARD DROP,之前 Pod 间流量能走完全是因为 Docker 自己在 FORWARD 链前面插了 DOCKER-USER / DOCKER-FORWARD。Cilium Native Routing 模式下跨节点的 Pod 流量要经过宿主机转发,这条 DROP 会把它吃掉。加入前补了两条:
ufw route allow from 10.244.0.0/16 to 10.244.0.0/16ufw route allow from 10.10.21.0/24 to 10.244.0.0/16后来发现 node4/node5 上 ufw 虽然 systemctl 是 active,规则却是 Status: inactive——安装器把它关了。所以 node3 删 Docker 时也应该 ufw disable,三台口径一致,否则以后排 Pod 间丢包会多一个变量。
iptables 与 eBPF。Docker 往 iptables 写了 36 条 DOCKER-* 规则,Cilium 写了 14 条 CILIUM_*,两边互不干扰:Cilium 的数据面在 eBPF(TCX attach 到 ens160,Pod 侧是 netkit),iptables 只用来做少量兜底;Docker 的 NAT 和 filter 规则只匹配 docker0/br-* 接口。cilium-dbg status 里 KubeProxyReplacement: True 说明 Service 转发也在 eBPF 里,跟 Docker 的 DNAT 不打架。
要注意的是 net.ipv4.ip_forward=1 Docker 已经开了,br_netfilter 模块需要自己加载,swapoff 也要做——这些是任何 kubeadm 节点的基本要求,跟共存无关。
冲突点三:端口
kubeadm 的 preflight 只查 worker 需要的 10250。但节点加入后 DaemonSet 会带着 hostPort 落上来,这些 preflight 不管。我从集群里把所有 hostNetwork Pod 和 hostPort 拉了一遍:
2381, 4244, 6443, 9234, 9879, 9962-9965, 10257, 10259其中 6443/2381/10257/10259 只在控制面节点,worker 上实际是 Cilium 的 4244(hubble)、9962–9965(envoy/agent metrics)、9234(tetragon)。Pigsty 占的是 80/443/3000/2379/2380/5432-5438/6432/9100/9428/8428 那一片,没有撞上。
但有一个隐含的坑:如果哪天想把 node3 升成控制面,Pigsty 的 etcd 正占着 2379/2380,kubeadm join --control-plane 会直接失败。worker 身份不受影响。
冲突点四:镜像拉取
这一块花的时间最多,而且根因跟 Docker 共存无关,值得单独记。
症状:节点 NotReady,Cilium 的 Pod 卡在 Init:0/6,事件是拉不到 registry.k8s.io/pause:3.10.2——node3 直连 registry.k8s.io 超时。node4/node5 能拉是因为 certs.d 里配了国内镜像站,node3 没有。
第一步:把 node4 的 /etc/containerd/certs.d 整目录复制到 node3。hosts.toml 每次拉取时读取,不需要重启 containerd。顺便把三台的 containerd.service.d/http-proxy.conf 都删了——集群走镜像站,不该再依赖代理。
第二步踩坑:镜像站也没拉下来,我从自己的 TCR 仓库拉了一份 pause:3.10.2。拉是拉下来了,Pod 却全部 CrashLoopBackOff,错误变成:
runc create failed: unable to create new parent process:namespace path: lstat /proc/144697/ns/ipc: no such file or directory这个报错看起来像 containerd/runc 的状态坏了,我先怀疑是 Docker 的旧 shim 干扰,停了 docker、清了 /run/containerd、全部重启——没用,业务容器倒是白白重启了一轮。
真正的原因:那个 pause 镜像是在 Mac 上 docker pull 再 docker tag/push 上去的,推上去的是 linux/arm64。amd64 节点上 pause 进程一启动就 exec format error 退出,sandbox 的 IPC namespace 随之消失,后面所有容器都创建失败。ctr images ls 能看到平台列:
registry.k8s.io/pause:3.10.2 ... linux/arm64正确做法是从已有节点导:
ssh node4 'ctr -n k8s.io images export --platform linux/amd64 - registry.k8s.io/pause:3.10.2' \ | ssh node3 'ctr -n k8s.io images import --platform linux/amd64 -'换完镜像、删掉旧 Pod 让它重建 sandbox,不到一分钟节点 Ready。
教训有两条:一是 lstat /proc/<pid>/ns/ipc 这种错先看 sandbox 容器(pause)自身有没有活着,再怀疑运行时;二是 Mac 上转镜像必须带 --platform linux/amd64,而且 docker pull 第二次加参数时显示 Image is up to date 并不代表本地是 amd64 的那份。
第三步:Spegel 的 P2P 注入在我复制 certs.d 时被覆盖丢了(我从仓库里的基线目录恢复的,基线不含运行时注入的那段),三台一起丢。按安装器的逻辑重新在每个 hosts.toml 的 server = 行后面插入本机 :30021 段并生成 _default/hosts.toml。node3 的 Spegel 日志还有 bootstrap completed but connectivity has not been reached,P2P 没跟另外两台接上,怀疑还是 ufw 的 INPUT DROP——等删 Docker 时一并关掉 ufw 再看。
冲突点五:磁盘与 OpenEBS
我一开始以为 node3 有 50 GB 没分配。实际是:
ubuntu-vg VSize <148G VFree 0150 GB 全在 LVM 里,只是根文件系统还有 85 GB 空闲。文件系统空闲空间 ≠ LVM 未分配空间,OpenEBS LVM LocalPV 要的是后者,node3 没有。
选项只有三个:加一块盘、缩 /data 腾空间(XFS 不能原地缩,要停 Docker 和 Pigsty 做迁移)、或者用 hostpath。业务不需要副本和故障自动恢复,选了 hostpath:
openebs-hostpath这个 StorageClass 集群里本来就有,BasePath: /var/openebs/local,在 node3 建好目录即可- 代价是 PVC 跟系统、kubelet、镜像共用根分区,没有容量隔离
由此带来一个必须知道的事:集群默认 StorageClass 是 openebs-lvm,它要求节点上有 openebs-vg。任何没写 storageClassName 的 PVC,一旦 Pod 被调度到 node3,就会因为找不到 VG 而挂在 Pending。要么给 node3 的负载显式指定 openebs-hostpath,要么用 nodeAffinity 把需要 LVM 卷的负载挡在 node3 之外。
另外 /data 是 40 GB XFS,Docker 的镜像层全在这里,已经 74%。Kubernetes 的镜像走 /var/lib/containerd 在根分区,两边不抢同一块空间,这是运气好,不是设计。
冲突点六:资源和调度
node3 只有 7.5 GB 内存。加入后 DaemonSet 立刻上来八个(cilium、cilium-envoy、kured、vector、openebs 节点插件、otel agent、spegel、tetragon),requests 合计约 1 GB;Pigsty 那套 Victoria + Grafana + PostgreSQL 本身就要两三个 G;Docker 里还有 Elasticsearch 和 Kafka Connect 各 768 MB 的 JVM。三方叠在一起,内存是紧的。
更直接的影响是:节点一 Ready,集群里所有 Pending 的 Pod 都会往上涌。我这里一个 ecommerce 的网关 Pod 两天前就因为没节点可调而 Pending,node3 一 Ready 它就落上来了。如果不希望过渡期间业务 Pod 跑到这台机器,kubeadm join 之前就该准备好 taint,或者加入后第一时间 kubectl cordon。
一个与迁移无关、但差点背锅的问题
排查过程中发现 Pigsty 的 PostgreSQL 根本没在跑:patroni 一直在 waiting on etcd,连的是 10.10.21.172:2379,而 node3 现在的 IP 是 10.10.21.163。日志显示 PG 在我登录前几个小时就干净关闭了——机器换过 IP,Pigsty 配置里写死的地址失效,整套 HA 栈都在等一个不存在的 etcd。
这件事和加入 Kubernetes 没关系,但如果先动了集群再发现 PG 挂了,很容易被当成迁移导致的故障。**动一台不干净的机器之前,先把它现有服务的健康状态记下来。**顺带的好处是:PG 处于干净关闭状态,直接冷拷贝 /pg/data 就是一致的备份,连 pg_wal 都只需要留最后一段。
验证:真的能当工作节点吗
节点 Ready 不等于能跑业务。我在 node3 上起了一个 Pod 实测:
| 项目 | 结果 |
|---|---|
openebs-hostpath PVC 绑定到 node3,写入后在宿主机 /var/openebs/local/pvc-*/ 能看到 | 通过 |
访问 ClusterIP https://10.96.0.1/version(验证 eBPF 的 kube-proxy 替代) | 返回 apiserver 版本 JSON |
CoreDNS 解析 kubernetes.default、其它 namespace 的 Service | 通过 |
| 跨节点 Pod→Pod:ping node4/node5 上的 Pod,HTTP 访问 consul 8500 | 0.3 ms,0 丢包 |
| Pod 出网 | 通过 |
Cilium 侧:
KubeProxyReplacement: True [ens160 10.10.21.163 (Direct Routing)]Controller Status: 46/46 healthyModules Health: Degraded(0) OK(113)Cluster health: 3/3 reachablekube-system 里 node3 的四个 Pod 最近 200 行日志没有 warn/error。到这里才能说 node3 是一个正常的 worker。
为什么共存只能是过渡态
跑通了,但我不打算让它长期这样:
- 两套运行时状态的责任边界不清。containerd 是共享的,一次为了 Kubernetes 改配置的重启会影响 Docker 容器(虽然这次没影响,但下次改 runc 或 cgroup 相关配置就不一定)。反过来,Docker 升级会替换
containerd.io包,可能覆盖我改过的config.toml。 - 网络规则是叠加而不是协调。Docker 和 Cilium 各写各的 iptables,现在不冲突是因为接口名和网段恰好错开,没有任何机制保证以后也错开。
- 磁盘和内存都在三方抢。前面说过了。
- 安装器不再能管这台机器。以后集群升级 containerd 或 kubelet,node3 都要手工跟,否则
assess_existing_runtime那段逻辑会把 Docker 那份 containerd 当残留处理。
计划是:Docker 里的业务迁到 Pod 之后,apt purge docker-ce*(保留 containerd.io),清掉 docker0/br-* 和 /data/docker,停掉 Pigsty 的宿主服务,ufw disable,让 node3 回到和 node4/node5 一样的状态。PostgreSQL 用 CNPG 重建,从冷拷贝导逻辑备份。
清单
给下一次做同样事情的自己:
加入前
- 记录现有服务健康状态(
systemctl、docker ps、关键端口) - 确认
containerd --version与集群一致或接近,config.toml里 CRI 未禁用,cgroup 驱动为 systemd -
ip route对照 Pod/Service CIDR,确认无重叠 - 列出集群所有 hostPort / hostNetwork 端口,对照本机
ss -lntup - 防火墙:FORWARD 策略、集群网段放行;最好与其它节点一致
-
lsblk+vgs:搞清楚是文件系统空闲还是 LVM 空闲,决定存储方案 - certs.d 镜像站配置从现有节点复制,确认 Spegel 注入没丢;删代理 drop-in
- 准备 taint 或计划 cordon
加入后
-
cilium-dbg status:Cluster health 全绿,KubeProxyReplacement 生效 - 每个 DaemonSet Pod 都 Running(拉不动的镜像用
ctr export/import从现有节点导) - 实测:PVC、ClusterIP、DNS、跨节点 Pod、出网
- 检查 Pending 的业务 Pod 是否涌到了新节点
排错时
lstat /proc/<pid>/ns/ipc→ 先看 pause 镜像的架构- 节点
NotReady+cni plugin not initialized→ 是 Cilium 没起来,不是 CNI 配置问题 - 重启 containerd 不会杀 Docker 容器,但清
/run/containerd会让 Docker 丢失容器状态——不要为了 Kubernetes 的问题去动它