
1. 部署前的设计思路为什么是 v1.28.2 containerd先说结论Kubernetes 从 1.24 版本开始正式移除了 dockershim也就是说从那一刻起你在集群里再用 Docker 作为运行时就必须额外套一层 cri-dockerd折腾不说还有额外损耗。v1.28.2 这个版本是 2023 年下半年的稳定补丁版本社区里大量生产环境踩过坑之后已经把问题收敛得差不多了我自己实际部署下来从 kubeadm init 到工作节点全部就绪整个过程比早期版本顺滑太多。选 containerd 而不是别的主要是三方面考虑。第一是性能containerd 本身就是为容器运行时设计的镜像解压、分层管理、Cgroup 资源隔离这些底层能力做得非常扎实CRI 插件直接内置不需要额外的适配层。第二是稳定性Kubernetes 官方在移除了 dockershim 之后containerd 事实上已经成为云原生社区默认的运行时事实标准所有 kubelet 的日志、监控、排障工具链都围绕它做适配。第三是资源占用同样一个 Pod 跑起来containerd 的内存占用比 Docker 要低不少对于节点配置有限的测试环境来说尤其明显。网络插件方面我选的 Calico为什么不用 FlannelFlannel 的 VXLAN 模式简单是简单但到了生产环境做 NetworkPolicy 的时候就有点力不从心。Calico 用的是纯三层 BGP 路由方案它直接在宿主机上通过路由表转发数据包性能上比 VXLAN 封装好一个量级而且天然支持 NetworkPolicy。这一点在后面的集群安全策略里会非常有用。当然如果你只是想在笔记本上跑个测试环境Flannel 也完全够用我自己的建议是测试环境用 Flannel 图省事正经学习或者接近生产的部署直接用 Calico。关于 v1.28 这个版本有几个细节值得提一下。首先是 kubelet 默认启用了 Node 优雅关闭和 Pod 就绪门控相关的特性这意味着节点维护时的 Pod 驱逐行为会比以前更智能。其次是证书相关的改进kubeadm 管理的证书有效期默认是一年但是 Kubernetes 1.28 已经支持了证书轮换和手动续期的更好流程部署完成后不需要像老版本那样手动写好多 cron 任务去处理证书过期的风险。还有一点v1.28 对 ipvs 模式下的 kube-proxy 做了不少修复我在后面的配置里会专门提。部署目标很明确三台服务器1 个 master 2 个 worker操作系统用 Ubuntu 22.04 LTSKubernetes 用 v1.28.2运行时用 containerd网络插件用 Calico。这套组合是我在实际项目里反复验证过的最稳定的搭配下面的章节我会按部署顺序逐步拆解每一步都写清楚当初为什么这么做以及踩过的坑。2. 环境准备与 Containerd 配置的关键细节2.1 系统基础配置在装任何 Kubernetes 组件之前有一堆系统层面的配置必须提前做好。这些看似琐碎的步骤其实决定了集群能不能稳定运行尤其是内核参数和 Cgroup 驱动的匹配问题一旦弄错后面 Pod 老是报 CrashLoopBackOff。先把三台机器统一配置好操作系统Ubuntu 22.04 LTS64 位配置建议master 节点 4C8Gworker 节点 2C4G最低要求 2C2G但会比较紧张磁盘建议独立数据盘挂载到 /var/lib/containerd避免系统盘被镜像和容器日志占满# 所有节点执行 sudo apt update sudo apt upgrade -y # 装依赖工具 sudo apt install -y curl wget apt-transport-https ca-certificates gnupg lsb-release vim net-tools # 关闭 swap sudo swapoff -a sudo sed -i / swap / s/^/#/ /etc/fstab # 加载内核模块 cat EOF | sudo tee /etc/modules-load.d/k8s.conf overlay br_netfilter EOF sudo modprobe overlay sudo modprobe br_netfilter # 配置内核参数 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这里面的关键点是 net.ipv4.ip_forward这个参数不开的话Pod 的出网流量直接不通。我见过有人在这里漏掉之后排查了半天最后发现是 IP 转发没开启。另外一个容易忽略的是 /etc/hosts 的主机名解析三台机器的 IP 和主机名必须提前配好否则 kubeadm init 的时候会提示 kubelet 无法解析 master 地址报错信息还特别隐晦。2.2 配置 containerd 的 config.toml从 Docker 时代走过来的朋友容易有个惯性思维觉得 containerd 装完就能直接用。实际上 containerd 默认的 config.toml 对于 Kubernetes 场景来说有两个地方必须改不改的话 kubelet 根本起不来或者 Pod 疯狂重启。生成默认配置文件containerd config default | sudo tee /etc/containerd/config.toml然后修改两个核心项。第一个是 SystemdCgroup第二个是 sandbox_image。[plugins.io.containerd.grpc.v1.cri] # 这是关键让 containerd 使用 systemd 作为 cgroup 驱动 systemd_cgroup true [plugins.io.containerd.grpc.v1.cri] # 这是关键sandbox 镜像 sandbox_image registry.k8s.io/pause:3.9为什么 systemd_cgroup true 这么重要因为 kubelet 默认使用 systemd 作为 Cgroup 驱动而 containerd 如果还停留在 cgroupfs两者就会不匹配。它们在内核里管理 Cgroup 的方式不同会出现资源统计错乱的问题具体表现就是 Pod 在创建之后立刻被 OOM Kill或者在 kubelet 日志里不断出现 failed to get cgroup stats。简单粗暴的理解就是kubelet 用 systemd 管 Cgroupcontainerd 就必须跟着用 systemd保持一致。sandbox_image 这个字段我踩过很深的坑。Kubernetes 每个 Pod 都会先启动一个 Pause 容器来共享网络命名空间这个镜像的版本如果和你的 Kubernetes 版本不匹配新建 Pod 就会一直处于 ContainerCreating 状态crictl ps 能看到 pause 容器但就是起不来。v1.28.2 对应的标准版本是 pause:3.9有些朋友用的是 pause:3.6 或者默认镜像地址拉不下来的情况都会遇到这种诡异的问题。国内环境的镜像地址问题也得说一下。不管你用阿里云还是其他镜像加速器registry.k8s.io 在国内大部分环境直接拉取非常慢。我习惯的做法是先用代理机器把需要的镜像 docker pull 下来再 docker save 成 tar 包传进内网或者直接修改 containerd 配置里的镜像仓库地址。注意这跟网上说的“拉取不了就去改 hosts”是两回事最好的方式是提前把 kube-apiserver、kube-controller-manager、kube-scheduler、kube-proxy、pause、coredns、calico、etcd 这些镜像全部准备好换个 registry 前缀导入进去后面就都顺了。2.3 用 crictl 测试 containerd 是否就绪配置完成后重启 containerd然后可以用 crictl 测试一下。crictl 是专门为 Kubernetes CRI 设计的命令行工具平时在你调试 Pod 的时候非常有用。sudo systemctl restart containerd # 查看 containerd 是否正常 sudo systemctl status containerd # 如果没装 crictl设置一个 alias 也行 cat EOF | sudo tee /etc/crictl.yaml runtime-endpoint: unix:///run/containerd/containerd.sock image-endpoint: unix:///run/containerd/containerd.sock timeout: 10 debug: false EOF # 验证一下 CRI 通信是否正常 sudo crictl versioncrictl version 如果能正常输出包含 RuntimeName 和 RuntimeVersion 的信息就说明 containerd 已经准备好了。这一步相当于考试前的模拟考我在实际部署中每次都先确认这一步再往后走。这里再多说一句containerd 有一个好用的命令体系crictl ps、crictl images、crictl logs这几个命令在排障时是主力。习惯了 docker ps 的人一开始可能觉得有点局限但实际上它的输出更贴近 Kubernetes 的 Pod 概念尤其是通过 label 去定位容器的时候效率很高。后续章节的排障部分我会专门写一些 crictl 的组合用法。3. kubeadm 初始化集群的完整实操3.1 安装 kubeadm、kubelet、kubectl系统组件配置好之后就开始装 Kubernetes 的核心组件。建议先把 apt 源换成阿里云的 kubernetes 镜像源不然默认的 packages.cloud.google.com 在国内基本连不上。# 安装 GPG 密钥和 apt 源Ubuntu 22.04 curl -fsSL https://mirrors.aliyun.com/kubernetes/apt/doc/apt-key.gpg | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg cat EOF | sudo tee /etc/apt/sources.list.d/kubernetes.list deb [signed-by/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://mirrors.aliyun.com/kubernetes/apt/ kubernetes-xenial main EOF sudo apt update # 查看可用的版本这里直接指定 1.28.2 sudo apt-cache madison kubelet # 安装指定版本三台机器都执行 sudo apt install -y kubelet1.28.2-00 kubeadm1.28.2-00 kubectl1.28.2-00 # 锁定版本防止意外升级 sudo apt-mark hold kubelet kubeadm kubectl这里有个值得注意的点apt 源里的 kubernetes-xenial 只是一个代号它其实对应的是所有版本的 Kubernetes 包仓库不用理会 xenial 这个名字。另外 kubelet 和 kubeadm 的版本必须完全一致不一致的话 kubeadm init 会直接报错说发现版本不匹配还没到部署那一步就卡住了。对于多节点部署kubeadm 只负责初始化 master 节点然后生成一条 join 命令给 worker 节点用。kubectl 的配置文件admin.conf在 master 节点上生成worker 节点不需要安装 kubectl但建议也装一下方便调试只是不用配置 kubeconfig。3.2 kubeadm init 参数详解初始化的核心命令是这个我把每一步的参数拆开说清楚如果你需要根据集群规划调整至少知道每个参数在干什么sudo kubeadm init \ --kubernetes-versionv1.28.2 \ --image-repositoryregistry.aliyuncs.com/google_containers \ --apiserver-advertise-address192.168.31.101 \ --pod-network-cidr10.244.0.0/16 \ --service-cidr10.96.0.0/16 \ --control-plane-endpoint192.168.31.101逐项解释一下。--image-repository 是镜像仓库地址国内环境直接用阿里云的我之前试过用 registry.cn-hangzhou.aliyuncs.com 下面的自定义仓库其实也可以但阿里云 maintain 的这个 google_containers 镜像同步得最及时版本最全。--apiserver-advertise-address 是 master 节点的内网 IP。如果你的机器有多块网卡必须显式指定否则 kubeadm 可能会选错网卡导致 apiserver 监听到错误地址worker 节点怎么都连不上。--pod-network-cidr 是 Pod 网络的 CIDR这个必须和后面安装的网络插件对应的配置保持一致。我这里选的 10.244.0.0/16 是 Flannel 的默认网段但如果你用 CalicoCalico 默认通常是 192.168.0.0/16。我当时用 Calico 的时候这里没有统一为 Calico 的默认网段导致后面部分 Pod 一直拿不到 IP。所以这个值一定跟网络插件对齐不然金丝雀发布、跨节点访问都会出问题。--control-plane-endpoint 是控制平面的高可用地址。单节点部署的时候直接填 master IP 就行生产环境做高可用的时候是一个 VIP 或者域名。kubeadm 会把这个地址写进证书的 SAN所以不要随便改改了就签名不匹配。执行完成后kubeadm 会输出一段提示包括mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config这三行是用来配置 kubectl 的复制完你就可以用 kubectl get nodes 看到 master 节点处于 NotReady 状态。这个状态是正常的因为还没有装网络插件。不用慌继续往后走。3.3 安装 Calico 网络插件网络插件是整个集群连通性的开关。我在前面反复强调要在 init 时把 pod-network-cidr 和网络插件默认网段对齐这里就是起作用的时候。Calico 建议用 Operator 安装kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.2/manifests/tigera-operator.yaml然后写一个自定义资源文件apiVersion: operator.tigera.io/v1 kind: Installation metadata: name: default spec: calicoNetwork: ipPool: cidr: 10.244.0.0/16用 kubectl apply 应用之后就等它调度。Calico 的运行状态可以通过以下命令观察kubectl get pods -n calico-system -wCalico 的组件比较多calico-typha、calico-node、calico-kube-controllers 这些你看到它们全部变成 Running 状态再回过头看节点kubectl get nodes这时候节点状态应该会从 NotReady 变成 Ready。整个集群的骨架就算搭起来了。3.4 worker 节点加入集群worker 节点加入集群非常简单就是把 kubeadm init 完成时输出的 join 命令复制到 worker 节点上执行。但实际部署中往往你已经把终端关掉了这时候可以重新生成 join token# 在 master 节点上执行 kubeadm token create --print-join-command这条命令会输出一条新的包含 token 和 ca-cert-hash 的 kubeadm join 命令复制到 worker 节点执行即可。整个过程 kubelet 会自动启动然后向 apiserver 注册节点。这里有一个容易犯的错worker 节点也必须有刚才部署好的内核模块和内核参数以及配置正确的 containerd。如果你在 worker 节点上看到 Node 一直处于 NotReady查看日志journalctl -u kubelet -f如果出现 failed to register node ... 或者 cgroup driver mismatch 相关的字眼基本就是 containerd 的配置没有与 kubelet 对齐。回到第 2 章把 worker 节点的 SystemdCgroup 重新确认一遍。还有一点join 的时候如果提示 token is invalid那是因为 token 默认只有 24 小时有效期重新生成一条就行。证书 hash 也得注意更换 token 时 hash 通常不变但如果控制平面证书轮换过hash 会变重新用 --print-join-command 生成最保险。4. 集群验证与 Nginx 部署测试4.1 关键组件状态检查集群初始化完成节点 Ready 之后不要急着部署业务。先把基础组件全部检查一遍就像做完一台手术先看心电监护再拔管。# 查看节点状态 kubectl get nodes -o wide # 查看所有系统组件 Pod 状态 kubectl get pods -n kube-system -o wide # 查看核心组件是否健康 kubectl get --raw/healthz健康的集群应该有这样几个特征kube-apiserver、kube-controller-manager、kube-scheduler、etcd 全部运行在 master 节点上并且是 Running 状态kube-proxy 在每个节点都运行coredns 有两个副本或至少一个可用Calico 的 pod 遍布所有节点。这里尤其要关注 coredns。很多网络问题最初的表现就是 DNS 解析失败而 DNS 失败往往是因为 coredns 没有调度起来。我之前遇到过一个情况是 coredns 一直 CrashLoopBackOff查看日志发现是 configmap 里 upstream 配置不正确改了一下 CoreDNS 的 Corefile 就好了。遇到这种问题先别慌用 crictl logs 看一下崩溃容器的真实报错。4.2 用 Nginx 验证业务流量和 DNS部署一个测试应用来验证集群可用性我习惯用 Nginx 来做这个测试对象因为它的镜像小、启动快、网络配置也简单。# 创建一个最简单的 Nginx Deployment cat EOF | kubectl apply -f - apiVersion: apps/v1 kind: Deployment metadata: name: nginx-test spec: replicas: 3 selector: matchLabels: app: nginx-test template: metadata: labels: app: nginx-test spec: containers: - name: nginx image: nginx:latest ports: - containerPort: 80 EOF # 创建一个 Service 来暴露 cat EOF | kubectl apply -f - apiVersion: v1 kind: Service metadata: name: nginx-test-svc spec: selector: app: nginx-test ports: - port: 80 targetPort: 80 nodePort: 30080 type: NodePort EOFDeployment 创建之后查看 Pod 的状态kubectl get pods -o wide如果三个 Pod 都是 Running用 kubectl exec 进入任意一个 Pod 测试网络kubectl exec -it nginx-test-xxxx-xxxx -- curl http://nginx-test-svc.default.svc.cluster.local能返回 Nginx 的欢迎页说明集群内部 DNS 解析、Service 转发、跨节点容器网络全部正常。然后在外部直接用 node IP NodePort 访问curl http://192.168.31.101:30080 curl http://192.168.31.102:30080 curl http://192.168.31.103:30080这三个 IP 都要能返回内容才说明每个节点的 kube-proxy 和网络打通了。这个 Nginx 测试集其实比一些 fancy 的监控工具更直观。部署 nginx 的这个过程顺便把 containerd 的常用命令用上了。比如说你想看当前节点上拉取了哪些镜像用 crictl images 看想看某个 Pod 对应的容器日志先 kubectl get pod -o jsonpath 拿到 containerID再 crictl logs 即可。不需要再依赖 docker ps 之类的命令。4.3 containerd 常见运维命令速查用了一段时间 containerd 之后我整理了一些高频命令这里分享给大家。这些命令在没有 Docker 的环境中就是你的眼睛和手。场景命令查看本机所有容器sudo crictl ps -a查看运行中的容器sudo crictl ps按 Pod 名过滤容器sudo crictl ps -p查看 Pod 的容器列表sudo crictl pods查看容器日志sudo crictl logs查看容器标准输出sudo crictl logs -f查看本机镜像sudo crictl images拉取镜像sudo crictl pull删除镜像sudo crictl rmi进入命名空间断点exec 等价sudo crictl exec -it shcrictl 支持 -p 参数来按 Pod 筛选容器这在排查某个 Pod 里面到底哪个容器崩溃的时候特别高效。另外kubectl logs 和 crictl logs 的区别在于kubectl logs 会自动根据 Pod 里的容器名去定位你不需要手动找容器 ID而 crictl 适合在节点层面做系统级排障。5. 部署过程中的常见问题与排查套路5.1 初学者最容易遇到的几个坑在实际部署里很多问题都有比较固定的报错模式我把这段时间遇到的和同行反馈过的典型问题整理成一张速查表方便直接对照定位。现象可能原因排查命令 / 修复方法节点一直 NotReady网络插件未安装或 CNI 配置文件缺失kubectl get events --field-selector typeWarning检查 /etc/cni/net.dPod 一直 ContainerCreatingsandbox_image 版本不对或 pause 镜像拉不下来sudo crictl ps -a 看 pause 容器状态手动拉取 pause:3.9kubelet 一直 CrashLoopBackOff系统内核参数未生效或者 kubelet 默认配置被改过journalctl -u kubelet -n 50apiserver 健康检查失败kube-scheduler / controller 配置里访问了错误地址kubectl get --raw/healthz检查 apiserver 日志worker join 时证书校验失败控制平面证书过期或者 hash 不匹配重新生成 kubeadm token create --print-join-commandcalico-typha 资源不够调度节点资源不足或 NodeSelector 未匹配kubectl describe pod -n calico-systemDNS 解析失败coredns Pod 起不来查看 coredns 日志检查 Corefile 里 forward 配置3306 端口映射不通Service 类型没选对或 NodePort 与宿主机冲突kubectl get svc确认 nodePort 没有被占用5.2 排查思路从报错日志反推我这里想强调一个很重要的排查思路先看 kubelet 日志再看各个容器的日志最后才是各种网络排查工具。很多新手一看到 Pod 报错第一反应是去 ping 宿主机或者 dig 域名其实很多时候问题在更底层。举一个真实场景。我遇到过 ClusterIP 的 Service 无法访问curl 的时候 response 一直是 timeout。这个问题的风格就很典型表面上是网络不通但实际上是因为 ipvs 模式下 kube-proxy 没启用 ipvs 相关内核模块。排查过程是这样的# 先确认 kube-proxy 是否用了 ipvs 模式 kubectl get configmap -n kube-system kube-proxy -o yaml | grep mode # 再检查节点 IPVS 模块 lsmod | grep ip_vs如果确认是 ipvs 模式但模块没加载加载对应内核模块之后清理未生效模块就行。这种问题的特点是表面看起来是网络不通实则是内核功能缺失。所以我的习惯是遇到网络问题永远是先查 kubelet 和 kube-proxy 日志再查 conntrack 和 iptables 规则最后才看业务容器本身。5.3 集群升级与证书续期的经验之谈v1.28.2 部署完成后证书有效期是一年但这个时间限制是发给 apiserver 等组件的。你一年之后需要做的是 kubeadm 自动续期而不是重启集群。kubeadm 1.28 已经支持了自动续期前提是 kubelet 在运行中。# 检查当前证书有效期 kubeadm certs check-expiration # 手动续期所有证书 kubeadm certs renew all续期完成后需要重启 kubelet 才能生效sudo systemctl restart kubelet关于集群升级v1.28.2 到后续的 1.28.x 小版本升级路径很清晰但要记住一个原则一次只能升级一个小版本。比如从 1.28.2 升级到 1.28.3直接 kubeadm upgrade apply v1.28.3 就行了。跨大版本比如 1.28 到 1.29需要先去读官方的升级说明确认 API 变更和弃用项再做完整的备份和回滚方案。真实生产环境中我见过太多人因为跨版本太多导致自定义控制器和 CRD 出问题。另外etcd 的备份是很多人忽略的一环。就算你的集群只是测试环境也建议在 kubeadm 部署完成后立刻做一次 etcd 快照ETCDCTL_API3 etcdctl --endpointshttps://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 /root/etcd-snapshot-$(date %Y%m%d).db后期做恢复的时候直接通过 etcd 快照恢复。这个动作在你的集群控制面崩溃时能救命。6. 一些实操心法与后续扩展整个集群部署到这里就结束了但作为一篇实战手册我还想分享几个额外的经验。首先强烈建议在 master 节点把 kubectl 命令补全做好。执行 echo source (kubectl completion bash) ~/.bashrc 之后输命令的速度会快很多。尤其是面对长资源名和复杂 label 的时候Tab 补全的价值超过你的想象。其次可以考虑在集群里装一个轻量的 Kubernetes Dashboard。虽然很多人说 Dashboard 安全性有问题但对于学习和内部环境来说可视化的价值还是很大的。部署方式很简单官方仓库拉一下 yaml 然后改 Service 类型即可。还有一个很多人容易忽略的点给节点打上 label 和污染。比如给 master 节点加上 node-role.kubernetes.io/control-planetrue: NoSchedule 的 Taint防止业务 Pod 调度到控制平面节点。测试环境常常放松这个限制但生产环境一定要在初始化之后就正确设置否则控制平面节点容易承载高负载业务apiserver 性能受影响。在优化方面建议把 kubelet 的日志轮转开起来。默认情况下 kubelet 日志在 journald 里单文件可以无限增长时间长了会撑爆 systemd 的 /run/log。修改 /etc/systemd/journald.conf 里的 SystemMaxUse1G 和 RuntimeMaxUse1G 可以有效控制日志体积这个问题我在节点磁盘报警的时候吃过不少亏。最后提一个后续还能玩的方向在这个集群上再部署一套 kube-prometheus。v1.28 的 Metrics API 已经成熟Prometheus 采集集群指标加上 Grafana 的 Kubernetes 官方 Dashboard能让你对集群资源使用、Pod 调度分布、网络流量都有非常直观的认识。部署的方法也不复杂git clone prometheus-operator/kube-prometheus然后 kubectl apply --server-side -f manifests/setup 和 kubectl apply -f manifests 就行。说实话我第一次从 Docker 切到 containerd 加纯 kubeadm 构建集群时也踩了无数坑。包装一下情绪就是刷了三天的 CI翻了一周的文档。但现在回头看这套 v1.28.2 的部署路径已经非常成熟和顺畅了。最重要的体会就是把每一步的原理弄清楚比背命令要管用得多。至少下次你再从零搭集群面对那些奇怪的报错时能一眼看到底层的逻辑。