开篇直接说Pod 这玩意儿是 Kubernetes 里最小的调度单元也是大部分人入门 K8s 时第一个真正需要啃下来的概念。你可能已经知道它相当于一组容器的集合但真到排障和调优的时候光有这一层认知远远不够——我见过不少朋友在面试和实际开发里把 Pod 当成就是容器来理解结果一遇到重启策略、探针失效、容器启动顺序这种实际问题直接就懵了。这篇文章我准备把自己从懵到通的过程梳理一遍从最小的概念讲起一直讲到资源配额、探针配置、调度约束和排障思路争取让你看完之后面对一个异常 Pod 时不再只是会kubectl describe和kubectl logs。适合谁看刚接触 K8s 的开发、运维、测试都适合如果你已经写过几个 Deployment YAML但说不清 Pod 和 Deployment 的关系、搞不明白探针参数怎么调那这篇文章同样值得花二十分钟过一遍。1. Pod 到底是什么先搞清楚这个最小的调度单元1.1 为什么不直接调度容器非要包一层 PodKubernetes 之所以没有直接把单个容器作为调度单位核心原因在于有些场景下多个进程必须部署在同一台机器上共享网络栈和存储还要能够互相协作。你写业务的时候可能觉得一个容器只跑一个进程很干净可真到了实际业务里你会遇到三类非常典型的需求第一类是日志代理场景。业务容器把日志写进文件旁边需要一个 sidecar 容器专门采集日志并转发出去。如果这是两个独立的容器它们之间的文件系统隔离会让这个组合很难实现——要么把日志通过远程接口传要么就得引入额外的共享存储方案复杂度一下子就上来了。第二类是网络代理场景。比如业务容器监听 localhost你需要一个代理容器帮它接收外部流量。这类组合要求容器之间能通过 localhost 直接通信这只有在它们共享同一个网络命名空间时才能实现而 Pod 提供的正是这一点。第三类是初始化场景。应用启动前需要拉取配置、初始化数据库结构、等待依赖服务就绪这些任务做完之后主容器才开始运行。如果让主容器自己处理代码就得写一堆 bash 脚本判断依赖更优雅的做法是让一个专门的初始化容器先跑跑完退出主容器再接管。把这三种需求凑到一起你会发现容器组这个概念几乎是必然产物。Pod 的作用就是给这一组容器提供共享环境同一个网络命名空间共享 IP 和端口、共享的存储卷、同时被调度到同一台节点。说白了Pod 内部容器之间可以通过 localhost 访问对外暴露的是一个统一的 IP 地址。1.2 每个 Pod 里都有个隐形的 pause 容器你如果用docker ps查看节点上的容器列表会发现每个 Pod 对应一个名字里带pause或者sandbox的容器。很多初学者第一次看到会以为是系统自动创建的垃圾容器其实它是 Pod 的地基。pause 容器在 Pod 创建时第一个启动主要负责两件事一是持有 Pod 的网络命名空间二是作为收养孤儿进程的父进程。业务容器启动时加入的就是 pause 容器创建的网络命名空间因此整个 Pod 共享同一个 IP 和端口空间。而容器内进程在退出时如果还有子进程没有回收这些子进程会被挂到 pause 名下避免它们变成没人管的僵尸进程。这个小细节平时用不上但排查localhost 访问不到或者容器退出后网络异常的时候就会回来感谢它——建议你至少在某个节点上手动看一下容器列表把 Pod 的容器结构理解成pause 业务容器 其他辅助容器这个模型后面很多概念都会顺很多。在写 YAML 的时候你不需要手动定义 pause 容器Kubernetes 会自动注入。你看到的 Pod 定义里通常只有业务容器和辅助容器实际运行时会多出这个隐形的 pause这个认知偏差如果不纠正看监控数据的时候会很困惑。2. Pod 的生命周期与状态机2.1 从创建到消失要经过哪些状态Pod 的状态不像传统运维里启动-运行-停止那么简单它有一整套状态机。最常见的有五种Pending、Running、Succeeded、Failed、Unknown。另外还有一个 CrashLoopBackOff它其实不是 Pod 状态而是容器状态的展示很多人会混淆。Pending 阶段是调度的白热化时期。你提交了一个 Pod 定义调度器正在为它寻找合适的节点资源够不够、有没有满足 nodeSelector 的节点、节点是否处于 Ready 状态。如果资源不足或没有匹配的节点Pod 会一直卡在 Pending。这个阶段最容易踩的坑是集群里有节点但节点上标记了污点taint而你的 Pod 没有对应的容忍toleration调度器会直接无视这些节点。Running 阶段表示 Pod 已经被成功调度并绑定到了某个节点至少有一个容器还在运行。注意Running不代表你的业务一定正常——容器可能正在反复崩溃重启但只要它在循环中比如 CrashLoopBackOff 间歇期Pod 状态仍然是 Running。我第一次看kubectl get pods看到 STATUS 是 Running 就觉得万事大吉结果业务方说服务完全不可用后来才发现容器每三十秒崩一次这个教训记忆深刻。Succeeded 和 Failed 都对应任务型 Pod跑完就退出的批处理任务。如果 Pod 里的所有容器都正常退出而且退出码为 0Pod 进入 Succeeded如果只要有容器以非 0 退出码结束Pod 就变成 Failed。普通 Web 服务型 Pod 很少主动进入 Succeeded除非你故意让容器主进程退出。Unknown 状态比较冷门它表示 API Server 与节点上的 kubelet 失联了无法获取 Pod 的最新状态。常见原因包括节点宕机、网络分区或者 kubelet 假死。遇到 Unknown 别慌先看节点状态和 kubelet 日志多数情况下是节点的问题而不是 Pod 本身的问题。2.2 restartPolicy 选错会让你的排障思路绕远路restartPolicy 是 Pod 级别的字段可选值只有三个Always、OnFailure、Never。它决定的是kubelet 在容器退出后是否帮你重启容器而不是应用崩溃后 K8s 怎样恢复整个 Pod。默认值是 Always。这意味着即使你的任务型容器跑完并正常退出退出码 0kubelet 也会启动一个新的容器实例替代它。如果你写的是 Job正常情况下应该让 Job 控制器和 restartPolicy 协同配合如果你只是手动创建了一个裸 Pod 去跑一次性任务而忘了设置 Never你会发现任务明明完成了Pod 却一直在那里反复启动新容器资源白白消耗。OnFailure 意味着只有退出码非 0 才重启经常和 Job 配合使用。Never 则完全不重启适合真正的任务型场景失败了就让记录留在那里后续由人工或上层控制器处理。实际项目里托管型工作负载Deployment 管理的 Pod几乎都是 Always因为控制器需要保证副本数Job 型任务通常用 OnFailure 或 Never。有一个很容易搞混的点是restartPolicy 只影响当前节点上 kubelet 的重启行为不会触发 Pod 重新调度。如果节点本身挂了Pod 是否被重新创建在其他节点上取决于它有没有被 Deployment、StatefulSet 这类控制器管理。3. 从零创建一个 Pod最常用的三种方式3.1 kubectl run 的快速上手与局限如果你只是想在测试集群里快速起一个 Nginxkubectl run nginx --imagenginx是最快的方式。它会在当前命名空间创建一个 Deployment新版 kubectl 默认行为然后帮你生成一个副本。但这种方式有个明显的坑它产生的 YAML 是隐式生成的你几乎无法控制探针、资源限制、环境变量等关键配置。我建议你把kubectl run当成冒烟测试工具来用真正的业务 Pod 一定要写 YAML哪怕一开始从简单开始也要把配置项显式化。另一个实用技巧是用kubectl run nginx --imagenginx --dry-runclient -o yaml生成初始 YAML导出来再往里面补充配置这样比手写全量 YAML 要省事得多。3.2 用 YAML 定义 Pod最小可运行模板下面这个 YAML 是创建单个裸 Pod 的最小模板我建议你把它存成自己的脚手架文件apiVersion: v1 kind: Pod metadata: name: web-demo labels: app: web-demo env: dev spec: containers: - name: web image: nginx:1.25 ports: - containerPort: 80 name: http restartPolicy: Always把这个文件存成pod.yaml执行kubectl apply -f pod.yaml即可创建。这里有几个细节值得注意第一metadata.labels不是可选项。虽然不写也能创建但后续 Service、Deployment、监控系统都靠标签来选择 Pod没有标签等于把 Pod 丢进了无法被管理的角落。第二ports.containerPort只是声明不是端口映射。容器监听 80 端口并不是因为你写了这段配置而是因为 Nginx 本身就监听 80。这段声明的作用更接近元数据方便其他人读 YAML 了解意图某些云平台也会用它生成负载均衡规则。很多新手误以为不写这段就不会暴露端口其实 Pod 的网络模型是从共享网络命名空间继承的端口本来就通。第三kubectl apply和kubectl create有区别。apply是声明式的会记录上次应用的配置适合反复更新同一个文件create是命令式的对已存在的同名资源会直接报错。日常使用优先apply。3.3 为什么实际项目很少直接创建裸 Pod裸 Pod 有一个致命问题如果它所在的节点挂了kubelet 无法把它迁移到别的节点上这个 Pod 就永远消失了。Kubernetes 的设计哲学是让控制器来管理 Pod 的生命周期而不是让人直接操作 Pod。所以实际项目中你几乎不会用kind: Pod直接创建工作负载而是用 Deployment。Deployment 管理 ReplicaSetReplicaSet 维护期望的副本数并创建 Pod。它们的层级关系是Deployment → ReplicaSet → Pod。你在 YAML 里写的spec.template就是 Pod 模板Deployment 用这份模板生成 ReplicaSetReplicaSet 再根据模板实例化 Pod。这个三层结构不是多余的设计它解决了一个核心问题滚动更新。当你更新 Deployment 的镜像版本时Deployment 会创建一个新的 ReplicaSet逐渐增加新副本数、减少旧副本数整个过程对你配置的 Pod 模板完全透明。如果你操作的是裸 Pod想做到不停机更新只能手动一个接一个地删建效率和安全性都差很多。4. Pod 实战进阶健康检查探针的正确打开方式4.1 三种探针功能边界必须分清楚K8s 提供了三种探针livenessProbe存活探针、readinessProbe就绪探针、startupProbe启动探针。它们分别回答三个不同的问题容器还活着吗容器能接收流量吗容器启动完成了吗livenessProbe 失败会让 kubelet 杀掉容器并按 restartPolicy 重启。它的职责是兜底死循环、死锁这类进程还在但业务已经不可用的问题。比如你的 Java 应用发生线程池耗尽进程本身还活着但已经无法处理请求这时候 liveness 探针能把它拉回来。readinessProbe 失败不会重启容器而是把 Pod 从 Service 的 Endpoints 列表中摘除让流量不再打到它。等探针恢复成功Pod 会自动重新加入流量池。这个探针特别适合应用已启动但依赖的数据库还没连上的场景——进程起来了但你不能让它接流量。startupProbe 是后加入的探针专门解决慢启动应用的存活误杀问题。如果你的应用需要 60 秒才能完成启动而 liveness 探针的第一轮检查在启动 10 秒后就执行了还没等应用就绪就把它杀了。正确的做法是给慢启动应用配置一个 startupProbe在它成功之前liveness 和 readiness 探针都不会执行等 start 完成后再交棒给另外两个探针。理解了三者的分工配置顺序就清楚了先配 startupProbe 兜住慢启动再配 readinessProbe 控制流量接入最后用 livenessProbe 做兜底防护。4.2 探针参数从一次误杀事故说起我曾遇到过一个服务在高峰期频繁重启查到最后发现是 livenessProbe 的超时时间设置得太短。HTTP 请求在高峰期响应要 3 秒而探针的timeoutSeconds只给了 1 秒kubelet 每次请求都超时连续三次失败就把容器杀了。这个案例说明探针参数不能随便抄模板。常用的探针参数包括initialDelaySeconds容器启动后等多长时间才开始第一次探测给应用留启动窗口。periodSeconds探测频率默认 10 秒。timeoutSeconds单次探测的超时时间默认 1 秒。successThreshold连续成功多少次才认为容器健康默认 1 次。failureThreshold连续失败多少次才触发对应动作杀容器或摘流量默认 3 次。一个比较稳妥的 HTTP 探针配置长这样livenessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 15 periodSeconds: 15 timeoutSeconds: 3 failureThreshold: 3 readinessProbe: httpGet: path: /ready port: 8080 initialDelaySeconds: 5 periodSeconds: 5 timeoutSeconds: 2 failureThreshold: 1这里有个经验点readiness 的failureThreshold设置成 1 意味着一次探测失败就摘流量适合对抖动敏感的服务而 liveness 的阈值通常要稍微宽松一点因为误杀一个健康容器比短暂保留一个亚健康容器代价更高。我最早踩过的另一个坑是探针路径。很多人把 liveness 和 readiness 指向同一个接口看起来省事实际上违背了它们的设计意图liveness 应该是一个只要进程活着就能返回 200的极简接口readiness 才应该检查依赖资源数据库、缓存、下游服务是否可用。如果你让两个探针都去检查数据库数据库抖动时 liveness 失败导致容器被反复重启问题会被放大而不是被缓解。5. 多容器 Pod 与 Init 容器的正确用法5.1 Sidecar 模式为什么要把两个容器放在同一个 Pod多容器 Pod 最常见的就是 sidecar 模式。主容器负责业务逻辑sidecar 容器负责辅助能力。除了最典型的日志采集还有几个很实用的场景值得提一下文件同步场景。一个容器把远程文件同步到本地共享目录另一个业务容器消费这些文件。它们共享同一个 volume通过文件系统协作主容器代码里不需要任何远程调用的逻辑。代理转发场景。业务容器只监听 localhostsidecar 容器承担对外端口监听和转发。比如一个 Python 应用不擅长处理并发连接旁边放一个 Nginx sidecar 做反向代理整个 Pod 对外只暴露 Nginx 的端口。安全加固场景。主容器里不放 SSH 和调试工具sidecar 容器单独承担调试和运维操作。这样攻击面缩小了主镜像可以做到尽量精简调试工具集中在 sidecar 里。多容器共享同一 IP 和端口空间意味着端口冲突是真实存在的两个容器不能用同一个端口监听 localhost。设计时要提前约定好每个容器使用的端口范围避免 A 容器写了 8080B 容器也想用 8080 的情况。5.2 Init 容器先决条件由它来完成Init 容器是排在业务容器之前运行的专用容器它的特点是必须全部执行成功Pod 才会进入后续的容器启动流程任何一个 Init 容器失败整个 Pod 会被重启或重建。典型的用法包括等待依赖服务就绪比如轮询数据库端口拉取配置并写入共享卷执行数据库迁移脚本把某个目录的权限和所有权改好。Init 容器跑完后就退出不会长期占用资源。Init 容器的特殊之处在于如果集群里已经有正在运行的主容器你新增或修改 Init 容器配置Pod 不会热更新而是会被重建因为 Init 容器必须在主容器启动前执行。这个行为像一个隐性约束改 Init 容器配置前要有Pod 会重启的心理准备。另外要注意Init 容器的资源申请和主容器是分开计算的。调度器在判断节点容量时会把所有 Init 容器中最大的资源请求值与主容器资源请求相加所以 Init 容器如果写得贪心可能导致 Pod 无法被调度到合适的节点。我之前见过有人给 Init 容器申请了 8 核 16G而主容器只需要 2 核 4G结果整个集群没有节点放得下这个 Pod排查了半天才发现是 Init 容器吃掉了调度预算。6. Pod 的资源管理与调度约束6.1 requests 和 limits理解配额得先弄清这三个概念Kubernetes 的资源模型里requests 和 limits 分别代表最低保障和最高上限。requests 是调度依据调度器会检查节点剩余的可用资源是否满足所有 Pod 的 requests 总和limits 是运行时上限超出会触发 CPU 节流或 OOM 杀死容器。CPU 和内存的行为完全不同很多人在这里栽跟头。CPU 是可压缩资源超过 limits 时容器会被限流throttling表现为性能下降但不会死。内存是不可压缩资源超过 limits 时 kubelet 会触发 OOM Kill直接干掉容器。所以在配置内存时limits 一定不能设置过低否则容器会在达到真正危险水位之前被系统杀掉。一个在实际项目中验证过多次的建议如果不知道怎么配先把 requests 设成你观测到的稳定使用量的 70%~80%把 limits 设为 requests 的 1.5~2 倍但要留出明确业务特征需要更精细配置的空间。设置过高的 limits 意味着承诺了超出实际需要的资源上限节点上的资源会被 Pod 的额度占住调度器按 requests 而不是实际使用量来判断容量所以 requests 设置比实际需要高得多是集群资源利用率低的头号原因。6.2 调度约束nodeSelector、亲和性与污点容忍Pod 调度到哪个节点默认由调度器按资源余量和评分策略决定。但有些场景需要你主动干预比如某些节点有 GPU某些节点挂了 SSD某些业务要求和其他业务物理隔离。最基础的是 nodeSelector它要求 Pod 只调度到包含指定标签的节点。比如给 GPU 节点打标签gputruePod 里写上nodeSelector: gpu: true就行。它简单直接但表达不了复杂逻辑比如尽量调度到 GPU 节点但 GPU 节点满了也可以放到普通节点。更灵活的是 nodeAffinity。它支持 requiredDuringScheduling 和 preferredDuringScheduling 两种语义前者是硬性要求后者是软性偏好。硬性要求和 nodeSelector 类似但表达式丰富得多软性偏好则在调度评分阶段给满足条件的节点加分不满足也不会调度失败。实际项目里preferred语义用得很多因为它能提高调度命中率又不会因为个别节点标签不一致导致 Pod 无法调度。污点Taint和容忍Toleration是另一对概念。污点标记在节点上表示这个节点有特殊属性普通的 Pod 不要来容忍写在 Pod 上表示我能接受这个污点我可以来。这对机制常用来实现节点独占给专用节点打上污点只有带对应容忍的 Pod 才能调度过去。常见的污点效果有三种NoSchedule不调度新 Pod、PreferNoSchedule尽量不调度、NoExecute不仅不调度还会驱逐已经运行在上面的 Pod。NoExecute 会影响正在运行的 Pod修改时一定要谨慎否则一个误操作会让节点上的存量 Pod 全部被赶走。7. 常见问题与排查技巧实录7.1 ImagePullBackOff镜像拉取失败的完整排查路径如果你看到一个 Pod 卡在 ImagePullBackOff第一反应别急着kubectl delete先执行kubectl describe pod pod-name。在 Events 一段里会明确显示失败原因。最常见的几个原因分别是镜像名或标签打错、私有仓库需要认证而 Pod 没有配置 imagePullSecret、仓库地址从集群节点网络不可达。镜像标签错误是最容易自查的直接在节点上手动docker pull验证一下。私有仓库认证问题需要给 Pod 的spec.imagePullSecrets配置对应的 Secret。网络不可达的问题要检查节点是否能访问镜像仓库域名。如果集群用的容器运行时是 containerd记得用crictl images而不是docker images去查节点上的镜像缓存二者管理的数据不一致。ImagePullBackOff 还有一个隐藏细节拉取失败的次数会有一个指数退避的过程一开始可能是几秒慢慢增加到几分钟。如果你只是修改了 imagePullSecrets 之后马上重试会发现状态仍然没有立刻变化有一部分原因是回到了退避周期。此时可以等待或者重新创建 Pod 来跳过等待。7.2 CrashLoopBackOff别再只会看 logsCrashLoopBackOff 表示容器启动了又退出反反复复。很多人第一步就是kubectl logs但这个路径其实有盲区如果容器根本没有成功启动到业务日志输出的阶段logs 可能是空的这时候你要去看的是上一轮启动失败的具体原因。排查顺序建议是先看kubectl describe pod的 Events 和 State确认退出码再看 logs 和 Previous 日志kubectl logs pod --previous如果要看容器进程是被 OOM 杀死的在 describe 输出里会有OOMKilled: True的提示。退出码是一个重要的信号137 通常是 OOM 或者容器被外部 kill127 是命令找不到1 或 2 一般是你自己业务代码的非 0 退出。把退出码和日志结合起来看比盲目看日志高效得多。常见的一个坑是探针配置不当导致容器被杀。你的业务进程其实运行正常但 liveness 探针路径返回的不是 2xxkubelet 判断存活失败就杀容器。这种崩溃是自杀式的——日志里看不到业务异常只有探针失败的痕迹所以排查时别忘了把探针配置纳入检查范围。7.3 Pod 一直 Pending调度器的无声拒绝Pod 停在 Pending 通常意味着调度器没有为它找到合适的节点。kubectl describe pod的 Events 里会写 0/5 nodes are available后面跟着一串原因。最常见的原因是节点资源不足、节点标签不满足 nodeSelector、节点存在无法容忍的污点、PVC 没有绑定成功。资源不足时Events 会告诉你哪个节点资源不够以及缺口多大。此时要么释放节点资源要么缩减 Pod 的 requests。被污点挡住时Events 会列出 matched node 但标注有未容忍的污点解决方式是给 Pod 添加对应的 toleration或者确认这个节点确实不该被使用。PVC 未绑定也是容易被忽视的对象Pod 引用了 PVC但如果 StorageClass 不存在或卷没创建成功调度器也不会真正开始调度。一个比较隐蔽的 Pending 原因是调度器本身的问题。如果你自建集群曾经遇到过scheduler组件没有正常工作结果是所有新 Pod 全部 Pendingdescribe 里连调度失败的提醒都没有——因为调度器压根没处理这些 Pod。排查到这里的时候需要看一眼控制平面组件的健康状况。8. 几个踩过坑之后沉淀下来的实用习惯最后分享几个我在实际项目中反复用到的习惯不一定写在文档里但真的能帮你省很多事。第一个习惯改动配置之前先导出当前 YAML。kubectl get pod name -o yaml backup.yaml在做任何调整前做一份备份。尤其是 Pod 已经被上层控制器管理的情况下手动修改 Pod 字段基本都会被控制器覆盖回去这时候你需要的不是改 Pod而是改 Deployment。先看这个 Pod 是哪个控制器管着的kubectl get deploy -A里搜一下或查看 Pod 的metadata.ownerReferences这个字段直接告诉你它的老板是谁避免白做一顿操作。第二个习惯善用kubectl get pod -o wide查看 Pod 被调度到了哪个节点以及它的内部 IP。排查网络问题时这个 IP 能帮你快速判断是集群网络的问题还是业务本身的问题。如果你发现 Pod 频繁在节点之间漂移而又没有明确的调度需求很可能是某台节点出现了资源碎片最直接的观测工具是kubectl describe node里的资源统计别靠猜。第三个习惯给 Pod 加上描述性标签。业务、环境、版本、负责人至少四个维度。成本核算、监控告警、故障分群都依赖标签。有很多团队一开始图省事不写标签等规模上来之后想通过kubectl get pods -l按业务线筛选却发现根本筛选不了只能在全量列表里人肉找那个体验相当酸爽。第四个习惯探针配置要像代码一样做版本管理。探针参数看起来只是几个数字但它直接决定服务的可用性。我见过最典型的问题是上线前所有探针都正常上线后高峰期因为 timeoutSeconds 设置太短一小时内连续触发多次重启服务水平直接腰斩。探针参数应该随应用发布一起评审把它当成业务代码的一部分来看待。最后想说的是Pod 虽然是最小调度单元但真正把它用明白需要你理解它在你整个工作负载体系里的位置以及它和控制器、探针、调度器、资源模型之间的协作方式。这篇文章写的这些内容都是我实际踩坑之后的总结希望能让你在学习和排障的路上少走几个坑。如果你在动手过程中遇到了这上面没有覆盖到的怪问题建议先把kubectl describe pod的输出完整贴给身边同事或社区很多问题在描述信息里已经有答案。
阅读完成 · 觉得有帮助?