1. 问题现场还原:当K8S节点卡在NOT READY,CNI却在静默摆烂
你执行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),它会立即标记NetworkReady=False并记录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 启动它并指定了User=kubelet,那文件就必须对kubelet用户可读。常见陷阱是chmod 600了配置文件,结果 kubelet 读不了;或者插件二进制没有+x权限,导致“配置存在但插件无法执行”。 - 内容失效:
.conf文件必须是合法的 JSON,且plugins数组里至少有一个插件的type字段值,必须对应/opt/cni/bin/目录下真实存在的二进制名。例如,配置里写了"type": "flannel",但/opt/cni/bin/下只有flannel-v0.12.0这个文件,没有软链接flannel,kubelet 就会判定为“插件不存在”,进而认为整个配置无效。
这三个环节环环相扣,少一个,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-plugin=cni \ # --cni-bin-dir=/opt/cni/bin \ # --cni-conf-dir=/etc/cni/net.d \ # --container-runtime=remote \ # --container-runtime-endpoint=unix:///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-plugin=cni这个参数必须存在,否则 kubelet 根本不会进入 CNI 初始化流程。如果它被误删或注释掉,你会看到NetworkReady=False,但日志里不会有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 2>&1 || echo "❌ Missing 'cniVersion'" jq -e '.plugins' "$CONFIG_FILE" >/dev/null 2>&1 || 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 2>&1; 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.0,jq脚本会立刻告诉你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 的实时日志(Ctrl+C 退出) 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: NetworkReady=false 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创建了 master,kubeadm join加入了一个 worker 节点,但kubectl get nodes显示 worker 一直是NotReady,describe 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-local)
Flannel 本身不处理 IPAM(IP地址分配),它依赖 CNI 标准插件host-local。所以必须先装好基础插件:
# 1. 下载最新版 CNI 插件(1.13 兼容,选 0.8.x 系列) # 官方发布页:https://github.com/containernetworking/plugins/releases # 我们用 0.8.7,它是 1.13 最稳定的搭档 CNI_VERSION="v0.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_VERSION="v0.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 nodes,NotReady就会变成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 "*.conflist" |
failed to load network config: failed to parse network configuration | .conf或.conflist文件 JSON 语法错误(多逗号、少引号、中文标点) | 用jq empty filename定位错误行 | jq empty /etc/cni/net.d/10-flannel.conflist |
Plugin 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/flannel | ls -l /opt/cni/bin/flannel |
permission deniedon config file | CNI 配置文件权限不足(如600),kubelet 无法读取 | sudo chmod 644 /etc/cni/net.d/*.conf* | ls -l /etc/cni/net.d/ |
NetworkReady=Falsebut nocni config uninitializedin logs | --network-plugin=cni参数缺失,或被注释 | 检查ps aux | grep kubelet,确认参数存在 | ps aux | grep kubelet | grep network-plugin |
5.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.0,jq可能验证通过,但 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.conflist,type字段是"calico"。你需要下载calico二进制(不是calicoctl),并确保/opt/cni/bin/calico存在且可执行。Calico 的cniVersion也必须是"0.3.1"。 - Weave:Weave 的 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 给你的一次考试——考的不是你记住了多少命令,而是你是否真正理解了这个系统是如何呼吸、如何思考、如何建立信任的。我踩过的坑,都写在了上面的每一步里。现在,轮到你了。