首页 / 资讯中心 / 文章详情

K8s Pod运维实战:状态排查、探针配置与资源限制

K8s Pod运维实战:状态排查、探针配置与资源限制 ★ FEATURED ARTICLE
先聊点实际的。如果你已经会用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 namespacedescribe输出的 Events 部分是最关键的它记录了调度器、kubelet 对这个 Pod 的所有操作结果。比如最常见的FailedScheduling说明没有节点能满足调度条件Failed to pull image说明镜像拉取失败Liveness probe failed说明健康检查挂了Back-off restarting failed container说明容器启动后不断崩溃。有一次我排查一个 Pending 状态的 Podget 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 --tail200 pod-name -n namespace # 只看过去 1 小时内的日志 kubectl logs --since1h 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 execkubectl exec -it pod-name -n namespace -- /bin/sh如果容器里没有/bin/sh可能是/bin/bash两种都试一下。还有一个常见问题是基础镜像特意精简过连 shell 都没有。这时候可以用kubectl debug把调试容器附加到目标 Pod 里后面专门说。进入容器后别急着乱翻我推荐按这个顺序确认问题ps aux看进程是否存在curl或wget探一下本机服务端口/proc/meminfo和/proc/cpuinfo粗略看资源余量应用日志目录再捞一遍。3.3 临时容器最终兜底kubectl debug是个很实用的兜底手段适合“容器已经起不来但你又想进去看环境变量、文件系统、网络配置”的场景。基本用法是这样kubectl debug -it pod-name --imagebusybox:1.36 --targetcontainer-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: 512MiCPU 单位里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 设得特别大节点上连调度都调不进去。我自己的做法是先不设 limits只设 requests用监控工具跑个三五天看实际内存曲线的峰值根据峰值设置一个“合理上限”通常我会取峰值的 1.2~1.5 倍作为 limitsCPU 的 requests 不要设满保留一定 burst 余量否则节点利用率会很低一个关键原则limits.memory不能小于requests.memory否则创建 Pod 会直接报错。如果一时拿不到监控数据至少可以先给一个相对宽裕的值比如 Java 服务先给requests: 512Mi, limits: 1Gi再逐步压测收紧。别一上来就追求极限Pod 稳定运行远比省那几百兆内存重要。4.4 OOMKilled 排查实录遇到OOMKilled时先别急着重启按这个顺序来kubectl describe pod pod-name kubectl logs --previous pod-namedescribe会显示容器最后一次退出码和终止原因。如果看到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: ssdnodeAffinity则更灵活支持硬性要求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 dedicatedgpu:NoScheduletolerations: - key: dedicated operator: Equal value: gpu effect: NoSchedule6.4 用场景决定策略选哪种调度控制取决于你想解决什么问题只想让“独占机器”的 Pod 固定落点用nodeSelector或节点亲和性想让多个 Pod 互相靠拢减少跨节点通信用 Pod 亲和性想让多副本分散开降低故障影响面用 Pod 反亲和性想临时把某节点从调度池里摘出来维护用kubectl cordon加污点想让某些基础组件独占节点不让普通业务混跑用节点污点加容忍。实际使用中调度策略往往组合出现。比如我维护的一套大数据集群控制面组件打上control-plane:true污点普通业务 Pod 不带容忍就上不去计算节点的 Pod 则用反亲和性保证每个节点最多一个计算实例避免资源互相抢占。7. 常见问题速查与排查习惯7.1 状态异常速查表我把平时最常遇到的几种 Pod 异常整理成一张速查表真出了事照着查就好现象可能原因排查方向Pending调度失败、资源不足、污点未容忍、PVC 未绑定describe看 Events重点看 FailedSchedulingImagePullBackOff镜像名写错、仓库认证失败、镜像拉取超时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-runclient先做一遍校验避免语法错误直接把一组 Pod 搞挂。写在最后这一篇把 Pod 从“创建完成”到“稳定运行”之间的几个关键操作串了一遍看状态、看日志、限资源、配探针、管调度。这些东西单独看都不难但组合起来就是日常运维的核心能力。我个人最大的体会是K8s 排查问题最忌讳“乱试”一定要按状态 → 事件 → 日志 → 配置的顺序一步步来每一步都基于上一步的结果做判断。你越能准确读状态和事件后面定位问题就越快。另外还是要提一句探针和资源限制这两个配置一定不要照抄别人的模板直接上生产。你的业务启动时间、内存曲线、接口耗时和别人的完全不一样最稳妥的做法是先小流量验证、持续观察几天再逐步收紧参数。踩过几次 OOM 和探针误杀之后你会慢慢形成自己的判断标准。下一篇如果继续我打算把 Deployment、StatefulSet 这些工作负载控制器补上讲清楚 Pod 和控制器之间的协作关系以及滚动更新的几个实操细节。那个话题和这篇正好衔接遇到问题时的整体视野会更完整。
阅读完成 · 觉得有帮助?
咨询建站