☰
Kubernetes Pod 完全解读:从 YAML 到探针、资源与调度
2026/10/21 22:05:01 网站建设 项目流程

刚接触 Kubernetes 的开发者,十个里有八个都卡在 Pod 这个门上。倒不是因为它有多难,而是因为它和 Docker 容器的概念太像了,容易让人产生"这个我学过"的错觉。实际写第一个 YAML 时,有人把端口映射写上去了,有人把 restartPolicy 忽略了,有人发现容器怎么删都删不掉,还有人看着 Pending 状态半天不知道去哪里找原因。

这个系列前两篇分别讲了集群搭建和 kubectl 常用操作,这篇就彻底停在 Pod 这一个对象上,把它掰开揉碎讲清楚。名字叫精简版,但生产环境里真正用得上、需要理解到的核心点,一个都不会少。Pod 是 Kubernetes 里你操作频率最高的东西,也是后面学习 Deployment、Service、调度策略的基石,把这篇文章消化了,你再看后面那些控制器,会轻松很多。

1. 为什么说 Pod 是"最小调度单元",不是"容器的包装袋"

1.1 从 Docker 到 Pod:变化的不只是名字

Docker 的时代,我们习惯把"一个容器"当成"一个应用"。到了 Kubernetes,最小调度单位变成了 Pod,而不是容器。这个变化背后藏着一个核心转变:Kubernetes 不再假设一个应用只能由一个进程组成,而是允许你把一群"必须紧密协作、共享命运"的进程打包在一起管理。

一个 Pod 里的多个容器,共享同一个网络命名空间和存储卷。网络共享意味着它们可以互相用 localhost 访问,存储共享意味着它们能读写同一份数据。这解决了一个实际问题:有些进程之间耦合度极高,放在同一台机器上才靠谱,但你又不想让它们挤进同一个容器镜像里。最典型的例子是主业务容器加 Sidecar 容器的组合——比如一个 Web 服务配一个日志采集容器,日志采集器直接从共享目录里读文件,不再需要额外的 agent 进程。这比在镜像里塞一堆进程干净得多,也方便单独升级、替换某个辅助进程。

1.2 用"一桌人吃饭"来理解 Pod 的内部结构

把 Pod 想象成一张餐桌,Pod 里的容器就是围坐在桌边的人。所有坐在同一张桌子前的人,用着同一个桌面(共享的存储卷),可以直接递盘子(通过 localhost 互相访问),但每个人手里的筷子碗是自己的(各自的进程、文件系统、资源限制)。这张桌子的存在感很强:只要桌子被撤走,坐在桌上的人全部都要走。Pod 就是这样一个"一起存活、一起结束"的打包单位。

这个类比还能推导出两个实际结论。第一,同一个 Pod 里的容器,端口绝对不能冲突,因为只有一个网络命名空间,localhost:8080 只能被一个进程占用。第二,Pod 的 IP 是这组容器共同的门牌号,外部访问一个 Pod 里的不同容器,只能通过不同端口来区分。记住了这两点,很多写在 YAML 里的奇怪报错你一眼就能看出来原因。

1.3 什么时候该拆、什么时候该合

这是新手面对的第一个真实设计问题:两个组件到底放一个 Pod 还是放两个?我一般只问自己三个问题:它们是否必须运行在同一台节点上?它们共享数据或本地通信吗?它们是不是"要么一起活着,要么一起死去"的关系?如果三个答案都是"是",那就该放同一个 Pod。比如一个业务进程需要从一个本地文件里读配置,而另一个进程负责每隔一段时间从远端拉取配置写进这个文件,这两个就是典型的共生关系,拆开了反而要引入额外的网络配置和状态同步。

反过来,如果两个服务只是单纯的调用关系,比如订单服务调支付服务,那绝对不合适塞进同一个 Pod。它们需要独立伸缩、独立更新、独立承受故障。强耦合的才放一起,弱耦合的必须分家,这个判断标准比任何理论都好用。

2. 从 YAML 到运行:一个 Pod 的诞生全程

2.1 最小可运行 Pod 的逐字段拆解

先写一个能跑起来的 Pod YAML,这是所有 Pod 配置的基础骨架:

apiVersion: v1 kind: Pod metadata: name: nginx-demo labels: app: nginx spec: containers: - name: nginx image: nginx:1.25-alpine imagePullPolicy: IfNotPresent ports: - containerPort: 80

apiVersion 是 v1,kind 是 Pod,这两行是给 Kubernetes API 看的"开箱条码"。metadata 里最重要的是 name 和 labels,name 是 Pod 的唯一标识,labels 则决定了这个 Pod 如何被 Service、Deployment 这些上层对象"认领"。spec 是 Pod 的规格声明,里面至少得有一个 containers 列表。

containers 里的 image 指定镜像,imagePullPolicy 有三个值:Always、IfNotPresent、Never。IfNotPresent 表示本地已有镜像就不去远端仓库拉取,在离线环境或想省流量时很实用;Always 表示每次创建容器都重新拉,后面接的是 tag 可变的镜像时建议用这个;Never 则强制只用本地镜像。最坑的场景是把本地构建的镜像直接部署上去,没设 imagePullPolicy,碰巧标签又不带 latest,节点上没有镜像时就会一直报 ImagePullBackOff。

ports 里的 containerPort 很多新人当成"我们要暴露端口",其实它只是声明一下"我这个容器监听了 80 端口",属于元信息,不产生任何实际效果。真正把流量送进来需要 Service 或者其他入口机制。这个声明不写也不影响容器运行,但写上可以让 kubectl describe 时信息更完整。

2.2 创建、查看、进容器、删掉,完整命令链路

Pod YAML 写好后,我会按这个顺序走一遍:

# 创建 Pod kubectl apply -f nginx-demo.yaml # 查看 Pod 状态,-o wide 能看到运行节点和 IP kubectl get pod -o wide # 持续观察状态变化,新部署时很有用 kubectl get pod -w # 查看完整 YAML,看实际调度结果 kubectl get pod nginx-demo -o yaml # 排错第一命令:describe 会展示事件和每个容器的状态细节 kubectl describe pod nginx-demo # 看业务日志 kubectl logs nginx-demo # 进入容器 kubectl exec -it nginx-demo -- /bin/sh # 删除 Pod kubectl delete pod nginx-demo

这里面 kubectl describe 是排错神器,它会在最底部列出 Pod 运行至今的所有事件,比如镜像拉取失败、调度失败、探针检查失败、被 OOM 杀死等等。kubectl logs加--previous可以看容器上一次退出前的日志,容器发生重启时这个参数几乎是必用的,不然看到的永远只有重启后的空日志。

2.3 一个总是被忽略的字段:restartPolicy

scheduler 会把裸 Pod 调度到某个节点,然后节点上的 kubelet 负责维护它,重启策略 restartPolicy 就是 kubelet 的行为准则。默认值是 Always,意思是只要容器退出,kubelet 就会重启它——哪怕是业务进程正常跑完退出也一样。很多人第一次测试一个跑完就结束的脚本任务时,发现 Pod 一直在重启,就是因为忘了把 restartPolicy 改成 OnFailure 或 Never。

裸 Pod 在生产环境不建议直接使用。它没有自愈能力,节点挂了 Pod 就永远消失了,也不支持滚动更新、副本管理这些高级能力。但学习阶段用裸 Pod 是最干净的,它把概念边界缩到最小,让你专注理解 Pod 本身。等理解透了,再切换到 Deployment 管理同样这套配置就会非常顺。

3. Pod 的七种生命状态与"假死"现场

3.1 五种相位(phase)到底表示什么

相位说的是 Pod 这个整体的生命周期阶段,反映的是"做成了一件事还是没做成"。看 kubectl get pod 的 STATUS 列,你会遇到这几种:

  • Pending:已经被 API Server 接受,但目前还没完成调度,或者镜像还在拉取。很多人看到 Pending 就以为只是启动慢,其实资源不足、节点有污点、镜像名写错,都会让 Pod 一直停在 Pending。
  • Running:至少有一个容器还在运行状态。
  • Succeeded:所有容器都成功运行完毕退出,且退出码是 0。
  • Failed:至少有一个容器以非 0 退出码结束,或者被系统强制终止。
  • Unknown:kubelet 失联,API Server 拿不到这个 Pod 的状态。这个状态很低概率出现,出现的时候基本意味着节点有问题。

还有一个常见的 STATUS 是 CrashLoopBackOff,它不在五种相位里,而是一种容器级别的循环故障状态:容器启动后马上就崩,kubelet 按指数退避策略不断重启它,从 10 秒递增到 5 分钟,每次重启间隔越来越长。看到这个状态,你要做的不是删 Pod 重来,而是排查容器为什么崩。

3.2 容器级别的三个子状态

容器比 Pod 多一层状态机。ContainerWaiting 是容器还在等待条件满足,比如镜像没拉完、存储卷没挂上;ContainerRunning 是容器正在运行;ContainerTerminated 是容器已结束运行,会有一个退出码。排错时真正有价值的是退出码:137 表示被 SIGKILL 杀死,143 是收到 SIGTERM 后终止,0 是正常退出。结合退出码和 describe 里的事件,大部分问题都能定位。

有一个典型的"假死"场景,主业务进程变成了 chroot 或休眠工具,或者跑成了后台守护进程,真正干活的服务根本没起来,但这种 Pod 的 STATUS 依然是 Running。容器在跑,业务不在服务,这种故障最迷惑人。另外一个常见场景是容器镜像里命令行带了前台参数,拉起来马上就执行完退出,restartPolicy 又是 Always,于是陷入 CrashLoopBackOff。遇到这种情况,先kubectl logs 名字 --previous看历史日志,再确认一下镜像默认的启动命令是不是阻塞式的。

3.3 InitContainers:主容器启动前的"前置条件"

InitContainers 是 Pod 里一种特殊容器,它们严格按顺序依次执行,全部必须成功退出后,主容器才开始启动。如果中间有一个失败,整个 Pod 会反复重跑所有的 init 容器。它适合干这些事:给数据卷初始化权限或目录结构、等待某个依赖服务就绪、执行数据库迁移、把启动所需的配置文件生成好。

下面是一个最简单的示例,init 容器等待某个 TCP 地址可达后再启动业务端:

spec: initContainers: - name: wait-for-db image: busybox:1.36 command: ['sh', '-c', 'until nc -z -w 5 db-service 3306; do echo waiting; sleep 2; done'] containers: - name: app image: my-app:latest

这里有个经验点:放在 init 容器里的探测逻辑一定要设置超时时间,不然应用里的 bug 或网络策略变更会导致 init 容器无限等待,主容器永远起不来。另外 init 容器也消耗计算资源,在资源紧张时,资源配额要把它占用的部分算进去。

3.4 Pending 状态的完整排查路径

如果 Pod 一直 Pending,我会按这个顺序排:

  1. kubectl describe pod xxx看 Events,调度器会直接告诉你哪一步失败。
  2. 如果是调度失败,kubectl get nodes --show-labels看节点标签和资源剩余情况。
  3. 检查是否设置了资源 requests,节点剩余可分配资源不足时调度器会卡住。
  4. 检查节点是否有污点(taints),看 Pod 是否需要容忍(toleration)才能调度上去。

这个顺序基本能覆盖九成以上的 Pending 原因。特别是资源不足这条,在共享集群里非常常见,不是你的 Pod 有问题,而是整个集群塞满了。

4. 探针(Probe):让 k8s 真正知道你的应用"活着"和"能用"

4.1 进程活着不等于业务正常

把容器跑起来只是第一步,Kubernetes 还需要回答一个更关键的问题:这个容器里的服务到底还能不能干活?一个 Java 进程可能还活着,但线程池已经耗尽,HTTP 请求全部超时;一个数据库容器可能没崩,但磁盘满了,写不进去任何数据。这些情况下进程存在,可业务已经"死了",Kubelet 光看进程状态是没法识别的。

探针(Probe)就是 Kubelet 定期对容器做的体检,目的是让系统知道应用是否真的健康。三种探针分工不同:

  • livenessProbe(存活探针):只管"要不要重启这个容器"。如果失败次数超过阈值,kubelet 会杀掉容器并按重启策略重新拉起。
  • readinessProbe(就绪探针):只管"要不要把这个 Pod 的 IP 加进 Service 后端"。失败时流量不会打到这个 Pod 来,但容器不会被重启。
  • startupProbe(启动探针):给那些启动极慢的程序一个安全的缓冲期,在这期间不做 liveness 判断。

4.2 三种探针的典型设置

一个常见的配置如下:

spec: containers: - name: app image: my-app:latest startupProbe: httpGet: path: /healthz port: 8080 periodSeconds: 2 failureThreshold: 30 livenessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 5 periodSeconds: 10 timeoutSeconds: 3 failureThreshold: 3 readinessProbe: httpGet: path: /readyz port: 8080 initialDelaySeconds: 5 periodSeconds: 5 failureThreshold: 2

startupProbe 用较短的检查周期加较大的失败阈值,给应用一个"慢慢来"的窗口;livenessProbe 关注持续健康,失败 3 次就重启;readinessProbe 关注业务可用性,一旦失败就摘流量。要注意的是 readiness 的检查路径和业务健康不一定相同,比如主备切换中的节点、正在执行长任务的节点,都可以通过 readiness 返回失败来主动退出流量池。

三个关键参数的直觉感受:periodSeconds是体检频率,设置太大会让故障发现变慢,设置太小又会让健康检查本身成为性能负担;timeoutSeconds是单次检查超时时间,有些慢接口在 timeoutSeconds 太小的情况下会产生大量超时误判;failureThreshold决定容忍度,出现偶发抖动时不会立刻重启或摘流量。

4.3 三种探针类型怎么选

httpGet 是最直觉的方式,只要接口返回 2xx 或 3xx 就算健康,适合所有业务接口,也是我用得最多的。tcpSocket 只验证 TCP 端口能连上,适合那些没有 HTTP 接口还不好加的内部服务,它判断的是"端口通没通",不够细但胜在简单。exec 执行容器内命令,按退出码判断,适合检测内部状态,比如检查 lock 文件、执行健康脚本等。

探针踩过的坑不少,值得单独提醒的有三个。第一,健康检查接口本身不能带太多计算逻辑,我一个回调接口曾把全量数据灌进去做健康检查,探针一打,CPU 直接飙高,影响正常请求,健康检查路径必须轻量化。第二,探针日志要严格控制输出,如果 exec 方式里命令大量打日志,会把容器日志刷爆。第三,httpGet 的端口一定要监听在容器内的正确端口上,端口改了探针配置没同步改的场景我见过太多次。

4.4 探针引出的"滚动更新死锁"回忆

在实际维护里,探针配错会导致很隐蔽的故障。有一次我给一个应用加了 readinessProbe,但把检查的路径写在了业务主流程后面,结果新版本启动后,业务流量还没完全初始化,readiness 一直不通过,控制器就不断创建新 Pod,旧 Pod 又一直守在线上,结果新版本永远没流量,还造成了一轮资源抢占。后来把探针路径改成独立的、轻量的健康接口才恢复正常。这类问题一旦发生很难一眼看到原因,因为在 STATUS 层面,新 Pod 还是 Running,只是 readiness 一直"不满足"。

5. resources:一句配置引发的调度与驱逐差异

5.1 requests 与 limits:两个不同的作用域

resources 字段是最被人低估的一个,很多初学者甚至完全不写。但它的影响贯穿调度、运行、驱逐三个阶段。requests 用来告诉调度器"我需要这么多资源才能工作",是调度决策的依据;limits 则用来限制运行时的资源上限,是 kubelet 执行的硬性约束。

CPU 是"可压缩资源",超过 limits 会被限流,表现为 CPU 使用率被强制压低,但进程不会死。内存是"不可压缩资源",一旦超过 limits,容器会被 OOM(内存溢出)机制直接杀掉。这就导致两种资源超限的后果完全不同:CPU 超了是变慢,内存超了是死亡。所以内存的 limits 是生产环境最需要保守设置的一项。

5.2 QoS 等级:资源冲突时的"谁先被牺牲"

Kubernetes 会根据容器的 requests 和 limits 设置,把 Pod 划分成三个 QoS(服务质量)等级。这个等级决定了节点资源紧张时,谁会先被驱逐:

  • Guaranteed(有保障):每个容器都设置了 requests,且 requests 与 limits 完全相等。
  • Burstable(充满弹性):至少有一个容器设置了 requests 或 limits,但不完全相等。
  • BestEffort(尽力而为):所有容器都没有设置 requests 和 limits。

驱逐顺序是 BestEffort 先走,然后是 Burstable 里实际用量超 requests 的,最后才是 Guaranteed。节点内存紧张时,kubelet 会优先清退"最不受保护"的 Pod。生产环境里,最理想的状态是让核心业务达到 Guaranteed,这既是对业务的保护,也能避免和服务争抢时被动。

5.3 OOM 与 Eviction 现场怎么判断

描述了这么多资源概念,落到实际排查就是两个场景。一个是容器退出码 137,kubectl describe pod xxx里能看到一行OOMKilled,这表示容器超过了自身 limits,被内核 OOM killer 杀了。另一个是 Pod 被驱逐(Evicted),原因是节点级的内存压力、磁盘压力等问题,和单容器超限是不同层级的两回事。

实操中区分两种情况的习惯是:先看容器退出码,137 加 OOMKilled 基本锁定是容器级内存超限;再kubectl top pod看实际内存占用,和 limits 对比一下,往往能发现 limits 设得太低,或者业务真的有内存泄漏;最后看节点状态,kubectl get node和kubectl describe node里的压力条件,如果节点本身有 MemoryPressure,那还要关注节点级的驱逐策略。

5.4 设多少合适?一个保守的起步公式

我一般把 requests 和 limits 的初始值定在:CPU requests 为平时稳定使用的 60% 到 80%,limits 为峰值使用上限加 20% 余量;内存 requests 按 JVM 堆等固定开销加 30% 的余量,limits 按峰值计算的 1.2 到 1.5 倍。这两项观察一两周真实监控后逐步收敛。除非有明确理由,不要一开始就设得极其贴近真实值,给调度和运行留出缓冲。

6. 让 Pod 被正确调度和优雅退出

6.1 节点选择:nodeSelector 和 nodeAffinity,以及污点

集群里的节点并不都是一样的。有的节点挂着 GPU,有的节点是高性能磁盘,有的节点专门给某些团队用。这就要用到调度约束。nodeSelector 是最简单的硬性匹配,要求节点必须带指定标签,例如disktype: ssd就只调度到有这个标签的节点上。它的缺点是表达能力弱,只能做"必须等于"的匹配。

nodeAffinity 更进一步,分为 required(必须满足)和 preferred(尽量满足)。preferred 会把满足条件的节点排在前面,不满足也能调度,只是优先级低。这在多可用区场景、节点资源水位不均衡的场景里比 nodeSelector 灵活太多。

污点(taint)和容忍(toleration)是另一套机制,和亲和性正好互补。亲和性解决"Pod 想往哪跑",污点解决"节点不想让某些 Pod 跑"。给节点打上一个 NoSchedule 污点后,不带对应容忍的 Pod 就不会被调度过来。常见的用途是给专用节点加污点,确保只在需要时调度指定服务过去。

6.2 优雅退出的完整流水线

Pod 被删除或滚动更新时,不会直接被一刀砍掉。kubelet 收到终止信号后会执行这样一个流程:先执行 preStop 钩子(如果配置了),然后向主容器发送 SIGTERM 信号,接着等待终止宽限期(terminationGracePeriodSeconds,默认 30 秒),宽限期内进程没有自行退出,最后才发送 SIGKILL 强制杀掉。

一段简单的 preStop 示例:

spec: containers: - name: app image: my-app:latest lifecycle: preStop: exec: command: ['sh', '-c', 'sleep 5 && kill -QUIT $(cat /tmp/pid)']

preStop 的存在价值在于,有些应用收到 SIGTERM 后的清理逻辑不完整,或者需要告诉上游注册中心"我要下线了",而主进程自身不做这件事。把所有下线动作用 preStop 接管,做成可预期的流程,比在应用里改造要快。

6.3 一个让宽限期失效的常见场景

终止宽限期最怕两类情况:一类是 preStop 脚本里 sleep 时间比宽限期还长,进程还没做完下线操作就被 SIGKILL 了,白等;另一类是应用收到 SIGTERM 后开始执行耗时的资源清理,但应用内部没有监听 SIGTERM 的逻辑,等于直接忽略,到了宽限期照样被杀。这两种情况本质上都是"信号进来后的动作超时"。解决办法是把宽限期调大,同时确保应用代码真的监听了 SIGTERM 并实现了优雅退出。宽限期也不建议设得无限大,超过一定时间后,集群的滚动更新和故障转移都会被拖慢。

6.4 Pod 的 IP 是会"漂移"的

最后提一个常被新手忽略的运维现实:Pod 的 IP 是不稳定的。Pod 被删除、重新调度、在节点间迁移后,IP 都会变化。因此永远不要手动把 Pod 的 IP 缓存到本地配置或硬编码进代码里,要实现跨 Pod 访问,必须经过 Service 或 DNS 发现机制。这也是为什么 Deployment 上层永远挂着一层 Service,所有流量都走 Service 的稳定入口,由它把请求转发到后端 Pod。

我在实际运维里见过最离谱的事情,是有人把某个 Pod 的 IP 写进了另一套系统的白名单,Pod 一重建,流量全部中断。Kubernetes 的设计哲学就是"实例会死、IP 会变、只有约定不变",不要把约定建立在易变的 IP 上,要建立在 Service 和标签上。

写在最后:先把 Pod 的"死法"背熟

Pod 详解讲到这里,核心内容基本覆盖了:它的内部结构、YAML 写法、生命周期、探针、资源管理、调度和优雅退出。总结这些经验的初衷是,我在实际维护的大量问题里,九成以上都发生在"Pod 是怎么死的"这个方向——要么探针配错导致误杀,要么内存 limits 设错导致 OOM,要么优雅退出超时导致业务中断。先把死法搞清楚了,再谈怎么把它跑得更好。Pod 是 Kubernetes 的最小单元,但它的每个字段都牵扯到集群的调度、网络、存储、监控多个维度,值得多花时间认真研究。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询