K8S 1.13节点NotReady:CNI配置未初始化故障精解 1. 问题现场还原当K8S节点卡在NOT READYCNI却在静默摆烂你执行kubectl get nodes看到的不是熟悉的 Ready 状态而是一行刺眼的NotReady。更糟的是kubectl describe node node-name的 Events 区域里反复刷着同一句报错cni config uninitialized。这不是网络不通、也不是磁盘爆满它像一个系统级的“失语症”——节点本身活得好好的kubelet 进程也在跑但整个集群就是拒绝承认它有资格参与调度。这个问题在 Kubernetes 1.13 版本尤为典型它不像 1.20 那样有清晰的NodeProblemDetector日志堆栈也不像早期版本那样直接报Failed to initialize network plugin。它就那么安静地躺在那里用四个单词告诉你CNI 没加载一切免谈。这个标题里的关键词——K8S集群、NOT READY、1.13、cni config uninitialized——不是随便凑的。它精准锁定了一个特定历史阶段的“经典死锁”。1.13 是 K8S 从 beta 走向稳定的关键分水岭CNI 插件管理机制刚完成重构但 kubelet 的初始化逻辑还没完全跟上插件生态的碎片化节奏。此时任何一点配置偏差、路径错误或权限缺失都会被放大成整个节点的“失联”。它不挑环境无论是你用 kubeadm 在三台物理机上搭的测试集群还是用 Terraform 在云上批量部署的生产节点只要 CNI 插件没在 kubelet 启动前“正确就位”这个错误就会准时出现。它解决的不是某个具体功能而是集群最底层的信任链——kubelet 必须确认网络已就绪才敢向 API Server 报告自己“Ready”。所以这不是一个“修好就能用”的小故障而是一个必须穿透到容器运行时、CNI 规范、kubelet 启动流程三层底下的系统性诊断任务。如果你正卡在这里说明你已经过了“照着文档装完就跑”的新手期现在要面对的是真实生产环境中最磨人的那一类问题没有明确报错只有沉默的拒绝。2. 核心设计思路拆解为什么是CNI初始化失败而不是网络不通2.1 NOT READY 的真正含义kubelet 的“上岗体检报告”很多人第一反应是去 ping 宿主机网关、查 flannel 的 pod 是否 running、甚至重启 docker。这方向就偏了。NotReady状态在 K8S 中有明确定义它代表kubelet 自身健康检查失败而非节点整体宕机。kubelet 的健康检查清单里有一项硬性指标叫NetworkReady它不关心你的 Pod 能不能上网只关心一件事CNI 配置文件是否已存在且可读CNI 插件二进制是否已安装且可执行。这个检查发生在 kubelet 启动的早期阶段在它尝试拉取任何 Pod 之前。所以当你看到cni config uninitialized本质是 kubelet 在说“我连网络插件的配置文件都没找到凭什么让我去管容器网络我连上岗证都拿不到。”提示kubectl get nodes显示的NotReady和kubectl get pods -A | grep node-name显示的ContainerCreating是因果关系不是并列现象。后者是前者的结果——因为节点不 Ready调度器不会往上面派发任何 Pod自然也就没有容器在创建。2.2 1.13 版本的特殊性CNI 初始化逻辑的“临界点”Kubernetes 1.13 对 CNI 的处理引入了一个关键变化kubelet 不再主动等待 CNI 插件就绪而是严格遵循“配置即承诺”原则。在 1.12 及更早版本中kubelet 会启动一个后台 goroutine轮询/etc/cni/net.d/目录直到发现有效的.conf文件才继续。而 1.13 开始kubelet 在启动时会一次性扫描该目录如果扫描时为空或只有无效文件比如空文件、语法错误的 JSON它会立即标记NetworkReadyFalse并记录cni config uninitialized然后继续启动其他模块——但它绝不会回头再看第二眼。这个设计提升了启动速度却把配置时机的容错性降到了零。这意味着如果你用 Ansible 或 shell 脚本部署 CNI而脚本执行顺序是“先启 kubelet再拷贝 CNI 配置”那 1.13 就会永远卡住但在 1.12 上可能等几秒就自动恢复了。这就是为什么同一个部署脚本在 1.13 上必现在 1.10 上却能蒙混过关。2.3 CNI 配置未初始化的三大根源路径、权限、内容cni config uninitialized这个错误信息极其吝啬它不告诉你缺哪个文件、权限错在哪、JSON 哪里格式不对。根据我在多个 1.13 集群上的排障经验95% 的案例都落在以下三个相互独立又常同时发生的环节路径错位kubelet 默认只认/etc/cni/net.d/这个绝对路径。但很多教程会教你把配置放到/opt/cni/net.d/或~/cni/甚至有人为了“整洁”改成了/etc/cni/conf.d/。1.13 不支持任何自定义路径除非你显式修改 kubelet 的--cni-conf-dir参数——而绝大多数人根本没动过这个参数。权限锁死CNI 配置文件.conf和插件二进制如bridge,host-local必须对 kubelet 进程可读。kubelet 在 1.13 默认以root用户运行但如果你用 systemd 启动它并指定了Userkubelet那文件就必须对kubelet用户可读。常见陷阱是chmod 600了配置文件结果 kubelet 读不了或者插件二进制没有x权限导致“配置存在但插件无法执行”。内容失效.conf文件必须是合法的 JSON且plugins数组里至少有一个插件的type字段值必须对应/opt/cni/bin/目录下真实存在的二进制名。例如配置里写了type: flannel但/opt/cni/bin/下只有flannel-v0.12.0这个文件没有软链接flannelkubelet 就会判定为“插件不存在”进而认为整个配置无效。这三个环节环环相扣少一个cni config uninitialized就稳稳坐在那里等着你一层层剥开。3. 核心细节解析与实操要点手把手定位每一个断点3.1 第一步确认 kubelet 的 CNI 配置路径与用户身份别猜直接看 kubelet 的真实启动参数。这是所有诊断的起点# 查看 kubelet 进程的完整命令行 ps aux | grep kubelet | grep -v grep # 更可靠的方式查看 systemd 服务配置如果用 systemd sudo systemctl cat kubelet | grep ExecStart # 典型输出示例 # ExecStart/usr/bin/kubelet --bootstrap-kubeconfig/etc/kubernetes/bootstrap-kubelet.conf \ # --kubeconfig/etc/kubernetes/kubelet.conf \ # --config/var/lib/kubelet/config.yaml \ # --network-plugincni \ # --cni-bin-dir/opt/cni/bin \ # --cni-conf-dir/etc/cni/net.d \ # --container-runtimeremote \ # --container-runtime-endpointunix:///run/containerd/containerd.sock重点锁定两个参数--cni-conf-dir这是 kubelet 查找.conf文件的目录默认是/etc/cni/net.d/。必须确保你的 CNI 配置文件放在这里且路径拼写完全一致区分大小写。--cni-bin-dir这是 kubelet 查找插件二进制的目录默认是/opt/cni/bin/。必须确保 flannel/calico/weave 的核心二进制文件如bridge,host-local,flannel放在这里。注意--network-plugincni这个参数必须存在否则 kubelet 根本不会进入 CNI 初始化流程。如果它被误删或注释掉你会看到NetworkReadyFalse但日志里不会有cni config uninitialized而是更模糊的network plugin is not ready。这是另一个常见混淆点。3.2 第二步暴力验证路径与文件存在性假设--cni-conf-dir是/etc/cni/net.d/我们进行一次无死角扫描# 1. 检查目录是否存在且非空 ls -ld /etc/cni/net.d/ ls -l /etc/cni/net.d/ # 2. 检查目录下是否有 .conf 或 .conflist 文件注意1.13 支持两种后缀 # 正确的文件名示例10-flannel.conflist, 01-cilium.conf, 20-calico.conf find /etc/cni/net.d/ -name *.conf -o -name *.conflist | xargs ls -l # 3. 检查 --cni-bin-dir 目录默认 /opt/cni/bin/ ls -ld /opt/cni/bin/ ls -l /opt/cni/bin/ # 4. 关键检查列出所有 CNI 插件二进制并验证其可执行性 for bin in $(ls /opt/cni/bin/); do echo -n $bin: file /opt/cni/bin/$bin 2/dev/null | grep -q ELF echo ✓ ELF binary || echo ✗ Not an executable [ -x /opt/cni/bin/$bin ] echo ✓ Executable || echo ✗ Not executable done这个脚本会暴露所有表层问题。我见过太多案例/etc/cni/net.d/目录下只有一个空的README.md/opt/cni/bin/里flannel文件是个损坏的下载中断文件file命令显示data而非ELF或者host-local文件权限是644而非755。这些都不是“网络问题”而是“文件系统问题”。3.3 第三步深度解析 CNI 配置文件的内容合法性即使文件存在内容错一个字符也会让 kubelet 直接放弃。1.13 对 JSON 格式极其苛刻。我们用jq工具做一次外科手术式检查# 安装 jq如果未安装 sudo apt-get install -y jq # Ubuntu/Debian # 或 sudo yum install -y jq # CentOS/RHEL # 假设你的配置文件是 /etc/cni/net.d/10-flannel.conflist CONFIG_FILE/etc/cni/net.d/10-flannel.conflist # 1. 检查是否为合法 JSON jq empty $CONFIG_FILE 2/dev/null || { echo ❌ JSON syntax error in $CONFIG_FILE; exit 1; } # 2. 检查顶层结构必须有 cniVersion 和 plugins 字段 jq -e .cniVersion $CONFIG_FILE /dev/null 21 || echo ❌ Missing cniVersion jq -e .plugins $CONFIG_FILE /dev/null 21 || echo ❌ Missing plugins array # 3. 检查 plugins 数组中的每个插件 # 提取所有插件的 type 字段 jq -r .plugins[].type $CONFIG_FILE 2/dev/null | while read type; do if [ -z $type ]; then echo ❌ Empty type field found else # 检查该 type 对应的二进制是否存在于 /opt/cni/bin/ if [ ! -x /opt/cni/bin/$type ]; then echo ❌ Plugin binary $type not found or not executable in /opt/cni/bin/ # 尝试找一个带版本号的变体如 flannel-v0.12.0 if ls /opt/cni/bin/$type-* 1/dev/null 21; then echo Hint: Found versioned binary, create a symlink: ln -s /opt/cni/bin/${type}-* /opt/cni/bin/$type fi fi fi done这个检查脚本会揪出所有隐藏的坑。比如flannel 的官方配置里type是flannel但你下载的二进制文件名是flannel-v0.12.0jq脚本会立刻告诉你Plugin binary flannel not found。这时候解决方案不是改配置而是加一条软链接sudo ln -s /opt/cni/bin/flannel-v0.12.0 /opt/cni/bin/flannel。这是 1.13 时代最常用的“急救包”操作。3.4 第四步模拟 kubelet 的初始化过程获取原始日志kubectl describe node的日志太简略。要看到 kubelet 的“内心独白”必须直连它的日志流# 查看 kubelet 的实时日志CtrlC 退出 sudo journalctl -u kubelet -f # 或者查看最近 100 行的详细日志过滤 CNI 相关 sudo journalctl -u kubelet --since 1 hour ago | grep -i -A 5 -B 5 cni\|network\|plugin # 关键日志模式1.13 典型输出 # I0315 10:23:45.123456 12345 cni.go:203] Unable to update cni config: no networks found in /etc/cni/net.d/ # E0315 10:23:45.123457 12345 kubelet.go:2192] Container runtime network not ready: NetworkReadyfalse reason:NetworkPluginNotReady message:docker: network plugin is not ready: cni config uninitialized注意第一条日志里的no networks found in /etc/cni/net.d/—— 这比cni config uninitialized更精确它明确告诉你 kubelet 扫描了那个目录但什么也没找到。如果日志里出现failed to load network config: failed to parse network configuration: invalid character那就是 JSON 语法错误必须回第三步用jq修复。实操心得在排查过程中永远不要重启 kubelet 服务除非你已经确认所有文件、路径、权限都 100% 正确。因为 1.13 的 kubelet 不会重试重启只是让它再失败一次。正确的做法是停掉 kubelet → 修复所有问题 → 再启动。这样你能确保第一次启动就成功。4. 完整实操过程与核心环节实现从零构建一个 1.13 可用的 Flannel 网络4.1 场景设定一台全新的 Ubuntu 18.04 节点kubeadm 初始化后卡在 NotReady假设你已经用kubeadm init创建了 masterkubeadm join加入了一个 worker 节点但kubectl get nodes显示 worker 一直是NotReadydescribe node报cni config uninitialized。我们按标准流程走一遍。4.2 步骤一停服、清场、确认基础# 1. 停止 kubelet关键避免它在你操作时抢跑 sudo systemctl stop kubelet # 2. 彻底清理旧的 CNI 配置防止残留文件干扰 sudo rm -rf /etc/cni/net.d/ sudo rm -rf /opt/cni/bin/ # 3. 创建干净的目录结构 sudo mkdir -p /etc/cni/net.d/ sudo mkdir -p /opt/cni/bin/ # 4. 确认当前节点的 Pod 网络 CIDR这是 flannel 的命脉 # 查看 kubeadm init 时指定的 --pod-network-cidr或从 kube-controller-manager 的启动参数里找 # 如果记不清可以临时查 master 节点的 configmap # kubectl -n kube-system get cm kubeadm-config -o yaml | grep podSubnet # 假设结果是podSubnet: 10.244.0.0/16 # 那么 flannel 的 Backend Network 就必须是 10.244.0.0/164.3 步骤二下载并安装 CNI 标准插件bridge/host-localFlannel 本身不处理 IPAMIP地址分配它依赖 CNI 标准插件host-local。所以必须先装好基础插件# 1. 下载最新版 CNI 插件1.13 兼容选 0.8.x 系列 # 官方发布页https://github.com/containernetworking/plugins/releases # 我们用 0.8.7它是 1.13 最稳定的搭档 CNI_VERSIONv0.8.7 wget https://github.com/containernetworking/plugins/releases/download/${CNI_VERSION}/cni-plugins-linux-amd64-${CNI_VERSION}.tgz # 2. 解压到 /opt/cni/bin/注意是 bin 目录不是 net.d sudo tar -C /opt/cni/bin/ -xzf cni-plugins-linux-amd64-${CNI_VERSION}.tgz # 3. 验证安装 ls -l /opt/cni/bin/ | grep -E (bridge|host-local|loopback) # 应该看到类似-rwxr-xr-x 1 root root 3.2M Mar 15 10:00 bridge # -rwxr-xr-x 1 root root 3.1M Mar 15 10:00 host-local # -rwxr-xr-x 1 root root 2.1M Mar 15 10:00 loopback4.4 步骤三下载并安装 Flannel 二进制与配置# 1. 下载 flannel 二进制1.13 推荐 v0.12.0 FLANNEL_VERSIONv0.12.0 wget https://github.com/coreos/flannel/releases/download/${FLANNEL_VERSION}/flannel-${FLANNEL_VERSION}-linux-amd64.tar.gz # 2. 解压并安装 tar -xzf flannel-${FLANNEL_VERSION}-linux-amd64.tar.gz sudo cp flanneld /opt/cni/bin/ sudo cp mk-docker-opts.sh /opt/cni/bin/ # 这个脚本在 1.13 已废弃但留着无害 # 3. 创建软链接关键让 kubelet 认出 flannel 插件 sudo ln -s /opt/cni/bin/flanneld /opt/cni/bin/flannel # 4. 编写 flannel 的 CNI 配置文件/etc/cni/net.d/10-flannel.conflist # 注意这是 conflist 格式不是 conf且必须是 JSON sudo tee /etc/cni/net.d/10-flannel.conflist /dev/null EOF { cniVersion: 0.3.1, name: flannel, plugins: [ { type: flannel, delegate: { hairpinMode: true, isDefaultGateway: true } }, { type: portmap, capabilities: { portMappings: true } } ] } EOF # 5. 设置文件权限必须对 kubelet 可读 sudo chmod 644 /etc/cni/net.d/10-flannel.conflist4.5 步骤四终极验证与启动# 1. 再次用 jq 验证配置确保万无一失 sudo jq empty /etc/cni/net.d/10-flannel.conflist sudo jq -r .plugins[].type /etc/cni/net.d/10-flannel.conflist # 2. 检查 flannel 二进制是否可执行 ls -l /opt/cni/bin/flannel # 3. 启动 kubelet sudo systemctl start kubelet # 4. 实时观察日志等待奇迹 sudo journalctl -u kubelet -f | grep -i cni\|ready # 成功的日志特征 # I0315 11:45:22.345678 12345 cni.go:203] Loaded network config from /etc/cni/net.d/10-flannel.conflist # I0315 11:45:22.345679 12345 kubelet_node_status.go:71] Attempting to register node worker-node-1 # I0315 11:45:22.345680 12345 kubelet_node_status.go:112] Node worker-node-1 was previously registered # I0315 11:45:22.345681 12345 kubelet_node_status.go:115] Updating node status # I0315 11:45:22.345682 12345 kubelet_node_status.go:122] Successfully updated node status如果看到Loaded network config from ...恭喜CNI 初始化成功稍等 10-20 秒执行kubectl get nodesNotReady就会变成Ready。整个过程不需要重启 docker/containerd也不需要重新 join纯粹是修复 kubelet 的“上岗材料”。5. 常见问题与排查技巧实录那些文档里不会写的坑5.1 问题速查表症状、原因、解决方案症状日志/现象根本原因解决方案验证命令no networks found in /etc/cni/net.d//etc/cni/net.d/目录为空或文件后缀不是.conf/.conflist检查ls -l /etc/cni/net.d/确保有合法文件find /etc/cni/net.d/ -name *.conf -o -name *.conflistfailed to load network config: failed to parse network configuration.conf或.conflist文件 JSON 语法错误多逗号、少引号、中文标点用jq empty filename定位错误行jq empty /etc/cni/net.d/10-flannel.conflistPlugin binary flannel not found/opt/cni/bin/下没有名为flannel的可执行文件只有flannel-v0.12.0创建软链接sudo ln -s /opt/cni/bin/flannel-v0.12.0 /opt/cni/bin/flannells -l /opt/cni/bin/flannelpermission deniedon config fileCNI 配置文件权限不足如600kubelet 无法读取sudo chmod 644 /etc/cni/net.d/*.conf*ls -l /etc/cni/net.d/NetworkReadyFalsebut nocni config uninitializedin logs--network-plugincni参数缺失或被注释检查ps aux | grep kubelet确认参数存在ps aux | grep kubelet | grep network-plugin5.2 独家避坑技巧来自血泪教训的 3 条铁律铁律一永远用conflist不用conf在 1.13 上虽然 1.13 理论上支持两种格式但conf格式要求type字段必须是插件的全名如flannel而conflist格式允许你用delegate委托给其他插件。更重要的是conflist是 flannel 官方推荐的格式所有 flannel 的 YAML 部署清单生成的都是conflist。如果你硬要用conf会发现host-local的 IPAM 配置很难写对。所以别挑战规范直接用10-flannel.conflist。铁律二cniVersion必须是0.3.1不能是1.0.0这是 1.13 的一个隐藏兼容性开关。cniVersion字段告诉 kubelet 用哪个版本的 CNI 规范来解析配置。1.13 的 kubelet 内置的 CNI 库是 0.3.x 系列它不认识1.0.0规范里的新字段如ipam.range。如果你用了1.0.0jq可能验证通过但 kubelet 日志里会静默失败不报错只显示no networks found。所以无论你用什么插件cniVersion固定写死为0.3.1。铁律三flanneld二进制必须放在/opt/cni/bin/不能只放flannel很多教程说“下载 flannel 二进制放到/opt/cni/bin/”然后就完了。这是大错。flanneld是 flannel 的守护进程负责和 etcd 通信、分配子网而 CNI 插件flannel是一个轻量级的二进制它只负责在 Pod 启动时调用flanneld获取 IP。1.13 的 kubelet 在初始化时会尝试执行/opt/cni/bin/flannel这个命令。如果你只放了flanneld它找不到flannel就会报错。所以你必须同时有flanneld用于后台服务和flannel用于 CNI 调用或者用软链接让两者合一。5.3 进阶场景当你的集群用的是 Calico 或 Weave虽然标题是 flannel但原理相通。这里给出快速迁移指南Calico它的 CNI 配置文件通常叫10-calico.conflisttype字段是calico。你需要下载calico二进制不是calicoctl并确保/opt/cni/bin/calico存在且可执行。Calico 的cniVersion也必须是0.3.1。WeaveWeave 的 CNI 插件叫weave-net但它的二进制名是weave。所以配置里type: weave然后sudo ln -s /opt/cni/bin/weave-net /opt/cni/bin/weave。Weave 的配置文件结构更简单通常只有type和hairpinMode两个字段。提示无论用哪个 CNI第一步永远是停 kubelet第二步永远是ls -l /etc/cni/net.d/和ls -l /opt/cni/bin/第三步永远是jq empty。这套组合拳能干掉 90% 的 1.13 CNI 初始化问题。6. 经验总结与延伸思考为什么这个问题值得你花时间深挖这个问题的价值远不止于“让节点变绿”。它是一把钥匙能帮你打开 K8S 底层架构的黑箱。当你亲手把cni config uninitialized这个错误从NotReady状态里揪出来你实际上已经完成了三次关键认知跃迁第一次是从“API 操作者”到“组件协作者”的转变。你不再满足于kubectl apply -f而是开始理解 kubelet、CNI、containerd 之间如何握手、如何传递信任。你知道了--cni-conf-dir不是一个可有可无的参数而是 kubelet 的“入职申请表”投递地址。第二次是从“功能使用者”到“规范遵循者”的转变。CNI 规范0.3.1、JSON Schema、文件系统权限模型——这些不再是教科书里的名词而是你每天要和它们打交道的实体。你明白了为什么conflist比conf更健壮为什么cniVersion是一个强制契约而不是一个建议。第三次也是最重要的是从“问题解决者”到“系统设计者”的转变。当你能预判kubeadm init后必须手动部署 CNI能写出幂等的 Ansible Playbook 确保flannel二进制和软链接永远存在你就已经具备了设计高可用集群的基础能力。因为真正的稳定性不来自于“出了问题再修”而来自于“让问题根本没有机会发生”。所以下次再看到cni config uninitialized别烦躁。把它当成 K8S 给你的一次考试——考的不是你记住了多少命令而是你是否真正理解了这个系统是如何呼吸、如何思考、如何建立信任的。我踩过的坑都写在了上面的每一步里。现在轮到你了。