1. 报错现场还原与核心症结定位
kubeadm init 这个命令,但凡自己手动搭过 Kubernetes 集群的人,对它都是又爱又恨。爱的是它把 control plane 组件、证书、kubeconfig、etcd 这些繁琐的东西一把梭哈全给你安排明白;恨的是它一旦报错,信息量巨大,新手往往盯着满屏日志不知道从哪下手。我前前后后在不同环境里初始化过不下几十次集群,踩过的坑从端口占用到镜像拉取失败,从 cgroup 驱动不匹配到 swap 没关,几乎集齐了一套“kubeadm 报错图鉴”。这篇就专门聊第二类高频报错——也就是初始化过程中与容器运行时、网络插件前置条件、系统内核参数相关的那一批问题,这类报错往往不是 kubeadm 本身的问题,而是它依赖的底层环境没准备好。
先明确一个认知:kubeadm init 报错大致可以分成几个阶段。第一阶段是Preflight 检查,也就是命令刚跑起来那几秒,它会检查端口、swap、内核模块、容器运行时状态等;第二阶段是证书与配置文件生成,这一步一般不太会出问题;第三阶段是拉取镜像并启动静态 Pod,这里容易卡在镜像拉取和容器运行时通信上;第四阶段是等待 control plane 就绪,如果 kubelet 起不来或者网络插件没装,就会一直卡在等待 API Server 的阶段。我这次要拆解的“报错 02”,主要聚焦在 Preflight 阶段和容器运行时交互阶段的典型故障,因为这两类问题在实际操作中占比最高,也最容易让人一头雾水。
你可能会问,为什么同样的命令在别人机器上跑得好好的,到我这就报错?答案往往藏在环境差异里。Kubernetes 对底层环境的要求其实相当挑剔:它要求容器运行时实现了 CRI 接口、要求内核开启了特定模块、要求 swap 关闭、要求 iptables 能正常转发流量。任何一个条件不满足,kubeadm 就会在 Preflight 阶段直接把你拦下来。所以解决这类报错的核心思路不是去改 kubeadm 的参数,而是把底层环境对齐到 Kubernetes 的期望状态。下面我会按照“先定位、再拆解、后实操”的顺序,把这类报错的排查逻辑和解决手法完整讲一遍。
提示:遇到 kubeadm init 报错时,第一件事不是去搜索引擎复制粘贴命令,而是把终端输出完整读一遍。kubeadm 的报错信息其实写得相当清楚,它会用
[ERROR ...]标出具体哪一项检查没过,后面通常还跟着一句人话解释。很多人跳过这一步直接搜报错关键词,结果搜到的方案和实际原因对不上,越修越乱。
2. 容器运行时与 CRI 接口的匹配逻辑
2.1 为什么容器运行时是报错重灾区
Kubernetes 从 1.24 版本开始移除了对 Docker Shim 的支持,也就是说,kubelet 不再通过 dockershim 去跟 Docker 对话,而是直接通过 CRI(Container Runtime Interface)跟容器运行时通信。这个变化影响巨大,因为在此之前,很多人装完 Docker 就以为万事大吉,kubeadm 能自动识别 Docker。但 1.24 之后,你必须显式安装一个实现了 CRI 的运行时,比如 containerd 或者 CRI-O,并且要确保 kubelet 配置里指向的 socket 路径是对的。
我见过最多的报错就是这一类:kubeadm init 跑到 Preflight 阶段,直接甩出一句[ERROR CRI]: container runtime is not running,或者failed to get container runtime status。这个报错的字面意思是“容器运行时没在跑”,但实际情况可能是运行时明明在跑,只是 kubelet 找不到它,或者 CRI 插件没启用。containerd 默认配置里,CRI 插件在某些发行版打包的版本中是被注释掉的,你需要手动把disabled_plugins = ["cri"]这一行改掉或者删掉,然后重启 containerd。这个细节非常隐蔽,因为systemctl status containerd显示的是 active (running),你会误以为一切正常。
另一个高频问题是 socket 路径不匹配。containerd 默认的 CRI socket 在/run/containerd/containerd.sock,而 CRI-O 在/var/run/crio/crio.sock。kubeadm 在初始化时会尝试自动探测,但如果系统里同时装了 Docker 和 containerd,它可能探测到 Docker 的 socket,然后因为版本不兼容而报错。这时候你需要在 kubeadm 的配置文件里显式指定nodeRegistration.criSocket字段,或者在命令行用--cri-socket参数指定。我个人的习惯是,只要机器上装了多个运行时,就一定在配置文件里写死 socket 路径,避免 kubeadm 猜错。
2.2 containerd 配置的关键参数拆解
containerd 的配置文件通常在/etc/containerd/config.toml,但如果你是用包管理器安装的,这个文件可能不存在,需要用containerd config default > /etc/containerd/config.toml生成一份默认配置。生成之后,有几个参数必须检查。第一个是disabled_plugins,确保里面没有"cri"。第二个是SystemdCgroup,这个参数决定了 containerd 是用 systemd 还是 cgroupfs 来管理 cgroup。Kubernetes 官方推荐用 systemd,因为大多数现代 Linux 发行版的 init 系统就是 systemd,用 systemd 驱动可以避免资源管理上的冲突。如果这个参数设成 false,而 kubelet 那边用的是 systemd,就会出现 cgroup 驱动不一致的问题,导致 Pod 启动失败。
第三个关键点是 sandbox 镜像。containerd 需要一个 pause 镜像来创建 Pod 的 sandbox 环境,默认配置里这个镜像是从外网拉取的。如果你的环境网络受限,这个镜像拉不下来,kubeadm init 就会卡在启动静态 Pod 的阶段。解决办法是提前把 pause 镜像拉到本地,或者配置一个可用的镜像仓库地址。我一般会在初始化之前,先用crictl pull命令手动拉一下 pause 镜像,确认能拉通再跑 kubeadm,这样能把问题提前暴露出来。
# 生成 containerd 默认配置 containerd config default > /etc/containerd/config.toml # 检查并修改关键参数 # 1. 确保 cri 插件未被禁用 # 2. 设置 SystemdCgroup = true # 3. 配置 sandbox_image 为可访问的镜像地址 # 修改后重启 containerd systemctl restart containerd systemctl enable containerd注意:修改 containerd 配置后一定要重启服务,而且重启之后最好用
crictl info命令验证一下 CRI 插件是否正常加载。如果crictl info报错说连不上 socket,那说明 containerd 的 CRI 插件还是没启用,或者 socket 路径不对。这个验证步骤能帮你省下大量反复跑 kubeadm 的时间。
2.3 crictl 工具的正确使用姿势
crictl是排查容器运行时问题的利器,它直接跟 CRI 接口对话,不依赖 Docker。但很多人第一次用 crictl 会遇到connection refused的报错,这是因为 crictl 默认去连/var/run/dockershim.sock或者别的路径,而不是你实际使用的运行时 socket。你需要配置/etc/crictl.yaml文件,把runtime-endpoint和image-endpoint指向正确的 socket 路径。配置好之后,crictl ps能列出当前运行的容器,crictl images能列出本地镜像,crictl pull能拉取镜像。这几个命令在排查 kubeadm 报错时非常有用,比如你可以用crictl ps -a看看 kubelet 有没有成功创建静态 Pod 的容器,如果容器创建了但一直退出,就去看crictl logs输出的日志。
我个人的排查习惯是,kubeadm init 卡住或者报错时,先开另一个终端,用crictl ps -a看容器状态,再用journalctl -u kubelet -f实时看 kubelet 日志。这两个信息源结合起来,基本能定位到问题出在哪个环节。比如如果crictl ps -a里能看到 kube-apiserver 的容器但状态是 Exited,那就去看它的日志,通常是证书问题或者端口冲突;如果连容器都没创建,那说明 kubelet 根本没跟运行时通信上,问题就在 CRI 配置或者 kubelet 配置上。
3. 系统内核参数与网络前置条件实操
3.1 swap 关闭与内核模块加载
Kubernetes 对 swap 的态度非常明确:必须关闭。这是因为 kubelet 的内存管理机制假设节点上没有 swap,如果开了 swap,Pod 的内存限制和调度行为会变得不可预测。kubeadm init 在 Preflight 阶段会检查 swap 状态,如果发现 swap 是开着的,直接报错[ERROR Swap]: running with swap on is not supported。关闭 swap 的命令很简单,swapoff -a临时关闭,然后编辑/etc/fstab把 swap 那一行注释掉,永久生效。但这里有个坑:有些云主机或者虚拟机镜像默认开了 swap,而且/etc/fstab里可能没有 swap 条目,swap 是通过 systemd 的 swap 单元管理的。这种情况下你需要用systemctl mask把对应的 swap 单元屏蔽掉,否则重启之后 swap 又回来了。
内核模块方面,Kubernetes 需要br_netfilter和overlay这两个模块。overlay是给容器文件系统用的,br_netfilter是给 iptables 做网桥流量过滤用的。这两个模块在大多数现代内核里都有,但默认可能没加载。你需要用modprobe手动加载,并且把它们写进/etc/modules-load.d/k8s.conf文件里,确保开机自动加载。如果br_netfilter没加载,kube-proxy 的 iptables 规则可能不生效,导致 Service 无法访问。这个问题的隐蔽性在于,kubeadm init 本身可能不报错,但集群起来之后网络不通,排查起来更麻烦。
# 关闭 swap swapoff -a # 永久关闭:注释 /etc/fstab 中的 swap 行 # 或者屏蔽 systemd swap 单元 systemctl mask dev-sda2.swap # 根据实际设备名调整 # 加载内核模块 modprobe overlay modprobe br_netfilter # 写入开机自动加载 cat <<EOF > /etc/modules-load.d/k8s.conf overlay br_netfilter EOF3.2 sysctl 网络参数调优
除了内核模块,还有几个 sysctl 参数必须设置。net.bridge.bridge-nf-call-iptables和net.bridge.bridge-nf-call-ip6tables要设成 1,这样网桥上的流量才会经过 iptables 处理,kube-proxy 的规则才能生效。net.ipv4.ip_forward要设成 1,开启 IP 转发,否则 Pod 之间的跨节点通信会失败。这些参数在 kubeadm 的 Preflight 检查里也会被验证,如果没设置,会报[ERROR FileContent--proc-sys-net-bridge-bridge-nf-call-iptables]: /proc/sys/net/bridge/bridge-nf-call-iptables contents are not set to 1这样的错误。
设置方法有两种,一种是临时用sysctl -w命令,另一种是写进/etc/sysctl.d/k8s.conf文件永久生效。我建议两种都做,临时设置让当前会话立即生效,永久设置保证重启后不丢失。写文件的时候注意格式,每行一个参数,等号两边可以有空格也可以没有,但参数名和值必须写对。写完之后用sysctl --system重新加载所有配置文件,然后用sysctl net.ipv4.ip_forward验证一下值是不是 1。
# 临时设置 sysctl -w net.bridge.bridge-nf-call-iptables=1 sysctl -w net.bridge.bridge-nf-call-ip6tables=1 sysctl -w net.ipv4.ip_forward=1 # 永久设置 cat <<EOF > /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 sysctl --system提示:
br_netfilter模块必须先加载,/proc/sys/net/bridge/目录才会存在,sysctl 参数才能设置成功。如果你直接设置 sysctl 参数报错说文件不存在,先检查模块有没有加载。这个顺序问题很多人会忽略,导致反复设置不生效。
3.3 端口占用与防火墙策略
kubeadm init 在 Preflight 阶段会检查 control plane 需要的端口是否被占用。API Server 默认监听 6443,etcd 监听 2379 和 2380,kubelet 监听 10250,controller-manager 和 scheduler 分别监听 10257 和 10259。如果这些端口中有任何一个被别的进程占了,kubeadm 就会报错。常见的占用者包括之前装过 Kubernetes 残留的进程、手动启动的 etcd、或者某些监控代理。排查方法是先用ss -tlnp或者netstat -tlnp列出所有监听端口,找到冲突的进程,要么停掉它,要么给 kubeadm 换端口。
防火墙方面,如果你的机器开了 firewalld 或者 ufw,需要放行这些端口,或者干脆在初始化阶段临时关闭防火墙。我一般建议在搭建集群的初期先关闭防火墙,等集群跑通了再按需开放端口,这样能排除防火墙带来的干扰。但要注意,生产环境不能长期关闭防火墙,该开的端口还是要开。另外,iptables 的 FORWARD 链默认策略如果是 DROP,也会导致 Pod 跨节点通信失败,需要把它改成 ACCEPT,或者添加明确的放行规则。
# 查看端口占用 ss -tlnp | grep -E '6443|2379|2380|10250|10257|10259' # 临时关闭防火墙 systemctl stop firewalld systemctl disable firewalld # 检查 iptables FORWARD 链策略 iptables -L FORWARD -n # 如果策略是 DROP,改成 ACCEPT iptables -P FORWARD ACCEPT4. kubeadm 配置文件与镜像拉取避坑指南
4.1 用配置文件代替命令行参数
很多新手习惯直接用kubeadm init --pod-network-cidr=10.244.0.0/16这样的命令行参数来初始化,但一旦参数多了,命令行会变得很长,而且容易写错。更稳妥的做法是写一个 kubeadm 配置文件,把所有参数都写进去,然后用kubeadm init --config指定。配置文件的好处是可版本化、可复用、可读性强,而且一些高级参数只能通过配置文件设置,比如nodeRegistration.criSocket、imageRepository、apiServer.extraArgs等。
配置文件的基本结构包括apiVersion、kind、kubernetesVersion、imageRepository、networking、nodeRegistration等字段。kubernetesVersion指定你要装的版本,imageRepository指定镜像仓库地址,如果你在国内环境,这个字段特别重要,因为默认的registry.k8s.io可能访问不了。networking.podSubnet指定 Pod 网段,这个网段不能和节点所在网段重叠,否则路由会出问题。nodeRegistration.criSocket指定容器运行时的 socket 路径,前面说过,多运行时环境下一定要写死。
apiVersion: kubeadm.k8s.io/v1beta3 kind: InitConfiguration nodeRegistration: criSocket: unix:///run/containerd/containerd.sock imagePullPolicy: IfNotPresent --- apiVersion: kubeadm.k8s.io/v1beta3 kind: ClusterConfiguration kubernetesVersion: v1.28.0 imageRepository: registry.aliyuncs.com/google_containers networking: podSubnet: 10.244.0.0/16 serviceSubnet: 10.96.0.0/124.2 镜像拉取失败的排查与解决
镜像拉取失败是 kubeadm init 最常见的卡点之一。报错信息通常是failed to pull image或者ImagePullBackOff,然后 kubeadm 会提示你手动拉取缺失的镜像。这个问题的根源在于默认镜像仓库在国内访问不稳定,解决办法有三种:一是配置镜像加速器,二是用imageRepository指向国内可访问的镜像仓库,三是提前手动拉取所有需要的镜像。
我一般用第二种方法,在配置文件里把imageRepository设成国内某云厂商提供的镜像地址,然后跑kubeadm config images pull --config先把镜像拉下来。这个命令会按照配置文件里的版本和仓库地址拉取所有 control plane 需要的镜像,拉取成功后再跑kubeadm init,就不会卡在镜像拉取上了。如果某个镜像还是拉不下来,可以用crictl pull单独拉,拉完之后用crictl tag给它打上 kubeadm 期望的标签。这个技巧在镜像仓库同步延迟的时候特别有用。
# 提前拉取所有镜像 kubeadm config images pull --config kubeadm-config.yaml # 如果某个镜像拉取失败,手动拉取并打标签 crictl pull registry.aliyuncs.com/google_containers/kube-apiserver:v1.28.0 crictl pull registry.aliyuncs.com/google_containers/kube-controller-manager:v1.28.0 # ... 其他镜像同理 # 查看本地镜像列表 crictl images注意:
kubeadm config images pull命令会读取配置文件里的kubernetesVersion和imageRepository,所以配置文件一定要写对。如果版本号写成了v1.28而不是v1.28.0,拉取会失败。另外,如果你用的是 containerd,拉取镜像时可能会遇到http: server gave HTTP response to HTTPS client的报错,这说明镜像仓库是 HTTP 的,需要在 containerd 配置里给这个仓库加上insecure_skip_verify或者配置 TLS 证书。
4.3 初始化成功后的收尾工作
kubeadm init 成功之后,终端会输出一段kubeadm join命令,这是给工作节点加入集群用的,一定要保存好。同时它还会提示你配置 kubectl 的 kubeconfig 文件,也就是把/etc/kubernetes/admin.conf复制到$HOME/.kube/config。这一步不做的话,kubectl 命令会报The connection to the server localhost:8080 was refused。复制完之后,用kubectl get nodes验证一下,应该能看到 control plane 节点,但状态是NotReady,因为网络插件还没装。
网络插件是集群能正常工作的最后一块拼图。Calico、Flannel、Cilium 这些插件选一个装上,装完之后节点状态会变成Ready。装网络插件的时候要注意,它的 Pod CIDR 必须和 kubeadm 配置文件里的podSubnet一致,否则路由会冲突。我一般用 Calico,因为它的文档全、社区活跃、排查工具多。装完之后用kubectl get pods -n kube-system看看所有系统 Pod 是不是都 Running,如果有 Pending 或者 CrashLoopBackOff,就去看对应 Pod 的日志。
# 配置 kubectl mkdir -p $HOME/.kube cp /etc/kubernetes/admin.conf $HOME/.kube/config chown $(id -u):$(id -g) $HOME/.kube/config # 验证节点状态 kubectl get nodes # 安装 Calico 网络插件 kubectl apply -f https://docs.projectcalico.org/manifests/calico.yaml # 查看系统 Pod 状态 kubectl get pods -n kube-system -w5. 高频报错速查与独家排查心得
5.1 常见报错对照表
| 报错关键词 | 根本原因 | 解决方向 |
|---|---|---|
container runtime is not running | CRI 插件未启用或 socket 路径不对 | 检查 containerd 配置,确认 cri 未被禁用,配置 crictl.yaml |
running with swap on is not supported | swap 未关闭 | swapoff -a并永久关闭,检查 systemd swap 单元 |
bridge-nf-call-iptables contents are not set to 1 | 内核模块未加载或 sysctl 未设置 | 加载 br_netfilter,设置 sysctl 参数 |
failed to pull image | 镜像仓库不可访问 | 配置 imageRepository,提前拉取镜像 |
port 6443 is in use | 端口被占用 | 用 ss 排查占用进程,停掉或换端口 |
kubelet is not running | kubelet 服务未启动或配置错误 | 检查 kubelet 状态和日志,确认配置指向正确的运行时 |
node not found | 节点未注册或 hostname 不一致 | 检查 hostname 和 /etc/hosts,确认 kubelet 注册成功 |
5.2 我的排查顺序与工具组合
遇到 kubeadm init 报错,我通常按照这个顺序排查:第一步,看终端输出的[ERROR ...]具体是哪一项检查没过,这是最直接的线索;第二步,如果 Preflight 过了但卡在后面,用crictl ps -a看容器状态,用journalctl -u kubelet -f看 kubelet 日志;第三步,如果容器起来了但一直重启,用crictl logs看具体容器的日志;第四步,如果所有容器都正常但节点 NotReady,检查网络插件和 kube-proxy 的状态。
这个顺序的逻辑是从外到内、从粗到细。先看 kubeadm 自己的检查结果,再看 kubelet 和运行时的交互,最后看具体组件的日志。每一步都有明确的工具和命令,不需要凭感觉猜。我见过很多人一上来就改配置、重装系统,结果问题没解决还把环境搞乱了。其实只要按照这个顺序走,大部分问题都能在十分钟内定位到。
提示:
journalctl -u kubelet -f这个命令在排查时几乎是万能的。kubelet 是节点上的核心代理,它负责跟容器运行时通信、管理 Pod 生命周期、上报节点状态。kubelet 的日志里会记录它跟 CRI 的每一次交互,如果 CRI 有问题,日志里会有明确的报错。我建议在跑 kubeadm init 之前就开一个终端跑这个命令,这样初始化过程中的所有 kubelet 行为都能实时看到。
5.3 几个容易忽略的细节
第一个细节是 hostname。kubeadm 默认用机器的 hostname 作为节点名注册到集群,如果 hostname 包含大写字母或者特殊字符,可能会导致注册失败。另外,如果/etc/hosts里没有把 hostname 解析到本机 IP,kubelet 启动时可能会报错。我一般会在初始化之前用hostnamectl set-hostname把 hostname 改成全小写的简单名字,然后在/etc/hosts里加一行127.0.0.1 <hostname>。
第二个细节是时间同步。Kubernetes 的证书和 token 都有有效期,如果节点时间偏差太大,证书验证会失败。我遇到过一台虚拟机因为休眠导致时间落后了几个小时,kubeadm init 一直报证书相关的错误,排查了半天才发现是时间问题。所以初始化之前用timedatectl确认一下时间同步服务是开着的,时间偏差在秒级以内。
第三个细节是 SELinux。在某些发行版上,SELinux 默认是 enforcing 模式,它会阻止容器运行时访问某些文件,导致 kubelet 启动失败。解决办法是临时用setenforce 0设成 permissive,或者永久修改/etc/selinux/config把SELINUX=enforcing改成SELINUX=permissive。这个坑在 CentOS 和 RHEL 系上特别常见,很多人装完系统直接跑 kubeadm,结果被 SELinux 拦住了还不知道。
# 检查 hostname hostnamectl cat /etc/hosts # 检查时间同步 timedatectl status # 检查 SELinux 状态 getenforce # 如果返回 Enforcing,临时设为 Permissive setenforce 05.4 重置集群的正确姿势
如果 kubeadm init 失败了很多次,环境已经被搞乱了,最稳妥的做法是重置之后再重来。kubeadm reset命令会清理掉 kubeadm 创建的证书、配置文件和静态 Pod 容器,但它不会清理网络插件的配置和 iptables 规则。所以完整的重置流程是:先kubeadm reset -f,然后手动清理 CNI 配置目录/etc/cni/net.d/,再清理 iptables 规则,最后重启 kubelet 和容器运行时。如果不清 iptables,残留的规则可能会干扰下一次初始化。
我个人的习惯是,在重置之前先把 kubelet 停掉,避免它在重置过程中又创建新的容器。重置完成之后,用crictl ps -a确认没有残留的容器,用iptables -L -n确认没有残留的 KUBE- 开头的链。确认干净之后再重新跑 kubeadm init,成功率会高很多。这个流程看起来繁琐,但比在脏环境上反复试错要高效得多。
# 停止 kubelet systemctl stop kubelet # 重置集群 kubeadm reset -f # 清理 CNI 配置 rm -rf /etc/cni/net.d/* # 清理 iptables 规则 iptables -F && iptables -t nat -F && iptables -t mangle -F && iptables -X # 重启容器运行时 systemctl restart containerd # 确认无残留容器 crictl ps -a6. 从报错中积累的环境预检清单
6.1 初始化前的十分钟自检流程
与其等 kubeadm 报错再去排查,不如在初始化之前花十分钟做一遍自检。我整理了一个自检清单,每次搭新集群之前都过一遍,能避免百分之九十以上的 Preflight 报错。第一步,确认 swap 已关闭,free -h看 swap 那一行是不是全零;第二步,确认内核模块已加载,lsmod | grep -E 'br_netfilter|overlay'看有没有输出;第三步,确认 sysctl 参数已设置,sysctl net.ipv4.ip_forward net.bridge.bridge-nf-call-iptables看值是不是 1;第四步,确认容器运行时正常,crictl info能返回运行时信息;第五步,确认端口没被占用,ss -tlnp看关键端口是否空闲;第六步,确认镜像能拉取,kubeadm config images pull能成功。
这个清单看起来简单,但每一步都对应着 kubeadm Preflight 检查里的一项。提前做完这些检查,kubeadm init 基本就能一次通过。我见过太多人跳过自检直接跑 kubeadm,然后被各种报错搞得焦头烂额,最后回头一看,全是这些基础环境问题。花十分钟自检,省一小时排查,这笔账怎么算都划算。
6.2 不同发行版的差异处理
不同的 Linux 发行版在这些前置条件上有一些细微差异,需要特别注意。Ubuntu 和 Debian 系默认可能没装br_netfilter模块,需要手动modprobe;CentOS 和 RHEL 系默认开了 SELinux 和 firewalld,需要额外处理;某些云厂商的定制镜像可能预装了 Docker 并且开了 swap,需要先清理掉。我在不同发行版上搭集群时,会先确认发行版版本和内核版本,然后针对性地调整自检清单里的步骤。
比如在 CentOS 上,除了关 swap、加载模块、设 sysctl 之外,还要额外做两件事:一是把 SELinux 设成 permissive,二是把 firewalld 停掉或者放行端口。在 Ubuntu 上,则要注意ufw的状态,以及containerd的包来源,因为 Ubuntu 官方源里的 containerd 版本可能比较旧,建议用 Docker 官方源或者 containerd 官方源安装。这些差异看起来琐碎,但每一条都可能导致 kubeadm init 失败,所以值得花时间了解清楚。
6.3 把排查经验固化成脚本
搭集群的次数多了之后,我把这些自检步骤和排查命令写成了一个 shell 脚本,每次初始化之前跑一遍,有问题它会直接提示。脚本里包括检查 swap、检查模块、检查 sysctl、检查端口、检查容器运行时、拉取镜像等步骤。这个脚本帮我省了很多重复劳动,也避免了因为疏忽漏掉某个检查项。如果你经常搭集群,强烈建议也搞一个类似的脚本,把经验固化下来。
脚本不需要写得很复杂,就是把那些命令串起来,加上一些条件判断和输出提示。比如检查 swap 的时候,用free -h | grep Swap | awk '{print $2}'取出 swap 总量,如果不等于 0 就输出警告。检查模块的时候,用lsmod | grep -c br_netfilter判断模块是否加载。这些判断逻辑都很简单,但组合起来就是一个很实用的自检工具。我现在的脚本已经迭代了好几个版本,每次遇到新的报错就加一条检查,慢慢就覆盖了大部分常见问题。
#!/bin/bash # k8s 环境自检脚本 echo "=== 检查 swap ===" if [ "$(free -h | grep Swap | awk '{print $2}')" != "0B" ]; then echo "警告:swap 未关闭" else echo "OK:swap 已关闭" fi echo "=== 检查内核模块 ===" for mod in br_netfilter overlay; do if lsmod | grep -q "^$mod"; then echo "OK:$mod 已加载" else echo "警告:$mod 未加载" fi done echo "=== 检查 sysctl 参数 ===" for param in net.ipv4.ip_forward net.bridge.bridge-nf-call-iptables; do value=$(sysctl -n $param 2>/dev/null) if [ "$value" = "1" ]; then echo "OK:$param = 1" else echo "警告:$param = $value" fi done echo "=== 检查容器运行时 ===" if crictl info >/dev/null 2>&1; then echo "OK:容器运行时正常" else echo "警告:容器运行时异常" fi echo "=== 检查关键端口 ===" for port in 6443 2379 2380 10250 10257 10259; do if ss -tlnp | grep -q ":$port "; then echo "警告:端口 $port 被占用" else echo "OK:端口 $port 空闲" fi done这个脚本我放在每台新节点的/root/k8s-precheck.sh,初始化之前跑一遍,心里就有底了。脚本的输出很直观,哪一项有问题一眼就能看到,然后针对性地去解决。比起盲目跑 kubeadm 然后对着报错发呆,这种方式效率高太多了。而且脚本本身也是可以持续迭代的,每次遇到新问题就加一条检查,久而久之就成了一套完整的预检体系。
6.4 关于报错排查的心态建设
最后聊点心态上的东西。kubeadm init 报错其实是一件好事,它在告诉你环境哪里不符合要求。每一次报错都是一次学习的机会,解决了之后你对 Kubernetes 底层依赖的理解就深了一层。我刚开始搭集群的时候,一个报错能折腾一整天,现在大部分报错看一眼就知道问题在哪,这种进步就是靠一次次踩坑积累出来的。所以遇到报错不要慌,按照“读报错、查日志、定位环节、针对性解决”的流程走,大部分问题都能搞定。
另外,不要害怕重置集群。很多人觉得重置了之前的努力就白费了,其实不是。重置只是清理掉 kubeadm 创建的东西,你的系统环境、容器运行时、网络配置都还在。重置之后重新初始化,往往比在脏环境上修修补补要快得多。我现在的习惯是,同一个环境初始化失败超过三次,就直接重置重来,不纠结。这样反而能更快地找到问题根源,因为干净的环境排除了之前操作的干扰。
提示:如果你在排查过程中改了某个配置,记得记录下来改了什么、为什么改。下次遇到类似问题,这份记录就是你的排查手册。我自己的笔记里记了几十条这样的配置变更和对应的报错现象,现在遇到新问题,先翻笔记,很多时候直接就能找到答案。这种积累比任何教程都管用,因为它是你亲手踩出来的经验。