Kubernetes 实战指南:从安装到弹性伸缩的容器编排实践

📅 发布时间:2026/10/17 6:42:18
Kubernetes 实战指南:从安装到弹性伸缩的容器编排实践
1. 先搞清楚K8s 在真实项目里到底解决什么问题聊 K8s 之前我想先说一段自己的真实经历。早些年我维护过一个微服务项目服务数量从几个快速膨胀到二十多个。每次发版都要手动登录服务器、拉镜像、停容器、再起容器遇到流量高峰还要半夜爬起来扩容。最痛苦的不是操作本身而是漏了一台机器这种低级错误——明明每个步骤都有文档但人总有走神的时候。后来我们把服务迁到 Kubernetes以下简称 K8s上发版变成了一个命令扩容变成了一个数字机器宕了业务自动恢复。那个阶段我才真正体会到K8s 的价值不在跑容器而在让几十上百个容器像一台机器一样被管理。所以这篇教程的思路也很明确不堆概念而是从一个使用者的角度告诉你 K8s 是什么、怎么装、怎么用、踩过哪些坑。适合刚接触容器编排的开发者、想给团队引入 K8s 的运维以及所有听过很多次但没真正跑通的朋友。K8s 本质上是一个开源的容器编排平台解决的核心问题可以概括成三句话自动调度容器该跑在哪台机器上、自动恢复容器挂了如何拉起新的、自动伸缩流量涨了如何扩展实例。它对外暴露的是一套资源描述方式——你想让系统变成什么样就声明什么样剩下的交给控制面去协调。为了避免一开始就被术语吓住我先把 K8s 的架构压缩成一个简单的图景控制面Control Plane集群的大脑负责决策。里面有 API 服务所有操作的入口、调度器决定 Pod 放哪台节点、控制器管理器确保实际状态向期望状态收敛以及存储集群状态的数据库。工作节点Worker Node集群的手脚真正跑容器的地方。每台节点上有容器运行时、负责执行指令的代理进程 kubelet以及负责网络规则的小组件。从这个图景出发后面所有的操作都能对上号你提交一份描述文件给 API 服务调度器决定在哪台节点上启动kubelet 收到指令后拉起容器控制器时刻检查现状 vs 期望发现偏差就纠正。理解了这条链路K8s 就没有那么玄乎了。2. 环境选型本地学习、单机生产、多节点集群该走哪条路动手之前先解决装在哪里的问题。很多新手一上来就跟着网上的教程装 kubeadm结果卡在初始化、网络插件、镜像拉取这些环节折腾两天还没跑起来一个 Pod信心全没了。这是很典型的选型错误导致的挫败。我的建议是分场景选择部署方式不要一套方案打天下。使用场景推荐方案优势注意点本地学习、快速体验Minikube一条命令启动单节点集群功能完整需要能运行虚拟机的环境消耗资源适中隔离环境练习多节点排障KindK8s in Docker用 Docker 模拟多节点启动快、易销毁适合学习不适合生产生产或长期使用kubeadm 部署官方推荐的生产级工具可控性强网络、证书、升级都要自己维护在已有基础设施上快速建集群各家云厂商的托管 K8s免运维控制面按量付费有厂商绑定倾向需要注意我不想做工具的广告但可以给一个很实在的选型建议如果你是第一次接触 K8s直接选 Minikube 或者 Kind别碰 kubeadm。原因很简单——本地学习的目标是尽快把核心对象跑通而不是在安装环节就耗尽耐心。等你掌握了 Pod、Deployment、Service 这些概念再回头用 kubeadm 搭一个正经的多节点集群那时候你对每个组件的作用已经有直观感受排障也更有方向感。另外很多同学会纠结K8s 是不是必须要 Docker。这里要澄清一个长期存在的误解K8s 要求的是符合容器运行时接口CRI的运行时Docker 只是其中一种。现在更多团队默认使用 containerd 这类轻量运行时直接由 kubelet 调用。用 Docker 作为开发环境的运行时完全没有问题但不要把Docker K8s 唯一的运行时当成铁律。以 kubeadm 为例多节点集群的搭建大致是这么几步准备两台以上 Linux 服务器我习惯用某 Linux 发行版内核版本不要太老配置好主机名和/etc/hosts。在所有节点上安装容器运行时并启用内核网络模块。这部分最常出的问题有两个一个是忘记加载模块导致网络插件起不来另一个是运行时和 kubelet 的 cgroup 驱动不一致后面会有专门的排障段落。在控制面节点执行kubeadm init初始化集群。初始化成功后kubectl 的配置文件会被生成到$HOME/.kube/config这一步很多人忘了设置导致执行 kubectl 命令报 connection refused。安装网络插件。初始化完成后集群处于 NotReady 状态原因是节点之间没有网络方案。用kubectl apply安装一份网络插件清单即可。将工作节点通过kubeadm join加入集群带上初始化输出的 token 和证书哈希。我第一次搭集群时在第四步卡了将近三个小时——Pod 一直处于 ContainerCreating事件里反复出现网络错误。后来查了很久才发现是安装网络插件前忘了确认节点之间的网络连通性防火墙把某些端口挡了。所以这里强烈建议集群节点之间的端口放行一定要先确认清楚尤其是控制面 API 端口和节点间通信端口。3. 核心资源对象Pod、Deployment、Service 的职责边界很多人学 K8s 的第一步就是背概念但背完之后依然不会写配置。我想换个讲法先把资源对象当成你向集群声明期望状态的不同粒度再看它们之间怎么配合。3.1 Pod最小的逻辑主机Pod 是 K8s 中最小的调度和运行单位。一个 Pod 里可以放一个容器也可以放多个紧密协作的容器这些容器共享同一个网络命名空间和存储卷。关于 Pod我最想强调的一点是不要把 Pod 当成跑单个容器的普通进程来看而要把它当成一台逻辑主机。容器只是这台主机里的进程而主机的生命周期和里面的进程是绑定在一起的。Pod 里面的容器会被调度到同一台物理节点上所以它们之间用 localhost 就能通信。业务上需要同生共死的容器比如日志采集 sidecar 和应用主容器就应该放在一个 Pod 里。但在实际生产中你很少直接创建 Pod原因很简单Pod 一旦被删除或被重新调度名字和 IP 都会变化。你需要一种能保持稳定身份、能扩容缩容、能保证存活数量的机制这就是 Deployment。3.2 Deployment声明式的副本管理Deployment 是你在生产环境最常打交道的一个对象。它管的事情很清晰保证副本数你声明replicas: 3它就保证系统里始终有 3 个 Pod 在运行。滚动更新修改镜像版本后它会逐个替换旧 Pod而不是一次性全部杀掉避免服务中断。回滚如果新版本有问题可以通过kubectl rollout undo回退到上一个版本。写 Deployment 的 yaml 时有几个字段我建议每个新手都认真理解apiVersion: apps/v1 kind: Deployment metadata: name: demo-web labels: app: demo-web spec: replicas: 3 selector: matchLabels: app: demo-web template: metadata: labels: app: demo-web spec: containers: - name: web image: nginx:1.26 ports: - containerPort: 80 resources: requests: cpu: 100m memory: 128Mi limits: cpu: 500m memory: 256Miselector.matchLabels这个字段很关键它决定了这个 Deployment 管理哪些 Pod。一旦 Pod 的标签不匹配Deployment 就会认为这些 Pod 不是自己管辖的从而创建新的 Pod 去补足数量。一个很常见的误操作是手动修改了 Pod 的标签导致 Deployment 反复重建 Pod——这在生产环境会造成不小的混乱。3.3 Service把变化的 Pod IP 变成稳定的访问入口Deployment 保证了 Pod 的数量和状态但 Pod 的 IP 是随时会变的。Service 就是那个扛把子——它提供一组 Pod 的稳定访问入口。Service 的工作机制可以简单理解成给一组 Pod 打上标签选择器然后分配一个稳定的虚拟 IPClusterIP。访问这个虚拟 IP 时流量会被转发到后面实际的 Pod 上。Service 有几种常见类型ClusterIP默认类型只在集群内部可以访问适用于服务间调用。NodePort在每个节点的固定端口上暴露服务外部通过节点IP:NodePort访问。LoadBalancer在云环境里通过云负载均衡器暴露服务是生产环境比较直接的方式。从使用感受上说Deployment 管数量Service 管访问两者配合起来你就能在完全不知道 Pod 实际 IP 的情况下稳定地访问到一组 Pod。3.4 ConfigMap 与 Secret让环境配置和敏感信息脱离镜像还有一个很容易被忽略但非常实用的对象是 ConfigMap 和 Secret。它们解决的核心问题是同一个镜像要在不同环境开发、测试、生产里用不同的配置总不能把配置写死在镜像里。ConfigMap 存的是明文配置Secret 存的是敏感信息比如数据库密码、密钥。两者都可以挂载成文件或者注入成环境变量。我见过不少项目把配置写进镜像每次改配置都要重新构建镜像并重新发布这其实完全没有必要——把配置存成 ConfigMap一个kubectl set或直接修改 Deployment 引用版本就能完成配置更新。4. 从零部署一个带配置和存储的 Web 服务概念讲得再多不亲手跑一个完整应用等于白学。这一节我就用最常见的场景——一个 web 服务加上配置文件和持久化存储——带你走一遍完整的部署链路。4.1 准备好镜像和配置假设我有一个简单的 web 应用它需要在启动时读取一个配置文件并且要把日志写到磁盘上以便后续排查问题。我先准备两个文件demo-config.yamlConfigMap用来存放配置文件内容。demo-deployment.yamlDeployment引用这个 ConfigMap并声明一个持久化存储卷。先来看 ConfigMap 的写法apiVersion: v1 kind: ConfigMap metadata: name: demo-config data: app-config.yml: | server: port: 8080 log: level: info这里把app-config.yml整个文件内容塞进 ConfigMap 的 data 字段里后续挂载到容器里就会生成一个以此为文件名的文件。4.2 部署的体会先从 kubectl 命令说起很多教程一上来就是让你写一大段 yaml然后 apply却不说清 kubectl 的基本用法。实际上 kubectl 的常用命令非常集中我平时用得最多的就这几条kubectl apply -f file创建或更新资源。kubectl get resource查看资源列表。kubectl describe resource name查看资源的详细事件和状态。kubectl logs pod-name查看 Pod 日志。kubectl exec -it pod-name -- command进入 Pod 内执行命令。kubectl delete -f file删除资源。刚开始用的时候我建议遇到问题先get看状态再describe看事件最后用logs看应用日志。这个排查顺序后面会专门展开。4.3 挂载配置和存储的完整 yaml接下来是核心部分。我把一个完整但不过度复杂的 Deployment 写出来再逐段解释。apiVersion: apps/v1 kind: Deployment metadata: name: demo-app namespace: default spec: replicas: 2 selector: matchLabels: app: demo-app template: metadata: labels: app: demo-app spec: containers: - name: demo-app image: registry.example.com/demo-app:v1.0.0 ports: - containerPort: 8080 env: - name: APP_ENV value: production volumeMounts: - name: config-volume mountPath: /etc/demo - name: log-volume mountPath: /var/log/demo resources: requests: cpu: 200m memory: 256Mi limits: cpu: 1000m memory: 512Mi volumes: - name: config-volume configMap: name: demo-config - name: log-volume emptyDir: {}我先说几个设计点这些是我在实际项目中总结出来的习惯不一定每个都绝对正确但至少踩过几次坑之后证明好用把配置文件和日志目录分开挂载。配置文件来自 ConfigMap日志目录用emptyDir。emptyDir表示卷的生命周期跟 Pod 一致Pod 删除后数据就没了。这里只是让你先跑通流程如果日志要长期保留应该换成持久卷PersistentVolume。资源 requests 和 limits 一定要写。不写的话调度器不知道你的应用需要多少资源线上如果某个节点被一个没限上限的容器打爆整个节点都会进入不稳定状态。我遇到过不止一次某个 Pod 把节点内存耗尽其余 Pod 被驱逐的事故根因就是没写 limits。端口映射写在containerPort这里它更像一个声明信息真正的访问入口是通过 Service 暴露。4.4 让外部能访问到创建 ServiceDeployment 只负责把 Pod 建起来外部的访问还需要 Service。这里我用 NodePort 类型来演示方便本地测试apiVersion: v1 kind: Service metadata: name: demo-service spec: type: NodePort selector: app: demo-app ports: - port: 8080 targetPort: 8080 nodePort: 30080这样配置之后通过任意节点的30080端口就能访问到 demo-app 了。selector里的app: demo-app要和 Deployment 里的 Pod 标签保持一致这是 Service 能找到后端 Pod 的关键。我整理一个常见的问题排查表格对照着看会更快定位现象可能原因排查方法Service 访问不通selector 和后端 Pod 标签不一致kubectl describe service看 Endpoints 是否有 IPEndpoints 为空后端的 Pod 没起来或标签不匹配kubectl get pods --show-labels核对标签Pod 一直 Pending节点资源不足或调度约束不满足kubectl describe pod看事件Pod 一直 CrashLoopBackOff启动命令或配置有问题kubectl logs查看应用日志镜像拉取失败镜像名写错或私有仓库未配置密钥kubectl describe pod的 Events 中会给出具体原因4.5 存储升级日志要持久化怎么办上面用了emptyDirPod 一删日志就没了。如果日志需要留底就要用到 PersistentVolumeClaimPVC和 StorageClass。在大多数集群里StorageClass 已经由管理员预先创建应用只需要声明 PVCapiVersion: v1 kind: PersistentVolumeClaim metadata: name: demo-log-pvc spec: accessModes: - ReadWriteOnce resources: requests: storage: 5Gi然后在 Deployment 的 volumes 和 volumeMounts 里换成 PVCvolumes: - name: log-volume persistentVolumeClaim: claimName: demo-log-pvc这样日志就会写到持久卷里Pod 无论怎么重建数据都能保留。这块的坑在于accessModes 要和存储类型匹配。很多本地测试环境只支持 ReadWriteOnce如果多个副本需要同时读写同一份数据就要用 ReadWriteMany但这个能力非常依赖底层存储实现要先确认再使用。5. 探针、滚动更新与优雅停机保障发布的稳定性学会了部署下一步就是让发布过程更稳。这一节讲三个直接影响线上稳定性的机制探针、滚动更新、优雅停机。5.1 探针别让流量打到没准备好的 Pod探针Probe是 K8s 用来判断容器是否健康的机制主要分三种存活探针livenessProbe探测容器是否还活着。如果失败kubelet 会杀掉容器并重启。就绪探针readinessProbe探测容器是否能够接收流量。如果失败该 Pod 会从 Service 的 Endpoints 中摘除不接收新流量。启动探针startupProbe给启动较慢的应用提供一个缓冲避免存活探针在启动阶段误杀。我见过一个很经典的事故某应用启动需要 15 秒但存活探针配置的initialDelaySeconds: 5、periodSeconds: 5导致容器才启动 5 秒就被判定不健康杀掉重启重启后再被杀陷入 CrashLoopBackOff。这其实是探针参数和业务实际启动时间不匹配造成的。合理的配置思路是先按经验估算启动时间然后用启动探针给足缓冲再用就绪探针决定何时接入流量。下面是一个示例livenessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 20 periodSeconds: 10 timeoutSeconds: 3 failureThreshold: 3 readinessProbe: httpGet: path: /ready port: 8080 initialDelaySeconds: 5 periodSeconds: 5 timeoutSeconds: 2 failureThreshold: 3 startupProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 5 periodSeconds: 5 failureThreshold: 30failureThreshold: 30配合每 5 秒一次能容忍最长 150 秒的启动时间对大多数应用来说已经足够。探针的路径一定要是应用真正能反映健康状态的接口而不是一个写死的 200 页面否则探针形同虚设。5.2 滚动更新让发版不中断业务Deployment 默认就支持滚动更新但很多人没有理解它的更新策略导致发布过程中出现短暂不可用。默认策略是RollingUpdate参数包括maxUnavailable和maxSurge。前者表示更新过程中最多允许有多少个 Pod 不可用后者表示最多允许超出期望副本数多少个 Pod。默认值是 25% 左右也就是说如果你的副本数是 4更新时会先拉起 1 个新 Pod等它就绪后再下线 1 个旧 Pod依次类推。我自己实践的推荐配置是strategy: type: RollingUpdate rollingUpdate: maxUnavailable: 1 maxSurge: 1对于副本数不多的小服务这个配置简单可控最多同时有 1 个旧 Pod 停掉和 1 个新 Pod 启动不会出现大规模不可用。副本数很大时用百分比扩张会更方便。需要注意滚动更新的前提是新 Pod 能正常变成 Ready。如果新版本镜像启动就崩溃Deployment 会一直卡在更新中不会把旧的全部替换掉。配合 ready 探针能大大提高发布安全性。5.3 优雅停机让请求不中断很多人在容器里直接忽略了 SIGTERM 信号的处理。K8s 删除 Pod 时会先发送 SIGTERM 信号然后等待宽限期默认 30 秒后再发送 SIGKILL 强制杀死。如果你的应用不处理 SIGTERM它可能直接退出导致正在处理的请求被中断。正确的做法是让应用监听 SIGTERM先停止接收新请求、处理完存量请求再退出。对于 Web 应用来说这通常意味着从负载均衡摘除、等待存活请求完成、最后关闭连接。K8s 还提供了terminationGracePeriodSeconds字段可以按业务需要调整宽限期长度。spec: terminationGracePeriodSeconds: 30如果业务里有长轮询或者比较慢的请求处理这个值可以适当调大。但调大不等于无脑调——如果 60 秒内还处理不完要么设计上有问题要么宽限期再长也只是推迟问题。6. 线上踩坑实录六个高频故障和排查思路这一节可能是全篇最实用的部分。我把自己在真实环境里踩过的坑、帮别人排查过的问题挑出六个高频场景按现象-排查-根因-规避的方式写出来。6.1 镜像拉取失败从 ImagePullBackOff 说起现象Pod 状态是ImagePullBackOff事件里显示拉取镜像失败。排查链路kubectl describe pod pod-name查看 Events。区分是镜像名不存在、私有仓库未认证还是网络原因。我在内网环境遇到过一种情况镜像在私有仓库里Pod 一直拉取失败原因是部署 Pod 所在的节点上没有配置仓库的登录凭证。解决方式是在命名空间里创建 Secret然后在 Deployment 的imagePullSecrets里引用spec: imagePullSecrets: - name: registry-secret另外如果用了latest标签要考虑拉取策略。默认的拉取策略是IfNotPresent即本地存在就不拉。但某些场景下你需要强制每次拉取可以改成Always。一个合理的习惯是开发测试用latest方便迭代生产环境固定版本号避免不可控的漂移。6.2 Pod 一直 Pending节点资源不够还是调度约束不对现象kubectl get pods看到 Pod 一直 Pending很长时间不变化。排查思路kubectl describe pod看 Events通常能看到0/3 nodes are available以及具体原因。常见原因包括节点 CPU/内存不足、污点Taint不匹配、节点选择器nodeSelector没有节点匹配、PVC 未绑定。一个让新手很困惑的场景是明明每个节点都有空闲资源Pod 还是 Pending。这时候要检查 CPU 和内存的 requests因为调度器判断的是 requests而不是实际使用量。如果你把 requests 设置得特别高比如给一个小工具设 2 核 4G节点上其实已经放不下这个规格的 Pod。这是 requests 设计不合理导致的调度失败。6.3 CrashLoopBackOff启动即崩溃的循环现象Pod 反复处于CrashLoopBackOff说明容器启动后崩溃然后被重启再次崩溃。这类问题要先分清是启动后立刻退出还是启动一段时间后 OOMKilled立刻退出多半是应用配置错误比如连不上数据库、读取配置文件失败。用kubectl logs --previous查看上一次崩溃的日志。OOMKilled容器内存超过 limits被内核杀掉。kubectl describe pod里会显示OOMKilled状态。这种情况要审视是不是 limits 设置过低或者应用存在内存泄漏。我第一次写 requests 和 limits 时把 limits 压得太紧结果应用在流量高峰直接被 OOM 杀掉一个健康的 Pod 完全没有抗压能力。后来我把内存 limits 调高并且加了就绪探针流量高峰才稳定下来。这里不是让你无限调高 limits而是说limits 应该基于压测数据来设置而不是拍脑袋。6.4 Service 有了但访问不了Endpoints 是排查的钥匙现象Service 存在Pod 也 Running但外部访问不通。排查顺序kubectl get endpoints看这个 Service 有没有关联到 Pod IP。如果 Endpoints 为空检查 Service 的 selector 与 Pod 的标签是否匹配。如果有 Endpoints但访问还是不通检查 NodePort 是否被防火墙挡了或者网络策略是否拦截。这里有一个很多人踩过的细节Service 的targetPort要和容器实际监听的端口一致。如果容器监听 8080而 Service 的 targetPort 写成了 80流量会被转发到一个不存在的地方Pod 虽然健康但请求全部失败。这类问题单看get不好发现一定要用describe或者实际请求去验证。6.5 更新之后 Pod 没变镜像拉取策略的默认陷阱现象我改了镜像 tagapply之后 Pod 并没有拉取新镜像。原因如果你没改镜像 tag比如一直用latest且镜像在节点本地已存在Pod 不会重建。Deployment 根本不知道镜像是否发生了变化。此时有两个选择显式把imagePullPolicy设为Always。或者修改镜像 tag 指向新版本触发滚动更新。我更推荐第二种因为固定 tag 会让版本可追溯出问题时能快速定位线上跑的是哪一版镜像。latest看起来方便但在团队协作中很容易造成我以为线上跑的是这个版本其实是另一个的尴尬。6.6 节点 NotReady别慌先看 kubelet现象kubectl get nodes显示某个节点 NotReady。排查链路登录到该节点检查 kubelet 是否运行正常。查看 kubelet 日志常见问题是证书过期、资源耗尽、容器运行时沟通失败。如果控制面正常可以考虑重启 kubelet 服务让节点重新注册。这里我不想给一个万能修复命令因为 NotReady 的根因差异很大。但我可以给一个非常重要的原则任何时候都不要直接kubectl delete node去解决 NotReady。这个操作会把节点从集群里删掉而节点上如果有状态型应用恢复成本会很高。正确的做法是排查根因恢复节点让 kubelet 重新上报状态。7. 按规模迭代从 10 个服务到 100 个服务配置思路的转变很多团队在 K8s 上跑通第一个服务之后会陷入能用就行的状态。但集群规模和业务复杂度上来之后很多早期图省事的做法会变成定时炸弹。7.1 命名空间的划分刚开始把所有服务丢在 default 命名空间20 个服务以内问题不大。服务多之后你会开始头疼这几个问题同名资源冲突。权限难以划分。日志和监控不好归类。我建议在服务数量变多的初期就按环境dev、prod或者按业务域order、payment、user划分命名空间。命名空间的隔离成本很低但它带来的组织收益很高。配合 RBAC基于角色的访问控制你还能做到不同团队只能操作自己的命名空间。7.2 标签是长期运维的地基标签Labels是一个被严重低估的功能。它本质上是资源的元数据但用好了可以非常灵活地选择和管理资源按环境envproduction、envtest按业务域apporder-service按版本versionv2.0.1一条kubectl get pods -l apporder-service,envproduction就能精确筛出生产环境订单服务的所有 Pod。配合换标签在 A/B 测试和流量灰度上也有发挥空间。7.3 从逐个管理到声明式治理在规模变大后我最推荐的一个习惯变化是不要让 kubectl 变成日常手工操作的首选。YAML 文件放进版本库用 CI/CD 管道执行 apply结合代码评审让每一次集群变更都可追踪。我见过不少团队用kubectl edit deployment在线改配置改完之后没有任何记录出问题完全不知道谁在什么时候改了什么。这是非常危险的。把配置当成代码来管理是 K8s 运维走向正规化的必由之路。7.4 成本与治理宁可不够用不要不敢动最后说一个有点反直觉的经验在资源治理上我犯过的最大的错误不是资源不够而是资源 requests 设置过高导致资源被白白浪费。举个例子一个服务实际只用了 256Mi 内存但 requests 写在 1Gi集群能调度的 Pod 数量就减少了一大截更多的计算资源被打上已分配的标记却没有被真正使用。正确做法是先用监控数据计算服务的真实用量。requests 设为略高于平时的峰值limits 设为应对突发流量的上限。定期复查用水平 Pod 伸缩HPA按 CPU 或自定义指标自动调节副本数而不是手动加副本。HPA 的配置不算复杂核心是定义目标指标和副本范围apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: demo-web-hpa spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: demo-web minReplicas: 2 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 60这段配置的含义是让 demo-web 的 CPU 平均使用率维持在 60% 左右副本数在 2 到 10 之间自动伸缩。HPA 让高峰期自动扩容、低谷期自动缩容变成现实但前提是 Pod 的 requests 有真实参考意义——如果 requests 明显高于实际HPA 看到的利用率会普遍偏低扩容策略也就不准了。最后再分享一点个人体会说实话从我在真实项目里维护 K8s 的体会来说这套系统最难的从来不是某个 yaml 怎么写而是你要从盯着单个容器的思维转换到管理一组容器的思维。你的重心不再是怎么启动某个进程而是描述清楚期望状态、定义好健康标准、设置合理资源边界然后让平台去收敛。每次 Pod 被重新调度、应用自动恢复、新版本平滑发布我都会有那么一点感慨——这种程序托底、人只做决策的方式才是 K8s 真正值钱的地方。建议你先在小集群里把本文提到的 Deployment、Service、ConfigMap、PVC、探针、HPA 逐个跑一遍遇到问题就按看事件、看日志、查 Endpoints的顺序去排查。等这些对象都跑顺了再回头看它在整个研发流程里的位置你会对容器编排这件事有完全不一样的理解。K8s 的功能远不止这些但你已经把最核心的地基打牢了后面无论是做有状态服务、可观测性还是多集群管理都不会太慌。