
做 Kubernetes 久了你会发现大部分线上事故都不是出在业务代码本身而是出在 Pod 的生命周期管理上。服务启动顺序乱了、流量打到还没就绪的实例上、发布时连接被硬生生掐断这些问题看似无关痛痒却直接影响用户体感和数据完整性。这篇文章围绕 Pod 从创建到销毁的完整生命周期展开重点拆解 Init 容器、探针Probe和优雅终止这三块硬骨头。不管你是刚接触 K8s 的开发者还是已经在生产环境摸爬滚打了一段时间的运维同学读完应该都能对你现有的 YAML 配置有一个全新的审视角度。我会把每个配置背后的原理、参数计算的逻辑、以及实际踩过的坑都讲清楚尽量让你看完就能直接用。1. Pod 生命周期全景图先弄懂 Pod 是怎么“活”的1.1 Pod 生命周期的几个关键阶段在 Kubernetes 中每一个 Pod 从诞生到消亡都会经历一系列状态变化。kubectl get pods 里显示的 Pending、Running、Succeeded、Failed、Unknown 这几个状态是大多数人最先接触的“生命体征”。但很多人不知道的是在这几个大状态之间还藏着 ContainerCreating、Init、PodInitializing、Terminating 这些中间态。这就好比一个人从出生到离开证件上的状态可能只有“在生”和“离世”但中间的成长过程、生病过程、临终收尾其实才是决定生命质量的关键。Kubernetes 对 Pod 的管理也是同样的逻辑关键不在于最终显示成什么状态而在于每个阶段做了什么、卡没卡住、有没有异常。要真正掌握 Pod 生命周期管理需要把视野放大到“从提交 YAML 到 Pod 完全消失”这个完整时间轴。这条时间轴大致可以分成三段调度前、启动中、运行与终止。调度前主要涉及 API Server 的写入、调度器的节点选择、kubelet 的响应启动中涉及镜像拉取、存储卷挂载、Init 容器执行、主容器启动运行与终止则涉及探针检查、存活保障、退出前的清理动作。1.2 从 Pending 到 Running一次完整调度背后的状态流转我们提交一个 Deployment 之后控制平面会创建出 ReplicaSet再由 ReplicaSet 创建出 Pod 对象。这个 Pod 刚出现在集群里时状态是 Pending。Pending 意味着 Pod 已经被 API Server 接受了但还没找到合适的节点运行或者已经分配到节点但容器还没完全启动。Pending 阶段最常见的卡点有两个一是调度器找不到可用的节点比如资源不足、节点亲和性不满足、污点没被容忍二是 kubelet 已经拿到了 Pod 对象但在拉镜像或挂卷的过程中出现长时间阻塞。这时候很多人会直接去看 kubelet 日志其实用 kubectl describe pod 先看一眼 Events 更高效事件信息里往往已经写明了问题原因。当 kubelet 开始创建容器时状态会进入 ContainerCreating。如果定义了 Init 容器这个阶段还会出现 Init:N/M 这样的中间状态表示正在执行 M 个 Init 容器中的第 N 个。这里要特别提醒不要把 Init:N/M 单纯理解成“卡住了”如果 M 值比较大耐心观察一会是正常的真正需要警惕的是长时间停留在同一个 N 上不动那多半是第 N 个 Init 容器自身的逻辑出了问题。2. Init 容器在主容器启动前先把“地基”打牢2.1 Init 容器到底是什么和普通容器的区别Init 容器是 Pod 中一种特殊的容器它和普通容器最大的区别在于执行时机和生命周期Init 容器会按顺序逐个执行每一个都必须成功结束后下一个才会启动所有 Init 容器都成功结束主容器才会被创建。主容器的启动不依赖 Init 容器是否还在运行因为 Init 容器在执行完任务后就会退出它不是一个常驻进程。Init 容器和普通容器在配置上有几点显著差异Init 容器不支持 readinessProbe因为它们必须在主容器启动前完成而探针是给运行中的容器用的语义上说不通。Init 容器不支持 lifecycle.postStart这个钩子的设计初衷是在容器启动后执行而 Init 容器启动后是要退出的同样存在语义不匹配的问题。在资源分配上Init 容器可以用 requests 和 limits 定义资源但调度器会以所有 Init 容器和普通容器中 requests 的最大值作为调度依据这一点特别容易被忽视。这里还有个关键区别普通容器失败后kubelet 会根据 restartPolicy 决定是否重启而 Init 容器失败后即使 restartPolicy 是 Alwayskubelet 重启的也只是当前失败的这个 Init 容器本身不是跳过它去启动下一个。也就是说失败重试仍然从当前失败的 Init 容器开始不会跳号。2.2 Init 容器的典型使用场景Init 容器最常见的用途可以归成四类等待依赖服务就绪、初始化数据、权限配置、前置检查。第一类是等待依赖就绪。比如主容器需要访问数据库和缓存但数据库初始化可能需要几十秒这时候用一个 Init 容器循环检测数据库端口是否可连就绪后再放行主容器。有人说这不就是和探针的功能重叠了吗其实不一样。探针是持续检查主容器自身状态用的而 Init 容器解决的是“主容器启动前的外部依赖是否满足”的问题它不关心主容器启动后依赖有没有挂那属于探针的职责范围。第二类是初始化数据。典型的例子是把 Git 仓库里的配置文件拉下来或者从对象存储下载模型文件写入共享卷后再启动主容器。因为 Init 容器和主容器可以挂载相同的 Volume所以 Init 容器完成的数据写入主容器可以直接使用天然适配这种“预加载”场景。第三类是权限配置。有些容器镜像默认用非 root 用户运行但业务可能需要临时修改某些文件属主、创建目录、写入密钥。在 Init 容器里以较高权限完成这些操作要比在主容器里去改启动脚本安全得多也能让业务镜像保持干净不需要在业务代码里掺入系统配置逻辑。第四类是前置检查。比如检查某个自定义资源是否存在、检查当前集群版本是否满足要求、校验配置模板是否合法。不满足就直接让 Init 容器失败退出Pod 会保持在 Init 状态并触发重试这比业务启动到一半才发现环境不对要友好得多也更容易被监控系统第一时间捕获。2.3 实战Init 容器配置与注意事项下面用一个实际配置来演示 Init 容器的写法和几个容易踩坑的细节。apiVersion: v1 kind: Pod metadata: name: init-demo spec: initContainers: - name: wait-for-db image: busybox:1.36 command: - sh - -c - | until nc -z db-service.default.svc.cluster.local 3306; do echo waiting for db... sleep 2 done resources: requests: cpu: 100m memory: 128Mi - name: init-data image: busybox:1.36 command: - sh - -c - | if [ ! -f /shared/config.yaml ]; then echo config missing exit 1 fi volumeMounts: - name: shared-data mountPath: /shared containers: - name: main-app image: nginx:1.25 volumeMounts: - name: shared-data mountPath: /usr/share/nginx/html volumes: - name: shared-data emptyDir: {}这个例子里有几个细节值得展开说。第一Init 容器建议使用确定版本的镜像。busybox 这种工具镜像也一样写 busybox:1.36 而不是 busybox:latest避免镜像更新带来的不确定行为。我在实际项目里见过因为 busybox 版本变化导致 nc 命令不可用结果 Init 容器一直失败的情况最后排查了很久才发现是镜像 tag 的问题。第二Init 容器里写 shell 循环时建议加上最大重试次数。如果依赖服务长时间不可用理论上会无限重试下去Pod 会一直卡在 Init 状态。虽然这可能比直接启动失败要“稳”但对监控和告警来说很不友好。所以循环里加一个计数器超过一定次数就退出把问题暴露给上层监控比让 kubelet 无限重试更好排查。第三Init 容器执行时间不要太长。Kubernetes 对 Init 容器没有强制超时时间默认只有一个 activeDeadlineSeconds 可以控制 Pod 整体运行时长所以一个卡死的 Init 容器理论上可以阻塞很久。建议给 Pod 配上 activeDeadlineSeconds或者让 Init 容器内部自带超时退出逻辑。3. 探针Probe让 Kubernetes 学会“看病”3.1 三种探针的区别Liveness、Readiness、StartupProbe探针是 Kubernetes 对容器进行健康检查的机制本质上是 kubelet 按照配置的周期去探测容器是否“活着”“能用”“起来了”。三个探针各管一件事很多人把这仨混为一谈是配置事故的重灾区。LivenessProbe 决定的是“要不要重启”。如果 liveness 探测失败kubelet 会杀掉容器并按照 restartPolicy 重启它。它回答的问题是这个容器是不是已经凉了救不活了比如进程出现死锁、内存泄漏导致响应超时liveness 失败就可以触发重启来恢复。ReadinessProbe 决定的是“要不要给流量”。如果 readiness 失败kubelet 会把 Pod 从 Endpoint 列表里摘掉Service 就不会再把流量调度过来。它回答的问题是这个容器能不能处理请求比如依赖的下游服务还没连上、缓存还没预热完成这时容器进程本身是活着的但还不能对外服务就适合用 readiness 来“拉闸”。StartupProbe 解决的是“启动慢能不能别误杀”的问题。有些应用的启动时间特别长可能要几十秒甚至几分钟才能监听端口如果此时 liveness 已经开始探测大概率会探测失败导致容器被反复重启。StartupProbe 在启动阶段单独承担探测任务只有当 startup 探测成功之后kubelet 才把探测权交给 liveness 和 readiness。换句话说StartupProbe 是给慢启动应用加的“保护期”。3.2 探针的三种检测方式Exec、HTTP、TCP每种探针都有三种检测方式选型思路其实很简单看你的业务暴露了什么接口就用对应的方式。Exec 方式是在容器内执行一个命令通过命令的退出码判断健康状态。退出码为 0 表示健康非 0 表示不健康。适合没有暴露 HTTP 接口的场景或者需要做复杂判断的场景。比如检查某个 pid 文件是否存在、调用内部工具校验数据一致性。要注意的是Exec 方式需要在容器里有对应的命令镜像里没有就白搭。HTTP 方式通过访问一个 URL 来判断状态码在 200 到 399 之间都视为健康。这是最常见的探针方式适合暴露了 HTTP 接口的 Web 服务。配置时需要注意探针请求的是容器内部的端口不是通过 Service 暴露出来的 NodePort所以 Pod 的网络策略必须要允许 kubelet 访问容器的这个端口。我见过有人把探针端口写成 Service 端口导致探针一直失败的案例排查过程极其痛苦。TCP 方式检查端口能否建立 TCP 连接适合数据库、Redis、消息队列这类没有 HTTP 接口的服务。但 TCP 探测有个弱点端口能连上不代表服务真的可用。比如 MySQL 端口监听正常但主从同步状态异常TCP 探针是探不出来的这时候就得用 Exec 方式去执行 mysqladmin ping 之类的命令或者依赖业务层自己的健康检查接口。三种方式的判断逻辑可以简单总结成下表检测方式判断依据适合场景注意事项Exec命令退出码为 0无 HTTP 接口、需要自定义判断逻辑镜像需包含命令脚本逻辑避免过于复杂HTTP状态码 200-399Web 服务、API 服务端口要写容器端口路径要真实存在TCP端口能否建立连接数据库、中间件只能验证端口连通性无法验证业务状态3.3 参数调优与常见坑探针的配置参数里最容易被忽视的是 initialDelaySeconds、periodSeconds、timeoutSeconds 和 failureThreshold 这四个的组合关系。很多人随便写几个数字就开始用结果不是误杀就是探不出来。initialDelaySeconds 是容器启动后多少秒开始第一次探测。这个值要结合应用的启动时间设置设置太短探针会在应用还没起来时就开始打很可能是失败状态设置太长又会让故障发现变得迟钝。我的经验是先观察应用从进程启动到能正常服务的平均耗时然后在这个基础上加 3 到 5 秒作为 initialDelaySeconds 的基准值。periodSeconds 是探测周期默认 10 秒。timeoutSeconds 是单次探测的超时时间默认 1 秒这个值在很多场景下太短了。比如 Exec 方式执行一个脚本如果脚本里涉及网络请求1 秒很容易超时。我在生产环境一般把 timeoutSeconds 调到 3 到 5 秒。failureThreshold 是连续失败多少次才判定不健康。liveness 的 failureThreshold 如果设置太小比如 1那一次网络抖动就可能触发容器重启。建议 liveness 的 failureThreshold 给 3 到 5readiness 的可以设小一点比如 2 到 3因为 readiness 失败不重启容器只是摘流量惩罚没那么重。这里有个特别重要的数学计算kubelet 的故障判定时间要结合 periodSeconds 和 failureThreshold 来算。比如 periodSeconds10, failureThreshold3那么从第一次失败到判定为不健康总共需要约 30 秒。这个时间决定了故障恢复的延迟调参时要心里有数。如果业务对恢复速度要求高可以把 periodSeconds 调小一点如果对抖动容忍度高就调大一点。另一个高频坑探针会消耗额外的资源。Exec 探针每次探测都会在容器内启动一个进程如果探测脚本写得低效频繁执行会白白占用 CPU。HTTP 探针如果路径里做了复杂的查询或者鉴权也会影响业务接口本身的性能。所以我一般建议单独开一个轻量级的健康检查端点比如 /healthz不要在业务核心接口上做探针更不要在探针里做重查询。4. 优雅终止给容器一个体面的告别4.1 从 Delete Pod 到真正结束中间发生了什么优雅终止Graceful Shutdown是服务发布和缩容时最考验功底的一环。很多人眼里的“删除 Pod”就是一个 kubectl delete 命令的事但实际上从用户发起删除到 Pod 真正被清理中间发生了一连串步骤。当你执行 kubectl delete pod 或者 Deployment 滚动更新触发缩容时控制平面会更新 Pod 的状态随后 kubelet 收到终止通知。这时候 Pod 进入 Terminating 状态kubelet 开始执行终止流程。整个流程大致是Pod 被标记为 Terminating同时从 Service 的 Endpoints 中摘除新的流量不再进入该 Podkubelet 向 Pod 内的每个容器的主进程PID 1发送 SIGTERM 信号容器开始执行优雅终止逻辑比如关闭监听、处理完正在进行的请求、释放连接如果容器在 terminationGracePeriodSeconds 设定的宽限期内没有退出kubelet 会发送 SIGKILL 强制杀死。这个宽限期默认是 30 秒也就是 terminationGracePeriodSeconds 的默认值。这里要特别强调第一步的重要性。很多人会问为什么不先发 SIGTERM 再去摘流量其实顺序反了。如果流量还在而容器已经开始关闭那新的请求就会被拒绝或者超时。所以 Kubernetes 的终止流程是先让这个 Pod 从负载均衡池中“下线”再给容器发信号。但在实际网络环境下摘流量是异步完成的还存在一个传播延迟所以容器应用层面最好再留一点缓冲时间避免摘流量还没生效就已经开始关闭了。4.2 优雅终止的完整配置实战要写出一个合格的优雅终止配置需要从两个维度去考虑Kubernetes 侧的宽限期设置以及应用代码侧的信号处理实现。Kubernetes 侧的配置是这样的spec: terminationGracePeriodSeconds: 60 containers: - name: app image: myapp:1.0 lifecycle: preStop: exec: command: - sh - -c - sleep 5这里的 preStop Hook 能帮我们解决一个很现实的时序问题摘流量是异步的Pod 收到 SIGTERM 时可能还有一部分流量在路上。所以在容器真正进入关闭逻辑之前先用 preStop 睡眠几秒给流量摘除留出传播时间再进入优雅关闭这样可以有效避免请求被掐断。但这里有个很多人没注意到的细节terminationGracePeriodSeconds 的总宽限期是从 Pod 被标记为 Terminating 那一刻开始算的preStop 的执行时间也包含在这个宽限期里。如果你在 preStop 里 sleep 了 20 秒应用自己的优雅关闭又需要 20 秒加起来 40 秒那宽限期就不能设成默认的 30 秒否则应用还没优雅关闭完就会被 SIGKILL。所以宽限期设置必须把 preStop 的时间和应用自身的关闭时间都算进去。应用侧的信号处理是另外一个维度。Java 的 JVM 默认会对 SIGTERM 做处理并执行 shutdown hook但如果你的应用是通过脚本启动的要确保信号能传递到真正的业务进程。我遇到过一个典型案例应用的启动脚本用了 exec 启动 Java 进程结果 shell 自身没有正确转发信号导致 SIGTERM 发给了 shell 而不是 Java 进程最终只能靠 SIGKILL 强杀。正确的做法是在脚本的最后使用 exec java -jar app.jar 让 Java 进程成为 PID 1。4.3 优雅终止的常见排查优雅终止失败最典型的表现是日志显示 SIGKILL 被触发应用没有来得及处理完请求就被杀了。这时候要做的第一件事是用 kubectl describe pod 查看 Pod 的终止时间和 terminationGracePeriodSeconds 的关系对照应用日志里的关闭耗时判断是不是宽限期太短。还有一个容易被忽略的问题如果 Pod 里有多个容器kubelet 是并行向所有容器发送 SIGTERM 的而不是串行。有些场景下你可能希望先停止某一个容器再停止另一个容器Kubernetes 原生不支持这种顺序控制。有一种绕法在网络层面先通过独立 Service 的方式把流量切走或者在业务代码里通过分布式锁、消息队列等机制自己协调退出顺序。另外要留意的是如果你的 Service 用的是 headless Service 或者 StatefulSet 的 Pod终止时的摘流量行为会有所不同。headless Service 没有 ClusterIP也不做负载均衡摘流量依赖的是客户端 DNS 缓存和 TTL 的过期机制这在东西向流量场景下更复杂需要结合 DNS 缓存策略一起调优。5. 综合实战一个完整的 Pod 生命周期配置示例5.1 完整 YAML 示例把前面几个部分串起来下面这个 Deployment 配置覆盖了 Init 容器、三种探针和优雅终止的完整设置可以直接作为一个调整模板来用。apiVersion: apps/v1 kind: Deployment metadata: name: app-demo spec: replicas: 3 selector: matchLabels: app: demo template: metadata: labels: app: demo spec: terminationGracePeriodSeconds: 60 initContainers: - name: init-check image: busybox:1.36 command: - sh - -c - | max_retry30 retry0 until nc -z config-service 8080; do retry$((retry1)) if [ $retry -ge $max_retry ]; then echo config service not ready after ${max_retry}s exit 1 fi sleep 1 done echo config service is ready resources: requests: cpu: 50m memory: 64Mi containers: - name: app image: myapp:1.0 ports: - containerPort: 8080 startupProbe: httpGet: path: /health/startup port: 8080 failureThreshold: 30 periodSeconds: 2 readinessProbe: httpGet: path: /health/ready port: 8080 initialDelaySeconds: 5 periodSeconds: 5 timeoutSeconds: 3 failureThreshold: 3 livenessProbe: httpGet: path: /health/live port: 8080 initialDelaySeconds: 10 periodSeconds: 10 timeoutSeconds: 3 failureThreshold: 3 lifecycle: preStop: exec: command: - sh - -c - sleep 5 resources: requests: cpu: 100m memory: 256Mi limits: cpu: 500m memory: 512Mi5.2 验证与效果这个配置的设计思路我展开说一下。startupProbe 使用 httpGet 探测 /health/startupfailureThreshold 设成 30periodSeconds 设成 2意味着最大允许启动时间为 60 秒。如果应用启动特别慢startupProbe 会兜住不会让它被 liveness 误杀。而 liveness 的全部探测参数是在 startup 成功后才开始生效的所以 liveness 的 initialDelaySeconds 根本不影响启动阶段。readinessProbe 和 livenessProbe 我故意使用了不同的路径。虽然很多场景下可以复用一个 /healthz但更规范的做法是区分开/health/ready 不仅要检查进程状态还要检查依赖的服务是否可用/health/live 只需要确认进程本身还活着。这样设计的好处是当下游依赖故障时readiness 先“拉闸”摘流量而 liveness 不会因为依赖故障去重启容器避免无意义的重启。preStop 里 sleep 5 秒是为了给 Service 的端点摘除留出传播时间。配合 terminationGracePeriodSeconds 设为 60 秒整个宽限期内有 5 秒用于 preStop剩余 55 秒足够应用自身关闭。我一般会在订单类、支付类这种对数据一致性要求高的服务上把宽限期拉得更长比如 90 秒因为这类服务的连接池、消息队列可能有很多在途请求需要更长的排空时间。6. 常见问题与排查技巧实录6.1 Pod 一直 Pending调度失败怎么查Pending 的最常见原因是资源不足。用 kubectl describe pod 看 Events会看到类似 Failed to schedule pod, 0/3 nodes are available 之类的事件。然后看具体原因是 CPU、内存不够还是节点亲和性不满足或者 PVC 等待创建。如果是资源不足可以用 kubectl top nodes 看节点余量再决定是扩容节点还是调低资源配置。另一个容易忽略的点是如果集群有污点TaintPod 没有对应的容忍Toleration就永远不会被调度到该节点。这种问题在 describe 里也会有明显提示很多人一看到 Taint 事件就直接慌其实加上对应的 toleration 就能解决。6.2 Init 容器一直失败或卡住先看 Pod 的 Events 和 Init 容器日志区分是逻辑失败还是环境问题。逻辑失败通常会在日志里打印明确原因比如等待超时退出环境问题则可能是镜像不存在、拉取失败、Volume 挂载不了。用 kubectl logs pod-name -c init-container-name 查看日志记住 Init 容器的日志是可以通过 -c 指定容器名来查看的很多人不知道这一点总以为 Init 容器的日志看不了。如果 Init 容器反复重启检查脚本有没有幂等性。比如脚本中创建了文件但失败重跑时不会覆盖旧文件这种非幂等脚本在重试时可能一直失败。我习惯在脚本开头先做幂等处理先清理上一次的产物再重新生成避免重试时的隐藏问题。6.3 Readiness 探针失败导致 502/504Readiness 探针失败时 Pod 会被摘流量但如果整个应用的副本数过少摘掉几个之后就可能导致没有可用副本请求全部报 502。这类问题要从两个方向排查一是 Readiness 探针的阈值设置是否太敏感比如 path 里的接口因为引入了一个数据库查询而变慢导致探针频繁失败二是副本数是否足够是否需要配置 PodDisruptionBudget 来保证最少可用副本数避免主动运维操作或故障时出现可用副本归零的情况。6.4 SIGTERM 后容器不退出只能被强杀如果容器收到 SIGTERM 后在宽限期内没退出kubelet 会发 SIGKILL。排查时先确认你的应用是否注册了信号处理函数。很多语言的默认行为是收到 SIGTERM 直接退出但一些框架需要你显式注册优雅关闭逻辑。还要注意多进程问题如果容器启动命令是脚本而脚本启动的是子进程信号可能被脚本进程接住但没有转发给子进程相当于关错了对象。这时候就需要脚本用 exec 方式启动真正的进程让业务进程成为 PID 1。6.5 探针配置导致容器不断重启容器不断重启第一反应是看 liveness 探针是不是配置太激进。结合前文的计算方式我把故障场景帮大家捋一下当 liveness 连续失败达到 failureThreshold 时kubelet 会重启容器。这时如果应用启动速度跟不上 liveness 的检查节奏就会陷入“启动-被探测失败-重启”的循环。解决办法就是引入 startupProbe给启动阶段单独设置缓冲区同时调整 liveness 的参数让判定更宽容。这类问题在日志里最典型的表现是容器频繁进入 CrashLoopBackOff但实际上容器并没有真正崩溃只是被探针“误杀”而已。6.6 我的几个实战心得根据我这几年的实践经验Pod 生命周期管理所谓的“实战指南”本质上是在回答三个问题主容器什么时候能开始干活干活的过程中怎么判断健康停止干活的时候要怎么优雅收尾Init 容器解决第一个问题探针解决第二个优雅终止解决第三个。把这三个问题想清楚你的 YAML 就不会再是网上抄来的一堆配置拼凑。还有一点不得不提Pod 生命周期管理不是纯 Kubernetes 层面的技术它需要应用代码的配合。探针需要应用提供健康检查端点优雅终止需要应用注册信号处理函数。如果应用完全不配合K8s 侧配置再完美也是白搭。所以我在实际工作中会把“开发规范”和“平台规范”一起推进让每个服务在开发阶段就把健康检查端点和优雅关闭逻辑写进代码里而不是部署时再去打补丁。最后再给大家一个小建议每次调整探针参数或者终止策略之后不要急着上生产先用一个小集群或者本地环境跑一遍滚动发布观察 Pod 的状态变化和流量切换情况。我用这个办法避免过很多次线上事故成本极低但收益极高。Pod 生命周期管理这件事看起来是纯基础设施问题实际上贯穿了整个服务的开发、部署、运维全流程值得投入时间去打磨。