ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

kubeadm 安装 Kubernetes 集群完整指南:初始化、加入节点与排障

kubeadm 安装 Kubernetes 集群完整指南:初始化、加入节点与排障

经常有人问我:“想自己搭一套 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_netfilter
cat <<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 默认没启用 SELinux,CentOS 则需要设置成 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 为什么首选 containerd

K8s 从 1.24 版本起彻底移除了对 Docker 作为运行时的一部分直接支持,但这不代表不能用了,只是需要你额外装一个 cri-dockerd 适配层。我的建议是:直接上 containerd。

containerd 本身是 CNCF 的项目,和 K8s 的 CRI(Container 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 a+r /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"

修改完成之后重启 containerd:

sudo systemctl daemon-reload sudo systemctl enable --now containerd sudo systemctl status containerd

3.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 kubectl

apt-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-address=192.168.1.10 \ --pod-network-cidr=10.244.0.0/16 \ --kubernetes-version=v1.28.0

解释一下这些参数:

  • --apiserver-advertise-address:指定 API Server 对外宣告的 IP,也就是工作节点要连接的地址。如果不填,kubeadm 默认取节点默认路由对应的 IP。在多网卡机器上,这一步必须显式指定,否则集群搭起来之后,工作节点可能连不上 API Server。
  • --pod-network-cidr:Pod 网络的 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 ~/.bashrc

4.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.0

4.4 集群状态验证的正确姿势

这里多说一句:kubectl get nodes看到的 STATUS 是 NotReady 的话,先别急着怀疑自己哪步做错了。控制面节点初始化完成之后,集群内部还没有安装网络插件,所有节点都会处于 NotReady 状态,这是正常的。用一句大白话说:机器之间能注册成功,但 Pod 还不会互相通信,所以集群认为自己“还没准备好”。

要查 kubelet 的实际状态,可以登录对应节点执行:

systemctl status kubelet journalctl -u kubelet -f

kubelet 是一个高频写入日志的组件,如果它在反复启动,日志里多半会直接告诉你为什么。下一节要装的网络插件,就能解决 NotReady 这个问题。

5. 网络插件:让 Pod 真正互通的关键一步

5.1 CNI 插件到底干了件什么事

K8s 的网络模型要求每个 Pod 都拥有独立的 IP,Pod 之间可以直接通过 IP 互通,不需要做 NAT。这个要求不是 K8s 自身实现的,而是通过 CNI(Container 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-app=calico-node

5.3 网络验证的两种操作

集群 Ready 之后,建议大家做一次真实的网络验证,而不是只看 Node 状态。最简单的方式是创建一个临时的 busybox Pod,然后测试 Pod 之间和 Pod 访问外网的能力:

kubectl run test-net --image=busybox --restart=Never -- 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 集群的安装本身并不神秘,很多问题的根子都在于对每个组件职责不熟,加上版本、驱动、网段这些细节没对齐。把这些基础打牢,后面做应用的滚动发布、弹性伸缩,你才有底气。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进