
经常有人问我“想自己搭一套 Kubernetes 集群到底从哪下手”我一般都会回复一句话先把 kubeadm 用熟再谈别的。kubeadm 是 Kubernetes 官方维护的集群引导工具它的目标就是让你别在节点初始化、证书签发、控制面组件部署这些琐碎事情上反复折腾。这篇文章就围绕“kubeadm 安装 k8s集群”这条主线从环境准备、组件安装、控制面初始化、工作节点加入、网络插件部署一直到常见故障排查完整走一遍。适合有一定 Linux 基础、想亲手搭建一套可用集群的读者也适合那些已经被各种安装教程绕晕、想搞清楚每一步到底在干什么的人。1. 为什么要用 kubeadm 装集群1.1 自建集群的几种主流方案很多人第一次搭集群脑子里冒出来的方案可能有这几种二进制手动部署、kubeadm、minikube、kind、k3s。它们不能说谁绝对好只能说适用场景完全不同。方案上手难度适合场景说明二进制手动部署很高生产环境、高度定制化需求所有组件都要手动下载、编排、生成证书链路长且容易出错kubeadm中等生产环境、标准化部署、学习 K8s 内部机制官方工具组件以静态 Pod 方式托管升级维护相对简单minikube / kind低本地开发、快速验证单节点为主和真实多节点集群的行为有差异k3s低边缘计算、资源受限环境架构做了裁剪很多组件被合并和有完整功能的集群不完全一致我的建议是如果你想在生产环境或者测试环境里用一套“正经的”多节点集群又不想从零开始写 systemd 单元文件去手动拉起 kube-apiserver那就老老实实走 kubeadm。它把 K8s 决定要做好的事情——证书、token、kubeconfig、控制面组件、节点 bootstrap——全部自动化了而你仍然保留了对集群关键配置的掌控权。1.2 kubeadm 的设计思路kubeadm 的核心思路用一个词概括就是“引导”。它不会替你管理集群的日常运维但它负责在你执行kubeadm init或者kubeadm join的那一刻把节点从“一个普通的主机”变成“集群的一个成员”。它具体做的事包括自动生成 CA 证书、apiserver 证书、kubelet 证书并把证书分发到对应位置把 kube-apiserver、kube-controller-manager、kube-scheduler 以静态 Pod 的形式写到/etc/kubernetes/manifests目录下由 kubelet 负责拉起为控制面节点生成 kubeconfig 文件方便你使用kubectl管理集群生成用于工作节点加入的 token 和 discovery-token-ca-cert-hash。理解了这个设计思路你后面遇到很多问题都不会慌。比如控制面组件起不来你第一反应应该是去看kubectl get pods -n kube-system比如 kubelet 状态异常你要知道 kubelet 是集群的“底座”控制面组件是它拉起来的底座不稳上面全崩。2. 环境准备这一步做不好后面全是坑2.1 硬件与系统规划先说硬件。K8s 官方要求控制面节点至少 2 核 CPU、2GB 内存工作节点至少 2 核、2GB 内存。这个只是“能跑”的门槛实际用起来建议控制面节点 4 核 4GB 起步工作节点看负载情况再定。我这里演示的是一套三个节点的集群一台控制面节点两台工作节点。如果你只有一台机器也可以把控制面和工作节点复用但只适合学习和功能验证别拿单节点去扛生产流量。操作系统方面Debian、Ubuntu、CentOS 等主流发行版都能装但要注意内核版本别太老官方比较推荐内核 4.15 以上。我习惯用 Ubuntu 22.04 LTS 系列做演示包管理走 apt命令逻辑也清晰。无论用哪个发行版请记住一个原则集群内所有节点的主机名必须唯一且不要带下划线IP 能互通、hostname 能解析是硬前提。2.2 初始化系统的四项硬性操作在敲 kubeadm 命令之前有几项系统层面的准备必须先做。很多新手集群装不上九成问题出在这一步。第一配置主机名和 hosts 解析。建议把控制面节点主机名设为master01工作节点设为node01、node02然后在每台机器的/etc/hosts里把三台机器的 IP 和主机名对应写上。# 每个节点分别执行替换为自己的主机名 hostnamectl set-hostname master01# /etc/hosts 追加内容IP 替换为实际地址 192.168.1.10 master01 192.168.1.11 node01 192.168.1.12 node02第二时间同步。K8s 集群里的证书校验、日志时间戳、调度逻辑都依赖节点间时间一致偏差大了会出现各种诡异问题。用 chrony 或者 systemd-timesyncd 都行确保状态为 synchronized。第三加载内核模块并设置 sysctl 参数。K8s 网络通信依赖 iptables 和 IP 转发需要提前把模块挂上。cat EOF | sudo tee /etc/modules-load.d/k8s.conf overlay br_netfilter EOF sudo modprobe overlay sudo modprobe br_netfiltercat EOF | sudo tee /etc/sysctl.d/k8s.conf net.bridge.bridge-nf-call-iptables 1 net.bridge.bridge-nf-call-ip6tables 1 net.ipv4.ip_forward 1 EOF sudo sysctl --system解释一下这几条参数的作用。bridge-nf-call-iptables的作用是让桥接的流量也经过 iptables 规则这对 K8s 里 Service 的 DNAT 和负载均衡至关重要ip_forward则是让数据包能在不同网络命名空间之间转发Pod 之间通信和 Pod 访问外网都依赖它。第四关闭 swap。kubelet在默认情况下不允许节点启用 swap因为 K8s 的 CPU 和内存资源模型假设调度是基于节点可分配内存的而 swap 的存在会让内存压力判断失真。关闭方式如下并且要保证重启后依然生效。sudo swapoff -a # 然后注释掉 /etc/fstab 中的 swap 相关行还有一个小细节防火墙和 SELinux 的问题。不同发行版情况不同Ubuntu 默认没启用 SELinuxCentOS 则需要设置成 permissive。防火墙方面如果是在内网环境搭建我建议先把默认防火墙放行必要端口或者直接临时关闭重点是确保节点间 6443、10250、2379、30000-32767 等端口互通。生产环境再根据安全策略精细化配置学习阶段被防火墙卡住实在不值得。2.3 容器运行时的前置条件iptables 模式K8s 在 1.27 版本之后已经不再推荐使用 Docker 作为直接运行时而是通过 containerd 来承载容器。不管最终选哪个运行时底层依赖都一样iptables 或者 nftables。Ubuntu 22.04 默认使用的是 nftables 后端而 K8s 的 kube-proxy 默认用 iptables为了避免模式冲突需要显式切换。sudo update-alternatives --set iptables /usr/sbin/iptables-legacy这一步不处理后面 kube-proxy 可能起不来或者 Service 的转发规则根本不生效。虽然现在 K8s 对 nftables 的支持也在逐步完善但为了少踩坑传统方式最稳。3. 安装容器运行时和 kubeadm 工具链3.1 为什么首选 containerdK8s 从 1.24 版本起彻底移除了对 Docker 作为运行时的一部分直接支持但这不代表不能用了只是需要你额外装一个 cri-dockerd 适配层。我的建议是直接上 containerd。containerd 本身是 CNCF 的项目和 K8s 的 CRIContainer Runtime Interface是原生的语言适配关系不需要中间转换层稳定性更高资源占用也更少。而且 kubeadm 在初始化集群时拉取的 pause 等系统镜像走 containerd 是最顺的。3.2 安装 containerd 并做两处关键修改以 Ubuntu 为例安装 containerd 可以直接从 Docker 官方 apt 源获取。sudo apt-get update sudo apt-get install -y ca-certificates curl gnupg sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod ar /etc/apt/keyrings/docker.gpg echo \ deb [arch$(dpkg --print-architecture) signed-by/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ $(. /etc/os-release echo $VERSION_CODENAME) stable | \ sudo tee /etc/apt/sources.list.d/docker.list /dev/null sudo apt-get update sudo apt-get install -y containerd.io装好之后先初始化 containerd 的默认配置sudo mkdir -p /etc/containerd containerd config default | sudo tee /etc/containerd/config.toml然后必须修改两处地方否则 kubelet 和 containerd 的 cgroup 驱动不一致节点会一直显示 NotReady。第一处SystemdCgroup false要改成true。找到[plugins.io.containerd.grpc.v1.cri.containerd.runtimes.runc.options]这一段确认里面有SystemdCgroup true。这是因为现在绝大多数 Linux 发行版都用 systemd 作为 init 系统让 containerd 和 kubelet 都走 systemd cgroup 驱动两者才不会有冲突。第二处sandbox_image要设为和你的 K8s 版本匹配的 pause 镜像地址。比如[plugins.io.containerd.grpc.v1.cri] sandbox_image registry.k8s.io/pause:3.9修改完成之后重启 containerdsudo systemctl daemon-reload sudo systemctl enable --now containerd sudo systemctl status containerd3.3 安装 kubeadm、kubelet、kubectl版本匹配是底线这三个工具必须安装相同版本千万不要一个 1.27 一个 1.28 混着来。我用的是 1.28 系列作为示例。sudo apt-get update sudo apt-get install -y apt-transport-https ca-certificates curl gpg curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.28/deb/Release.key | \ sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg echo deb [signed-by/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.28/deb/ / | \ sudo tee /etc/apt/sources.list.d/kubernetes.list sudo apt-get update sudo apt-get install -y kubelet kubeadm kubectl sudo apt-mark hold kubelet kubeadm kubectlapt-mark hold这步很关键防止哪天不小心执行apt upgrade把版本给带偏了。装完之后kubelet 默认是不会自动启动的这很正常要等kubeadm init或者kubeadm join时才会拿到配置并被引导起来。用命令验证一下版本kubeadm version kubelet --version kubectl version --client到这里所有节点上的基础软件都准备好了。接下来就在控制面节点上执行集群初始化。4. 控制平面初始化与工作节点加入4.1 kubeadm init 命令和参数别乱填在控制面节点执行初始化。我用一个包含两个核心参数的示例sudo kubeadm init \ --apiserver-advertise-address192.168.1.10 \ --pod-network-cidr10.244.0.0/16 \ --kubernetes-versionv1.28.0解释一下这些参数--apiserver-advertise-address指定 API Server 对外宣告的 IP也就是工作节点要连接的地址。如果不填kubeadm 默认取节点默认路由对应的 IP。在多网卡机器上这一步必须显式指定否则集群搭起来之后工作节点可能连不上 API Server。--pod-network-cidrPod 网络的 CIDR 网段。这个参数和后面要装的网络插件强相关比如我准备用 Flannel所以填了10.244.0.0/16如果你用 Calico可能得填192.168.0.0/16。这个网段规划错了网络插件和 kube-proxy 之间会打架Pod 之间通信直接炸掉。--kubernetes-version指定 K8s 版本避免 kubeadm 从网上拉取最新版本信息时和本地的 kubelet 版本不一致。如果你的控制面节点有两个网卡或者你想为将来高可用做铺垫还可以加--control-plane-endpoint. 单节点学习阶段可以不填但建议从一开始就养成填这个参数的习惯因为它指定的是集群的统一入口后续想扩展成高可用控制面这个参数不用回改。init 过程会拉取一批镜像包括 kube-apiserver、kube-controller-manager、kube-scheduler、etcd、pause 等耗时取决于网络状况。如果网络环境一般可以先手动预拉看看到底是哪些镜像卡住了sudo kubeadm config images list sudo kubeadm config images pull我当时第一次装的时候就是先看到了registry.k8s.io下的几个镜像列表才意识到预拉这步有多省心不至于让 init 卡在一半干瞪眼。4.2 init 输出的信息一定要看懂init 成功之后终端会输出一大段提示这段信息含金量很高最容易让人忽略的就是最后面的几行Your Kubernetes control-plane has been initialized successfully! You can now join any number of worker nodes by running the following on each as root: kubeadm join 192.168.1.10:6443 --token xxxxxx \ --discovery-token-ca-cert-hash sha256:xxxxxxxxxxxxxxxx这里有两个关键信息--token和--discovery-token-ca-cert-hash。前者是工作节点加入集群的临时凭据后者是校验控制面 CA 证书的哈希两个必须同时带上join 才能成功。还有一段是关于 kubeconfig 的。root 用户或普通用户需要把生成的 kubeconfig 放到~/.kube/config下kubectl 才知道该连哪个集群。mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config这一步不做你执行kubectl get nodes的时候会报本地没有集群配置的错误。顺便可以给 kubectl 配个自动补全用起来效率高一些echo source (kubectl completion bash) ~/.bashrc source ~/.bashrc4.3 工作节点 join 进来接下来在所有工作节点上执行上面输出的 join 命令。注意这条命令必须在 root 权限下执行因为 kubelet 需要写系统级配置文件。sudo kubeadm join 192.168.1.10:6443 --token xxxxxx \ --discovery-token-ca-cert-hash sha256:xxxxxxxxxxxxxxxx如果遇到 token 过期或者一开始忘记保存这段输出也不用慌在控制面节点上重新生成一条即可sudo kubeadm token create --print-join-command这个命令会自动生成新的 token 并拼好 join 命令直接复制到工作节点跑就行。等所有节点执行完 join回到控制面节点执行kubectl get nodes正常情况下能看到类似下面的输出NAME STATUS ROLES AGE VERSION master01 Ready control-plane 6m v1.28.0 node01 Ready none 3m v1.28.0 node02 Ready none 3m v1.28.04.4 集群状态验证的正确姿势这里多说一句kubectl get nodes看到的 STATUS 是 NotReady 的话先别急着怀疑自己哪步做错了。控制面节点初始化完成之后集群内部还没有安装网络插件所有节点都会处于 NotReady 状态这是正常的。用一句大白话说机器之间能注册成功但 Pod 还不会互相通信所以集群认为自己“还没准备好”。要查 kubelet 的实际状态可以登录对应节点执行systemctl status kubelet journalctl -u kubelet -fkubelet 是一个高频写入日志的组件如果它在反复启动日志里多半会直接告诉你为什么。下一节要装的网络插件就能解决 NotReady 这个问题。5. 网络插件让 Pod 真正互通的关键一步5.1 CNI 插件到底干了件什么事K8s 的网络模型要求每个 Pod 都拥有独立的 IPPod 之间可以直接通过 IP 互通不需要做 NAT。这个要求不是 K8s 自身实现的而是通过 CNIContainer Network Interface插件完成的。CNI 插件在 kubelet 启动 Pod 的时候会被调用它的任务包括给 Pod 分配 IP、创建 veth 对、设置路由规则、把流量从 Pod 的网络命名空间导出来。不同插件的底层实现思路差异很大Flannel 走的是 VXLAN 叠加网络简单、易排查Calico 走的是纯三层 BGP 路由模式性能好、策略能力强生产环境用得最多。这里有个不能偷懒的认知Pod 网段必须和 kubeadm init 时的--pod-network-cidr保持一致否则插件分配的 IP 和控制面规划的网段对不上SDN 直接瘫痪。5.2 以 Calico 为例完成部署我习惯用 Calico因为它在性能和功能之间平衡得比较好还能顺带提供网络策略能力。部署前要先下载对应版本的清单文件。从 Calico 官方文档获取对应版本的calico.yaml然后执行kubectl apply -f calico.yaml如果你选择的 Pod 网段不是 Calico 默认的网段需要先修改清单文件里的CALICO_IPV4POOL_CIDR这个环境变量的值改成和--pod-network-cidr一致。这一步忘了改Calico 会按照默认网段分配 IP集群内部路由就乱了。等待资源创建完成可以通过下面的命令确认 Calico 组件是否正常运行kubectl get pods -n kube-system | grep calico看到 STATUS 为 Running节点状态会在几十秒内慢慢变成 Ready。如果一直没变化看 Calico 的日志排查kubectl logs -n kube-system -l k8s-appcalico-node5.3 网络验证的两种操作集群 Ready 之后建议大家做一次真实的网络验证而不是只看 Node 状态。最简单的方式是创建一个临时的 busybox Pod然后测试 Pod 之间和 Pod 访问外网的能力kubectl run test-net --imagebusybox --restartNever -- sleep 3600 kubectl exec -it test-net -- ping 8.8.8.8再测一下集群的 DNS 解析。K8s 集群里的核心 DNS 组件是 CoreDNS它部署在 kube-system 命名空间。如果 DNS 出问题Service 之间互相调用会全部失败那是很隐蔽的故障。kubectl exec -it test-net -- nslookup kubernetes.default.svc.cluster.local这一步能通说明集群内部 DNS 和 Service 网络都正常。到这一步一套多节点的 K8s 集群就已经具备了基本的可用性。6. 常见问题与排查实录6.1 kubelet 报 cgroup 驱动不一致这是新手最容易碰到的经典报错。症状是 kubelet 服务起不来journalctl -u kubelet里出现类似这样一段failed to run kubelet: failed to create kubelet ... misconfiguration: kubelet cgroup driver: systemd is different from docker cgroup driver: cgroupfs原因就是我前面强调的kubelet 和容器运行时必须是同一个 cgroup 驱动。Ubuntu 上 kubelet 默认用 systemd如果 containerd 还保持默认的 cgroupfs两边就对不上。解决办法修改/etc/containerd/config.toml把SystemdCgroup置为true然后重启 containerd。这是个高概率翻车点也是我几乎每次排查节点 NotReady 时都会确认的第一个地方。6.2 初始化时镜像拉取失败kubeadm init 输出一直卡在[cri] pulling images或者直接报failed to pull image多见于镜像仓库无法连接或网络策略受限的环境。我的处理办法分三步走。先执行kubeadm config images list把需要的镜像清单打印出来再逐一手动拉取确认到底是哪个失败接着给所有节点提前拉好镜像包或者通过内网分发最后再执行 kubeadm init。这里提醒一句不要在 join 完成之前换节点名或者改 IP否则证书和 kubelet 配置里的地址信息会全部对不上。6.3 node 一直 NotReady节点 join 成功但 STATUS 一直是 NotReady先别急着重置节点按这个顺序排查。先看 CNI 插件有没有正常跑起来kubectl get pods -n kube-system发现 flannel 或 calico 的 Pod 一直 Pending大概率是节点资源不足或者镜像没拉下来。再看节点上的 kubelet 状态systemctl status kubelet如果 kubelet 在反复重启用journalctl -u kubelet -f看实时日志里面有具体的报错原因。还有一个容易忽略的原因节点的主机名和 MAC 地址冲突。K8s 集群要求每个节点的 hostname 必须唯一如果你用虚拟机模板克隆出来的机器hostname 会重复节点注册时会互相顶掉表现就是节点状态疯狂在 Ready 和 NotReady 之间横跳。解决办法就是把 hostname 改掉。6.4 token 过期与重新生成join 报错提示 token 无效或者过期不必重新初始化集群。kubeadm 生成的 token 默认有效期是 24 小时重新生成即可sudo kubeadm token create --print-join-command如果控制面上也找不到原来的 CA 哈希可以用下面的命令重新获取openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt | openssl rsa -pubin -outform der 2/dev/null | openssl dgst -sha256 -hex | sed s/^.* //然后把 hash 拼到 join 命令里。6.5 集群重置与清理打个比方kubeadm 的好处就是“有后悔药”kubeadm reset可以把节点上的配置打回原形。sudo kubeadm reset -f sudo rm -rf /etc/kubernetes /var/lib/kubelet /var/lib/etcd还有一类残留容易被人遗忘iptables 规则和路由表。如果你之前装过 Calico节点上会有大量路由和 IPVS 规则重置之后不清干净下次重新初始化集群很可能留下一堆脏路由。清理方式sudo iptables -F sudo iptables -t nat -F sudo iptables -t mangle -F sudo ip link delete cni0重置之后再重新执行kubeadm init或者kubeadm join基本能回到一个干净的初始状态。这也是我喜欢 kubeadm 的原因之一它把集群的“可重入性”做得很好出错成本低。最后再分享一点个人习惯每次初始化集群前我习惯把所有节点的时间同步先确认一遍然后一定在控制面节点执行kubeadm config images pull预拉镜像。这两件事做完下面几乎都是一路顺畅。K8s 集群的安装本身并不神秘很多问题的根子都在于对每个组件职责不熟加上版本、驱动、网段这些细节没对齐。把这些基础打牢后面做应用的滚动发布、弹性伸缩你才有底气。