1. 从“能跑”到“跑得好”:Pod下半场到底在聊什么
很多人学 Kubernetes 的时候,前面几篇都挺顺:装个集群、跑个 Deployment、暴露个 Service,看着 Pod 一个个 Running,心里挺美。但真到了生产环境,问题就来了——Pod 怎么老是重启?为什么明明配了资源限制还是被 OOM Kill?健康检查探针到底该怎么写才不误杀?一个 Pod 里塞两个容器到底合不合理?这些问题,全都落在 Pod 这个最小调度单元的“下半场”里。
这篇内容就是接着上一篇 Pod 基础概念往下走的,重点聊 Pod 的生命周期管理、多容器协作模式、资源与调度约束、健康检查机制以及排障思路。说白了,上篇讲的是“Pod 是什么”,这篇讲的是“Pod 怎么用才不出事”。适合已经能跑起基本工作负载、开始接触真实业务部署的开发和运维同学,也适合那些被 Pod 反复重启折磨过、想系统梳理一遍的人。
我自己的体会是,Pod 这块知识看起来零散,其实有一条主线:调度器决定 Pod 落在哪,kubelet 决定 Pod 怎么活,控制器决定 Pod 死了怎么办。把这三者的职责边界理清楚,后面所有配置项你都能找到它该待的位置。下面我就按这条主线,把下半场的关键内容拆开讲。
2. Pod 生命周期与状态流转:搞懂它才能读懂报错
2.1 五个阶段与容器状态的区别
很多人看kubectl get pod输出的 STATUS 列,以为那就是 Pod 的生命周期阶段,其实不是。Pod 的status.phase只有五个值:Pending、Running、Succeeded、Failed、Unknown。而你在终端看到的CrashLoopBackOff、ImagePullBackOff、Terminating这些,是容器状态和事件综合出来的展示结果,不是 phase。
这个区别很关键。比如你看到CrashLoopBackOff,Pod 的 phase 其实还是Running——因为 Pod 已经被调度到节点上了,只是里面的容器反复启动失败。真正判断问题性质,要看kubectl describe pod里的 Events 和容器state字段。我见过不少同学一看到非 Running 就去查调度问题,方向就偏了。
容器的state有三个互斥的字段:waiting、running、terminated。waiting里的reason才是排障金矿,常见的有ContainerCreating(还在准备挂载卷、拉镜像)、CrashLoopBackOff(启动后退出,kubelet 在退避重试)、ImagePullBackOff(镜像拉不下来)、ErrImagePull(拉取直接报错)。terminated里的exitCode和reason则告诉你容器是怎么死的——是正常退出(0)、被信号杀掉(137 通常是 SIGKILL,多半是 OOM)、还是应用自己报错(1 或其他)。
提示:
kubectl get pod -o wide只能看到节点和 IP,真正要看容器退出码,用kubectl get pod <name> -o jsonpath='{.status.containerStatuses[*].state}'更直接。
2.2 重启策略与控制器行为的配合
Pod 的restartPolicy有三个值:Always、OnFailure、Never。这里有个容易踩的坑:这个策略是容器级别的重启,不是 Pod 级别的重建。也就是说,Always意味着容器退出后 kubelet 在原地把它拉起来,Pod 的 UID、IP、节点都不变。而 Deployment 管理的 Pod 如果整个被删掉,那是控制器新建了一个 Pod,跟 restartPolicy 没关系。
实际选型上,长期运行的服务用Always(Deployment 默认就是它),批处理任务用OnFailure或Never(Job 的语义)。我见过有人在 Deployment 里手动改成Never,结果容器一崩就再也不起来,还以为是调度出了问题。记住:restartPolicy 决定的是“同一个 Pod 内容器挂了怎么办”,控制器决定的是“Pod 没了怎么办”,两层逻辑别混。
退避重试的节奏也值得知道:kubelet 重启容器不是无脑狂拉,而是按 10s、20s、40s……指数退避,上限 5 分钟。所以CrashLoopBackOff状态下你看到的重启间隔越来越长,这是正常的保护机制,不是集群卡了。想快速定位,直接看容器日志和退出码,别干等。
2.3 优雅终止:preStop 与 terminationGracePeriodSeconds
Pod 被删除时,流程是这样的:API Server 标记删除时间戳 → kubelet 开始终止 → 执行 preStop 钩子 → 发送 SIGTERM 给容器主进程 → 等待terminationGracePeriodSeconds(默认 30 秒)→ 超时则 SIGKILL 强杀。同时 Endpoints 控制器会把该 Pod 从 Service 后端摘除。
问题在于,摘除 Endpoints 和发送 SIGTERM 是并行的,不是串行。这意味着可能还有流量正在打进来,你的进程已经收到终止信号了。所以生产环境里,preStop 里加一个短暂的sleep是常见做法,给负载均衡器一点时间把流量切走。比如:
lifecycle: preStop: exec: command: ["sh", "-c", "sleep 5"]这个 5 秒不是拍脑袋,要结合你的服务发现刷新周期来定。如果用的是外部负载均衡,刷新可能有延迟,那就得适当拉长。terminationGracePeriodSeconds也要覆盖 preStop 时间加上应用自身优雅关闭的时间,否则 preStop 还没跑完就被强杀了,等于白配。
3. 多容器 Pod 的协作模式:什么时候该塞在一起
3.1 三种经典模式与适用边界
一个 Pod 里放多个容器,共享网络命名空间和存储卷,这个特性用好了很优雅,用滥了就是灾难。业界总结出三种经典模式:Sidecar(边车)、Ambassador( ambassador)、Adapter(适配器)。
Sidecar 最常见,典型场景是日志收集和代理。主容器写日志到共享卷,边车容器负责采集转发。这样业务代码不用关心日志往哪送,职责分离。Ambassador 是帮主容器代理对外通信,比如主容器只管连localhost:6379,实际由 ambassador 容器转发到真实的外部服务,好处是主容器配置简单、切换后端不用改代码。Adapter 则是把主容器的输出做标准化转换,比如把自定义格式的监控指标转成统一格式暴露出去。
判断要不要放一个 Pod 里,我的经验标准是:生命周期是否必须同步、是否必须共享网络或存储、是否强耦合到无法独立扩缩容。三条里满足两条以上才考虑。如果只是“部署方便”就想塞一起,那还是拆开用独立 Deployment 更稳妥,否则一个容器崩了影响整个 Pod,扩缩容也被绑死。
3.2 共享资源的坑:端口冲突与卷竞争
多容器共享网络命名空间,意味着它们不能监听同一个端口。这个坑很隐蔽,尤其当两个容器都默认监听 8080 时,第二个起来的直接报address already in use,然后 CrashLoop。解决办法是给其中一个改端口,或者用hostPort之外的显式containerPort区分(注意 containerPort 本身只是声明,不强制,但端口冲突是真实存在的)。
共享卷的竞争也要小心。多个容器同时写同一个emptyDir里的同一个文件,没有锁机制,写坏了都不知道是谁干的。合理做法是划分目录:主容器写/logs/app,边车只读/logs,各管各的。另外emptyDir的生命周期跟 Pod 绑定,Pod 一删数据就没了,别拿它当持久存储用。
注意:多容器 Pod 里,任何一个容器进入非就绪状态,整个 Pod 的 Ready 条件都会变成 False,从而被摘出 Service 后端。所以边车容器的健康检查同样要认真配,别让它成为整个 Pod 的短板。
3.3 Init 容器:比边车更“前置”的角色
Init 容器是 Pod 里在主容器之前运行的专用容器,按顺序执行,全部成功主容器才启动。它跟普通容器的区别在于:不支持生命周期钩子、不支持探针、必须运行到完成。典型用途是等待依赖服务就绪、初始化配置文件、做数据迁移。
我经常用 Init 容器解决“主容器启动太快导致依赖没准备好”的问题。比如主容器要连数据库,但数据库可能还没起来,就在 Init 容器里循环探测端口,通了再退出。这样主容器启动时依赖一定就绪,省掉一堆重试逻辑。写法上:
initContainers: - name: wait-db image: busybox:1.36 command: ['sh', '-c', 'until nc -z db-service 5432; do echo waiting; sleep 2; done']这里用nc -z探测端口,比 curl 轻量。注意 Init 容器失败会按 Pod 的 restartPolicy 重试,如果配的是Always,它会一直重试直到成功,所以探测逻辑里最好有超时保护,别无限等下去。
4. 资源请求与限制:调度和稳定的双重开关
4.1 requests 与 limits 的本质区别
resources.requests和resources.limits是两个完全不同的东西,但经常被混为一谈。requests 是调度依据:调度器看节点剩余可分配资源是否满足 Pod 的 requests 总和,满足才把 Pod 放上去。limits 是运行时约束:kubelet 通过 cgroup 限制容器实际能用的资源上限。
CPU 和内存的表现还不一样。CPU 是可压缩资源,超过 limit 会被限流(throttle),表现为变慢但不死。内存是不可压缩资源,超过 limit 直接 OOM Kill,容器被杀掉重启。所以内存 limit 设小了,症状就是莫名其妙的 137 退出码和重启;CPU limit 设小了,症状是延迟升高、吞吐下降,但 Pod 看着挺健康。
一个常见误区是只设 limits 不设 requests。这样调度器会按默认值(通常是 0 或很小)来算,节点可能被超卖,实际运行时大家抢资源,互相拖累。正确做法是 requests 按稳态需求设,limits 按峰值容忍度设,两者之间留出弹性空间。
4.2 一个可参考的配置计算过程
假设一个 Java 服务,压测下来稳态占用 1.5 核、2GB 内存,峰值能到 2.5 核、3GB。JVM 堆设了 1.5GB,加上堆外内存和元空间,实际 RSS 大概 2.2GB 到 2.8GB 波动。
配置思路:requests 设cpu: 1500m、memory: 2Gi,保证调度时有足够资源;limits 设cpu: 3000m、memory: 3Gi,给峰值留余量。内存 limit 一定要大于 JVM 最大堆加上堆外开销,否则 JVM 还没到堆上限,容器先被 OOM 了。这里 3Gi 相对 1.5GB 堆是偏宽松的,因为要算上 Metaspace、线程栈、直接内存这些。
resources: requests: cpu: "1500m" memory: "2Gi" limits: cpu: "3000m" memory: "3Gi"如果节点资源紧张,可以适当压缩 requests,但要清楚这是在赌超卖比例。我一般建议生产环境 requests 总和不超过节点可分配资源的 80%,留出缓冲给系统组件和突发。
4.3 QoS 等级:你的 Pod 被杀的优先级
Kubernetes 根据 requests 和 limits 的配置自动给 Pod 分三个 QoS 等级:Guaranteed(所有容器 requests 等于 limits)、Burstable(至少一个容器设了 requests 但不等)、BestEffort(全都没设)。节点内存压力大时,kubelet 按 BestEffort → Burstable → Guaranteed 的顺序驱逐。
这个机制直接影响稳定性。核心服务尽量配成 Guaranteed,虽然资源利用率低一点,但被驱逐的风险最小。批处理、可中断的任务用 Burstable 甚至 BestEffort,省钱。我见过把数据库配成 BestEffort 的,节点一紧张第一个被赶走,数据一致性都受影响,这个教训值得记。
5. 健康检查探针:别让“健康”变成误杀
5.1 三种探针的职责划分
liveness、readiness、startup 三种探针,职责完全不同,配错了比不配还糟。liveness判断容器是否还活着,失败就重启容器;readiness判断容器是否能接收流量,失败就从 Service 后端摘除,但不重启;startup只在启动阶段生效,成功一次后就不再执行,专门给启动慢的应用用。
最常见的错误是把依赖检查写进 liveness。比如 liveness 里探测数据库连接,数据库一抖动,所有 Pod 的 liveness 全失败,集体重启,雪崩。正确做法是 liveness 只检查进程自身是否卡死(比如 HTTP 服务返回 200 就够),依赖问题交给 readiness,让流量绕开而不是重启。
startup 探针是给慢启动应用准备的。没有它的时候,慢启动应用要么把 liveness 的initialDelaySeconds设得很大(导致真正卡死时发现太晚),要么被 liveness 误杀。有了 startup,启动阶段只看 startup,成功后才交给 liveness 接管,逻辑清晰多了。
5.2 参数调优:failureThreshold 与 periodSeconds 的配合
探针的几个参数要一起看:initialDelaySeconds(首次探测延迟)、periodSeconds(探测间隔)、timeoutSeconds(单次超时)、failureThreshold(连续失败几次判定失败)、successThreshold(连续成功几次判定成功,liveness 只能是 1)。
判定“失败”的时间窗口 =periodSeconds × failureThreshold。这个窗口要大于应用的正常抖动时间,否则偶发慢请求就触发重启。比如 period 10s、failureThreshold 3,就是 30 秒内连续失败才动手,比较稳妥。反过来,如果窗口太长,真卡死了也要等很久才恢复,要权衡。
readiness 的successThreshold可以大于 1,让服务在确认稳定后才接流量,适合有预热过程的应用。但别设太大,否则恢复变慢。我一般 readiness 用 period 5s、failureThreshold 2、successThreshold 1,响应快一点;liveness 用 period 10s、failureThreshold 3,稳一点。
5.3 探针实现方式的选择
探针支持 exec、httpGet、tcpSocket 三种方式。httpGet 最常用,但要注意它只看状态码,2xx 和 3xx 算成功,4xx 和 5xx 算失败。如果你的健康检查端点返回 401 或 403,会被判失败,这个坑不少人踩过。exec 灵活但每次探测都起一个进程,开销大,别在里面跑重活。tcpSocket 只验证端口能连上,最轻量,但只能证明进程在监听,证明不了业务正常。
提示:探针的 httpGet 默认走 Pod IP,如果你的应用只监听 127.0.0.1,探测会失败。确保监听地址是
0.0.0.0或者显式配host字段。
6. 调度约束与亲和性:让 Pod 落到该落的地方
6.1 nodeSelector、亲和性与反亲和性
最简单的调度约束是nodeSelector,按节点标签硬匹配。比如节点打了disktype: ssd,Pod 里写nodeSelector: {disktype: ssd},就只会调度到这类节点。简单直接,但表达能力有限,只能精确匹配。
nodeAffinity更灵活,支持In、NotIn、Exists等操作符,还分requiredDuringSchedulingIgnoredDuringExecution(硬性,不满足不调度)和preferredDuringSchedulingIgnoredDuringExecution(软性,尽量满足但不保证)。名字很长,记住“required 是必须,preferred 是偏好”就行。
podAffinity和podAntiAffinity则是按 Pod 之间的关系来调度。反亲和性最常用:同一个服务的多个副本打散到不同节点,避免单节点故障全挂。配置时用topologyKey: kubernetes.io/hostname表示按节点打散。但要注意,反亲和性在大规模集群里可能让 Pod 一直 Pending——如果副本数超过节点数,硬性反亲和就永远满足不了。
6.2 污点与容忍:节点侧的主动排斥
污点(Taint)是节点主动拒绝 Pod 的机制,容忍(Toleration)是 Pod 声明自己能接受某种污点。两者配合,实现“专用节点”的效果。比如给 GPU 节点打上nvidia.com/gpu: NoSchedule污点,只有配了对应容忍的 Pod 才能上去,普通业务 Pod 自动避开。
污点效果有三种:NoSchedule(不调度新 Pod)、PreferNoSchedule(尽量不调度)、NoExecute(不仅不调度,还驱逐已运行的 Pod)。NoExecute要慎用,配错了会把正在跑的业务赶走。容忍的写法要注意operator:Equal要匹配 value,Exists只要 key 存在就行,后者更宽松。
6.3 拓扑分布约束:比反亲和更精细的打散
topologySpreadConstraints是相对新的能力,用来控制 Pod 在拓扑域(节点、可用区)之间的分布均衡度。相比反亲和性,它能表达“最多允许差几个”的柔性约束,不会因为副本数和节点数不整除就卡死。
topologySpreadConstraints: - maxSkew: 1 topologyKey: topology.kubernetes.io/zone whenUnsatisfiable: ScheduleAnyway labelSelector: matchLabels: app: my-service这段配置的意思是:按可用区打散,各可用区之间 Pod 数量差不超过 1,不满足时仍然调度(ScheduleAnyway)。如果改成DoNotSchedule,就是硬约束。实际用下来,跨可用区高可用场景用这个比反亲和性更省心,尤其是副本数动态变化的时候。
7. 常见问题排查实录:那些年踩过的 Pod 坑
7.1 排查思路与常用命令组合
Pod 出问题,我的排查顺序固定是:先看状态和事件,再看日志,最后进容器看现场。命令组合上,kubectl describe pod看 Events 和容器状态,kubectl logs看应用输出(多容器要加-c指定),kubectl exec -it进去验证网络和文件。如果是节点层面的问题,还得kubectl describe node看资源压力和污点。
一个高效技巧:kubectl get events --sort-by=.lastTimestamp按时间排序看最近事件,比翻 describe 输出快。另外kubectl logs --previous能看上一个被杀容器的日志,CrashLoop 场景下特别有用,因为当前容器可能刚起来还没输出。
7.2 典型问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| Pending 不调度 | 资源不足、亲和性不满足、污点未容忍 | describe 看 Events 里的调度失败原因 |
| ImagePullBackOff | 镜像名错、私有仓库认证失败、网络不通 | 检查 image 字段、imagePullSecrets、节点到仓库连通性 |
| CrashLoopBackOff | 应用启动报错、配置缺失、依赖未就绪 | logs --previous 看退出前输出,检查 exitCode |
| OOMKilled(137) | 内存 limit 太小、内存泄漏 | 对比实际 RSS 和 limit,调大或修泄漏 |
| Readiness 一直失败 | 探针路径错、端口错、应用未监听 0.0.0.0 | exec 进去手动 curl 探针地址验证 |
| Terminating 卡住 | finalizer 未处理、preStop 卡死、卷卸载失败 | describe 看 finalizers,检查 preStop 逻辑 |
7.3 几个反直觉的坑
第一个坑:Pod 显示 Running 但服务不通。这种情况多半是 readiness 没配或者配错了,Pod 被当成健康塞进了 Service 后端,但应用其实没准备好。加个 readiness 探针就能暴露问题。
第二个坑:改了 Deployment 的镜像,Pod 没更新。检查imagePullPolicy,如果是IfNotPresent且 tag 没变(比如一直用 latest),kubelet 不会重新拉镜像。生产环境别用 latest,用具体版本号或者 digest。
第三个坑:节点资源明明够,Pod 却 Pending。看 Events 里是不是0/3 nodes are available: 3 Insufficient cpu,这往往是 requests 设太大,或者节点上有 DaemonSet 占了不少。用kubectl describe node看 Allocated resources 那一栏,能清楚看到已分配和剩余。
第四个坑:多容器 Pod 里某个边车挂了,整个 Pod 重启。因为 restartPolicy 是 Pod 级别的,任何容器退出都会触发整个 Pod 的重启逻辑(取决于策略)。如果边车是辅助性质,考虑把它拆成独立 Deployment,别让它拖累主容器。
8. 我个人的几条实操心得
配 Pod 这件事,配置项多但真正影响稳定性的就那么几个:requests/limits 别乱设、探针别乱写、优雅终止别忽略。我见过太多事故追根溯源都是这三样没配好。资源限制是底线,探针是保险,优雅终止是体面,缺一个都可能在关键时刻掉链子。
另外,别迷信默认值。terminationGracePeriodSeconds默认 30 秒,对大多数 Web 服务够用,但对有长连接或者大批量任务处理的服务可能不够,该调就调。探针的默认参数也是同理,initialDelaySeconds默认 0,慢启动应用不加 startup 探针基本必炸。
最后分享一个习惯:每次上线新服务前,先在测试环境用kubectl describe pod把 Events 从头到尾看一遍,确认没有异常事件,再用kubectl top pod观察一段时间的资源曲线,跟 requests/limits 对一对。这个动作花不了几分钟,但能提前发现大部分配置问题。Pod 这东西,配对了它安安静静,配错了它天天给你惊喜,前期多花点心思,后期少熬几个夜。