ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

kubeadm实战:从零搭建Kubernetes集群的完整指南

kubeadm实战:从零搭建Kubernetes集群的完整指南

如果你已经习惯在某云厂商控制台里点几下鼠标,十分钟拿到一个托管版Kubernetes集群,那这篇文章可能会让你稍微回到“石器时代”——但相信我,这趟石器时代走完之后,你对Kubernetes的理解会完全不一样。标题写的是“kubernetes 入门实践-搭建集群”,我就按最扎实的方式来做这件事:用kubeadm从零搭一套真正能跑业务的集群,把每条核心命令、每个关键参数、每次踩坑背后的逻辑都讲清楚,而不是给你一份复制粘贴就算完事的脚本。

这篇文章适合三种人:刚看完Kubernetes概念、准备动手但不知道第一步做什么的小白;已经在云上用过托管集群、想搞明白底层组件怎么协作的开发者;以及公司有离线环境、私有化部署需求,不得不自己维护集群的人。我会把环境准备、容器运行时、控制平面初始化、网络插件、节点加入、冒烟测试和排错链路全部走一遍,全程基于我在实际搭建中反复试错、确认过可靠的方案。

1. 为什么自己从零搭集群,而不是直接开云托管

1.1 托管集群与自建集群的本质差异

托管集群的好处是省心:控制平面由云厂商维护,你只需要关注节点池和应用。但问题也出在这里——很多人在云上用了两三年Kubernetes,还是说不清kube-apiserver、etcd、kubelet之间到底怎么通信,Pod网段和Service网段是什么关系,为什么节点加入要带一个token。因为这些东西全被托管了,你根本没有机会碰到它们。

自己用kubeadm搭集群,最大的收获不是“我搭起来了”,而是把整个控制链路亲手走了一遍。kubeadm把复杂度降到了“每条命令有明确用途”的程度,它不会替你隐藏架构,反而会通过init、join两个核心动作,让你直观看到控制平面各组件是怎么被编排起来的。

另外还有一层很现实的原因:很多公司和项目组有离线环境、内网部署的需求。托管集群进不去,你必须在裸机或虚拟机里手工搭建。早一点把这套流程摸熟,真到需要离线部署的时候会从容很多。我建议每个做云原生相关工作的人,都应该至少完完整整亲手搭一次集群,哪怕你平时根本不负责运维。

1.2 这套演示适合哪类人

我这次用的方案是三节点标准拓扑:一个控制平面节点(master节点),两个工作节点(worker节点)。控制平面节点运行kube-apiserver、etcd、kube-scheduler、kube-controller-manager,工作节点只运行kubelet和容器运行时。这是Kubernetes最经典、最能反映真实生产环境形态的入门架构。

当然你也可以用单节点(控制平面和工作负载混跑),但那种拓扑会掩盖掉很多网络和调度上的细节,比如Pod跨节点通信是怎么走的、污点是怎么阻止普通Pod调度到master的。所以我建议有条件就至少准备三台机器,资源不够的话用虚拟机也是可以的,只要网络互通就行。

这次我用的是三台Debian系虚拟机,主机名规划如下:

  • node-master:控制平面节点,2核4GB内存
  • node-worker1:工作节点,2核2GB内存
  • node-worker2:工作节点,2核2GB内存

内存是硬指标,2GB是kubelet和系统组件能稳定运行的下限,4GB给控制平面更稳妥。如果你只有两台机器,可以把控制平面和其中一个工作节点合并,但我不推荐把这个作为首次搭建方案,排查问题时你会分不清到底是控制平面的问题还是调度的问题。

2. 动手前的规划:至少三台机器和一串预留网段

2.1 节点角色与硬件底线

在敲第一条命令之前,先想清楚你的节点角色。生产环境里控制平面和工作节点通常是分开的,控制平面要跑etcd、多个控制器,资源需求明显更高。我这次做演示,控制平面给了2核4GB,工作节点2核2GB,跑一般的Demo应用完全够用。

操作系统建议统一为同一个发行版和版本,不要混用。我选择Debian系是因为包管理简单、内核版本较新,对overlay文件系统和网络模块支持友好。如果你熟悉RHEL系,也没问题,但务必保持三台机器的环境一致,否则后面排查问题时,你会被“为什么这台有报错那台没有”这种问题折磨。

主机名规划好后,第一时间写入各节点的/etc/hosts。这一步很多人跳过,实际运行中非常影响体验:

# 三台节点都执行,IP换成你自己的实际地址 192.168.31.101 node-master 192.168.31.102 node-worker1 192.168.31.103 node-worker2

这样配置之后,后续部署网络插件、生成证书、节点join时,组件之间可以通过主机名互相访问,避免IP变动带来的连锁故障。

2.2 网络架构的顶层设计

Kubernetes一上来就要规划三个网段,很多新人在这一步彻底懵掉:

  • 宿主机网络:你的节点所在局域网,比如192.168.31.0/24
  • Service网段:用于ClusterIP,是一个虚拟网段,不直接对应任何物理设备
  • Pod网段:用于给每个Pod分配IP,同样是一个虚拟网段

这三个网段不能重叠。我在演示中选择的是:Pod网段10.244.0.0/16,Service网段10.96.0.0/12。请注意,Pod网段不是随便选的,它必须和你之后部署的CNI网络插件匹配,这是新手最容易栽的地方——后面我在网络插件章节会专门展开。

为什么Service要用10.96.0.0/12而不是其他网段?因为这是kubeadm的默认值,和kube-proxy配置中默认的子网划分兼容性最好。非必要不修改,除非你的内网真的和这些网段冲突,否则用默认值能省掉大量配置联调成本。

2.3 操作系统层的统一预处理

在安装任何Kubernetes组件之前,三台节点都要做同样的系统准备。这些操作看起来琐碎,但每一件都直接影响集群能否正常建立。

第一件事:关闭交换分区。Kubernetes从设计上就不允许在swap启用的情况下运行,因为调度器基于节点可用资源做调度决策,如果内存可以换出到磁盘,资源评估就失真了。执行以下命令并永久生效:

# 立即关闭 swapoff -a # 永久注释掉 /etc/fstab 中的 swap 条目 sed -i '/swap/s/^/#/' /etc/fstab # 验证 free -h

第二件事:加载内核模块并配置网络转发。Kubernetes依赖iptables处理流量,依赖overlay文件系统支撑容器镜像分层。这两项不配好,后面kubelet会起不来或容器运行时异常。

cat <<EOF | tee /etc/modules-load.d/k8s.conf overlay br_netfilter EOF modprobe overlay modprobe br_netfilter 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

第三件事:确保节点时间同步。证书验证、日志时间戳都依赖准确的时间,强烈建议配置NTP同步。时间偏移过大会直接导致kubeadm init或join时报证书相关错误,这类问题隐蔽且难排查,最好提前杜绝。

3. 容器运行时的坑:containerd的SystemdCgroup与非默认配置

3.1 为什么现在默认使用containerd

Kubernetes从1.24版本开始废弃了对Docker作为运行时socket的直接支持,这不是说不能用Docker镜像了,而是kubelet不再通过Docker来启动容器,而是直接通过容器运行时接口(CRI)和containerd通信。containerd本身可以独立管理容器,Docker镜像的格式它完全兼容,所以现在的主流方案就是安装containerd作为底层运行时。

很多新手对这一步的理解停留在“装一个containerd就行”,但实际操作中至少有两个配置必须手动修改,否则节点加入集群后要么无法启动Pod,要么启动后反复重启。

3.2 安装后必须修改的两处配置

安装containerd(我这里以Debian系为例,包管理器路径可能有差异)并生成默认配置:

# 安装后先生成默认配置文件 containerd config default | tee /etc/containerd/config.toml

然后打开/etc/containerd/config.toml,修改两个关键位置。

第一个:把SystemdCgroup设置为true。containerd默认的cgroup驱动是cgroupfs,而kubelet默认使用systemd作为cgroup驱动。两者不一致时,kubelet会报“failed to run Kubelet: cgroup driver"错误。这是一条非常典型的配置项,我每次帮别人排查集群问题时,第一站永远是检查这里:

[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc] ... [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options] SystemdCgroup = true

第二个:将pause镜像的Sandbox镜像地址改为你网络环境能拉取到的地址。containerd的默认配置里写的是registry.k8s.io/pause版本号,国内网络环境直连这个地址常常超时。你需要改成可用的公共镜像仓库地址。这一步不调整,后续初始化集群时,每个节点拉取pause基础镜像都会卡住,表现为Pod一直处于ContainerCreating状态。

改完后重启并验证:

systemctl restart containerd systemctl enable containerd ctr version

3.3 验证运行时是否就绪

一个容易被忽略的细节是:修改了/etc/containerd/config.toml后,最好先用containerd config check检查语法和配置项,确认关键值已经生效。启动后可以用crictl验证CRI接口是否正常,crictl是独立的调试工具,常用于手动查看容器和镜像状态:

crictl version crictl info

如果crictl没有默认连接,需要检查/etc/crictl.yaml(如果存在)里是否配置了endpoint。containerd的CRI默认监听unix:///var/run/containerd/containerd.sock。配置好之后,crictl info能看到容器运行时的版本、cgroup驱动等信息,确认无误再进入下一步。

提示:cgroup驱动一致性是集群能否稳定运行的基础。请确保containerd的SystemdCgroup=true,且后续kubelet配置不额外指定cgroup-driver为cgroupfs,否则它们会互相冲突。

4. 控制平面初始化:kubeadm init的每个参数都有用

4.1 kubeadm、kubelet、kubectl的版本对齐

三台节点都要安装这组工具,版本必须统一。Kubernetes集群对版本差异非常敏感,控制平面和节点之间的版本差距不能超过一个小版本。我这次用的是一个稳定的Kubernetes v1.29系列版本,你没有必要追最新版,选一个已经发布几个月的稳定版本即可。

安装后建议锁定版本,避免apt或yum意外升级导致控制平面和节点版本不一致。Debian系可以用如下方式锁定(不同发行版命令不同):

apt-mark hold kubeadm kubelet kubectl

请你务必养成这个习惯。我见过不止一次生产事故是因为节点上一个apt upgrade -y把kubelet升级到了和apiserver不兼容的版本,整个集群直接报警。

4.2 init命令参数全拆解

所有前置条件准备就绪后,在控制平面节点node-master上执行初始化。先执行以下命令,它会清理环境中可能的残留配置,并把运行时要用的镜像预先拉取到本地:

kubeadm config images pull --image-repository=你的公共镜像仓库地址

你可能会问,为什么不直接执行kubeadm init?因为init过程中如果镜像拉取失败,会直接中断,你很难分辨是镜像源的问题还是配置的问题。先单独拉取镜像,把源的问题提前暴露掉。这一步同时也是验证containerd配置是否正确的快捷方式,如果拉取成功,说明容器运行时的网络和配置都没问题。

接下来正式初始化:

kubeadm init \ --apiserver-advertise-address=192.168.31.101 \ --image-repository=你的公共镜像仓库地址 \ --kubernetes-version=v1.29.0 \ --pod-network-cidr=10.244.0.0/16 \ --service-cidr=10.96.0.0/12

逐项解释一下:

  • --apiserver-advertise-address:指定kube-apiserver对外广播的地址,也就是控制平面节点的IP。这一步是为了让apiserver生成证书时把该IP加入SAN,同时让工作节点能通过这个IP访问API。注意必须使用节点的实际IP,不能用127.0.0.1。
  • --image-repository:指定镜像仓库前缀,因为我们把拉取源改为国内公共镜像仓库。
  • --pod-network-cidr:Pod网段,必须和后续CNI插件要求的网段一致。这里我写10.244.0.0/16,对应后面要部署的flannel。
  • --service-cidr:Service网段,保持默认10.96.0.0/12即可。

有些教程会加了--control-plane-endpoint参数,这是为高可用多控制平面准备的,这里不展开。单控制平面集群没有这个参数也能正常工作。

如果init成功,Kubernetes会输出一段信息,包括:控制平面已初始化、加入工作节点的完整kubeadm join命令、以及配置kubeconfig的方法。请保存好这段join命令,后面要用。

4.3 初始化后的kubeconfig与环境配置

init完成后,当前用户还不能直接用kubectl,因为kubeconfig文件生成在/etc/kubernetes/admin.conf,普通用户默认没有权限读取。需要把它复制到当前用户的~/.kube目录:

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

然后验证控制平面组件状态:

kubectl get pods -n kube-system

正常情况你可以看到apiserver、etcd、scheduler、controller-manager都进入Running状态,但此时集群还是“残疾”的——你会发现coredns一直处于Pending状态,节点status也是NotReady。原因在于缺少网络插件,没有CNI,Pod之间无法创建网络通道。这正是下一节要解决的问题。

5. 网络插件部署:Pod网段与CNI必须二选一对准

5.1 flannel与calico的实际差异

Kubernetes本身不提供Pod间网络的实现方式,它把网络交给了CNI插件。两个最主流的选择是flannel和calico,我从实际使用体验的角度说一下区别。

flannel的特点是简单省心,使用VXLAN隧道技术,对网络环境要求低,普适性好。代价是网络性能有一些损耗,并且只关注三层网络连通性,不支持NetworkPolicy。calico使用BGP路由协议直接在各节点间路由Pod流量,性能更好,支持NetworkPolicy,功能强大,但对网络环境要求更高,在某些云环境或复杂网段里需要额外配置。

我这次演示选flannel,原因只有一个:它是初学者失败率最低的方案。大部分人的第一次集群搭建,目标应该是“让它跑起来”,而不是一开始就上最复杂的网络方案。等你熟悉了整个链路,再换calico也不迟。

5.2 部署flannel并验证

flannel的部署文件是一个YAML清单,里面包含DaemonSet、ServiceAccount、ConfigMap等资源。你可以从官方仓库获取部署清单文件并应用。部署前第一件事是检查清单里的Pod网段是否和init参数匹配。默认情况下,flannel的配置里写的是10.244.0.0/16,所以你在init时指定这个网段就是为了和它对齐。如果你的环境必须用其他Pod网段,一定要同步修改flannel YAML里的ConfigMap配置。

应用清单:

kubectl apply -f kube-flannel.yml

然后观察状态:

kubectl get pods -n kube-flannel -o wide kubectl get nodes

正常情况下,几秒钟到几分钟内,每个节点上的flannel Pod会变为Running,节点状态会从NotReady变为Ready。如果节点一直NotReady,问题大概率出在容器运行时配置或Pod网段不一致上。这个状态转换是集群能不能继续走的关键节点,值得盯紧一点。

5.3 换网络方案时的清理顺序

很多人会搭到一半想从flannel换成calico,或反过来。此时最忌讳的做法是“先apply一个,再看情况把旧的删掉”,这会导致每个节点上残留两个CNI配置,kubelet会随机选择一个,最终结果就是Pod网络时通时不通。

正确的清理顺序是:先删除旧插件的DaemonSet和相关资源,然后检查并清理所有节点上/etc/cni/net.d/目录下的残留配置文件,确认接口清理干净后,再应用新的CNI插件。每更换一次网络插件,建议直接从kubectl delete走到节点重建,而不是在同一个节点上反复覆盖。

6. 工作节点加入与集群冒烟验证

6.1 生成join命令的两种方式

刚才kubeadm init输出里有一串join命令,里面包含一个token。如果当时没保存,或token过期了,可以在控制平面节点上重新生成:

kubeadm token create --print-join-command

这会在输出一个完整的kubeadm join命令,包含鉴权token和ca证书的哈希值。在每一个工作节点上执行该命令即可。如果你有多个工作节点,可以在控制平面生成一次命令,然后在所有工作节点上重复执行。

这里有个细节值得说明:kubeadm join的本质是让新节点用token向apiserver发起CSR(证书签名请求),然后获取自己的kubelet客户端证书。默认token有效期是24小时,如果你第二天再来加入节点,会发现token已过期。企业环境里可以通过配置更长的token有效期,但出于安全考虑,我建议还是按需生成,用完即失效。

6.2 节点加入后的状态排查

在工作节点上执行join后,回到控制平面查看节点:

kubectl get nodes

如果节点状态是NotReady,典型的排查路径是:

  1. 确认flannel或CNI插件是否已经覆盖到新节点。新节点加入后,CNI的DaemonSet默认会在所有节点上新调度一个Pod,检查这个Pod是否正常运行。
  2. 如果CNI Pod也在Pending或CrashLoopBackOff,查看其日志。最常见的原因是拉取pause镜像失败,以及前面讲过的SystemdCgroup配置问题。
  3. 如果在控制平面看不到新节点出现,检查该节点上的kubelet服务和容器运行时是否正常运行:
systemctl status kubelet journalctl -u kubelet -f

6.3 冒烟测试:部署一个临时应用验证链路

集群搭建完成后,先别急着去跑真实业务,用一个最简单的Deployment验证整条链路是更明智的做法。运行一个轻量级的Web服务,比如nginx:

kubectl create deployment nginx-smoke --image=nginx:latest kubectl expose deployment nginx-smoke --port=80 --type=NodePort kubectl get svc nginx-smoke

然后拿到NodePort映射的端口,从任意节点访问:

curl http://节点IP:映射端口

能返回nginx的默认页面,说明Pod调度、容器启动、Service负载均衡、节点间路由全部正常,集群已经达到“可用”状态。再深入一层,可以测试一下DNS功能:

kubectl run curl-test --image=busybox --rm -it --restart=Never -- nslookup kubernetes.default

如果DNS解析能返回Service IP,说明coredns工作正常,集群内部服务发现机制也确认可用。

7. 我踩过的坑和完整排查链路

7.1 kubelet反复崩溃:cgroup驱动不一致

这个问题我在不同环境里遇到过至少五六次,而且每次的报错信息都不太一样。有时是“Failed to start Kubelet”,有时是容器运行时启动失败,有时节点永远NotReady。最后追根究底,几乎都是同一个根源:containerd的SystemdCgroup没有设为true,或者kubelet启动参数里指定了一个和containerd不一致的cgroup驱动。

排查路径是这样的:先看kubelet日志,定位到cgroup相关关键词。然后在控制平面或节点上检查运行时配置,看看实际生效的cgroup驱动是什么。这里有个实操技巧:直接在节点上检查containerd日志或在crictl info中确认Runtime配置,通常可以发现SystemdCgroup是否为true。如果确认不一致,修改containerd配置并重启服务,再重启kubelet,节点状态通常一两分钟之内就会恢复Ready。

7.2 节点一直NotReady:容器运行时未注册

另一个高频问题是,kubelet起来后报类似“Failed to get node info: node "node-worker1" not found”的错误。这种情况下,你要检查的不是网络插件,而是kubelet是否成功向apiserver注册了自己。常见的坑有:kubelet配置里指定的容器运行时socket不对、节点上存在旧的kubelet缓存、证书过期。

快速处理方式:停止kubelet,删除/var/lib/kubelet目录下可能的缓存和证书文件,再重新启动kubelet让它重新走一遍注册流程。更理智的做法是在节点上执行:

kubeadm reset

然后重新执行join。kubeadm reset是处理节点加入失败问题的“一键重置”方案,可以重新来过。不少教程把reset当作兜底方案,但对于刚入门的人,我反而建议更早使用reset——这比在一堆残留配置文件里猜来猜去高效得多。

7.3 证书与Token问题

证书类问题通常发生在两个时间点:集群刚搭的时候和运行一段时间后。

刚搭建时遇到证书问题,八成是系统时间偏差导致。排查方法与前面说的NTP同步有关。我曾经在一组节点上遇到apiserver的HTTPS证书验证失败,排查了所有配置都没发现问题,最后发现是其中一台工作节点的系统时间和控制平面差了十几分钟。

运行一段时间后遇到的证书问题,则是kubeadm管理的证书过期。Kubernetes控制平面组件的证书有效期默认是一年,可以通过以下命令查看:

kubeadm certs check-expiration

如果发现即将到期,可以通过kubeadm certs renew all配合kubeadm certs renew --use-api等方式续期。续期后需要重启相关组件或节点,这块属于运维范畴,但作为入门实践,至少要能看懂证书列表里哪些证书需要关注。

7.4 一条内核参数引发的网络黑洞

我最后一次分享的坑来自一个小细节:只配置了net.bridge.bridge-nf-call-iptables但没开启net.ipv4.ip_forward,或者反过来。Kubernetes的Pod网络依赖宿主机IP转发,如果ip_forward为0,Pod可以创建成功、Service也可以创建,但跨节点的Pod间通信会直接“黑洞化”,表现为Pod能起来但ping不通或者curl超时。

排查这类问题可以从两端下手:先在源节点上ping目标Pod的IP,观察是否通;如果通,说明路由正常,问题可能在Service层;如果不通,重点检查目标节点的iptables规则和内核转发参数。一个简单而有效的验证命令是在节点上执行:

sysctl net.ipv4.ip_forward

只要返回值不是1,就说明转发被禁用。这一类基础系统参数问题,比任何组件配置问题都更隐蔽,因为它不会直接报错,只会让你看到“网络不正常”的模糊现象。

我在搭建这套集群的过程中踩过不少类似坑,坦白讲,每次解决完都发现是某个小细节没有按规范处理好。Kubernetes集群搭建这件事的难点并不在于某个组件的配置有多复杂,而在于它要求所有组件、所有配置、所有系统参数都同时处于正确状态。所以我的建议是:严格按本篇文章的步骤走,每一步的验证都确认通过后再进下一步,不要想着“先搭起来,后面再修复”。集群搭建的试错成本,永远是事后修复比事中验证高得多。这也是为什么我把所有命令的参数含义和系统层面的注意事项都写了出来——你理解了每一步在做什么,才能真正掌握这套体系,而不是只获得一个“碰巧跑起来的集群”。

RELATED READING

延伸阅读

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