1. 问题现场还原:K8S节点卡在 NotReady,CNI 配置未初始化的典型症状
刚接手一个某高校实验室搭建的轻量级教学集群,三台物理机部署了 Kubernetes v1.13.12(这个版本虽已归档,但在教学环境和老旧硬件上仍有大量存量使用),控制平面跑在 master 节点,两个 worker 节点分别命名为 node-01 和 node-02。集群初始化用的是 kubeadm,流程走完后kubectl get nodes的输出却让人皱眉:
NAME STATUS ROLES AGE VERSION master Ready master 15m v1.13.12 node-01 NotReady <none> 12m v1.13.12 node-02 NotReady <none> 10m v1.13.12Status 列清一色的 NotReady,但奇怪的是,kubectl describe node node-01的 Events 区域一片空白,没有报错、没有 Warning,连 Pod 调度失败的提示都没有——这比报错更棘手。进一步查日志,journalctl -u kubelet -n 100 --no-pager | grep -i "cni\|network"瞬间锁定了关键线索:
kubelet[12345]: E0521 14:22:37.102198 12345 cni.go:203] Unable to update cni config: No networks found in /etc/cni/net.d/ kubelet[12345]: E0521 14:22:37.102215 12345 kubelet.go:2192] Container runtime network not ready: NetworkReady=false reason:NetworkPluginNotReady message:docker: network plugin is not ready: cni config uninitialized核心错误就这一句:“cni config uninitialized”。它不是说 CNI 插件没装,也不是说插件崩溃了,而是 kubelet 根本没在约定路径下找到任何有效的网络配置文件。这就像你给快递员留了地址,但他翻遍整个通讯录都找不到你家门牌号——不是快递员不干活,是根本没拿到“投递单”。
v1.13 是一个承上启下的关键版本:它正式将 CNI 作为默认网络插件接口,但尚未引入 v1.15+ 中的--cni-bin-dir和--cni-conf-dir这类显式参数(这些参数在 v1.13 中还只是实验性标志)。因此,kubelet 对 CNI 的依赖是硬编码的“信仰”:它只认/opt/cni/bin/下的二进制和/etc/cni/net.d/下的.conf或.conflist文件。只要这个目录空着,或者里面只有个空文件、格式错误的 JSON,kubelet 就会坚定地认为“网络插件未就绪”,并把节点状态钉死在 NotReady。
我试过直接重启 kubelet,也试过kubeadm reset && kubeadm init重来,结果一样。因为问题不在 kubelet 本身,也不在 kubeadm 初始化逻辑里,而在于一个被绝大多数新手忽略的“前置动作”:CNI 插件的二进制和配置,从来就不是 kubeadm 自动安装的。它只负责生成证书、启动组件、写入/etc/kubernetes/manifests/,但/etc/cni/net.d/这个目录,从始至终都是空的。这就像盖楼时,地基(kubelet)和框架(apiserver)都搭好了,但水电图纸(CNI 配置)和施工队(CNI 二进制)压根没进场。
提示:v1.13 的 kubelet 日志非常“诚实”,它不会报“calico 启动失败”或“flannel 无法连接 etcd”,它只会冰冷地告诉你“cni config uninitialized”。这意味着排查路径必须从最底层的文件系统开始,而不是一头扎进某个具体 CNI 插件的日志里。
2. 根因深挖:为什么 /etc/cni/net.d/ 目录会是空的?三个被掩盖的真相
这个问题看似简单,实则背后藏着三个相互交织、但常被文档一笔带过的真相。很多教程只说“装个 flannel 就行”,却从不解释“装”的具体动作到底是什么,导致读者在 v1.13 这种老版本上反复踩坑。
2.1 真相一:kubeadm 不负责分发 CNI 二进制,只负责“调用”
这是最根本的认知偏差。翻阅 kubeadm v1.13 的官方源码(cmd/kubeadm/app/phases/bootstraptoken/node/join.go),你会发现它在 join 流程中,对 CNI 的处理仅限于检查/etc/cni/net.d/是否存在,并验证其中是否有合法配置。它完全不包含下载、解压、校验或安装任何 CNI 插件二进制的逻辑。它的设计哲学是“职责分离”:kubeadm 只管集群编排,网络是独立插件的事。
所以,当你执行kubeadm join ...时,kubelet 启动后立刻去/etc/cni/net.d/找配置,而这个目录在全新节点上就是个空壳。此时 kubelet 的行为是:每隔几秒轮询一次该目录,一旦发现有合法配置,就加载并上报 Ready;如果一直为空,就持续报错并维持 NotReady。这个机制本身没有问题,问题在于用户误以为“kubeadm init/join = 集群开箱即用”。
2.2 真相二:CNI 配置文件不是“自动生成”的,而是需要手动或脚本注入
CNI 规范要求每个网络插件提供一个 JSON 格式的配置文件(如10-flannel.conflist),里面定义了插件类型(type: "flannel")、后端模式(backend: "vxlan")、etcd 地址、网段等关键参数。这个文件不能由 kubelet 生成,它必须由运维人员根据集群实际拓扑(比如 master 的 IP、etcd 的监听地址、期望的 Pod 网段)精确编写,然后放到/etc/cni/net.d/下。
以 Flannel 为例,其官方提供的kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml这个 YAML,本质是一个 DaemonSet,它会在每个节点上启动一个 flanneld 容器,并通过 volumeMount 将宿主机的/run/flannel/和/etc/cni/net.d/挂载进去。flanneld 容器启动后,会读取自己的配置,计算出本节点的子网,然后主动创建/etc/cni/net.d/10-flannel.conflist这个文件。也就是说,“配置文件的诞生”是 flanneld 这个进程的行为,不是 kubeadm 或 kubelet 的行为。
但在 v1.13 上,这个过程有个致命前提:flanneld 容器必须能成功启动。而它启动的前提,是宿主机上必须有/opt/cni/bin/目录下的flannel、bridge、host-local等二进制。如果这个目录不存在,flanneld 容器会因找不到host-local插件而 crashloop,进而永远无法生成配置文件——形成一个死循环。
2.3 真相三:/opt/cni/bin/ 目录的缺失,是“CNI 二进制分发”环节的彻底缺席
这才是整个链条上最常被跳过的环节。CNI 插件不是单一程序,而是一套工具集。flannel二进制负责管理网络,但它需要bridge来创建网桥,需要host-local来分配 IP,需要loopback来配置本地回环。这些二进制必须全部放在/opt/cni/bin/下,且具有可执行权限(chmod +x),kubelet 才能在调用时找到它们。
v1.13 的安装文档(如 kubernetes.io/docs/setup/production-environment/tools/kubeadm/create-cluster-kubeadm/)在“Installing a pod network add-on”章节,通常只给出一句:“Choose a CNI provider and install it.” 然后附上 Calico/Flannel 的 YAML 链接。它默认你已经知道,或者应该自己去 CNI 项目官网下载cni-plugins的 tarball 并手动解压。这个“手动”步骤,在自动化脚本盛行的今天,成了最大的知识断层。
我曾看到某次线上故障复盘报告里写道:“工程师执行了 kubeadm join,节点 NotReady,他花了 3 小时排查 etcd、apiserver、证书,最后发现/opt/cni/bin/目录下只有 2 个文件,而标准 CNI 插件包要求至少 12 个。” 这不是能力问题,是信息不对称造成的必然结果。
注意:
/opt/cni/bin/和/etc/cni/net.d/是 kubelet 的“刚需”。前者是“工具箱”,后者是“施工图纸”。缺一不可。任何试图绕过这两个目录、用其他路径替代的方案(比如修改 kubelet 启动参数),在 v1.13 上都是高风险操作,因为该版本的 kubelet 编译时硬编码了这些路径。
3. 实操四步法:从零开始让 NotReady 节点真正 Ready
基于上述根因分析,解决 “cni config uninitialized” 的唯一正解,就是按顺序补全这四个物理存在的环节。这不是一个命令就能搞定的魔法,而是一套必须亲手完成的“基础设施交付”。
3.1 第一步:确认并安装 CNI 二进制到 /opt/cni/bin/
这是整个链条的起点。必须确保/opt/cni/bin/目录存在,且里面包含了所有必需的插件二进制。
首先,检查现状:
ls -l /opt/cni/bin/ # 如果返回 "No such file or directory",说明目录不存在 # 如果目录存在但内容为空或极少,说明二进制缺失然后,下载并安装标准 CNI 插件包。v1.13 兼容的最新稳定版是cni-plugins-amd64-v0.7.5.tgz(注意:v0.8.x 及以上版本对 Go 版本有更高要求,可能与 v1.13 的 kubelet 不兼容):
# 创建目录(如果不存在) sudo mkdir -p /opt/cni/bin # 下载(国内用户建议用镜像源,如清华 TUNA) curl -L "https://github.com/containernetworking/plugins/releases/download/v0.7.5/cni-plugins-amd64-v0.7.5.tgz" -o cni-plugins.tgz # 解压到 /opt/cni/bin/ sudo tar -C /opt/cni/bin/ -xzf cni-plugins.tgz # 验证安装(应看到至少 bridge, host-local, loopback, flannel 等) ls -l /opt/cni/bin/ | wc -l # 正常应为 12 或 13 个文件为什么选 v0.7.5?因为它是最后一个明确支持 Go 1.10(v1.13 kubelet 的构建依赖)的 CNI 插件版本。v0.8.0 开始要求 Go 1.12+,强行安装会导致exec format error或undefined symbol错误。这是一个典型的“版本对齐”陷阱,很多教程不提,导致用户下载最新版反而失败。
3.2 第二步:选择并部署 CNI 插件(以 Flannel 为例)
CNI 二进制装好了,但光有工具没有图纸,还是没法开工。现在要部署一个具体的网络插件,让它来生成那张“图纸”。
Flannel 是 v1.13 最成熟、最轻量的选择。它的部署分为两步:准备配置、应用 DaemonSet。
准备 Flannel 配置:
Flannel 的官方 YAML (kube-flannel.yml) 默认使用host-gw模式,这要求所有节点在同一个二层网络。对于跨网段或云环境,必须改为vxlan模式。编辑 YAML 文件,找到net-conf.json部分,修改为:
{ "Network": "10.244.0.0/16", "Backend": { "Type": "vxlan" } }同时,确保image字段指向一个稳定的、与 v1.13 兼容的 Flannel 镜像,例如quay.io/coreos/flannel:v0.11.0-amd64(v0.11.0 是最后一个支持 v1.13 的 Flannel 主版本)。
应用 DaemonSet:
# 应用修改后的 YAML kubectl apply -f kube-flannel.yml # 检查 DaemonSet 状态 kubectl get ds -n kube-system # NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE # kube-flannel-ds-amd64 3 3 3 3 3 <none> 2m # 检查 flanneld Pod 是否 Running kubectl get pods -n kube-system -l app=flannel # NAME READY STATUS RESTARTS AGE # kube-flannel-ds-amd64-abcde 1/1 Running 0 90s关键观察点:在flanneldPod 进入 Running 状态后,立刻登录到对应节点,检查/etc/cni/net.d/:
# 在 node-01 上执行 ls -l /etc/cni/net.d/ # 应该能看到类似:10-flannel.conflist cat /etc/cni/net.d/10-flannel.conflist | jq . # 查看内容是否合法如果这个文件存在且 JSON 格式正确,说明 Flannel 已成功“画好图纸”。如果文件不存在,说明flanneldPod 虽然 Running,但内部逻辑失败,需kubectl logs -n kube-system <flannel-pod-name>查看具体原因(常见于 etcd 地址配置错误)。
3.3 第三步:强制触发 kubelet 的 CNI 配置重载
即使/etc/cni/net.d/下有了正确的配置文件,kubelet 也不会立刻感知。它有一个内部的缓存和轮询机制,默认轮询间隔是 3 秒,但有时会因状态机卡住而延迟。
最可靠的方法是重启 kubelet 服务,强制它从头加载所有配置:
# 在 NotReady 的节点上执行(如 node-01) sudo systemctl restart kubelet # 立即检查状态 sudo journalctl -u kubelet -n 50 --no-pager | grep -i "cni\|ready" # 应该看到类似:I0521 14:45:22.123456 12345 kubelet_node_status.go:294] Setting node condition for node "node-01" to true (NetworkReady)为什么不是systemctl reload kubelet?因为reload只会重新读取 systemd 的 service 文件,不会重启 kubelet 进程本身,也就不会触发 CNI 配置的完整初始化流程。restart是唯一能保证“从零开始”的操作。
3.4 第四步:验证与收尾:从 NotReady 到 Ready 的完整证据链
重启 kubelet 后,不要只看kubectl get nodes,要建立一条完整的、可追溯的证据链,证明问题已被根治。
证据链一:节点状态
kubectl get nodes # NAME STATUS ROLES AGE VERSION # master Ready master 45m v1.13.12 # node-01 Ready <none> 42m v1.13.12 # node-02 Ready <none> 40m v1.13.12证据链二:CNI 配置文件
# 在 node-01 上 ls -l /etc/cni/net.d/ # -rw-r--r-- 1 root root 422 May 21 14:45 10-flannel.conflist cat /etc/cni/net.d/10-flannel.conflist | jq '.name, .plugins[0].type, .plugins[0].backend.Type' # "cbr0" # "flannel" # "vxlan"证据链三:CNI 二进制可用性
# 在 node-01 上 /opt/cni/bin/bridge version # bridge version 0.7.5 /opt/cni/bin/host-local version # host-local version 0.7.5证据链四:网络连通性
# 在 master 上,创建一个测试 Pod kubectl run test-pod --image=busybox:1.28 -- sleep 3600 kubectl get pods -o wide # NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES # test-pod 1/1 Running 0 30s 10.244.1.2 node-01 <none> <none> # 从 master ping 这个 Pod 的 IP ping -c 3 10.244.1.2 # PING 10.244.1.2 (10.244.1.2) 56(84) bytes of data. # 64 bytes from 10.244.1.2: icmp_seq=1 ttl=63 time=0.823 ms当这四条证据链全部闭合,才意味着 “cni config uninitialized” 这个错误被真正、彻底地解决了。它不是一个状态的切换,而是一整套基础设施的交付完成。
4. 高阶避坑指南:那些让问题“复发”的隐藏雷区
解决了初次 NotReady,不代表万事大吉。在后续的集群维护中,有三个极易被忽视的“复发点”,它们会让节点在毫无征兆的情况下,再次陷入 NotReady。
4.1 雷区一:/etc/cni/net.d/ 目录被意外清空或覆盖
这是最“冤枉”的复发。某次,一位同事为了“清理旧配置”,在节点上执行了rm -rf /etc/cni/net.d/*,然后kubectl delete -f kube-flannel.yml删除了 DaemonSet。他以为删干净了,再kubectl apply就能重来。结果新起的 flanneld Pod 日志里全是open /etc/cni/net.d/10-flannel.conflist: permission denied。
原因在于:kube-flannel.yml中的 volumeMount 将/etc/cni/net.d/挂载为hostPath,但它的type字段默认是DirectoryOrCreate。当目录被rm -rf清空后,hostPath会尝试重建它,但重建的目录权限是root:root 755,而 flanneld 容器是以uid=0(root)运行的,理论上没问题。但问题出在 SELinux 上——如果节点开启了 SELinux,hostPath挂载的目录会被打上system_u:object_r:container_file_t:s0标签,而 flanneld 容器内的进程默认没有权限写入这个标签的目录。
解决方案:永远不要手动rm -rf /etc/cni/net.d/。如果要重装,先kubectl delete -f kube-flannel.yml,等待所有 flanneld Pod 彻底终止(kubectl get pods -n kube-system -l app=flannel返回空),然后再sudo rm -rf /etc/cni/net.d/*。最后,确保kube-flannel.yml中的hostPath配置显式指定了type: DirectoryOrCreate,并在securityContext中添加privileged: true(虽然不推荐生产环境,但在 v1.13 的调试阶段是最快捷的绕过方式)。
4.2 雷区二:CNI 二进制版本与 kubelet 版本的 ABI 不兼容
v1.13 的 kubelet 是用 Go 1.11.13 编译的。如果你不小心安装了 v1.0.1 的 CNI 插件(它要求 Go 1.16+),那么当 kubelet 尝试exec调用/opt/cni/bin/bridge时,会收到fork/exec /opt/cni/bin/bridge: no such file or directory的错误。注意,这个错误信息极具误导性——它不是说文件不存在,而是说动态链接器(ld-linux-x86-64.so.2)找不到,因为新版本二进制链接了旧版本系统没有的 glibc 符号。
如何快速诊断?在节点上直接执行:
/opt/cni/bin/bridge version # 如果报错 "No such file or directory",但 `ls -l` 显示文件存在,基本可以断定是 ABI 不兼容。 # 进一步验证:`file /opt/cni/bin/bridge` 查看其链接的 libc 版本,`ldd /opt/cni/bin/bridge` 查看依赖。解决方案:严格遵循“版本矩阵”。v1.13 对应 CNI 插件 v0.7.5,对应 Flannel v0.11.0。任何偏离这个组合的操作,都必须经过充分的e2e测试,而不是凭经验猜测。
4.3 雷区三:kubelet 启动参数中的--network-plugin=cni被错误覆盖
这是一个极其隐蔽的配置冲突。某些定制化的 kubelet service 文件(比如通过 Ansible 或 Puppet 生成的),可能会在ExecStart行后面追加--network-plugin=kubenet或--network-plugin=(空值)。这会直接覆盖 kubeadm 生成的默认参数。
如何排查?在 NotReady 节点上:
ps aux | grep kubelet | grep -o "network-plugin=[^ ]*" # 如果输出是 "network-plugin=kubenet" 或 "network-plugin=",那就找到了元凶。解决方案:永远不要直接修改/etc/systemd/system/kubelet.service.d/10-kubeadm.conf。如果需要自定义参数,应该创建一个新的 drop-in 文件,如/etc/systemd/system/kubelet.service.d/20-custom.conf,并在其中只写你需要覆盖的参数,确保--network-plugin=cni这一行始终存在且未被覆盖。修改后,务必执行sudo systemctl daemon-reload && sudo systemctl restart kubelet。
提示:在生产环境中,我习惯在集群初始化完成后,立即在所有节点上运行一个简单的健康检查脚本,它会自动验证
/opt/cni/bin/的完整性、/etc/cni/net.d/的存在性、以及ps aux | grep kubelet中的关键参数。这个脚本被集成到 CI/CD 流水线中,每次节点变更都会触发,将这类“人为失误”扼杀在摇篮里。
5. 经验沉淀:从 v1.13 的“古老”问题中提炼出的通用法则
虽然我们讨论的是 v1.13 这个特定版本,但解决 “cni config uninitialized” 的过程,实际上提炼出了适用于所有 Kubernetes 版本的、关于网络插件部署的通用法则。这些法则,是我过去十年在数十个不同规模、不同版本的集群中反复验证、摔打出来的。
5.1 法则一:CNI 是“声明式交付”,而非“命令式安装”
很多人把 CNI 插件当成一个软件包,用apt install或yum install就完事了。这是巨大的误解。CNI 的本质是一套契约:kubelet 声明“我需要网络”,CNI 插件声明“我能提供网络”,而/etc/cni/net.d/下的配置文件,就是双方签署的“合同文本”。这个“合同”必须由插件自身(如 flanneld)或一个独立的初始化脚本(如 calicoctl)来签署,kubelet 只是合同的执行方和监督者。
因此,任何脱离“配置文件生成”这个核心环节的所谓“安装”,都是无效的。这也是为什么kubectl apply -f calico.yaml能成功,而curl -L https://.../calicoctl | bash却常常失败——前者包含了完整的 DaemonSet 和 ConfigMap,能自动生成配置;后者只是一个 CLI 工具,它不负责部署网络。
5.2 法则二:节点就绪状态(Ready)是“多条件与门”,而非“单点开关”
kubectl get nodes输出的 Ready 状态,是 kubelet 内部多个健康检查项的综合结果。其中,NetworkReady只是NodeCondition数组中的一个元素。其他还有PIDReady、DiskPressure、MemoryPressure等。cni config uninitialized导致的是NetworkReady=False,但这并不影响PIDReady=True或DiskPressure=False。
所以,当你看到一个节点是 NotReady,第一反应不应该是“网络坏了”,而应该是“打开 kubelet 的 NodeCondition 详情,看是哪个条件不满足”。kubectl describe node <node-name>的输出中,Conditions:部分会清晰列出所有条件的状态和最后更新时间。这比盲目重启 kubelet 或重装插件,效率高出数倍。
5.3 法则三:日志是唯一的真相,而journalctl -u kubelet是你的第一现场
在容器化时代,我们习惯了kubectl logs。但对于 kubelet 这个“容器的容器”,它的日志不在 Kubernetes API 里,而在宿主机的 systemd journal 里。journalctl -u kubelet是诊断一切 kubelet 相关问题的黄金入口。它记录了从启动、证书加载、CNI 初始化、到 Pod 同步的每一个细节。
我给自己定了一条铁律:只要节点 NotReady,第一件事就是journalctl -u kubelet -n 200 --no-pager | grep -E "(cni|network|error|failed)"。这条命令能在 5 秒内,把问题的根源从茫茫日志海中精准捞出来。它比任何 GUI 控制台、比任何第三方监控工具都更直接、更真实。
最后再分享一个小技巧:在集群初始化脚本的末尾,加上一行echo "CNI check: $(ls -l /etc/cni/net.d/ 2>/dev/null | wc -l) files" >> /var/log/cluster-init.log。这样,每次集群部署完成后,你都能在日志里一眼看到 CNI 配置是否就位。这个看似微不足道的“快照”,在后续的故障复盘中,往往能成为决定性的证据。