1. 从“跑起来”到“用起来”:为什么必须懂Pod生命周期
Kubernetes里有个老生常谈但又特别容易翻车的话题:Pod生命周期。很多人刚接触k8s时,会用kubectl get pod看到一堆状态,一会儿ContainerCreating,一会儿又CrashLoopBackOff,不知道它到底在干什么。作为系列第三篇,这篇文章就是想把这个过程彻底讲透——Pod从被提交到集群,到最终被销毁,中间到底经历了什么,每个阶段启动容器、执行钩子、健康检查的顺序是怎样的,以及在什么场景下该用哪种检查方式。
这个内容能帮着你解决几类实际问题:服务启动慢导致流量打进来就报错、发布新版本时旧Pod还没处理完请求就被杀掉、Pod反复崩溃但看不出原因、依赖服务没就绪导致业务启动后疯狂重试。如果你正准备排查这些问题,或者想把k8s的稳定性做得更细,这篇文章值得认真过一遍。
文章会按这样的顺序展开:先梳理Pod从创建到销毁的完整时间线,再讲PostStart和PreStop这两个生命周期钩子怎么用,接着对比Liveness、Readiness、Startup三种探针的适用场景,然后分析三种重启策略的控制逻辑,最后用一个真实发布的例子把前面所有知识点串起来。
2. Pod的“一生”:从创建到销毁的完整时间线
2.1 五个核心状态:Pending、Running、Succeeded、Failed、Unknown
Kubernetes官方把Pod的生命周期分为五个阶段,kubectl get pod输出的STATUS列就是这五个状态之一。
Pending:Pod已经被集群接受,但还没有就绪。这个阶段可能正在调度、正在拉取镜像,或者Initalize容器还没跑完。Pod在这个阶段没有真正的工作负载在运行。Running:Pod已经绑定到了某个节点,而且所有容器至少启动了一个,正在运行中。注意“至少一个”这个描述,意味着Running不代表业务就绪,这一点很多人会踩坑。Succeeded:Pod里所有容器都成功退出,退出码为0。这一般是批处理任务或Job的正常结束状态。Failed:Pod里至少有一个容器以非0退出码退出,或者因为系统原因被终止。Unknown:kubelet无法向API Server汇报状态,比如节点失联、网络分区,这时控制平面就把Pod标记为Unknown。
只看状态描述可能还是有点飘,我一般用“人从叫车到下车”来类比:Pending是你在App上点了“呼叫”,司机还没接单或者车还在路上;Running是车已经在开了,但你在车上不一定意味着你已经到目的地了;Succeeded是你付钱下车了,一整趟都顺利;Failed是半路出了事故;Unknown则是司机和平台完全失联,平台不知道你在哪。
2.2 一个Pod从提交到稳定运行的完整时序
很多人搞不清Pod内部那些容器到底按什么顺序启动,这里把完整流程列一下:
- 用户通过
kubectl apply创建Pod对象,请求到API Server,对象被写入etcd。 - 调度器(kube-scheduler)开始调度,选择合适的节点,更新Pod的
nodeName字段。 - 节点上的kubelet观察到Pod被分配给自己,开始处理。
- 如果有Init容器,按定义顺序依次初始化,只有前一个Init容器成功退出,后一个才会启动。
- 初始化完成后,kubelet启动主容器,同时执行
postStart钩子(如果配置了的话)。 - 容器启动后,如果配置了Readiness探针,根据探测结果决定Pod是否进入Ready状态。
- Liveness探针持续运行,一旦探测失败,kubelet会根据restartPolicy决定是否重启容器。
有一个细节容易被忽略:postStart和容器主进程是异步执行的。说得直白点,postStart钩子不保证一定在容器的ENTRYPOINT之前运行,它两个是同时触发的,中间存在竞争关系。在JAVA服务启动脚本里读取某个配置文件,而postStart恰好要往文件里写内容,就可能出现先后顺序不确定的问题,这点要格外注意。
再补充一个行为细节:postStart钩子如果执行失败或者一直卡住,容器会被标记为Failed状态并可能被重启。这个机制本意是好的,防止配置没生效就启动容器,但实际中如果钩子里写了个死循环,容器就会一直处于ContainerCreating,排查半天找不到问题,以为在拉镜像,其实是钩子卡住了。
2.3 生命周期中的两个关键窗口:启动期与优雅退出期
在Pod生命周期里,有两个阶段容易引发线上故障:启动期和退出期。
启动期的核心问题是容器启动慢但探针默认立刻开始探测。比如一个JAVA应用要加载大量配置、连接数据库,启动可能要20到30秒,但Liveness探针在容器启动后马上开始检测,前几次失败就会触发重启,形成CrashLoopBackOff。解决思路有两个:配置startupProbe,或者把livenessProbe的initialDelaySeconds调大。从k8s 1.17开始推荐的方案是引入startupProbe,因为它在成功之前不会让liveness探针参与判定。
退出期的核心问题是优雅停机。默认情况下,如果Pod被删除,kubelet会向容器发送SIGTERM信号,然后等待terminationGracePeriodSeconds(默认30秒)超时后强制发送SIGKILL。如果应用不处理SIGTERM,30秒后就会被强杀,正在处理的请求直接断掉。对于流量型业务,一般建议在preStop钩子里做流量摘除和排空,比如先调用注册中心的注销接口,再sleep几秒,让网关把流量切走,之后再慢慢退出。
3. 生命周期钩子实战:PostStart与PreStop的配置与坑
3.1 PostStart:容器启动后的钩子怎么用
postStart在容器创建之后立刻执行,主要用来做一些“容器内不方便做、但又必须在业务起来之前完成”的初始化操作。常见场景包括:
- 修改容器内配置文件,比如往Nginx的配置里写入上游地址。
- 把本Pod的IP注册到外部注册中心。
- 预热本地缓存或者建立数据库连接池。
配置格式比较简单,支持两种写法:exec和httpGet。exec执行一段shell命令,httpGet请求一个URL。
apiVersion: v1 kind: Pod metadata: name: demo-poststart spec: containers: - name: demo image: nginx:1.24 lifecycle: postStart: exec: command: ["/bin/sh", "-c", "echo hello >> /tmp/startup.log"]这个配置会在容器主进程启动的同时执行echo hello >> /tmp/startup.log。看起来简单,但要注意几个问题。
第一个问题是postStart和主进程的竞争关系。postStart跟ENTRYPOINT同时触发,并不是先后关系,如果两边同时操作同一个文件,就容易出现“钩子写的内容主进程读不到”的情况。我在实际部署某状态上报服务时,就遇到过主进程起来后读配置,配置还没写完的问题,排查了很久最后定位到是竞态。
第二个问题是钩子不能用http://localhost来访问容器的服务。要知道postStart执行时容器主进程可能还没监听端口,即使监听了,钩子执行的上下文也未必完全等价于容器内的网络命名空间。不是所有网络访问都能像预期一样工作,如果在postStart里访问localhost:8080/health,可能请求直接被拒绝。
第三个问题是钩子执行没有超时控制。如果钩子里命令一直挂在那里,Pod会一直卡在ContainerCreating状态。按社区实践,postStart里不要放耗时长的任务,超过几十秒要怀疑是不是有问题,最好用独立进程去完成初始化,或者放到业务进程内部的启动逻辑里。
3.2 PreStop:优雅停机钩子的正确打开方式
preStop在容器被终止前执行,执行完才发SIGTERM。典型用途是在删除Pod之前把流量从负载均衡上摘掉,然后让业务进程有足够时间处理存量请求。
apiVersion: v1 kind: Service metadata: name: demo-web spec: selector: app: demo-web ports: - port: 80 targetPort: 8080 --- apiVersion: apps/v1 kind: Deployment metadata: name: demo-web spec: replicas: 3 selector: matchLabels: app: demo-web template: metadata: labels: app: demo-web spec: terminationGracePeriodSeconds: 30 containers: - name: demo-web image: registry.example.com/demo-web:v2 lifecycle: preStop: exec: command: ["/bin/sh", "-c", "sleep 10"]这里的sleep 10是预留时间,让Endpoints里先摘掉这个Pod,新的流量不再进来,旧请求继续被处理,具体时间可以根据业务调整。如果应用本身处理SIGTERM逻辑比较好,preStop里的等待时间可以短一些;如果应用完全不处理SIGTERM信号,那就必须靠preStop拖时间,否则强杀。
再提醒一个细节:preStop的执行时间是被terminationGracePeriodSeconds限制的。kubectl删除Pod时开始计时,30秒内必须完成preStop和主进程退出,超时就被SIGKILL。如果你的preStop要10秒,应用优雅退出要20秒,共存期就要留到35秒,否则存活请求还是会断。
还有一点,preStop不会因为业务的请求处理完而自行结束,它就是在那里执行命令直到完成。所以不要在里面写无限等待的逻辑,一定要能退出。
3.3 什么时候不该用钩子
钩子不是万能的,有些场景用钩子反而添乱。
如果你需要的是“先做A再做B”这种严格顺序,postStart不适合,应该用Init容器。Init容器串行执行,前一个成功退出后一个才开始,天然有序。
如果只是想等一个外部依赖就绪,比如数据库、Redis,postStart也不合适,应该用Readiness探针。postStart阻塞会让容器卡住,Readiness探针只是不让服务接流量,业务进程本身还能跑,状态也能保持可观测。
最忌讳的是把postStart当成部署脚本用。部署脚本涉及大量文件复制、依赖安装,应该打镜像,不该在Pod启动时现场操作。镜像里没有的东西,要重新构建镜像。
4. 三种探针一次讲透:Liveness、Readiness、Startup Probe
4.1 三种探针到底在探什么
探针是kubelet定期对容器发起检查的机制,配置在容器级别。三者定位完全不同,简单理解:
- Liveness探针:决定要不要重启容器。探测失败,kubelet按重启策略杀掉并重启容器。
- Readiness探针:决定要不要把流量打进来。探测失败,Pod会从Service的Endpoints里被摘掉,业务进程不重启。
- Startup探针:决定要不要给容器“豁免期”。探测成功之前,Liveness和Readiness的探测都会暂停。
用类比来看:Liveness像是人是否活着,心跳停了就送急救(重启);Readiness像是人是否能够接待客户,状态不好就先挂“暂停服务”的牌子(流量摘除);Startup像是给新人一个适应期,你不用一上来就判断他是否熬得住,先让他顺利度过启动阶段再说。
这个机制在配置上的体现如下:
readinessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 5 periodSeconds: 10 livenessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 15 periodSeconds: 20 startupProbe: httpGet: path: /health port: 8080 failureThreshold: 30 periodSeconds: 54.2 参数怎么调才不踩坑
每个探针都有几个通用参数,先把这些参数讲明白。
initialDelaySeconds:容器启动后,延迟多久才开始第一次探测。这是最容易被忽视的。如果业务启动需要20秒,这个值设成3秒,前几次探测大概率失败。periodSeconds:探测间隔,默认10秒。间隔太长,故障发现慢;间隔太短,探测请求对业务有压力,还容易误判。timeoutSeconds:单次探测超时时间,默认1秒。如果接口响应经常超过1秒,要调大,否则探针请求超时被当成失败。failureThreshold:连续失败多少次才触发动作,默认3次。Liveness连续失败3次就重启;Readiness连续失败3次就摘流量。successThreshold:连续成功多少次才认为健康,默认1次。Readiness一般保持默认,别调高,否则Pod恢复接流量的时间变长。
实际调参经验可以概括为:先摸清业务的启动时间,再配探针。比如某服务启动要25秒,API接口最坏响应2秒,可以这样配:
startupProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 0 periodSeconds: 5 timeoutSeconds: 3 failureThreshold: 30 readinessProbe: httpGet: path: /health port: 8080 periodSeconds: 5 timeoutSeconds: 3 failureThreshold: 3 livenessProbe: httpGet: path: /health port: 8080 periodSeconds: 10 timeoutSeconds: 3 failureThreshold: 3这样配置的逻辑是:startupProbe给了最多150秒的启动窗口,业务25秒内启动绰绰有余;启动成功后,readiness每5秒查一次,最多15秒摘流量;liveness判断的是运行期活没活,间隔10秒比较合理,不会过于频繁。
4.3 探针选择与常见误区
很多人会把Liveness和Readiness混用,或者只配一个Liveness,这里说说两者的权衡。
如果只配Liveness不配Readiness,Pod启动过程中流量就会打进来。因为Pod状态是Running且Ready(不带readinessProbe时,默认容器启动即为Ready),而实际业务还没起来,请求失败率飙升。正确的做法是:业务启动慢或需要依赖外部服务,必须配Readiness。
如果只配Readiness不配Liveness,业务彻底死锁后流量不会进,但容器一直占用资源,不重启不恢复。所以核心服务的标准配置是:startupProbe保证慢启动容器有缓冲,readinessProbe保证流量不进不可用Pod,livenessProbe保证死锁容器能被回收。
还有一个比较容易忽略的地方,httpGet探针的返回值处理。只要HTTP状态码在200到399之间就认为成功,其他一律失败。如果健康检查接口返回302重定向,探针会判定成功,但如果重定向到了一个不存在的位置,业务层不健康,探针却认为正常,这会导致流量打到坏的Pod上。
5. 重启策略:Pod反复崩溃背后的控制逻辑
5.1 Always、OnFailure、Never的适用场景
restartPolicy是Pod级别的字段,控制的是“容器异常退出后是否重启”,而不是“整个Pod是否被调走”。
Always:容器无论以什么方式退出,都重新启动。Deployment、StatefulSet这类工作负载默认是Always,这也是为什么普通服务崩了之后k8s会不断拉起重启。OnFailure:只有容器以非0退出码退出时才重启。适合Job类任务,比如批处理失败了自动重跑。Never:容器退出后不重启。适合一次性任务,第二次跑直接创建新Pod。
我见过有人把restartPolicy跟“Pod删除后是否重建”搞混。Pod是受Deployment等控制器管理的,重启策略只管Pod内部容器的行为;Pod被删掉后是否重新创建,那是控制器的事,跟restartPolicy无关。
5.2 CrashLoopBackOff的逻辑与处理
如果容器反复启动又退出,会进入CrashLoopBackOff状态。kubelet会指数退避,比如第一次隔10秒重启,第二次20秒,第三次40秒,上限300秒。
看到CrashLoopBackOff,不要急着直接去改配置,第一步要去看日志,才能定位到根因,常见原因可能包括:
- 启动命令写错,容器起来就退出,这个通过
kubectl logs基本就能看到。 - 配置缺失或者环境变量传错,进程启动时直接panic。
- Liveness探针配置太激进,容器明明还在启动,就被判定为失败然后杀掉重启。
- 镜像启动命令里依赖了不存在的文件或者挂载的ConfigMap不完整。
提供几个常用排查命令:
# 查看当前状态 kubectl get pod -o wide # 查看具体事件 kubectl describe pod <pod-name> # 查看日志 kubectl logs <pod-name> --tail=100 # 查看上一次容器的退出原因 kubectl get pod <pod-name> -o jsonpath='{.status.containerStatuses[0].lastState}'顺带说个经验:如果容器重启得很快,日志里看不到崩溃信息,可以查看退出的容器日志,命令需要加--previous参数,例如kubectl logs <pod-name> --previous。这是最早被我漏掉的排查方式,却很管用。
5.3 重启策略与Docker重启策略的区别
有Docker基础的人可能自然而然想到--restart=always。两者机制有本质区别:
Docker的重启策略是Docker守护进程负责执行,崩溃后由dockerd拉起,不关心容器在哪个宿主机上,也不做分布式调度。Kubernetes的重启是由kubelet负责,重启只是它的一种处理动作,而且会结合探针状态和控制器期望数来决定。更关键的是,Kubernetes还有一个机制:如果节点本身故障,Pod会被重新调度到其他节点,这是Docker单机模式不具备的。
不要以为容器崩溃重启了就等于服务恢复。重启后的容器进程能起来,不代表业务就绪,依赖的外部服务没过健康检查,CrashLoopBackOff还是会再来。
6. 实操复盘:一次滚动更新中的Pod生命周期全过程
6.1 场景与配置
接下来用一个发布场景把所有知识串起来。假设我现在管理一个叫demo-web的服务,当前版本是v1,准备发布v2版本,Deployment副本数为3,配置了readinessProbe和livenessProbe,同时在preStop里做了流量摘除等待。
发布命令很简单:
kubectl set image deployment/demo-web demo-web=registry.example.com/demo-web:v2或者用apply方式更新镜像。关键是想清楚发布过程中每个Pod如何完成“旧下线、新上线”的交接。
6.2 观察生命周期状态变化
发布触发后,Deployment控制器会创建新的ReplicaSet,期望副本数为0逐步增加。发布过程可以随时用kubectl rollout status deployment/demo-web观察。
大概率能看到的事件,按顺序是这样的:
- 新Pod创建,状态进入Pending,调度中选择可用节点。
- 如果节点资源不足,会卡在Pending,事件里能看到
FailedScheduling,通常会给出资源不足的信息。 - Pending阶段还可能拉镜像,尤其是新镜像首次拉取,耗时较长。
- 镜像拉取完成后,容器开始创建,进入ContainerCreating。这个阶段如果配置了postStart,也在此时执行。
- 容器启动后进入Running状态,但因为配置了readinessProbe,一开始Ready列是0/1,探针通过后变为1/1。
- 新Pod全部就绪后,旧的ReplicaSet开始缩容,旧Pod被终止。
- 终止旧Pod时,preStop开始执行,Sleep等待,之后主进程收到SIGTERM,开始优雅退出。
- 旧Pod全部终止,发布完成。
整个过程可以尝试用一个“类似滚动更新记录”的节奏来观察:
# 以JSON格式实时观察Pod状态 kubectl get pods -w # 观察Endpoints的变化,流量是否切换 kubectl get endpoints demo-web -w6.3 发布过程中可能遇到的经典问题
发布过程中最容易出问题的点是新Pod还没就绪,旧Pod就已经被下线了,导致服务空窗。这个问题的根因是Deployment的maxUnavailable和maxSurge参数配置不合理。
默认配置里maxSurge=25%,maxUnavailable=25%,也就是每次允许额外创建一个新Pod,允许一个旧Pod不可用。如果副本数是3,滚动过程可能先杀掉一个旧Pod,同时创建一个新Pod,此时所有副本的总可用Pod最少为2,一般不会太慌。
但有些团队会为了加速发布把maxUnavailable调成100%,那就会先杀掉所有旧Pod,再创建新Pod,整个过程服务完全不可用。现场如果探针没配好,新Pod一直不就绪,服务就长时间挂掉。经验是线上流量型服务默认别把maxUnavailable调太大,宁可慢一点,保证总可用Pod不要低于期望值的60%到70%。
另一个经典问题是新Pod启动后readiness一直不通,Deployment发布会阻塞,旧Pod继续运行,表面看起来服务没挂,但新版本一直发布不完。这时要立刻查探针配置是不是有问题,比如健康检查接口路径配错了,或者新版本接口返回状态码异常。
6.4 这个场景能带给你哪些判断思路
一旦把生命周期状态和发布机制串起来看,排障思路会清晰很多。
- Pod卡在Pending:优先看调度事件,常见原因是资源不足、污点不容忍、亲和性不匹配。
- Pod卡在ContainerCreating:优先看是否有Init容器、postStart钩子是否卡住、镜像是否能正常拉取。
- Pod变成Running但Ready为0/1:优先查readiness探针,看健康检查接口是否正常。
- Pod反复重启:优先查liveness探针和重启策略,再看业务日志。
- Pod删除后一直Terminating:优先看preStop是否执行完,terminationGracePeriodSeconds是否足够。
这些判断本质上都是在问一个问题:当前Pod正处在生命周期流程的哪一步,它期望的下一步是什么。
7. 常见问题速查与经验建议
7.1 Pod生命周期相关排查速查表
| 现象 | 可能原因 | 排查命令 | 处理建议 |
|---|---|---|---|
| 一直Pending | 资源不足、污点、亲和性 | kubectl describe pod | 查看Events,调整资源或调度配置 |
| ContainerCreating超时 | 镜像拉取慢/失败、钩子卡住 | kubectl describe pod | 看镜像地址是否正确,钩子逻辑是否卡死 |
| Running但Ready为0 | Readiness探针失败 | kubectl get pod -o yaml | 检查健康检查接口路径与端口 |
| CrashLoopBackOff | 启动命令错误、探针误杀 | kubectl logs --previous | 先看上次退出日志 |
| 删除Pod卡在Terminating | preStop执行超时 | kubectl describe pod | 检查preStop命令和gracePeriod |
| 容器未收到SIGTERM | 业务忽略信号 | 业务日志 | 接入优雅退出逻辑或通过preStop补足 |
7.2 三条值得沉淀的实战经验
第一,配置探针时把启动时间想清楚。大多数容器启动慢的问题,都可以通过startupProbe解决,不要靠调整initialDelaySeconds硬撑。startupProbe失败意味着容器彻底起不来了,liveness和readiness探测会暂停,它们只关心运行期健康,这样的职责划分才清晰。
第二,preStop里加sleep要适度。有团队直接把sleep固定成20秒,完全不看业务实际需要的优雅退出时间。有些请求几百毫秒就处理完了,白白等20秒,发布一次要多等很久。正确做法是根据业务请求的最坏响应时间去设置,同时配合terminationGracePeriodSeconds整体规划。
第三,做任何涉及Pod生命周期配置的变更,都要先看影响面。改探针参数可能影响发布节奏,改重启策略可能影响故障恢复,改优雅退出时间可能影响发布耗时。要在测试环境完整跑一轮发布再来线上操作。
我个人用得比较多的一句检查清单是:创建时看Init容器、启动时看钩子、稳定后看探针、退出前看preStop。把Pod的一生按这几个环节切分,很多问题都能快速定位到具体环节,不会瞎猜。这套思路在维护实际业务系统时帮过我很多次,也建议你从今天开始,每次发布都用kubectl get pods -w观察一遍New Rs的变化,看多了就熟了。