kubeadm init踩坑实录:从零搭建Kubernetes集群的完整指南 写这篇《Kubernetes散记1》的时候我正盯着kubeadm init输出里的那行[init] using kubernetes version: v1.26.0发呆。Kubernetes这个词圈内没人陌生但真正从零开始用kubeadm把一个集群拉起来跟看文档完全是两码事。这篇不是教科书就是一份带温度的踩坑实录。我会把从准备环境、敲下初始化命令到处理preflight检查报错的全过程拆开揉碎讲清楚包括[preflight] running pre-flight checks后面那些你大概率会撞上的问题。适合刚接触Kubernetes、准备上手搭集群的人也适合那些被各种教程劝退、想看看真实操作长什么样的朋友。1. 项目由来与整体部署方案1.1 这件事的起因与实际目标我手头有三个不常用的物理节点配置不新但都还能打做一个稳定的Kubernetes测试集群绰绰有余。当时想得很简单把环境搭起来后面跑点自动化测试和CI场景验证。但真操作起来事情就没那么简单了。Kubernetes集群搭建路径有三条主流选择minikube适合单机学习、kind基于Docker容器模拟节点适合开发调试、kubeadm官方推荐的生产级集群工具支持多节点近生产环境。我没有犹豫就选了kubeadm。原因很简单目标不是学会敲几条命令而是理解一个真实集群从初始化到节点加入、再到跑通业务应用的完整链路。minikube和kind都做了太多封装很多底层交互你看不见摸不着出了事很被动。kubeadm能让我清楚地看到每一条证书、每一个组件的状态。1.2 部署环境与组件版本怎么选先交代硬件和软件基础信息机器数量3台1台master节点2台worker节点操作系统Ubuntu 22.04 LTS内核版本5.15容器运行时containerd 1.7.x为什么不用Docker后面细说Kubernetes版本v1.26.0网络插件Calico v3.26选Ubuntu 22.04是看重它的稳定性和系统服务管理方式加上主流云厂商镜像基本都基于它定制出问题的经验贴也最好找。containerd的选择在下文会专门展开Docker本身已经剥离出Kubernetes依赖链与其绕路不如直接选原生方案。1.3 版本组合的设计逻辑版本组合值得多说两句因为这里一旦贪图“最新版本”反而容易出问题。Kubernetes的版本迭代很快而各组件kubectl、kubelet、kubeadm、容器运行时、CNI插件之间的兼容矩阵是真实存在的。我选v1.26.0是因为它在当时属于比较成熟的版本线而且与containerd 1.7.x适配良好。Calico版本和Kubernetes API的兼容性也做了确认v3.26对v1.26的支持非常稳。版本选择的最好参考是Kubernetes官方维护的版本偏差策略文档和组件兼容矩阵不要凭感觉选“最新”稳定能用低于一切。2. kubeadm init的必备参数一个都不能少2.1 image-repository参数解决拉取镜像的痛点我在国内网络环境下部署直接用kubeadm默认的镜像源几乎无法完成初始化。v1.26.0的默认kube-apiserver、kube-controller-manager、kube-scheduler、kube-proxy、etcd、pause等核心镜像都托管在gcr.io上连不通会直接导致初始化卡死。所以我加了这一行参数--image-repositoryregistry.aliyuncs.com/google_containers这就是把默认的gcr.io源切换为国内可访问的镜像源。registry.aliyuncs.com/google_containers是国内外广泛使用的Kubernetes免登录镜像仓库实测下来稳定度很高拉取速度也够用。如果你身处海外网络环境或者公司内部有私有的镜像仓库缓存也可以把这个参数指向内部源。这个参数的本质是替换“前缀”后面的镜像路径和tag完全不变因此不会影响Kubernetes本身的完整性。2.2 apiserver-advertise-address和pod-network-cidr这两个参数是初始化命令里最容易让人困惑、但又最关键的。--apiserver-advertise-address192.168.1.10指定的是控制平面apiserver对外通告的IP地址。我强烈建议明确写出来而不是让kubeadm自动检测因为多网卡机器上默认检测经常漂到错误的网卡上后面所有节点join都会连错地址。你在自己的环境里把这个IP换成master节点的内网IP就行。--pod-network-cidr192.168.0.0/16看起来只是一个网段声明实际上它是给CNI容器网络接口插件用的“预约路段”。Calico或者其他网络插件会依据这个CIDR为每个Pod分配唯一的IP。这个网段需要和物理网络及Service网段错开否则路由表会冲突导致节点间网络完全不通。我这里选了192.168.0.0/16与机器所在的192.168.1.0/24网段仅一字之差但分属不同广播域在VPC或交换机层面做了隔离所以没有冲突。如果这两个网段有路由重叠会出现非常诡异的不定时网络异常排查起来极其痛苦。2.3 完整初始化命令与参数计算逻辑整理一下我最终执行的主命令供你直接参考sudo kubeadm init \ --image-repositoryregistry.aliyuncs.com/google_containers \ --kubernetes-versionv1.26.0 \ --control-plane-endpoint192.168.1.10:6443 \ --apiserver-advertise-address192.168.1.10 \ --pod-network-cidr192.168.0.0/16 \ --service-cidr10.96.0.0/12参数逐项说明--kubernetes-versionv1.26.0显式指定版本避免kubeadm从远端获取不匹配的版本号。--control-plane-endpoint192.168.1.10:6443这里的IP和apiserver地址保持一致主要用于生成证书里的SANSubject Alternative Name。如果你计划做高可用这里应该放负载均衡器地址。--service-cidr10.96.0.0/12为ClusterIP类型的Service预留的网段Kubernetes默认值就是这个显式写出来是为了当默认值变更时配置依旧稳定。整个初始化流程会经历[preflight]、[certs]、[kubeconfig]、[control-plane]、[etcd]、[mark-control-plane]等阶段。第一次操作时可能会被大量输出刷屏但核心只需要关注失败信息。正常情况下最后一行会出现Your Kubernetes control-plane has been initialized successfully!看到这句之后后续按输出提示配置kubectl并记录下join命令下面第4.1节会说怎么保存和重新生成。3. preflight检查逐项拆解避开高概率踩坑点3.1 preflight是什么它到底在检查什么kubeadm init输出的第二行就是[preflight] running pre-flight checks。听起来像走过场但如果这里报错后面的流程一步都不会继续。preflight检查相当于启动前的“自检流程”逐项验证当前机器是否满足Kubernetes运行底线要求。主要检查项包括操作系统是否为受支持的Linux发行版是否具备可用的容器运行时这里就是containerd关键端口6443、2379-2380等是否被占用swap是否处于关闭状态防火墙是否拦截了必要通信/etc/hosts和DNS是否正常节点主机名是否合法不能包含下划线它的价值很高把90%的“启动后发现起不来”的高级问题提前拦在门外让你可以基于清晰的报错信息去修复。3.2 swap没关最经典的第一道坎如果你直接拿全新Ubuntu 22.04来初始化大概率第一个报错就是我遇到的[ERROR Swap]: running with swap on is not supported. Please disable swapKubernetes的kubelet设计上不允许宿主机存在启用的swap。原因在于kubelet在做内存QoS和Pod驱逐管理时依赖Linux内存控制组cgroup的数据。swap的存在会让内存压力的统计失真明明内存已满但因为swap在顶cgroup无法触发应有的回收或驱逐策略导致Pod内存隔离失效。多Pod场景下一个应用的内存暴涨很可能会拖垮同节点的其他Pod。关swap的操作是sudo swapoff -a但这条命令只对当前运行生效。重启后如果分区表里挂载了swap还是会重新启用。因此必须永久关闭。编辑/etc/fstab注释掉所有包含swap的行sudo sed -i / swap / s/^/#/ /etc/fstab这里我补充一个细节swapoff -a只是让系统不再使用swap但需要确保没有进程仍占用swap否则无法全部释放。实际操作时可以先执行free -h确认swap占用为0再继续。3.3 容器运行时没配置好被Docker“惯性思维”坑的瞬间一开始我想沿用老经验给机器装了Docker。随后用kubeadm初始化时preflight报了[ERROR CRI]: container runtime is not running: output: ...这个报错表面上指向CRIContainer Runtime Interface容器运行时接口但它的本质是你安装好了某个运行时Kubernetes却没能连接上它。先解释一下CRI是什么这是Kubernetes定义的一套接口标准kubelet通过它和容器运行时交互。早年Docker是唯一的运行时后来CNCF牵头制定了CRI标准Docker本身不直接实现CRI需要通过一个叫docker-shim的中间层桥接。但在Kubernetes 1.24及以后docker-shim已经被正式移除。也就是说从v1.24开始Docker不再是被Kubernetes直接支持的运行时。所以纯粹的“装了Docker就能当运行时”已经行不通了。稳妥方案是装containerd它是Docker底层那个真正干活的部分原生实现CRI。安装方式sudo apt-get update sudo apt-get install -y containerd.io安装完还要做一步关键配置初始化containerd的默认配置否则它可能仍然不会启用CRI支持或者配置里的SystemdCgroup参数是false。sudo mkdir -p /etc/containerd sudo containerd config default | sudo tee /etc/containerd/config.toml生成完默认配置后必须检查文件里的一处关键参数[plugins.io.containerd.grpc.v1.cri.containerd.runtimes.runc] [plugins.io.containerd.grpc.v1.cri.containerd.runtimes.runc.options] SystemdCgroup trueSystemdCgroup必须改为true。原因在3.5小节细说这里先不动继续往前走。修改完配置重启containerdsudo systemctl restart containerd然后有个小技巧用crictl info快速验证CRI接口是否正常。能看到返回的运行时信息说明kubelet这边不会再报CRI相关的错了。3.4 端口占用和内核参数第二个排查盲区preflight也会检查关键端口。我这里踩过一个非常隐蔽的坑内存里的监听端口已经释放但系统仍然报占用。错误显示[ERROR Port-6443]: Port 6443 is in use当时我第一反应是端口留着旧进程netstat -tunlp | grep 6443却没有任何输出。后来才发现是旧的etcd或kubelet服务残留在不应该运行的机器上自动启动了。这里的教训很明确操作系统服务多、旧配置残留多的时候先查所有kube*相关服务状态sudo systemctl status kubelet etcd 2/dev/null如果kubelet状态是activerunning先停掉并禁用它再重新初始化sudo systemctl stop kubelet sudo systemctl disable kubelet没有显式关掉这些新装服务它们在系统重启后可能自动拉起把原本要用的端口占住。这个坑在多次尝试初始化场景里出现频率很高第一次失败后你改配置重试但旧进程还在悄悄工作。另外还有两个内核参数系统不会强制你改但如果不改后面网络表现会非常不稳定echo net.ipv4.ip_forward1 | sudo tee -a /etc/sysctl.conf echo net.bridge.bridge-nf-call-iptables1 | sudo tee -a /etc/sysctl.conf sudo sysctl -pnet.ipv4.ip_forward1是开启IP转发Kubernetes的Service转发和Pod间通信依赖数据包在节点之间转发不开启的话报文会被丢弃。net.bridge.bridge-nf-call-iptables确保经过Linux网桥的流量能正确经过iptables规则这一点对Calico等基于iptables的CNI插件极其重要。不设置它集群内部DNS解析会随机失败表现为“服务时而能通时而超时”。3.5 cgroup driver一致性看起来无害却致命的细节cgroup driver是preflight能通过、但运行才会暴露问题的一个典型。Kubernetes的kubelet和容器运行时各自管理cgroup它们使用的driver必须一致。这里有两种drivercgroupfs和systemd。系统里systemd几乎总是可用且是原生init系统Kubernetes官方推荐kubelet和containerd都使用systemd作为cgroup driver避免systemd和cgroudfs同时管理资源组带来双重管控冲突。我的操作路径是把/etc/containerd/config.toml里的SystemdCgroup改为true3.3节已提到。保证kubelet的/var/lib/kubelet/kubeadm-flags.env或config文件里的参数也是cgroup-driversystemd。kubeadm默认已经为kubelet写好了systemd因此你真正需要做的是确保containerd这边是true。如果containerd留的是false两个组件driver不一致你会在kubelet日志里看到类似下面的报错Failed to run kubelet errfailed to run Kubelet: failed to create kubelet: misconfiguration: kubelet cgroup driver: \systemd\ is different from docker cgroup driver: \cgroupfs\字面意思是cgroup driver不匹配但你在初始化阶段不一定看得到往往等到节点加入、Pod一直Pending时才暴露。提前把两边都设为systemd能少排查两小时。我把这个提醒放到最显眼的位置改了配置一定重启containerd否则改动不生效。4. 集群初始化后的常见问题与排查技巧实录4.1 token过期与join worker节点问题master节点初始化成功后kubeadm会在输出末尾给出一段join命令格式大概是kubeadm join 192.168.1.10:6443 --token token \ --discovery-token-ca-cert-hash sha256:hash有个很关键的点token有效期默认只有24小时而ca-cert-hash如果你没当场记录后面拿不到了。解决办法是随时重新生成tokensudo kubeadm token create --print-join-command这条命令会打印一条全新的join命令且带着当前有效的token。worker节点重复执行即可。但有个另外一个坑join命令的--discovery-token-ca-cert-hash值变化吗它不会变。它指向的是master节点CA证书的公钥哈希master节点没重建哈希就固定。所以丢失了旧哈希直接重新生成token会带着对的哈希输出。如果你把master节点整个重装重来才需要为了新CA证书重新计算哈希。正常的worker加入场景一条kubeadm token create --print-join-command就够了。4.2 节点状态NotReady的排查路径集群刚初始化完节点状态变成NotReady绝对正常。原因是网络插件还没安装。不过如果花几分钟装了Calico节点还在NotReady就开始考验基本功了。排查要按顺序来。第一看节点状态kubectl get nodes如果显示NotReady用kubectl describe node 节点名看Conditions字段里的信息重点看Ready状态下的Message和Reason。第二查kubelet日志sudo journalctl -u kubelet -f注意这个命令是实时流式输出。你先执行它再到另一个终端尝试重启kubelet从而对时空隙做对应。最常出现的问题是failed to get sandbox image registry.k8s.io/pause:3.6这个报错又回到镜像拉取。Calico或其他插件要跑Pod就要先把pause这个基础镜像拉下来。但它默认从registry.k8s.io拉取网络不通时这一步就直接卡死了。解决办法是手动拉取替换镜像名sudo ctr -n k8s.io images pull registry.aliyuncs.com/google_containers/pause:3.6然后让kubelet使用这个镜像源。涉及的配置在/etc/containerd/config.toml里搜sandbox_image把默认的registry.k8s.io/pause:3.6改成可用的镜像源。改完重启containerd和kubelet。第三确认CNI插件状态。这里我强烈推荐用kubectl get pods -n kube-system而不是只盯节点状态。你会在kube-system里有calico的DaemonSet PodNotReady根因往往能从某个calico Pod的CrashLoopBackOff状态里找到答案。比如日志里常见的Error getting Felix configuration: error accessing Calico etcd datastore或者BIRD: Cannot open UDP socket: Permission denied前者说明Calico无法连接etcd或Kubernetes API检查CALICO_KUBECONFIG环境变量和集群组件的网络连通性后者常见于启用IPv6的环境Calico默认可能需要额外的系统内核模块或特权确认/proc/sys/net/ipv6/conf/all/forwarding不为0。4.3 证书过期问题的预防性处理Kubernetes集群长期运行后会出现一个非常经典的问题kubeadm init生成的证书有效期只有一年。到期后kubectl访问API Server会报x509证书过期错误。这是步入稳定使用后最大的坑之一。如果你想避免这种紧张时刻有两个思路思路一在初始化时用--certificate-validity-period参数指定更长的有效期。这个参数只对kubeadm init生效后期修改需要重建集群。如果不是生产环境直接设个10年sudo kubeadm init ... --certificate-validity-period87600h注意参数单位是hours计算下来是3650天。但如果你的环境接近生产不建议这样做一年一换证书是更安全的标准做法。思路二真的过期了不用重建集群用kubeadm提供的手动更新功能。操作路径sudo kubeadm certs renew all sudo systemctl restart kubelet需要把更新后的kubeconfig分发到使用kubectl的机器上。最简单的方式如果是在master节点本地操作sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config然后kubectl get nodes验证即可。我知道有人会问证书过期前的预警怎么看建议写个脚本定时检查kubeadm certs check-expiration这个命令会详细列出每个证书的到期时间非常直观。4.4 答疑速查表用来救急的二十秒参考把前三个阶段遇到的问题归纳成速查表碰到相同报错时能快速定位症状根因一句话处理preflight报Swap错误swap未关闭sudo swapoff -a并注释fstabpreflight报CRI not running没有可用容器运行时安装并配置containerd设SystemdCgroup为truepreflight报Port-6443 is in use旧kubelet/etcd残留停掉并禁用kubelet服务再重试节点一直NotReadyCNI未安装或pause镜像拉不到安装Calico或手动改sandbox_imagekubelet日志报cgroup driver不一致containerd和kubelet配置不同将containerd的SystemdCgroup改为truejoin命令的token失效token 24h过期kubeadm token create --print-join-commandkubectl报x509证书过期1年有效期到了kubeadm certs renew all并更新kubeconfig集群Pod网络不通内核ip_forward未开或CNI冲突设置sysctl参数并检查CNI这些报错常见到什么程度可以说十个人搭kubeadm集群至少五个会撞上其中两三条。提前知道解决方案体验完全不一样。5. 推进过程中的几个额外体会把时间线拉回到我最初动手的节点。那时候看官方文档第一段就是“kubeadm init一条命令搞定”实际操作才发现一条命令背后隐藏了至少四条前置条件。关swap、配containerd、选镜像源、调内核参数每一步都有其必然性。如果你现在正准备开始我的建议是不要急着初始化先把3.3到3.5的环境配置逐一过一遍能省掉大半的报错。另外一个心得是关于日志的看法。很多人在排障时会下意识反复执行kubectl get pods和kubectl get nodes但真正能定位问题的其实是日志和describe。遇到调度失败先kubectl describe pod pod名看Events遇到节点异常先journalctl -u kubelet -f看kubelet视角。这比盲目重装强一百倍。最后补充一个很实用的小技巧集群初始化之前把三台机器的主机名改成见名知义的名字同时确保/etc/hosts里写好了所有节点的IP和主机名映射。Kubernetes内部的证书和kubeconfig会把主机名作为重要标识如果主机名混乱后面排查kubelet认证问题时非常容易晕头转向。我在这上面吃过亏三台机器的hostname都带了下划线preflight直接报错改完重来才通过。这套流程我前后跑了几遍从第一次的凌晨三点还在跟cgroup driver搏斗到后来半小时从零拉起一个集群差别只在于对preflight背后逻辑的理解。Kubernetes不是洪水猛兽它只是把运维的“基本功”用更结构化的方式展示出来了。希望这篇散记能帮你少走几步弯路。下篇我大概率会写网络插件深度对比和Pod调度实战看到时候又踩出哪些新的坑吧。