☰
K8s Pod运维实战:状态排查、探针配置与资源限制
2026/10/21 6:34:34 网站建设 项目流程

先聊点实际的。如果你已经会用kubectl apply -f把 Pod 拉起来,会用kubectl get pods看状态,那这篇就是给你准备的。这是 K8s 操作系列里 Pod 部分的第二篇,前一篇我们把 Pod 的创建、查看、删除、标签这些基础操作过了一遍,这一篇重点解决的是:Pod 起来之后,它到底健康不健康,出问题时怎么定位,资源怎么限制,以及怎么控制它落在哪台节点上。说得直接一点,就是从“能把 Pod 跑起来”过渡到“能稳稳地维护一批 Pod”。

很多人学 K8s 容易卡在这一步:kubectl get pods看到一个 Running 就以为万事大吉,看到 CrashLoopBackOff 就只会一顿重启。实际上 Pod 背后有一套挺完整的生命周期机制,状态只是结果,原因藏在各种字段和事件里。这篇文章我会按实际排查的顺序来讲,先看状态,再扒日志,接着处理资源问题,最后聊探针和调度,每一步都是可以直接照着敲的命令。

1. 为什么“操作(2)”先讲状态与生命周期

1.1 从“能跑”到“会出事”

我见过不少初学者在刚接触 K8s 时有个习惯:Pod 一直是 Running,就觉得“已经部署完了”。直到有一天流量上来了,服务突然超时,翻遍日志也没看到报错,最后才发现容器早就被 OOMKilled 重启过好几轮了。其实 K8s 在 Pod 状态里已经给了足够多的信号,只是大多数人没仔细看。

这就是我把生命周期放在这期的原因。Pod 不是一个“启动完就固定不变”的进程,它随时可能因为健康检查失败、资源不足、节点驱逐、容器崩溃而进入各种异常状态。如果连状态机都没建立起来,后面排查问题会非常被动。

1.2 先明确“操作(2)”的边界

既然是“Pod 相关操作(2)”,我先把本期要覆盖的内容边界划清楚,避免大家觉得内容太散:

  • Pod 的完整状态阶段,以及怎么看状态背后的原因;
  • 重启策略和优雅终止参数,理解 K8s 里“重启”和“删除”的语义;
  • 日志查看、进入容器、临时容器这些日常调试操作;
  • 资源 requests / limits 的配置思路,以及 OOMKilled 排查;
  • 探针(liveness / readiness / startup)的配置与常见坑;
  • 调度层面的控制,包括 nodeName、nodeSelector、亲和性、污点容忍。

这些都属于“Pod 已经在跑了,但我要把它维护好”必须掌握的技能。前置要求很简单:有一个可用的 K8s 集群,会基本的kubectl get、kubectl describe、kubectl apply,并且能自己写一个简单的 Pod YAML。如果这些还不熟,建议先回头补一下前一篇再继续。

1.3 一个类比:像看服务器开机流程

理解 Pod 生命周期,可以类比成一台物理服务器的开机流程:提交配置就像按下电源键,调度器负责挑选机房里的哪台机器,kubelet 负责拉镜像、启动容器,探针就相当于服务器上的自检灯。任何一个环节有问题,最终都会反映在“状态”上。但状态只是红灯,要修还得打开机箱看 CPU、内存、日志分别是什么情况。接下来这部分,就是教你怎么看这些“自检灯”。

2. Pod 状态与重启策略:先判断“卡在哪”

2.1 几个关键状态阶段

K8s 里 Pod 有五个大的状态阶段(phase),分别是 Pending、Running、Succeeded、Failed、Unknown。我看很多人一看到 Running 就放心,其实这几个状态都有自己的含义,用对了能省大量排查时间。

状态阶段含义常见原因
PendingPod 已创建,但还没完成调度或者容器还没启动节点资源不足、镜像拉取失败、调度器无法匹配节点
RunningPod 已绑定节点,至少一个容器正在运行正常状态,但容器内部可能有问题
SucceededPod 内所有容器正常退出,且不会再重启Job、一次性任务执行完成
FailedPod 内至少一个容器以非 0 状态退出应用异常退出、脚本错误
Unknownapiserver 无法获取到 Pod 状态节点失联、网络分区

刚接触的人最容易忽略的是:Pod 处于 Running 不代表容器就一定健康,页面 500 照样能是 Running。所以状态阶段只是第一层,真正要盯的是容器状态里的 Waiting、Running、Terminated,以及 Pod 的 Conditions。比如kubectl describe pod xxx里能看到Conditions字段,里面有PodScheduled、Initialized、ContainersReady、Ready几个布尔条件,逐个看就知道问题卡在哪一环。

2.2 用 describe 快速定位

我给一个通用的排查顺序,刚开始可以照着敲:

kubectl get pods -n <namespace> kubectl describe pod <pod-name> -n <namespace>

describe输出的 Events 部分是最关键的,它记录了调度器、kubelet 对这个 Pod 的所有操作结果。比如最常见的:

  • FailedScheduling说明没有节点能满足调度条件;
  • Failed to pull image说明镜像拉取失败;
  • Liveness probe failed说明健康检查挂了;
  • Back-off restarting failed container说明容器启动后不断崩溃。

有一次我排查一个 Pending 状态的 Pod,get pods看半天也没看出原因,describe一看 Events 里写着0/8 nodes are available: 4 node(s) had taint,瞬间就明白是节点污点没容忍。所以先把“看 Events”变成肌肉记忆。

2.3 重启策略的理解和配置

Pod 的restartPolicy只有三个值:Always、OnFailure、Never。默认是 Always,这也是很多 Deployment 管理的 Pod 的行为方式:只要容器异常退出,kubelet 就会按这个策略决定是否重建。

策略行为适用场景
Always容器退出后总会被重启常驻服务,默认推荐
OnFailure只有非 0 退出码才重启批处理任务、可能失败的初始化任务
Never不管退出码是什么都不重启一次性任务,希望自己看到日志

注意一个容易踩的坑:restartPolicy只在同一 Pod 内重启容器,而不是重新创建一个 Pod。对于 Deployment 管理的 Pod,如果容器反复崩溃,Deployment 控制器会根据recreate或rollingUpdate策略去处理整个 Pod 的重建,这和 restartPolicy 是两套机制。排查时要区分你现在看的是容器重启还是 Pod 重建。

2.4 优雅终止参数的意义

Pod 被删除时会先收到 SIGTERM,等到terminationGracePeriodSeconds时间到了还没退出,才会被 SIGKILL 强杀。这个参数默认是 30 秒,但对于需要优雅关闭的服务来说,30 秒未必够用。

apiVersion: v1 kind: Pod metadata: name: graceful-pod spec: terminationGracePeriodSeconds: 60 containers: - name: app image: nginx:1.25

我一般建议这样配置:应用里处理好 SIGTERM 信号,把存量请求处理完再退出,同时把terminationGracePeriodSeconds调成应用最大处理时间再加 5~10 秒的裕量。如果配置过短,用户请求可能在中途被断开;配置过长,批量删除 Pod 时整个流程会被拖得很慢。这个参数没有“标准值”,一定要结合业务的单请求最长耗时来定。

3. 日志与调试:故障现场还原

3.1 kubectl logs 的常用姿势

很多新手只会用最基础的kubectl logs <pod-name>,一旦日志刷屏或者想追溯历史,就没办法了。这里把我日常最常用的几个参数列一下:

# 追踪日志输出,相当于 tail -f kubectl logs -f <pod-name> -n <namespace> # 看最近 200 行,适合日志特别多的时候 kubectl logs --tail=200 <pod-name> -n <namespace> # 只看过去 1 小时内的日志 kubectl logs --since=1h <pod-name> -n <namespace> # 查看某个容器崩溃前的日志 kubectl logs --previous <pod-name> -n <namespace>

这里最关键的是--previous。容器被自动重启后,当前日志已经变成了新进程的输出,旧进程为什么挂掉,用普通kubectl logs是看不到的。加上--previous才能看到上一次容器的日志,也就是崩溃现场的“遗言”。

还有一个细节:如果 Pod 里有多个容器,直接执行kubectl logs会提示你用-c <container-name>指定容器。日常排障时可以养成“先kubectl get pod <name> -o jsonpath='{.spec.containers[*].name}'确认容器名,再精确抓日志”的习惯,省得反复试。

3.2 进入容器的正确方式

需要进容器排查时,用kubectl exec:

kubectl exec -it <pod-name> -n <namespace> -- /bin/sh

如果容器里没有/bin/sh,可能是/bin/bash,两种都试一下。还有一个常见问题是基础镜像特意精简过,连 shell 都没有。这时候可以用kubectl debug,把调试容器附加到目标 Pod 里,后面专门说。

进入容器后别急着乱翻,我推荐按这个顺序确认问题:

  1. ps aux看进程是否存在;
  2. curl或wget探一下本机服务端口;
  3. /proc/meminfo和/proc/cpuinfo粗略看资源余量;
  4. 应用日志目录再捞一遍。

3.3 临时容器:最终兜底

kubectl debug是个很实用的兜底手段,适合“容器已经起不来,但你又想进去看环境变量、文件系统、网络配置”的场景。基本用法是这样:

kubectl debug -it <pod-name> --image=busybox:1.36 --target=<container-name>

--target指定要共享哪个容器的进程命名空间,这样调试容器里能看到目标容器里的进程。临时容器不会影响原 Pod 的存活,排查完删掉即可。但注意,这个能力需要集群支持,老版本 K8s 里临时容器属于 alpha/beta 特性,使用前先确认一下你的集群版本和 API 是否开启。另外临时容器里看到的文件系统是它自己镜像的,不是目标容器的文件系统,除非你自己挂载卷进去,这一点容易让人误解。

3.4 复制文件的另一种思路

有时候不想进容器,只是想快速把容器里的某个文件拿出来,或者把文件传进去,可以用kubectl cp:

# 从容器复制到本地 kubectl cp <namespace>/<pod-name>:<path> <local-path> -c <container-name> # 从本地复制进容器 kubectl cp <local-path> <namespace>/<pod-name>:<path> -c <container-name>

这条命令我也建议只在临时排障时用,别把它当成日常发布工具。真正要改配置、发文件,应该走 ConfigMap、Secret、InitContainer 或者重新构建镜像,否则环境差异会越来越大。

4. 资源限制:从 OOMKilled 说起

4.1 requests / limits 到底管什么

资源这块如果不理解清楚,后面基本会被 OOMKilled 反复折磨。先明确两个概念:

  • requests:调度时用的“最低保障量”,节点必须满足这些资源才会被调度上去;同时它也是 QoS 计算的重要依据。
  • limits:容器实际运行时不能超过的上限,超过内存会被杀掉,超过 CPU 会被节流。

可以理解为点外卖:requests 是你告诉商家“这顿饭至少要给够这些量”,limits 是你规定“最多只能给这么多,多了我吃不下”。调度器站在商家角度,看你约了多少量来决定能不能接单;运行时看管人员(kubelet / runtime)按 limits 来限制你的实际用量。

resources: requests: cpu: 250m memory: 256Mi limits: cpu: 500m memory: 512Mi

CPU 单位里,1 表示一个虚拟核,250m 表示 0.25 核;内存里 256Mi 就是 256 兆,1Gi 是 1024Mi。这里最坑的坑是:CPU 的limits超了只会被限流,进程不会死,但性能会突然变差;内存的limits超了会被 OOMKilled,容器直接消失并重启。

4.2 QoS 级别:资源竞争时的“淘汰顺序”

K8s 会根据 requests 和 limits 的配置方式,给 Pod 分到不同的 QoS 级别。常见的几种:

QoS 级别配置特征说明
Guaranteed所有容器都同时设置 requests 和 limits,且两者相等被杀死概率最低,稳定性最高
Burstable至少有一个容器设置了 requests,但不满足 Guaranteed 条件大多数业务的默认选择,相对宽松
BestEffort完全没有设置 requests 和 limits调度最容易,但节点资源紧张时第一个被驱逐

我经历过一次节点内存吃紧,BestEffort 的 Pod 被优先杀掉,而旁边有个 Burstable 的 Pod 里面的缓存容器虽然也在拼命吃内存,但因为 QoS 级别高一点,反而活了下来。所以生产环境里,重要业务不建议使用 BestEffort,至少把 requests 配上。

4.3 资源数值怎么定

新手最容易犯的错是照抄别人的 YAML,要么把内存 limits 设置得特别小,应用稍微一涨就 OOM;要么 requests 设得特别大,节点上连调度都调不进去。

我自己的做法是:

  1. 先不设 limits,只设 requests,用监控工具跑个三五天看实际内存曲线的峰值;
  2. 根据峰值,设置一个“合理上限”,通常我会取峰值的 1.2~1.5 倍作为 limits;
  3. CPU 的 requests 不要设满,保留一定 burst 余量,否则节点利用率会很低;
  4. 一个关键原则:limits.memory不能小于requests.memory,否则创建 Pod 会直接报错。

如果一时拿不到监控数据,至少可以先给一个相对宽裕的值,比如 Java 服务先给requests: 512Mi, limits: 1Gi,再逐步压测收紧。别一上来就追求极限,Pod 稳定运行远比省那几百兆内存重要。

4.4 OOMKilled 排查实录

遇到OOMKilled时,先别急着重启,按这个顺序来:

kubectl describe pod <pod-name> kubectl logs --previous <pod-name>

describe会显示容器最后一次退出码和终止原因。如果看到Last State: Terminated / Reason: OOMKilled / Exit Code: 137,基本可以确定是内存超限被杀了。这个时候再去翻日志,大概率能看到类似java.lang.OutOfMemoryError或者进程直接被系统杀掉的痕迹。

有一次我排查一个消息推送服务,日志停在某一条处理记录后就没动静了,describe里明确写着 OOMKilled。加内存 limits 后再观察,发现是缓存没有上限,把所有请求数据都堆在内存里。后来通过限制缓存容量、调整垃圾回收参数,才算真正解决。教训是:OOM 只是现象,背后的内存泄漏或者无上限缓存才是病根,不要以为调大 limits 就完事。

5. 健康检查:让 Pod 自己报告状态

5.1 三类探针的职责划分

K8s 提供了三种探针,刚接触的人容易混淆,其实分工很清晰:

  • livenessProbe:判断容器是否“活着”,失败就按 restartPolicy 重启容器。相当于心跳监护。
  • readinessProbe:判断容器是否“准备好了”,失败就暂时从 Service 的 Endpoints 里摘掉,不转发流量。相当于上岗考试。
  • startupProbe:判断应用是否已经启动完成,专门给冷启动慢的业务用的,避免在启动阶段就被 liveness 误杀。

举一个场景:一个应用启动需要 40 秒,如果 liveness 探针的initialDelaySeconds只配了 5 秒,应用还在慢慢初始化,探针就失败了,容器会被反复重启。这时候应该加一个startupProbe,让它在 40 秒内确认应用启动成功,之后再启动 liveness 和 readiness 的正常检查。

5.2 探针配置参数和经验值

每个探针都有几个通用参数,配置时一定要理解它们的含义:

参数作用常见经验值
initialDelaySeconds容器启动后等多久开始探测根据应用启动时间,一般 3~10 秒
periodSeconds每隔多久探测一次5~10 秒
timeoutSeconds探测请求超时时间1~3 秒
failureThreshold连续失败多少次才判定失败3 次
successThreshold连续成功多少次才判定成功1 次(只对 readiness 有意义)

这里提个醒:periodSeconds太短会频繁打爆应用接口,太长了故障发现又会滞后。我给普通 Web 服务用periodSeconds: 10,比较合适。另外探测接口最好是自己应用里的一个独立健康检查端点,不要直接把业务主接口当探针用,否则一次流量高峰导致响应变慢,探针超时了,Pod 就会被反复重启,造成更大的流量损失。

5.3 探针配置案例

看一下完整例子:

apiVersion: v1 kind: Pod metadata: name: probe-demo spec: containers: - name: app image: myapp:latest ports: - containerPort: 8080 startupProbe: httpGet: path: /startup port: 8080 initialDelaySeconds: 5 periodSeconds: 5 failureThreshold: 30 readinessProbe: httpGet: path: /ready port: 8080 initialDelaySeconds: 5 periodSeconds: 10 failureThreshold: 3 livenessProbe: httpGet: path: /live port: 8080 initialDelaySeconds: 15 periodSeconds: 10 timeoutSeconds: 3 failureThreshold: 3

这个配置里,startupProbe给了最多 30 次 × 5 秒的窗口,覆盖应用最长 150 秒的冷启动时间。等启动成功后,readinessProbe负责决定是否接流量,livenessProbe负责发现运行时的“假死”状态。三个探针用不同的路径,避免互相干扰。

5.4 探针误配置的典型故障

探针配置错,最典型的表现是“一切看着正常,但服务就是时好时坏”。我踩过的一个坑是某接口在请求量大时会变慢,健康检查超时被判定失败,Pod 被摘流量甚至重启。而实际上应用并没有死,只是慢。后来我把健康检查改成更轻量的内存状态查询接口,才恢复正常。

还有一种情况是readinessProbe失败后容器不会被杀,但 Endpoints 会摘掉 Pod,这时候看kubectl get pods状态依然是 Running,只是READY列显示0/1。很多人看到 Running 就不管了,直到流量异常才发现问题。所以我建议日常巡检时,除了用get pods看状态,也顺手看一眼READY列,这个列比 phase 更能反映真实服务状况。

6. 调度控制:Pod 落在哪台节点上

6.1 默认调度和手动指定

默认情况下,调度器会根据节点资源、污点、亲和性等条件自动选择节点。但排查问题或者做特殊部署时,偶尔需要手动控制节点。

nodeName是最暴力的方式,直接把 Pod 固定在某台节点上:

spec: nodeName: node-01

但我强烈不建议在正式环境里用这个,因为一旦节点宕机,Pod 不会自动漂移到其他节点,相当于放弃了调度器的所有能力。它只适合调试场景,比如某个问题只在特定节点出现,需要复现现场时临时用一下。

6.2 亲和性与反亲和性

相对温和的调度控制是nodeSelector和节点亲和性。nodeSelector的用法很简单,就是要求节点必须包含指定标签:

spec: nodeSelector: disktype: ssd

nodeAffinity则更灵活,支持硬性要求(requiredDuringSchedulingIgnoredDuringExecution)和软性偏好(preferredDuringSchedulingIgnoredDuringExecution)。硬性要求相当于“非 SSD 节点不可调度”,软性偏好是“尽量调度到 SSD 节点,没有也不报错”。

spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: disktype operator: In values: - ssd preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 preference: matchExpressions: - key: zone operator: In values: - zone-a

注意名字里的“IgnoredDuringExecution”,指的是节点标签在调度之后发生变更,Pod 不会被重新调度。所以不要指望改了节点标签就能自动迁移 Pod,那永远是“下次调度”才生效。

还有一种更细的控制是 Pod 间的亲和性/反亲和性,比如“这两个 Pod 尽量调度到同一个节点”或者“不要让两个 Pod 落在同一台机器”。多副本高可用场景通常用反亲和性,把同一服务的副本打散到不同节点,避免单点故障。

6.3 污点与容忍

污点(Taint)是给节点加的属性,表示这个节点有特殊偏好,不希望随便什么 Pod 都上来。Pod 上对应的容忍(Toleration)表示“我愿意接受这个污点”。

节点污点有三种效果:

效果行为
NoSchedule如果不容忍,Pod 不会调度到这个节点
PreferNoSchedule软性拒绝,尽量不调度,但没地方时也可能调度过来
NoExecute不仅不调度,已经在节点上的 Pod 如果不容忍,会被驱逐

NoExecute 配合tolerationSeconds使用,可以允许 Pod 在被驱逐后继续待一段时间,常用于节点维护时的优雅迁移。

给节点打污点和添加容忍的命令:

kubectl taint nodes node-01 dedicated=gpu:NoSchedule
tolerations: - key: "dedicated" operator: "Equal" value: "gpu" effect: "NoSchedule"

6.4 用场景决定策略

选哪种调度控制,取决于你想解决什么问题:

  • 只想让“独占机器”的 Pod 固定落点,用nodeSelector或节点亲和性;
  • 想让多个 Pod 互相靠拢,减少跨节点通信,用 Pod 亲和性;
  • 想让多副本分散开,降低故障影响面,用 Pod 反亲和性;
  • 想临时把某节点从调度池里摘出来维护,用kubectl cordon加污点;
  • 想让某些基础组件独占节点,不让普通业务混跑,用节点污点加容忍。

实际使用中,调度策略往往组合出现。比如我维护的一套大数据集群,控制面组件打上control-plane:true污点,普通业务 Pod 不带容忍就上不去;计算节点的 Pod 则用反亲和性保证每个节点最多一个计算实例,避免资源互相抢占。

7. 常见问题速查与排查习惯

7.1 状态异常速查表

我把平时最常遇到的几种 Pod 异常整理成一张速查表,真出了事照着查就好:

现象可能原因排查方向
Pending调度失败、资源不足、污点未容忍、PVC 未绑定describe看 Events,重点看 FailedScheduling
ImagePullBackOff镜像名写错、仓库认证失败、镜像拉取超时describe看 Events,手动docker pull验证
CrashLoopBackOff容器启动后立即退出、健康检查失败logs --previous看崩溃前日志
OOMKilled内存超限、内存泄漏describe看 Exit Code 137,结合监控看内存曲线
Running 但 READY 0/1readinessProbe 失败、应用初始化未完成describe看探针结果,访问健康检查接口
Unknown节点失联、apiserver 与 kubelet 通信异常kubectl get nodes看节点状态

这张表不可能覆盖全部情况,但覆盖了大概 80% 的日常问题。剩下的多是小概率的底层问题,比如网络策略拦截、存储卷挂载失败、节点磁盘满等,排查思路都还是离不开 describe 与日志。

7.2 三板斧定位法

不管遇到什么 Pod 问题,我自己的排查固定套路就是三板斧:

# 第一板斧:确认当前状态和 READY 列 kubectl get pods -n <namespace> -o wide # 第二板斧:看事件和配置细节 kubectl describe pod <pod-name> -n <namespace> # 第三板斧:看当前日志和崩溃前日志 kubectl logs <pod-name> -n <namespace> kubectl logs --previous <pod-name> -n <namespace>

这三条命令能解决绝大多数问题的定位。如果三板斧没看出来,再去查监控面板、节点状态、网络策略,不要一上来就重启 Pod,否则连现场都被你破坏了。

7.3 我自己的一些操作习惯

最后分享几个工作里养成的操作习惯,不一定适合所有人,但确实帮我省过很多时间:

第一,写 Pod 或 Deployment YAML 时,我会在模板里先把resources、探针、terminationGracePeriodSeconds这三个字段写好,哪怕值是宽松的,也不留空。这样后面调参只需要改数值,不用每次从零补。

第二,创建完 Pod 后,我会用kubectl get pods -o wide看一眼它落在哪个节点上,然后用kubectl logs确认日志正常输出。别等到报警响了才去补这一步。

第三,但凡遇到 CrashLoopBackOff,我很少直接重启。先logs --previous把上一次的日志拿到手,再用describe看退出码。有一次排查一个服务,日志里明明已经打印了“数据库连通失败”,但因为没看 previous 日志,我光在 kubelet 事件里绕了半天,浪费了不少时间。

第四,线上环境操作前,尽量给关键改动留备份。改 YAML 之前先kubectl get pod <name> -o yaml > backup.yaml;改完用kubectl apply --dry-run=client先做一遍校验,避免语法错误直接把一组 Pod 搞挂。

写在最后

这一篇把 Pod 从“创建完成”到“稳定运行”之间的几个关键操作串了一遍:看状态、看日志、限资源、配探针、管调度。这些东西单独看都不难,但组合起来就是日常运维的核心能力。我个人最大的体会是,K8s 排查问题最忌讳“乱试”,一定要按状态 → 事件 → 日志 → 配置的顺序一步步来,每一步都基于上一步的结果做判断。你越能准确读状态和事件,后面定位问题就越快。

另外还是要提一句:探针和资源限制这两个配置,一定不要照抄别人的模板直接上生产。你的业务启动时间、内存曲线、接口耗时和别人的完全不一样,最稳妥的做法是先小流量验证、持续观察几天,再逐步收紧参数。踩过几次 OOM 和探针误杀之后,你会慢慢形成自己的判断标准。

下一篇如果继续,我打算把 Deployment、StatefulSet 这些工作负载控制器补上,讲清楚 Pod 和控制器之间的协作关系,以及滚动更新的几个实操细节。那个话题和这篇正好衔接,遇到问题时的整体视野会更完整。

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

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

立即咨询