
这几节内容本来是给内部团队做培训时整理的现在抽出来单独成文。上一期讲了Pod的创建、删除、日志查看这些基本操作这次往后走专门聊生命周期、探针、资源配额和调度策略。说实话这些才是Pod真正难搞的地方。很多同学YAML能写出来Pod也能跑起来但一遇到Pod一直重启、Pending在那不动、或者流量打不进来就完全没了头绪。这篇文章就是冲着这些问题来的。1. Pod生命周期与状态流转细节1.1 五阶段状态机再理解Kubernetes官方文档把Pod的状态划分成五个阶段但这个划分粒度比较粗。实际排障的时候只靠这五个阶段根本不够用需要结合容器状态、Init容器状态、以及Pod自身的Conditions一起看。先过一遍五阶段Pending代表Pod已被集群接受但容器还没全部跑起来Running表示Pod已经绑定到节点所有容器都已创建至少有一个在运行Succeeded意味着所有容器正常退出退出码为0Failed则是至少有一个容器以非0码退出或被系统终止Unknown一般出现在kubelet与apiserver失联的场景。这里要纠正一个常犯的错误Running并不代表Pod健康。容器起来了但业务可能还没就绪。判断一个Pod是否真正可用要看它的Ready状态就是kubectl get pods输出里那个1/1或0/1的列。这个列的分母是Pod里定义的容器总数分子是Ready的容器数而这个Ready是由Readiness探针决定的。1.2 从YAML到Running的完整链路我画过一条时间线用来跟新人讲Pod从提交到运行的整个过程kubectl apply -f pod.yaml → apiserver校验并写入etcd → scheduler感知Pod创建开始调度决策 → 选定节点更新Pod的nodeName字段 → kubelet watch到Pod被调度到本机 → 依次拉取镜像、创建Pod沙箱、启动Init容器若有 → Init容器全部成功后启动业务容器 → 容器启动后kubelet持续执行存活和就绪探测这条链路里任何一个环节卡住Pod就卡在Pending或者ContainerCreating。排查Pending问题几乎都是从这条链路找突破口。之前帮某公司排查过一批Pod集体Pending最后定位是调度器绑核失败——调度器选完节点但节点上的kubelet版本太老与apiserver的绑定协议有兼容性问题绑定请求一直超时。这种问题用kubectl describe pod几乎看不出来因为事件里只有一条Failed to bind得把scheduler的日志翻出来才能看到根因。1.3 Conditions不为人知的细节Pod有多个Conditions最常用的是PodScheduled、Initialized、ContainersReady和Ready。这里有个容易踩坑的点PodScheduled为True只代表调度决策完成不代表Pod能在节点上跑起来。调度成功之后镜像拉取失败、volume挂载失败都会导致Pod一直卡在Pending或者ContainerCreating。Initialized这个Condition也有讲究。如果Pod定义了Init容器这个Condition在Init容器成功运行前都是False。我见过一个极端案例Init容器里面写了一个死循环导致Pod永远处于Pending但Scheduled的状态是正常的搞了半天才发现问题在Init容器那边。提示看Conditions不是看一两个字段就行要配合kubectl describe pod底部的事件列表一起看。事件按时间戳排列通常最下面的是最早发生的从最早的异常事件开始查往往就是根因。2. 三种探针的选型与配置实战2.1 liveness、readiness、startup到底怎么选探针问题是群里问得最多的。很多人的配置方式是三个探针全部复制粘贴本质上根本没搞明白各自的职责。这里我用一句话总结区分liveness决定Pod要不要重启readiness决定流量要不要进来startup决定其他探针要不要开始执行。用生活化的类比。liveness就像家里的煤气报警器一旦触发直接把房间里的火灭了重开。readiness有点像餐厅门口挂的营业中牌子牌子没挂出来客人就不往你店里进。startup更像电脑的开机自检没完成自检之前别急着测其他因为系统还在启动。打个真实的场景。之前给某图像处理服务配探针Java进程5秒就完成了JVM启动但真正加载完模型权重需要40秒。如果用默认配置liveness探针在容器启动后10秒就开始探测第15秒探测失败第20秒触发重启模型永远加载不完Pod陷入了无限重启。后来加了startup探针给了120秒的启动缓冲期问题才解决。2.2 探针参数的敏感度分析探针配置有四组参数很多人只会填个path和port另外几个全用默认值。这几个默认值在实际生产中非常不靠谱。initialDelaySeconds是容器启动后等多久开始探测。这个值太短服务还没起来就一直失败太长又会拖慢故障恢复速度。它必须根据服务的冷启动时间动态调整不是拍脑袋填个5或者10就行。periodSeconds的默认值是10也就是说每10秒探测一次。如果你的探针逻辑本身比较重例如需要查数据库这个频率可能太高但反过来对于核心链路10秒的探测间隔又可能显得太迟钝。failureThreshold默认是3次。这3次失败的判定时机是periodSeconds*3的时间段内连续3次失败才触发。我把这个乘积称为容错窗口窗口越短探针越敏感。timeoutSeconds是最容易被忽略的。很多探针请求服务默认1秒超时。如果你的服务正常响应时间就要800毫秒突发流量来了变成2秒那探针就会报超时紧接着触发重启。我见过不止一次把正常业务抖动误判成故障的情况就是因为这个值没调。2.3 探针实战配置清单记录一份我总结的探针配置套路spec: containers: - name: app livenessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 30 periodSeconds: 15 timeoutSeconds: 3 failureThreshold: 3 readinessProbe: httpGet: path: /ready port: 8080 initialDelaySeconds: 5 periodSeconds: 5 timeoutSeconds: 2 failureThreshold: 3 startupProbe: httpGet: path: /startup port: 8080 initialDelaySeconds: 0 periodSeconds: 2 failureThreshold: 60这套配置的思路是startup探针给了120秒的启动窗口覆盖慢启动场景readiness探针频率高确保服务可用性变化能被及时感知liveness探针频率低但有足够的容错窗口避免瞬时抖动引发不必要的重启。注意探针的httpGet请求会落到业务端口如果这个端口是用户流量入口探针请求量要算进容量规划。曾有一家公司只开了4个副本每个副本30秒一次探针流量高峰时探针响应时间从50毫秒飙升到2秒误伤率陡增。后来把探针单独放在管理端口上问题才缓解。2.4 探针故障诊断清单探针导致的故障往往表现为Pod不断重启。定位时先用kubectl describe pod看Events重点看Liveness probe failed或Readiness probe failed的日志。需要明确一个容易混淆的地方readiness探测失败不会导致Pod重启它只会把Pod的Ready状态改成False流量会被摘掉liveness失败才触发重启。所以当Pod状态一直显示Running但ready列是0/1时去看readiness探针的配置当Pod显示CrashLoopBackOff时优先怀疑liveness探针太激进其次才是业务本身崩溃。我的排查习惯是先看业务日志再看kubelet日志。业务日志能确认进程是否真的异常退出kubelet日志能看到探针请求的具体返回码和耗时。有些情况下探针请求打到了业务端口但业务框架的默认路径返回404探针就一直失败这就是典型的探针路径错误问题日志里会有明显标识。3. 资源请求与限制的设定方法论3.1 从QoS类别看资源配置本质Kubernetes把所有Pod分成三类QoSGuaranteed、Burstable和BestEffort。这三类的划分规则极其简单所有容器都设置了等值的requests和limits就是Guaranteed至少有一个容器设置了requests或limits但不满足Guaranteed条件就是Burstable所有容器都没有设置requests或limits就是BestEffort。很多初学者以为这个分类只是概念性的实际上它对Pod的生存机会有直接影响。节点内存压力时kubelet会按QoS顺序驱逐Pod先BestEffort再Burstable最后才是Guaranteed。简而言之QoS等级就是Pod在被驱逐时的优先保护等级。有朋友问过为什么他的Pod在节点内存吃紧时总是被驱逐而别人的Pod没事。十有八九是他的Pod没有设置资源limits落到了Burstable或BestEffort等级。这类Pod在系统层面的保护优先级是一致的所以同样的内存压力下先被驱逐的就是那个没设limits的。3.2 requests和limits的最佳实践比例资源配比没有万能公式但有一条经验法则limits是requests的1.5到2倍同时保证limits不超节点容量的可分配上限。具体怎么定取决于业务类型。CPU相对简单因为CPU是可压缩资源线程跑不动会用调度等待换取时间不会造成节点崩溃。所以CPU的requests可以设置得接近实际使用量limits设到2倍问题都不大。内存完全不同内存是不可压缩资源超过limits后容器会被OOMKilled。内存的limits必须比实际使用量高一些至少留出30%的缓冲否则业务一有波动就频繁重启。我唯一一次严重的内存配置翻车是把内存requests设得刚好等于进程平均使用量。结果业务一开会话数上来内存飙到可分配上限容器直接被OOM。那次之后我定了一个规矩内存配置必须基于压测数据而不是拍脑袋估。所谓压测数据至少要覆盖峰值场景下持续5分钟的内存曲线取峰值的1.2到1.5倍才作为limits参考线。3.3 命名空间层面的配额治理容器级别的requests和limits是基础命名空间层面的ResourceQuota和LimitRange则是治理手段。ResourceQuota能限制一个命名空间的资源总量LimitRange能限制单个Pod或容器的资源范围。实际使用场景是这样的开发环境的命名空间limits设得太大单个Pod就能吃光一台节点的资源这时通过LimitRange把单Pod内存limit上限卡住比如上限8Gi。同时用ResourceQuota保证一个命名空间的总容量不超额比如最多申请24个CPU、64Gi内存。apiVersion: v1 kind: LimitRange metadata: name: mem-limit-range spec: limits: - max: memory: 8Gi cpu: 4 min: memory: 256Mi cpu: 100m default: memory: 2Gi cpu: 1 defaultRequest: memory: 512Mi cpu: 500m type: Container提示LimitRange的default和defaultRequest是两个完全不同的概念。default是创建Pod时没设limits时自动补上的defaultRequest是没设requests时自动补的。被这个自动补全机制坑过一次某次我们有个Pod明明没写limits但kubectl describe显示它带着limits查了半天才发现是LimitRange给补的。4. Pod调度策略与亲和性控制4.1 从nodeSelector到nodeAffinity的能力跨越nodeSelector是Kubernetes里最古老的调度约束手段它只支持精确匹配也就是说节点必须包含某个标签才会被选中。它非常简单但满足不了复杂需求。比如想把工作负载调度到SSD类型的节点上同时排除某些特定机型nodeSelector就写不出不等于或者在集合内这种条件。nodeAffinity就是升级版它支持In、NotIn、Exists、DoesNotExist、Gt、Lt六种操作符。这个能力带来的变化足以让调度策略从硬性指定进化为软性偏好。nodeAffinity分requiredDuringSchedulingIgnoredDuringExecution和preferredDuringSchedulingIgnoredDuringExecution两种。前者是硬性条件调度时不满足就不调度后者是软性偏好调度器会尽量满足但满足不了也能调度到别的节点。听起来很简单但实际使用有个坑在后面IgnoredDuringExecution的意思是这个条件只在调度阶段生效运行阶段如果节点标签变化了已经运行的Pod不会受影响不会被重新调度。4.2 亲和性配置的典型场景分享两个配亲和性时踩过坑的场景。第一个是把服务A和服务B放到同一个节点的诉求。初期用节点标签硬绑把所有需要共置的Pod都配上同一个节点标签的nodeAffinity结果节点分布逐渐变得畸形某些节点塞了好几个Pod其他节点空的。后来改成用podAffinity——Pod与Pod之间的亲和性让服务B倾向于调度到运行着服务A的节点上。这个思路正确了但配置时忘了加topologyKey默认是kubernetes.io/hostname只在节点粒度生效。实际上我们要的是同区域即可所以topologyKey要改成区域级别的标签。第二个场景是节点维护。大版本升级前要把某些节点上的工作负载赶走最稳妥的做法不是逐个delete Pod而是给节点打node.kubernetes.io/unschedulable污点再配好容忍度让Pod自然迁移。这里要搭配PodDisruptionBudget保证迁移过程中服务不中断。4.3 污点与容忍度的对抗关系污点和容忍度的关系我习惯用门禁来理解节点打了污点相当于设了门禁只有带了对应容忍度的Pod才被允许进入。如果节点打了一个污点Pod没有对它设置容忍这Pod就不会被调度到该节点上。实际配置时最常用的是三个系统级污点node.kubernetes.io/not-ready、node.kubernetes.io/unreachable和node.kubernetes.io/out-of-disk。默认情况下大多数Pod都会自动加上这些污点的容忍度所以节点暂时NotReady时Pod还能在节点上继续运行一段时间。这个容忍时长的默认值是300秒。也就是说节点失联5分钟后系统才会真正开始把Pod调度到其他节点。这个值在生产环境经常被调短。之前有家公司的节点因为网络抖动频繁失联但业务要求故障转移必须在2分钟内完成。他们就改写了全局的容忍配置把key为node.kubernetes.io/unreachable的tolerationSeconds调成120。这个改动的影响范围是整个集群所有Pod要特别慎重不建议直接动全局默认值而是通过admission webhook按需注入。注意容忍度不等于强制调度。即使Pod完全容忍了节点的所有污点调度器依然会综合考虑资源、亲和性、拓扑分布等条件。容忍度只是解除了限制并不会增加任何倾向性。5. 优雅终止与Pod故障排查实录5.1 优雅终止全流程拆解很多人以为删除Pod就是一条命令的事实际上删除动作发出后系统内部有一个完整的优雅终止流程理解这个流程对保证服务不断连非常重要。当你执行kubectl delete pod时apiserver将Pod标记为Terminating随后kubelet开始执行终止流程Pod进入Terminating状态 → kubelet并行执行preStop钩子若配置 → 向容器主进程发送SIGTERM信号 → 等待容器退出最多等待terminationGracePeriodSeconds默认30秒 → 超时仍未退出发送SIGKILL强制杀死 → 容器退出后Pod从apiserver列表移除这个过程中最容易踩坑的是两个preStop钩子里的操作时间过长把整个terminationGracePeriodSeconds占满了SIGTERM还没来得及触发就把进程杀了另一个是业务进程不响应SIGTERM全靠SIGKILL强杀这类Pod重启后流量有短暂中断下游可能出现大量错误。之前帮某项目排查过一次诡异现象滚动更新期间总有少量请求报502。后面用kubectl get events和kubelet日志一看发现Pod在滚动更新时旧副本的endpoint还没来得及从Service的Endpoints里摘除容器就已经退出了于是新流量还是被转发到了正在退出的Pod上。解决方式是在preStop里加一个sleep给endpoint摘除留出时间窗口spec: containers: - name: app lifecycle: preStop: exec: command: [sh, -c, sleep 10]这个方案虽然不是Kubernetes官方的标准做法但在业务端无法配合优雅退出时是一个非常实用的兜底手段。5.2 Pending的三大常见原因复盘Pod卡在Pending是最常见的故障原因基本上可以锁定在三个方向资源不足占据的比例最高。kubectl describe pod底部会给出0/3 nodes are available的提示后面跟着failedScheduling原因。如果是Insufficient cpu或Insufficient memory就是节点资源不够或者requests设置太大集群没有足够资源去满足调度的硬性要求。第二种原因是节点亲和性不满足。比如服务要求调度到带GPU的节点但集群里根本没有带对应标签的节点Pod就会一直Pending。这时提示往往是一串0/N nodes are available后面带了node(s) didnt match node selector。第三种是存储问题。Pod引用了不存在的PVC或者存储类有问题也会卡在Pending。这里的Hint通常比较隐蔽需要切换查看Event有时Event里只有一条FailedScheduling但没写具体原因得进一步看storage provider的日志。5.3 CrashLoopBackOff的定位思路CrashLoopBackOff是Pod反复崩溃重启的状态是K8s运维里最让人头疼的问题之一。定位思路是有步骤的而且顺序很重要。第一步看Pod的事件能拿到崩溃的概要信息。第二步看业务日志kubectl logs能拿到最近一次非正常退出的日志。如果容器在日志输出前就崩溃了可以用kubectl logs --previous看上一次容器的日志这句话很多新人不知道它能让你在看完整退出前的输出。第三步看退出码Exit Code 137代表OOMKilled、128信号导致的退出、1或2是业务自身的错误。分享一个实际案例。某服务反复重启Exit Code一直是137但kubectl describe里没看到OOMKilled字样。一度以为只是简单的OOM后来查kubelet日志发现实际是cgroup内存限制触发了oos但容器进程是被SIGKILL掉的所以037的退出码旁边并没有标明OOMKilled字样。这个经验说明退出码和描述里的标识要交叉验证不能只看一处。5.4 排查命令速查手册把日常用得最多的命令整理成一张速查表方便按图索骥场景命令备注查看Pod全量状态kubectl get pods -A -o wide显示节点、IP、状态查看调度决策kubectl describe pod name -n ns底部Events详细记录调度过程查看容器日志kubectl logs pod -c container -n ns多容器Pod需指定容器名查看上次退出的日志kubectl logs --previous pod -n nsCrashLoopBackOff专用进入容器排查kubectl exec -it pod -n ns -- /bin/sh需要容器里有shell查看节点资源分配kubectl describe node node显示已分配和可分配资源查看全部事件kubectl get events -n ns --sort-by.lastTimestamp按时间排序方便查根因直接查etcd里的Pod状态kubectl get pod name -o yaml可确认字段级状态6. 一次载入式Pod故障的排查过程回溯讲一个实际的排查过程整个过程绕了三个弯价值比前面所有理论加起来都大。某天线上反馈某服务的部分Pod频繁重启表现为存活探针失败导致重启。我先是看了Pod事件发现探针失败集中在每天的固定时间窗口。一开始怀疑是业务上有什么定时任务导致高负载但业务方说没有定时任务。接着看节点监控发现那几个Pod所在节点的磁盘使用量在固定时间窗口内飙升到90%以上紧接着节点NotReadyPod被重新调度。这时候方向对了是磁盘问题但不知道什么东西在写。最后登录节点排查定位到是某数据服务的日志文件配置了每天轮转但轮转后的压缩任务非常消耗磁盘高峰期直接把节点写满。根因找到后把日志轮转策略调整了同时给节点挂了独立的持久化盘。这个案例的几个核心教训值得记住探针失败的根因不一定在探针本身节点上的IO、磁盘、网络都可能是隐形的元凶排查问题要跨组件联动不能只盯Pod本身。7. 写在最后关于经验沉淀的一点想法做Kubernetes这段时间最大的体会是Pod本身的YAML语法并不难难的是把各种机制之间的关系摸清楚。探针、资源配额、调度策略、优雅终止它们彼此独立又互相影响。一个资源limits写错可能导致Pod被驱逐一个探针配置太激进可能引发无休止的重启一个preStop钩子写错可能让滚动更新出现零星报错。所以我一直跟团队强调一个原则任何Pod配置的改动都要过三个问号——这个配置影响哪些环节异常时它会怎么表现我怎么快速验证配置是有效的这三问过完再小的改动都能沉淀成经验。最后分享一个小技巧。排查临时问题时可以短暂开启kubectl get pods --watch实时观察Pod状态变化很多时候故障的规律会在这个过程中自己浮出来。比如你发现某个Pod每隔10分钟重启一次跟探针的周期对齐那大概率就是探针问题如果重启间隔不固定就要往资源或者业务代码方向查。状态变化节奏本身就是最直接的线索。