☰
Kubernetes Pod 资源清单实战:从生命周期到 YAML 编写与排障
2026/10/21 13:02:22 网站建设 项目流程

1. 从实操看 Pod:它在集群里的真实角色和生命周期

1.1 为什么集群的最小调度单位是 Pod 而不是容器

上一轮搭建集群的时候,很多人会有个疑问:容器明明可以一个个直接跑,为什么 k8s 偏偏要在外面再包一层 Pod?我在第一次接触这个设计时也犯过嘀咕。直到自己真实去部署一个前后端联调的小项目,才明白 Pod 这个抽象有多关键。

核心原因是:容器是进程级的隔离单元,而 Pod 是“一组需要共享网络和存储的容器”的调度单元。打个比方,如果你要把日志采集 sidecar 和应用主容器放到一台机器上,希望它们能通过 localhost 互相访问,在裸容器时代你得自己解决端口共享、存储共享、生命周期绑定这些事;在 k8s 里,Pod 天然把这些问题都包住了。Pod 内的容器共享同一个 network namespace、同一个 IPC namespace,还能挂载同一个 volume,所以它们之间的通信成本几乎为零,而且 Pod 作为一个整体被调度、被重启、被伸缩。

实际项目里最常见的组合是“主业务容器 + 日志采集容器 + 临时工具容器”。例如在模拟项目 X 中,我用一个 Pod 同时运行了 Nginx 和应用容器,应用把访问日志写到共享卷里,Nginx 负责对外统一入口。如果没有 Pod 这层封装,我就得自己写脚本去协调两个容器的启停顺序和资源归属,这违背了容器编排的初衷。理解了这个“容器组”的概念,再看后面所有 Pod 管理的操作,就不会觉得只是记命令,而是有逻辑在里面。

1.2 Pod 生命周期状态机:别等出问题才去翻文档

运维 Pod 的人,几乎每天都要和状态打交道。kubectl get pods 输出的那一列 STATUS,其实对应一个完整的状态机,搞清楚它能省掉大量排查时间。

  • Pending:Pod 已经被 API Server 接受,但还没有完成调度,或者镜像还在拉取中。如果卡在 Pending 很久,多半是节点资源不足、存在未匹配的污点或节点选择器。
  • ContainerCreating:容器正在创建,通常是镜像拉取阶段或存储卷挂载阶段。这个状态是过渡态,长时间停留多半是镜像仓库拉不动,或者存储卷权限有问题。
  • Running:Pod 至少有一个容器在运行中。这个状态只表示进程活着,不表示业务健康。
  • Succeeded:Pod 内所有容器都正常退出,退出码是 0。一般用于一次性任务,比如 Job。
  • Failed:Pod 内至少有一个容器以非零码退出,或者被系统终止。
  • Unknown:API Server 与节点失联,拿不到 Pod 的最新状态。这种情况要先检查节点本身,而不是盯着 Pod 看。

我个人的经验是,看到 Running 别急着觉得“稳了”,一定要结合后面的 readinessProbe 和日志判断业务是否真的通了。就像你看到一台服务器开机成功,不代表上面那个 Web 服务就一定能返回 200。

1.3 用 kubectl 快速管理单个 Pod 的高频命令

在进入资源清单之前,先把命令行下最常用的几组操作讲清楚。虽然我们提倡用 YAML 管理,但排障和临时验证时,命令行的速度是最快的。

  • 创建与查询:kubectl run 可以快速创建 Pod;kubectl get pods -o wide 可以看到 Pod 落在哪个节点、内网 IP 是什么。我经常在排查网络问题时加 -o wide,因为不仅要看到状态,还要看到 IP 和 NODE 列。
  • 进入容器:kubectl exec -it pod-name -- /bin/sh。如果 Pod 有多个容器,需要加 -c 指定容器名。这个操作频率极高,尤其是镜像里没有 curl、ping 等工具时,我都是靠 exec 进去用宿主机网络排查问题。
  • 查看日志:kubectl logs pod-name 看单容器日志;多容器用 -c 指定;如果容器已经 CrashLoopBackOff,用 kubectl logs pod-name --previous 查看上一次退出时的日志,这个参数在排查崩溃问题时是救命级的。
  • 删除与强制清理:kubectl delete pod pod-name 是优雅删除;如果遇到卡在 Terminating 的 Pod,可以加 --force --grace-period=0 强制清理,但这个操作要谨慎,只适用于确定不会影响数据的情况。

这些命令是 Pod 管理的“快拳”。真正要持久化、要可复用、要进 CI/CD 的,还是下一部分要讲的声明式资源清单。

2. 两种管理方式对比:命令式一时爽,声明式保平安

2.1 kubectl run 创建的 Pod,最后都去哪儿了

很多人上手 k8s 的第一条命令就是 kubectl run,我最早也是。但用过一段时间你就会发现,直接用命令创建的 Pod 有天然的短板:它不是“可追踪”的配置。

首先,kubectl run 生成的 Pod 是你敲的这条命令的历史,而不是代码仓库里的一个文件。团队协作的时候,别人根本不知道这个 Pod 是怎么配置的,镜像版本、资源限制、探针这些都散落在聊天记录和个人终端里。其次,kubectl run 创建的 Pod 不受 ReplicaSet 或 Deployment 管理,它挂了就是挂了,k8s 不会自动帮你拉起来。你在生产环境里想要的高可用、滚动更新、回滚能力,在这种“裸 Pod”上统统不存在。

所以我的建议是:kubectl run 可以用于本地测试、临时验证某个命令或镜像行为,但凡是需要长期运行的业务,一律使用声明式清单。这也是为什么这篇内容重点放在资源清单上——命令只是敲门砖,清单才是你真正要写的东西。

2.2 声明式资源清单的工作流:写 YAML 而不是敲命令

声明式管理的核心思路是:你告诉 k8s “我想要什么状态”,k8s 自己想办法把现状变成目标状态。在 Pod 管理这个层面,最常见的承载对象其实是 Deployment,而不是裸 Pod,但先搞懂 Pod 清单,后面扩展 Deployment、StatefulSet、DaemonSet 才有基础。

一个标准流程是这样的:

  1. 编写 Pod 的 YAML 文件,描述镜像、端口、环境变量、资源限制、探针等。
  2. 使用 kubectl apply -f pod.yaml 提交到集群。
  3. 之后每次修改,都修改 YAML 文件,再重新 apply 一次。
  4. 通过 git 保存 YAML 文件的变更历史,随时可以回滚。

相比命令式,这套流程的优势是显而易见的:配置可审计、可评审、可回滚。我在公司团队里推行这套方式后,最直观的变化是“出错之后知道反过来查哪个文件”,而不是让同事回忆“上次用哪个参数创建的”。

2.3 为什么新手也要从 YAML 学起,而不是只背命令

可能有人觉得,我只是自己玩一玩集群,背几条命令足够了。但实际上,命令背后的参数就是 YAML 字段的另一种表达。比如 kubectl run 后面加的 --image、--port、--limits,都可以一一对应到清单里的 spec.containers[].image、ports、resources。你如果不懂 YAML 的组织逻辑,遇到命令行不支持的参数时会直接卡住。

另一个重要理由是:kubectl explain 是学习清单的最佳入口。比如你输入 kubectl explain pod.spec.containers,它会告诉你每个字段的类型、含义、默认值,这比翻任何教程都准确。我强烈建议把 kubectl explain 当作随身手册,记住大骨架之后,细节全部靠它现场查。

3. 资源清单核心字段:拿到一份 YAML,到底先看哪里

3.1 从 apiVersion 到 spec:一份清单的基本骨架

任何 Kubernetes 资源清单都逃不开四段结构:apiVersion、kind、metadata、spec。先记住这个骨架,再看细节就不会乱。

  • apiVersion:表示资源所属的 API 组和版本。Pod 属于核心组,所以是 v1。Deployment 属于 apps 组,所以是 apps/v1。版本写错了,apply 时会直接报 no matches for kind。
  • kind:资源类型,比如 Pod、Deployment、Service、ConfigMap。
  • metadata:元数据,最重要的是 name 和 namespace。同一种资源在同一 namespace 下不能重名,这是 k8s 的硬性规则。labels 也在这里定义,后面做选择器、做关联都靠它。
  • spec:期望状态描述。不同 kind 的 spec 结构完全不同。

在这个骨架里,真正需要你动脑的只有 spec。metadata 基本是复制模板,apiVersion 和 kind 也是固定值。所以我分析一份陌生的 YAML,永远是先看 spec,再看 metadata 里有没有 labels。

3.2 容器描述:image、imagePullPolicy 与 command 的取舍

spec.containers 是一个数组,意味着一个 Pod 可以塞多个容器。每个容器最常用的字段如下:

  • name:容器名。exec、logs 指定 -c 时用的就是它,不要起太随意的名字。
  • image:镜像地址。需要注意容器会按 tag 决定是否拉取,这受 imagePullPolicy 控制。
  • imagePullPolicy:Always、IfNotPresent、Never 三选一。默认规则是:如果 tag 是 latest 或用数字 tag,默认 Always;否则默认 IfNotPresent。我踩过的坑是:本地测试时改了代码重新构建镜像,却忘了重新打 tag,结果 Pod 一直用旧镜像,排除了半天才想起 imagePullPolicy 是 Always 且本地仓库没有缓存清理机制。
  • command 和 args:对应 Docker 镜像里的 ENTRYPOINT 和 CMD。我个人的习惯是:镜像尽量做到自包含,command 和 args 能少写就少写。因为一旦你在 YAML 里指定了 command,它会覆盖 Dockerfile 里的 ENTRYPOINT,行为可能和本地 docker run 时完全不同。
  • ports:声明容器监听的端口。这里有个容易误解的点:ports 只是“宣告”,不是“自动暴露”。即使你不写 ports,应用在容器里监听 80 照样能工作;写上去更多是给人看,给服务发现工具提供信息。

3.3 CPU 和内存的 requests/limits:容量分配的心算方法

资源清单里最让新手头疼的就是 requests 和 limits。我用一个通俗比喻来解释:requests 是“最低保障”,limits 是“上限封顶”。

CPU 的单位是 core,1 核就是 1 个完整的 CPU。你经常看到 100m,m 是 milli-core,100m 就是 0.1 核。调度器用 requests 来选节点,比如一个节点有 2 核可分配,已经有两个 Pod 各 request 了 800m,那剩下的可分配就只有 400m,新 Pod 如果 request 500m 就调度不上去了。搞清楚这一点,就能理解为什么集群明明看着很空闲,Pod 却一直 Pending。

内存的单位是字节,常用的是 Mi、Gi。64Mi 是 64 兆字节,注意这里不是 64Mb 比特。为什么生产环境一定要设置 limits?因为如果没有 limits,一个进程可能无限吃内存,最后把节点打爆,引发宿主机 OOM,甚至拖垮同节点其他 Pod。而设了 requests 不设 limits 会导致 QoS 等级变成 Burstable,行为较复杂,我建议线上环境 requests 和 limits 必须成对出现。

心算分配的时候,我一般遵循“一核一 Gi,留两成余量”的经验:requests 按实际负载的 80% 估,limits 按峰值或两倍估。比如模拟项目 X 里的一个 PHP 应用,日常负载大约 300m CPU、300Mi 内存,我就写 requests CPU 200m、内存 256Mi,limits CPU 500m、内存 512Mi。这个配法不是拍脑袋,而是看监控曲线之后做的决策。

3.4 探针与重启策略:让故障恢复自动化

Pod 管理的核心目标之一,就是让 k8s 替你处理故障。探针(Probe)就是实现这个目标的关键组件。

  • livenessProbe:存活探针,判断容器是否还活着。失败后 kubelet 会根据 restartPolicy 杀死并重启容器。相当于“心跳检测”。
  • readinessProbe:就绪探针,判断容器是否准备好接收流量。失败的话,Service 的 Endpoints 里会临时摘掉这个 Pod。相当于“营业状态牌”。
  • startupProbe:启动探针,用于启动很慢的应用。在 startupProbe 成功之前,livessProbe 和 readinessProbe 不会启动,避免在应用启动阶段就反复被杀。

探针的探测方式有 exec、httpGet、tcpSocket 三种。我最常用 httpGet,因为绝大部分业务都暴露 HTTP 接口,比如 livenessProbe 打 /healthz,readinessProbe 打 /ready。写探针时有两个重要的时间参数:initialDelaySeconds 是启动后等多久才开始探测,periodSeconds 是探测间隔。首次给一个启动慢的 Java 应用配探针时,我把 initialDelaySeconds 设为 5,结果应用还在加载类,就被连续杀了好几次,最后查到日志才知道问题。后来我把 startupProbe 加上,initialDelaySeconds 调大,问题立刻消失。

restartPolicy 有三个值:Always、OnFailure、Never。Pod 的默认是 Always,也就是说只要容器非正常退出,kubelet 就会重新拉起来。对长期运行的服务型 Pod,默认值就行;对跑一次性批处理的 Job,才需要改成 OnFailure 或 Never。

4. 从需求到落地:手写一份 Pod 资源清单的完整过程

4.1 一个真实可复现的模拟需求

假设我们要在测试环境部署一个轻量级的 Web 服务,镜像名定为 registry.example.local/demo-web:1.2.0。业务上它需要接管 8080 端口,接口 /healthz 返回 200 表示存活,/ready 在依赖的本地缓存初始化完成后返回 200。由于测试环境机器内存有限,我们希望把资源占用控制住,同时允许 k8s 自动重启异常退出的容器。

这个需求虽然简单,但已经覆盖了 Pod 清单的绝大多数核心字段。下面我把它逐步写成 YAML。

4.2 逐步完善清单:从最小可用到符合生产习惯

第一步,先写一个最小可用的版本:

apiVersion: v1 kind: Pod metadata: name: demo-web namespace: default spec: containers: - name: demo-web image: registry.example.local/demo-web:1.2.0

这个文件 apply 后能跑,但不够“生产化”。第二步,加上端口声明和两个探针:

apiVersion: v1 kind: Pod metadata: name: demo-web namespace: default labels: app: demo-web env: test spec: containers: - name: demo-web image: registry.example.local/demo-web:1.2.0 imagePullPolicy: IfNotPresent ports: - name: http containerPort: 8080 livenessProbe: httpGet: path: /healthz port: http initialDelaySeconds: 10 periodSeconds: 5 timeoutSeconds: 2 failureThreshold: 3 readinessProbe: httpGet: path: /ready port: http initialDelaySeconds: 5 periodSeconds: 5 timeoutSeconds: 2

这里注意一个小细节:探针的 port 可以直接写 http,对应上面 ports 里的 name。这样如果以后端口变了,只需要改 ports.name 的映射关系,探针不用跟着改,提高可维护性。我在多次维护里体验到这个小技巧的好处。

第三步,加上 resources 限额和环境变量:

resources: requests: cpu: 200m memory: 256Mi limits: cpu: 500m memory: 512Mi env: - name: APP_ENV value: "test" - name: CACHE_SIZE value: "1024"

到这一步,这份清单已经可以在测试环境稳定部署了。如果还想要更完整,可以在 metadata 加上 namespace,或者在 spec 里加上 tolerations 调整调度行为,但对单个 Pod 来说不是必须的。

4.3 校验、提交与回顾:apply 之前的最后一道闸

写好的 YAML 不要直接 apply,先做三件事。

第一件事是语法校验。我常用 kubectl create --dry-run=client -f demo-web.yaml -o yaml 这种干跑模式,它会在客户端做一次校验,有问题会直接报错,不会去碰集群。第二件事是看字段合法性,我会用 kubectl explain pod.spec.containers.resources 这种命令逐一确认没把握的字段。第三件事是检查命名空间和 labels,确认不会和已有资源冲突,尤其是 namespace 忘写时 Pod 会落进 default,这在测试环境很容易造成资源混乱。

确保没问题后再执行:

kubectl apply -f demo-web.yaml

然后立刻用 kubectl get pods -o wide 和 kubectl describe pod demo-web 观察状态。如果状态卡在 ContainerCreating,describe 里会给出具体事件,这是最直接的线索。我见过不少人一遇到 Pod 起不来就反复删了建、建了删,其实大部分答案都写在 Events 里。

5. 常见问题与排查技巧实录:Pod 管理里那些真实踩过的坑

5.1 Pod 一直 Pending,到底卡在哪一环

Pending 是新手最常遇到的状态,也是我排查次数最多的一个。列一个排查清单给你,按顺序过一遍,基本都能找到原因。

第一,看调度器有没有选择到节点。kubectl describe pod 里如果出现 0/1 nodes are available,后面会跟着具体原因,最常见的是 insufficient cpu、insufficient memory,这时候去 kubectl describe node 看各节点剩余资源即可。第二,看是否有 nodeSelector 或亲和性配置,导致所有节点都不满足条件。第三,看是否有 pending 状态下的 taints 和 tolerations 冲突。一个带有专用污点的节点,默认是拒绝普通 Pod 上去的,需要你在清单里显式声明 tolerations。

这几个场景我都真实遇到过。最隐蔽的一次是 nodeSelector 写错了键值,把测试环境的标签写成了disktype: ssd,但集群里根本没有节点带这个标签,导致 Pod 一直 Pending,describe 里只有一句 “didn't match node selector”。遇到这种事情,别慌,逐行检查 metadata.labels 和节点标签是否对得上。

5.2 CrashLoopBackOff:从日志到退出码的逐层排查

Counterly,CrashLoopBackOff 是仅次于 Pending 的高频问题。它的含义是:容器启动后立刻崩溃,kubelet 尝试重启,但反复失败,所以进入退避状态。

我的排查顺序一般是这样的:

  1. 先看当前日志:kubectl logs pod-name。
  2. 如果当前日志为空,看上一次日志:kubectl logs pod-name --previous。
  3. 仍然没线索就查事件:kubectl describe pod pod-name。
  4. 最后实在不行,手动复现:kubectl run test-pod --image=镜像名 --command -- 你的启动命令,到宿主机侧直接看输出。

这个顺序看起来简单,但很有效。有一次我排查一个应用,它一直 CrashLoopBackOff,日志里全是正常启动信息,没有报错。后来用 --previous 才发现容器其实已经启动成功,只是启动时往 stdout 写了一行固定输出后立刻执行 os.Exit(1)。问题出在程序内部的启动校验,而不是 k8s 配置。如果当时不查 previous,我可能还会以为自己写的 livenessProbe 有误。

另外,如果状态是 OOMKilled,说明是内存超限被杀。这种问题优先调大 limits 或者优化应用内存,而不是瞎改重启策略。你可以用 kubectl describe pod 看 Last State 里的 OOMKilled 标记,确认后再调参。

5.3 镜像拉取失败与配置错误的典型现场

镜像拉取问题一般会在状态列显示 ImagePullBackOff、ErrImagePull。常见原因有四个:

  • 镜像地址写错,tag 不存在,或者仓库里根本没有这个镜像。
  • 拉取认证失败。私有仓库没有在 k8s 里配置 imagePullSecret,或者 secret 里的凭证过期了。
  • 镜像仓库域名解析不了,特别是自建仓库或内网环境。
  • 网络限制导致拉取超时,尤其是镜像比较大时。

如果确认地址没错,第二件事就是检查 secret。用 kubectl get secrets 看有没有相应认证,然后确认 Pod 的 spec.imagePullSecrets 里有没有引用它。我经常看到有人把 secret 建好了,却忘了在 Pod 清单里写 imagePullSecrets,导致拉取依然 401。

配置错误类的典型是 CreateContainerConfigError,常见于环境变量引用了不存在的 ConfigMap 或 Secret 的 key。kubectl describe 里也会给出“references non-existent config key”之类的信息。这种问题的核心是:k8s 启动容器时会先解析依赖,任何缺失项都会让容器创建失败,所以排查优先级要放在“配置依赖”上,而不是容器代码里。

还有一个细节,kubectl get events --sort-by='.lastTimestamp' 可以按时间排序查看集群事件。很多问题光看单个 Pod 的事件不够,全局事件的顺序能帮你还原整个出问题的链条,比如某段时间多个 Pod 同时 Pending,可能就是某个节点 NotReady 导致的大面积驱逐。

5.4 现场排障的几组实用组合命令

最后分享几组我自己定义的高频组合,基本可以覆盖日常 Pod 排障 80% 的场景。你可以把它们存成 shell 别名,临时用的时候敲一下,效率提升很明显。

# 看所有 Pod 及所在节点,适合排查调度问题 kubectl get pods -A -o wide # 看某个 Pod 的完整事件 kubectl describe pod <pod-name> # 实时跟踪日志 kubectl logs -f <pod-name> -c <container-name> # 进入容器并执行命令 kubectl exec -it <pod-name> -c <container-name> -- /bin/sh # 快速查看资源占用 kubectl top pod <pod-name>

kubectl top pod 如果报 metrics 不可用,说明集群没安装 metrics-server。这个工具对查看真实资源占用非常有用,因为 get pods 的表示和实际负载差距很大。我强烈建议有条件的集群先把 metrics-server 装好,排查问题时能少走很多弯路。

6. 最后关于 Pod 管理资源清单,我还想说的一件事

如果你照着上面的方式自己练一遍,就会体会到一个规律:Pod 管理表面上是记命令、写 YAML,本质上是在训练一种“声明式思维”。你不再关心“怎么启动这个容器”,而是关心“这个容器最终应该是什么样”。这种思维一旦建立,后面学 Deployment、Service、StatefulSet,全都是在同一个心智模型上加零件而已。

我个人的体会是,刚开始写清单时不要急着追求一步到位,更不要盲目抄网上大段大段的模板。先用最小可用版本跑起来,再逐步加上探针、资源限制、健康检查,每一步都搞清楚为什么。我接手过的不少问题,根因都是“照着别人模板复制,却不知道那个字段到底在干什么”。写 YAML 不是背字段,是理解状态。理解了状态,k8s 这个系统在你眼里就不再是一个黑盒,而是一套可预测、可控制、可复盘的编排逻辑。

顺便说一个实战里最后的小技巧:给每个 Pod 清单都加上 labels,并且 label 里包含应用名和环境名。这个习惯前期看不出价值,等集群里 Pod 数量超过几十个,你靠 kubectl get pods -l app=demo-web,env=test 一条命令就能精准锁定目标组时,就会感谢当时的这个决定。Pod 管理这条路没有捷径,但每踩过一个坑,都会变成你排障经验里最可靠的一块底牌。

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

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

立即咨询