
上篇文章我们把 Kubernetes 集群跑起来了Node、网络、核心组件都正常运作但这只是热身。我见过不少刚接触 K8s 的朋友集群一装好就急着写 Deployment 和 Service结果 pod 起不来或者起来了又频繁重启然后跑来问我“到底哪里写错了”。这类问题九成九都出在两个根基上对 Pod 这个最小调度单元的理解以及对 YAML 资源清单字段的把控。所以这篇实战系列第 2 篇我就聚焦这两个主题——Pod 管理和资源清单配合平时在集群里真踩过的坑把这些门道讲透。这篇内容适合两类人一类是刚搭好集群、正准备深入 K8s 核心概念的开发者另一类是已经在用 Deployment 管理服务但遇到 Pod 异常时不知道从哪下手的运维或后端同学。读完你可以做到看到一个 Pod 的 YAML能逐行解释每个字段在干什么写一个最小 Pod 清单知道哪些字段必须写、哪些可以省略遇到 Pending、CrashLoopBackOff 这类状态能按步骤定位问题而不是乱删重建。1. 为什么 Pod 是 Kubernetes 的最小调度单元1.1 一个 Pod 里为什么可以放多个容器先纠正一个常见误区Pod 不等于容器。Pod 是 Kubernetes 里最小的调度、创建、销毁单元它可以包含一个或多个容器。这些容器共享同一个网络命名空间、共享存储卷、共享同一份生命周期并且一定会被调度到同一个节点上。很多人不理解为什么需要“多容器 Pod”这种设计。我举一个最常见的场景业务容器跑 Nginx你想采集它的访问日志并根据内容做简单清洗如果在传统环境里你会在这台机器上再装一个采集 Agent。在 K8s 里这个 Agent 就是一个独立的容器和业务容器放在同一个 Pod 中。两个容器共享一个 emptyDir 类型的 Volume业务容器把日志写到共享目录采集容器从同一目录读日志二者通过 localhost 就能互相访问。这种把辅助功能容器放进同一个 Pod 的模式就是常说的 sidecar 模式。这种设计带来的好处非常直接调度器把整个 Pod 当作一个原子单位投放不关心 Pod 里有几个容器Pod 里的所有容器共享网络的 IP 和端口空间当 Pod 被删除或迁移时里面所有容器一起结束不会出现“主业务起来了辅助进程还在另一个节点上”的尴尬状态。所以你在写 YAML 时第一步要想清楚哪些进程是同一个应用实例的一部分哪些是独立服务。前者放一个 Pod后者拆成多个 Pod。1.2 容器配置的细节镜像、启动参数、环境变量、端口声明Pod 里最核心的字段是spec.containers这个数组里每个元素定义了一个容器的运行方式。初学者最容易在这几个子字段上翻车。第一是image。镜像地址和标签一起构成拉取来源比如nginx:1.26。K8s 对镜像拉取策略有默认行为如果标签是latest默认策略是Always也就是每次都会尝试拉取最新镜像如果是固定标签默认是IfNotPresent即本地存在就用本地的。这个行为可以在imagePullPolicy字段里显式覆盖。我在集群里经常遇到的问题是本地已经改过镜像但写 YAML 时忘了更新标签导致 Pod 一直使用旧的镜像。第二是command和args。这两个字段容易和 Dockerfile 里的指令混淆。command会覆盖 Dockerfile 中的 ENTRYPOINTargs会覆盖 CMD。如果你希望执行的是一个带参数的脚本正确做法通常是在command里写执行器在args里传参数。比如spec: containers: - name: app image: myapp:v1 command: [/bin/sh, -c] args: [echo start sleep 3600]第三是env。环境变量可以直接写固定值也可以引用 ConfigMap、Secret甚至可以引用 Pod 的其他字段。引用其他字段时用fieldRef这在写容器感知自身 Pod 信息的场景下非常实用。第四是ports。必须强调ports.containerPort只是一个“声明”标明容器在监听什么端口它不是一把打开外部流量的钥匙。容器里的端口能不能被集群内其他服务访问取决于是否存在对应的 Service能不能被浏览器访问取决于 Service 类型和 Ingress 配置。很多新手写完这个字段后直接去浏览器访问节点 IP发现不通就开始怀疑网络插件出了问题其实问题出在理解上这个字段更多是给 K8s 和运维人员看的元数据。1.3 initContainers容器启动前的“准备工作区”除了普通的containersPod 还有initContainers字段。initContainers 里的容器会按照书写顺序串行执行并且必须全部成功退出后主容器才会启动。如果某个 init 容器执行失败Kubelet 会根据重启策略重新执行而 Pod 的状态可能一直停留在 Pending 或 Initialized。这个机制适合做三类事第一类是等待外部依赖就绪比如等数据库账号真正创建完成第二类是初始化目录结构、下载静态文件或生成配置文件第三类是修改权限或写内核参数。我常用它来做工程化前的“预处理”比如下面的例子spec: initContainers: - name: init-dir image: busybox:1.36 command: [sh, -c, mkdir -p /data/logs chown -R 65532:65532 /data/logs] volumeMounts: - name: data mountPath: /data containers: - name: main-app image: myapp:v1 volumeMounts: - name: data mountPath: /data这种写法的好处是清晰分离了“准备阶段”和“运行阶段”。准备阶段如果一直失败你通过kubectl logs pod-name -c init-dir就能精确看到是哪一步卡住了而不是在主容器的日志里翻半天。2. 资源清单把期望状态写清楚2.1 YAML 的四个顶层字段怎么选Kubernetes 的核心设计是声明式 API而资源清单就是用户与 API Server 沟通的语言。任何一份资源清单都围绕四个顶层字段组织apiVersion、kind、metadata、spec。apiVersion决定了你使用的是哪个 API 版本的资源定义。不同资源版本差异很大最常被搞混的就在这里。写 DaemonSet 时如果用了 v1API Server 会直接拒绝因为正确版本是apps/v1。我列一个高频对照表资源正确 apiVersion说明Pod / Service / ConfigMap / Secretv1核心稳定资源Deployment / StatefulSet / DaemonSetapps/v1控制器资源Job / CronJobbatch/v1任务类型Ingressnetworking.k8s.io/v1七层路由HorizontalPodAutoscalerautoscaling/v2水平扩缩容kind是资源类型比如 Pod、Deployment、Service。metadata提供唯一标识至少要有name通常还要加namespace、labels、annotations。spec是这个资源的期望状态描述不同 kind 的 spec 结构天差地别。如果不确定某个字段能不能写、字段值是什么格式最好的办法不是去查博客而是用kubectl explain。这个命令能直接查看 API 对字段的定义和注释。例如kubectl explain pod.spec.containers会列出容器字段的结构想深入某一层就继续追加路径。我一直把这个命令当作“随身手册”因为它展示出来的说明与当前集群版本完全一致。2.2 一个最小 Pod 资源清单的逐行拆解直接写一个最小的 Pod 清单然后逐行过一遍apiVersion: v1 kind: Pod metadata: name: nginx-demo namespace: default labels: app: nginx env: dev spec: containers: - name: nginx image: nginx:1.26 ports: - containerPort: 80 restartPolicy: AlwaysapiVersion: v1因为 Pod 是核心资源所以用 v1没有任何争议。kind: Pod告诉 API Server 我要创建的是 Pod而不是其他控制器对象。metadata.namePod 的名字同一个 namespace 内必须唯一。metadata.namespace归属的命名空间不写就进 default。metadata.labels标签是键值对Pod 会被哪个 Service、哪个 Deployment 选中靠的就是它。spec.containers.name容器名字必须唯一且符合 DNS 命名规范。spec.containers.image镜像地址是容器运行的蓝本。spec.containers.ports.containerPort声明容器监听端口语义上更像“告知”而非“开放”。spec.restartPolicyPod 内容器的重启策略默认其实是 Always。把这 9 行写完kubectl apply -f pod.yaml就能跑起来。但请注意直接创建的裸 Pod 如果挂掉Kubelet 只会根据 restartPolicy 重启它而不会重新调度、也不会自动创建新的替代品。遇到节点宕机这个 Pod 就彻底没了。所以生产环境中Pod 几乎总是由 Deployment、StatefulSet 这类控制器来创建和管理。裸 Pod 适合用来做测试和定位问题不适合作为长期服务载体。2.3 labels 与 annotations标签是索引注释是便签在 metadata 里labels和annotations都是键值对但用途完全不同。labels 是选择器机制的核心Service、Deployment、StatefulSet 都通过 label selector 关联 Pod。比如一个 Service 的 selector 写成app: nginx它只会把流量转发给带这个标签的 Pod。所以标签相当于资源的“索引”它参与集群的查询与匹配。annotations 则更像“便签”它可以携带大量元数据并且不支持选择器查询。比如记录某个 Pod 由哪个发布系统创建、上次变更时间、负责人的联系方式这些信息放进 annotations 不会影响调度和选择逻辑又方便在故障时快速查看。写标签时我建议遵循一套固定规范。比如app表示应用名env表示环境version表示版本tier表示层级。有了一套规范你在查询时就能做精确过滤比如kubectl get pods -l appnginx,envdev一两秒钟内定位到目标。3. Pod 生命周期与节点上的自愈机制3.1 五个状态怎么看Pod 的状态是排查问题时的第一线索。kubectl get pods看到的 STATUS 列背后是 Kubernetes 对 Pod 生命周期阶段的抽象主要包括 Pending、Running、Succeeded、Failed、Unknown。状态含义常见场景Pending已接受创建请求但 Pod 还没被调度或容器还没完全启动节点资源不足、调度约束不满足、镜像拉取中RunningPod 已绑定宿主机所有容器已创建且至少有一个在运行正常提供服务Succeeded所有容器成功退出Job 类任务执行完Failed至少一个容器以非 0 状态退出容器崩溃或脚本报错UnknownAPI Server 无法获取 Pod 状态节点异常或网络分区Pending 下面其实还能再分。调度器如果一直找不到合适节点Pod 会一直卡在 Pending此时kubectl describe pod的 Events 里会写清楚失败原因比如节点 CPU 不足、内存不足、没有匹配的节点选择器。容器创建阶段如果一直拉镜像也会表现为 Pending但 Events 里会有 Pulling 或 Failed to pull image 的记录。Running 状态也有很多坑。Pod 进入 Running 不代表程序真的健康它只代表 Kubelet 把容器拉起来了。要判断业务是否可用得靠探针也就是下一节要说的 Probe。3.2 探针容器活着不等于业务可用Kubernetes 提供三种探针用于判断容器的健康状态。livenessProbe判断容器是否存活失败会触发重启readinessProbe判断容器是否就绪失败会把 Pod 从 Service 的端点列表里摘除startupProbe用于保护启动很慢的容器避免它在初始化阶段就被 livenessProbe 误杀。实际生产里我曾经踩过一个很典型的坑。一个 Java 应用启动需要 80 秒但默认的 livenessProbe 在 10 秒后就开始探测连续失败 3 次后容器被强制重启结果启动时间每次都到不了 80 秒就被杀掉陷入无限重启。后来加上 startupProbe设置失败阈值到 30 次、探测间隔 5 秒给足启动时间容器稳定下来livenessProbe 才开始接管。所以当你遇到一个“永远起不来”的 Pod先想想是不是探针把进程掐死了。一个常见的探针配置如下spec: containers: - name: app image: myapp:v1 startupProbe: httpGet: path: /health/startup port: 8080 failureThreshold: 30 periodSeconds: 5 livenessProbe: httpGet: path: /health/live port: 8080 initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: httpGet: path: /health/ready port: 8080 initialDelaySeconds: 5 periodSeconds: 5探针的路径千万不要随手写。我见过有人在容器里根本没有/health这个端点的情况下给 Pod 配置了 HTTP 探针结果 Pod 永远进不了 Running。更合理的做法是在应用代码里专门暴露一个健康检查接口不依赖数据库、不依赖缓存中间件纯粹判断进程本身是否处理得了新请求。3.3 restartPolicy重启是 Kubelet 的事重建是控制器的事Pod 的restartPolicy决定的是容器退出后 Kubelet 在本地怎么处理只有三个可选值Always、OnFailure、Never。默认是 Always意思是容器退出后无论退出码是什么都会重新拉起OnFailure 只在容器以非 0 状态退出时重启Never 则完全不重启。这道边界非常关键restartPolicy 只管同一个节点上的容器重启它不会帮你创建新 Pod、不会把 Pod 调度到别的节点。如果你的 Pod 是 Deployment 管理的Deployment 会在 Pod 被删除或节点故障时重新创建一个新的 Pod Replica如果你的 Pod 是裸 Pod节点宕机后它就不会自行恢复。所以“自愈”这两个字在 K8s 里其实是分层实现的底层是 Kubelet 的同节点重启上层是控制器的跨节点重建。3.4 优雅终止preStop 和 terminationGracePeriodSeconds“让进程体面退出”这件事在 K8s 里通过两个机制配合完成。一个是spec.terminationGracePeriodSeconds默认 30 秒它定义了从向容器发送 SIGTERM 到强制发送 SIGKILL 的宽限时间。另一个是lifecycle.preStop它允许你在 SIGTERM 之前执行特定动作。典型场景是这样的应用容器通过 Nginx 提供服务删除 Pod 时如果直接 SIGTERMNginx 会立刻退出导致正在处理的连接直接断掉。更优雅的做法是preStop 里执行 sleep 10给上游负载均衡器一点时间摘掉节点sleep 结束后进程再优雅退出。配置如下spec: containers: - name: app image: myapp:v1 lifecycle: preStop: exec: command: [sh, -c, sleep 10]关于终止时长我的经验是不要盲目调大也不要无脑用默认值。先把应用优雅退出需要的时间测出来比如看日志从收到退出信号到完全退出花了多久再加 5 到 10 秒作为余量。如果超过这个时间还没退出Kubelet 会强制 SIGKILL长连接同样会断。4. 从命令到实战Pod 管理高频操作4.1 常用 kubectl 命令与使用场景管理 Pod 不需要记很多命令高频的就这些# 根据 YAML 清单创建或更新资源 kubectl apply -f pod.yaml # 查看 Pod 列表加 -o wide 可以看到节点和 IP kubectl get pods -o wide # 查看 Pod 详细信息和事件 kubectl describe pod pod-name # 查看容器日志--previous 查看上一次崩溃实例的日志 kubectl logs pod-name -c container-name --previous # 进入容器执行命令 kubectl exec -it pod-name -- sh # 删除 Pod kubectl delete pod pod-name # 把本地端口映射到 Pod 端口调试时很好用 kubectl port-forward pod/pod-name 8080:80apply和create的区别值得强调。create是命令式操作资源已经存在时会报错apply是声明式操作它通过对比清单与集群中的实际状态执行创建或更新。日常维护清单时务必统一用apply否则很容易出现“改个 image tag然后 create 报 AlreadyExists只能手动 delete 再建”的尴尬循环。describe是最有价值的排障命令。它会输出 Pod 的事件列表包括镜像拉取、调度结果、挂载结果。很多问题一眼就能在 Events 里找到答案。有些新人习惯反复kubectl get pods看到容器的状态从 CrashLoopBackOff 变到 Running 又变回去却不知道发生了什么其实就是一直没看 Events。4.2 典型异常状态排查表很多时候一个状态对应多种可能原因。我把在生产里遇到的高频异常整理成一个速查表状态典型原因第一步排查动作Pending资源不足 / 节点不满足约束kubectl describe pod看 Events 里的调度失败原因ContainerCreating镜像拉取慢 / 存储卷挂载失败观察状态是否卡住kubectl describe看是否有 Volume 相关报错ImagePullBackOff镜像不存在 / 仓库凭据错误检查镜像名和 tag确认拉取凭据CrashLoopBackOff容器启动后立即退出kubectl logs pod --previous看上一次日志OOMKilled容器内存超过 limitkubectl describe pod看 Last State 的 Exit Code检查 resources.limits.memoryEvicted节点资源压力触发驱逐查看节点压力来源扩容或调整资源请求Errorinit 容器失败 / 容器启动失败检查 init 容器日志和 Events以 CrashLoopBackOff 为例它表面上是“容器退出了Kubelet 在重启退出了又重启”。但根本原因可能是三类启动命令本身报错、依赖的配置不存在、探针失败。有一个快速区分的方法先kubectl logs pod-name --previous看当前崩溃实例的日志如果日志里能直接看到异常栈或明确报错问题在应用本身如果日志为空再看 Events 里的探针相关事件可能是健康检查路径写错了。4.3 排查 Pod 问题的几条私藏经验第一条经验永远先看 Events再看日志。Events 会给你一个宏观判断比如调度失败、证书拉取失败、探针失败日志则帮助你确认具体细节。反过来先看日志容易陷入局部可能折腾半天发现只是某个节点内存不够导致 Pod 被驱逐。第二条经验不要轻易kubectl delete pod --force。强制删除会跳过优雅终止流程对无状态服务可能影响不大但有状态服务例如数据库、需要持久化的缓存很容易出现数据损坏。如果 Pod 卡在 Terminating先查节点状态、再查 Finalizers最后才考虑强制删除。第三条经验用kubectl get events --sort-by.metadata.creationTimestamp查看整个命名空间的事件流。特别是在 Pod 反复重启时单看一个 Pod 的 describe 不一定能看到全貌因为有些事件会被较早的事件顶掉。按时间排序后整个创建流程的先后顺序会非常清楚。5. 资源配额别让 Pod 把节点内存打爆5.1 requests 与 limits调度靠请求运行时靠限制如果写 Pod 清单时不写资源相关字段Pod 就能“无限”使用节点 CPU 和内存这等于埋雷。更规范的做法是在每个容器里设置resources它包含两个核心区间requests和limits。requests是调度依据。调度器会统计节点上已经分配的 requests 总和当节点剩余可用资源小于新 Pod 的 requests 时这个 Pod 不会调度到该节点。也就是说 requests 相当于“预留”它保证你能用到的兜底资源。limits是运行时约束。CPU 超过 limits 会被抑制限流内存超过 limits 会触发 OOM 机制并杀掉容器。注意 CPU 是可压缩资源超限后进程变慢但不会死内存是不可压缩资源超了就是死。所以在设计资源额度时比较稳妥的原则是requests 按正常负载估算留出 20% 到 30% 的波动余量limits 按峰值负载估算再留一些保护节点稳定性的空间。不要把 limits 设置得远远超过实际需求否则节点资源被大量预留其他 Pod 反而调度不上去。5.2 CPU 与内存单位换算别写错单位不然差很远Kubernetes 对 CPU 和内存有自己的单位系统我见过不少因为单位理解错误导致资源申请过大的情况。CPU 的单位是核可以用小数表示比如0.5代表半个核更推荐用 millicores 表示后缀是m500m等于 0.5 核。1 核等于 1000m250m相当于四分之一核。在低负载服务中100m到500m是很常见的配置千万别写成100因为不带单位时整数会被视为 100 核调度器直接傻眼。内存的常见后缀是Mi、Gi、Ki它们基于 1024 进制即 1Mi 1024Ki。这个细节很重要因为M和Mi并不一样1M是十进制意义上的 1 兆字节约 1000000 字节而1Mi是二进制意义的 1MiB约 1048576 字节。严格来说是两套体系。我在项目里统一使用Mi和Gi尽量避免写裸数字或大小写混淆。下面是一个合理的资源配置示例spec: containers: - name: app image: myapp:v1 resources: requests: cpu: 250m memory: 256Mi limits: cpu: 1 memory: 512Mi如果容器内存持续超过 512MiPod 会由于 OOMKilled 被反复杀掉。排障时看到这个状态先查应用的堆内存或缓存配置而不是急着把 limits 调大因为不留余地的无限调大会把问题掩盖最终害了整个节点的稳定性。5.3 LimitRange 与 ResourceQuota从 Namespace 层面卡上限单靠每个 Pod 自觉写 resources很难管住全局。Kubernetes 提供了两层兜底工具LimitRange和ResourceQuota。LimitRange作用于 Pod 或容器级别它可以在一个 Namespace 内规定默认值。比如未显式写 requests 的容器会被自动填充默认值也可以设置单个容器内存最大值如果清单里的限制超过这个值API Server 会直接拒绝创建。示例apiVersion: v1 kind: LimitRange metadata: name: mem-limit-range spec: limits: - max: memory: 1Gi min: memory: 128Mi default: memory: 512Mi defaultRequest: memory: 256Mi type: ContainerResourceQuota则作用于命名空间总量比如限制整个命名空间内 Pod 总内存不超过 20Gi、CPU 总请求不超过 20 核。它的作用是防止某个团队或某个应用把集群资源吃光。一个简化版示例apiVersion: v1 kind: ResourceQuota metadata: name: quota spec: hard: requests.cpu: 10 requests.memory: 20Gi limits.cpu: 20 limits.memory: 40Gi pods: 50这两层配置配合起来效果是“制度 兜底”。ResourceQuota 控制总账LimitRange 保证默认值和边界。没有这两层时一条没写 resources 的清单也能顺利建出来有它们在规范才能真正落地。5.4 关于资源配额我个人踩过的复盘在项目初期我们组曾经只给 Pod 配置了 requests没配 limits。表面上服务跑得很稳实际遭遇一次流量高峰时部分节点的内存被打满触发了系统 OOM 和 Pod 驱逐大量业务 Pod 被重新调度到其他节点整条链路出现了一连串雪崩。复盘时才发现根本原因是requests 太小导致调度器认为节点资源还很宽裕把很多 Pod 塞到一起没有 limits又让进程在峰值期无限吃内存最终把节点压穿。从那以后我们的规范改成requests 按基线负载的 1.2 倍估limits 按峰值负载的 1.5 倍估同时用 ResourceQuota 给每个命名空间设置总量上限。每次新服务上线前都要先在测试环境跑一遍实际负载拿监控数据反推资源参数。这个习惯坚持下来集群层面的“资源事故”基本绝迹了。我个人的核心体会是Kubernetes 的 Pod 和资源清单看起来只是两组概念但它们背后牵涉调度、健康检查、自愈、资源隔离、Namespace 管理整个体系。先把这两块地基打牢后面再上 Deployment、Service、Ingress、存储、自动扩缩容时你会发现自己理解速度明显快很多。下一篇我们继续往上层走深入控制器和网络模型到时候你会看到它们的每一步都离不开今天这些基础概念。