资讯详情

kubeadm与Docker搭建Kubernetes集群:cri-dockerd与Flannel实战复盘

📅 2026/10/21 6:15:21 | 华诺云谱 👁 阅读
kubeadm与Docker搭建Kubernetes集群:cri-dockerd与Flannel实战复盘
最近给一个内部项目搭环境需求很明确三台机器跑一套 Kubernetes承载日常测试和一批非核心服务。团队平时用 Docker 用得很顺手镜像构建、发布脚本全都围着 docker 命令转短期内让他们切到 containerd 不太现实。权衡之后我选了 kubeadm Docker cri-dockerd Flannel 的组合从零开始把三节点集群搭起来整个过程把关键的坑都踩了一遍。这篇文章就是这套方案的完整复盘环境准备、控制平面初始化、工作节点加入、跨节点通信验证以及几个高频故障的排查思路适合想用 Docker 运行时快速搭一套 k8s 环境、又不想被网上过时教程误导的人直接参考。1. 为什么现在还选 kubeadm Docker Flannel 这套组合1.1 Docker 与 containerd 的关系以及 cri-dockerd 这个绕不开的桥先讲一个很多人栽过的跟头Kubernetes 的 kubelet 和容器运行时之间通过 CRIContainer Runtime Interface通信早期 kubelet 内部塞了一个叫 dockershim 的组件负责把 CRI 请求翻译成 Docker API 调用所以那时候装完 Docker 直接 kubeadm init 就能用。但从 1.24 起 dockershim 被彻底移除kubelet 不再内置这套翻译能力。这不等于 Docker 不能用了社区把 dockershim 抽出来做成了独立进程 cri-dockerd由它实现 CRI 服务底层仍然去调 Docker API。说白了cri-dockerd 就是一个“外挂版 dockershim”。我这么说你就明白了如果你照着一篇 2022 年以前的教程操作装完 Docker 直接 init新版集群大概率起不来报错往往集中在找不到合适的 CRI socket。这是这套组合最容易踩的第一个坑。那为什么不干脆用 containerd 当运行时kubeadm 默认就支持反而更省事。但当时团队现有 CI/CD 里大量脚本基于 docker 命令镜像构建、本地调试都在 Docker 环境里切运行时意味着运维习惯、监控脚本全要跟着动。对这种规模不大、非核心的内部环境用 cri-dockerd 保留 Docker 工作流成本最低也最容易被团队接受。1.2 Flannel 的定位够用、简单、三层网络CNI 方案里现在被推荐最多的是 Calico我也在别的环境用过。Calico 确实强NetworkPolicy、BGP、IPIP 隧道一应俱全但对应的学习成本和排障成本也高。Flannel 只做一件事——给每个节点分配一个独立子网然后通过 VXLAN 把这些子网串成一个大三层网络。它不实现 NetworkPolicy也不搞 BGP但对于“让集群先跑起来、容器之间能互相访问”这个目标来说Flannel 是最不容易出意外的选择。这里有一个绑定关系必须记住Flannel 默认期望 Pod 网段是 10.244.0.0/16每个节点在其中拿一个 /24 的子网。所以 kubeadm init 时你必须用 --pod-network-cidr10.244.0.0/16 把集群 Pod 网段指过去两边对不上后面所有节点的子网分配都会乱Pods 会出现各种诡异现象。后来我翻阅官方配置也印证了这一点kube-flannel.yml 里的 net-conf.json 默认 Network 字段写的就是 10.244.0.0/16这个默认值是有意为之的。1.3 我的环境规划与版本矩阵这套方案的成败一半在配置一致性上先把我实际用的环境列出来节点角色IP主机名节点A控制平面192.168.100.10k8s-master节点B工作节点192.168.100.11k8s-node1节点C工作节点192.168.100.12k8s-node2操作系统是 Ubuntu 22.04 LTS每台机器 2 核 4G 内存。这套组合跑这个配置绰绰有余如果 master 上还想顺带跑业务负载建议加到 4 核 8G。三台机器在同一二层网络中间没有 NAT。防火墙方面需要放行这些端口6443kube-apiserver、2379-2380etcd、10250kubelet、10259kube-scheduler、10257kube-controller-manager以及 8472/UDPFlannel VXLAN 数据面。如果你开了 ufw 或者云安全组先按这个清单放行不然集群能建起来但通信断断续续排起来相当费劲。版本上我固定为 docker-ce 24.0.x、cri-dockerd 0.3.10、Kubernetes 1.28.2、Flannel v0.24.x。这套组合不是最新但几个版本互相验证过稳定而且包名都能在各自源里锁定适合照着抄。2. 三台机器上的基础环境准备2.1 系统初始化主机名、hosts、swap 和内核参数三台都是全新安装的 Ubuntu 22.04装完第一件事是改主机名并写入 /etc/hosts。主机名如果不对后续 join 时证书校验会莫名其妙失败而且日志里很难看出来是主机名问题所以这一步别偷懒。# 每台机器分别执行注意替换主机名 sudo hostnamectl set-hostname k8s-master # 节点B / 节点C 分别改成 k8s-node1 / k8s-node2 # 三台机器都写同样的 hosts cat /etc/hosts EOF 192.168.100.10 k8s-master 192.168.100.11 k8s-node1 192.168.100.12 k8s-node2 EOF关闭 swap 是 kubelet 的硬性要求默认配置下开着 swap 的节点 kubelet 根本起不来。注意要把 /etc/fstab 里的 swap 行也注释掉否则机器一重启 swap 又挂回来集群就悄悄退化。sudo swapoff -a sudo sed -i / swap / s/^/#/ /etc/fstab然后是内核模块和参数。很多人直接抄命令但不清楚每一行在干什么出了问题就抓瞎。overlay 模块是 Docker overlay2 存储驱动的基础br_netfilter 让桥接流量也走 iptablesKubernetes 的 Service 和 kube-proxy 全靠 iptables 转发不加载它Service 流量会直接断掉。net.ipv4.ip_forward1 让 Linux 转发跨节点数据包Flannel 的 VXLAN 隧道本质上是靠内核转发能力实现的。cat EOF | sudo tee /etc/modules-load.d/k8s.conf overlay br_netfilter EOF sudo modprobe overlay sudo modprobe br_netfilter cat EOF | sudo tee /etc/sysctl.d/k8s.conf net.bridge.bridge-nf-call-iptables 1 net.bridge.bridge-nf-call-ip6tables 1 net.ipv4.ip_forward 1 EOF sudo sysctl --system操作完要验证两个模块都已经加载、三个内核参数都变成 1再继续往下走lsmod | grep br_netfilter sysctl net.bridge.bridge-nf-call-iptables net.ipv4.ip_forward2.2 安装 Docker 并统一 cgroup 驱动Docker Engine 我直接用官方 apt 源安装三台机器步骤一样这里不展开安装细节。装完立刻要改 /etc/docker/daemon.json把 cgroup 驱动切成 systemd同时设置日志滚动防止容器日志把磁盘撑满{ exec-opts: [native.cgroupdriversystemd], log-driver: json-file, log-opts: { max-size: 100m, max-file: 3 }, storage-driver: overlay2 }解释一下为什么必须切。Linux 上 cgroup 驱动有 cgroupfs 和 systemd 两种kubelet 在 1.24 之后推荐 systemd 驱动而 Docker 默认是 cgroupfs。两边不一致时kubelet 和运行时各自管理 cgroup资源统计会错乱节点状态不稳定最典型的报错就是 cgroup driver 不一致。所以这里先统一掉后面少一个坑。sudo systemctl restart docker sudo systemctl enable docker docker info | grep -i cgroup # 应显示 systemd docker info | grep -i storage # 应显示 overlay22.3 安装 cri-dockerd桥接 kubelet 与 Docker到 cri-dockerd 项目的官方 Release 页面下载对应架构的压缩包我用 0.3.10 为例。解压后把二进制放到 /usr/local/bin 即可tar xvf cri-dockerd-0.3.10.amd64.tgz sudo install -o root -g root -m 0755 cri-dockerd /usr/local/bin/cri-dockerd然后创建一个精简的 systemd 服务。项目自带的单元文件没问题但我更习惯自己写这样能明确看到它在监听什么cat EOF | sudo tee /etc/systemd/system/cri-dockerd.service [Unit] DescriptionCRI Docker Bridge (cri-dockerd) Afterdocker.service Requiresdocker.service [Service] ExecStart/usr/local/bin/cri-dockerd \ --container-runtime-endpointunix:///var/run/cri-dockerd.sock \ --pod-infra-container-imageregistry.k8s.io/pause:3.9 Restarton-failure RestartSec10 [Install] WantedBymulti-user.target EOF sudo systemctl daemon-reload sudo systemctl enable --now cri-dockerd systemctl status cri-dockerd --no-pager这里有两个细节。第一--pod-infra-container-image 是 pause 沙箱镜像如果你的节点拉 registry.k8s.io 不方便可以把它替换成你内部镜像仓库里准备好的 pause:3.9。第二后面 kubeadm init 和 join 时传的 --cri-socket 必须和这里的 unix socket 路径完全一致这是整个方案里最容易疏忽的地方。Docker 装完后系统里会同时存在 containerd.sock、cri-dockerd.sock 和 docker.sockkubeadm 自动探测时不一定选对显式指定是最稳的。2.4 安装并锁定 kubeadm / kubelet / kubectl三个组件通过 Kubernetes 官方 apt 源安装版本必须一致。master 和 worker 都装 kubelet、kubeadmkubectl 至少 master 上要装。以下是 v1.28 的操作方式curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.28/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg echo deb [signed-by/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.28/deb/ | sudo tee /etc/apt/sources.list.d/kubernetes.list sudo apt-get update sudo apt-get install -y kubelet1.28.2-1.1 kubeadm1.28.2-1.1 kubectl1.28.2-1.1 sudo apt-mark hold kubelet kubeadm kubectlapt-mark hold 锁定版本是很多人忽略的一步。k8s 的 minor 版本升级如果让 kubelet 和 kubeadm 不同步或者 CNI、运行时兼容性跟不上第二天集群就可能挂掉。锁住之后升级都是手动触发可控得多。3. 控制平面初始化与 Flannel 网络打通3.1 kubeadm init 的完整参数与配置文件在 master 上执行初始化。我强烈建议用配置文件而不是一长串命令行参数因为配置文件可重复、可解释、好交接cat kubeadm-init.yaml EOF apiVersion: kubeadm.k8s.io/v1beta3 kind: InitConfiguration localAPIEndpoint: advertiseAddress: 192.168.100.10 nodeRegistration: criSocket: unix:///var/run/cri-dockerd.sock name: k8s-master --- apiVersion: kubeadm.k8s.io/v1beta3 kind: ClusterConfiguration kubernetesVersion: v1.28.2 networking: podSubnet: 10.244.0.0/16 serviceSubnet: 10.96.0.0/12 --- apiVersion: kubelet.config.k8s.io/v1beta1 kind: KubeletConfiguration cgroupDriver: systemd EOF sudo kubeadm init --configkubeadm-init.yaml --v5逐个说下关键字段。advertiseAddress 是 apiserver 对外监听的地址必须是 master 的局域网 IP工作节点要靠它连接。criSocket 必须指向 cri-dockerd 的 unix socket前面已经强调过。podSubnet 填 10.244.0.0/16 是为了和 Flannel 默认网段对齐。如果你所在环境的节点拉 registry.k8s.io 的镜像超时可以在 ClusterConfiguration 里加 imageRepository 字段指向你内部可用的 kube 镜像仓库前提是仓库里得备齐 kube-apiserver、kube-controller-manager、kube-scheduler、kube-proxy、etcd、pause 这些镜像。如果不喜欢配置文件也可以用等效命令sudo kubeadm init \ --apiserver-advertise-address192.168.100.10 \ --pod-network-cidr10.244.0.0/16 \ --cri-socketunix:///var/run/cri-dockerd.sock \ --kubernetes-versionv1.28.2初始化成功会输出 Your Kubernetes control-plane has initialized successfully 和一段 join 提示。如果卡在 preflight 检查多半是前面某一步没做干净——swap 没关、内核参数没生效、cgroup 不一致这些都是 preflight 直接报错的常见原因。3.2 配置 kubectl 和 kubeconfig 里的坑初始化完成后root 可以直接用 /etc/kubernetes/admin.conf但普通用户要把它拷到自己的 kubeconfig 路径mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config为什么用 admin.conf 而不是其他几个kubeadm 会生成 admin.conf、kubelet.conf、controller-manager.conf、scheduler.conf 四份凭证admin.conf 拥有最全的控制平面管理权限kubectl 和后续的 Web UI 都靠它。如果你不想拷文件也可以 export KUBECONFIG/etc/kubernetes/admin.conf 临时指向。此时执行 kubectl get nodesmaster 大概率是 NotReady。这是正常的CNI 还没装节点网络还没就绪。看到这个状态不要慌继续装 Flannel。3.3 安装 Flannel版本选择与 --iface 参数从 Flannel 项目官方发布页面下载当前版本的 kube-flannel.yml然后直接 applykubectl apply -f kube-flannel.yml这里提醒一下版本来源。早期很多教程用的是 quay.io/coreos/flannel 的旧镜像项目维护节奏和兼容性都不太行务必用 flannel 项目自己发布的 manifest。这个 manifest 会创建一个 DaemonSet每个节点跑一个 flannel Pod它负责从 Kubernetes API 申请 Pod 网段、创建 VXLAN 设备 flannel.1、把 CNI 配置写到 /etc/cni/net.d/10-flannel.conflist同时把 CNI 二进制解压到 /opt/cni/bin。kubelet 每次创建 Pod 时调用 CNI 插件由 flannel 插件完成 IP 分配和 VXLAN 封装。如果节点上有多个网卡Flannel 默认选默认路由所在的网卡这个选择在很多云环境里是错的。解决办法是在 DaemonSet 的容器 args 里加 --iface 强制指定物理网卡args: - --ip-masq - --kube-subnet-mgr - --ifaceeth1改完再 apply。判断是否需要这一步很简单等 Pod 起来后看 flannel 日志里实际用的 IP 是不是节点的真实 IP如果不是就需要 --iface 纠正。3.4 单节点先跑通再谈扩容Flannel 装完观察 kube-system 下所有 Pod 的状态kubectl get pods -n kube-system -o wide kubectl get nodes等 kube-apiserver、etcd、kube-controller-manager、kube-scheduler、kube-proxy、coredns、kube-flannel 都变成 Running 后master 应该变成 Ready。这里有个小技巧因为运行时是 Docker这些控制平面组件其实是 kubelet 以静态 Pod 方式拉起来的直接 docker ps | grep kube- 就能看到它们在容器里跑着排查起来比抽象猜测快得多。我习惯先把单节点集群跑通确认网络没问题再去加工作节点。这样能把变量隔离出问题要么是 master 本身要么是 join 流程不会两头同时怀疑。4. 工作节点加入与集群状态确认4.1 生成和管理 join tokenkubeadm 生成的 token 默认 24 小时过期。在 master 上直接打印带参数的完整 join 命令kubeadm token create --print-join-command输出会长得像这样kubeadm join 192.168.100.10:6443 --token token --discovery-token-ca-cert-hash sha256:hash这条命令包含三样关键信息apiserver 地址端口、一次性 token、CA 证书哈希。后两个是安全校验用的缺一不可。如果 join 时报哈希对不上说明集群 CA 和节点拿到的证书不一致需要重新核对如果 token 过期用 kubeadm token list 查看再重新 create 一条。CA 哈希丢了也可以重算openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt | openssl rsa -pubin -outform der 2/dev/null | openssl dgst -sha256 -hex | sed s/^.* //注意 join 命令默认不带 --cri-socket而我们这套环境必须显式指定 cri-dockerd 的 socket否则 kubeadm 自动探测可能连到 containerd.sock导致运行时连接失败。实际执行时把它补上sudo kubeadm join 192.168.100.10:6443 --token token --discovery-token-ca-cert-hash sha256:hash --cri-socketunix:///var/run/cri-dockerd.sock另外提一嘴如果业务要求节点长期可加入可以用 kubeadm token create --ttl 0 生成永不过期的 token但我不建议在生产环境这么干token 泄露的窗口是无限的。内部测试环境图省事倒是无所谓。4.2 两个工作节点执行 join 并观察日志在 node1 和 node2 上分别执行上面拼好的 join 命令。join 过程本质上是kubelet 启动并注册节点节点从 apiserver 获取凭证随后 Flannel DaemonSet 会把新节点的网络组件调度过去。整个流程一到两分钟。如果 join 卡在 preflight 不动优先检查两件事一是 master 的 6443 端口是否真正可达telnet 一下便知二是三台机器时间是否同步证书校验对时间偏移极其敏感差太多就会出现 TLS handshake timeout。Ubuntu 默认带 systemd-timesyncd确认它在运行即可。4.3 从 master 验证节点状态和关键 Pod两个节点 join 完成后回到 master 上验证kubectl get nodes -o wide kubectl get pods -n kube-system -o wide理想状态是这样一张表NAMESTATUSROLESVERSIONINTERNAL-IPk8s-masterReadycontrol-planev1.28.2192.168.100.10k8s-node1Readyv1.28.2192.168.100.11k8s-node2Readyv1.28.2192.168.100.12如果新节点迟迟 NotReady快速定位看几样东西节点 spec 里有没有 PodCIDRkubectl get node k8s-node1 -o jsonpath{.spec.podCIDR}、kube-system 里新节点上 flannel Pod 是否 Running、kubelet 日志有没有报错。这三个信息基本能锁定 80% 的问题。5. 实测验证跨节点 Pod 通信与常用排查链路5.1 部署一个跨调度的 workload 验证数据面集群 Ready 不等于网络真的通了我习惯部署一个跨节点分布的 Deployment 实测。这里用 nginx 镜像kubectl create deployment web-test --imagenginx:1.25-alpine --replicas3 kubectl rollout status deployment/web-test kubectl get pods -o wide正常情况下三个副本会调度到不同节点各自拿到 10.244.x.x 网段的 IP。再起一个调试 Pod在它里面去访问别的节点上的 Pod IPkubectl run nettest --imagebusybox:1.36 --rm -it -- sh进入后 ping 一下跨节点的 Pod IP能通就说明 Flannel 数据面没问题。想看得更底层在节点上执行 ip route你会看到本节点子网路由走 cni0 桥其他节点的子网路由走 flannel.1 隧道设备再配合 ip -d link show flannel.1 和 bridge fdb show flannel.1 能看到 VXLAN VTEP 的邻居表。对这些命令熟起来以后排查网络问题会非常有底。5.2 实战排障节点一直 NotReady 的完整排查链路以我这套环境实际遇到过的情况举例。node1 join 后kubectl get nodes 一直显示 NotReady我当时没有直接重装而是沿着一条链路查下去。第一步在 master 上看节点的具体状态kubectl describe node k8s-node1 | grep -A 10 ConditionsConditions 里的 Ready 那条会给出 KubeletNotReady 的原因比如 network plugin not ready: cni config uninitialized或者是 PLEG is not healthy。这两种原因的排查方向完全不同别混着来。第二步到 node1 上看 kubelet 日志。kubelet 的日志是排障的核心现场systemctl status kubelet --no-pager journalctl -u kubelet -f --since 10 min ago如果日志里出现 dial unix /var/run/cri-dockerd.sock 之类的连接失败基本可以断定 cri-dockerd 没起来或者 socket 路径不对。先 systemctl status cri-dockerd 确认服务状态再 ls -l /var/run/cri-dockerd.sock 看 socket 是否存在。我这个案例里就是 join 时没带 --cri-socketkubeadm 自动连了 containerd 的 socket导致后续运行时和 kubelet 各说各话。第三步如果运行时没问题把目光转到 Flannel。查 kube-system 里 node1 上的 flannel Podkubectl get pods -n kube-system -o wide | grep node1 kubectl logs -n kube-system kube-flannel-ds-xxxxx -c kube-flannelFlannel 日志里如果出现 failed to acquire lease: node ... pod cidr not assigned 这类错误说明节点的 PodCIDR 是空的。用 jsonpath 确认一下kubectl get node k8s-node1 -o jsonpath{.spec.podCIDR}如果输出为空说明集群初始化时没配置 Pod 网段kube-controller-manager 根本没有可分配的 CIDR。这种情况最稳妥的解法是重建集群——别试图手动 patch 节点 PodCIDR因为底层 CNI 的路由、网段分配已经乱了硬改只会越补越漏。第四步如果 PodCIDR 正常但 flannel 日志报接口相关错误比如拿不到指定网卡的 IP那就回到 3.3 节的 --iface 问题。多网卡机器上这个坑很常见改了 DaemonSet 参数后删掉旧的 flannel Pod等新的拉起来再看节点状态。5.3 镜像拉取、cgroup 兼容等高频坑一览除了上面那条完整链路下面这些坑也是这套组合里的常客我直接列成表现象最常见原因处理方式init 卡在拉镜像阶段默认镜像仓库在当前网络环境不可达配置 imageRepository 指向可用镜像仓库或配置镜像加速flannel Pod ImagePullBackOffflannel 镜像拉取失败把 DaemonSet 镜像替换成可达仓库里的同名镜像kubelet 报 cgroup driver 不一致Docker 与 kubelet 的 cgroup 驱动不同改 daemon.json 为 systemd重启 docker节点 NotReady 且 kubelet 日志出现 PLEG not healthy运行时响应慢、磁盘压力或 docker 卡住查看磁盘和 inode重启 docker 再重启 kubeletjoin 报 TLS handshake timeoutapiserver 端口不通或节点时间不同步放通 6443确保系统时间同步机器重启后 Pod 大面积异常服务未设开机自启或 swap 重新挂载确认 docker、cri-dockerd、kubelet 都 enablefstab 已注释 swap还有一个容易被忽略的cri-dockerd 的版本和 Kubernetes 版本跨度不能太大。kubelet 升到新版本后太旧的 cri-dockerd 可能在 CRI 握手时报版本不支持升级 cri-dockerd 就好。6. 三节点集群搭好后的几点使用体会整套环境跑起来之后我对这套组合的边界认识得更清楚了。cri-dockerd 本质上是给“必须用 Docker”的场景留的过渡方案它比原生 containerd 多了一层转发多一层就多一个故障点容器密度上来之后资源占用也更高。如果是从零开始、没有历史包袱的新项目我大概率会直接 containerd但如果你像我一样要迁就现有 Docker 工作流cri-dockerd 是目前最平滑的答案别犹豫。另外一个切身建议把 kubeadm-init.yaml 和 join 命令存起来放到项目文档或者自己的笔记里。这套东西不是搭完就完的后面要加节点、换证书、重装环境有了配置文件五分钟就能复现。我后来重搭过一次靠的就是当初留下的这份文件。如果你预料到后面要上 NetworkPolicy 或者更复杂的网络策略建议一开始就选 Calico 而不是 Flannel。从 Flannel 迁移到 Calico 不是改个 manifest 那么简单整个集群的网络组件要重新调度数据面会中断一轮得挑维护窗口做。反之只是想要一个简单稳定、能跑测试的集群Flannel 完全胜任。最后分享一个排查技巧把 docker、cri-dockerd、kubelet 三个服务的日志合并到一条命令里看出问题时能快速对齐时间线journalctl -u docker -u cri-dockerd -u kubelet --since today --no-pager这三个服务串起来的链路就是这套集群运行时侧的命脉。日志对齐了绝大多数问题都不会困住你太久。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑