提到部署K8s集群,很多人的第一反应是收藏一堆教程,然后……就没有然后了。我之前也是,收藏了不下十篇,真正动手时不是卡在网络插件上,就是卡在版本不匹配上,前前后后折腾了一周才把一套双节点集群跑起来。这篇文章想把踩过的坑和最终沉淀下来的一套可复用流程完整讲清楚。
这篇内容适合下面几类人:对K8s只有概念、没动过手的开发者;想在实验室或自己电脑上搭一套练手环境、又不想用太重方案的运维新人;以及被各种博客片段搞得晕头转向、需要一条完整主线的人。我这里用的是最标准的工具链,不玩花活,保证你跟着做完之后,得到的是一套思路清晰、可持续维护的集群,而不是"能跑但说不清为什么能跑"的黑盒。
1. 部署前先想清楚:你要的到底是不是K8s集群
1.1 单机容器编排的极限在哪里
很多人接触容器都是从单机开始的。一个应用拆成几个容器,用现成的编排工具把它们拉到一起,跑起来很爽。这种模式在服务数量少、节点单一的时候完全够用,甚至比上K8s清爽得多。
但业务稍微复杂一点,痛点就来了:多个容器分布在好几台机器上,谁来发现谁?A容器挂了,谁来把它拉起来、把流量切走?流量一大,怎么从一台机器扩展到五台机器?这几个问题在单机阶段可以靠脚本硬顶,但脚本写到最后会变成一团没人敢动的毛线球。我见过有人用了一大堆自己写的脚本做容器的自动重启和流量切换,表面上能跑,实际上每次发版都提心吊胆。
K8s的价值不在于"能把容器跑起来",而在于它把上面这一堆问题做成了标准的、声明式的解决方案。你告诉它"我希望有几个实例、什么镜像、什么端口",剩下的调度、重启、扩缩容、服务发现,全部交给集群去处理。这种思路上的转变,才是它值得学的根本原因。
1.2 K8s真正解决的问题清单
我把K8s解决的问题列个清单,你在部署之前务必对照着看一遍,确认自己的需求真的在这个范围内:
- 多节点容器调度:容器该放哪台机器,由调度器根据资源情况决定;
- 自愈能力:容器异常退出后自动重建,节点宕机后上面的Pod会被迁移到其他节点;
- 服务发现与负载均衡:一组Pod对外提供一个稳定的访问入口,客户端不用关心后端IP怎么变;
- 弹性扩缩容:根据CPU、内存或自定义指标调整实例数量;
- 滚动更新与回滚:发版时逐个替换旧实例,失败可以快速回到上一版本。
这五点是K8s最核心的能力。如果你只是在一两台机器上跑三五个固定容器,没有弹性需求、也没有跨节点调度需求,那用轻量一点的方案确实更省心。K8s不是万能的,它本身就是一套有学习成本的分布式系统,没有明确的收益之前,不必为了"潮"而上它。
2. 环境规划:决定部署成败的往往是这一步
2.1 节点拓扑怎么定:最少两台,角色怎么分配
我给你的建议是:第一套实验环境用两台机器,一台控制平面节点,一台工作节点。
控制平面节点负责运行集群的管理组件,相当于大脑;工作节点负责跑实际业务容器,相当于四肢。生产环境里控制平面节点一般至少三台,组成高可用,否则单点故障会导致整个集群的管理面瘫痪。但那是后话,第一套环境没必要一上来就搞高可用,先把标准流程跑通,理解每个组件在干什么,比盲目堆节点重要得多。
节点规划参考如下:
| 角色 | 数量 | 建议配置 | 主要职责 |
|---|---|---|---|
| 控制平面节点 | 1 | 2核4G起,SSD磁盘 | API服务、调度器、集群状态存储、控制器 |
| 工作节点 | 1 | 2核4G起,容量随业务调整 | 运行业务容器,执行实际负载 |
我实际测下来的体感是:2核4G是最低底线,低于这个配置,控制平面组件本身就会因为资源紧张出现各种奇怪现象,比如kubelet心跳超时、API Server响应慢,看起来像配置问题,实际上就是饿的。有条件的话控制平面节点给到4核8G,后面跑监控、日志组件时你会感谢这个决定。
2.2 硬件、系统与版本选型
操作系统方面,Debian系或RHEL系都可以,命令略有差异,核心逻辑一致。我这里统一用Debian系的命令做示例,你如果用的是其他发行版,把包管理器对应一下即可。
有一点要提醒:网上至今还有大量基于CentOS 7的教程,而这些系统已经停止维护了,继续照着它们装新版本的K8s会遇到依赖问题和安全风险。如果你是新学习,直接用新系统;如果你手里是存量系统,先确认它还在维护周期内,再开始部署。
版本选型是一个很重要的坑。很多人一上来就装最新版,结果生态里的其他组件还没跟上,出了问题报错信息都搜不到几条。我建议遵循一个原则:选择当前稳定版本,并且kubeadm、kubelet、kubectl三个工具的版本必须完全一致。容器运行时和K8s之间的版本兼容关系也要看一眼官方文档的版本矩阵,不同大版本之间互相搭配很容易出现"运行时接口对不上"的低级问题。
2.3 三个网段的划分逻辑
这是新手最容易忽略、也最容易翻车的地方。K8s集群里同时存在三套网段:
- 节点网络:机器本身的IP,物理网络,这个不用你配;
- Pod网段:集群内部为每个Pod分配的虚拟IP段,由网络插件管理;
- Service网段:集群内部服务的虚拟IP段,跟Pod网段不同。
后两者都是你安装时通过参数指定的。关键要求是:Pod网段和Service网段绝对不能和你现有的节点网络冲突。比如你的机器IP是192.168.1.x,Pod网段再设为192.168.0.0/16,两边重叠之后,路由规则会乱成一锅粥,排查起来极其痛苦。
我习惯的规划方式是:节点网络按机房或云厂商分配的走;Pod网段用10.244.0.0/16或192.168.0.0/16这类专用段;Service网段用10.96.0.0/12。这三个段边界清晰,互不干扰,后面装任何网络插件都不需要反复改参数。
3. 控制面组件与kubeadm的定位:看懂再动手,不是黑魔法
3.1 控制面五个组件:用一个公司来类比
很多人记不住控制平面组件,是因为把它们当成一堆孤立的名字在背。我换个思路,你想象集群是一家公司:
- etcd:档案室,整个公司的全部状态都存在这里。节点列表、Pod列表、配置信息,无一例外。它是分布式存储,自己也是一个小集群;
- API Server:前台,也是唯一的对外入口。所有组件的读写请求都必须经过它,它负责鉴权、校验、转发。你执行的kubectl命令,本质上都是在向它发请求;
- Scheduler:人事主管,负责决定一个新来的Pod应该安排到哪台机器上。它看资源、看约束,做出调度决策;
- Controller Manager:监工,负责保证“现状”无限逼近“期望状态”。你说要有三个副本,它发现现在只有两个,就会触发创建动作,直到凑齐为止;
- Cloud Controller Manager:对接云厂商的模块,学习阶段用不到,知道有它就行。
这套类比记住之后,排查问题时的思路会清晰很多:状态对不上,去看etcd里的数据对不对;命令执行不了,去看API Server日志;Pod调不动,去看Scheduler日志。
3.2 工作节点上的三件套
控制面负责决策,真正干活的在节点上。每个工作节点必须运行三样东西:
- kubelet:节点上的管家,负责管理本节点所有Pod的生命周期,定期向API Server汇报自己的状态。它是K8s在节点上的核心代理;
- kube-proxy:负责实现Service的访问规则。它维护iptables或IPVS规则,让流量能正确转发到后端的Pod上;
- 容器运行时:真正跑容器的东西,目前主流用的是containerd。kubelet不直接操作容器,而是通过容器运行时接口和它打交道,这个接口我们通常叫CRI。
它的工作节奏是:API Server下发"本节点要有一个nginx容器"的指令给kubelet,kubelet通过CRI请求containerd拉镜像、创建容器,然后持续监控容器状态并汇报回去。
3.3 kubeadm到底替你做了什么
手工搭建K8s集群是可行的,但我非常不建议你手工搭第一套。证书生成、服务配置、组件之间的鉴权关系,任何一个环节出错都很难排查。kubeadm就是官方提供的"半自动安装器",它的核心作用有两个:
第一,kubeadm init把控制平面组件以静态Pod的形式拉起来。你看不见手动配置服务的繁琐过程,它已经在背后把证书、kubeconfig、审计配置全部写好了。
第二,它生成一条join命令,让工作节点可以一键加入集群。这条命令里包含了集群地址、token、证书校验信息,节点拿着它就能完成与控制面的互信握手。
理解了这层关系,你就知道以后排障该去哪里看:控制面组件全部以容器形态运行在kube-system命名空间里,用kubectl就能查看它们的日志,不需要再去翻系统服务目录了。
4. 控制平面初始化:每一步命令的前因后果
4.1 节点预配置:四个不起眼却致命的检查
动手敲kubeadm init之前,有四个检查必须先做,我吃过亏,所以顺序给你排好了。
第一,确认主机名。控制平面节点和工作节点的主机名必须不同,而且不能有下划线或特殊字符。K8s以主机名作为节点标识,重名或非法字符都会导致注册异常。设置方式很简单:
sudo hostnamectl set-hostname k8s-master第二,关闭swap。K8s的设计假设是节点内存的管理交给kubelet来做,swap开启会导致资源统计失真、调度决策错误。实验环境里直接关掉最省心:
sudo swapoff -a同时注释掉/etc/fstab里的swap行,否则重启以后swap又回来了,节点直接报错无法启动。
第三,加载两个内核模块。overlay提供容器镜像分层的能力,br_netfilter让iptables能正确处理桥接流量。这是容器网络的基础:
cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf overlay br_netfilter EOF sudo modprobe overlay sudo modprobe br_netfilter第四,配置内核参数。两条关键项,一条是让桥接流量经过iptables,一条是开启IP转发:
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这套操作做完之后,用free -h确认swap为0,用lsmod | grep br_netfilter确认模块已加载。别急着往下走,预配置漏一项,后面报错会让你查得怀疑人生。
4.2 容器运行时:containerd不是装完就能用
容器运行时选择containerd是当前的主流方向,因为它轻量、稳定,直接对接CRI,不需要额外的中间层。安装之后,有两处配置必须改,否则kubelet根本连不上它。
第一处是生成默认配置文件:
sudo mkdir -p /etc/containerd containerd config default | sudo tee /etc/containerd/config.toml然后编辑config.toml,找到SystemdCgroup这个字段,把它设为true。这里面的原理值得说一下:当前Linux主流的进程管理方式是基于systemd的cgroup机制,而containerd的默认值是cgroupfs。两者不一致,会导致kubelet认为容器资源统计异常,轻则告警,重则Pod无法正常运行。
第二处是镜像源相关配置。在config.toml里有sandbox_image这个参数,它指定了每个Pod启动前必须拉取的基础暂停镜像。同时,你还可以在配置里设置registry mirror,也就是镜像加速地址。这一步的作用是解决"网络环境导致镜像拉取不稳定"的问题,属于标准的容器运行时配置手段。
改完之后重启并设置开机自启:
sudo systemctl restart containerd sudo systemctl enable containerd这里再分享一个调试技巧:containerd装好后,先用crictl这个命令行工具测一下它能不能正常响应:
sudo crictl versioncrictl是直接对接CRI的客户端,K8s节点上的问题排查很多都靠它。如果这步返回了版本信息,说明运行时本身没问题,后面遇到"节点NotReady"时就能排除一个嫌疑。
4.3 安装三件套并锁定版本
kubeadm、kubelet、kubectl这三个工具,分别负责初始化集群、管理节点上的工作负载、向集群发命令。安装时最容易犯的错是三个版本号不一致,直接导致控制面初始化后工作节点无法注册。
先添加Kubernetes官方软件源,然后直接安装。以Debian系为例:
sudo apt-get update sudo apt-get install -y apt-transport-https ca-certificates curl gpg # 添加官方源(以当前官方文档提供的地址为准) sudo 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安装完成后,立即用apt-mark hold这三个包,防止系统升级时把版本带走:
sudo apt-mark hold kubelet kubeadm kubectl这就是在生产环境里被验证过无数次的习惯:K8s的版本升级必须是有计划的动作,绝不能跟着系统包管理器的节奏被动升级。
4.4 kubeadm init执行与kubectl配置
终于到了最关键的一步。在控制平面节点上执行:
sudo kubeadm init \ --pod-network-cidr=192.168.0.0/16--pod-network-cidr这个参数必须跟你后面要装的网络插件匹配。我这里给的是192.168.0.0/16,对应Calico的默认配置。如果你选了Flannel,它默认用10.244.0.0/16。两个插件不能混用,网段也不能随意改,这一点决定了Pod之间能不能互相通信。
init执行过程中如果顺利,最后会输出一大段内容,里面有两条信息必须保存下来:一条是kubeadm join开头的完整命令,另一条是配置kubectl的三行命令。我当时第一次搭的时候没存,结果token过期,只能重新生成,白白多花了几分钟。
配置kubectl的命令如下:
mkdir -p $HOME/.kube sudo cp /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config然后验证控制面是否起来:
kubectl get nodes kubectl get pods -n kube-system这个时候节点多半是NotReady状态,而且kube-system里的CoreDNS等Pod会停在Pending。这不是你操作错了,而是因为网络插件还没装,集群内的Pod之间还无法通信。很多人在这一步就慌了,开始反复重装,其实只需要继续往下走。
5. 工作节点接入与网络插件:从“控制面可用”到“集群可用”
5.1 join命令的三个核心参数
工作节点接入集群,只需要在工作节点上安装好containerd、kubelet、kubeadm(kubectl可以不装),然后执行控制面初始化时保存的join命令即可:
sudo kubeadm join 192.168.1.10:6443 \ --token xxxx.xxxxxx \ --discovery-token-ca-cert-hash sha256:xxxxxx这条命令里有三个核心参数值得理解一下,而不要只是一股脑复制:
- 集群地址:控制平面节点的IP加6443端口,这是API Server的监听地址;
- token:相当于临时入场券,默认有效期24小时;
- discovery-token-ca-cert-hash:控制平面证书的哈希值,节点用它验证自己连接的确实是目标集群,防止中间人冒充。
如果token过期了,不用急,你只要在控制平面节点上执行一条命令就能生成新的:
kubeadm token create --print-join-command这条命令会把完整的join命令直接打印出来,比手动拼接方便得多。
5.2 网络插件安装:Pod网段决定成败
集群里所有节点注册完成之后,下一步装网络插件。我用的是Calico,它在功能和性能之间比较均衡,还支持网络策略,更适合从实验环境过渡到生产环境。
安装方式很简单,下载官方的manifest文件后应用:
kubectl apply -f https://raw.githubusercontent.com/.../calico.yaml注意,官方manifest里的Pod网段默认就是192.168.0.0/16,这正好跟你kubeadm init时指定的网段对上了。如果你改了网段,需要先下载文件、修改里面的配置,再执行apply。我见过不少案例是网段不一致,Calico报了"BGP session established"但Pod之间就是不通,最后发现是子网划分互相矛盾,非常隐蔽。
5.3 节点从NotReady到Ready的变化链路
装完网络插件之后,你观察到的现象会有这样一个过程:
- kubectl get nodes,两个节点都是NotReady;
- kube-system里的calico相关Pod开始创建,先是ContainerCreating,等镜像拉完变成Running;
- 过几分钟,节点状态依次变为Ready。
从NotReady到Ready,背后其实是这条链路:kubelet成功连接API Server → 容器运行时健康检查通过 → 节点上报状态 → 网络插件就绪,Pod的网络通了 → 节点标记为Ready。
如果卡在NotReady,不要急着重启节点。先看kubelet日志,再用kubectl describe node看状态里的Conditions部分,最后检查kube-system里的网络插件Pod有没有正常运行。按这个顺序排查,基本都能定位。
6. 部署一个业务应用:验证集群不是“看起来能用”
6.1 从Deployment到Service:最小可运行业务
节点Ready之后,很多人就宣布"集群部署完成",其实这不完整。集群能跑是一回事,集群能跑业务是另一回事。我建议你做一个最小验证,也顺便体会一下K8s的声明式工作方式。
创建一个Deployment,声明两个nginx副本:
kubectl create deployment nginx --image=nginx --replicas=2查看Pod状态:
kubectl get pods -o wide你会看到两个Pod,它们的名称末尾带一串随机后缀,说明是Deployment自动创建出来的副本。它们会被调度到不同的节点上(只要节点资源允许),这本身就是对调度器的一次实际检验。
然后暴露一个Service入口:
kubectl expose deployment nginx --port=80 --type=NodePort查看Service分配的端口:
kubectl get svc nginxNodePort类型的Service会在每个节点上开一个高位端口,外部流量通过节点IP:端口就能访问到后端Pod。找任意一个节点的IP加上这个端口,在浏览器里打开,如果看到nginx的欢迎页,说明从外部到集群内部的流量链路已经通了。
6.2 集群内部DNS与服务发现验证
光验证外部入口还不够,集群内部的服务发现能力也需要验证。K8s为每个Service分配一个DNS名称,格式是服务名.命名空间.svc.cluster.local。这意味着Pod之间不用靠IP地址互访,直接用服务名即可。
验证方法很简单,在集群内临时跑一个带命令行工具的Pod:
kubectl run curl-test --image=curlimages/curl --rm -it -- sh进入容器后执行:
nslookup nginx如果能正确解析出Service的ClusterIP,说明集群DNS工作正常。这一步验证的是K8s内部服务的自洽能力,也是以后微服务互相调用的基础。很多教程根本不提这一笔,但这一笔才是最见真章的地方。
6.3 调度的基本验证:Pod真的会“漂移”
最后做一个破坏性测试。删掉一个Pod:
kubectl delete pod nginx-xxxxx注意观察:Deployment会立刻重新创建一个新Pod,新Pod的节点和序号都变了。这就是自愈能力最直观的体现——副本数量是期望状态,任何偏差都会被Controller Manager拉回来。
如果条件允许,你还可以在工作节点上执行systemctl stop kubelet模拟节点故障,过一会儿会发现Pod被迁移到控制面节点上(前提是控制面节点解除了污点)。不过这一步会触发Pod驱逐,对实验环境来说稍重,新手看删Pod重建这个级别的验证就足够了。
7. 常见故障排查与维护习惯:文档里不会写的那部分
7.1 节点NotReady:从现象到根因的完整链路
节点NotReady是新手遇到最多的故障,没有之一。直接告诉你根因之前,我先给你一套排查链路,这套链路在以后遇到任何状态类问题时都能复用:
第一步,看现象细节:
kubectl describe node <节点名>重点看Conditions字段下的Ready一行的Message。很多信息就藏在这里,比如"kubelet is not running"或"calico is not ready"。
第二步,看kubelet服务状态:
sudo systemctl status kubelet journalctl -u kubelet -fkubelet的日志里如果出现"failed to connect to the API Server",往往是网络或证书问题;出现"failed to initialize container runtime",则是容器运行时接口配置问题;出现"Error registering node with API Server",先检查主机名是否合法、是否重复。
第三步,检查容器运行时:
sudo crictl ps如果crictl都连不上containerd,问题多半出在运行时本身,而不是K8s。我遇到过最离谱的一次是containerd启用了SystemdCgroup但对应的服务没起来,果然后续就是一连串配置不匹配的报错。
第四步,检查网络插件状态:
kubectl get pods -n kube-system如果网络插件Pod处于CrashLoopBackOff,查看它的日志,重点看BGP和隧道相关的输出。网络插件挂掉,节点状态一定变NotReady,这是K8s的健康检查机制在起作用,不是误报。
7.2 镜像拉取失败:不含玄学的三种解法
镜像拉取失败是部署时常见的问题,我先给你一个中性的判断原则:先确认网络环境本身能访问到镜像仓库,再判断是地址写错、镜像不存在,还是仓库连接超时。不要一上来就乱改配置。
解法一:配置registry mirror,也就是在containerd的config.toml里设置一个你所在网络环境可以稳定访问的镜像仓库地址。这是最正规的做法,改完重启containerd即可。
解法二:如果集群处于离线环境,提前在有网络的机器上拉取镜像,导出后再导入目标节点:
# 在有网络的地方 docker pull nginx:1.25 docker save nginx:1.25 -o nginx.tar # 在目标节点上 ctr -n k8s.io images import nginx.tar注意导出的镜像要导入到k8s.io命名空间,因为containerd默认就是从这个命名空间读取K8s容器镜像的。这一笔很多教程不会写,等你发现ctr导入后K8s还是看不到镜像,再回头查就会浪费不少时间。
解法三:换镜像tag。有些镜像的某个版本自身就有兼容性问题,换个发行版本往往就解决了。
7.3 CoreDNS异常与集群基本依赖问题
CoreDNS是集群内部的DNS服务,它挂了,整个集群的服务发现就瘫了。常见表现为CrashLoopBackOff。
排查思路是:先看coredns Pod的日志:
kubectl logs -n kube-system <coredns-pod-name>常见原因有两个。一个是Pod网段配置异常,导致CoreDNS拿到的IP不在有效范围内,这种一般要回溯网络插件的配置。另一个是节点禁用了IPv6,而CoreDNS默认同时监听IPv4和IPv6,造成启动失败。解决方式是修改CoreDNS的ConfigMap,在Corefile里把IPv6监听项去掉,然后滚动重启CoreDNS Pod。
这不算什么高深知识,但凡是第一次搭建的人几乎都会碰到。我的经验是:把CoreDNS当作整个集群的健康指示器。它状态正常,说明网络、DNS、容器运行时、调度四大模块都基本健康。
7.4 证书与token过期,以及日常维护建议
集群搭好才是维护的开始。实验环境里最常见的是两个"时间炸弹":证书一年到期,token 24小时失效。
token过期好处理,前面已经说了用kubeadm token create --print-join-command重新生成。证书到期需要专门处理,可以执行kubeadm certs check-expiration查看各个证书的到期时间。这个命令应该进入你的定期巡检清单,至少每个季度看一次。
日常维护我有几个习惯,分享给你参考:
- 每周执行一次
kubectl get nodes和kubectl get pods -A,把输出存一份; - 每次变更前用
kubectl get xxx -o yaml先把当前配置备份下来; - 不要追新,生产环境的版本升级至少滞后最新版一个小版本,等社区反馈稳定后再动手;
- 把kubeadm init的完整输出保存成文件。它不只是给你看的历史记录,里面包含了集群初始化时的所有关键参数,未来排障、升级都要用到它。
最后再分享一个小习惯:我在每次搭建集群时,都会把节点准备阶段的四条命令在init之前完整跑一遍,并逐条确认输出结果。预配置的疏漏是新手最常踩的坑,而这四条命令的排查成本极低,收益却极高。这套流程我重复过很多次,每跑一遍都能发现一些可以优化的细节。你第一次搭的时候,不要贪快,每一步都确认好再走下一步,K8s本身不复杂,复杂的是那些被省略的中间态。