ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

kubeadm三节点Kubernetes集群部署:从环境准备到节点加入

kubeadm三节点Kubernetes集群部署:从环境准备到节点加入

搞了大半天的 k8s 集群部署,中间踩了不少坑,趁周末把整个过程重新整理了一遍。这篇主要记录我用 kubeadm 方式部署一套三节点 Kubernetes 集群的全过程,从环境准备、容器运行时安装、控制平面初始化,到网络插件部署、工作节点加入、验证集群可用性,全部按实际执行顺序走下来,每步都有命令和输出参考。

如果你正准备接触 Kubernetes,想自己搭一套环境练手,或者工作中要在测试环境快速拉起一套集群,这篇可以作为直接参考的操作手册。我会把部署过程中“为什么要这样配置”“遇到问题从哪里查”也一并写清楚,少走弯路。

1. 环境准备:三台机器怎么规划才不会在集群部署时返工

1.1 节点规划与版本选择

我这边用的是三台虚拟机做实验,角色分别是控制平面节点(也就是常说的 Master,新版本里叫 Control Plane)和两台工作节点。这种“一个控制平面 + 两个工作节点”的组合是练手最舒服的配置:能体验完整的调度、网络、服务暴露流程,又不需要为高可用付出太多成本。

角色主机名IP 地址配置要求操作系统
控制平面节点master01192.168.1.102核 4G 50GUbuntu 22.04
工作节点worker01192.168.1.112核 2G 50GUbuntu 22.04
工作节点worker02192.168.1.122核 2G 50GUbuntu 22.04

版本这里我选的 Kubernetes 1.28.2,这个版本不算最新,但相当稳定,网上资料也多,各种问题基本都有现成答案。如果你完全按这篇来,建议保持同样的版本号,避免因为版本差异导致参数变更。

顺带说一句,很多人刚开始会想用单机 all-in-one 的方式部署,所有角色塞在一台机器上。这种方案只适合感受一下 kubectl 命令长什么样,真要测试调度、跨节点通信、NodePort 访问,完全体验不到集群和单机的区别。所以别偷懒,三台机器是最低配置。

1.2 系统初始化清单:每个节点的必备配置

不管哪台节点,部署前都应该做一轮系统初始化。这些操作看似琐碎,但每一项都对应着一个常见的部署失败原因。

第一项是设置主机名和 hosts 解析。三个节点分别设置成 master01、worker01、worker02,然后分别在 /etc/hosts 里加上三行记录。Kubernetes 组件之间通信会用到主机名,如果解析不正确,kubelet 可能会因为无法访问 apiserver 一直报错。

第二项是关闭 swap。这个很多新手不理解,明明内存不够,为什么还要关交换分区?因为 kubelet 的资源管理和内存 QoS 依赖 cgroup 提供的准确内存使用统计,一旦启用 swap,内存回收行为就不受 kubelet 控制,Pod 的内存上限也就形同虚设了。执行 swapoff -a 并注释掉 /etc/fstab 里的 swap 行即可。

第三项是确认内核模块。Kubernetes 网络转发依赖 br_netfilter 和 overlay 模块,可以预先加载:

cat <<EOF | tee /etc/modules-load.d/k8s.conf overlay br_netfilter EOF modprobe overlay modprobe br_netfilter

第四项是内核参数。桥接流量需要经过 iptables 规则,否则集群内 Pod 之间的通信可能被防火墙规则挡住:

cat <<EOF | 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 sysctl --system

第五项是时间同步。Kubernetes 集群的证书验证和日志时间戳都依赖各节点时间一致,偏差太大时,kubelet 和 apiserver 之间的 TLS 握手容易直接失败。Ubuntu 下默认有 systemd-timesyncd,确认它处于运行状态即可;如果是 CentOS 环境,就装一下 chrony。

第六项是防火墙。我这个实验环境直接关闭了 firewalld,因为端口放行规则在复杂网络环境里很容易漏掉。但生产环境千万别这么干,应该按端口清单精细化放行。Kubernetes 主要需要放行 6443、2379/2380、10250、10251/10252、30000-32767 这几个端口。

这些初始化步骤虽然多,但你把这些命令在三个节点上都跑一遍,后面部署会顺畅非常多。

1.3 容器运行时的选择:为什么我用 containerd 而不是 Docker

这里要说明一个很多人容易搞混的点:新版 Kubernetes 已经不直接使用 Docker 作为容器运行时了,而是通过 CRI(Container Runtime Interface)对接 containerd。自 Kubernetes 1.24 起,dockershim 从 kubelet 中移除,你用 Docker 也最终会在底层调用 containerd,所以与其绕一圈,不如直接安装 containerd。

安装 containerd 在 Ubuntu 上可以用 apt,也可以从二进制包装。我直接用的官方 apt 源:

apt-get update apt-get install -y containerd

安装完成后需要生成一个配置文件,默认情况下 containerd 没有 /etc/containerd/config.toml 这个文件,需要手动生成:

mkdir -p /etc/containerd containerd config default > /etc/containerd/config.toml

关键的一步是修改 SystemdCgroup 这个配置项。默认生成的配置里它是 false,也就是使用 cgroupfs,而 kubelet 通常期望使用 systemd 驱动。两者不一致会导致节点上的资源统计错乱,甚至 kubelet 无法启动。把配置文件里 SystemdCgroup = false 改成 true:

sed -i 's/SystemdCgroup = false/SystemdCgroup = true/g' /etc/containerd/config.toml

另外,containerd 默认拉取 pause 镜像的源是 registry.k8s.io,在国内访问经常超时,建议改一下 sandbox_image 的配置,把它替换成国内可访问的镜像仓库地址。这一步我在后面问题排查部分会详细说。

配置改好后重启 containerd 并设置开机自启:

systemctl daemon-reload systemctl restart containerd systemctl enable containerd

到这里三个节点的系统层面准备就完成了,接下来进入 Kubernetes 组件的部署阶段。

2. kubeadm 部署的核心思路:先搞懂它帮你做了什么

2.1 控制面组件各自的作用(一张表讲清楚)

直接用 kubeadm 初始化之前,有必要先了解一下 Kubernetes 控制平面有哪些组件,各自干什么活。这样后续看到相关 Pod、日志的时候心里有底,排查问题也不至于抓瞎。

组件职责生活类比
kube-apiserver所有请求的唯一入口,提供 REST API,读写 etcd前台的业务办理窗口
etcd集群所有状态数据的存储中心合同档案室
kube-controller-manager维持集群期望状态,处理节点、副本、Endpoint 等各类控制器纠察队
kube-scheduler决定新 Pod 调度到哪个节点房产中介
kubelet每个节点上的代理,负责启动、停止、健康检查容器现场执行人员
kube-proxy维护节点上的网络规则,实现 Service 的负载转发快递分发站

搞清楚这些组件的分工之后,再去看部署日志就不会一头雾水。比如 etcd 启动失败,那么 apiserver 大概率也起不来,因为 apiserver 依赖 etcd 做数据存储。

2.2 kubeadm init 的执行过程拆解

用二进制方式手动部署 Kubernetes 需要自己管理证书、kubeconfig、systemd 服务、静态 Pod 定义,步骤非常多,容易漏。而 kubeadm 把这些工作自动化了,它的好处在于标准化流程和可预期的失败位置。简单拆解一下 kubeadm init 主要做的事情:

第一步是预检,也就是 preflight checks。它会检查内核参数、swap 状态、端口占用、容器运行时可用性等,发现问题就直接报错提示。所以如果 kubeadm init 刚执行就失败,99% 的报错信息会明确告诉你缺什么。

第二步是生成 CA 证书以及各组件证书,包括 apiserver、etcd、kubelet、controller-manager、scheduler。这些证书默认存放在 /etc/kubernetes/pki 下,有效期通常是一年,社区也提供了证书轮换机制,但实验环境下一般不用操心。

第三步是生成一下 kubeconfig 文件,放在 /etc/kubernetes 目录下,分别是 admin.conf、kubelet.conf、controller-manager.conf 和 scheduler.conf。

第四步是把控制平面组件的静态 Pod 定义写入 /etc/kubernetes/manifests 目录。这些组件不是通过 kubeadm 直接启动的,而是由 kubelet 以静态 Pod 的方式拉起。如果你进到 /etc/kubernetes/manifests 目录下,会看到 apiserver.yaml、controller-manager.yaml、scheduler.yaml、etcd.yaml 几个文件。

第五步是为集群生成 bootstrap token,并输出 kubeadm join 命令,供工作节点加入时使用。这个 token 默认有效期 24 小时,过期后需要重新生成。

2.3 版本一致性:控制平面和工作节点为什么要保持同版本

kubeadm、kubelet、kubectl 和 kubernetes 服务器版本之间最好保持一致。虽然 Kubernetes 官方允许 kubelet 版本略低于 apiserver 版本,但偏差过大时会出现兼容性问题,例如部分 API 资源无法识别,调度逻辑异常等。

我统一选择 1.28.2,所有节点执行同样的安装命令,避免出现版本错配这种难以排查的怪问题。这也是实验环境必须坚持的原则,别在某一个节点上单独升级或降级。

3. 控制平面安装实操:从安装工具到集群初始化

3.1 安装 kubeadm、kubelet、kubectl

在三个节点上统一安装 kubeadm、kubelet、kubectl。国内环境直接用 Google 的 apt 源肯定不行,可以改用阿里云的 Kubernetes 软件源。先把源配置好:

apt-get update apt-get install -y apt-transport-https ca-certificates curl gpg curl -fsSL https://mirrors.aliyun.com/kubernetes/apt/doc/apt-key.gpg | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg echo "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://mirrors.aliyun.com/kubernetes/apt/ kubernetes-xenial main" | tee /etc/apt/sources.list.d/kubernetes.list

这里有个细节:阿里云源里的源码库叫 kubernetes-xenial,但它在 Ubuntu 22.04 上也能用,不用特意改成 jammy。接下来执行安装:

apt-get update apt-get install -y kubelet=1.28.2-00 kubeadm=1.28.2-00 kubectl=1.28.2-00

安装完成后,先执行 systemctl enable kubelet,但暂时不要 start。因为 kubelet 现在还没有任何配置,强行启动会失败,这是正常的。

3.2 编写 kubeadm-init.yaml 初始化配置

在 master01 上,用下面这条命令生成默认配置作为模板:

kubeadm config print init-defaults > kubeadm-init.yaml

然后编辑这个 YAML 文件,我需要修改这几个关键位置:

  • advertiseAddress:改成 master01 的 IP,也就是 192.168.1.10
  • imageRepository:改成一个国内可访问的镜像仓库,否则后面拉取组件镜像会很痛苦
  • kubernetesVersion:改成 1.28.2
  • networking.podSubnet:改成 10.244.0.0/16 或者你后续网络插件要求的值

修改后的核心片段类似下面这样:

apiVersion: kubeadm.k8s.io/v1beta3 kind: InitConfiguration localAPIEndpoint: advertiseAddress: 192.168.1.10 bindPort: 6443 --- apiVersion: kubeadm.k8s.io/v1beta3 kind: ClusterConfiguration kubernetesVersion: 1.28.2 imageRepository: registry.aliyuncs.com/google_containers networking: podSubnet: 10.244.0.0/16

有人会问,podSubnet 为什么写成 10.244.0.0/16,不能随便写吗?这要看你后面选什么网络插件。Flannel 默认就使用 10.244.0.0/16 这个网段,保持一致可以避免网段冲突。

3.3 执行 kubeadm init 并处理输出

配置写好后,在 master01 上执行初始化:

kubeadm init --config kubeadm-init.yaml

这一步会花一些时间,主要卡在镜像拉取和控制平面容器启动上。如果一切正常,最后会看到类似下面的输出:

Your Kubernetes control-plane has initialized successfully! To start using your cluster, you need to run the following as a regular user: mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config Then you can join any number of worker nodes by running the following on each as root: kubeadm join 192.168.1.10:6443 --token abc123.def456xyz789 --discovery-token-ca-cert-hash sha256:xxxxxx

这段输出非常关键,务必复制保存下来,尤其是最后那行 kubeadm join 命令,后面添加工作节点要用的。如果忘记保存也没关系,可以用 kubeadm token create --print-join-command 重新生成一条,这个我在问题排查部分会再讲。

3.4 配置 kubectl 并验证控制平面组件

按输出提示在 master01 上配置 kubectl:

mkdir -p $HOME/.kube cp -i /etc/kubernetes/admin.conf $HOME/.kube/config chown $(id -u):$(id -g) $HOME/.kube/config

然后验证集群状态:

kubectl get nodes

此时 master01 应该显示为 NotReady,这是正常现象,控制平面的 Pod 虽然起来了,但整个集群还没有网络插件,节点状态无法变为 Ready。可以再看看控制平面组件的运行情况:

kubectl get pods -n kube-system

你会发现 kube-apiserver、etcd、kube-controller-manager、kube-scheduler 这几个 Pod 都在 Running 状态。如果它们出现了 CrashLoopBackOff,多半是静态 Pod 定义有问题,可以直接查看对应容器的日志,比较常见的诱因是证书目录权限、参数写错或者 etcd 端口占用。

这里还有一个新版 Kubernetes 很容易让人误解的地方,执行 kubectl get cs 会显示连接错误。这个命令在新版本里已经不准了,因为健康检查改用了单独的 healthz 端点,返回错误不代表集群有问题,可以直接忽略。

4. CNI 网络插件与工作节点的接入

4.1 选 Flannel 还是 Calico

控制平面跑起来之后,接下来就要解决集群网络的问题。CNI(Container Network Interface)插件负责为每个 Pod 分配 IP,并打通跨节点通信。

常见的两个选择是 Flannel 和 Calico。Flannel 使用 VXLAN 技术,简单、轻量、性能够用,适合学习和中小规模集群。Calico 基于 BGP 协议,支持网络策略,性能更好,配置也更复杂,适合生产环境有细粒度安全策略需求的场景。

我这次实验用的是 Flannel,原因很简单:先用最小成本让集群跑起来,把整体流程跑通。等你熟悉了基础操作,再上 Calico 也不迟。

4.2 部署 Flannel 网络插件

部署 Flannel 其实就是应用一份 Manifest 文件。官方提供的文件是一个包含了很多资源定义的 YAML,里面定义了 DaemonSet、RBAC、ConfigMap 等。

在 master01 上执行:

kubectl apply -f kube-flannel.yml

这里需要留意一个问题:默认的 Flannel 仓库地址在国内可能下载不通,导致镜像拉取失败。可以在部署前先把 kube-flannel.yml 文件里镜像地址改成国内可访问的,或者提前把 flannel 和 flannel-cni 相关镜像加载到 containerd 里。

部署完成后,查看 kube-system 命名空间下面的 Pod:

kubectl get pods -n kube-system

你会看到 flannel 的 Pod 会出现在每个节点上,当前也就是 master01 上有一个。因为 DaemonSet 会确保每个节点运行一个网络插件的 Pod。如果这个 Pod 一直处于 ImagePullBackOff,多半是镜像地址的问题,按上面提到的思路处理。

Flannel 正常后,master01 会变成 Ready 状态:

kubectl get nodes

输出里的状态应该是 master01 Ready,这里 Ready 的含义是 kubelet 健康、容器运行时正常、CNI 网络可用。

4.3 工作节点加入集群的完整流程

工作节点加入集群的流程其实比控制面简单得多。worker01 和 worker02 上已经完成了系统初始化和 containerd 安装,现在只需要补上 kubeadm 和 kubelet 组件。

apt-get install -y kubelet=1.28.2-00 kubeadm=1.28.2-00 systemctl enable kubelet

然后直接在两个节点上执行之前保存的 join 命令:

kubeadm join 192.168.1.10:6443 --token abc123.def456xyz789 --discovery-token-ca-cert-hash sha256:xxxxxx

如果 join 过程卡住,或者报容器运行时找不到,先检查 containerd 是否启动、服务是否在监听。Kubelet 启动后会自动向 apiserver 注册节点并上报状态。

回到 master01 上,查看节点状态:

kubectl get nodes

正常情况下会看到三个节点,但 worker01 和 worker02 大概率是 NotReady。不用着急,它们需要等 kubelet 把状态上报上去,Flannel 的 DaemonSet 也会自动调度到新节点并启动。等一两分钟再执行一次,你会发现三个节点全部变成 Ready。

4.4 用 Nginx 快速验证集群可用性

节点全部 Ready 只是架构层面的成功,真正能跑业务才叫部署完成。我习惯用 Nginx 做一个最简单的验证。

先创建一个 Deployment:

kubectl create deployment nginx --image=nginx

检查 Pod 调度情况:

kubectl get pods -o wide

你会看到 nginx Pod 被调度到某一个工作节点上,比如 worker01。此时如果你想从外部访问这个 Nginx,需要创建一个 Service,类型选 NodePort:

kubectl expose deployment nginx --port=80 --type=NodePort

查看服务分配到的 NodePort:

kubectl get svc

假设 NodePort 是 30567,那么在你自己电脑的浏览器里访问 http://192.168.1.11:30567,如果看到 Nginx 的欢迎页,说明集群从部署到内部网络到外部访问,整条链路都是通的。你可以把访问不同节点的同一个 NodePort 试一下,就会体会到 kube-proxy 的转发效果。

5. 部署过程中的常见问题与排查笔记

5.1 镜像拉不动:换源与预加载

kubeadm init 阶段最常见的失败原因就是镜像拉取超时。老版本中默认的镜像仓库是 k8s.gcr.io,后来换成了 registry.k8s.io,这两个地址在部分网络环境下访问速度都很慢,甚至直接超时。解决方案就是在 kubeadm-init.yaml 里指定 imageRepository 为国内可访问的仓库地址,例如:

imageRepository: registry.aliyuncs.com/google_containers

但这里要注意一个细节,并不是所有镜像都在这个仓库里齐全,尤其是 pause、coredns 这几个。如果 init 阶段还是报某个镜像找不到,可以用:

kubeadm config images list --config kubeadm-init.yaml

查看完整的镜像列表,然后逐一手动 pull 并打上原仓库的 tag,再用 kubeadm init 时跳过拉取。这种方式虽然笨,但非常可靠。另外 containerd 的 sandbox_image 也需要同步改成可拉取的 pause 镜像地址,否则 Kubernetes 运行时在创建 Pod 时会卡在 SandBox 阶段。

5.2 节点 NotReady 的排查顺序

节点一直是 NotReady,可以从这几个方向依次排查:

第一,看 kubelet 服务状态:

systemctl status kubelet journalctl -u kubelet -f

kubelet 的日志信息非常丰富,几乎每个具体问题都会直接扫一眼就能看到关键词。比如没装网络插件时日志里出现 CNI 相关报错或网络未就绪。

第二,看节点详情:

kubectl describe node worker01

描述信息底部有 Conditions 列,能直接看到 Ready、MemoryPressure、PIDPressure 等具体状态,以及最近发生的事件。这部分信息能帮你快速判断是网络、存储还是资源问题。

第三,确认 CNI 是否正常。最简单的方式就是看 kube-system 下的 flannel Pod 是否 Running,如果它一直报镜像拉取或宿主机网络配置失败,节点就不可能 Ready。

这个排查顺序基本可以覆盖 90% 的 NotReady 场景。

5.3 cgroup 驱动不匹配导致 kubelet 起不来

在 containerd 配置时我已经特意提到过 SystemdCgroup,原因就在这个坑上。如果 containerd 的 SystemdCgroup 保持 false,而 kubelet 默认使用 systemd 驱动,kubelet 启动时就会报类似这样的错误:

kubelet cgroup driver: "systemd" is different from docker cgroup driver: "cgroupfs"

不过新版 kubelet 大多会直接失败,提示你检查容器运行时。解决办法就是改 containerd 配置里的 SystemdCgroup 为 true,然后重启 containerd:

systemctl restart containerd

切记,修改后最好在三个节点上统一检查一遍,因为这种错误通常是一台台地冒出来,不是同时出现。

还有一个相关联的坑:如果你的节点上同时装了 Docker 和 containerd,而 Docker 的 cgroup 驱动是 cgroupfs,也可能出现类似报错。这里面的经验是,保持整个节点环境里“所有容器运行时 + kubelet”的 cgroup 驱动一致,就能避开大部分资源问题。

5.4 token 过期、kubectl get cs 报错等琐碎问题

kubeadm init 输出的 token 默认有效期是 24 小时。如果第二天再来加节点,join 会报 token 无效。解决办法是重新生成一条:

kubeadm token create --print-join-command

这条命令会直接输出完整的 join 命令,复制粘贴到工作节点上执行就行。

还有一个问题是 CA 证书 hash 不匹配。如果 join 时报 hash 校验失败,可以在 join 命令里加上 --discovery-token-ca-cert-hash 参数来指定,不过要注意,这个 hash 是固定的,它对应控制平面的 CA 证书,不要随意绕过校验,除非是实验环境临时使用。

再有就是 kubectl get cs 这个命令,之前提过了,新版里它返回 connection refused 不代表集群有问题,直接改用 kubectl get componentstatuses 或检查核心 Pod 状态即可,避免被这种历史遗留命令误导。

5.5 实验环境一条命令重置集群

反复练习时,你可能经常会想从头来一遍。这时候不需要重装系统,直接在当前节点上执行:

kubeadm reset -f

然后清理网络相关的残留配置:

rm -rf /etc/cni/net.d iptables -F && iptables -t nat -F

如果你想彻底一点,可以把 /etc/kubernetes 也备份后清掉,再把 containerd 数据目录清空。重置后重新执行 kubeadm init 或 join 流程即可。这一套流程多走几遍,你会对 kubeadm 内部做了什么有更直观的认识。

最后说点实际经验。环境从零部署到三节点 Ready,理想情况下 40 分钟到一个小时,但对第一次操作的人来说,两三个小时也很正常。最容易出问题的永远是镜像拉取和 cgroup 驱动,其次是网络插件的安装。我现在的习惯是先把所有节点的系统初始化脚本整理好,跑完一遍再折腾 Kubernetes 相关的内容,这样整个流程的稳定性会高很多。

部署完集群不等于完事,后面的 Pod 调度、持久化存储、Ingress 接入都还有得折腾。这套集群如果你还想继续用,建议尽早把证书续期、组件升级这些运维事项纳入计划,不然过一年再回来,会发现集群内部不少证书都过期了。

RELATED READING

延伸阅读

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