ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Pod生命周期与资源管理:Kubernetes排障实战指南

Pod生命周期与资源管理:Kubernetes排障实战指南 在Kubernetes里摸爬滚打这些年我越来越确信一个规律很多看起来玄乎的线上故障追根溯源都绕不开两个词——Pod生命周期和Pod资源管理。要么是Pod反复重启、永远Ready不了要么是requests和limits配得稀里糊涂导致Pod不是Pending到天荒地老就是被OOMKilled一刀切。这篇文章不打算讲教科书式的概念而是把我在实际运维中怎么理解生命周期、怎么配置资源参数、怎么排查故障的经验完整捋一遍适合刚入门K8s的开发者也适合被线上Pod折腾到失眠的运维同学。1. 先把Pod的“人生轨迹”理清楚生命周期到底包含什么1.1 从提交到回收Pod的完整旅程Pod的生命周期本质上就是从你执行kubectl apply提交给API Server开始直到Pod被彻底清理回收为止的一段完整旅程。在用户视角里Kubernetes把它抽象成了几个PhasePending、Running、Succeeded、Failed、Unknown。PendingPod已经被API Server接受并写入etcd但还没有完全就绪。这个阶段可能是调度器正在寻找合适的节点可能是镜像还没拉完也可能正卡在init container执行阶段。排障时需要进一步确认卡点在哪里。RunningPod已经成功绑定到某个节点至少有一个容器处于运行状态。注意这里的“运行”不一定是健康运行可能正在启动也可能已经进入重启循环。SucceededPod内所有容器都正常退出退出码为0且不会重启。这种状态通常出现在Job、CronJob这类一次性任务场景。普通Deployment管理的Pod极少出现这个状态除非你手动把副本缩到0。Failed所有容器都已终止但至少有一个容器以非零退出码退出或者被系统强制终止。这个状态意味着任务执行失败。UnknownAPI Server无法获取Pod当前状态绝大多数情况是节点失联、kubelet无法上报心跳导致的。这个Phase表看起来简单但新手特别容易踩一个坑以为Pod进入Running就等于万事大吉。其实Phase只是一个粗粒度的指示器它只告诉你Pod“大体上活着”完全不代表Pod的业务逻辑是健康的。我见过太多同事看到Pod是Running就不再深究结果流量已经断了半天。1.2 Phase只是指示器真正要盯的是Condition和ContainerState线上排障时单独看Phase远远不够真正决定问题定位效率的是PodConditions和ContainerState这一层细节。PodConditions是Kubernetes通过探针和调度结果推断出来的一组状态标签常见的有PodScheduledPod是否已经完成调度。如果是False下面通常会有Unschedulable相关的reason。Initialized所有init container是否执行完成。ContainersReadyPod里的所有容器是否都通过了readiness检查。ReadyPod整体是否就绪只有这个字段为TruePod才会被纳入Service的Endpoints承担流量。DisruptionTarget1.21以后出现Pod是否因为节点维护、驱逐等原因即将被终止。这个Condition对理解Pod突然消失非常有帮助。每个Condition里还藏着lastProbeTime、lastTransitionTime、reason和message。遇到问题先看kubectl describe podEvents区和Conditions区通常会直接告诉你答案比瞎猜快得多。再往下拆就是ContainerState。单个容器的状态分三种Waiting容器尚未开始运行常见reason包括ContainerCreating、ErrImagePull、CrashLoopBackOff等。Running容器正在执行但你别忘了核对它的Ready字段。Terminated容器曾运行但现在已退出。这里一定要看exitCode137代表被杀OOMKilled或SIGKILL1一般是业务代码异常退出。我习惯的排查顺序是四步第一步kubectl get pod看Phase和RESTARTS第二步kubectl describe pod看Conditions和Events第三步看容器State和exitCode第四步才去看日志。这个顺序看起来笨拙但能保证你不被日志里的噪声带偏直接锁定问题发生的层级。这里顺便说下“有效生命周期”这个词。一个Pod从Ready变成True到它开始被终止退出这段时间对线上服务来说才是真正“有效”的。探针、readiness、preStop钩子这些设计本质上都是在延长或保护这段有效生命周期后面我会详细讲。2. 生命周期里的三个关键旋钮RestartPolicy、钩子与探针2.1 restartPolicy决定容器“倒下后”谁来管restartPolicy是PodSpec里被忽视得最厉害的一个字段但它直接决定了容器退出后的行为。它有且只有三个值Always、OnFailure、Never。Always不管容器以什么方式退出kubelet都会在本机重启它。Deployment、StatefulSet管理的Pod默认就是Always。OnFailure只有当容器以非零退出码退出时才会重启。正常退出exitCode0不会触发重启适合Job这类任务型Pod。Never容器一旦退出就不再本地重启。任务型Pod如果失败可以依靠控制器重新调度新Pod。这里必须澄清一个大坑restartPolicy管的是同一个节点上、同一个容器组的“原地重启”不是把Pod重新调度到别的节点。Pod能不能切换节点是Deployment、StatefulSet这类控制器基于副本数和调度策略决定的。很多人把restartPolicy当成“高可用机制”这是不对的。另外kubelet重启容器不是无限连击而是采用指数退避策略第一次重启等待约10秒之后每次翻倍最大等300秒。所以你会看到CrashLoopBackOff这个状态。注意它还带一个exponential backoff的特性连续失败几次之后容器的重启间隔会越来越长这是kubelet在保护自己避免把CPU和IO全耗在无意义的重启上。如果你在监控里看到restartCount保持在某个数字不涨但Pod状态一直不是Ready多半就是处于退避等待阶段。2.2 postStart与preStop容器的入场和退场回调生命周期钩子提供了两个时机postStart和preStop。postStart在容器创建后立即执行注意它和容器的entrypoint命令是并行的并不保证钩子先执行完主进程才启动。这意味着你可能以为postStart里完成了某个初始化动作但实际上主进程已经抢跑。如果postStart本身就执行失败kubelet会杀掉容器并按照restartPolicy决定是否重启同时Events里会出现PostStartHookFailed。preStop则刚好相反它是阻塞的。当Pod收到删除请求时kubelet会先执行preStop里定义的命令等它执行完再发送SIGTERM给容器主进程。这个特性非常适合做优雅退出从注册中心下线服务从负载均衡摘除流量等待存量请求处理完成刷新缓存或持久化内存中的状态我踩过一个非常深的坑某次运维同学把数据库迁移脚本写进preStop结果每次滚动更新都触发一次数据变更最后演变成生产事故。preStop里只适合放轻量、幂等、快速的下线动作千万别放重量级任务。另外preStop的执行时长受terminationGracePeriodSeconds约束如果钩子执行时间超过这个期限SIGTERM不会来等来的直接是SIGKILL强杀。默认值30秒很多人没意识到这个字段的存在导致超大流量应用在优雅退出上形同虚设。2.3 探针liveness/readiness/startup如何配置才不误伤探针是生命周期与流量调度的桥梁三种探针角色完全不同livenessProbe判断容器是否“活着”失败后kubelet杀死容器并重启。适合检测死锁、长期卡死这类不可恢复状态。readinessProbe判断容器是否有能力接收流量失败后从Service的Endpoints中摘除但容器本身继续运行。适合检测依赖是否就绪。startupProbe专门给慢启动容器准备的。在startupProbe成功之前其余探针都不会开始执行避免容器启动慢导致被liveness误杀。举一个最典型的案例。JVM应用冷启动经常要60秒甚至90秒如果只配了livenessProbe且initialDelaySeconds只给30秒大概率容器会在启动阶段被打死然后陷入启动即重启的死循环。正确做法是配一个startupProbe把periodSeconds设成10failureThreshold设成12这样给容器最多120秒启动时间startup成功后livenessProbe再接管日常健康检查。参数上的建议也一并说了periodSeconds默认10不建议小于5否则探针本身会对容器产生额外的CPU和端口压力。timeoutSeconds默认1如果接口本身要几百毫秒才能返回务必调大避免误杀。failureThreshold连乘periodSeconds就是“失败容忍时长”配置前先算清楚。initialDelaySeconds对startupProbe无效startupProbe从容器启动那一刻就开始计时。探针的探测方式有exec、httpGet、tcpSocket、grpc四种。日常最常用的是httpGet和exec。grpc探针需要额外开通grpc健康检查协议支持不是所有服务都适用。我个人的习惯是核心业务用readinessliveness组合慢启动服务再加startup绝不只依赖单一探针。3. Pod资源管理的底层逻辑requests/limits与QoS3.1 CPU和内存的计量方式为什么CPU可压缩内存不可压缩资源管理是Pod生命周期能够稳定运行的地基。理解requests和limits之前必须先理解CPU和内存的本质区别。CPU是可压缩资源。你用cpu: 500m表示半个核100m表示0.1个核。Kubernetes通过cgroup的CPU配额机制CFS quota实现限制当容器实际使用的CPU超过limits时内核会强制限流让容器进入throttle状态表现为CPU使用率被“削平”但进程不会死。所以CPU limits设低一点最多是性能变差不会直接导致进程退出。内存是不可压缩资源。超过内存limit内核OOM killer会直接找一个进程杀掉对应到容器侧就是OOMKilledexitCode为137。不可压缩的意思就是内存用完就是没了没法像CPU那样排队等待。这决定了内存相关的配置必须保守宁多勿少。还有一个关键点经常被忽略调度器只看requests不看limits。也就是说一个Pod能不能调度到某台节点上只取决于它声明的requests是否在节点allocatable容量内。limits只对运行时生效。这导致一个经典问题大量Pod只设了limits没设requests节点上每个Pod的requests都很小调度器可能把它们堆在同一台机器上结果运行时大家一起撞limits发生CPU争抢或内存超卖。如果你写了limits没写requestsKubernetes会默认把requests“顶格”到和limits一样。这本身是一种保护机制但也意味着调度时的资源占用会比你想象的高。所以一定要显式声明requests不要让系统悄悄替你决定。3.2 QoS三个等级Guaranteed、Burstable、BestEffort根据requests和limits的组合方式Kubernetes给每个Pod划分了QoS等级这个等级直接决定节点资源紧张时谁先被驱逐、谁先被OOM。GuaranteedPod内每个容器都显式设置了CPU和内存的requests且requests等于limits。这是最高优先级平时节点资源紧张时最不容易被驱逐进程也不容易被OOM killer盯上。Burstable至少有一个容器设置了requests或limits但存在某容器requests不等于limits或只有requests没有limits。这是日常最常见的一类。BestEffort所有容器都没有设置任何requests和limits。这类Pod在节点内存压力时是第一个被驱逐的OOM打分也最高。驱逐顺序很残酷BestEffort最先死其次是Burstable中内存使用率超过requests比例高的最后才是Guaranteed。简单说Guaranteed相当于给关键业务买了一张“优先存活”的保险。我强烈建议核心业务、有状态服务、数据库类应用一律配成Guaranteed。这样系统行为可预期不会因为邻居Pod的突发流量影响到你的稳定性。非关键的离线任务、测试环境Pod可以用Burstable来提升节点利用率。3.3 requests和limits设置不当会怎样这里分享几个我实际处理过的案例。requests设置过高有一个团队把requests.cpu写成4但实际业务平均只用0.5核。结果就是集群明明有很多节点的CPU利用率很低新Pod却一直Pending。一看节点Allocated resources发现CPU requests已经被“纸面资源”占满了。这不是容量不够是requests占位不够。解决办法是统计一周实际用量把requests压到P50甚至P30水平给突发流量留出余量。limits设置过低最常见的是JVM应用。Java默认按照宿主机内存计算堆大小如果你分配的Pod limit是1Gi但宿主机的内存是64GiJVM启动时可能给自己预留了十几G堆一跑起来直接超limit被OOMKilled。现代JDK可以加参数-XX:MaxRAMPercentage75让JVM感知容器限制或者直接用容器内存配堆。别小看这个细节很多CrashLoopBackOff的根因就是它。只设置limits不设置requestsrequests会被隐式拷贝成limits导致调度时被当成一个大块头明明可以落两台小机器结果一台都塞不下。这个坑的隐蔽性很高因为YAML里你只写了limits但kubectl describe pod会发现requests也是同样数值。4. 资源管理的落地操作LimitRange与ResourceQuota4.1 LimitRange给单个Pod戴上限纸上谈兵说完QoS实际落地时最常用的控制手段是LimitRange和ResourceQuota。前者管单个Pod后者管整个命名空间。LimitRange在命名空间级别定义Pod和容器的资源边界。它可以限制最大值、最小值、默认值以及limits和requests的最大比例。一个典型配置长这样apiVersion: v1 kind: LimitRange metadata: name: example-limitrange namespace: dev spec: limits: - type: Container max: cpu: 2 memory: 2Gi min: cpu: 20m memory: 32Mi default: cpu: 200m memory: 256Mi defaultRequest: cpu: 100m memory: 128Mi maxLimitRequestRatio: cpu: 10 memory: 10default和defaultRequest的作用是当Pod没写requests或limits时自动补上。这个默认值非常重要因为在企业环境里我们没法保证每个开发都会认真写资源字段。LimitRange配合default能让所有新Pod一出生就带上合理的资源声明不至于跑着跑着变成BestEffort。需要提醒一点maxLimitRequestRatio设成10表示limits最大是requests的10倍。这个可以防止有人把requests写得很小、limits写得很大既享受低requests带来的调度便利又占着高limits的资源承诺。这其实是一种资源管理上的“占位套利”值得用配额限制。4.2 ResourceQuota命名空间级的总量控制ResourceQuota管的是命名空间里所有Pod加在一起的资源总量。它通常在apiserver准入控制阶段生效Pod创建时如果发现总量超配直接拒绝创建。示例apiVersion: v1 kind: ResourceQuota metadata: name: dev-quota namespace: dev spec: hard: requests.cpu: 4 requests.memory: 8Gi limits.cpu: 8 limits.memory: 16Gi pods: 20 persistentvolumeclaims: 4这个配置表示dev这个命名空间里所有Pod的CPU requests总和不能超过4核内存requests总和不能超过8Gi所有Pod的limits总和不能超过8核和16Gi同时Pod总数不能超过20个PVC不能超过4个。这里有一个隐藏很深的坑ResourceQuota统计的是requests和limits字段本身如果Pod没有设置这两个字段而命名空间里又配置了ResourceQuota创建Pod会直接报错错误信息一般是“failed quota: dev-quota must specify requests.cpu, requests.memory”。所以ResourceQuota和LimitRange通常要成对出现LimitRange负责给Pod补默认值ResourceQuota负责在总量上卡预算。只配Quota不配LimitRange等于是把所有没写资源字段的Pod全部拒之门外开发会疯掉的。另一个坑是配额只在新Pod创建时校验已运行Pod不受影响。也就是说某团队忽然删掉了一个大Pod释放出来的配额不会立刻被Pending的Pod追认需要重新创建或者等待调度器重试。线上遇到配额不足最稳的套路是删掉不用的Pod再重建依赖它的Deployment。4.3 实操配置示例从一个命名空间看资源完整性我建议每个命名空间都做成“LimitRange ResourceQuota 显式资源声明”三件套。实际配置顺序和验证方式是这样的创建命名空间kubectl create ns dev应用LimitRange保证新Pod自动带上默认资源值。应用ResourceQuota限制总量。用一个Deployment验证默认值是否生效kubectl apply -f https://k8s.io/examples/controllers/nginx-deployment.yaml kubectl get pod -o yaml | grep -A6 resources如果配置正确你会看到Pod的resources字段里带上了LimitRange注入的默认值。再用kubectl describe resourcequota dev-quota查看配额使用情况可以清晰看到当前已用和剩余。这里还有一个实用命令kubectl get limitrange example-limitrange -o yaml查看LimitRange的当前生效状态。注意修改LimitRange只会影响之后新建的Pod已经在运行的Pod不会跟着改。所以资源规范最好在命名空间创建之初就定好否则后面“补课”的代价很高。5. 常见故障与排查实录5.1 Pod一直Pending从调度的角度排查Pod卡在Pending是最高频的问题。排查思路一定要按层级展开第一层看Eventskubectl describe pod name | grep -A 20 Events如果看到FailedScheduling说明是调度失败。第二层看节点资源kubectl describe node node重点看Allocated resources和Allocatable。如果CPU/内存的requests已经接近上限说明节点容器被纸面资源占满了。第三层看污点节点如果有NoSchedule污点而Pod没有对应容忍必然Pending。kubectl describe node里能直接看到Taints。第四层看PVC如果Pod挂载了PVC而PVC处于PendingPod也会卡住。kubectl get pvc一看便知。我处理过一个比较迷惑的案例Pod的requests只有500m集群也大把空闲节点但依然Pending。最后查出来是Pod设置了nodeSelector而带指定label的节点全部处于NotReady状态。调度器的视角里Pod在逻辑上没有可容纳它的地方这和资源充足与否没有关系。还有个技巧如果Pod的Events里报的是Unschedulable可以试试kubectl get events --sort-by.metadata.creationTimestamp把全局事件按时间排一下往往能看见调度器给出的更多细节。5.2 Pod反复重启OOMKilled与CrashLoopBackOff的现场复盘Pod反复重启的两种最常见原因一种是内存超限被OOMKilled一种是业务进程启动即崩导致CrashLoopBackOff。OOMKilled的典型特征kubectl describe pod里ContainerState的reason显示OOMKilledexitCode是137。这时去kubectl logs看到的日志往往很少因为进程直接被内核杀掉了根本没机会留下堆栈。正确排查方式是看实际内存使用kubectl top pod name再和limits对比基本一眼就能看出是不是超limit了。如果确认是内存问题调整策略不是简单把limit翻倍而是先连续观察3到5天的内存曲线取P90或者最大值再乘1.2到1.5的安全系数作为新limit。如果是JVM或者Golang这类运行时先检查是否感知容器限制避免在宿主机视角下给堆分配了过多内存。CrashLoopBackOff的特征更复杂一些。容器可能启动了几秒就退出退出码可能是1、127、128等等。这时kubectl logs是主力工具如果日志里没有有效信息可以用kubectl logs pod --previous查看上一次容器实例的日志。很多情况下崩溃原因在最后一次日志里已经写明只是你没看对容器实例。5.3 镜像拉取失败ImagePullBackOff排查思路ImagePullBackOff和ErrImagePull虽然不会直接导致容器退出但会让Pod永远无法进入Running。常见原因有四类镜像名或tag写错最常见的是少写了镜像仓库前缀或tag不存在。镜像仓库认证失败拉取私有仓库镜像时没有配置imagePullSecret。Registry不存在或网络不通尤其是自建仓库地址错误。镜像被误删或tag被覆盖导致旧的tag无法拉取。排查第一步永远是kubectl describe podEvents里会直接给出拉取失败的具体原因。比如Failed to pull image xxx:latest: rpc error: code NotFound基本就是镜像名或tag问题。如果报unauthorized那就是认证问题需要在同一个命名空间创建imagePullSecret并在Pod的spec里引用。5.4 从监控角度守护Pod的有效生命周期最后补充一下怎么用监控盯住这些故障。我强烈建议在监控面板里至少放四个核心指标kube_pod_container_status_restarts_total容器重启次数快速发现CrashLoop。OOMKilled事件计数可以通过kubelet日志或Prometheus的kube_pod_container_status_terminated_reason统计。内存使用vs limits用container_memory_working_set_bytes除以container_spec_memory_limit_bytes超过0.9就该告警。CPU throttle时间container_cpu_cfs_throttled_periods_total增长异常说明CPU limit确实压住了业务。这几个图能覆盖绝大多数资源类故障。我见过太多团队只盯CPU和内存均值结果Pod已经被OOMKilled重启了三轮监控面板上还是绿灯。原因就是聚合粒度太粗异常被平均值抹平了。6. 写在最后从“有效生命周期”角度谈资源管理我个人在实际操作中的体会是Pod生命周期和Pod资源管理从来就不是两个独立的话题它们共同决定了一个Pod的“有效生命周期”——从开始正常处理流量到被安全终止这段时间到底有多长。有一个很典型的场景业务滚动发布时老Pod要被终止。如果不做任何优雅退出处理kubelet会直接给容器发SIGTERM很多服务收到后立刻断开连接正在处理的请求全部失败。我在生产环境常用的方案是给容器加一个preStop钩子里面执行sleep 10同时在PodSpec里把terminationGracePeriodSeconds调大到45秒。这样kubelet会在删除Pod时先等10秒让新Pod完成启动并从Service拿到流量再发送SIGTERM给老Pod实现一种接近无损的切换。这个方案在Nginx ingress和Spring Cloud服务上我都验证过实测下来很稳但要注意sleep时间不能太长否则滚动更新的耗时会被无限拉长。再补充一个实用建议调任何资源参数之前先用kubectl top pod导出至少一周的实际用量曲线然后根据P90或P95来定requests根据P99或最大值来定limits。别拍脑袋更别照抄官方示例里的数值。资源配比这事每家业务差异太大只有基于自身数据做出来的配置才靠谱。踩过几次坑之后我现在看Pod的状态不再只盯着Phase而是习惯性把describe里的Conditions、ContainerState、QoS等级、配额使用量全部扫一遍。把这些事当成肌肉记忆线上的Pod才能真正做到心里有数。
返回列表