
前阵子重装了一套 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 的旧 APIPodSecurity 等新特性也进入了普遍可用阶段。对于使用者来说你在网上能搜到的绝大多数解决方案、实践案例都能直接套用不用像追新版本那样担心插件还没适配。为什么不冲最新版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 initpre-flight 到底在查什么3.1 逐项看 pre-flight checks 在做什么正式 init 命令我贴出来kubeadm init \ --apiserver-advertise-address192.168.1.10 \ --kubernetes-versionv1.26.0 \ --pod-network-cidr10.244.0.0/16 \ --image-repositoryregistry.aliyuncs.com/google_containers命令一开始的输出前两行你一定眼熟[init] Using Kubernetes version: v1.26.0 [preflight] Running pre-flight checkspre-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-repositoryregistry.aliyuncs.com/google_containers3.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/16Calico 默认的 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容器网络接口是集群数据面能跑通的决定性组件。装晚了你会看到节点一直 NotReadyCoreDNS 随时 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.ymlCalico 的安装方式类似只是 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 --imagenginx然后 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 指定运行时 socketruntime-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 tokenkubeadm token create --print-join-command算 CA 证书 hashopenssl 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。如果不手动删下次在同一台机器上重新 initCNI 目录里残留的是旧配置网络插件起来之后会读取到历史遗留Pod 网络大概率起不来。我踩过一次这个坑之后每次 reset 都默认带上 rm -rf /etc/cni/net.d 这一步再没莫名奇妙复发过。写这篇散记时我又对照着把整套初始化流程走了一遍最深的体会是Kubernetes 初始化虽然命令就那么几条但每条命令背后的概念链条特别长。比如 --pod-network-cidr 和 CNI 插件要能对上号SystemdCgroup 要能解释清楚为什么和 kubelet 资源管理强相关pre-flight 的每个 ERROR 要能说清它防的是什么。把这些关系理清楚之后kubeadm init 就不再是照着敲的咒语而是一份你可以自己检查和调整的配置清单。下一篇散记我打算写 CNI 插件内部到底在做什么或者写证书到期后怎么续期。看日常生活里哪个坑先把我绊倒到时候再接着记。