带过几轮 Kubernetes 实验课之后,我越来越确定一件事:同一个实验指导书,有人能在半小时内把集群拉起来,有人却对着同一个屏幕盯上两个小时。差别不在于手速,而在于部署之前是不是把决策做完了。这篇文章是一份经过实战检验的 Kubernetes 集群部署与管理实验作业指导书,我把指导书里经常省略掉的“为什么”补全,也把实验过程中最容易踩的坑和排查思路完整写出来。适合正在做课程实验、准备自己搭一套集群环境练手、或者刚开始接触生产级 K8s 的同学。
整份实验我建议按“部署前决策、节点初始化、控制平面搭建、工作节点加入、CNI 网络插件、部署后的管理实验、故障修复练习”这条链路来做。前四步属于“搭起来”,后面三步属于“会管理”,两部分都不能省。
1. 实验开始前,先把三个决策做掉
很多同学拿到指导书第一件事就是敲kubeadm init,我一般会拦一下。实验环境的选型、容器运行时选型和组件版本选型,这三个决策会在后面大约两个小时的实验里反复影响你。它们不是“高阶选项”,而是部署前的必修课。
1.1 实验规模:单机、双节点、还是多节点
先看你能拿到多少机器。如果只是为了体验 Kubernetes 的基本功能,Minikube或者Kind就够了,一条命令能把控制平面和工作节点都跑在一台机器上。但实验作业的意义通常不是“把集群跑起来”,而是理解集群的几个核心机制:节点加入、工作负载调度到不同节点、节点故障后 Pod 如何迁移。这些场景在单机环境里根本没法真正验证。
我的建议是:实验作业至少用一主一从,两台节点。如果条件允许,一主二从更好。三节点能做的实验边界比二节点大很多,比如你可以把其中一个工作节点 cordon 掉,观察另一个节点的调度行为;也可以模拟一个节点宕机,看看已有的 Pod 会不会被重新调度。这些动作在二节点集群里虽然也能演示,但效果打了折扣。
先看一张选型对比表:
| 方案 | 推荐工具 | 适合场景 | 能覆盖的实验点 |
|---|---|---|---|
| 单机体验 | Minikube / Kind | 只想熟悉 kubectl 命令 | 应用发布、Service、配置管理 |
| 一主一从 | kubeadm 手动搭建 | 标准课程实验 | 节点加入、CNI 网络、节点管理 |
| 一主二从 | kubeadm 手动搭建 | 进阶实验、故障演练 | 节点驱逐、故障迁移、多副本调度 |
| 高可用三主多从 | kubeadm + keepalived | 接近生产环境 | 控制平面高可用、etcd 备份恢复 |
实验环境不够也没关系,还有折中方案:把控制平面节点打上污点(taint),不让业务 Pod 调度上去,只让它在集群里承担管理职责。这样一台控制平面节点加一台工作节点,也能撑起大部分实验。
1.2 容器运行时:containerd 是当前默认选择
Kubernetes 1.24 版本把dockershim从 kubelet 里移除之后,Docker 就不再是 kubelet 直接支持的容器运行时了。现在最常见的运行时是containerd,也有团队用CRI-O。实验环境里我推荐直接用 containerd,因为它是当前部署工具默认配好的选择,踩坑资料也最多。
你在初始化节点的时候只需要把 containerd 装好,kubelet 会通过 CRI(Container Runtime Interface)接口去调用它。实验里容易翻车的点有三个:一是 containerd 版本太老,和 kubelet 的 CRI 版本对不上,kubelet 日志里会直接报unknown service runtime.v1.RuntimeService;二是 containerd 的SystemdCgroup参数没改,导致 Pod 内的 cgroup 资源统计异常;三是 pause 镜像(registry.k8s.io/pause)拉取失败,Pod 一直卡在ContainerCreating。
实验前可以手动验证一下运行时是否正常:
# 查看 containerd 是否在跑 systemctl status containerd # 用 crictl 检查 CRI 是否可用 crictl info # 手动拉一下 pause 镜像,确认镜像仓库连通 crictl pull registry.k8s.io/pause:3.9crictl是实验阶段最常用的诊断工具之一,它可以查看节点上的容器、镜像和 Pod 沙箱状态。很多你从 kubectl 里看不到的信息,在crictl里一眼就能看出来。
1.3 组件版本与安装方式:为什么实验里要“锁版本”
Kubernetes 部署涉及的组件很多:kubeadm、kubelet、kubectl、etcd、apiserver、controller-manager、scheduler、coredns,再加上网络插件。它们之间的版本组合有兼容关系。最省心的做法是在所有节点上安装相同版本的kubeadm/kubelet/kubectl,并且通过系统包管理工具把版本“锁住”。
比如在 Debian/Ubuntu 上,安装时可以直接指定版本号:
apt-get install -y kubeadm=1.28.2-00 kubelet=1.28.2-00 kubectl=1.28.2-00不建议使用apt-get install -y kubeadm这种不带版本号的写法,因为你无法控制它会拉到什么版本。实验中经常出现的“kubelet 起来了一秒又退出”“apiserver 容器不断重启”,很多就是因为 kubeadm 和 kubelet 的小版本不一致。
至于安装方式,kubeadm是当前手动搭建集群的主流选择,它把控制平面组件的生命周期拉到了容器里,你只需要关心初始化和加入节点的动作。二进制方式能让你看到每个组件的启动参数,但对实验时间成本太高;脚本一键部署方式虽然快,却把太多细节隐藏了,出了问题反而不好排查。实验作业我更推荐 kubeadm,这也是目前社区资料最丰富、遇到问题最容易搜到答案的方式。
Kubernetes 版本建议选当前维护期内的小版本,不要选太老也不要选刚发布的开发版本。做实验的阶段,稳定优先。
2. kubeadm 部署实操链路:从节点准备到集群可用
这一整段是实验的核心操作链路。我会把每个步骤背后的原因讲清楚,而不是只给一段命令。因为你一旦明白了这一步“到底在解决什么问题”,后续出故障时就能快速定位到具体是哪一层没做好。
2.1 基础配置阶段:那些“看起来没用”的操作其实都在保命
在安装 Kubernetes 组件之前,每个节点都要做一组基础配置。这部分看起来琐碎,但实验中大部分“初始化失败”都源于此。
先设置主机名,并写进/etc/hosts。kubelet 会拿主机名作为节点的标识,如果两台节点的主机名重复,节点注册时会互相覆盖,集群里会出现一台机器“消失”的情况。
# 控制平面节点 hostnamectl set-hostname k8s-master # 工作节点 hostnamectl set-hostname k8s-worker1 # 在每台节点上都配好 hosts echo "192.168.100.10 k8s-master" >> /etc/hosts echo "192.168.100.11 k8s-worker1" >> /etc/hosts接下来是关闭 swap。Kubernetes 在默认配置下要求节点必须关掉 swap,因为 kubelet 的 QoS(服务质量)模型在 swap 启用时无法正确估算 Pod 的内存占用。临时关闭命令是swapoff -a,但为了重启后不失效,最好把/etc/fstab里的 swap 行注释掉。
然后加载内核模块并调整系统参数:
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这三个系统参数为什么不能省?net.ipv4.ip_forward控制 IP 转发,Pod 跨节点通信时,宿主机需要转发数据包,不开这个会直接导致跨节点的 Pod 网络不通。net.bridge.bridge-nf-call-iptables则让桥接流量也能经过 iptables 规则,kube-proxy给 Service 做的 DNAT/SNAT 才能对容器流量生效。实验里最常见的“Service 能通,Pod 之间不通”,很多就出在这两个参数漏配。
还有时间同步。节点之间时间差超过一定范围,组件之间的 TLS 证书会因为“证书有效期判断失败”而无法建立连接。实验阶段装好 chrony 或 ntpdate,至少保证所有节点时钟源一致。
最后是防火墙端口。如果实验环境里有防火墙,需要放行一组端口。最关键的是控制平面节点的6443(apiserver)、2379/2380(etcd)、10250(kubelet),以及工作节点的30000-32767(NodePort 范围)。如果只是课程实验且网络环境隔离,最简单的办法是关闭防火墙,但要清楚这是“实验妥协”,不是生产环境正确做法。
2.2 控制平面初始化:kubeadm init 的每个参数都在管什么事
基础配置做完后,安装 kubeadm、kubelet、kubectl,然后就可以初始化控制平面了。以 Kubernetes 1.28 为例,初始化命令大概长这样:
kubeadm init \ --kubernetes-version=v1.28.2 \ --control-plane-endpoint=k8s-master:6443 \ --pod-network-cidr=10.244.0.0/16 \ --apiserver-advertise-address=192.168.100.10这几个参数要逐一说清楚。
--control-plane-endpoint设置的是控制平面的统一入口地址。单控制平面实验环境里,它就是本机的主机名和 apiserver 端口;做高可用实验时,这个地址通常是一个虚拟 IP,由 keepalived 或云负载均衡器提供。实验代里如果不写这个参数,kubeadm 会默认使用第一个网络接口的 IP,如果机器有多个网卡,很容易指向错误地址。
--pod-network-cidr是 Pod 网络地址池。它必须和后面要装的 CNI 插件保持一致。比如 Flannel 默认用10.244.0.0/16,Calico 的默认地址池是192.168.0.0/16。你在初始化时指定的网段如果和 CNI 期望的不一样,后面网络插件会拒绝工作,或者给 Pod 分配出和 Service/ClusterIP 冲突的地址。
--apiserver-advertise-address明确告诉 apiserver 对外宣告的 IP 地址。在多网卡实验机上,这个参数能避免 apiserver 选错地址。
初始化过程会拉取一组镜像:kube-apiserver、kube-controller-manager、kube-scheduler、kube-proxy、etcd、coredns、pause。网络条件不理想时可以指定一台镜像加速仓库,用--image-repository参数。
初始化成功后会显示两段关键信息:一是配置 kubectl 的命令,二是工作节点加入集群的kubeadm join命令。保存好 join 命令,尤其是 token 和--discovery-token-ca-cert-hash。token 默认 24 小时过期,如果过期了也不用重新初始化,用kubeadm token create --print-join-command重新生成即可。
初始化完成后先配置 kubectl:
mkdir -p $HOME/.kube cp -i /etc/kubernetes/admin.conf $HOME/.kube/config chown $(id -u):$(id -g) $HOME/.kube/config不执行这一步,kubectl 就无法连接集群,会报connection refused或者提示找不到 kubeconfig。
2.3 网络插件与工作节点加入:决定集群能否立刻可用的最后一步
控制平面初始化完成后,集群实际上还没有 Ready。你执行kubectl get nodes会看到控制平面节点的状态是NotReady,原因就是缺少 CNI 网络插件。
CNI 插件的选择在实验阶段主要有两个:Calico 和 Flannel。Flannel 配置简单,适合快速搭环境,但 NetworkPolicy(网络安全策略)支持有限;Calico 功能更全,也是很多生产环境的选择。我的建议是实验作业里直接用 Calico,哪怕只是把它的默认配置跑起来,也能顺便学习一下网络策略怎么配。
安装 Calico 最简单的方式:
kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.27/manifests/calico.yaml安装后观察它的运行状态:
kubectl get pods -n kube-system -w等到calico-node和calico-kube-controllers变成 Running,控制平面节点通常就会变成 Ready。这时候再让工作节点加入,成功率会高很多。顺序很重要:先让控制平面节点的网络可用,再 join 工作节点,否则工作节点加入后因为没有网络插件,核心 Pod 无法和 apiserver 通信。
工作节点上执行控制平面初始化完成后输出的 join 命令:
kubeadm join 192.168.100.10:6443 \ --token <token> \ --discovery-token-ca-cert-hash sha256:<hash>控制平面节点上验证:
kubectl get nodes如果两个节点都显示 Ready,说明集群已经跑起来了。再跑一遍kubectl get pods -A,看到kube-system命名空间里的 Pod 基本都是 Running,这个部署实验才算真正通关。
3. 部署实验最容易翻车的三个环节:一次完整的排查思路演练
部署过程中最痛苦的不是敲命令,而是集群状态一切正常、但有一个 Pod 或者节点就是不对劲。实验指导书里往往只有“常见问题列表”,却没有教你怎么一步步定位。这一节我把排查思路完整走一遍。
3.1 kubelet 异常:先看日志而不是先怀疑网络
kubelet 是每个节点上的核心代理,它负责和容器运行时交互、向 apiserver 上报节点状态。很多节点级问题,根因都在 kubelet,但表面症状会表现为“节点 NotReady”或者“Pod 调度不上去”。
我建议把排查命令固定成一套组合拳:
systemctl status kubelet journalctl -u kubelet --since "10 minutes ago" -f crictl ps crictl logs <container-id>先看 kubelet 有没有跑起来。很多实验里 kubelet 装完后根本没启动,或者启动一下就退出,systemctl status会直接告诉你。然后看日志,kubelet 的日志信息量巨大,常见错误会在里面直接出现。
举个例子:如果你看到日志里有failed to get sandbox image "registry.k8s.io/pause:3.9",说明是 kubelet 调容器运行时创建沙箱时,拉不到 pause 镜像。这种情况不用重启集群,手动把 pause 镜像拉到本机就行:
crictl pull registry.k8s.io/pause:3.9另一个高频错误是Failed to run kubelet ... cgroup ... swap is enabled,这种就是 swap 没关干净,去检查/etc/fstab和当前swapon -s输出。还有一个容易被忽略的原因:--cgroup-driver和容器运行时的 cgroup 驱动不一致。实验环境里如果都用 systemd 作为 cgroup 驱动,在 containerd 的配置文件里确认SystemdCgroup为 true。
3.2 Pod 一直 Pending:事件记录比“猜原因”有效率得多
部署网络插件或者跑业务应用时,经常遇到 Pod 一直 Pending 或者 ContainerCreating。这时候不要盯着kubectl get pods看,事件记录才是最快的入口:
kubectl describe pod <pod-name> -n <namespace> kubectl logs <pod-name> -n <namespace>describe输出的 Events 区域会告诉你调度器为什么不调度、容器为什么没起来。我整理了几个高频事件的对应原因:
| 事件关键字 | 常见原因 | 下一步动作 |
|---|---|---|
0/1 nodes are available | 没有满足调度条件的节点 | 检查节点状态、污点和 label |
FailedCreatePodSandBox | CNI 网络插件未就绪或运行时异常 | 查看 kubelet 日志,检查 calico/flannel 状态 |
ImagePullBackOff | 镜像拉取失败 | 检查镜像名称、仓库连通性、私有仓库凭据 |
Back-off restarting failed container | 容器启动后立即退出 | 用kubectl logs看应用日志 |
0/1 nodes are available这类调度问题,结合kubectl describe node看节点上的污点和资源情况,通常能马上找到原因。比如工作节点上有node.kubernetes.io/unschedulable污点,说明节点被 cordon 了,去掉即可。
FailedCreatePodSandBox是最常见也最复杂的,它可能由 CNI 插件没装好、containerd 启动异常、网络内核参数不对等多种因素触发。看到这个事件后,先去kubectl get pods -n kube-system检查网络插件 Pod 是否 Running,再回到节点上crictl ps看有没有沙箱容器被创建。这两个动作能过滤掉至少一半问题。
3.3 “照着指导书敲,集群仍然 NotReady”的版本与配置问题
还有一类翻车最让人头大:所有命令都照着挂了,节点还是 NotReady,Pod 也起不来。这种情况多半是版本组合或 CIDR 配置出了问题。
先说版本组合。kubeadm 初始化时如果指定的--kubernetes-version和命令行工具版本偏差过大,kubeadm 会尝试拉取指定版本的组件镜像,而 kubelet 还是旧版本,两边 CRI 调用协议对不上,日志里会出现CRI v1 runtime API is not implemented。解决办法是统一三个组件的版本,不要只固定了 kubelet 而初始化时传了另一个版本。
再看 CIDR。如果你初始化时写了--pod-network-cidr=10.244.0.0/16,但 Calico 的默认配置期望是192.168.0.0/16,Calico 就会报告 IP 池冲突,Pod 无法获得地址。修复方式不是重新初始化集群,而是修改 Calico 清单文件里的CALICO_IPV4POOL_CIDR字段再重新 apply。
还有一个隐蔽问题:控制平面节点初始化后,如果这个节点的 IP 地址发生变化(比如 DHCP 重新分配),apiserver 的证书里记录的 IP 就失效了,kubelet 连不上 apiserver。实验环境建议在初始化前给节点配置静态 IP 或者 DHCP 保留地址,省得后面折腾证书。
4. 把“部署之后”变成实验内容:管理作业的实验设计
集群搭起来只完成了一半,实验作业要求的“管理”部分同样可以设计成可操作、可验证的任务。这里给一套可以直接用在实验课上的管理模块。
4.1 节点维护实验:cordon、drain 和污点的边界在哪
管理作业里我建议先让学员做节点维护实验,因为它是理解调度器行为的最直观方式。
先执行:
kubectl cordon k8s-worker1cordon的效果是标记节点为不可调度,但它只影响后续 Pod 的调度,已经在节点上运行的 Pod 不迁移。想验证这个区别,可以在 cordon 后用kubectl create deployment部署一个新应用,然后kubectl get pods -o wide观察 Pod 是否都落在控制平面节点上。节点上已存在的 Pod 原样运行,不受影响。
然后执行:
kubectl drain k8s-worker1 --ignore-daemonsetsdrain会把节点上的 Pod 驱逐到其他可用节点,但 DaemonSet 部署的 Pod(比如 calico-node)不会被驱逐,所以要加--ignore-daemonsets。学员如果不加这个参数,命令会卡住或者直接报错,这也是理解 DaemonSet 调度逻辑的好机会。
维护完成后执行kubectl uncordon k8s-worker1,节点恢复调度。这个三步操作可以做成一个完整实验:让学员在 worker 节点上部署一个带副本数的应用,然后执行 cordon/drain/uncordon,记录 Pod 的变化。记录结果比单纯敲命令更有价值。
污点(taint)实验也值得做。给节点打污点:
kubectl taint node k8s-worker1 dedicated=experiment:NoSchedule之后新调度的 Pod 不会跑到这个节点上,除非 Pod 显式带对应的容忍(toleration)。这个机制在生产环境里用来隔离专用节点。实验作业可以让学生自己设计一个“只有指定应用能调度到某节点”的场景。
4.2 应用发布实验:从 Deployment 到 Ingress 的一条完整链路
管理实验的另一个重点是应用发布,它贯穿 Deployment、Service、Ingress 三个对象。我建议让学员最终实现一个“通过域名访问应用”的目标,而不是停留在kubectl run跑个 Pod 就结束。
先创建 Deployment:
kubectl create deployment nginx --image=nginx:1.25 --replicas=3 kubectl scale deployment nginx --replicas=5 kubectl rollout status deployment/nginx滚动更新和回滚是实验中很关键的动作:
kubectl set image deployment/nginx nginx=nginx:1.26 kubectl rollout status deployment/nginx kubectl rollout undo deployment/nginx然后创建 Service:
kubectl expose deployment nginx --port=80 --target-port=80 --type=NodePort到这里就能通过任意节点IP:NodePort访问应用了。但实验要求如果要更接近生产,还得加一层 Ingress。先把 Ingress Controller(推荐ingress-nginx)装好,再定义一个 Ingress 资源,让实验域名指向这个 Service:
apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: nginx-ingress spec: ingressClassName: nginx rules: - host: nginx.lab.local http: paths: - path: / pathType: Prefix backend: service: name: nginx port: number: 80最后在实验机/etc/hosts里把nginx.lab.local指向 Ingress Controller 所在节点 IP,能通过域名访问应用,这条链路就完整了。学员在这个过程里会自然理解ClusterIP、NodePort、LoadBalancer这三种 Service 类型的区别,以及 Ingress 和 Service 的分层关系。
4.3 故障演习与安全加固:实验作业里就该有的“拆装练习”
管理实验如果只有“部署-访问-删除”,对故障处理完全没有训练价值。我建议实验作业增加一个故障演习环节:制造一个可控故障,让学生自己恢复。
最简单的故障演习是等应用跑起来后,手动停掉工作节点的 kubelet:
systemctl stop kubelet观察这个节点的状态变成 NotReady,之前运行在这个节点上的 Pod 会在一段时间后被调度到其他节点。等学生记录完现象,再systemctl start kubelet恢复节点。这个实验的成本很低,但对理解“自愈”机制非常有效。
再进阶一点是 etcd 备份恢复。etcd 是集群的“数据库”,所有资源数据都存在那里。实验里可以让学生做一次快照:
ETCDCTL_API=3 etcdctl --endpoints=https://127.0.0.1:2379 \ --cacert=/etc/kubernetes/pki/etcd/ca.crt \ --cert=/etc/kubernetes/pki/etcd/server.crt \ --key=/etc/kubernetes/pki/etcd/server.key \ snapshot save /backup/etcd-snapshot.db恢复的流程相对复杂,实验课上建议用一个单独的虚拟机或者恢复到临时集群,不要直接在原有集群上反复试。这个过程主要让学员明白:etcd 快照是集群的“最后救命稻草”,很多不可恢复的误删除操作,只要有快照就能回到过去某个时间点。
安全加固方面,实验环境里最容易出现的问题是 apiserver 的6443端口直接暴露。Kubernetes 的未授权访问漏洞在现实里多次成为安全事故入口,根因通常是 apiserver 没有做访问控制,或者把管理端口暴露到了不该暴露的网络。实验作业里至少应该完成的动作是:在防火墙层面限制只有管理网段能访问 6443 端口;给集群开启 RBAC,用最小权限的 ServiceAccount 和 Role 做授权,而不是长时间使用默认的 cluster-admin。
带过几轮实验之后,我个人最大的体会是:部署失败的几个原因其实高度集中,要么是网络层没通,要么是容器运行时和 kubelet 之间没对上报,要么是版本之间的兼容陷阱。把这些点提前讲透,学生“照着敲”才真的敲得通;否则只会让他们觉得 Kubernetes 很难,而不是让他们觉得这个调度系统很有趣。
最后再分享一个小技巧:实验操作比较密集的时候,可以把初始化命令、join 命令、token、节点 IP 这些参数统一放到一个 shell 变量文件里,每次开新终端先source一下,能省掉大量复制粘贴的时间和因为手误产生的错误。我自己带实验课时,还会让每个学员把自己执行过的命令记录成一个 pty 日志文件,后面遇到诡异问题时直接翻历史命令,比回忆可靠得多。