
先交代清楚一件事这是一篇给所有准备在生产环境、实验环境里手动搭 Kubernetes 集群的人写的手册。版本锁定在 v1.28.2容器运行时固定用 containerd部署方式采用 kubeadm。关于 Kubernetes 集群部署网上有大量教程但绝大多数要么只讲 docker 时代的老流程要么在 containerd 和 kubeadm 配合的关键细节上含糊其辞。我这次把完整的部署过程、容易踩的坑、以及每个“为什么这么配置”的道理都记录下来确保你照着做能少走至少两天弯路。v1.28.2 是 2023 年 8 月放出的 1.28 系列里的修复版本稳定性口碑不错目前大量中小团队和生产环境都在用这个系列。而 containerd 从 Kubernetes 1.24 开始成为默认容器运行时之后已经成了事实上的标准选择现在新部署集群再纠结 docker 反而是给自己找麻烦。这套文档适合刚入门 Kubernetes 的运维、准备把 Doris/Spark 这类大数据组件往 K8s 上搬的平台工程师以及想彻底搞懂 kubeadm containerd 协作机制的开发者。全文按真实实操顺序来写不玩虚的。1. 版本选型与整体设计思路1.1 为什么选 v1.28.2 这个版本先说版本选择。Kubernetes 的版本发布节奏是三个月一个大版本每个版本支持约十二个月。v1.28 发布于 2023 年 8 月v1.28.2 是它的第二个 patch 版本主要修掉了上一版里比较影响使用的 BUG比如 kubelet 在某些场景下的状态上报异常、调度器的偶发抢占问题等。选 v1.28.2 而不是更新版本原因很务实v1.28 这个版本的 API 相对稳定大量主流 CNI 插件、负载均衡器、存储驱动都已经针对这个系列做过充分兼容测试。对于生产环境来说“大家都在用”就是最大的优势。另外如果你后续要基于这套集群做 Doris 这类大数据组件的容器化部署你会发现官方 operator 和 Helm Chart 的兼容性矩阵里v1.28 几乎是必测版本出问题时的排查资料也最好找。如果你是跟着本文首次部署我不建议你直接用最新版因为你遇到问题之后很难搜到足够多的踩坑记录。稳定版本的意义在于别人已经帮你验证过坑了。1.2 为什么运行时用 containerd 而不是 docker从 v1.24 开始Kubernetes 彻底移除了对 docker 作为运行时dockershim的内置支持containerd 成为默认和推荐选择。很多从老教程转型过来的人第一反应是“那我不习惯 docker 命令怎么办”这里要先扭转思路。containerd 是 Docker 的底层容器运行时即 Docker 实际干活的引擎就是 containerddocker 命令只是它上面的操作壳。对 K8s 而言kubelet 通过容器运行时接口CRI直接和 containerd 通信比之前的 kubelet - docker - containerd 链路短了一层。这意味着更少的资源消耗、更快的容器启动速度、更少的故障点。部署时你还会多学一个工具 crictl它是专门给 CRI 运行时用的容器管理命令。日常排查会用到crictl ps、crictl logs对应 docker 时代的docker ps、docker logs。这个切换成本很低而且管理语义更贴近 K8s 的“Pod”视角后面我会专门整理命令对照。1.3 部署方式、网络插件与组件规划部署方式这里我直接选用 kubeadm。有人会觉得二进制部署更“底层”、更“可控”但 kubeadm 生成的证书、配置、启动参数都是经过成千上万集群验证过的既保证可控性又节省时间。从 kubeadm init 到 worker 节点 join它已经把控制平面组件的编排方式标准化了。即使你想深入理解 Kubernetes 源码级别的细节kubeadm 生成的标准配置文件本身就是最好的学习范本。网络插件我用 Calico。对比同样是主流的 FlannelCalico 支持网络策略NetworkPolicy性能在小集群规模下也够用而且部署方式简单。Flannel 的核心优势是轻量但它太“单纯”不具备策略能力。如果你只是想跑个实验环境Flannel 也完全 OK但如果这个集群要承载大数据工作负载、多租户应用Calico 是更稳妥的选择。整体规划如下1 台 master 节点运行 kube-apiserver、kube-controller-manager、kube-scheduler、etcd以及 kubelet 和 containerd1~N 台 worker 节点运行 kubelet、kube-proxy、containerd网络插件CalicoVXLAN 模式服务网段10.96.0.0/12Pod 网段192.168.0.0/16这套规划对 3 台机器以内的小集群刚刚好如果是生产环境多 master后续单独讲高可用扩展。2. 环境准备与前置配置2.1 主机规划与系统要求我建议的最低配置如下低于这个配置跑起来会非常卡角色CPU内存磁盘系统master2 核4 GB40 GBRocky Linux 9 / Ubuntu 22.04 / CentOS 7.9worker4 核8 GB100 GB同上Kubernetes v1.28.2 对内核版本要求不算高3.10 以上的老内核也能跑但如果你要用到较新的网络特性建议内核 5.4 以上。系统我推荐 Rocky Linux 9 或 Ubuntu 22.04后面所有命令都以 CentOS/Rocky 系为主Ubuntu 的差异我会用小字标注。所有节点的 hosts 文件必须写清楚这是新手最容易忽略的一步。假设我的 master 是 192.168.100.10worker 是 192.168.100.11、192.168.100.12那么在每台机器上都要配置cat /etc/hosts EOF 192.168.100.10 k8s-master 192.168.100.11 k8s-node1 192.168.100.12 k8s-node2 EOF同时给每台机器设置好持久主机名hostnamectl set-hostname k8s-master # 其他节点对应修改2.2 内核参数与基础服务这里要做的环境配置每一条都是有原因的缺一条后面就多一个坑。首先加载两个关键内核模块cat EOF | tee /etc/modules-load.d/k8s.conf overlay br_netfilter EOF modprobe overlay modprobe br_netfilteroverlay是 overlayfs 文件系统containerd 的镜像层存储基于它br_netfilter让网桥上的流量也能被 iptables 规则处理。K8s 的服务转发依赖 iptables如果网桥流量绕过 iptablesService 的负载均衡和集群网络策略都会失效。接着写内核参数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 --systemnet.ipv4.ip_forward是让节点像路由器一样转发 Pod 发出的数据包不开它跨节点的 Pod 通信直接断掉。这三行参数配置完之后你可以不用深究原理但一定要在每台节点上执行。如果你是用云服务器部署建议提前在安全组里放行必要端口最核心的是master 的 6443API Server、worker 的 10250kubelet、以及 Calico 用的 BGP 端口 179如果你开 IPIP 模式则还需要放行 4789VXLAN。本地虚拟机部署直接关闭防火墙或按同样规则放行。2.3 安装前常见的三个“隐藏雷区”第一个是 swap。K8s 要求节点关闭 swap因为 kubelet 的 QoS 机制和内存限制在 swap 启用时会变得不可控。执行swapoff -a之后还要在/etc/fstab里注释掉 swap 那一行否则重启之后又回来了。我见过有人明明关了 swapreboot 后 kubelet 起不来一看 fstab 还在挂载 swap白排查半小时。第二个是时间同步。Kubernetes 的证书签发、token 校验全靠时间比对节点间时间差超过几秒join 过程就会出现诡异的证书错误。建议所有节点安装 chrony 并同步到同一时间源。这不是可以跳过的步骤。第三个是主机名冲突。如果节点重名kubectl get nodes 里只会显示一个节点在线另一个一直 NotReady。检查方式很简单每台机器执行hostname确保各不相同。补充一个小经验云厂商的机器默认/etc/hosts里可能已经绑定了本机私有 IP 和主机名hostnamectl set-hostname后 start 不了是常事。你要手动确认 hosts 里的本机映射一致宁可多写也不要不写。3. containerd 部署与配置3.1 安装 containerd 与 runccontainerd 的安装我有两种推荐方式。如果你使用的是 CentOS/Rocky直接配置 docker 官方源安装 containerd.io 也挺方便但版本可能不是最新。我这次为了精确控制版本直接用官方 release 的 tar 包安装。# 下载 containerd 1.7.7 的 Linux amd64 包具体版本号可以去官方 release 页确认 wget https://github.com/containerd/containerd/releases/download/v1.7.7/containerd-1.7.7-linux-amd64.tar.gz tar Cxzvf containerd-1.7.7-linux-amd64.tar.gz -C /usr/local这个包解压后/usr/local/bin下会出现containerd、containerd-shim-runc-v2、ctr等二进制。同时去 runc 的 release 页面下载对应版本一般建议至少 1.1.7 以上旧版 runc 存在安全问题且对 cgroup v2 支持不好wget https://github.com/opencontainers/runc/releases/download/v1.1.7/runc.amd64 install -m 755 runc.amd64 /usr/local/bin/runc接下来生成 systemd service 文件。虽然 tar 包不带 systemd 配置但 containerd 官方的建议方式很固定我直接写好放上去cat EOF | tee /etc/systemd/system/containerd.service [Unit] Descriptioncontainerd container runtime Documentationhttps://containerd.io Afternetwork.target local-fs.target [Service] ExecStartPre/sbin/modprobe overlay ExecStart/usr/local/bin/containerd Typenotify Delegateyes KillModeprocess Restartalways RestartSec5 LimitNPROCinfinity LimitCOREinfinity LimitNOFILEinfinity TasksMaxinfinity [Install] WantedBymulti-user.target EOF systemctl daemon-reload systemctl enable --now containerd启动后用systemctl status containerd确认状态再用ctr version查一下版本确保二进制正常。3.2 生成并修改 config.tomlcontainerd 的默认配置不一定适合 Kubernetes 使用必须手动生成并修改两处关键配置。执行mkdir -p /etc/containerd containerd config default | tee /etc/containerd/config.toml然后打开/etc/containerd/config.toml找到SystemdCgroup把它从false改成true。这一步至关重要Kubernetes 的 kubelet 默认使用 systemd 作为 cgroup 驱动如果 containerd 这边用 cgroupfs两边驱动不一致kubelet 直接启动失败。凡是教程里没强调这一步的大概率会让你卡在节点 NotReady。找到sandbox_image这个配置项默认值是registry.k8s.io/pause:3.9。这里面临一个现实问题默认的 registry.k8s.io 在国内或部分内网环境拉不动。我建议在 config.toml 里直接改成可访问的仓库地址比如我这边常用的是阿里云镜像仓库sandbox_image registry.aliyuncs.com/google_containers/pause:3.9如果你是在 tgz 离线环境部署需要提前拉好 pause 镜像然后在所有节点上通过ctr images import导入。我把这步放前面就是因为它直接影响 kubeadm init 的速度不提前想好初始化时卡片在拉取 pause 镜像那一步能急死人。修改完配置后重启 containerdsystemctl restart containerd3.3 安装 crictl 并验证运行时crictl 是排查 containerd 运行时问题最趁手的工具必须装上。它的版本最好和 Kubernetes 版本保持在同一大版本内1.28.x 对应 crictl v1.28.x。wget https://github.com/kubernetes-sigs/cri-tools/releases/download/v1.28.0/crictl-v1.28.0-linux-amd64.tar.gz tar zxvf crictl-v1.28.0-linux-amd64.tar.gz -C /usr/local/bincrictl 默认不知道 containerd 的 socket 地址需要写配置cat EOF | tee /etc/crictl.yaml runtime-endpoint: unix:///run/containerd/containerd.sock image-endpoint: unix:///run/containerd/containerd.sock timeout: 10 debug: false EOF验证一下crictl info如果输出 containerd 的版本信息说明运行时已经准备好接受 kubelet 的连接。4. kubeadm 引导集群安装4.1 安装 kubelet、kubeadm、kubectl三个核心组件都装在所有节点上版本必须对齐 v1.28.2。我的做法是直接用 Kubernetes 官方 yum 源安装为了易用性也可以换成国内镜像源但核心命令一样cat EOF | tee /etc/yum.repos.d/kubernetes.repo [kubernetes] nameKubernetes baseurlhttps://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64/ enabled1 gpgcheck0 EOF yum install -y kubelet-1.28.2 kubeadm-1.28.2 kubectl-1.28.2注意这样安装完 kubelet 后systemctl enable kubelet会启动失败这是正常的。现在还没有集群配置kubelet 会反复重启报错等 kubeadm init 完成之后它才会正常。安装完成后查看版本kubeadm version kubectl version --client如果输出v1.28.2组件安装成功。4.2 kubeadm init 初始化 master初始化之前最好先把需要的镜像列表拉下来。执行一次kubeadm config images pull --image-repositoryregistry.aliyuncs.com/google_containers --kubernetes-versionv1.28.2这是提前检查网络的好机会。如果这里卡住说明镜像仓库访问有问题你现在把问题解决了比在 kubeadm init 时卡住再排查要舒服很多。接下来执行初始化。我的网络规划是 Pod 网段 192.168.0.0/16这是 Calico 默认使用的网段如果你的网络插件选了 Flannel这里要改成 10.244.0.0/16。master 的内网 IP 是 192.168.100.10kubeadm init \ --apiserver-advertise-address192.168.100.10 \ --kubernetes-versionv1.28.2 \ --image-repositoryregistry.aliyuncs.com/google_containers \ --pod-network-cidr192.168.0.0/16 \ --service-cidr10.96.0.0/12这一步会创建整套控制平面组件耗时约一到两分钟。中途如果出现 block容器本身会用保存的镜像快速启动。初始化成功后会输出两段重要信息一段是 kubectl 的配置文件复制命令另一段是 worker 节点的 join 命令。这两段务必备份。在 master 上执行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 pods -n kube-system你会发现所有控制面 Pod 都是 Running 或 ContainerCreating其中 CNI 插件还没装Calico 的 Pod 还没创建所以 core-dns 可能处于 Pending 状态这是正常现象。4.3 工作节点加入与高可用说明把 kubeadm init 输出的 join 命令在每台 worker 上执行。如果当时忘记复制命令可以用下面的方式重新生成kubeadm token create --print-join-command这条命令会输出全新的 join 命令token 有效期默认 24 小时。如果连 CA 证书 hash 也想重新获取可以执行openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt | openssl rsa -pubin -outform der 2/dev/null | openssl dgst -sha256 -hex | sed s/^.* //然后把输出接在 join 命令的--discovery-token-ca-cert-hash参数后面。在 worker 节点执行完 join 命令后等约一分钟回到 master 执行kubectl get nodes如果看到三个节点都 Ready 了恭喜集群基础已经通了。如果还有节点是 NotReady先不要慌大概率是 CNI 网络插件还没部署看下一章。补充一句高可用的事情如果生产环境需要多 master需要在前面加一个负载均衡器keepalived 或者 haproxy然后把多个 master 的--control-plane-endpoint指向这个 VIP。这个配置链路比较长不属于“最小可用集群”的范畴我建议先按单 master 把流程跑通再考虑扩展。5. 网络插件部署与集群验证5.1 部署 Calico 网络插件Calico 的部署流程很成熟我用 operator 方式安装或者直接使用 manifest两种方式都行。这里用官方 manifest 的方式版本选择 v3.27.x适配 Kubernetes 1.28kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/calico.yaml如果你所在环境下载不了 GitHub 资源可以把 manifest 保存下来手动 edit 里面镜像地址后 apply。Calico 启动的核心组件包括 calico-node 和 calico-kube-controllers它们在 kube-system 命名空间下。部署后看状态kubectl get pods -n kube-system -w等 Calico 相关 Pod 都进入 Running再执行kubectl get nodes此时原来 NotReady 的节点应该陆续变成 Ready。因为 Calico 会在每个节点上创建 BGP 隧道或 VXLAN overlay 网络节点状态依赖这个网络的健康情况。如果长时间处于 CrashLoopBackOff最常见的两个原因一是 Pod 网段和 Calico 默认配置不匹配我上面特意用了 192.168.0.0/16就是为了和默认配置对齐二是节点的物理机防火墙屏蔽了 Calico 需要的 179/4789 端口。5.2 用 nginx 工作负载验证链路集群搭建完成后必须跑一个真实工作负载验证整条链路。我用一个无状态应用也就是 nginx来确认容器调度、镜像拉取、Service 转发和外部访问都正常。创建测试 Deployment3 副本kubectl create deployment nginx --imagenginx:1.25 --replicas3查看 Pod 分布和状态kubectl get pods -o wide如果所有副本都 Running说明 kubelet 能正常通过 containerd 创建并运行容器了。再把服务暴露出来kubectl expose deployment nginx --port80 --target-port80 --typeNodePort kubectl get svcNodePort 模式下K8s 会从 30000-32767 中分配一个端口比如 30080。访问任意节点的 IP 加这个端口比如http://192.168.100.11:30080看到 nginx 欢迎页说明流量从宿主机经过 kube-proxy 转发到了 Pod链路全部打通。5.3 containerd 命令速查与日常管理集群运行起来之后你会在排障时频繁接触 containerd 的命令。我整理了一张常用对照表建议截图保存用途docker 时代containerd/crictl 时代查看运行中容器docker pscrictl ps查看全部容器docker ps -acrictl ps -a查看镜像docker imagescrictl images查看容器日志docker logscrictl logs进入容器docker exec -it shcrictl exec -it sh查看状态详情docker inspectcrictl inspect拉取镜像docker pullcrictl pull删除容器docker rmcrictl rm有一个容易踩的坑ctr是 containerd 自带的 CLI它直接操作 containerd 的本地对象不走 Kubernetes 的 CRI 元数据层。用ctr能看到镜像但在 K8s 场景下不该用ctr来停容器或删镜像因为 kubelet 通过 CRI 维护的 Pod 状态不会感知这些操作。简单说凡是和管理集群负载相关的操作统一用crictl。ctr一般在离线导入镜像、查看 containerd 本身状态时使用。6. 常见故障排查与运维经验6.1 节点 NotReady 排查思路这是我被问过最多的问题。节点 NotReady 的排查路径我一般按下面顺序走第一看 node 的 Condition。kubectl describe node k8s-node1会显示 Reason如果是KubeletNotReady继续下一步。第二看 kubelet 日志。journalctl -u kubelet -f是主要手段常见的报错包括网络插件未就绪、镜像拉取失败、证书过期。第三看 Calico 组件状态。kubectl get pods -n kube-system | grep calico如果是 CrashLoopBackOffkubectl logs -n kube-system calico-node-xxxxx看具体报错。如果你发现所有节点上面有多个 containerD 报错最直接的方法是crictl ps -a看有没有一直重启的容器。有的话拿到容器 ID看日志八成问题马上浮出水面。有一类高发问题节点的/etc/resolv.conf是系统管理的里面的 DNS 指向了云厂商的内网域名CoreDNS 启动没问题但 Pod 解析集群内部 Service 失败。这个跟 NotReady 本身无关但也是新集群最常见的“假健康”问题建议把 CoreDNS 配置里 forward 的地址改成公网 DNS 或你自己规划的内部 DNS。6.2 cgroup 驱动与运行时通信问题kubelet 启动失败并且日志提示failed to validate kubelet flags: cgroup-driver is set to systemd but cgroup driver of runtime is cgroupfs这就提示 cgroup 驱动不匹配。解决办法就是修改/etc/containerd/config.toml里的SystemdCgroup true重启 containerd再重启 kubelet。注意要先把老的 stop 掉防止新配置还没生效时又被旧进程干扰。还有一个全天候可能出现的通信问题的报错kubelet: failed to connect to containerd这代表 kubelet 到 containerd 的 socket 没通。常见原因是 containerd 没起来或者 socket 路径不一致。默认路径是/run/containerd/containerd.sockkubelet 如果有特殊参数改过--container-runtime-endpoint要把两者对齐。我见过有人手动装了 containerd但没启动 containerd 服务就急着 initkubelet 一路报错这种情况直接systemctl start containerd就解决。6.3 证书、token 与集群重置速查Kubernetes 控制平面组件的证书默认有效期一年。集群搭建完你大概率一年后才会遇到证书过期问题。检查剩余时长kubeadm certs check-expiration如果快过期了执行kubeadm certs renew all然后重启相关容器组件不需要重建集群。这个操作我建议记在运维笔记里。token 过期导致 worker 节点无法加入也是高频事件。新集群如果当天没全部 join 完第二天再操作就报 token 过期。解决办法就是kubeadm token create --print-join-command重新生成。如果整个集群配置坏了需要推倒重来按这个顺序清理kubeadm reset -f rm -rf /etc/cni/net.d rm -rf /var/lib/cni/ systemctl restart containerd注意kubeadm reset不清理 CNI 配置如果不删掉/etc/cni/net.d重新部署 CNI 时会因为残留的 bridge 配置起不来这是个非常隐蔽的坑。以下几类问题我做了个速查表直接对着查现象原因解决方式init 卡住且有多个镜像等待镜像仓库访问慢提前kubeadm config images pull校验节点持续 NotReadyCNI 没部署或防火墙挡端口部署 Calico/Flannel检查 6443、10250、4789 端口Pod 一直 ContainerCreating镜像拉取失败或者存储不可用crictl images检查镜像crictl inspect看事件CoreDNS PendingTaint 或 CNI 没就绪确认网络插件运行必要时手动容忍调度kubectl 命令提示连接 refusedkubeconfig 没配好检查.kube/config和 apiserver 地址6.4 遗留问题与实操心得最后分享几条不算“故障”但能避免折腾的经验。第一在正式部署之前先在每台节点上手动拉取 pause 镜像哪怕只是crictl pull registry.aliyuncs.com/google_containers/pause:3.9也能提前发现仓库访问异常而不是等到 kubeadm init 时看到一堆 timeout 才紧张。第二如果是基于离线包安装组件Kubernetes 版本、crictl 版本、containerd 版本、Calico 版本这四者之间尽量选择和官方兼容矩阵里都验证过的组合不要全选最新。我选 v1.28.2 配 containerd 1.7.7、crictl v1.28.0、Calico v3.27 就是验证过的组合。第三如果你后面打算在这套集群上跑 Doris 这类需要本地持久化的大数据组件先想清楚存储方案本地目录用 hostPath 还是用 CSI这个题不小但集群网络通了只是开始存储才是大数据上云的第一堵墙。总结起来一句话Kubernetes 集群部署本身不是玄学细节决定成败。所有坑都在那些你觉得“无所谓”的小配置上。本文这一套流程我在不同机器上重复部署过多次每一步都写清楚了目的和检查方式你可以放心按顺序执行。如果你在部署过程中遇到我列表里没有覆盖的报错建议先按 kubelet 日志、containerd 日志、CNI 日志三步法去查绝大多数问题都藏在这三处日志里。