如果你执行kubeadm init时卡在 kubelet 启动环节,等一会儿终端里冒出一行error execution phase kubelet-start,下面跟着command failed" err="failed to parse kubelet flag: unknown flag: --network-plugin,那你遇到的和我是同一个问题。我最早在准备一个内部测试环境时碰到这报错,当时排了快一个下午才意识到,问题根本不在网络插件,而在 kubelet 自己压根没法按现有参数启动。这篇文章我就把这个坑的完整排查思路、根因和修复步骤整理出来,照着操作能省不少时间。
这个内容适合两类人:一类是刚接触 Kubernetes、第一次跑kubeadm init就撞上这个报错的新手;另一类是给线上或测试环境做集群安装,遇到 kubelet 启动异常但不想一上来就kubeadm reset清场的运维。核心思路不是“删了重来”,而是先搞清 kubelet 到底加载了哪些参数、参数从哪来、为什么会有一个它自己都不认识的 flag,然后再做针对性的清理和重试。
1. 问题现象:kubeadm init 卡住,kubelet 一直在重启
1.1 报错现场还原
先描述一下我当时的情形。环境是两台 CentOS 7.9 虚拟机,计划用 kubeadm 搭一个单控制面集群。我执行的是很常规的初始化命令:
sudo kubeadm init \ --apiserver-advertise-address=192.168.1.10 \ --pod-network-cidr=10.244.0.0/16 \ --kubernetes-version=v1.28.2前面预检阶段一切正常,走到类似下面这段输出时开始出问题:
[kubelet-start] Writing kubelet environment file with flags to file "/var/lib/kubelet/kubeadm-flags.env" [kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml" [kubelet-start] Starting the kubelet [kubelet-start] Waiting for the kubelet to boot up... [kubelet-check] Initial timeout of 40s passed.然后过了 40 秒左右,kubeadm 开始报错,错误信息里带着 kubelet 启动失败的细节:
error execution phase kubelet-start: a Node "node-01" with the same name and/or labels already exists这个报错信息比较有迷惑性,它说“同名节点已存在”,这其实是 kubelet 在重启过程中反复向 apiserver 注册节点导致的连锁结果。真正要看的不是这里,而是 kubelet 自己的日志。用 systemd 查一下:
sudo journalctl -u kubelet -n 100 --no-pager日志里翻到的最核心一行是:
failed to run Kubelet: failed to parse kubelet flag: unknown flag: --network-plugin到这一步,问题就明确了:kubelet 服务在解析启动参数时,遇到一个它不认识的--network-plugin参数,直接退出。systemd 又会自动拉起 kubelet,于是陷入“启动-崩溃-重启”的 CrashLoop,static pod 根本起不来,kubeadm 等待超时后失败。
1.2 kubelet 的 "unknown flag" 到底意味着什么
很多人第一次看到unknown flag会以为只是某个参数拼错了,但实际上这个错误背后是 kubelet 自己的参数解析机制。kubelet 是 Go 语言写的,用的是标准库flag和pflag两套解析逻辑。启动时会遍历所有命令行参数,逐个与已注册的参数定义做匹配,一旦遇到一个完全没有注册过的 flag,处理方式就是:打印错误,进程退出。
这种设计跟 nginx、MySQL 这类“遇到未知配置项就忽略或只告警”的服务完全不同。也就是说,kubelet 只要发现一个不认识的参数,不会跳过它继续跑,而是直接拒绝启动。这是 Kubernetes 组件一贯的严格参数校验风格,目的就是避免配置被静默忽略后出现难以定位的行为差异。
这里要特别说清楚:unknown flag和invalid value是两个不同层面的问题。
unknown flag: --xxx:kubelet 二进制压根不认识这个开关,通常意味着版本差异、参数被移除、或参数名拼写错误。invalid argument "xxx" for "--yyy" flag:kubelet 认识这个 flag,但你给的值不合法,比如网络相关的值填错、cgroup driver 填了不支持的值。
排查方向完全不一样,前者找“参数从哪来”,后者看“值为什么错”。
2. 顺着 kubeadm 的启动链路找参数来源
2.1 kubeadm init 从哪里把 kubelet 拉起来
要解决这个问题,先得明白 kubeadm init 过程中 kubelet 是在哪个阶段被启动的,以及它的启动参数是从哪里拼出来的。
kubeadm init有很多个阶段,其中和 kubelet 直接相关的叫kubelet-start。这个阶段做的事情大致有三件:
- 生成 kubelet 的环境变量文件
/var/lib/kubelet/kubeadm-flags.env。 - 生成 kubelet 的配置文件
/var/lib/kubelet/config.yaml。 - 通过 systemctl 启动 kubelet,并等待 kubelet 的健康检查通过。
关键在于第一件事。kubeadm-flags.env里存放的是一整串命令行参数,systemd 启动 kubelet 的时候会把这串参数原封不动地拼到 kubelet 的启动命令上。如果这个文件里出现 kubelet 不认识的参数,kubelet 就会立刻退出。
看下我当时这个文件的内容:
cat /var/lib/kubelet/kubeadm-flags.env输出大致是:
KUBELET_KUBEADM_ARGS="--network-plugin=cni --pod-infra-container-image=registry.k8s.io/pause:3.9 --provider-id=... --node-ip=192.168.1.10"问题就在这里出现了:--network-plugin=cni被写进了 kubelet 的命令行参数。
2.2 /var/lib/kubelet/kubeadm-flags.env 是主要嫌疑
kubeadm-flags.env是 kubeadm 在kubelet-start阶段生成的。它的作用是把 kubeadm 觉得“应该由 kubelet 命令行承载的参数”集中起来,供 systemd unit 文件读取。
如果你在/etc/systemd/system/kubelet.service.d/10-kubeadm.conf里看到类似这样的配置:
[Service] Environment="KUBELET_KUBECONFIG_ARGS=--bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --kubeconfig=/etc/kubernetes/kubelet.conf" Environment="KUBELET_CONFIG_ARGS=--config=/var/lib/kubelet/config.yaml" EnvironmentFile=-/var/lib/kubelet/kubeadm-flags.env ExecStart= ExecStart=/usr/bin/kubelet $KUBELET_KUBECONFIG_ARGS $KUBELET_CONFIG_ARGS $KUBELET_KUBEADM_ARGS $KUBELET_EXTRA_ARGS注意最后一行,kubelet 实际的启动命令把所有KUBELET_*变量都拼到了一起。其中:
KUBELET_KUBECONFIG_ARGS负责 kubeconfig 相关参数。KUBELET_CONFIG_ARGS负责指定 kubelet 配置文件。KUBELET_KUBEADM_ARGS从kubeadm-flags.env读入。KUBELET_EXTRA_ARGS是留给用户自定义的,通常来自/etc/default/kubelet或/etc/sysconfig/kubelet。
所以一个 kubelet 进程最终拿到的参数 = 上面四部分的叠加。任一部分出现不认识的 flag,都会导致failed to parse kubelet flag: unknown flag。
2.3 三个 KUBELET_* 变量叠加产生的"参数合并"
systemd 环境变量拼接参数的方式看似简单,实际排查时要多留个心眼:kubeadm-flags.env只是其中一个来源,还有两个地方也可能藏着不认识的参数。
第一处是KUBELET_EXTRA_ARGS。很多时候用户看文档说“设置 kubelet 额外参数”,会往/etc/default/kubelet里写:
KUBELET_EXTRA_ARGS="--network-plugin=cni --cni-conf-dir=/etc/cni/net.d --cni-bin-dir=/opt/cni/bin"这套写法在老版本上没问题,但在某些新版本 kubelet 上就会变成第一个“未知 flag”的来源。第二处是 kubelet 的--config指向的 YAML 文件/var/lib/kubelet/config.yaml。这个文件里如果写入了某个不再被 kubelet 支持的配置字段,报错往往不是unknown flag,而是unknown configuration key,但表现和排查路径很接近,我后面会专门对比。
建议你把以下三个文件全部检查一遍:
/var/lib/kubelet/kubeadm-flags.env/etc/default/kubelet或/etc/sysconfig/kubelet/var/lib/kubelet/config.yaml
3. 为什么会出现不认识的 flag:三个最常见根因
3.1 kubeadm 与 kubelet 版本代差
头号嫌疑是 kubeadm 和 kubelet 的版本不一致。
kubeadm 生成kubeadm-flags.env时,是根据自己内置的逻辑来生成参数的。如果 kubeadm 是较老版本,而 kubelet 是较新版本,情况可能还稍微好一点,因为老 kubeadm 生成的参数多半还在。反过来,如果 kubeadm 是较新版本,而 kubelet 是较老版本,kubeadm 可能按新版本的习惯生成参数,老 kubelet 自然不认识。
我那次就是典型的版本错位:系统里用 yum 装的东西,kubelet 来自一个较旧的 1.25 包,kubeadm 却被我手动用较新的 1.28 二进制覆盖了。kubeadm 在kubelet-start阶段生成的参数基于 1.28 的默认行为,而本地 kubelet 是 1.25,两个版本的 flag 集合不同,撞上未知参数几乎是必然。
kubeadm version -o short kubelet --version如果两者的主版本号差超过 1 个版本,优先怀疑这里。官方支持策略是 kubeadm 和 kubelet 的版本差保持在 minor version 正负 1 以内,跨大版本组合出问题的概率非常高。
3.2 参数被新版 kubelet 移除或改名
第二个常见原因是 kubelet 版本升级后,某些老参数被标记为 deprecated 然后移除。--network-plugin就是这类参数的典型代表。
在早期版本中,kubelet 使用--network-plugin=cni来声明使用 CNI 网络插件,配合--cni-bin-dir、--cni-conf-dir一起工作。后来 Kubernetes 推荐通过--config配置文件来传递这类设置,命令行 flag 逐步进入废弃流程。如果新版本把--network-plugin从参数列表里删掉了,旧文档里的写法就成了启动失败的导火索。
这里有一个很容易踩的坑:很多在旧版本上正常的安装文档、内部操作手册,会无脑把--network-plugin=cni写进 kubelet 的服务配置里。你照着文档操作,在新版本环境上就会看到unknown flag。所以排查时别光盯着 kubeadm 生成的参数,也要回头想想自己或手头文档是否添加过“历史遗留参数”。
3.3 手动残留与文档抄错
第三个原因很朴素:手动配置残留或参数名拼错。
比如有段时间 cni 相关参数在 kubelet 里真实的写法是--network-plugin=cni,但一些人会凭印象写成--network-policy、--network-plugin-dir、--network-cni之类的变体。哪怕只差一个字符,kubelet 也会老老实实回你一个unknown flag。
我自己还见过因为环境变量里参数带上了多余空格或引号,导致整个参数被截断的情况,比如:
KUBELET_EXTRA_ARGS=" --hostname-override=node-01 --network-plugin=cni"看似没问题,但如果在某个位置多了一个转义字符或制表符,systemd 解析EnvironmentFile时结果就会变得很怪,kubelet 最后收到的可能是一个半截参数,报错里甚至会出现unknown flag: --network-p这种被截断的提示。
遇到这种错误时,最快的处理办法是:先把所有自定义参数暂时清空,确认 kubelet 能裸启动,再逐步加回去。
4. 完整修复流程:从报错到集群可用
4.1 先让 kubelet 单独站出来说话
我遇到这类问题,第一反应不是改配置文件,而是先看 kubelet 在不带任何额外参数时能不能正常启动。因为混杂在 kubeadm 的启动链条里,很多问题会被表象掩盖。
先停掉 systemd 自动拉起:
sudo systemctl stop kubelet然后直接看 kubelet 支持的参数里有没有报错提到的那个 flag。这里不要用--help,因为 kubelet 的 help 输出很长,直接配合grep更高效:
/usr/bin/kubelet --help 2>&1 | grep -- '--network'如果--help里完全搜不到--network-plugin,那基本可以确认当前 kubelet 版本已经不认识这个参数了。如果搜到了,说明参数本身存在,问题更可能是拼写或格式异常。
也可以直接手跑一次 kubelet,不带 kubeadm 生成的任何参数,只看它会不会因为基础环境问题退出:
/usr/bin/kubelet --version systemd-run --unit=test-kubelet --description="test kubelet" \ /usr/bin/kubelet --kubeconfig=/etc/kubernetes/kubelet.conf \ --config=/var/lib/kubelet/config.yaml这个步骤能帮助你区分“参数解析失败”和“kubelet 运行环境有问题”两类情况。参数解析失败会在进程启动瞬间就报错退出,而环境问题通常会在日志里持续刷错误。
4.2 版本核对与参数清理
确定是unknown flag之后,按下面的顺序排查和清理。
先核对版本,确保 kubeadm 和 kubelet 属于同一主版本:
kubeadm version -o short kubelet --version如果版本差太大,最简单的方案是统一版本。以 1.28 为例:
sudo yum install -y kubelet-1.28.2 kubeadm-1.28.2 kubectl-1.28.2 sudo systemctl daemon-reload注意 kubelet 的版本变更后,一定执行systemctl daemon-reload,因为 kubelet 的 systemd unit 文件路径可能在安装时被调整,不重载的话 systemd 还在用旧的 ExecStart 配置。
如果版本没问题,或者临时没法升级,那就直接清理参数。
编辑/var/lib/kubelet/kubeadm-flags.env,把不存在的 flag 从KUBELET_KUBEADM_ARGS里去掉。比如清理--network-plugin=cni:
sudo sed -i 's/--network-plugin=cni //g' /var/lib/kubelet/kubeadm-flags.env sudo systemctl daemon-reload sudo systemctl restart kubelet同时检查/etc/default/kubelet或/etc/sysconfig/kubelet里的KUBELET_EXTRA_ARGS,把同样的问题参数一并清掉。这一步做完,再用前文的方法看日志:
sudo journalctl -u kubelet -n 50 --no-pager如果日志里出现类似Started Kubernetes kubelet、Running with systemd的字样,说明 kubelet 已经起来了。
4.3 环境重置后重跑 kubeadm init
如果 kubelet 起来之后,kubeadm init 早已因为超时失败,并且报错里有“同名节点已存在”这类信息,那么光修 kubelet 还不够,最稳妥的做法是彻底重置环境后重新初始化。
重置之前先确认没有重要的本地数据,然后执行:
sudo kubeadm reset -fkubeadm reset会清理/etc/kubernetes下的部分文件,但并不会删除/var/lib/kubelet和/var/lib/etcd里的全部内容。为了确保一个绝对干净的初始化环境,建议手动再清一轮:
sudo rm -rf /etc/kubernetes /var/lib/kubelet /var/lib/etcd /etc/cni/net.d注意这里的rm -rf有风险,只建议在确认不是生产环境的前提下执行。如果容器运行时用的是 containerd,最好也把 containerd 重启一遍,把之前残留的 pause 容器或网络命名空间清干净:
sudo systemctl restart containerd之后重新执行 kubeadm init:
sudo kubeadm init \ --apiserver-advertise-address=192.168.1.10 \ --pod-network-cidr=10.244.0.0/16 \ --kubernetes-version=v1.28.2这次kubelet-start阶段应该能顺利通过,kubeadm 会继续往下走,最终输出Your Kubernetes control-plane has initialized successfully。
4.4 集群就绪验证
初始化成功后,先按 kubeadm 给的提示配置 kubeconfig:
mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config然后看节点状态:
kubectl get nodes如果节点显示NotReady,别慌,这是因为还没有安装 CNI 网络插件。Calico、Flannel、Cilium 任选一个安装。以 Flannel 为例:
kubectl apply -f https://raw.githubusercontent.com/flannel-io/flannel/master/Documentation/kube-flannel.yml等一分钟左右再查一下:
kubectl get pods -A kubectl get nodes控制面组件和 kubelet 都处于 Running、节点变为 Ready,说明整个链路已经从“kubelet 启动失败”恢复到了正常状态。
5. kubelet 启动失败的避坑清单与排查速查
5.1 容易和 "unknown flag" 混在一起的其他报错
kubelet 启动失败有很多种姿势,unknown flag只是其中一类。把容易混淆的几种放在一起对比,排查时会更清晰。
| 报错特征 | 实际原因 | 排查方向 |
|---|---|---|
unknown flag: --network-plugin | 参数不存在、被移除或拼写错误 | 检查 kubeadm-flags.env、KUBELET_EXTRA_ARGS,版本对齐 |
unknown configuration key "networkPlugin" | kubelet 配置文件里有不认识的字段 | 检查 /var/lib/kubelet/config.yaml,字段名是否过期 |
failed to get cgroup stats | cgroup driver 与容器运行时不一致 | 检查 containerd 配置文件 systemdCgroup 是否为 true |
container runtime is not running | kubelet 找不到 CRI socket | 检查 containerd/cri-o 是否运行,/var/run/containerd/containerd.sock 是否存在 |
[ERROR Swap]: running with swap on is not supported | 预检时 swap 未关闭 | swapoff -a并注释 fstab |
其中unknown configuration key最容易被忽略,因为它不是在解析命令行 flag 时报错,而是在 kubelet 读取--config指定的 YAML 文件时失败。比如老配置里写networkPlugin: cni,新版本 kubelet 把这个字段改名或移到别处,就会直接报配置解析失败。排查办法是把/var/lib/kubelet/config.yaml里的可疑字段逐个与kubelet --help里列出的配置文件字段做比对。
5.2 我常用的排查命令与顺序
踩过几次坑之后,我形成了一套固定的排查顺序,分享出来供参考:
第一步,看服务状态和日志:
systemctl status kubelet journalctl -u kubelet -n 200 --no-pager第二步,看参数来源,检查 kubeadm 生成的文件和用户自定义文件:
cat /var/lib/kubelet/kubeadm-flags.env cat /etc/default/kubelet 2>/dev/null || cat /etc/sysconfig/kubelet 2>/dev/null cat /var/lib/kubelet/config.yaml第三步,确认版本一致性:
kubeadm version -o short kubelet --version crictl version第四步,确认容器运行时可用:
crictl info crictl ps -a这四步能筛掉绝大多数 kubelet 启动失败的场景。有一条经验很重要:不要在 kubelet 日志还没看清前就执行kubeadm reset。重置后旧日志被覆盖,现场就丢了。每年我都会看到有人因为“图省事直接 reset”导致问题复现但找不到原因。
5.3 几条长期受益的经验
最后说几条我自己的习惯。安装 Kubernetes 集群,哪怕是用 kubeadm 这种号称“一键初始化”的工具,也一定要把版本约束当回事。kubeadm、kubelet、kubectl 三件套建议同版本安装,不要混搭。你可以自己维护一个简单的版本清单,记录每个环境里三件套和容器运行时的版本,下次出问题直接对照。
另一个建议是不要照抄旧文档里的 kubelet 参数。早几年很多文档喜欢在 kubelet 上显式加--network-plugin=cni、--cni-bin-dir、--cni-conf-dir,但现在 kubelet 更推荐通过配置文件来管理这些设置。新环境上遇到unknown flag,优先怀疑这些“历史写法”,而不是怀疑机器有问题。
再补充一个小技巧:修改 kubelet 相关文件后,一定记得先systemctl daemon-reload再systemctl restart kubelet。很多人改了/etc/sysconfig/kubelet或 drop-in 文件后漏掉 daemon-reload,systemd 根本没加载新环境变量,kubelet 自然还是老配置在跑,排查半天也找不到问题。
kubelet 是集群里最容易出状况但又最关键的组件,它不像 apiserver 那样有清晰的外部 API,平时出问题只能靠日志和配置文件一步步定位。把参数来源链路捋清楚,把版本一致性当成默认前提,绝大多数kubeadm init启动失败都能在十分钟内解决。
我在实际处理中还有个体会:越是在压测或演示前临时搭的集群,越容易踩这种“参数不匹配”的坑,因为kubeadm init的预检只检查系统环境,不会校验 kubelet 参数的可解析性。所以搭完一个全新环境,别急着往下装网络插件,先手动看一下journalctl -u kubelet -n 20,确认 kubelet 是稳定运行状态再做下一步。这个习惯帮我避免了很多次“半路翻车”。