
1. 动手之前版本选型、角色规划与关键概念1.1 为什么选择 1.31 版本Kubernetes 1.31 是 2024 年 8 月正式发布的版本携带了不少很有分量的功能。比如 image volume 这个功能在 1.31 转为了 GA正式可用crds 和 API 聚合层也有很多细节优化。作为经历过从 1.20 一路装到 1.31 的老用户我的体感是1.31 在经过两三个 patch 版本之后稳定性已经相当不错尤其和 containerd 的联动、kube-proxy 的转发逻辑都比早期版本顺滑很多。所以这里给一个非常实际的建议如果你打算在生产或长期测试环境装 1.31尽量装到 1.31.4 或 1.31.5 这样的后续小版本不要卡在最早的 1.31.0 上。小版本里修了不少和 CRI 运行时、kubelet 心跳相关的 bug。安装方式上只要把版本号换成对应的小版本即可本文使用的命令全部兼容。还需要说明一点如果你看完这篇文章想去装更新的版本思路大体是类似的但有几个参数要重新确认比如 pause 镜像版本sandbox_image、kubeadm init 里的某些 flag 是否还存在。K8s 每个小版本都在调整默认行为照搬老版本命令经常会在一个不起眼的地方失败。1.2 集群节点角色与资源规划我这次搭建的是一个非常典型的三节点集群节点角色IP 地址硬件配置操作系统master1控制平面192.168.10.202C4GRocky Linux 9.4node1工作节点192.168.10.212C4GRocky Linux 9.4node2工作节点192.168.10.222C4GRocky Linux 9.4控制平面节点 2C4G 是底线如果是单机测试加上网络插件内存低于 2G 会出现 kubelet 频繁被 OOM 干掉的情况。如果你后面要跑复杂的生产工作负载控制节点建议 4C8G 起步。这里的 IP 和主机名大家可以替换成自己的但/etc/hosts里的映射关系一定要配好这一步偷懒后面全是坑。1.3 先搞明白 kubelet、containerd 和 Docker 的关系和很多刚接触 K8s 的朋友一样你可能还在想装 K8s 是不是要先装 Docker装完 Docker 是不是要配置 Docker 作为运行时这里我把概念一次性说清楚kubelet运行在每个节点上的代理进程负责接收控制平面下发的 Pod 定义然后去真正的运行时比如 containerd里创建、启停容器。containerd容器运行时真正干活的引擎。kubelet 通过 CRIContainer Runtime Interface和它通信。Docker上层封装了 containerd提供更友好的命令行和完整容器生态。K8s 从 1.24 开始就移除了对 docker-shim 的支持也就是说 kubelet 现在直接走 CRI 调用 containerd完全不需要安装 Docker。为什么要这样改因为 K8s 之前的链路是kubelet - docker-shim - docker - containerd中间多了 docker-shim 一层导致容器状态汇报有空档问题排查也麻烦。现在直接kubelet - containerd链路短性能更好报错也更直观。如果你习惯了docker ps在集群里要看 Pod 容器得用crictl ps下文会有详细用法。1.4 Pod 网段的提前决策K8s 里每个 Pod 都会获得一个独立的集群内 IP这个 IP 网段由 CNI 插件分配。Calico 默认的 Pod 网段是192.168.0.0/16Flannel 默认是10.244.0.0/16。我的物理网络正好是192.168.10.0/24如果直接用 Calico 默认网段就会和宿主机网段冲突后面 Pod 到宿主机通信会出现各种诡异的问题。所以我这里选择把 Pod 网段设置为10.244.0.0/16并搭配 Calico 使用。还有一个常见的网段是 Service 网段ClusterIP默认是10.96.0.0/12kubeadm init 的时候用--service-cidr指定。一般不用改只要和你机房内网的网段不重叠就行。做这个决策最稳妥的方式是先统计现有物理网络有哪些网段再避开它们规划 Pod CIDR 和 Service CIDR。2. 操作系统基础初始化模块、参数、防火墙一次性搞定2.1 主机名与 hosts 配置三台机器先固定主机名避免 kubeadm 初始化后证书里的名称混乱# master 节点 hostnamectl set-hostname master1 # node 节点 hostnamectl set-hostname node1 hostnamectl set-hostname node2然后编辑/etc/hosts把三台机的映射写进去192.168.10.20 master1 192.168.10.21 node1 192.168.10.22 node2很多新手栽在这hostname 没设置导致/etc/hosts里的解析和节点自称的名字不一致kubeadm init 生成的 apiserver 证书里主机名与实际网络名不匹配kubectl 访问时经常报证书 hostname 校验失败。2.2 加载 overlay 和 br_netfilter 内核模块K8s 的网络转发和容器存储都依赖两个内核模块overlayoverlayfs 文件系统给容器层提供联合挂载能力。br_netfilter让经过 Linux 网桥的流量也能被 iptables 规则过滤K8s 的 Service 转发和 kube-proxy 的 iptables 模式依赖它。创建/etc/modules-load.d/k8s.confoverlay br_netfilter然后立即加载modprobe overlay modprobe br_netfilter接着创建/etc/sysctl.d/k8s.confnet.bridge.bridge-nf-call-iptables 1 net.bridge.bridge-nf-call-ip6tables 1 net.ipv4.ip_forward 1执行sysctl --system让配置生效。其中ip_forward不打开的话Pod 内的流量就无法被转发到宿主机之外出网自然就不通。2.3 关闭 swap、防火墙和 SELinux# 临时关闭 swap swapoff -a # 永久关闭注释掉 /etc/fstab 里的 swap 行 # 关闭防火墙 systemctl disable --now firewalld # 关闭 SELinux setenforce 0 sed -i s/^SELINUXenforcing/SELINUXpermissive/ /etc/selinux/config为什么不保留 swapkubelet 在做内存资源统计时要求节点内存是可预测的开 swap 之后 cgroup 的数据会混乱节点会被 kubelet 标记为 NotReady。这也是 K8s 一直以来的硬性要求。防火墙方面如果企业环境不允许关闭需要放行 6443apiserver、2379/2380etcd、10250kubelet、30000-32767NodePort这些端口非常容易漏测试环境直接关掉省心。2.4 时间同步最容易被忽略的一步K8s 集群内大量用到证书和 token 校验节点时钟偏差超过五分钟就会出现无法认证、kubelet 反复报 x509 证书过期之类的怪问题。用 chrony 同步yum install -y chrony systemctl enable --now chronyd timedatectl set-ntp true装完之后用timedatectl status确认时间源正常。这一步我当时觉得没必要跳过后来在排查证书报错时吃过亏白白排查了两个小时。永远不要在时间同步上省事。3. 安装 containerd 运行时系统级踩坑提醒3.1 为什么直接用 containerd我前面说过K8s 从 1.24 开始就不依赖 Docker 了kubelet 直接通过 CRI 调用 containerd。所以你只需要一个 containerd 作为运行时。好处是不用装 Docker少一个守护进程少一份内存占用少了 docker 和 containerd 之间的版本兼容问题。3.2 安装方式选择在 Rocky Linux 9.x 上推荐直接从 Docker 官方源或者国内镜像源安装 containerd.io# 安装 yum-utils 和配置仓库 yum install -y yum-utils yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo yum install -y containerd.io-1.7.23如果访问官方源慢可以把仓库地址换成阿里云镜像yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo指定版本号很重要不要装最新的大版本后不见得和 kubeadm 1.31 完全兼容。containerd 1.7.x 是目前的生产趋势稳定性和 K8s 1.29 的配合都没有问题。3.3 配置 SystemdCgroup 和 sandbox_imagecontainerd 安装完成后先做一次默认配置生成mkdir -p /etc/containerd containerd config default | tee /etc/containerd/config.toml在config.toml里需要改两个关键点第一改成 systemd cgroup 驱动。原因是 kubelet 默认使用 systemd 作为 cgroup 驱动containerd 如果还用 cgroupfs两者对进程 cgroup 的管理方式不一致节点启动后 kubelet 会直接报错。找到对应位置[plugins.io.containerd.grpc.v1.cri.containerd.runtimes.runc] [plugins.io.containerd.grpc.v1.cri.containerd.runtimes.runc.options] SystemdCgroup true第二修改 sandbox_image。这是 pause 镜像的地址K8s 1.31 对应的 pause 版本是 3.10。如果直接使用默认的registry.k8s.io/pause:3.10在部分网络环境下拉取会非常慢建议改成阿里云等更快的镜像仓库[plugins.io.containerd.grpc.v1.cri] sandbox_image registry.aliyuncs.com/google_containers/pause:3.10改完重启并验证systemctl daemon-reload systemctl enable --now containerd ctr version看到 containerd 版本信息就说明运行时起来了。接下来可以先用crictl ps验证一下 CRI 接口是否响应一般会提示连接失败因为还没有任何配置不要担心等后续加入集群后就会正常。3.4 验证 containerd 与 kubelet 的配合准备这里分享一个我强烈建议提前做的小验证手动通过ctr拉取一个测试镜像确认网络和镜像仓库通路正常。因为 kubeadm init 时 kubelet 会去拉取一组镜像apiserver、etcd、controller-manager、scheduler、pause 等这些镜像拉不下来后面 kube-apiserver 的静态 Pod 就一直起不来报错往往就是the api server is not healthy。提前验证能帮你把问题定位从网络不通和配置错误中快速分离。4. 安装 kubeadm、kubelet、kubectl 并锁定版本4.1 配置 Kubernetes 软件源Rocky Linux 9 的 K8s 软件包仓库地址和之前的 CentOS 7 写法类似只是el9换了一下。我用的是阿里云镜像仓库访问速度比较快cat EOF /etc/yum.repos.d/kubernetes.repo [kubernetes] nameKubernetes baseurlhttps://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el9-x86_64/ enabled1 gpgcheck0 repo_gpgcheck0 EOF注意这里我把 gpgcheck 设为 0测试环境可以接受生产环境更严谨的做法是导入官方 GPG key 并开启校验避免软件包被篡改。如果你用的是 Ubuntu对应的源是curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.31/deb/Release.key | gpg --dearmor -o /usr/share/keyrings/kubernetes-apt-keyring.gpg echo deb [signed-by/usr/share/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.31/deb/ / /etc/apt/sources.list.d/kubernetes.list4.2 安装指定小版本为了保证三台机器的 kubeadm、kubelet、kubectl 完全一致我建议指定精确版本号yum install -y kubeadm-1.31.4 kubelet-1.31.4 kubectl-1.31.4 --disableexcludeskubernetes--disableexcludeskubernetes是为了绕过仓库中默认的 exclude 规则因为 kubernetes 仓库会把所有包加进 exclude不指定这个参数经常装不上版本。装完以后用命令锁定版本防止后续yum update自动升级yum install -y python3 python3 -c import yaml 2/dev/null || yum install -y python3-yaml更简单的方式是用yum versionlock插件yum install -y yum-plugin-versionlock yum versionlock kubeadm kubelet kubectl4.3 kubelet 开机自启的假失败现象systemctl enable --now kubelet执行完用systemctl status kubelet查看大概率会看到一个失败状态。不要慌这是正常现象。因为 kubelet 起来后会去找/etc/kubernetes/manifests里的静态 Pod 定义但控制平面还没有初始化找不到配置文件kubelet 就一直处于等待重启的循环中。等 kubeadm init 完成后把配置文件生成到对应目录kubelet 会自动恢复正常。我第一次遇到这个情况差点把机器重置了现在写在这里帮你避开这个心理恐慌。5. Master 节点初始化重点排查 api server is not healthy 报错5.1 kubeadm init 命令的参数解析在所有准备工作完成后在 master1 上执行初始化kubeadm init \ --apiserver-advertise-address192.168.10.20 \ --image-repositoryregistry.aliyuncs.com/google_containers \ --kubernetes-versionv1.31.4 \ --pod-network-cidr10.244.0.0/16 \ --service-cidr10.96.0.0/12逐项解释一下--apiserver-advertise-address指定 apiserver 对外宣告的 IP也就是 master 节点的局域网 IP。不指定的话kubeadm 会自己探测默认路由出口的 IP有多网卡的机器容易探错。--image-repository镜像仓库地址。这里我用了阿里云的google_containers镜像仓库镜像路径结构和官方保持一致。如果你网络直连稳定也可以省略这个参数使用官方仓库。--kubernetes-version必须和 kubeadm 版本一致否则会提示版本不匹配。--pod-network-cidrPod 网段和我在 1.4 节规划的10.244.0.0/16对应等会儿装 Calico 的时候要一致。--service-cidrService 网段保持默认的10.96.0.0/12即可。执行后耐心等待两到三分钟kubeadm 会在后台依次拉镜像、初始化 etcd、生成证书、以静态 Pod 方式启动控制平面组件。如果一切正常结尾会输出两段提示一段是kubeadm join命令加入工作节点用另一段是kubectl配置文件设置方式。5.2 常见的 the api server is not healthy 排查链路如果你很不幸地看到了这样一行提示[wait-control-plane] Waiting for the kubelet to boot up the control plane as static Pods from directory /etc/kubernetes/manifests ... [kubelet-check] The API server is not healthy after 4m0.00747357s它真正的含义是kubeadm init 在四分钟时间内反复探测 apiserver 的/readyz接口一直没有返回 200。也就是说控制平面的静态 Pod 没有起来或者起来了但没通过健康检查。注意不要被报错后的各种日志吓到下面这条排查链路可以帮你系统定位第一步查看 kubelet 日志journalctl -u kubelet -f如果在日志里看到类似于failed to load、Unable to connect to container runtime、cgroup driver这样的关键词多半是 containerd 配置有问题重点回查 3.3 节讲到的SystemdCgroup是否设置为 true。第二步查看静态 Pod 状态crictl ps -a正常情况下应该能看到 kube-apiserver、etcd、kube-controller-manager、kube-scheduler 这几个容器。如果crictl ps -a里只有 pause 容器说明镜像配置有问题如果一个容器都没有说明 kubelet 根本没拉起静态 Pod回到第一步继续看日志。第三步查看 apiserver 容器日志crictl logs $(crictl ps -a | grep kube-apiserver | awk {print $1})apiserver 起不来最常见的原因是证书生成错误、etcd 无法连接、镜像版本不对。日志里会有明确的报错比如etcdserver: request timed out就说明 kube-apiserver 到 etcd 的网络或证书有问题。第四步排查镜像拉取失败crictl images看看控制平面那几个镜像是否都在。如果registry.aliyuncs.com/google_containers/kube-apiserver:v1.31.4系列镜像不存在说明拉取失败需要检查 DNS 和镜像仓库通路。这一步配合 3.4 节说的提前验证排查会非常快。第五步检查 cgroup 驱动是否一致cat /etc/kubernetes/kubelet.conf 2/dev/null # 或者查看 kubelet 的系统环境变量 cat /var/lib/kubelet/kubeadm-flags.env确保 kubelet 的配置文件中cgroupDriver为systemd并且/etc/containerd/config.toml里也是SystemdCgroup true。这两个不一致apiserver 即使能起来也会反复崩溃。经过上面几步90% 的api server is not healthy都能被定位出来。我这次实际遇到的是镜像拉取超时导致 apiserver 起不来后来把sandbox_image换了镜像源重跑 init 就通过了。5.3 重置集群并二次初始化无论哪种原因导致的 init 失败第一次失败后不建议直接重跑。正确姿势是先重置kubeadm reset -f rm -rf /etc/kubernetes /var/lib/kubelet /var/lib/etcd ~/.kube然后再回到 containerd 配置那里逐项检查确认无误后重新执行kubeadm init。为什么要彻底清理因为 kubeadm init 失败后之前的 etcd 数据、证书文件甚至 kubelet 配置可能残留不清理干净重跑会叠加出更多诡异问题。把这一步当成习惯能省你大量时间。5.4 初始化成功后的 kubectl 配置初始化成功后kubeadm 会提示你配置 kubectl 的 kubeconfigmkdir -p $HOME/.kube cp -i /etc/kubernetes/admin.conf $HOME/.kube/config chown $(id -u):$(id -g) $HOME/.kube/config然后验证控制平面状态kubectl get nodes你会看到 master1 处于 NotReady 状态这是正常的因为网络插件还没装。同时可以用kubectl get pods -n kube-system观察控制平面组件是否运行等网络插件就绪后节点就会变成 Ready。6. 安装 Calico 网络插件、工作节点加入与最终验收6.1 Calico 安装注意版本与 Pod 网段一致性节点 NotReady 的根源就是没有 CNI 插件。我使用的是 Calico因为它对 NetworkPolicy 支持完整、BGP 模式性能好。官方推荐方式有两种Operator 部署和 manifest 方式部署。这里我用相对直观的 manifest 方式curl -o calico.yaml https://raw.githubusercontent.com/projectcalico/calico/v3.29.1/manifests/calico.yaml然后打开calico.yaml找到 CALICO_IPV4POOL_CIDR 环境变量把值改成和 kubeadm init 里一致的网段- name: CALICO_IPV4POOL_CIDR value: 10.244.0.0/16如果不改Calico 会使用自己的默认网段192.168.0.0/16和集群的 Pod CIDR 不一致Pod 网络就彻底错乱了。这个坑我已经见太多人踩过。然后应用清单kubectl apply -f calico.yaml等待一两分钟观察 calico 的 Pod 状态kubectl get pods -n calico-system全部 Running 之后再查看节点kubectl get nodes这时 master1 应该已经变成 Ready 了。6.2 工作节点加入集群在 node1、node2 上执行 kubeadm init 成功时输出的kubeadm join命令。如果当时命令没记下来可以在 master 上重新生成 tokenkubeadm token create --print-join-command比如输出是这样kubeadm join 192.168.10.20:6443 --token token --discovery-token-ca-cert-hash sha256:hash在 node1 和 node2 上分别执行。注意kubeadm join 同样依赖前面所有基础环境配置比如 containerd、内核参数、kubelet 组件。如果 join 报错重点看 node 上的 kubelet 日志大概率还是 cgroup 驱动或网络问题。等 30 秒左右回到 master 上确认kubectl get nodes看到三个节点全部 Ready 就说明集群真正搭建成功了。多节点模式下kubectl get nodes输出的角色信息里master 节点会标注control-plane, master工作节点只显示none角色。6.3 部署 Nginx 验证整个集群链路节点 Ready 不代表服务链路一定通。我最常用的验证方式是部署一个 Nginx 测试服务同时把 Service 和 Ingress 的链路一起验一遍kubectl create deployment nginx-test --imagenginx:1.27 kubectl expose deployment nginx-test --port80 --typeNodePort kubectl get svc nginx-test然后用任意一个节点的 IP 加 NodePort 访问比如curl http://192.168.10.21:NodePort如果返回 Nginx 默认欢迎页说明从调度Deployment、容器运行时containerd、网络Calico kube-proxy到 NodePort 转发整条链路都是通的。这一步做完你的 K8s 1.31 集群安装就算是真正全流程验收通过了。如果想再深一步验证跨节点通信可以试试两个不同节点上的 Pod 之间互相 ping IP能通就说明 Calico 跨主机的 BGP 或 Overlay 转发没有问题。7. 安装完成后的日常维护经验7.1 证书、token 与备份集群装好后有三件事建议立刻做一是把 master 上的/etc/kubernetes/目录整体备份一份这里面包含证书和 kubeconfig丢了重新生成的流程相当繁琐。二是记住 token 有效期默认 24 小时过期后需要重新用kubeadm token create --print-join-command生成。三是对 kubelet 和 containerd 的配置改动每次改完都要重启对应服务否则配置不生效而且还容易和运行中的静态 Pod 状态混淆。7.2 kubectl 常用排障命令速查用惯了 Docker 的人刚切到 K8s最常卡壳的地方就是怎么进容器。下面这张速查表建议收藏目的Docker 时代K8s 时代查看容器列表docker pskubectl get pods -o wide看容器日志docker logskubectl logs pod名进入容器docker exec -itkubectl exec -it pod名 -- /bin/bash查看运行时容器docker ps -acrictl ps -a查看运行时日志docker logs 容器IDcrictl logs 容器ID查看节点状态-kubectl describe node 节点名这里特别强调crictl的作用它才是直接面向 containerd 的管理工具在节点上用它观察静态 Pod 和运行时状态比kubectl更底层、更直接。很多kubectl层面看不出问题的场景靠crictl logs一下子就能看到根因。7.3 关闭 swap 和防火墙之后记得沉淀成文档踩过的坑要变成团队的资产。我的建议是把这套初始化脚本整理成 Shell 或 Ansible 剧本让以后新建节点的时候一条命令跑完所有基础配置。K8s 集群本身最大的复杂度不只在安装这一步而在于升级、证书轮换、故障排查这些长期运维事项。有个干净的初始化模板后面排障会轻松很多。装完这套 1.31 集群之后我的个人体会是K8s 安装阶段的大多数报错都不是版本问题而是基础环境一致性没做好。内核模块、cgroup 驱动、网段规划、镜像源、时间同步这五件事只要一次全做对后面基本一顺到底。尤其是那个反复出现的 api server is not healthy 报错90% 的情况下都是因为 containerd 的SystemdCgroup没开或者镜像拉取超时把本文第 5.2 节的排查链路走一遍几乎都能在三分钟内定位。最后再分享一个小技巧装完之后不妨故意在某个 worker 节点上执行一次systemctl stop kubelet看看节点状态何时变成 NotReady再systemctl start kubelet看它恢复 Ready。这一通操作能让你对 kubelet 的心跳机制和节点状态转换有非常直观的理解比看十遍文档都有用。这是我自己在搭建集群后必做的破坏性练习也推荐给你。