
我印象最深的一次集群搭建不是在云上点几个按钮而是模拟项目X里拿到三台裸机要求用 kubeadm 部署一套 Kubernetes 1.28 集群。当时最让我迷惑的不是命令记不住而是网上教程一大把照着敲完节点却一直 NotReady。后来我才想明白问题不在命令而在我没有理解 kubeadm 部署背后的整套架构逻辑。本文我会把 Kubernetes 1.28 基于 kubeadm 部署的架构彻底拆开讲清楚控制平面和工作节点各自在做什么、kubeadm init 到底执行了哪些关键步骤、为什么网络插件必须等集群初始化完成之后再装以及日常运维中最容易踩的几个坑。适合三类人看第一次用 kubeadm 搭集群、搭完集群后遇到各种 NotReady、或者虽然能跑通部署但想弄明白“每个组件为什么存在”的开发者。1. 为什么说 kubeadm 是理解 1.28 集群架构的最佳入口1.1 三种搭建方式为什么我推荐 kubeadm部署 Kubernetes 集群的方式大致可以分成三类二进制手动部署、kubeadm 官方引导工具、以及各种发行版或托管平台。二进制部署是把 kube-apiserver、kube-controller-manager、kube-scheduler、etcd、kubelet、kube-proxy 这些二进制文件全部下载到服务器上然后手动生成证书、写 systemd 服务文件、配置 kubeconfig。这种方式的好处是可定制性强坏处是太容易出现低级错误。证书签错名字、组件启动参数写错、某个服务没有按顺序启动都会让你排查到怀疑人生。kubeadm 是 Kubernetes 项目官方维护的引导工具它的定位不是“一鍵部署神器”而是把证书生成、组件静态 Pod 配置、kubeconfig 分发、节点引导凭证这些繁琐又容易出错的环节自动化。发行版或托管平台虽然更省事但对理解架构几乎没有帮助。你只知道“点一下集群就起来了”却不知道里面发生了什么。真出了问题只能干瞪眼。我推荐用 kubeadm 的另一个重要原因是它生成的目录结构就是 Kubernetes 架构的活地图。比如/etc/kubernetes/manifests下面是 kube-apiserver.yaml、kube-controller-manager.yaml、kube-scheduler.yaml、etcd.yaml 这四个文件它们静态声明了控制平面组件的全部启动参数。你读一遍这些 YAML比看十篇架构文章都管用。1.2 kubeadm 到底替你做了哪些事很多人把kubeadm init理解成“一条命令安装 Kubernetes”这个理解不准确。它实际做的是先做环境预检然后生成 PKI 证书体系生成各个组件需要的 kubeconfig 文件再把控制平面组件以静态 Pod 的方式写入/etc/kubernetes/manifests最后创建 bootstrap token 让工作节点可以安全加入。具体来说执行完kubeadm init之后你会看到这几个产物/etc/kubernetes/pki存放 CA、API Server 证书、etcd 证书、前端代理证书等。/etc/kubernetes/manifests存放控制平面四个静态 Pod 的定义文件。/etc/kubernetes/admin.conf、kubelet.conf、controller-manager.conf、scheduler.conf不同角色访问集群使用的 kubeconfig。/var/lib/kubelet/config.yamlkubelet 的完整配置。理解了这些产物你就知道 kubeadm 的价值不是“帮你把程序跑起来”而是“把集群里最复杂、最容易出错的初始化部分固化成标准化流程”。它不需要你在每个节点手工去验签证书、不需要你手工创建 Service Account也不需要你手工决定组件之间的信任关系。1.3 部署前必须满足的运行边界kubeadm 的预检环节叫 preflight checks很多失败都源于环境不达标。1.28 版本对节点的基本要求并不高但有一些硬性条件必须满足操作系统是 64 位、CPU 至少 2 核、内存至少 2GB内核版本尽量新一些。更重要的是以下几个配置必须加载overlay和br_netfilter内核模块因为容器网络和 iptables 过滤都依赖它们。必须设置net.bridge.bridge-nf-call-iptables1和net.ipv4.ip_forward1否则 Pod 网络和流量转发都会出问题。必须关闭 swap或者至少确保 kubelet 不会因为 swap 开启而报错。容器运行时建议使用 containerd并且要把 cgroup driver 配置为 systemd与 kubelet 保持一致。这些条件听起来琐碎但几乎每个部署失败的案例都能在这里找到源头。我见过有人明明配置了net.ipv4.ip_forward1却忘了sysctl -p让它立即生效结果集群初始化成功但节点网络始终起不来。提示执行kubeadm init --dry-run可以在真正初始化之前跑一遍预检。它能帮你提前发现内核参数、端口占用、运行时版本等问题强烈建议第一次部署时先用它兜底。2. 控制平面拆解1.28 集群里的四个关键组件如何协作2.1 kube-apiserver所有请求的唯一闸门kube-apiserver 是整个集群的入口也是一切控制流量的“唯一闸门”。无论是kubectl get pods、kubelet 上报节点状态还是 controller-manager 想要修改副本数所有请求都必须经过 kube-apiserver。它是唯一会同时和 etcd 通信的组件所有对集群状态的读写都以它为准。你可能会有疑问这么多请求都从它过它不会成为瓶颈吗确实会。所以 kube-apiserver 在设计上做了两件事一是支持水平扩展多实例通过 leader 选举机制分担读压力二是大量采用 watch 机制让其他组件维持长连接只要状态没有变化就不需要反复全量拉取数据。这也是为什么你一执行kubectl get pods会觉得很快因为很多数据其实来自客户端侧的缓存或 watch 结果。在 1.28 版本里apiserver 对请求并发和内存消耗的控制更加敏感。默认的--max-requests-inflight和--max-mutating-requests-inflight并不是无限大一旦超过阈值新请求会被拒绝并返回 429 错误。生产环境里如果你的 controller 突然高频重试或者有某个异常客户端疯狂调 API很容易把 apiserver 打到限流状态。架构层面记住一句话apiserver 是集群的“神经系统”它挂了其他组件再正常也无法协同工作。2.2 etcd强一致性存储与 Raft 的胜负规则etcd 是集群唯一的状态存储负责保存所有资源对象的数据。Pod、Service、Deployment、ConfigMap、Secret包括节点信息最终都是以 key-value 形式存在 etcd 里。kubeadm 默认会部署一个单节点的 etcd它和 apiserver 同机运行数据目录是/var/lib/etcd。etcd 之所以重要是因为它采用 Raft 共识算法来保证数据一致性。简单说在一个多节点 etcd 集群里只有超过半数节点确认写入这次写入才算成功。单节点 etcd 虽然因为“只有一个人说了算”而写性能最好但也意味着它挂了整个集群都读不到数据。所以生产环境通常会部署三节点或者五节点 etcd 集群kubeadm 也支持在初始化时通过配置文件自定义 etcd 节点。在架构层面不建议把业务 Pod 调度到 etcd 所在的控制平面节点上。原因很简单etcd 对 IO 延迟非常敏感而业务容器的磁盘、网络和 CPU 争抢都可能把 etcd 拖慢。分布式系统里数据面的性能抖动最终都会反馈为控制面的不稳定。2.3 scheduler 与 controller-manager监督者和调度者的分工kube-scheduler 和 kube-controller-manager 经常被混在一起但职责完全不同。kube-scheduler 负责“调度”也就是为新创建的 Pod 选择一个最合适的节点。它考虑的维度包括容器资源请求、节点标签、污点容忍、亲和性规则以及 1.28 版本里更细粒度的拓扑分布约束。调度器本身不创建 Pod它只会把一个待调度的 Pod 标记上nodeName。kube-controller-manager 负责“调和”它内部包含了一大堆控制器循环。DeploymentController 发现实际副本数和期望副本数不一致时会创建或删除 PodNodeController 周期性地检查节点心跳超过一定时间没收到就会把节点标记为 NotReadyEndpointsController 负责维护 Service 后面的一组 Pod 地址。这两者之间的关系可以理解成项目经理和监工。scheduler 决定“活派给谁”controller-manager 决定“活有没有干完”。它们都不会直接操作容器只通过 apiserver 修改资源状态。2.4 静态 Podkubeadm 让控制平面自举的关键设计控制平面组件本身跑在容器里这带来一个先有鸡还是先有蛋的问题apiserver 和 etcd 需要容器运行时来启动但容器 Pod 又需要 apiserver 来调度管理。kubeadm 的解法是静态 Pod。静态 Pod 是 kubelet 直接识别并启动的 Pod不需要通过 apiserver 调度。只要 YAML 文件出现在/etc/kubernetes/manifests目录里kubelet 就会盯着这个目录文件一旦改变它就会重建容器。所以 kubeadm init 其实只是在做一件事先把 kubelet 这个“地基”装好然后把控制平面组件的 YAML 放到指定目录kubelet 再反过来把 apiserver、etcd 这些组件拉起来。这也是为什么控制平面组件崩溃后不需要你去systemctl restart docker之类的操作kubelet 会自动感知并重启它们。理解静态 Pod就理解了 kubeadm 整个自举过程的精髓。3. 工作节点的运行闭环kubelet、kube-proxy 与容器的协作3.1 kubelet节点上最忙的协调者工作节点加入集群后真正干活的是 kubelet。它运行在每个节点上相当于节点自己的“小控制平面”。kubelet 会持续监听 apiserver拿到分配给本节点的 Pod 列表然后通过 CRI 协议告诉容器运行时启动这些容器、挂载这些卷、执行这些健康检查。kubelet 的很多问题都跟身份凭证有关。它通过/etc/kubernetes/kubelet.conf使用自己的客户端证书访问 apiserver。1.28 版本已经默认开启证书轮换机制证书快过期时 kubelet 会自己请求新证书不需要手工干预。这一点很重要因为以前很多集群运行一年后突然“失联”就是 kubelet 证书过期导致的。3.2 Bootstrap Token加入集群时的身份交接工作节点执行kubeadm join时靠的是两块信息一个是 bootstrap token另一个是 CA 证书的哈希值。token 相当于一次性的邀请码默认有效期 24 小时CA 证书哈希则用来确认你正在连接的确实是目标集群的 apiserver防止中间人欺骗。加入了初始握手之后kubelet 会生成自己的私钥和证书签名请求通过 bootstrap token 的权限提交 CSR最后由 kube-controller-manager 里的 CSRApprovingController 自动批准。整个流程完成后工作节点才真正获得稳定的身份凭证之后就不再依赖 bootstrap token 了。注意如果执行kubeadm join时提示 token 过期不需要重新初始化集群直接在控制平面节点执行kubeadm token create --print-join-command重新生成即可。这是一个非常高频的运维操作。3.3 kube-proxy 的转发逻辑与 1.28 的新后端kube-proxy 解决的是 Service 访问问题。它会把 Service 的虚拟 IPClusterIP转换为后端 Pod 的 IP。默认模式是 iptables数据包进来之后通过规则链做 DNATIPVS 模式则使用内核的 IP 虚拟服务器模块规则更高效适合大规模 Service 场景。1.28 版本引入了新的 nftables 后端作为 iptables 的现代化替代方向但整体还处于逐步成熟阶段。需要强调一点kube-proxy 不负责 Pod 之间的网络连通那部分由 CNI 插件完成。定位类似“入口转发代理”而不是“网络工程师”。所以集群里网络插件和 kube-proxy 各管一段判断网络故障时先分清是哪一层的职责。3.4 从 sandbox 到容器一次 Pod 启动的完整过程当 kubelet 收到一个新 Pod 任务时它会先让 containerd 创建一个 sandbox 容器也就是你常看到的 pause 容器。pause 容器的唯一作用是把一组网络命名空间、IPC 命名空间等底座提前建好业务容器启动时再共享这些命名空间。这样做的好处是Pod 内容器具备一致的网络视角所以访问 localhost 就能互相通信。随后 kubelet 逐个启动业务容器。每个容器启动之前都要先从镜像仓库拉取镜像、创建挂载卷、设置环境变量和启动命令。这些动作全部通过 CRI 接口发往 containerdcontainerd 再根据 OCI 规范调用底层运行时去创建进程。整个链路里kubelet 是决策者containerd 是执行者runC 是最底层真正拉起进程的“工匠”。4. kubeadm 部署 1.28 的实操细节命令、参数与验证4.1 基础环境配置内核模块、sysctl 与 containerd在控制平面节点上第一步是配置内核。推荐做法是手动创建/etc/modules-load.d/k8s.conf内容写入overlay和br_netfilter两个模块然后执行modprobe加载。接下来修改/etc/sysctl.d/k8s.conf塞入网络转发相关参数再执行sysctl --system生效。containerd 的配置是另一个重点。安装完成后先执行containerd config default | tee /etc/containerd/config.toml生成默认配置然后确认SystemdCgroup被设置为true。这个参数没配好kubelet 和 containerd 对进程 cgroup 的认知就会不一致轻则无法正确识别资源使用量重则容器启动失败。4.2 一份可复用的 kubeadm 配置文件推荐把初始化参数写进 YAML 文件而不是在命令行堆一堆--pod-network-cidr之类的参数。下面这份是 1.28 常见组合apiVersion: kubeadm.k8s.io/v1beta3 kind: InitConfiguration localAPIEndpoint: advertiseAddress: 192.168.10.10 bindPort: 6443 --- apiVersion: kubeadm.k8s.io/v1beta3 kind: ClusterConfiguration kubernetesVersion: v1.28.2 controlPlaneEndpoint: 192.168.10.10:6443 networking: podSubnet: 10.244.0.0/16 serviceSubnet: 10.96.0.0/12这里的advertiseAddress是本机对外提供 apiserver 服务的地址controlPlaneEndpoint则是高可用场景下所有客户端连接 apiserver 的统一入口。如果只有一个控制平面节点两者可以填同一个 IP如果有负载均衡器controlPlaneEndpoint就写负载均衡器的虚拟 IP 或域名。podSubnet和serviceSubnet必须根据你后续选择的网络插件来定。比如 Flannel 常用10.244.0.0/16Calico 默认则偏爱192.168.0.0/16两者不能冲突。4.3 kubeadm init 的六个阶段分别干什么执行kubeadm init --configkubeadm-config.yaml后你会看到一系列阶段输出。归纳下来大致是preflight检查系统环境、端口占用、运行时是否可用。certs生成和签发各种证书写入/etc/kubernetes/pki。kubeconfig生成给不同组件使用的 kubeconfig 文件。control-plane把 apiserver、controller-manager、scheduler 的静态 Pod 清单写入/etc/kubernetes/manifests。etcd同样以静态 Pod 方式启动 etcd。mark-control-plane给控制平面节点打上标签和污点并配置 admin.conf。如果初始化失败先看是哪个阶段报错。很多问题会集中在 preflight 和 control-plane。前者是环境检查没过后者很可能是镜像拉取失败或静态 Pod 配置有问题。4.4 安装网络插件与验收命令控制平面初始化成功后节点还处于 NotReady 状态。这是因为 CNI 网络插件尚未安装容器间的网络无法建立kubelet 的节点状态检查过不去。此时把网络插件的 YAML 文件下载下来执行kubectl apply -f即可。安装完成后需要验证几个关键对象kubectl get nodes kubectl get pods -n kube-system kubectl get cs第一行确认节点都是 Ready第二行确认控制平面的 Pod 都处于 Running第三行看组件健康状态。我遇到过有人只看第一项就认为集群成功了结果发现 kube-system 里一堆 CrashLoopBackOff后续所有操作都不稳定。所以验收时一定要同时检查这三项才算数。5. 证书、升级与 etcd 备份1.28 集群长期运维要点5.1 PKI 目录与证书轮换kubeadm 会在/etc/kubernetes/pki目录里建立一套完整的 PKI 体系。CA 证书的有效期默认是 10 年但 apiserver、etcd、kubelet 等组件的证书只有 1 年。这意味着集群长期运行后证书续期是每个运维人员都必须掌握的基本操作。kubeadm certs check-expiration可以一次性列出所有证书的过期时间。手动续期用kubeadm certs renew all执行完毕后需要重启相关组件才能加载新证书。如果只是 kubelet 证书过期很多情况下 kubelet 已经支持自动轮换但前提是证书签发流程正常、节点没有长期失联。提示证书过期不是一个“等到那天再说”的问题。1.28 集群尤其是生产环境一定要把证书检查纳入周期巡检脚本至少每个月跑一次kubeadm certs check-expiration。5.2 版本升级路线与注意事项Kubernetes 官方版本升级有一个基本原则不能跨大版本跳跃。1.27 可以直接升级到 1.28但如果你的集群还在 1.26想升到 1.28就必须先升级到 1.27再升到 1.28。升级顺序也有讲究先升级 kubeadm 工具本身再在控制平面节点执行kubeadm upgrade plan查看可升级版本然后执行kubeadm upgrade apply v1.28.x升级控制平面最后逐个升级工作节点上的 kubelet 和 kubeadm。升级前需要看当前版本到 1.28 之间的 API 变更。有些 API 在中间版本里被废弃或移除如果业务还在使用旧版本 API 资源升级后可能出现资源无法解析的问题。最稳妥的做法是先在测试集群走一遍升级并把整个升级窗口安排在业务低峰期。5.3 etcd 备份与恢复的基本方法apiserver 是集群的“大脑”etcd 就是“记忆”。丢了记忆再聪明的大脑也白搭。所以在架构规划里etcd 备份永远值得优先考虑。单节点 etcd 的备份很简单通过 etcd 的 snapshot 接口把数据打成快照文件保存到独立存储位置。kubeadm 部署的 etcd 运行在静态 Pod 里可以直接进入 etcd 容器执行etcdctl相关命令也可以通过etcdctl snapshot save的方式导出快照。恢复时先把集群置为维护状态再通过etcdctl snapshot restore把数据恢复到新数据目录然后替换 etcd 的静态 Pod 配置。如果不做备份遇到硬盘损坏或者误删除资源恢复难度会成倍上升。即使你的环境里已经有监控和日志也不能替代等。这是我在模拟项目X里学到的最深刻教训。6. 踩坑实录最容易让新手反复折腾的几个问题6.1 NotReady 的背后往往是 CIDR 不一致很多人的集群第一次kubectl get nodes就看到 NotReady第一个怀疑对象通常是网络插件没装好于是反复卸载重装。其实最典型的根因是 kubeadm 的podSubnet和 CNI 插件里的 IP 池配置不一致。比如 kubeadm 配置了10.244.0.0/16Flannel 默认就能对上但如果你换用 Calico却没有修改 Calico 的CALICO_IPV4POOL_CIDRCalico 默认可能用192.168.0.0/16那集群内部网络就直接劈成两半。解决方法是确认网段配置一致后再重启相关 Pod 或者重新 apply 一遍 CNI 清单。6.2 containerd 的 cgroup driver 与 kubelet 不匹配这个问题在 containerd 场景里特别常见。kubelet 默认推荐 systemd cgroup driver而 containerd 默认配置往往还是cgroupfs。表现出来的现象比较多样节点刚启动时正常过一段时间 kubelet 报错或者 Pod 创建后一直 ContainerCreating。排查方向很明确打开/etc/containerd/config.toml找到SystemdCgroup字段改成true然后重启 containerd再systemctl restart kubelet。很多所谓“集群不稳定”的诡异现象其实都是这里没对齐。6.3 swap 没有完全关闭节点状态反复跳动Kubernetes 对节点 swap 的要求是关闭。很多人只在当前的 shell 里执行了swapoff -a没有删除/etc/fstab里的 swap 条目。结果一重启swap 又回来了节点状态开始反复横跳。正确做法是两条命令一起做swapoff -a处理当前会话然后用编辑器注释掉/etc/fstab里的 swap 行。这样即使重启swap 也不会再被自动挂载。6.4 已经运行的集群不要随便改主机名和 IP有次模拟项目X里的同事为了整理机器命名把一个运行中的工作节点的主机名改了。结果 kubelet 在注册时用新主机名上报和旧节点的记录冲突最终导致节点被标记为 Unknown之后又变成 NotReady。在 Kubernetes 的架构里节点身份与主机名、IP 强相关。如果确实要改名标准流程是先把节点kubectl cordon并驱逐上面的 Pod然后把节点从集群中删除改完主机名后再重新kubeadm join。不要跟系统文件“硬刚”那只会让问题更复杂。最后再说一点我个人的习惯是每次 kubeadm 部署完成之后会把/etc/kubernetes/manifests下的四个 YAML 文件从头到尾读一遍再观察一遍 kube-system 里各 Pod 的启动顺序。这种“边读配置边看表现”的方式比我当年啃一整本原理书都管用。如果你还在准备第一次初始化别忘了kubeadm init --dry-run这个选项它能提前暴露大部分硬件环境问题。等集群真正跑起来之后也不要放过任何一个看似不起眼的警告日志很多架构层面的隐患最初都只是日志里一行不起眼的 warning。