ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

kubeadm部署Kubernetes集群全流程详解与踩坑实践

kubeadm部署Kubernetes集群全流程详解与踩坑实践

刚用kubeadm部署完一套Kubernetes集群,前后踩了不少坑,但也把整个流程彻底理顺了。说实话,kubeadm这套工具用熟了之后,从零到一搭一套单Master、双Worker的基础集群,一个下午就能收工。但你要是第一次接触,里面每一环节都可能让人卡住:镜像拉不下来、kubelet起不来、Node一直NotReady、Pod跨节点不通……这些都是新人必经的坎。

这篇文章就把我最近一次从0到1用kubeadm部署k8s的完整过程记录下来,从节点规划、系统初始化、容器运行时配置,到Master初始化、Worker加入、网络插件安装,最后再顺手验证一下跨节点调度和DNS解析。每一步都会说清楚“为什么这么做”,而不只是贴命令,适合刚学完K8s基础、想自己动手搭一套环境的人,也适合想搞明白kubeadm原理的运维同学。

1. 部署前的规划与关键选择

1.1 为什么选择 kubeadm

先聊一个大家都会纠结的问题:部署K8s的方式那么多,二进制、MiniKube、kubeadm、RKE、K3s,为什么偏偏选kubeadm?

kubeadm是Kubernetes官方提供的集群初始化工具,它的核心价值在于把“手动生成证书、配置控制面组件、生成kubeconfig”这些易出错的操作封装成了几条命令。它不是让你绕开K8s原理,而是把证书、密钥、组件启动参数这些标准化了,集群成员你可以用一条命令加进去,后期想扩展成高可用也方便。二进制部署虽然可控性最强,但要求你对每个组件的启动参数、证书路径、健康检查端口都了如指掌,第一次搭的话光证书就可能折腾一整天。

我这次自己搭环境时,最看重的是kubeadm下面三点优势:

  • 官方维护,社区资料多,报错搜解决方案容易。
  • 可以把初始化参数写进配置文件,整套集群具备可复现性,适合做交付文档。
  • 单节点到高可用集群,升级路径相对平滑,不用推翻重来。

当然kubeadm也有短板,比如它只负责把集群“启动”起来,不等于给你做了生产化运维,监控、日志、存储这些都得自己另外接。但对于想了解K8s内部构造或者搭建测试环境的人来说,它仍然是最合适的起点。

1.2 就绪检查与资源规划

我这次用的是三台机器,一个Master、两个Worker,都是全新虚拟机,操作系统统一用了Rocky Linux 9.3,Kubernetes版本选了1.28.2。选1.28这个版本主要是因为它稳定发布有一段时间了,同时还在官方支持窗口内。K8s版本迭代很快,新版本可能有一些容器运行时兼容性的问题,而老版本又可能过了维护期,1.28目前算是一个比较平衡的选择。

如果是自己练手,建议最低配置按下面这张表来:

节点角色配置建议系统盘用途
Master2C4G起步,建议4C8G至少40G运行kube-apiserver、etcd等控制面组件
Worker根据业务负载,一般2C4G起至少40G运行业务Pod,数据多的话需要额外数据盘

这里要特别说一下,etcd是集群的“数据库”,读写频繁且依赖磁盘随机IO性能,Master节点的磁盘千万别图便宜用普通机械盘,至少也要SSD。像我们这次是纯测试环境,机械盘也还能跑,但如果打算长期使用,SSD这块的预算不能省。

1.3 网络与网段设计

K8s集群内部有三种网络需要提前规划好:Node网络(物理机/虚拟机IP)、Pod网络、Service网络。Node网络就是机器本身的IP,这个大家都熟悉,关键在Pod和Service网段。

我在部署时选的Pod网段是10.244.0.0/16,Service网段用K8s默认的10.96.0.0/12。这两个网段默认是内网私有网段,但要求必须和你的物理机/虚拟机网段不重叠。比如你的机器IP是10.244.x.x,那Pod网段还选10.244.0.0/16,集群一跑起来路由就全乱了,各种诡异的现象都会冒出来。

Pod网段和Service网段选多大,也要提前想清楚。10.244.0.0/16按/24切分,每个节点可以分配出来256个Pod地址,对于中小规模集群完全够用。如果你的Pod数量特别大,或者未来可能扩容到几千个节点,就要考虑更大的网段,比如10.0.0.0/16这类。Service网段相对小一点没关系,因为Service数量远少于Pod数量,但也要留出余量。

2. 所有节点的系统初始化

2.1 主机名与hosts解析

不管是什么节点,先把主机名设置好。实践中最容易踩的坑是:大家图省事,直接让Node之间用IP互相访问,结果Pod网络插件一装,各节点通过主机名解析不到对方,报错日志里全是未知主机名。

我一般按角色来命名,比如Master叫k8s-master-01,两个Worker叫k8s-worker-01、k8s-worker-02。设置之后立刻改/etc/hosts,把三台机器IP和主机名的映射写进去,因为后面kubeadm init生成的证书、kubeadm join用的地址解析都会涉及主机名。

hostnamectl set-hostname k8s-master-01 cat >> /etc/hosts << EOF 192.168.100.10 k8s-master-01 192.168.100.11 k8s-worker-01 192.168.100.12 k8s-worker-02 EOF

这里有个容易被忽视的点:/etc/hosts的映射要写实际业务网卡的IP,别把节点管理网卡的IP写上去。如果一台机器上有多个网卡,比如有MaaS网卡和业务网卡,K8s组件默认会选择第一个网卡地址,到时候组件监听地址和你预期的不一致,排查起来相当费劲。

2.2 关闭交换分区与防火墙

K8s官方要求必须关闭swap,这点几乎人人知道,但很多人不知道为什么。简单说,Kubelet在计算节点可用内存时,如果发现存在swap,它无法准确判断Pod实际能用的内存量,反而可能导致内存过载。所以在集群里运行时,swap对K8s来说弊远大于利,生产环境一律关闭。

swapoff -a sed -i '/swap/s/^/#/' /etc/fstab

swapoff是临时关闭,重启后又会挂载回来,所以一定要同步注释掉/etc/fstab里对应的行,否则重启之后Kubelet又会因为检测到swap而拒吃工作。

防火墙这一块,如果你是刚搭测试环境,最简单粗暴的方式是先把firewalld关掉。k8s各个组件之间有大量的端口通信需求,新手期开着防火墙十有八九会漏放端口,建议先把环境跑通,再回去根据官方文档里的端口清单做收敛。

systemctl disable --now firewalld

我建议同时把SELinux也改成permissive模式。在高安全要求的场景下SELinux可以保留,但K8s相关操作在SELinux enforcing下很容易触发权限问题,测试环境不用折腾它,直接setenforce 0,并修改/etc/selinux/config保持永久生效。

2.3 内核参数与模块配置

这一步做的人多,但很少有人讲清楚原因。K8s的核心网络转发依赖一些内核模块和网络参数,你不配好,后面Pod网络怎么都跑不通。

需要加载的内核模块主要是br_netfilter和ip6table_filter。br_netfilter的作用是让桥接的流量也经过iptables规则的过滤,K8s的Service转发、NodePort访问都依赖iptables/ipvs规则,这个模块不加载,流量转发就会出问题。

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

光加载模块还不够,还要设置sysctl参数,核心就三个:net.bridge.bridge-nf-call-iptables、net.ipv4.ip_forward、net.bridge.bridge-nf-call-ip6tables。其中net.ipv4.ip_forward尤其重要,它负责开启当前节点的IP转发能力,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

有个小习惯我建议保留:每改完内核参数都用sysctl --system检查一遍有没有报错,然后查一下lsmod | grep br_netfilter确认模块确实加载了。很多情况下你觉得配了,其实配置文件路径写错或者模块名写错,静默失败,到后边网络出问题才追悔莫及。

2.4 安装容器运行时 containerd

容器运行时这块,我直接用了containerd 1.7.x,没有用Docker。原因很简单,Kubernetes从1.24版本开始已经完全不支持Docker作为运行时了,它只支持符合CRI标准的运行时,而containerd就是那个最主流的选择。

用包管理器装其实很省事,以Rocky/RHEL系为例:

dnf install -y containerd.io

装完之后先别急着启动,先改配置。这步是新手重灾区,直接不改也能起,但会有两个大问题:一是cgroup驱动不是systemd,和Kubelet的默认配置对不上;二是它默认去拉registry.k8s.io/pause镜像,在国内网络环境下大概率会超时。

先通过下面的命令生成默认配置:

mkdir -p /etc/containerd containerd config default | tee /etc/containerd/config.toml

然后修改两个关键点。在[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]下设置SystemdCgroup = true,让containerd统一使用systemd作为cgroup驱动,和Kubelet保持一致。第二个是修改sandbox_image为可访问的pause镜像地址。这一步在离线或者网络受限环境里非常重要,你把它指向你内网镜像仓库里的pause镜像即可,否则CNI插件装完后,Pod全部卡在ContainerCreating。

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

启动后建议用ctr version看看版本,确认服务正常。另外再强调一个细节:containerd默认的/opt/containerd和/var/lib/containerd目录尽量放在数据盘上,尤其Pod多了以后镜像和容器层占用的空间增长非常快。

3. 安装 kubeadm、kubelet、kubectl

3.1 配置仓库源与版本选择

K8s组件的安装源需要单独配置,这里面的坑在于:如果你直接用云厂商的公共镜像源,有些源的默认包版本可能不是你自己想要的。比如你之前已经规划好要装1.28.2,结果直接yum install kubeadm给你装了个1.29.x,后面你的教程全部要对不上了,所以版本一定要显式指定。

我这边习惯在每一个节点上统一配置官方源或某个镜像站,然后锁定版本安装。配置源的时候注意区分RHEL系和Debian系,命令本质是一样的,就是写仓库文件。仓库文件的内容大概是定义好baseurl指向你的软件源地址,然后导入GPG key。

cat <<EOF | tee /etc/yum.repos.d/kubernetes.repo [kubernetes] name=Kubernetes baseurl=https://mirrors.xxx.com/kubernetes/yum/repos/kubernetes-el9-x86_64/ enabled=1 gpgcheck=0 repo_gpgcheck=0 EOF dnf makecache

源配好之后,用dnf list available kubeadm --showduplicates查看该源提供的可用版本列表,确认一下1.28.2确实存在。这一步千万不能忽略,很多人栽在“明明配好了源,安装时却提示找不到版本”上面,很可能就是源的版本目录和你系统版本不匹配,比如你是el9系统却配了el7的源。

3.2 安装组件与锁定版本

安装时三个组件要一起装、版本要一致。kubelet、kubeadm、kubectl如果版本不一致,虽然集群往往也能跑起来,但后续排查问题的时候你会发现各种困惑,比如kubectl版本太新,连不上老版本的API Server。

dnf install -y kubelet-1.28.2 kubeadm-1.28.2 kubectl-1.28.2

装完以后有个附加操作很重要:加锁版本,防止以后执行系统级更新时被意外升级。K8s组件跨版本升级不是小事,多了一个自动升级的隐患多不值得,直接把update策略改成excluded,或者通过包管理器将版本锁住。

dnf versionlock add kubelet-1.28.2 kubeadm-1.28.2 kubectl-1.28.2

安装完成后,kubelet会被自动装上但还没启动。这里给第一次接触的人打个预防针:现在就算你手动启动kubelet,它也会处于反复重启的异常状态,日志里全是“找不到静态Pod”之类的报错,这是正常的,因为kubelet必须在集群初始化后才知道自己要做什么。你只需要先把服务设置成开机自启。

systemctl enable --now kubelet

3.3 配置crictl工具

这里要顺带提一个我摸索出来的习惯:装完containerd后,顺手把crictl也装上,这是社区提供的CRI命令行工具,用来查看容器状态。其实在K8s环境中排查问题,crictl ps比docker ps好用得多,因为它是通过CRI接口直接看Kubernetes视角的容器状态,能看到sandbox容器和业务容器。

dnf install -y cri-tools

crictl默认连接的endpoint可能是老的Docker socket路径,而我们用的是containerd,所以要配置一下连接地址。编辑/etc/crictl.yaml,把runtime-endpoint指向unix:///run/containerd/containerd.sock。配置好后先执行crictl info验证连通性,这一步通过了,后面遇到Pod起不来的问题,你可以先看crictl ps -a,快速定位容器没起来的原因。

4. 初始化 Master 节点

4.1 编写 kubeadm 配置文件

kubeadm init虽然可以直接用命令行参数执行,但我强烈建议你写成配置文件。理由很简单:命令行参数是一次性的,你无法直观看到所有配置;而配置文件可以提交到git里保存,以后升级或者重建集群,直接改改就能复用,这才是可运维的方式。

# kubeadm-config.yaml apiVersion: kubeadm.k8s.io/v1beta3 kind: InitConfiguration localAPIEndpoint: advertiseAddress: 192.168.100.10 --- apiVersion: kubeadm.k8s.io/v1beta3 kind: ClusterConfiguration kubernetesVersion: "1.28.2" networking: podSubnet: "10.244.0.0/16" serviceSubnet: "10.96.0.0/12" imageRepository: "registry.example.local/google_containers" controlPlaneEndpoint: "192.168.100.10:6443"

配置文件里:

  • advertiseAddress是采用本机的业务IP,也就是各节点能互相访问的那个IP。
  • podSubnet和serviceSubnet必须和你在前面规划的一致,不能随意改。
  • controlPlaneEndpoint是高可用场景下使用的虚IP,单Master时写Master本身IP即可。
  • imageRepository是大家在新手期最容易卡住的点,相关命令默认从国外镜像仓库拉取,国内环境成功率很低,改成你内网镜像仓库或加速镜像站是基本操作。

4.2 执行 init 并处理镜像问题

在执行kubeadm init之前,我习惯先预拉取镜像,把可能报错提前暴露出来:

kubeadm config images pull --config kubeadm-config.yaml

如果这一步报错,大概率是在镜像地址上。可以先看错误信息里显示的是哪个镜像地址,然后手动用crictl pull该地址的镜像测试连通性。把镜像仓库地址换成一个可访问的镜像站,或者通过离线包导入镜像,都能解决。

拉取镜像没问题后,正式执行初始化:

kubeadm init --config kubeadm-config.yaml --upload-certs

整个初始化过程持续两到三分钟,中间你会在日志里看到证书生成、控制面组件启动、etcd健康检查通过等步骤。初始化成功后,日志末尾会显示三块信息:如何配置kubeconfig、如何安装CNI网络插件、如何让Worker加入集群。强烈建议把这段输出完整保存下来,特别是最后一个join命令,省得回头又要去重新生成。

执行完kubeadm init后,紧接着配置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 kubectl get pods -n kube-system

此时Node状态大概率是NotReady,这完全正常,因为CNI网络插件还没装。K8s就是这样,控制面组件起来了,但没有网络插件,节点之间没法建立Pod网络,所有Pod会处于Pending或者CrashLoopBackOff状态。

4.3 安装CNI网络插件

CNI插件我选的是Calico,这也是目前K8s生产环境最主流的网络方案。Calico支持BGP、IPIP等方式,功能上能覆盖网络策略控制,性能在中小规模集群上表现也很稳。如果你只是想快速跑通环境,Flannel也是一个选择,它更简单,但功能比Calico少很多。

Calico的YAML清单一般是从它的发布维护仓库下载,下载之后先别急着直接应用,最关键的一步是把里面的CALICO_IPV4POOL_CIDR改成你在初始化时定的Pod网段,比如10.244.0.0/16。默认Calico清单自带的通常不是这个网段,你不改的话Calico的Pod网段和集群设计对不上,Pod分配不到可用的IP。

curl -O https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/calico.yaml kubectl apply -f calico.yaml

Calico安装完成后,kubectl get pods -n kube-system里会多出一组calico-*的Pod。等这些Pod全部转成Running,差不多一两分钟,再看节点状态:

kubectl get nodes -o wide

这里有个经验:如果Calico持续有一两个Pod一直Init:0/1或者CrashLoopBackOff,九成问题是节点上的/etc/cni/net.d/目录里已经有其他配置文件残留,或者Pod网段和节点IP网段冲突。遇到这种情况,先查看Calico Pod日志里的具体报错内容,再决定怎么改。

5. Worker 节点接入集群

5.1 生成加入集群的命令

Master节点初始化成功后,Worker节点加入集群用的是kubeadm join命令。本质上它要解决两件事:一是身份认证,证明你有权限加入;二是获取集群的证书信息,和API Server建立信任关系。

如果前面没有保存初始化输出,可以重新生成一个join命令:

kubeadm token create --print-join-command

这个token默认有效期24小时,你可以顺手加一个--ttl 4h之类的参数自定义有效期。其实如果你需要在多个Worker上复制命令,最简单的方式是把这个输出直接写成一段shell脚本,然后依次到每台Worker上执行。如果token过期了,重新生成即可,不用重新初始化集群。

5.2 在Worker节点上执行加入操作

到Worker节点上,先确认提前完成了系统初始化、containerd安装和kubelet安装,然后执行join命令:

kubeadm join 192.168.100.10:6443 --token xxxxx --discovery-token-ca-cert-hash sha256:xxxxxxxx

这个过程一般一分钟以内完成。执行成功后会提示This node has joined the cluster,这时回到Master上执行:

kubectl get nodes

刚加入的Worker节点同样会先显示为NotReady,这是它在等待CNI网络配置下发。如果一两分钟后还是NotReady,优先用journalctl -u kubelet -f实时查看kubelet日志排查。

5.3 端到端验证:跨节点Pod

集群加入完成后,做一轮实际验证很有必要。很多人以为节点变成Ready就够了,其实节点Ready只代表组件健康,不代表业务流量能通。我习惯直接部署一个Nginx测试应用,让它跨节点调度,验证Pod之间互通和服务访问。

先创建一个简单的Deployment和Service:

kubectl create deployment nginx-test --image=nginx:1.25 --replicas=2 kubectl expose deployment nginx-test --port=80 --type=NodePort kubectl get svc nginx-test kubectl get pods -o wide

从输出里你可以直观看到两个Pod分布在不同节点上,Service也分配了ClusterIP和NodePort。这时去浏览器或命令行访问任意节点IP加NodePort端口,能访问到Nginx页面说明网络链路已经通了。再做一次跨节点通讯检查,进到其中一个Pod里去ping另一个Pod的IP:

kubectl exec -it nginx-test-xxxx -- bash ping 10.244.1.3

能通的话,跨节点网络就没有问题了。最后再验证一下DNS:

kubectl exec -it nginx-test-xxxx -- bash nslookup nginx-test

能解析出Service的ClusterIP,说明CoreDNS工作正常。到这一步,整条k8s部署链路就验证完毕了。

6. 常见问题定位与排查

6.1 问题速查表

把这次实践中最典型的坑点整理成一张速查表,方便大家对照排查:

现象常见原因定位思路
kubelet服务反复重启缺少静态Pod配置,或cgroup驱动不一致journalctl -u kubelet -f看报错
初始化时报错failed to pull image...镜像仓库不可达或仓库地址写错kubeadm config images pull验证
Node长时间NotReadyCNI插件没装、网络网段冲突查看kube-system下对应Pod日志
Pod一直Pending节点资源不足、镜像拉取失败kubectl describe pod查看事件
初始化成功但kubectl无权限kubeconfig没配置好检查~/.kube/config是否存在
跨节点Pod不能互访firewalld没关、sysctl转发未开启检查ip_forward、iptables规则

排查问题的时候,一定要学会一个顺序:先看kubelet日志,再看Pod事件,最后看具体容器日志。绝大多数问题都能通过这个顺序定位到根因,不要一上来就到处翻文档。

6.2 三类重点坑与排查方法

第一类,kubelet一直起不来的问题。这是新手最多遇到的坑。systemctl status kubelet显示active但很快退出,日志里最常见的报错是failed to run Kubelet: misconfiguration: kubelet cgroup driver: "systemd" is different from docker cgroup driver: "cgroupfs"。解决方式很简单,把containerd配置文件里SystemdCgroup设为true,或者在kubelet配置里指定cgroup驱动。我建议直接统一用systemd,修改好containerd配置后重启containerd和kubelet。

第二类,节点状态一直是NotReady。造成这个问题的原因比较多,但最隐蔽的是Master节点上的/etc/hosts没有正确配置到Worker节点的主机名。CNI Agent启动时如果解析不了各节点的主机名,网络插件就会处于半初始化状态。可以先把所有节点的主机名和IP写进每个节点的/etc/hosts,然后观察kubectl get nodes是否恢复Ready。

第三类,初始化时证书哈希不匹配。执行kubeadm join时,如果不小心改了token中的哈希值,或者使用了过期的token,会报CA certificate hash mismatch。重新在Master上执行kubeadm token create --print-join-command,拿到新的join命令再来执行就行,不用重新初始化集群。

6.3 日常维护与续期问题

集群搭好以后,还有一个很多人会忽略的问题:证书有效期。K8s控制面组件的证书默认有效期一年,到期之前需要续期。kubeadm在这个场景里提供了很大的便利,一年之内执行kubeadm certs renew all即可续签所有证书,续签之后重启控制面组件即可生效。

为了防止你忘记这件事,我建议装一个监控或者定时任务,在证书快过期时提醒一下自己。检查当前证书过期时间可以用:

kubeadm certs check-expiration

这个命令会列出每张证书的剩余时间,一目了然。或者简单一点,直接在日历上标注好下次续期时间,一年一提醒,虽然粗暴,但比忘了好。

另外日常查看集群状态的时候,我习惯用这几条命令:

kubectl get nodes -o wide kubectl get pods -A -o wide | grep -v Running journalctl -u kubelet --since "1 hour ago" --no-pager | tail -n 50

这三条命令一次执行,节点健康、异常Pod、kubelet日志全覆盖,日常巡检够用了。

最后再说一个个人体会:用kubeadm部署k8s,最大的价值不是“把命令跑通”,而是你在处理各种报错的过程中,把K8s的组件构成、网络模型、证书体系都摸了一遍。我建议你部署完后,把初始化配置文件、CNI清单、加入命令这些关键资产全部整理到文档或git仓库里,下次重建集群就不用再从头摸石头过河了。环境跑起来只是开始,能把集群管住、把问题定位清楚,那才是真正的收获。

RELATED READING

延伸阅读

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