kubeadm部署Kubernetes集群:初始化、网络与故障排查实战
1. 为什么绕了一圈还是回到 kubeadm
kubeadm方式部署集群这件事,我在不同阶段干过三种完全不同的做法:早期照着文档一个二进制一个二进制地手工装,后来用各种自动化脚本打包,最后又老老实实回到 kubeadm。这个来回其实挺有代表性,很多刚接触容器编排的人也会走一遍同样的弯路。kubeadm 是 Kubernetes 官方维护的集群引导工具,它做的事情很聚焦:把一套符合官方最佳实践的控制平面装起来,把证书、静态 Pod、kubeconfig、启动参数这些琐碎但极容易出错的东西按标准生成好,剩下的交给你。它解决的核心问题是"装得快、装得对、装得可复现",特别适合中小规模自建集群、测试环境、离线环境、教学演示,以及想搞清楚集群到底由哪些零件组成的工程师。如果你只是想要一个能跑业务的集群,云上的托管服务更省事;但如果你需要掌控每一处配置、需要在内网里从零搭出一套、或者需要理解故障到底出在哪一层,kubeadm 是性价比最高的入口。
我见过太多人在这一步卡住:机器准备好了,命令也敲了,init 报了一堆错,然后就开始漫无目的地搜。真正的问题往往不在 kubeadm 本身,而在它前面的准备工作——内核参数没开、swap 没关干净、容器运行时的 cgroup 驱动和 kubelet 对不上、网段和宿主机网段撞了。这篇文章我打算把整个链路按我实际的施工顺序拆开讲,从选型判断到系统初始化、从运行时配置到 init 实录、从网络插件到节点扩容、最后给一份我自己常用的故障速查表。每个环节我都会说清楚"为什么这么配",因为参数抄错一个字母,排查时间可能就是两小时起步。
1.1 三种部署路径的真实取舍
先说清楚摆在面前的路有几条,免得看完文章还在犹豫。第一条是完全手工部署:自己下 etcd、自己生成证书、自己写 kube-apiserver 的 systemd unit。这条路的教学价值极高,你会被迫搞明白每一个组件的启动参数、每一个证书的用途、每一条 etcd 的 peer 配置。但代价是慢,一套三节点的集群从零到可用,熟手也要大半天,而且每次重装都得再来一遍,出错概率随着节点数线性上升。我早期干过一次,证书有效期配错、apiserver 的 service account key 忘生成,排查到半夜,最后发现是自己抄错了一行命令。这条路我只推荐给两种情况:一是真的要深入理解组件交互,二是环境极度特殊,任何引导工具都跑不起来。
第二条路是用各种社区脚本或者配置管理工具批量生成,把手工步骤固化成模板。这条路在规模化场景下是对的,但它有个前提——你已经手工趟过一遍,知道哪里会出问题。如果跳过手工阶段直接上模板,脚本报错的时候你连日志应该去哪看都不知道。我见过一个团队,脚本里写死了某个版本的镜像地址,版本一升级整个部署流程全废,没人知道该改哪一行。
第三条就是 kubeadm。它的定位很清晰:官方维护、跟随版本发布、把控制平面用静态 Pod 的方式跑起来。静态 Pod 这个设计值得多说一句,kube-apiserver、kube-controller-manager、kube-scheduler、etcd 的 manifest 文件被放在/etc/kubernetes/manifests目录下,kubelet 自己监视这个目录,发现有文件就拉起对应的容器。这意味着控制平面不依赖 kubelet 之外的任何东西,kubelet 挂了整个集群才真的挂了,但它自己又是 systemd 管的,重启策略很直接。这个链条短,出问题的时候定位路径清晰,是我偏好它的主要原因。
提示:kubeadm 引导出的集群,控制平面组件是静态 Pod,
kubectl get pods -n kube-system能直接看到。如果你发现这几个 Pod 不见了,先去看/etc/kubernetes/manifests目录里的文件还在不在,而不是去查 Deployment。
1.2 kubeadm 的边界:它管什么、不管什么
很多人对 kubeadm 有误解,以为跑完 init 就万事大吉,结果发现节点是 NotReady、CoreDNS 一直 Pending。这里必须把边界划清楚。
kubeadm 负责的部分:检查系统前置条件(部分)、生成 CA 和各类证书、生成各组件的 kubeconfig、渲染静态 Pod manifest、拉起控制平面、生成 bootstrap token、输出 join 命令、管理集群升级和证书续期。它不负责的部分:安装容器运行时、安装 CNI 网络插件、安装存储插件、配置负载均衡器、调整内核参数、关闭 swap、配置防火墙规则、安装 metrics 或日志组件。这些全部要你自己做,而且顺序不能乱——容器运行时必须在 kubeadm 之前装好,CNI 必须在 init 之后立刻装,否则 CoreDNS 起不来。
这个边界划清楚之后,很多事情就顺了。比如"为什么我的节点 NotReady",八成不是 kubeadm 的问题,而是 kubelet 没跑起来、或者 CNI 没装、或者 cgroup 驱动不匹配。再比如"为什么 CoreDNS 是 Pending 状态",因为 CNI 没装,Pod 拿不到 IP,调度器调度下去了但网络没通,容器起不来。这些因果关系搞明白,排查就变成了按图索骥。
还有一点值得提:kubeadm 不会自动帮你做高可用。单主集群 init 出来就是一个 apiserver 实例,挂了就是挂了。要做多主,你得自己准备负载均衡和虚拟 IP,这个我在后面第 6 章会展开。
1.3 版本与组件配套的对照表
版本匹配这件事,我吃过亏,所以单独拉出来讲。Kubernetes 的版本节奏是小版本每四个月左右一个,每个小版本的支持窗口有限。kubeadm 的配置文件格式也在演进,早期是 v1beta1、v1beta2,现在主流是 v1beta3,新版本又引入了 v1beta4。配置文件里写了不支持的 apiVersion,kubeadm 会直接拒绝,且报错信息不算特别友好。
实际施工时,我建议按下面的思路锁版本:
| 关注项 | 建议做法 | 踩坑说明 |
|---|---|---|
| kubeadm / kubelet / kubectl | 三者版本必须完全一致 | 小版本号不同就可能出现 kubelet 连不上 apiserver |
| kubeadm 配置 apiVersion | 与 kubeadm 主版本严格对应 | 配置模板跨版本直接复制是最常见的失败原因 |
| 容器运行时 | 与 Kubernetes 版本的 CRI 兼容性对齐 | 运行时版本过旧,CRI 接口可能不完整 |
| pause 镜像 | 必须与 kubeadm 期望版本一致 | 不一致时 Pod 卡在 sandbox 创建失败 |
| CNI 插件 | 与 Kubernetes 版本做兼容性验证 | 过于陈旧的 CNI 可能不支持新的 NetworkPolicy 字段 |
| etcd | 由 kubeadm 决定,不要手动干预 | 手动改版本会破坏静态 Pod 的一致性 |
我通常的做法是先用kubeadm config images list把当前 kubeadm 期望的镜像列表打出来,跟本地镜像仓库里已有的对照,缺什么补什么。这一步在离线环境里尤其关键,因为镜像拉不下来的时候 init 会卡在Pulling images很久,最后超时失败,报错还容易被误判成网络问题。
# 查看当前 kubeadm 期望的镜像清单 kubeadm config images list # 指定版本查看 kubeadm config images list --kubernetes-version v1.30.4这两条命令输出的清单,就是你在私有仓库里必须准备好的全部镜像。我一般会把这个清单存成一个文件,作为离线物料的一部分,重装或者扩容的时候直接对照。
2. 开装之前必须钉死的三张清单
动手之前我会把三件事写成清单贴在旁边:机器规格、网络规划、系统初始化。这三件事任意一件没做对,后面都得推倒重来。特别是网络规划,它属于那种"装完了才发现不对"的类型——Pod 网段和公司内网网段撞了,表面上集群是好的,但某些服务就是互相访问不到,排查起来极其恶心,最后只能整个集群重装。
2.1 机器规格与操作系统基线
规格这块,测试环境和生产环境的差别很大,我不给绝对数字,给一个能推下去的判断逻辑。控制平面节点的压力主要来自 apiserver 和 etcd,etcd 对磁盘 IO 延迟极其敏感,所以控制平面尤其是 etcd 所在节点,磁盘必须是 SSD,机械盘会让整个集群的响应变慢,而且这种慢是全方位的,你会觉得"什么操作都卡"。内存方面,kube-apiserver 的缓存和 etcd 的数据集都在内存里,2GB 只能勉强跑个演示,4GB 是能用的下限,8GB 以上才比较从容。
工作节点的规格取决于你跑什么。我自己的经验是,节点数比单节点规格更重要——三个 4 核 8G 的节点,比一个 16 核 32G 的节点好用得多,因为调度、故障隔离、滚动升级都需要多节点。单节点集群在测试阶段够用,但它会掩盖很多问题,比如污点与容忍的配置错误、Pod 反亲和没生效,这些问题只有多节点才会暴露。
操作系统层面,我的基线是这样几条:
- 内核版本不要太旧,较新的稳定版内核在 cgroup v2、网络命名空间、eBPF 相关特性上支持更好,很多诡异问题会自然消失。
- 关闭 swap,且要在
/etc/fstab里注释掉,光swapoff -a重启就回来了。kubelet 默认要求 swap 关闭,理由是不希望内存调度的不确定性影响 Pod 的行为。 - 主机名必须唯一且能互相解析,不要用默认的 localhost,也不要用带下划线或者大写的名字,DNS 规范里不允许。
- 时间必须同步,chrony 或者 systemd-timesyncd 都行,但必须真的在同步。证书校验、token 有效期、日志时间线全都依赖时钟,时钟偏移会引起一堆看起来毫不相关的报错。
- 防火墙要么关掉,要么精确放行端口。我不建议在生产环境直接关,但你必须清楚要放哪些。
2.2 网段规划:最容易埋雷的一步
这里我要展开说,因为这是我最常看到翻车的地方。一个集群涉及三个网段:节点网段(宿主机自己的 IP)、Pod 网段(容器拿到的 IP)、Service 网段(集群内虚拟 IP)。这三个网段必须互不重叠,并且 Pod 网段和 Service 网段不能与节点所在的物理网段重叠。
常见的错误是这样:公司内网用 10.0.0.0/8 划了一大片,节点在 10.20.x.x,然后 Pod 网段随手写了 10.244.0.0/16。表面上看不重叠,实际上如果路由表里有一条 10.0.0.0/8 的默认路由指向网关,那么访问 10.244.x.x 的包就会被送到网关去,而不是走本地 CNI 的虚拟网卡。结果就是 Pod 之间时通时不通,或者跨节点访问失败。这个问题的隐蔽之处在于,单节点上可能完全正常,一加节点就出问题。
我现在的习惯是,规划阶段先把所有已用网段列出来,然后从不会出现在物理网络里的私网段中挑选。Service 网段我一般用 10.96.0.0/12,这个范围比较大,够用很久;Pod 网段根据预期规模选,192.168.0.0/16 能容纳六万多个地址,中小集群绰绰有余。如果公司内网确实把 10.x 和 192.168.x 都占满了,那就得考虑 172.16.0.0/12 这个区间。
需要放行的端口我也列一下,这些是实打实会被用到的:
| 组件 | 端口/协议 | 方向 | 用途 |
|---|---|---|---|
| kube-apiserver | 6443/TCP | 所有节点到控制平面 | API 入口 |
| etcd | 2379-2380/TCP | 控制平面之间 | 集群数据同步 |
| kubelet | 10250/TCP | 控制平面到所有节点 | 执行指令、日志、exec |
| kube-controller-manager | 10257/TCP | 本机 | 健康检查与指标 |
| kube-scheduler | 10259/TCP | 本机 | 健康检查与指标 |
| NodePort 服务 | 30000-32767/TCP | 外部到所有节点 | 服务暴露 |
| VXLAN 覆盖网络 | 8472/UDP 或 4789/UDP | 节点之间 | 跨节点 Pod 通信 |
| BGP | 179/TCP | 节点之间 | 部分 CNI 的路由分发 |
| IP-in-IP | IP 协议号 4 | 节点之间 | 部分 CNI 的隧道模式 |
这张表我会直接抄进部署文档里,交给网络同事开策略。实际执行时我发现,最容易漏的是 VXLAN 的 UDP 端口和 IP-in-IP 的协议号,因为这两个不是"端口"的概念,防火墙策略里经常被忽略。
2.3 系统初始化脚本逐行拆解
初始化我会写成一个脚本,在所有节点上跑一遍。不是为了省事,是为了让每台机器的状态完全一致。脚本不长,但每一行都有理由。
#!/bin/bash set -e # 1. 关闭 swap,并持久化 swapoff -a sed -ri 's/.*swap.*/#&/' /etc/fstab # 2. 加载内核模块 cat > /etc/modules-load.d/k8s.conf <<EOF overlay br_netfilter EOF modprobe overlay modprobe br_netfilter # 3. 内核参数 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 net.ipv4.conf.all.forwarding = 1 EOF sysctl --system # 4. 主机名与解析 hostnamectl set-hostname "$1"逐行说。swapoff配合sed注释 fstab,这两步必须一起做,只做前者重启失效。overlay模块是 containerd 用 overlayfs 做存储驱动时必须的,br_netfilter模块决定了网桥上的流量会不会经过 iptables 规则——这一条极其关键,kube-proxy 的 Service 转发完全依赖 iptables 规则,如果网桥流量绕过了 iptables,Service 就彻底不通,症状是 Pod 之间能通、通过 Service 名访问不通,很多人会误以为是 CoreDNS 的问题。
net.ipv4.ip_forward是转发开关,不开的话节点就没法做路由转发,跨节点 Pod 通信直接断掉。net.bridge.bridge-nf-call-iptables是让二层网桥的流量也走三层 iptables 规则,这两条是配套的。
主机名那一步,我建议命名带规律,比如k8s-master-01、k8s-node-01这种。好处是kubectl get nodes输出一眼能看出角色,出问题的时候不用再去查哪台是控制平面。同时/etc/hosts里把每台机器的 IP 和主机名都写上,虽然集群内部用的是 kubelet 上报的地址,但很多排查脚本、监控采集会依赖主机名解析。
注意:如果你的环境用了 SELinux,kubelet 访问容器运行时 socket 和挂载卷都可能被拦住。要么设为 permissive,要么按官方文档打策略。我倾向于在集群节点上设 permissive,因为生产环境里 TLS 证书和挂载路径的组合太多,逐条放行成本高且容易遗漏。
跑完脚本我会做一次验证:sysctl net.ipv4.ip_forward看是不是 1,lsmod | grep br_netfilter看模块加载了没,free -h确认 swap 是 0。这三条验证做完,基础层才算干净。
3. 容器运行时:containerd 装完才是真正的开始
容器运行时的安装,网上教程多到看不过来,但真正让人栽跟头的是配置。装包三分钟,配置错一个参数能折腾一下午。目前主流的运行时是 containerd,从 Kubernetes 不再内置 dockershim 之后,直接对接 CRI 就是标准做法。我这里按 containerd 讲,因为它配置足够简洁、日志清晰、社区资料多。
3.1 为什么不再折腾 dockershim
历史原因我就不长篇大论了,简单说结论:dockershim 是一层为了兼容 Docker 而存在的桥接代码,维护成本高,且它提供的功能 CRI 原生运行时都能提供。移除之后,kubelet 直接通过 CRI 接口和 containerd 通信,调用链更短,排查路径更清晰。
实际影响有几个:一是docker ps看不到 Kubernetes 的容器了,得用crictl ps;二是镜像的命名规则变了,以前用docker images看到的名字,现在要用完整仓库地址;三是日志查看方式变了,crictl logs或者直接kubectl logs。这几个变化对习惯了 Docker 命令的人有点不适应,但crictl用两天就熟了,而且它的输出格式对 Kubernetes 场景更友好。
我通常会顺手配一个 alias,把crictl的默认端点指到 containerd 的 socket,避免每次敲一长串参数:
cat > /etc/crictl.yaml <<EOF runtime-endpoint: unix:///run/containerd/containerd.sock image-endpoint: unix:///run/containerd/containerd.sock timeout: 10 debug: false EOF配置完crictl ps就能直接用了。这个文件很小,但省下的敲键盘时间很可观。
3.2 config.toml 里必须改的三处
containerd 的主配置在/etc/containerd/config.toml。很多发行版包里自带一个默认配置,但那个配置基本不能直接用,必须改。我不建议手写整个文件,先让它自己生成一份,然后在上面改。
mkdir -p /etc/containerd containerd config default > /etc/containerd/config.toml生成之后,有三处必须动。
第一处是SystemdCgroup。默认值是false,必须改成true。原因是绝大多数现代发行版的 init 系统是 systemd,它用 cgroup 来管理进程和资源,而 kubelet 默认的 cgroup 驱动也是 systemd。如果 containerd 用 cgroupfs 驱动,而 kubelet 用 systemd,两边对 cgroup 的组织方式理解不一致,症状是 kubelet 反复重启,日志里出现类似failed to create pod sandbox或者cgroup driver mismatch的报错。这个坑我踩过不止一次,而且它有个特点——某些场景下集群能起来,跑一段时间后某个 Pod 突然起不来,很难跟配置关联上。
改法是在[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]下面把SystemdCgroup设为true。这里要注意缩进和段落层级,TOML 格式对层级敏感,写错位置就不生效,而且 containerd 启动时不会报错,你会以为改好了。
第二处是sandbox_image,也就是 pause 镜像的地址和版本。这个必须和 kubeadm 期望的一致。如果你的环境访问不了公共镜像仓库,这里要改成私有仓库地址。版本不一致的典型症状是 Pod 卡在ContainerCreating,describe出来提示 sandbox 镜像拉取失败。
第三处是镜像仓库的 mirror 配置。内网环境必须配,否则拉镜像全部超时。配置块在[plugins."io.containerd.grpc.v1.cri".registry.mirrors]下面,按仓库地址分组,每组指定endpoint。这块配置的语法在不同 containerd 版本间有变化,较新版本用config_path指向一个目录,目录里每个仓库一个配置文件。改之前先确认版本,别照着旧教程写。
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options] SystemdCgroup = true [plugins."io.containerd.grpc.v1.cri"] sandbox_image = "registry.internal.local/pause:3.9"改完重启服务:
systemctl daemon-reload systemctl enable --now containerd systemctl status containerd --no-pager重启之后别急着往下走,先验证 containerd 真的起来了且配置生效。systemctl status显示 active 只是第一步,有些配置错误不会导致启动失败,只在真正调用时才暴露。
3.3 验证运行时是否真的就绪
这一步很多人跳过,然后带着一个半残的运行时去做 init,失败了再回来找原因。我的验证清单有三项。
第一项,看 containerd 的 CRI 插件是否启用。containerd config dump会输出合并后的完整配置,搜enable_selinux或者直接看[plugins."io.containerd.grpc.v1.cri"]段是否存在。更直接的办法是看 socket 文件在不在:ls -l /run/containerd/containerd.sock。文件不存在说明 containerd 根本没正常启动。
第二项,用crictl拉一个测试镜像试试。能拉下来说明镜像仓库配置是对的,网络也是通的。这一步在离线环境里尤其重要,因为镜像仓库的认证配置出错时,报错信息经常是笼统的 unauthorized,看不出是账号问题还是证书问题。
第三项,也是最容易被忽略的,确认SystemdCgroup真的生效了。containerd config dump | grep -i systemdcgroup看输出是不是 true。有些版本里,如果配置写在了错误的段落,dump 出来的值还是默认的 false,但服务照样启动。这种情况你以为配好了,实际没有。
实操心得:我习惯在 init 之前先用
crictl runp跑一个 pause 容器,确认整个 sandbox 创建链路是通的。这一步能提前暴露 90% 的运行时配置问题,比 init 失败之后再回头查省事得多。
4. kubeadm init 全流程实录
准备工作做完,终于到正题了。我不建议直接用一长串命令行参数跑 init,原因很简单——参数记不住、没法版本化、重装的时候要重新翻历史记录。正确做法是把配置沉淀成一个 YAML 文件,用--config传进去。这个文件既是部署配置,也是文档,还能进版本库。
4.1 把命令行参数沉淀成配置文件
配置文件的内容我按实际用到的写一遍,每一项都说明理由。
apiVersion: kubeadm.k8s.io/v1beta3 kind: InitConfiguration localAPIEndpoint: advertiseAddress: 192.168.10.11 bindPort: 6443 nodeRegistration: name: k8s-master-01 criSocket: unix:///run/containerd/containerd.sock imagePullPolicy: IfNotPresent taints: - effect: NoSchedule key: node-role.kubernetes.io/control-plane --- apiVersion: kubeadm.k8s.io/v1beta3 kind: ClusterConfiguration kubernetesVersion: v1.30.4 clusterName: internal-cluster controlPlaneEndpoint: "192.168.10.100:6443" networking: serviceSubnet: 10.96.0.0/12 podSubnet: 192.168.0.0/16 dnsDomain: cluster.local etcd: local: dataDir: /var/lib/etcd apiServer: certSANs: - 192.168.10.11 - 192.168.10.100 - k8s-master-01 - api.internal.local extraArgs: audit-log-maxage: "7" audit-log-maxbackup: "5" imageRepository: registry.internal.local --- apiVersion: kubelet.config.k8s.io/v1beta1 kind: KubeletConfiguration cgroupDriver: systemdadvertiseAddress填本机 IP,这是其他组件连 apiserver 用的地址,填错了集群内部的组件会互相连不上。criSocket指向 containerd 的 socket,路径不对的话 kubelet 找不到运行时,节点永远 NotReady。
controlPlaneEndpoint这一项,单主集群可以填本机 IP,但我强烈建议即使单主也填一个后续会用于负载均衡的域名或者虚拟 IP。原因是这一项会被写进 ConfigMap,节点加入集群时从这个 ConfigMap 读取 apiserver 地址。如果你先填了本机 IP,后来想做高可用,改起来很麻烦,得手动改 ConfigMap 再重启所有控制平面组件。填个预留的地址,哪怕现在还没做负载均衡,后面升级路径会平滑很多。
certSANs是我见过最多人遗漏的配置。apiserver 的证书里会包含一组 SAN(Subject Alternative Name),客户端用某个地址访问 apiserver 时,这个地址必须出现在 SAN 列表里,否则 TLS 校验失败,报错是x509: certificate is valid for ..., not for ...。如果你通过域名、VIP、或者另一个网卡的 IP 访问,这些地址都要加进去。漏了的话,集群本身能跑,但你的 kubectl 从别的机器连不上,或者某些需要访问 apiserver 的组件起不来。
imageRepository指向私有仓库。如果是离线环境,这个必须配,同时要确保仓库路径下的镜像名称和 kubeadm 期望的完全一致。这里有个细节:配置了imageRepository之后,kubeadm 会把期望的镜像名拼接到这个前缀后面,所以私有仓库的目录结构要和上游一致。
cgroupDriver: systemd这一项要和 containerd 的配置对应。两边都写 systemd,一致了才不会出问题。
4.2 init 输出逐段解读
配置文件就绪,跑命令:
kubeadm init --config kubeadm.yaml --upload-certs --v=5--upload-certs会把控制平面的证书加密后上传到集群的 Secret 里,后续加控制平面节点时用--certificate-key解密,不用手动拷贝证书文件。这个参数在做多主的时候必不可少。
--v=5是日志级别,第一次部署我建议加上,输出详细一点,出问题的时候有据可查。稳定之后可以去掉。
init 的输出大致分几段,每段的含义值得说清楚。开头是[preflight]系列,它在做一系列检查:端口是否被占用、swap 是否关闭、容器运行时是否可用、是否有其他 kubeadm 集群残留、必要的内核参数是否开启。这一步失败的话,报错信息一般比较明确,比如提示 swap 还开着、提示某个端口被占用。我遇到过最迷惑的一次是提示[ERROR Port-10250]: Port 10250 is in use,查了半天发现是之前装的一个监控 agent 占了端口。
接着是[certs]段,生成 CA 和各类证书,全部落在/etc/kubernetes/pki目录。这里面有 apiserver 的证书、etcd 的证书、service account 的密钥对、前端代理的证书。这些文件很重要,尤其是 CA,建议 init 完之后立刻备份整个 pki 目录。CA 丢了,集群就没法继续签发新证书,节点加入、证书轮换全部做不了,只能重装。
然后是[kubeconfig]段,生成三份 kubeconfig 放在/etc/kubernetes下:admin.conf、controller-manager.conf、scheduler.conf。admin 这份是给管理员用的,权限最高。
[kubelet-start]段会写 kubelet 的配置和环境变量文件,然后启动 kubelet 服务。[control-plane]和[etcd]段把静态 Pod 的 manifest 写到/etc/kubernetes/manifests,kubelet 监视这个目录,自动拉起这些容器。所以你会看到 init 命令还在跑,但crictl ps已经能看到 apiserver 容器在启动了。
[bootstraptoken]段生成一个 token,这个是给后续节点加入用的。最后输出的 join 命令,很多人会直接丢掉,然后加节点的时候到处找。这个命令一定要复制保存,或者用命令重新生成:
kubeadm token create --print-join-command这个命令任何时候都能跑,前提是集群里的 token 机制正常。我一般会在第一次 init 之后,把 join 命令写进部署文档,同时把 token 的有效期理解清楚——默认 24 小时,过期后旧命令失效,得重新生成。
4.3 kubeconfig 与后续操作的衔接
init 成功之后,最后会提示三行命令,让你把 admin.conf 拷到用户目录下。这三行必须执行,否则kubectl不认识集群。
mkdir -p $HOME/.kube cp -i /etc/kubernetes/admin.conf $HOME/.kube/config chown $(id -u):$(id -g) $HOME/.kube/config拷完之后验证:
kubectl cluster-info kubectl get nodes kubectl get pods -n kube-system这三条命令的输出能说明很多问题。cluster-info显示 apiserver 和 CoreDNS 的地址,能通说明 apiserver 在正常工作。get nodes此时应该显示控制平面节点,状态是 NotReady——这是正常的,因为还没装 CNI。get pods -n kube-system应该能看到 etcd、apiserver、controller-manager、scheduler、kube-proxy、CoreDNS 这些。如果 CoreDNS 是 Pending,也是正常的,等 CNI 装完自动会好。
有几次我遇到get nodes报The connection to the server localhost:8080 was refused,这个报错的意思是 kubectl 没找到 kubeconfig,退回到默认的 localhost:8080 去连。原因通常是KUBECONFIG环境变量没设、或者拷贝的路径不对、或者用的是 sudo 导致 HOME 变了。排查很简单,kubectl config view看当前用的是哪份配置,echo $KUBECONFIG看环境变量。
5. 网络插件落地与集群体检
CNI 插件这块,选型的讨论能写一整篇,我只说判断维度和我的实际选择逻辑。装完之后必须做连通性验证,这一步跳过的话,后面部署业务的时候会遇到各种奇怪的网络问题,而且因为集群看起来是"好的",排查方向完全不清晰。
5.1 CNI 选型的三个判断维度
第一个维度是功能需求。你需不需要 NetworkPolicy?如果需要,就得选支持策略的插件。很多轻量级插件只做连通性,不做策略隔离,集群里所有 Pod 互相都能访问,这在多租户场景下是不可接受的。
第二个维度是网络模式。Overlay 模式(VXLAN 或者 IP-in-IP)把 Pod 的包封装在宿主机网络的包里传输,好处是对底层网络没有要求,跨子网也能用;坏处是有封装开销,MTU 要相应调小,性能略低于路由模式。路由模式(BGP)直接把 Pod 网段的路由通告给底层网络,效率高但要求网络设备配合,跨子网部署的复杂度上升不少。
第三个维度是运维成本。有些插件功能强大,但组件多、配置复杂、升级路径长,出了问题排查起来需要深度理解其内部机制。有些插件极其简单,就一个 DaemonSet,配置几行就完事,但功能也相对单一。
我自己的取舍是这样:中小规模、跨子网、需要策略、不想让网络团队介入太深的场景,我会选支持 BGP 和 VXLAN 双模式且有完整策略能力的方案,默认用 VXLAN 起步,性能有要求的时候切 BGP。规模很小、只要连通、不要策略的测试环境,用最简单的 overlay 插件就够了,配置一行一个参数,十分钟搞定。
无论选哪个,有一条铁律:CNI 配置里的 Pod 网段必须和 kubeadm 配置文件里的podSubnet完全一致。不一致的典型症状是节点之间 Pod 互相不通,或者 Pod 拿不到 IP。
5.2 安装后必须做的连通性验证
装 CNI 通常就是 apply 一个 YAML,但装完必须验证。验证的顺序我固定为四步。
第一步,看 CNI 的 Pod 是否全部 Running。kubectl get pods -n kube-system过滤出网络组件,全部应该是 Running 且 READY 列全绿。如果有 CrashLoopBackOff,先看日志:kubectl logs -n kube-system <pod> --previous。最常见的失败原因是网卡自动探测选错了,节点上有多块网卡的时候,插件可能选到了管理网卡或者存储网卡。解决办法是在配置里显式指定网卡名,或者指定nodeAddressAutodetectionV4的匹配规则。
第二步,看节点状态是否变成 Ready。CNI 装好之后,kubelet 会通过 CNI 给节点配置网络,节点状态在一两分钟内转为 Ready。如果一直是 NotReady,kubectl describe node看 Conditions 里的 NetworkUnavailable 原因,同时去节点上看 kubelet 日志和 CNI 配置目录/etc/cni/net.d。
第三步,验证 Pod 之间能通。起两个测试 Pod,一个在节点 A,一个在节点 B,互相 ping。这一步会暴露跨节点网络的问题。
kubectl run test-a --image=busybox --restart=Never --overrides='{"spec":{"nodeName":"k8s-node-01"}}' -- sleep 3600 kubectl run test-b --image=busybox --restart=Never --overrides='{"spec":{"nodeName":"k8s-node-02"}}' -- sleep 3600 kubectl get pod test-b -o jsonpath='{.status.podIP}' kubectl exec test-a -- ping -c 3 <test-b的IP>第四步,验证 Service 访问。这一步验证的是 kube-proxy 的转发规则。起一个 Deployment 挂到 Service 上,然后用集群内的 Pod 去访问 Service 的 ClusterIP 和 Service 名(后者验证 CoreDNS)。Service 名能解析通,说明 DNS 和 Service 两条链路都是好的。
注意:验证 Service 访问时,如果你访问的是没有 Endpoint 的 Service,或者访问方式是从宿主机直接 curl ClusterIP,都会失败。ClusterIP 只在集群内部有意义,宿主机上没有对应的路由。这个误解非常常见,很多人用它来判断"Service 不通",实际是测试方法本身错了。
5.3 一份可以存下来的体检清单
集群装完,我会跑一遍这份清单,全部通过才算交付。这份清单我用了很久,覆盖了从控制平面到网络到 DNS 的主要链路。
| 检查项 | 命令 | 期望结果 |
|---|---|---|
| 节点状态 | kubectl get nodes -o wide | 全部 Ready,版本一致 |
| 系统组件 | kubectl get pods -n kube-system | 全部 Running,无反复重启 |
| 组件版本 | kubectl version | 客户端与服务端版本接近 |
| 控制平面健康 | kubectl get componentstatuses | 各组件 healthy |
| DNS 服务 | kubectl get svc -n kube-system kube-dns | 有 ClusterIP,Endpoints 非空 |
| 证书有效期 | kubeadm certs check-expiration | 剩余时间充足 |
| 集群信息 | kubectl cluster-info | 正常返回地址 |
| 资源占用 | kubectl top nodes | 能正常返回(需 metrics 组件) |
| 跨节点网络 | 两个节点上的测试 Pod 互相 ping | 通 |
| 服务发现 | 集群内 Pod 访问 Service 名 | 能解析并访问 |
其中componentstatuses在新版本里已经废弃,返回的内容可能不全,这一点要知道。真正可靠的判断还是看kube-system下 Pod 的实际状态和事件。kubectl get events -n kube-system --sort-by=.metadata.creationTimestamp能看到最近发生了什么,比看状态更有价值。
6. 节点扩容与多主高可用
单节点集群只能算搭起来了,真正进入可用状态,得加节点。控制平面如果只有一个,那它就是个单点,宕机就是集群不可用。所以扩容这件事分两条线:工作节点扩容和多控制平面搭建。
6.1 join 的正确姿势与 token 生命周期
工作节点加入集群,前提是该节点已经完成了第 2 章的初始化、装好了同版本的 kubeadm 和 kubelet、装好并配置了相同的容器运行时。这些条件不满足就去 join,会在 preflight 阶段失败。
join 命令长这样:
kubeadm join 192.168.10.100:6443 --token abcdef.0123456789abcdef \ --discovery-token-ca-cert-hash sha256:xxxx这个命令里的两部分都值得说。--token是引导令牌,有效期默认 24 小时,用一次不会失效,过期了就作废。--discovery-token-ca-cert-hash是 CA 证书的指纹,作用是在不预先分发 CA 证书的情况下,让新节点验证它连上的 apiserver 是真的。这个机制设计得挺巧妙——token 明文传输也不怕被截获,因为攻击者没有对应的 CA 私钥,伪造不了 apiserver。
如果 token 过期了,重新生成:
kubeadm token create --print-join-command这条命令会生成新 token 并拼出完整的 join 命令,直接复制用。我习惯在加节点之前先跑一次,确保拿到的是新鲜的。
join 之后,节点状态先是 NotReady,等 CNI 组件在这个节点上拉起并配置好网络,才转 Ready。这个等待时间取决于镜像拉取速度,离线环境一般一两分钟。
有一个坑要说:如果新节点的主机名和已有节点重复,join 会失败或者出现奇怪的状态。kubelet 用主机名作为节点标识,重名会导致节点信息覆盖。我在虚拟机模板批量克隆的场景里遇到过,克隆出来的机器主机名完全一样,join 之后集群里只看到一个节点,另一个消失了。解决办法是 join 之前先改主机名。
还有一点,控制平面节点的污点。从某个版本开始,控制平面节点默认带node-role.kubernetes.io/control-plane:NoSchedule污点,普通业务 Pod 不会被调度上去。单节点测试环境如果想让它跑业务,需要去掉这个污点:
kubectl taint nodes k8s-master-01 node-role.kubernetes.io/control-plane:NoSchedule-注意结尾那个减号,是删除污点的语法。生产环境不要去掉。
6.2 控制平面高可用搭建要点
多控制平面这件事,核心难点不在 kubeadm,在它前面的负载均衡。apiserver 是多个实例,客户端怎么知道连哪个?答案是前面放一个负载均衡器,所有客户端连这个入口,由它转发到健康的 apiserver。
入口可以是一个外部负载均衡设备,也可以是集群内自己搭的。自建方案常见的组合是虚拟 IP 加四层代理:用 keepalived 维护一个 VIP 在多台机器间漂移,用四层代理监听 6443 端口,把流量转发到各个 apiserver。这套组合的配置不算复杂,但要注意几个点。
第一,VIP 必须在上文提到的controlPlaneEndpoint里配置过。前面强调过这一点,因为所有节点的 kubelet 都从这个地址读 apiserver 的入口,改起来很麻烦。
第二,健康检查要针对/healthz或者/livez,而不是简单探测端口通不通。apiserver 进程在但已经不健康的情况是存在的,只探端口会误导流量继续打过去。
第三,certSANs里要加上 VIP 和负载均衡的域名。这一条漏了,客户端连 VIP 的时候 TLS 校验会失败。
第四,节点的初始化步骤里,第一台机器用 init,后续控制平面节点用 join 加--control-plane,并且要带上--certificate-key:
kubeadm join 192.168.10.100:6443 --token <token> \ --discovery-token-ca-cert-hash sha256:<hash> \ --control-plane --certificate-key <key>certificate-key在 init 时用--upload-certs参数会自动上传,有效期两小时。过期了要重新生成:
kubeadm init phase upload-certs --upload-certs这条命令会输出一个新的 key。这个两小时的有效期是个常见的坑,很多人准备第二台机器的时候发现 join 报证书解密失败,就是因为 key 过期了。
etcd 在多控制平面场景下会自动组成集群,三个节点是推荐的起步数量,能容忍一个节点故障。两个节点是没意义的,因为 etcd 的多数派机制在偶数节点下并不能提升容错能力,反而增加了一致性开销。
6.3 证书到期这件事要提前排期
kubeadm 生成的证书默认有效期一年。一年之后,apiserver 的证书过期,整个集群的 API 访问全部失败,症状是 kubectl 报x509: certificate has expired or is not yet valid。这个问题在测试环境很常见,因为装完就没人管了,突然某天要用,发现完全连不上。
查证书状态:
kubeadm certs check-expiration输出会列出每一张证书的过期时间、剩余天数、是否可续期。我建议把这个命令放进监控或者月度检查清单里。
续期的命令:
kubeadm certs renew all续期之后必须重启控制平面的静态 Pod 才会生效。做法是把/etc/kubernetes/manifests下的 manifest 临时挪走再放回,kubelet 会重建容器。更简单粗暴的办法是重启 kubelet 服务。
systemctl restart kubelet还有一个容易忽略的点:kubelet 自己的客户端证书也会过期,而且它的续期机制和控制平面证书不一样。kubelet 的证书轮换默认开启,但需要在 kubelet 配置里确认rotateCertificates: true和serverTLSBootstrap: true。如果这两个没开,kubelet 证书到期后节点会变成 NotReady,而且kubeadm certs renew管不到它。
还有 ServiceAccount 的密钥、etcd 的证书,这些也都有一年的有效期。所以最稳妥的做法不是等到期了再续,而是在部署的时候就把"证书检查"写进运维手册,每季度看一眼。我见过最惨的情况是集群跑了十一个月,某天业务突然大面积不可用,排查两小时才发现是证书过期,而如果提前一周检查,五分钟就能解决。
7. 故障排查实录与速查表
前面几章是按顺利路径讲的,实际施工时不可能这么顺。这一章我把遇到过的、听说过的典型问题整理成速查表,再挑几个印象最深的展开说。排查这件事,说到底就是明确"问题在哪一层",然后去那一层看日志。kubeadm 的排查路径其实很清晰:kubelet 日志 → 容器运行时日志 → 控制平面组件日志 → CNI 组件日志,按这个顺序走,绝大多数问题定位不超过半小时。
7.1 首装阶段高频故障速查
| 现象 | 最可能的原因 | 排查动作 |
|---|---|---|
| preflight 报 swap 未关闭 | 只执行了 swapoff 没改 fstab | 检查/etc/fstab与free -h |
| 报端口被占用 | 旧集群残留或第三方 agent 占端口 | ss -lntp查 6443、10250 |
| 提示运行时连接失败 | socket 路径错或 containerd 未启动 | 确认/run/containerd/containerd.sock存在 |
| 卡在拉取镜像 | 私有仓库配置错或网络不通 | 用crictl pull单独测 |
| Pod 卡在 ContainerCreating | sandbox 镜像版本不匹配 | 对照 kubeadm 期望版本 |
| 节点一直 NotReady | CNI 未装或 cgroup 驱动不一致 | 看 kubelet 日志与/etc/cni/net.d |
| CoreDNS 一直 Pending | 没有可用 CNI,Pod 无 IP | 先装 CNI |
| Service 名无法解析 | CoreDNS 未就绪或 br_netfilter 未开 | 检查 DNS Pod 与内核参数 |
| kubectl 报 8080 连接拒绝 | kubeconfig 未配置 | 确认$HOME/.kube/config |
| 报 x509 证书错误 | 时钟不同步或 SAN 缺失 | 校时,检查 certSANs |
| 网络组件 CrashLoop | 多网卡导致自动探测选错 | 显式指定网卡名 |
| join 失败提示 token 无效 | token 超过 24 小时 | 重新生成 join 命令 |
| 加控制平面失败 | certificate-key 过期 | 重新 upload-certs |
这张表我打印出来贴在工位上过,实际用下来能覆盖八成以上的首装问题。剩下两成要么是环境特别特殊,要么是多个问题叠加,得逐个击破。
7.2 三个印象最深的踩坑复盘
第一个坑是 cgroup 驱动不一致。症状很有迷惑性:集群装完当天一切正常,第二天开始某个节点上的新 Pod 起不来,已有的 Pod 跑得好好的。describe出来提示failed to create containerd task,往上看有cgroup相关的报错。查了两个小时,最后containerd config dump发现SystemdCgroup是 false——配置文件改了,但改在了错误的段落。containerd 对 TOML 的结构很严格,写错层级就是静默不生效。这件事之后我养成了一个习惯:任何配置改完,都用 dump 或者 grep 验证一遍实际生效值,不看文件内容,看运行时读到的值。
第二个坑是 Pod 网段与内网冲突。这个集群装完看起来非常健康:节点 Ready,组件全绿,DNS 正常,测试 Pod 能互相 ping。但业务上线之后,有些服务调用超时,有些正常,规律很难总结。折腾很久才发现,Pod 网段和公司内网某个网段重叠了,访问那些地址的请求被 CNI 拦截,走了本地转发,而不是发到真实的服务器。这种问题的可怕之处在于它是"部分失败",没有明确报错,只有业务日志里的超时。解决办法没有捷径,就是重装集群换网段。所以我在第 2 章反复强调规划,这活儿是省不掉的。
第三个坑是证书过期。这个前面提过,但我再说一个细节:kubeadm certs renew all执行成功,输出全是 renewed,但集群还是不行。原因是静态 Pod 没有重启,apiserver 加载的还是旧证书。必须重启 kubelet 或者挪 manifest 触发重建。而且如果集群是多主,每个控制平面节点都要单独执行一次续期和重启,因为证书文件是各自本地的,不会自动同步。这一点文档里写得不显眼,我第一次做的时候只在一台上执行,结果负载均衡轮询到另一台就报证书错误。
实操心得:证书续期这件事,我现在的做法是把它做成一个带日期的检查项写进运维记录,每个季度第一周执行一次
check-expiration,剩余天数低于 90 就安排续期窗口。不做成"记得就查",因为没人会记得。
7.3 我固定的排查顺序
最后一节说排查方法论,这部分比具体命令更有价值。遇到问题我按固定顺序走,不跳步。
第一步,确定是哪个层面出问题。kubectl get nodes能不能返回?不能,说明 apiserver 或网络有问题,往下走客户端到控制平面的链路。能返回但节点 NotReady,说明是节点侧问题,去节点上看 kubelet。能返回节点也 Ready,但 Pod 有问题,那基本是调度、镜像、网络、存储其中之一。
第二步,看事件。kubectl describe和kubectl get events的信息量很大。Pod 的事件会告诉你调度失败的原因、镜像拉取的状态、挂载卷的错误、容器创建的过程。很多人习惯直接看日志,但日志是容器起来之后才有的,容器起不来的时候只有事件能看。
第三步,去节点上看 kubelet 日志。journalctl -u kubelet -n 200 --no-pager。kubelet 是所有容器操作的执行者,Pod 起不来最终都会在它的日志里留下痕迹。看的时候注意时间戳,从报错往前找第一个异常,而不是从后往前读。
第四步,看容器运行时。crictl ps -a看容器状态,crictl logs看容器日志,crictl inspect看详细配置。如果 kubelet 日志里提示容器创建失败,问题一定在这一层。
第五步,如果涉及网络,去节点上看 CNI 配置和路由表。ls /etc/cni/net.d、ip route、ip link。网络问题最终都要落在这几个命令上。
这套顺序的价值在于它不依赖猜测。每往下走一步,都是基于上一步的输出判断,而不是"我觉得可能是 XX 问题"。排查最怕的就是带着预设答案去找证据,找到的往往是自己想看到的,而不是真实的。
如果集群彻底装坏了,重来的成本其实不高。清理流程是这样:
kubeadm reset -f rm -rf /etc/cni/net.d rm -rf $HOME/.kube iptables -F && iptables -t nat -F && iptables -t mangle -F && iptables -X ipvsadm --clear rm -rf /var/lib/etcdkubeadm reset会停掉 kubelet、清理静态 Pod、删除大部分配置,但它不清理 CNI 的配置目录、不清理 iptables 规则、不清理 etcd 的数据目录。这几样如果不手动清,下一次 init 可能因为残留状态而失败,或者更糟——看起来成功了但行为异常。所以 reset 之后那几条命令必须补上。
这套流程走熟之后,从零到一套可用的三节点集群,我的记录是四十分钟左右,其中大半时间在等镜像。真正敲命令的时间不到十分钟。剩下的时间都花在验证上,而验证是值得的,因为一个装完就"看起来没问题"的集群,和一个是真正可以承载业务的集群,中间差的就是那十几项检查。