☰
Kubernetes 集群初始化实践:kubeadm init 踩坑与排障全记录
2026/9/28 12:44:08 网站建设 项目流程

前阵子重装了一套 Kubernetes 环境,版本选了 v1.26.0,走的是 kubeadm init 的老路。整个过程谈不上顺利,从 [init] Using Kubernetes version: v1.26.0 到 [preflight] Running pre-flight checks,再到节点终于变成 Ready,中间隔了整整一个晚上和好几轮排障。趁记忆还热,把这轮工作的判断和踩坑按散记的格式记下来,这是《Kubernetes 散记》的第 1 篇,聚焦初始化阶段。

为什么叫散记而不是教程?教程的职责是让读者照做能成,所以每一步都默认没有歧义;但真实世界里踩坑的顺序、做过的取舍、失败后的心态,全都被省略了。散记把这些东西保留下来:既能当步骤参考,也能让人知道某个参数为什么值得填成这样。如果你已经看过一遍官方入门文档,正打算动手搭自己的集群,这篇会比较对味;如果公司里让你从零交接一套环境,里面不少细节也能帮你少走弯路。

1. 散记的定位:记录决策过程,而不是复述官方文档

1.1 这个系列记录的到底是什么

散记和教程最大的区别在于:教程默认所有步骤都应该顺理成章,散记则把实际操作里那些反复、犹豫、查文档的过程全记录在案。碰到 Kubernetes 这种链条极长的系统,如果只看“官方步骤 + 成功截图”,很容易产生一种错觉——只要命令输对了,集群就起来了。事实上 kubeadm init 前后牵涉到内核参数、容器运行时、网络模型、证书、令牌等多个层面,任何一个环节有偏差,最后都会以某种诡异的方式暴露出来。写散记就是想把这些暴露的过程还原出来。

这一篇聚焦初始化阶段。读完你能得到几个明确的东西:第一,为什么我最终选了 v1.26.0 而不是最新版;第二,containerd 配置里哪两个字段是决定成败的;第三,pre-flight checks 每个检查项背后的动机;第四,从 NotReady 到 Ready 的排障路径。这些都是可复现、可验证的内容,不是感想式的东西。每段里出现的命令我会尽量解释为什么这么写,而不是只丢给你一串“能跑就行”的脚本。

1.2 哪些人读这篇收获最大

我默认读者具备基本的 Linux 操作能力:会编辑文件、能看 systemd 日志、理解网段和端口。如果连 vim 和 grep 都不熟,建议先补一下基础再回来。这篇散记对三类人最有用:第一类是刚把官方入门文档翻完、准备在自己机器上搭环境的人,可以少走很多弯路;第二类是已经搭好集群但经常处理 CoreDNS CrashLoopBackOff、镜像拉取失败等问题的运维,里面不少命令可以直接抄;第三类是需要在团队里做知识交接的工程师,把散记当作底稿整理成内部文档会省力很多。

老手也能看,因为 Kubernetes 初始化里的坑有很强的周期性。你过个一年半载重新搭环境,碰到的多半还是这几个问题:cgroup 驱动不一致、镜像仓库拉不动、CNI 和 Pod 网段对不上。把这些底层关系理清之后,无论版本怎么变,你都有快速定位问题的底气。

2. 动手前先想清楚:版本、服务器和运行时选型

2.1 v1.26.0 是一个耐用的版本号

先讲版本。v1.26.0 是 2022 年 12 月发布的,到现在已经过了多个 patch 版本,生态上的大组件基本都跟上来了。很多生产环境至今还在 1.24、1.25 上跑,说明这个版本区间对周边工具的兼容性足够稳。相比更早的版本,1.26 比较大的变化是清理了一批 v1beta1、v1beta2 的旧 API,PodSecurity 等新特性也进入了普遍可用阶段。对于使用者来说,你在网上能搜到的绝大多数解决方案、实践案例都能直接套用,不用像追新版本那样担心插件还没适配。

为什么不冲最新版?Kubernetes 每三个月发布一个大版本,最新版刚出来的时候,网络插件、监控组件、Ingress 控制器这些配套工具未必第一时间完成兼容验证。我搭环境的目的不是试验新特性,而是求稳。为什么不退回更老?老版本的镜像同步源里很多组件镜像已经下架或者难找,预拉镜像时会非常痛苦。1.26 正好卡在“往前能兼容、往后不太老”的舒适区,这也是很多发行版引导用户安装的位置。

2.2 主机配置与内核参数:这是 pre-flight 的前置条件

控制面节点官方要求至少 2 核 2G,但那是“能跑”的标准。我这次用的是一台 4 核 8G 的虚拟机,跑单集群加几个工作负载基本没有压力。如果你只在笔记本上起 VM 学习,2 核 4G 是舒服的底线;1 核的话你会发现 kubeadm init 阶段就慢得让人怀疑人生,后面组件一多更是卡到没法用。

系统我选 Ubuntu 22.04 LTS,内核 5.15。动手前先做四件事:关 swap、加载桥接模块、调内核转发、确认主机名。命令如下:

swapoff -a sed -i '/ swap /s/^/#/' /etc/fstab modprobe br_netfilter cat >/etc/modules-load.d/k8s.conf <<EOF br_netfilter EOF cat >/etc/sysctl.d/k8s.conf <<EOF net.bridge.bridge-nf-call-iptables = 1 net.bridge.bridge-nf-call-ip6tables = 1 net.ipv4.ip_forward = 1 EOF sysctl --system

这些都是干什么的?bridge-nf-call-iptables 让经过 Linux 网桥的 IPv4/IPv6 流量也走 iptables 规则。Kubernetes 里 kube-proxy 依赖 iptables 或 ipvs 来转发 Service 流量,如果这个开关不打开,集群内容易出现“节点能 ping 通,但 Service 访问总是不通”的诡异现象。net.ipv4.ip_forward 控制 IP 转发,不打开的话 Pod 之间的报文根本出不去。swap 则必须关,因为 kubelet 的资源隔离和 QoS 机制在 swap 存在时会失灵,kubeadm 默认直接报错拒绝初始化。

主机名也值得注意。Kubernetes 对节点名有 RFC1123 约束,只允许小写字母、数字、点、中划线。你给机器起个大写主机名,后面 join 或证书签发都会遇到奇怪问题。我这边的做法是统一用 hostnamectl set-hostname node01 这类短小名字,多个节点就 node01、node02,清晰好认。

2.3 containerd:不是装了就能用,两个字段决定成败

Docker 时代已经过去了。Kubernetes 从 1.24 起不再内置 dockershim,你在只有 Docker 的机器上直接跑 kubeadm init 大概率会报 CRI 不可用,正确做法是装 containerd。Ubuntu 上装 containerd 的快捷方式是用 Docker 官方源里的 containerd.io 包,装完生成默认配置:

containerd config default | tee /etc/containerd/config.toml

打开 /etc/containerd/config.toml,重点盯两个字段。

第一个是 SystemdCgroup。位置在 [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options] 下面,默认是 false,需要改成 true。原因后面排障部分会展开,这里先记住结论:kubelet 默认用 systemd 作为 cgroup 驱动,containerd 如果不跟着做,两边会各维护一套资源控制目录,kubelet 的资源统计就全乱套。

第二个是 sandbox_image。默认值是 registry.k8s.io/pause:3.9,这个地址在你所处的网络环境下拉取可能很慢,预期要换成同步源路径。pause 镜像是每个 Pod 的沙箱基础镜像,它起不来,任何 Pod 都进不了 Running 状态。这两个字段改完,执行:

systemctl daemon-reload systemctl restart containerd systemctl enable containerd systemctl is-active containerd

验证配置有没有生效,可以用:

containerd config dump | grep -i systemd

如果 grep 结果里只有注释而没有 true,说明目标行没改对,配置解析没有按你预期的路径走,这时候一定要回去检查字段的层级缩进。containerd 的 TOML 配置是严格按插件路径分层的,缩进错了等于没改。

3. 拆解 kubeadm init:pre-flight 到底在查什么

3.1 逐项看 pre-flight checks 在做什么

正式 init 命令我贴出来:

kubeadm init \ --apiserver-advertise-address=192.168.1.10 \ --kubernetes-version=v1.26.0 \ --pod-network-cidr=10.244.0.0/16 \ --image-repository=registry.aliyuncs.com/google_containers

命令一开始的输出,前两行你一定眼熟:

[init] Using Kubernetes version: v1.26.0 [preflight] Running pre-flight checks

pre-flight 是这个工具最贴心也最容易让人不耐烦的部分。它做的事本质上是“把所有可能在初始化中途才暴露的环境问题提前暴露出来”。我习惯把它分成四组理解。

第一组是机器条件:是否 root、swap 是否关闭、内核桥接模块是否加载、ip_forward 是否打开、cgroup 挂载是否正常。这些属于硬性依赖,缺一个后面都会出大事。

第二组是运行时连接:能不能通过 CRI socket 和 containerd 通信。Kubernetes 体系里容器运行时是独立服务,pre-flight 会主动去握手,握不上就直接报错终止。

第三组是端口和目录:apiserver 的 6443、etcd 的 2379/2380、kubelet 的 10250 等端口不能被占用,/etc/kubernetes 下不能有旧配置遗留。

第四组是镜像预拉:按你指定的 image-repository 把 kube-apiserver、kube-controller-manager、kube-scheduler、etcd、pause 等镜像全部拉到本地。这一步如果太慢,可以单独执行下面的命令预做:

kubeadm config images pull --image-repository=registry.aliyuncs.com/google_containers

3.2 我遇到的 ERROR 逐个拆解

这里把实际碰到过的报错列出,每一条都附上最短的解决方案。

[ERROR IsPrivilegedUser]:你不是 root。kubeadm init 要写 /etc/kubernetes,需要切换 root 或者用 sudo 执行。

[ERROR Swap]:swap 没关。注意 swapoff -a 只关当前会话,/etc/fstab 里那一行不注释,重启后又回来了。这就是为什么命令里要带 sed 那一步。

[ERROR FileContent--proc-sys-net-bridge-bridge-nf-call-iptables]:sysctl 配置没生效。执行 sysctl --system 后再跑 sysctl net.bridge.bridge-nf-call-iptables 确认值是 1。有些环境内核默认没有 br_netfilter 模块,modprobe 那一步不能省。

[ERROR CRI]:container runtime is not running。这个报错看着简单,坑其实最深。我遇到过 containerd 显示 active,但 preflight 就是连不上 socket 的情况,后来查日志发现 containerd 因为 config.toml 里的格式问题反复退出。判断真实状态用 journalctl -u containerd --no-pager,或者用 crictl info 直接验证连接。

[ERROR Port-10250]:端口被占。常见于本机跑过 docker 或者遗留 kubelet。ss -lntp | grep 10250 看一下谁占的,处理掉再 continue。

[ERROR FileAvailable--etc-kubernetes-manifests]:/etc/kubernetes 里有旧文件。先 kubeadm reset -f,再手动确认目录清干净再 init,千万别顺手加 --ignore-preflight-errors 硬闯。跳过检查不是解决问题的办法,只会把问题推到更后面。

3.3 init 参数别乱传:每个值都在画整张网络的蓝图

--kubernetes-version 直接决定拉哪个版本的组件镜像,你机器上的 kubelet 版本和它差距太大会带来不必要的兼容性麻烦,所以尽量和 kubeadm 的版本保持一致。

--apiserver-advertise-address 指定 apiserver 对外暴露的 IP。这个 IP 要稳定,别写虚拟浮动地址,也别依赖 DHCP 动态分配的地址。apiserver 是集群的入口,它不稳定,所有 kubectl 和 kubelet 通信都会出问题。

--pod-network-cidr 管的是 Pod 网段,它必须和你后面要装的 CNI 插件对得上。Flannel 的默认配置里写死了 10.244.0.0/16,所以如果你选 Flannel,这个参数最好就填 10.244.0.0/16;Calico 默认的 IPPool 是 192.168.0.0/16,你填别的网段未必跑不起来,但会显著增加排障成本。这些参数在 init 阶段敲定,等于给整个集群的网络画了蓝图,后面想改非常麻烦。

--image-repository 是镜像仓库路径。默认 registry.k8s.io,在部分网络环境下拉取很慢,改成国内同步源后,kubeadm 会把控制面组件的镜像都从这边拉。这一步熟练之后,你会发现 init 的耗时主要就花在镜像预拉上,仓库快,整个流程就快。

init 成功后的最后几行会输出 kubeadm join 命令,里面带 token 和 discovery-token-ca-cert-hash。这是 worker 节点入群的凭证,务必保存好。

4. init 成功不等于集群就绪:三件必须立刻做的事

4.1 让 kubectl 先能干活:admin.conf 的归属

控制面节点初始化完成后,第一件事是让 kubectl 能正常使用。kubeadm 会在 /etc/kubernetes/admin.conf 生成一份权限最高的 kubeconfig,把它放到默认位置:

mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config

为什么是复制而不是软链接?admin.conf 是 root 所有,普通用户如果直接软链过去,每次读写都会有权限问题;复制一份出来再 chown 给当前用户,是最干净的做法。这个文件里有 apiserver 地址、集群 CA、管理员客户端证书,kubectl 启动时不带任何参数,会直接读 ~/.kube/config。如果你省略这一步,后面每次敲 kubectl 都要带 --kubeconfig 参数,一旦在多个集群之间切换,那真是灾难。

配好先跑 kubectl get nodes,这时主节点处于 NotReady 状态是正常的,因为 CNI 还没装。也可以用 kubectl get nodes -o wide 看看内部 IP 和 kubelet 版本是否认对,这一步就能提前发现部分配置问题。

4.2 网络插件到底哪天装、装哪一个

CNI(容器网络接口)是集群数据面能跑通的决定性组件。装晚了你会看到节点一直 NotReady,CoreDNS 随时 CrashLoopBackOff。原因在于节点 Ready 的条件里包含“CNI 配置是否已存在”,没有网络插件,kubelet 会认为这个节点还没准备好。

Kubernetes 官方不替你选网络插件。常见的几个里,Flannel 最轻,基于 VXLAN 封装,适合学习和内网环境;Calico 功能全,支持 NetworkPolicy、BGP、IPIP/eBPF 等,适合生产多租户场景。我这次用 Flannel,因为实验环境不需要网络策略,越简单越不容易出问题。

Flannel 的安装就一条命令:

kubectl apply -f https://raw.githubusercontent.com/flannel-io/flannel/master/Documentation/kube-flannel.yml

Calico 的安装方式类似,只是 manifest 更大更复杂:

kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.26/manifests/calico.yaml

装完用 kubectl get pods -n kube-system -w 盯输出,等 flannel 和 coredns 都进入 Running/Ready,再跑 kubectl get nodes 就能看到节点变 Ready。这里有个容易掉进去的坑:如果你装 Flannel 但 init 时用了 192.168.0.0/16 的 Pod CIDR,网络分配会对不上,节点虽然可能显示 Ready,但 Pod 之间通信会非常混乱。遇到这种情况别硬修,重置重来最快。

4.3 单节点集群的污点清理

控制面节点默认带污点 node-role.kubernetes.io/control-plane:NoSchedule,意思是普通 Pod 不允许调度到它上面。多节点生产环境必须保留这个污点,控制面只跑系统组件;但单节点学习环境里,所有工作负载只能压在 master 上,所以要把污点去掉:

kubectl taint nodes --all node-role.kubernetes.io/control-plane-

解释一下这条命令:它把所有节点上 key 为 node-role.kubernetes.io/control-plane 的污点移除,末尾的短横线表示删除操作。单节点用 --all 没问题,因为集群里只有一个 control-plane 节点;如果以后加了 worker 节点,worker 本来就没有这个污点,命令也不会误伤。真正的风险在于你手动给某台机器配置过同名污点,用 --all 会一并清掉,所以生产环境更稳妥的写法是指定节点名:

kubectl taint node <node-name> node-role.kubernetes.io/control-plane-

除完污点可以立刻验证:kubectl create deployment nginx --image=nginx,然后 kubectl get pods -w 观察 nginx Pod 被调度到 master 上并进入 Running 状态。看到这个结果,说明这套单机集群已经具备完整的工作负载能力,后面可以放肆地折腾各种应用了。

5. 从 NotReady 到 Ready 的排障手记

5.1 镜像拉不下来时我试过的办法

排障部分先讲镜像。Kubernetes 的所有组件本质都是容器镜像,初始化失败很大概率卡在“镜像拉不下来”。

第一种情况发生在 init 阶段,输出停留在 Pulling images 很久然后超时。对策是先用 kubeadm config images pull 手动预拉,指定和 init 时一样的仓库。如果这条命令能完整跑完,说明仓库源没问题,init 的 preflight 也会顺利很多。

第二种情况发生在集群跑起来之后,kube-system 里的某个 Pod 一直 ImagePullBackOff。先 kubectl get pods -n kube-system 看状态,再用 kubectl describe pod -n kube-system 看事件里的具体错误。最常见的两个原因:

  • sandbox_image 没改,pause 镜像从默认仓库拉不动。改 /etc/containerd/config.toml 里的 sandbox_image,重启 containerd,然后把处于 ContainerCreating 或 ImagePullBackOff 的旧 Pod 删掉,让它重新调度。
  • 节点用了 containerd,但你还在用 docker 命令查镜像。containerd 对应的命令是 crictl,需要先写 /etc/crictl.yaml 指定运行时 socket:
runtime-endpoint: unix:///run/containerd/containerd.sock image-endpoint: unix:///run/containerd/containerd.sock timeout: 10 debug: false

写完这个文件,crictl ps 和 crictl images 才真正指向 containerd。否则 crictl 默认去找老版本的 dockershim socket,一查一个空。

5.2 cgroup 驱动不一致:最隐蔽的早期故障

cgroup 驱动不一致,是我在初始化阶段遇到过最隐蔽的故障。现象是 kubelet 日志不断刷:

failed to run Kubelet: failed to create kubelet: misconfiguration: kubelet cgroup driver: "systemd" is different from docker cgroup driver: "cgroupfs"

这里的 docker cgroup driver 其实是 CRI 运行时暴露出来的驱动信息。原因很直接:kubelet 默认的 cgroupDriver 是 systemd,而 containerd 的 SystemdCgroup 默认是 false,即 cgroupfs。两边不一致,kubelet 连启动都不愿意。

排查方法看两个文件:

grep cgroupDriver /var/lib/kubelet/config.yaml containerd config dump | grep SystemdCgroup

修复是把 containerd 的 SystemdCgroup 改成 true,重启 containerd,再执行 systemctl daemon-reload && systemctl restart kubelet。

为什么必须一致?cgroup 是 Linux 的资源控制树。kubelet 用 systemd 驱动时,会把 Pod 放在对应单元的 cgroup 路径下;运行时如果按 cgroupfs 去创建,两边各维护一套目录,kubelet 算资源用量、做驱逐的时候就会对不上账。集群短期可能看不出问题,跑上一段时间后,CPU 限流、内存回收、QoS 判断全靠这些数据源,不一致的后果会越积越深。所以这个字段在装 containerd 之后就应该顺手改掉,而不是等报错。

5.3 平时舍不得删、关键时刻救命的命令清单

最后整理一份排障时反复用的命令清单。散记写到这,这些命令都是实战里验证过能解决问题的:

  • 看集群事件:kubectl get events --sort-by=.lastTimestamp -A
  • 看节点详情:kubectl describe node
  • 看 kubelet 日志:journalctl -u kubelet -f --no-pager
  • 看 containerd 日志:journalctl -u containerd -f --no-pager
  • 找新的 join token:kubeadm token create --print-join-command
  • 算 CA 证书 hash:
openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt | \ openssl rsa -pubin -outform der 2>/dev/null | \ openssl dgst -sha256 -hex | sed 's/^.* //'
  • 重置整个环境:
kubeadm reset -f rm -rf /etc/cni/net.d ~/.kube

这里特别强调一下,kubeadm reset 不会自动清理 /etc/cni/net.d。如果不手动删,下次在同一台机器上重新 init,CNI 目录里残留的是旧配置,网络插件起来之后会读取到历史遗留,Pod 网络大概率起不来。我踩过一次这个坑,之后每次 reset 都默认带上 rm -rf /etc/cni/net.d 这一步,再没莫名奇妙复发过。

写这篇散记时我又对照着把整套初始化流程走了一遍,最深的体会是:Kubernetes 初始化虽然命令就那么几条,但每条命令背后的概念链条特别长。比如 --pod-network-cidr 和 CNI 插件要能对上号,SystemdCgroup 要能解释清楚为什么和 kubelet 资源管理强相关,pre-flight 的每个 ERROR 要能说清它防的是什么。把这些关系理清楚之后,kubeadm init 就不再是照着敲的咒语,而是一份你可以自己检查和调整的配置清单。

下一篇散记我打算写 CNI 插件内部到底在做什么,或者写证书到期后怎么续期。看日常生活里哪个坑先把我绊倒,到时候再接着记。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询