干运维这些年,Kubernetes 部署这件事,我从第一次照着网帖敲命令翻车,到后来帮团队搭测试环境、预发环境,前前后后折腾过几十套集群。最深的体会是:K8s 部署的难点从来不在“敲命令”,而在部署前那套环境逻辑有没有理顺。很多时候 Pod 一直 Pending、节点 NotReady,根本不是操作步骤不对,而是网络插件、容器运行时、系统参数这些底层东西没对齐。
这篇我按实际动手的顺序写,全程用 kubeadm 在 Ubuntu 22.04 上从零部署一套一主两从的 Kubernetes 集群,把部署方案怎么选、环境怎么初始化、containerd 和 kubelet 之间的调用链路、以及部署完成后最常见的故障排查思路都讲透。适合刚接触 K8s 的运维和开发照着操作,也适合有经验的人当一份排查手册翻。
1. 部署前的思路拆解:先想清楚再动手
1.1 为什么选 kubeadm,而不是二进制或发行版
Kubernetes 集群部署有好几条路:二进制包手动部署、kubeadm、RKE、K3s、云厂商托管的 EKS/ACK 这类。我这次选 kubeadm,原因很简单:它现在是官方主推的部署工具,也是社区里资料最全、排障最容易的一条路。
二进制部署能把每个组件都摸得很透,但对大多数人来说维护成本太高。K8s 的控制面有 kube-apiserver、kube-controller-manager、kube-scheduler、etcd 四个核心组件,二进制方式部署,你不仅要自己生成证书、写 systemd unit、手工配置每个组件的参数,还要自己处理组件之间的启动顺序和健康检查。一套下来非常耗时,而且换个版本可能就要重新踩一遍坑。
kubeadm 把这些都封装成了标准流程:证书生成、组件静态 Pod 编排、kubeconfig 分发、token 引导,全部自动完成。你只需要理解它留下的几个扩展点,比如 etcd 用外部还是内置、CNI 用哪家、负载均衡怎么做。它生成的集群是符合官方最佳实践的,不会像某些一键脚本一样,给你装出来一个“看起来能用、实际到处埋雷”的集群。
1.2 集群规模与拓扑设计
这次演示的拓扑是一主两从:Master 节点负责控制面,两个 Worker 节点跑业务负载。生产环境如果你对高可用有要求,通常会做三台 Master 加前置负载均衡的拓扑,但核心逻辑和单 Master 是一样的,多出来的部分主要是 etcd 选主和 apiserver 负载均衡。
IP 规划我列一下:
| 角色 | 主机名 | IP 地址 | 配置建议 |
|---|---|---|---|
| Master | k8s-master | 192.168.50.10 | 4C8G,SSD |
| Worker1 | k8s-node1 | 192.168.50.11 | 4C8G,SSD |
| Worker2 | k8s-node2 | 192.168.50.12 | 4C8G,SSD |
节点配置这块,演示环境 2C4G 就够跑,但如果你要同时跑 CNI、Ingress、监控这些组件,建议至少 4C8G。磁盘最好用 SSD,etcd 对 IO 延迟非常敏感。
1.3 版本选择的关键考量
版本选择是部署前最容易被忽略的决策点。我的建议是:不要追最新版,选当前阶段最稳的版本。这次演示用 Kubernetes v1.28.2,containerd 1.7.x,Calico v3.26.x,这个组合我实跑了很多次,兼容性没有坑。
选版本时需要注意:
- kubeadm、kubelet、kubectl 三个组件的版本差必须在两个小版本以内,否则可能报版本不匹配。
- containerd 和 Kubernetes 的版本配套关系,官方有兼容性矩阵,部署前先确认。
- CNI 插件的支持范围。Calico 3.26 支持 Kubernetes 1.26 到 1.28,我用的版本正好在区间内。
提示:如果照着本文部署时你拿到的版本更新了,先去查官方 Kubernetes 版本兼容矩阵,别直接套旧参数硬上。版本不匹配导致的问题,排查起来比配置错误要难得多。
2. 环境准备与基础组件安装
2.1 操作系统与系统初始化
我的环境是 Ubuntu 22.04 LTS,内核 5.15,自带 containerd 模块。如果用的是 CentOS 7 或 8,步骤大差不差,但包管理器和防火墙命令会有差异。
系统初始化这一步,很多人图省事跳过了,后面 kubeadm init 直接失败。建议按下面的步骤来,全都是在三台机器上执行的通用操作。
关闭 swap。Kubernetes 从 1.28 开始对 swap 的支持虽然有一些变化,但默认情况下 kubelet 仍然要求关闭 swap,否则直接报错:
sudo swapoff -a sudo sed -i '/ swap / s/^/#/' /etc/fstab加载内核模块。kube-proxy 依赖 ipvs 或 iptables,overlay 网络依赖 br_netfilter:
cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf overlay br_netfilter EOF sudo modprobe overlay sudo modprobe br_netfilter配置网络转发参数。这里的关键是让桥接流量经过 iptables,否则跨 Pod 的流量可能直接不通:
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顺手做两件事:把三台机器的主机名改掉,再配置好 hosts 解析。集群内组件之间通过主机名互访,如果解析不了,etcd 和 kubelet 都会出妖蛾子。
sudo hostnamectl set-hostname k8s-master # 另外两台分别改为 k8s-node1、k8s-node2cat <<EOF | sudo tee -a /etc/hosts 192.168.50.10 k8s-master 192.168.50.11 k8s-node1 192.168.50.12 k8s-node2 EOF2.2 容器运行时的选型:containerd 还是 Docker
Kubernetes 在 1.24 版本之后移除了 dockershim,这之后如果你还继续用 Docker 作为运行时,需要额外装 cri-dockerd 这个适配层。生产上没有任何理由再绕这么一圈,直接用 containerd 是更干净的方案。
containerd 本身就是 Docker 的底层容器运行时,Kubernetes 社区一直在维护对它的直接支持。而且 containerd 比 dockerd 占用资源更少,调用链更短,Pod 启动也更快。
安装 containerd 我直接用了官方源:
sudo apt-get update sudo apt-get install -y containerd装完之后需要改配置,重点是把 SystemdCgroup 打开。版本不同,配置方式略有差异。我这次装的 containerd 1.7.x,用containerd config default生成配置再手动改:
sudo mkdir -p /etc/containerd containerd config default | sudo tee /etc/containerd/config.toml然后编辑/etc/containerd/config.toml,找到SystemdCgroup,从 false 改成 true:
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc] ... [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options] SystemdCgroup = true这一步为什么重要?因为 kubelet 默认使用 systemd 作为 cgroup 驱动,而 containerd 如果不开启 SystemdCgroup,它管理的容器会使用 cgroupfs。两者不一致时,kubelet 会在启动时报 cgroup 驱动不匹配的错误,节点直接 NotReady。
改完后重启 containerd:
sudo systemctl restart containerd sudo systemctl enable containerd2.3 安装 kubeadm、kubelet、kubectl
接下来安装三个核心工具。我用的是阿里云镜像源,国内速度快,配置方法:
sudo apt-get update sudo apt-get install -y apt-transport-https ca-certificates curl gpg curl -fsSL https://mirrors.aliyun.com/kubernetes/apt/doc/apt-key.gpg | sudo 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' | sudo tee /etc/apt/sources.list.d/kubernetes.list sudo apt-get update sudo apt-get install -y kubelet=1.28.2-00 kubeadm=1.28.2-00 kubectl=1.28.2-00 sudo apt-mark hold kubelet kubeadm kubectlapt-mark hold是为了防止后续手滑升级,把集群搞炸。K8s 集群的组件版本必须一致,这是部署里最常见的坑之一。
装完后确认版本,我的输出是:
$ kubeadm version kubeadm version: &version.Info{Major:"1", Minor:"28", GitVersion:"v1.28.2", ...}3. 核心实操:从 init 到节点加入
3.1 初始化 Master:kubeadm init 的参数与输出解读
所有准备工作完成后,在 Master 节点上执行初始化。我这套集群的 Pod 网段规划为192.168.0.0/16,这个网段是给后续 Calico 用的,必须保持和 Calico 默认的网段一致,否则网络插件起不来。
sudo kubeadm init \ --apiserver-advertise-address=192.168.50.10 \ --pod-network-cidr=192.168.0.0/16 \ --kubernetes-version=v1.28.2几个参数解释一下:
--apiserver-advertise-address:Master 对外的 API Server 地址。如果在云上,通常要对内网地址;如果是裸机,填实际 IP。--pod-network-cidr:Pod 网段,这个网段的大小决定了整个集群能承载多少 Pod。/16有 65536 个地址,够绝大多数场景用。--kubernetes-version:指定版本,明确告诉 kubeadm 用哪个版本初始化。避免它自己检测时因为网络请求超时导致失败。
常见的报错是镜像拉取失败。kubeadm init 默认会从registry.k8s.io拉镜像,如果在部分网络环境下不通,可以在 init 前先手动拉取或给 containerd 配置镜像加速。我在这次部署中预先配置好了镜像加速器,把registry.k8s.io指向了可访问的镜像源,做法是在 containerd 的 config.toml 里增加 endpoint。
init 过程正常的话,最后终端会输出这样一段:
Your Kubernetes control-plane has been 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 You can now join any number of worker nodes by running the following on each as root: kubeadm join 192.168.50.10:6443 --token abcdef.0123456789abcdef \ --discovery-token-ca-cert-hash sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx这段输出里藏着两个关键信息:kubectl 的配置文件路径,以及 worker 加入集群的 join 命令。join 命令要保存好,如果忘了,后面用kubeadm token create --print-join-command重新生成。
先配置 kubectl,让当前用户能操作集群:
mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config配置好后,检查节点状态:
kubectl get nodes这时候 Master 节点显示为 NotReady 是正常的,因为还没有装网络插件。没有任何网络插件的情况下,控制面组件虽然能起,但 CoreDNS 会一直 Pending,节点就绪状态不会被标记。
3.2 安装 CNI 网络插件
CNI(Container Network Interface)负责为每个 Pod 分配 IP、配置网络策略。Kubernetes 本身不提供 Pod 网络实现,你必须在部署后马上装一个。我这次选 Calico,原因可以在一个 Pod 里同时跑 BGP 和 IPIP,性能在裸机环境表现不错,还支持网络策略。
Calico 的官方安装方式:
kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.26.1/manifests/tigera-operator.yaml然后创建自定义资源:
kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.26.1/manifests/custom-resources.yamlcustom-resources.yaml里最关键的一段是 Pod 网段,要和 kubeadm init 时保持一致:
spec: ipPools: - cidr: 192.168.0.0/16注意:如果这里把网段写错,Calico 会把 Pod IP 池配到别的网段,kubelet 分配 IP 时对不上,业务 Pod 全都会卡在 ContainerCreating。
装完后,等 Calico 的 Pod 全部 Running:
kubectl get pods -n calico-system然后再次查看节点状态:
kubectl get nodes NAME STATUS ROLES AGE VERSION k8s-master Ready control-plane 12m v1.28.2等到 Master 变成 Ready,说明控制面已经真正健康了。这种“先装网络插件,再看节点就绪”的顺序,是 K8s 部署的标准节奏,别急着加 worker。
3.3 Worker 节点加入集群
在 Worker 节点上执行 init 输出的 join 命令:
sudo kubeadm join 192.168.50.10:6443 --token abcdef.0123456789abcdef \ --discovery-token-ca-cert-hash sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx如果 token 过期了,在 Master 上重新生成:
kubeadm token create --print-join-commandjoin 成功后,回到 Master 查看:
kubectl get nodes NAME STATUS ROLES AGE VERSION k8s-master Ready control-plane 15m v1.28.2 k8s-node1 Ready <none> 40s v1.28.2 k8s-node2 Ready <none> 35s v1.28.2三个节点都 Ready,基础集群就算搭完了。
3.4 验证集群功能
光有节点 Ready 还不够,要确认 Pod 调度、服务发现、网络转发都正常。我一般用这种方式验证:
创建测试 Deployment:
kubectl create deployment nginx-test --image=nginx:1.25 kubectl expose deployment nginx-test --type=NodePort --port=80然后查看 Pod 和 Service:
kubectl get pods -o wide kubectl get svc nginx-test用 NodePort 访问测试页:
curl http://192.168.50.11:$(kubectl get svc nginx-test -o jsonpath='{.spec.ports[0].nodePort}')能正常返回 nginx 欢迎页,说明 Pod 调度、CNI 网络、kube-proxy 转发、NodePort 映射这几条链路都是通的。这套验证动作我建议每个新集群都跑一遍,比什么kubectl cluster-info可靠得多。
4. 关键原理:Kubernetes 是怎么调用 containerd 的
热词里有朋友专门问“Kubernetes 是如何调用 containerd 的”,这确实是个值得深入理解的问题。很多人部署完集群,一路顺畅却讲不清 kubelet 到容器之间发生了什么。
4.1 CRI 和 OCI 的职责边界
Kubernetes 本身不直接管理容器,它通过 CRI(Container Runtime Interface)调用任意实现了该接口的运行时。CRI 是一套 gRPC API,核心方法包括:
RunPodSandbox:创建 Pod 沙箱。CreateContainer:在沙箱里创建容器。StartContainer:启动容器。StopContainer/RemoveContainer:停止和删除容器。
containerd 在启动时会自动开启一个 CRI 插件,plugin 名字是io.containerd.grpc.v1.cri,它监听在/run/containerd/containerd.sock这个 socket 上。kubelet 通过--container-runtime-endpoint=unix:///run/containerd/containerd.sock找到它。
CRI 只管“创建容器”“启动容器”这些高层操作,真正把一个 Linux 进程创建出来的底层逻辑属于 OCI(Open Container Initiative)的范畴。containerd 内部把 CRI 请求转换成 OCI spec,再交给 runc 这个低层运行时去执行。runc 负责和内核打交道,最终通过clone()系统调用创建出一个隔离的进程。
所以完整链路是:
kubelet -> CRI gRPC 请求 -> containerd 的 CRI 插件 -> containerd 内部处理(拉镜像、解包、挂载) -> 调用 runc -> 内核创建容器进程4.2 kubelet 创建 Pod 的完整调用链
结合我们部署集群时的实际情况,走一遍 kubelet 创建 Pod 的完整过程。
先看 kubelet 的配置,它通过/var/lib/kubelet/kubeadm-flags.env里的参数知道容器运行时的 socket:
cat /var/lib/kubelet/kubeadm-flags.env KUBELET_KUBEADM_ARGS="--container-runtime-endpoint=unix:///run/containerd/containerd.sock --pod-infra-container-image=registry.k8s.io/pause:3.9"当你在笔记本上执行kubectl apply -f deployment.yaml,请求到 apiserver,写入 etcd。kubelet 通过 apiserver 的 watch 机制发现集群里出现了一个需要调度的 Pod,这是第一段链路:API 请求和状态同步。
调度器(kube-scheduler)选好节点后,Pod 被绑定到某一台 Worker。这台节点上的 kubelet 开始干活,第一步是通过 CRI 调用RunPodSandbox。沙箱对应的是 Pod 的基础容器 pause,它不运行业务逻辑,只负责持有这个 Pod 的网络命名空间、PID 命名空间、IPC 命名空间。containerd 收到请求后,先把 pause 镜像拉下来,然后通过 runc 创建 pause 容器。
pause 容器起来之后,Pod 级别的网络栈就有了。业务容器创建时,containerd 会把它直接加入 pause 容器的网络命名空间,这也是同一个 Pod 里的不同容器为什么共享 IP 和端口的原因。
接下来是镜像处理。kubelet 调用CreateContainer,containerd 先去拉镜像,如果镜像已经存在就跳过。然后 containerd 会把镜像解包成 OCI 格式的 rootfs,需要用到快照器(snapshotter)。snapshotter 管理着一层一层的镜像层,通过 overlayfs 把它们挂载成一个可写目录。
最后是StartContainer。containerd 把这整个 rootfs、网络配置、环境变量、命令参数都组装成一份 OCI spec,然后调用 runc。runc 解析 spec,调用内核的clone()创建容器进程,同时通过命名空间(namespace)和控制组(cgroup)完成资源和隔离限制。
这个过程里有一个细节值得注意:kubelet 是通过 watch apiserver 感知 Pod 变化的,所以从应用 Deployment 到 Pod 真正启动,中间的消息链路是异步的。有时候你在kubectl get pods里看到 Pod 已经创建了但状态是 Pending,说明调度器还没选出节点,或者 kubelet 正在处理沙箱创建请求。
4.3 容器运行时出问题时的排查切入点
理解了调用链,排障和调试就也有思路了。如果 Pod 一直卡在 ContainerCreating,先确认是沙箱阶段、镜像阶段还是启动阶段出了问题。
沙箱阶段的问题,一般是 pause 镜像拉不下来,或者 containerd 的 CRI 插件没有正确加载。检查 CRI 插件状态的方法是crictl info,如果提示连接失败,多半是 containerd 没起来或者 socket 路径不对。
镜像阶段的问题,最典型的就是 ImagePullBackOff,先kubectl describe pod看看事件里的错误信息,再手动用crictl pull验证能不能拉取镜像。
启动阶段如果出错,crictl logs能看到容器启动后的日志,crictl inspect可以查看容器状态。这些工具在排查容器运行时问题时,比 kubectl 更贴近底层。
5. 线上故障排查实录与避坑指南
部署完不等于万事大吉,接下来几个排查方向是从多次故障里实打实趟出来的。我按发生频率从高到低整理成一份速查表,再挑三个典型问题展开说说。
5.1 高频故障速查表
| 现象 | 可能原因 | 排查命令 | 解决办法 |
|---|---|---|---|
| 节点 NotReady | 容器运行时未启动 / CNI 故障 / swap 未关闭 | journalctl -u kubelet -f | 修复对应问题,重启 kubelet |
| 节点 NotReady(报cgroup 驱动不匹配) | containerd 的 SystemdCgroup 未开启 | crictl info | 修改 config.toml,重启 containerd |
| Pod 一直 Pending | 资源不足 / 污点 / 调度器异常 | kubectl describe pod <pod> | 查看事件,增加资源或调整调度策略 |
| Pod 卡在 ContainerCreating | CNI 网络未就绪 / pause 镜像拉不到 | kubectl describe pod <pod> | 安装或修复 CNI,镜像加速 |
| ImagePullBackOff | 镜像名写错 / 仓库认证失败 / 网络问题 | kubectl describe pod <pod> | 修正镜像配置或镜像源 |
| CrashLoopBackOff | 应用启动失败 / 健康检查不通过 | kubectl logs <pod> --tail=100 | 看日志定位应用启动问题 |
| kubeadm init 卡在等待 apiserver | 镜像拉取失败 / 端口冲突 | journalctl -u kubelet -n 50 | 检查镜像和端口占用 |
| join 时报 token 无效 | token 过期 / CA hash 不对 | kubeadm token list | 重新生成 join 命令 |
| 跨节点 Pod 网络不通 | Calico IPIP 模式问题 / 防火墙 | calicoctl node status或kubectl exec内 ping | 检查主机防火墙和安全组,确认 IP 转发开启 |
| Service 无法访问 | kube-proxy 未正常工作 / NodePort 被防火墙拦截 | kubectl get svc -A和curl测试 | 修复 kube-proxy,放通端口 |
| CoreDNS 一直 Pending | CNI 未安装或 pod 网段不一致 | kubectl get pods -A | 安装 CNI,检查网段 |
| etcd 健康检查失败 | 证书过期 / 磁盘空间不足 | etcdctl endpoint health | 更新证书或清理磁盘 |
5.2 三个让人印象深刻的排障案例
先讲讲 cgroup 驱动不一致的问题。有一次我帮同事排查一套新部署的集群,两个节点都是 NotReady,kubelet 日志反复刷:
failed to run Kubelet: misconfiguration: kubelet cgroup driver: "systemd" is different from docker cgroup driver: "cgroupfs"解决方式前面提到过,把 containerd 的 SystemdCgroup 改成 true。这个坑因为排障信息很直接,还算好解决。最怕的是改完之后 containerd 配置写错,里面有两个 SystemdCgroup 字段,一个在 runc 的 options 里,一个在 CRI 的参数里,两个要同时为 true 才行。
第二个案例是 Pod 一直 Pending。那次新搭建的集群,创建 Deployment 后所有 Pod 都卡在 Pending,describe 查看事件,发现没有任何调度器的调度记录,一直是0/3 nodes are available。检查 kube-scheduler 日志,发现调度器 Pod 反复重启,因为 master 节点上有污点,而调度器本身没有容忍这个污点。这种情况在二进制部署和 kubeadm 部署的混合迁移场景里比较常见,需要手动给 kube-scheduler 的静态 Pod 清单加 toleration。
第三个案例是跨节点网络不通。集群状态看起来完全正常,节点全 Ready,Pod 都是 Running,但一个 Pod 访问另一个节点上的 Pod,网络就是不通。后来发现 Calico 默认用 IPIP 模式,它需要在节点间封装隧道,如果主机防火墙把ipip协议的流量拦截了,跨节点通信就会失败。手动加入规则放通 ipip 协议后,一切恢复正常。
5.3 集群部署完之后的几件事
集群搭建完成、故障也排查完了,我会再做几件事,这几件事能让集群长期稳定运行。
监控必须装上。一个没有监控的 K8s 集群就像闭着眼睛开车。Prometheus 加 Grafana 是社区标配,kube-prometheus-stack一个 helm chart 就能把 kubelet 指标、etcd 指标、apiserver 的指标全采上来,再配几个告警规则。我自己的习惯是装完集群的当天就把监控搭好,不给自己留“等有空再说”的借口。
日志采集建议用 Loki 或者 ELK。K8s 的 Pod 是飘的,节点一重启,容器日志就没了,必须用采集器集中收走。这个一般在业务量上来之后再考虑,但部署时就规划好日志目录和存储策略,后面会省很多事。
etcd 备份一定要做。etcd 里存着集群的所有状态,一旦损坏,整个集群就废了。最简单的方案是写个 cronjob,每小时执行一次etcdctl snapshot save,把快照存到远程存储。我见过太多人忽略这一步,等集群出事了才后悔。
不要禁用 Node 的磁盘警报。kubelet 默认在节点磁盘使用率超过 85% 的时候会把节点标记为磁盘压力,然后赶走上面的 Pod。这个机制看起来“很烦”,但它是保护集群的最后一道防线,别为了省事关掉它。
最后分享一点个人的经验
Kubernetes 部署这件事,做过一次和做过十次的区别,不在于你能背出多少条命令,而在于你头脑里有没有一张完整的调用链地图。部署命令只是入口,真正有价值的是当你看到 Pod 卡在 ContainerCreating 时,能立刻定位到是镜像问题、CNI 问题还是运行时问题,然后直接走到根因去解决。
我在这次部署过程中,所有的配置文件和操作命令都同步到了文档里,每个步骤后顺手记录观察到的输出。这个习惯看似简单,但在集群出问题需要回溯时,能帮你省下大把的时间。如果你照着这篇文章搭建过程中遇到疑问,欢迎交流,我踩过的坑大概率也能帮你避一次。