3. pod的生命周期
Pod生命周期
一个 Pod 的完整生命周期过程,其中包含 Init Container、Pod Hook、健康检查 三个主要部分,接下来我们就来分别介绍影响 Pod 生命周期
的部分。
首先在介绍 Pod 的生命周期之前,我们先了解下 Pod 的状态,因为 Pod 状态可以反应出当前我们的 Pod 的具体状态信息,也是我们分析排错的一个必备的方式。
Pod状态
首先先了解下 Pod 的状态值,我们可以通过 kubectl explain pod.status 命令来了解关于 Pod 状态的一些信息,Pod 的状态定义在 PodStatus 对象中,其中有一个 phase 字段,下面是 phase 的可能取值:
- 挂起(Pending):Pod 信息已经提交给了集群,但是还没有被调度器调度到合适的节点或者 Pod 里的镜像正在下载
- 运行中(Running):该 Pod 已经绑定到了一个节点上,Pod 中所有的容器都已被创建。至少有一个容器正在运行,或者正处于启动或重启状态
- 成功(Succeeded):Pod 中的所有容器都被成功终止,并且不会再重启
- 失败(Failed):Pod 中的所有容器都已终止了,并且至少有一个容器是因为失败终止。也就是说,容器以非0状态退出或者被系统终止
- 未知(Unknown):因为某些原因无法取得 Pod 的状态,通常是因为与 Pod 所在主机通信失败导致的
除此之外,PodStatus 对象中还包含一个 PodCondition 的数组,里面包含的属性有:
- lastProbeTime:最后一次探测 Pod Condition 的时间戳。
- lastTransitionTime:上次 Condition 从一种状态转换到另一种状态的时间。
- message:上次 Condition 状态转换的详细描述。
- reason:Condition 最后一次转换的原因。
- status:Condition 状态类型,可以为 True , False 和 Unknown。
- type:Condition 类型,包括以下方面:
- PodScheduled:Pod 已经被调度到其他 node 里
- Ready:Pod 能够提供服务请求,可以被添加到所有可匹配服务的负载平衡池中
- Initialized:所有的init containers已经启动成功
- Unschedulable:调度程序现在无法调度 Pod,例如由于缺乏资源或其他限制
- ContainersReady:Pod 里的所有容器都是 ready 状态
# 获取完整描述 Kubernetes API Server 返回的 Pod 对象的完整描述
kubectl get pod <pod-name> -n <namespace> -o yaml
# 里面有这个
status:
conditions:
- lastProbeTime: null
lastTransitionTime: "2025-09-04T01:51:31Z"
status: "True"
type: PodReadyToStartContainers
- lastProbeTime: null
lastTransitionTime: "2025-09-04T01:28:55Z"
status: "True"
type: Initialized
- lastProbeTime: null
lastTransitionTime: "2025-09-04T01:51:33Z"
status: "True"
type: Ready
- lastProbeTime: null
lastTransitionTime: "2025-09-04T01:51:33Z"
status: "True"
type: ContainersReady
- lastProbeTime: null
lastTransitionTime: "2025-09-04T01:28:53Z"
status: "True"
type: PodScheduled
# status的话直接看就好了
kubectl get pods -A
# 或者(比默认多输出一些信息(比如 Pod 的 IP、Node 等))
kubectl get pods -A -o wide
重启策略(restartPolicy)
我们可以通过配置 restartPolicy 字段来设置 Pod 中所有容器的重启策略,其可能值为 Always、OnFailure 和 Never,默认值为 Always,restartPolicy 指通过 kubelet 在同一节点上重新启动容器。通过 kubelet 重新启动的退出容器将以指数增加延迟(10s,20s,40s…)重新启动,上限为 5 分钟,并在成功执行 10 分钟后重置。不同类型的的控制器可以控制 Pod 的重启策略:
- Job:适用于一次性任务如批量计算,任务结束后 Pod 会被此类控制器清除。Job 的重启策略只能是 OnFailure或者 Never。
- ReplicaSet、Deployment:此类控制器希望 Pod 一直运行下去,它们的重启策略只能是 Always。
- DaemonSet:每个节点上启动一个 Pod,很明显此类控制器的重启策略也应该是 Always。
初始化容器(initContainers)
了解了 Pod 状态后,首先来了解下 Pod 中最先启动的 Init Container,也就是常说的初始化容器。InitContainer 就是用来做初始化工作的容器,可以是一个或者多个,如果有多个的话,这些容器会按定义的顺序依次执行。我们知道一个 Pod 里面的所有容器是共享数据卷(要主动声明)和 Network Namespace 的,所以 InitContainer 里面产生的数据可以被主容器使用到。从上面的 Pod 生命周期的图中可以看出初始化容器是独立与主容器之外的,只有所有的初始化容器执行完之后,主容器才会被启动。那么初始化容器有哪些应用场景呢:
- 等待其他模块 Ready:这个可以用来解决服务之间的依赖问题,比如我们有一个 Web 服务,该服务又依赖于另外一个数据库服务,但是在我们启动这个 Web 服务的时候我们并不能保证依赖的这个数据库服务就已经就绪了,所以可能会出现一段时间内 Web 服务连接数据库异常。要解决这个问题的话我们就可以在 Web 服务的 Pod 中使用一个InitContainer,在这个初始化容器中去检查数据库是否已经准备好了,准备好了过后初始化容器就结束退出,然后我们主容器的 Web 服务才开始启动,这个时候去连接数据库就不会有问题了。
- 做初始化配置:比如集群里检测所有已经存在的成员节点,为主容器准备好集群的配置信息,这样主容器起来后就能用这个配置信息加入集群。
- 其它场景:比如将 Pod 注册到一个中央数据库、配置中心等。
比如现在我们来实现一个功能,在 Nginx Pod 启动之前去重新初始化首页内容,如下所示的资源清单:
# init-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: init-demo
spec:
# 这里指定重启策略
restartPolicy: Always
volumes:
- name: workdir
emptyDir: {}
initContainers:
- name: install
image: busybox
command:
- wget
- "-O"
- "/work-dir/index.html"
- "http://www.baidu.com"
volumeMounts:
- name: workdir
mountPath: "/work-dir"
containers:
- name: web
image: nginx
ports:
- containerPort: 80
volumeMounts:
- name: workdir
mountPath: /usr/share/nginx/html
上面的资源清单中我们首先在 Pod 顶层声明了一个名为 workdir 的 Volume,这里我们使用的是一个 emptyDir{}类型的数据卷,这个是一个临时的目录,数据会保存在 kubelet 的工作目录下面,生命周期等同于 Pod 的生命周期。然后我们定义了一个初始化容器,该容器会下载一个网页的 html 代码文件到 /work-dir 目录下面,但是由于我们又将该目录声明挂载到了全局的 Volume,同样的主容器 nginx 也将目录 /usr/share/nginx/html 声明挂载到了全局的Volume,所以在主容器的该目录下面会同步初始化容器中创建的 index.html 文件。
直接创建上面的 Pod:
ubuntu@ubuntu:~/example$ kubectl apply -f ./init-pod.yaml
pod/init-demo created
ubuntu@ubuntu:~/example$ kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
init-demo 1/1 Running 0 59s 10.244.1.12 node2 <none> <none>
# 去宿主机 curl
ubuntu@ubuntu:~$ curl 10.244.1.12
<!DOCTYPE html>
<!--STATUS OK--><html> <head><meta http-equiv=content-type content=text/html;charset=utf-8><meta http-equiv=X-UA-Compatible content=IE=Edge><meta content=always name=referrer><link rel=stylesheet type=text/css href=http://s1.bdstatic.com/r/www/cache/bdorz/baidu.min.css><title>百度一下,你就知道</title></head> <body link=#0000cc> <div id=wrapper> <div id=head> <div class=head_wrapper> <div class=s_form> <div class=s_form_wrapper> <div id=lg> <img hidefocus=true src=//www.baidu.com/img/bd_logo1.png width=270 height=129> </div> <form id=form name=f action=//www.baidu.com/s class=fm> <input type=hidden name=bdorz_come value=1> <input type=hidden name=ie value=utf-8> <input type=hidden name=f value=8> <input type=hidden name=rsv_bp value=1> <input type=hidden name=rsv_idx value=1> <input type=hidden name=tn value=baidu><span class="bg s_ipt_wr"><input id=kw name=wd class=s_ipt value maxlength=255 autocomplete=off autofocus></span><span class="bg s_btn_wr"><input type=submit id=su value=百度一下 class="bg s_btn"></span> </form> </div> </div> <div id=u1> <a href=http://news.baidu.com name=tj_trnews class=mnav>新闻</a> <a href=http://www.hao123.com name=tj_trhao123 class=mnav>hao123</a> <a href=http://map.baidu.com name=tj_trmap class=mnav>地图</a> <a href=http://v.baidu.com name=tj_trvideo class=mnav>视频</a> <a href=http://tieba.baidu.com name=tj_trtieba class=mnav>贴吧</a> <noscript> <a href=http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u=http%3A%2F%2Fwww.baidu.com%2f%3fbdorz_come%3d1 name=tj_login class=lb>登录</a> </noscript> <script>document.write('<a href="http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u='+ encodeURIComponent(window.location.href+ (window.location.search === "" ? "?" : "&")+ "bdorz_come=1")+ '" name="tj_login" class="lb">登录</a>');</script> <a href=//www.baidu.com/more/ name=tj_briicon class=bri style="display: block;">更多产品</a> </div> </div> </div> <div id=ftCon> <div id=ftConw> <p id=lh> <a href=http://home.baidu.com>关于百度</a> <a href=http://ir.baidu.com>About Baidu</a> </p> <p id=cp>©2017 Baidu <a href=http://www.baidu.com/duty/>使用百度前必读</a> <a href=http://jianyi.baidu.com/ class=cp-feedback>意见反馈</a> 京ICP证030173号 <img src=//www.baidu.com/img/gs.gif> </p> </div> </div> </div> </body> </html>
Pod Hook
我们知道 Pod 是 Kubernetes 集群中的最小单元,而 Pod 是由容器组成的,所以在讨论 Pod 的生命周期的时候我们可以先来讨论下容器的生命周期。实际上 Kubernetes 为我们的容器提供了生命周期的钩子,就是我们说的 Pod Hook,Pod Hook 是由 kubelet 发起的,当容器中的进程启动前或者容器中的进程终止之前运行,这是包含在容器的生命周期之中,我们可以同时为 Pod 中的所有容器都配置 hook。
Kubernetes 为我们提供了两种钩子函数:
- PostStart(不阻塞,初始化别用这个):Kubernetes 在容器创建后立即发送 postStart 事件,然而 postStart 处理函数的调用不保证早于容器的入口点(entrypoint) 的执行。postStart 处理函数与容器的代码是异步执行的,但 Kubernetes的容器管理逻辑会一直阻塞等待 postStart 处理函数执行完毕。只有 postStart 处理函数执行完毕,容器的状态才会变成 RUNNING。
- PreStop:这个钩子在容器终止之前立即被调用。它是阻塞的,所以它必须在删除容器的调用发出之前完成。主要用于优雅关闭应用程序、通知其他系统等。
我们应该让钩子函数尽可能的轻量,当然有些情况下,长时间运行命令是合理的,比如在停止容器之前预先保存状态。
postStart
以下示例中,定义了一个 Nginx Pod,其中设置了 PostStart 钩子函数,即在容器创建成功后,写入一句话到/usr/share/message 文件中:
# pod-poststart.yaml
apiVersion: v1
kind: Pod
metadata:
name: hook-demo1
spec:
containers:
- name: hook-demo1
image: nginx
lifecycle:
postStart:
exec:
command:
[
"/bin/sh",
"-c",
"echo hello ? im from post start -> /usr/share/message"
]
注意: PostStartHook 执行失败会导致pod重启
启动验证:
ubuntu@ubuntu:~/example$ kubectl apply -f ./pod-poststart.yaml
pod/hook-demo1 created
ubuntu@ubuntu:~/example$ kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
hook-demo1 1/1 Running 0 13s 10.244.2.35 node1 <none> <none>
init-demo 1/1 Running 0 15m 10.244.1.12 node2 <none> <none>
ubuntu@ubuntu:~/example$
# master 执行
ubuntu@ubuntu:~/example$ kubectl exec -it hook-demo1 -- cat /usr/share/message
hello ? im from post start -
# 或者直接进去看 kubectl exec -it hook-demo1 -- /bin/bash
# 和docker操作一样
PreStop
以下示例中,定义了一个 Nginx Pod,其中设置了 PreStop 钩子函数,即在容器退出之前,优雅的关闭 Nginx:
# pod-prestop.yaml
---
apiVersion: v1
kind: Pod
metadata:
name: hook-demo2
spec:
containers:
- name: hook-demo2
image: nginx
lifecycle:
preStop:
exec:
command: ["/usr/sbin/nginx", "-s", "quit"] # 优雅退出
---
apiVersion: v1
kind: Pod
metadata:
name: hook-demo3
spec:
volumes:
- name: message
hostPath:
path: /tmp
containers:
- name: hook-demo3
image: nginx
ports:
- containerPort: 80
volumeMounts:
- name: message
mountPath: /usr/share/
lifecycle:
preStop:
exec:
command:
[
"/bin/sh",
"-c",
"echo Hello from the preStop Handler > /usr/share/message"
]
启动验证:
ubuntu@ubuntu:~/example$ kubectl apply -f pod-prestop.yaml
pod/hook-demo2 created
pod/hook-demo3 created
ubuntu@ubuntu:~/example$ kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
hook-demo2 1/1 Running 0 46s 10.244.1.16 node2 <none> <none>
hook-demo3 1/1 Running 0 46s 10.244.2.39 node1 <none> <none>
# 删除 hook
kubectl delete pod hook-demo2 -n default
kubectl delete pod hook-demo3 -n default
# 去宿主机(hook-demo3)
ubuntu@ubuntu:~$ cat /tmp/message
Hello from the preStop Handler
另外 Hook 调用的日志没有暴露给 Pod,如果处理程序由于某种原因失败,它将产生一个事件。对于 PostStart,这是FailedPostStartHook 事件,对于 PreStop,是 FailedPreStopHook 事件,比如我们修改下面的 lifecycleevents.yaml 文件,将 postStart 命令更改为 badcommand 并应用它。
# lifecycle-events.yaml
apiVersion: v1
kind: Pod
metadata:
name: lifecycle-demo
spec:
containers:
- name: lifecycle-demo-container
image: nginx
lifecycle:
postStart:
exec:
command:
[
"/bin/sh",
" -c",
"echo Hello from the postStart handler > /usr/share/message"
]
preStop:
exec:
command:
[
"/bin/sh",
"-c",
"nginx -s quit; while killall -0 nginx; do sleep 1; done"
]
执行测试:
# 启动
ubuntu@ubuntu:~/example$ kubectl apply -f ./lifecycle-events.yaml
pod/lifecycle-demo created
# 查看事件
ubuntu@ubuntu:~/example$ kubectl describe pod lifecycle-demo
Name: lifecycle-demo
Namespace: default
Priority: 0
Service Account: default
Node: node2/192.168.236.103
Start Time: Thu, 04 Sep 2025 11:33:07 +0000
Labels: <none>
Annotations: <none>
Status: Running
IP: 10.244.1.18
IPs:
IP: 10.244.1.18
Containers:
lifecycle-demo-container:
Container ID: containerd://6319b2960ae16a7e48bd2e0fd44446bb16083c818278921354f00b84ac80453b
Image: nginx
Image ID: docker.io/library/nginx@sha256:33e0bbc7ca9ecf108140af6288c7c9d1ecc77548cbfd3952fd8466a75edefe57
Port: <none>
Host Port: <none>
State: Waiting
Reason: CrashLoopBackOff
Last State: Terminated
Reason: Completed
Exit Code: 0
Started: Thu, 04 Sep 2025 11:33:15 +0000
Finished: Thu, 04 Sep 2025 11:33:15 +0000
Ready: False
Restart Count: 1
Environment: <none>
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-2k9c2 (ro)
Conditions:
Type Status
PodReadyToStartContainers True
Initialized True
Ready False
ContainersReady False
PodScheduled True
Volumes:
kube-api-access-2k9c2:
Type: Projected (a volume that contains injected data from multiple sources)
TokenExpirationSeconds: 3607
ConfigMapName: kube-root-ca.crt
Optional: false
DownwardAPI: true
QoS Class: BestEffort
Node-Selectors: <none>
Tolerations: node.kubernetes.io/not-ready:NoExecute op=Exists for 300s
node.kubernetes.io/unreachable:NoExecute op=Exists for 300s
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 11s default-scheduler Successfully assigned default/lifecycle-demo to node2
Normal Pulled 8s kubelet Successfully pulled image "nginx" in 2.771s (2.771s including waiting). Image size: 72324501 bytes.
Normal Pulling 7s (x2 over 11s) kubelet Pulling image "nginx"
Normal Created 4s (x2 over 8s) kubelet Created container: lifecycle-demo-container
Normal Started 4s (x2 over 8s) kubelet Started container lifecycle-demo-container
Warning FailedPostStartHook 4s (x2 over 8s) kubelet PostStartHook failed
Normal Killing 4s (x2 over 8s) kubelet FailedPostStartHook
Warning FailedPreStopHook 4s (x2 over 8s) kubelet PreStopHook failed
Normal Pulled 4s kubelet Successfully pulled image "nginx" in 3.014s (3.014s including waiting). Image size: 72324501 bytes.
Warning BackOff 2s (x2 over 3s) kubelet Back-off restarting failed container lifecycle-demo-container in pod lifecycle-demo_default(2e54caaf-f9fd-481e-884d-477ea2b25d81)
Pod健康检查
现在在 Pod 的整个生命周期中,能影响到 Pod 的就只剩下健康检查这一部分了。在 Kubernetes 集群当中,我们可以通过配置 liveness probe(存活探针)、readiness probe(就绪探针) 以及 startupProbe(启动探针) 来影响容器的生命周期:
- liveness probe:kubelet 通过使用 liveness probe 来确定你的应用程序是否正在运行,通俗点将就是是否还活着。一般来说,如果你的程序一旦崩溃了, Kubernetes 就会立刻知道这个程序已经终止了,然后就会重启这个程序。而我们的liveness probe 的目的就是来捕获到当前应用程序还没有终止,还没有崩溃,如果出现了这些情况,那么就重启处于该状态下的容器,使应用程序在存在 bug 的情况下依然能够继续运行下去。
- readiness probe:kubelet 使用 readiness probe 来确定容器是否已经就绪可以接收流量过来了。这个探针通俗点讲就是说是否准备好了,现在可以开始工作了。只有当 Pod 中的容器都处于就绪状态的时候 kubelet 才会认定该 Pod 处于就绪状态,因为一个 Pod 下面可能会有多个容器。如果就绪态探测失败,端点控制器将从与 Pod 匹配的所有服务的端点列表中删除该 Pod 的 IP 地址,这样我们的流量就不会被路由到这个 Pod 里面来了。
- startup probe:kubelet 使用 startup probe 来指示容器中的应用是否已经启动。如果提供了启动探针,则所有其他探针都会被
禁用,直到该探针成功为止。如果启动探测失败,kubelet 将杀死容器,而容器依其重启策略进行重启。如果容器没有
提供启动探测,则默认状态为 Success。
何时使用存活探针(服务假死需要)
如果容器中的进程能够在遇到问题或不健康的情况下自行崩溃,则不一定需要存活态探针;kubelet 将根据 Pod 的restartPolicy 自动执行修复操作。如果你希望容器在探测失败时被杀死并重新启动,那么请指定一个存活态探针, 并指定 restartPolicy 为 Always 或 OnFailure。
何时使用就绪探针(服务就绪前加载大量配置,文件)
如果要仅在探测成功时才开始向 Pod 发送请求流量,那么就需要指定就绪态探针。在这种情况下,就绪态探针可能与存活态探针相同,就绪态探针的存在意味着 Pod 将在启动阶段不接收任何数据,并且只有在探针探测成功后才开始接收数据。如果你希望容器能够自行进入维护状态,也可以指定一个就绪态探针,检查某个特定于就绪态的不同于存活态探测的端点。如果你的应用程序对后端服务有严格的依赖性,你可以同时实现存活态和就绪态探针。当应用程序本身是健康的,存活态探针检测通过后,就绪态探针会额外检查每个所需的后端服务是否可用,这可以帮助你避免将流量导向返回错误信息的 Pod。如果你的容器需要在启动期间加载大型数据、配置文件等操作,那么这个时候我们可以使用启动探针。
请注意,如果你只是想在 Pod 被删除时能够排空请求,则不一定需要使用就绪态探针;在删除 Pod 时,Pod 会自动将自身置于未就绪状态,无论就绪态探针是否存在。 等待 Pod 中的容器停止期间,Pod 会一直处于未就绪状态。
何时使用启动探针( 容器 启动特别慢)
该探针在 Kubernetes v1.20 版本才变成稳定状态,对于所包含的容器需要较长时间才能启动就绪的 Pod 而言,启动探针是有用的。你不再需要配置一个较长的存活态探测时间间隔,只需要设置另一个独立的配置选项,对启动期间的容器执行探测,从而允许使用远远超出存活态时间间隔所允许的时长。如果你的容器启动时间通常超出 initialDelaySeconds + failureThreshold × periodSeconds 总值(默认30秒),你应该设置一个启动探针,对存活态探针所使用的同一端点执行检查。 periodSeconds 的默认值是 10 秒,还应该将其failureThreshold 设置得足够高,以便容器有充足的时间完成启动,并且避免更改存活态探针所使用的默认值。 这一设置有助于减少死锁状况的发生。
容器探测
probe 是由 kubelet 对容器执行的定期诊断,要执行诊断,kubelet 既可以在容器内执行代码,也可以发出一个网络请求。使用探针来检查容器有四种不同的方法。每个探针都必须准确定义为这四种机制中的一种:
- exec:在容器内执行指定命令,如果命令退出时返回码为 0 则认为诊断成功。
- grpc:使用 gRPC 执行一个远程过程调用,目标应该实现 gRPC 健康检查。如果响应的状态是 SERVING,则认为诊断成功,不过需要注意 gRPC 探针是一个 Alpha 特性,只有在启用了 GRPCContainerProbe 特性门时才能使用。
- httpGet:对容器的 IP 地址上指定端口和路径执行 HTTP GET 请求,如果响应的状态码大于等于 200 且小于400,则诊断被认为是成功的。
- tcpSocket:使用此配置,kubelet 将尝试在指定端口上打开容器的套接字。如果可以建立连接,容器被认为是健康的,如果不能就认为是失败的。实际上就是检查端口。
每次探测都将获得以下三种结果之一:
- Success(成功):容器通过了诊断。
- Failure(失败):容器未通过诊断。
- Unknown(未知):诊断失败,因此不会采取任何行动。
我们先来给大家演示下存活探针的使用方法,首先我们用 exec 执行命令的方式来检测容器的存活,如下:
# liveness-exec.yaml
apiVersion: v1
kind: Pod
metadata:
name: liveness-exec
spec:
containers:
- name: liveness
image: busybox
args:
- /bin/sh
- -c
- touch /tmp/healthy; sleep 30; rm -rf /tmp/healthy; sleep 600
livenessProbe:
exec:
command:
- cat
- /tmp/healthy
# 容器启动后等待 5 秒才开始探测
initialDelaySeconds: 5
# 探针 每隔 5 秒探测一次
periodSeconds: 5
说明:
容器启动后执行:touch /tmp/healthy
→ 等 30 秒 → 删除 /tmp/healthy
→ 再 sleep 600。
探针 cat /tmp/healthy
:
-
前 30 秒文件存在,探针成功。
-
30 秒后文件被删,探针失败,Pod 会被 Kubelet 重启。
验证:
ubuntu@ubuntu:~/example$ kubectl apply -f ./liveness-exec.yaml
pod/liveness-exec created
# 查看
kubectl describe pod liveness-exec
# 观察事件 同时 restartCount 会记录重启次数
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 56s default-scheduler Successfully assigned default/liveness-exec to node1
Normal Pulling 55s kubelet Pulling image "busybox"
Normal Pulled 46s kubelet Successfully pulled image "busybox" in 9.163s (9.163s including waiting). Image size: 2223685 bytes.
Normal Created 46s kubelet Created container: liveness
Normal Started 46s kubelet Started container liveness
Warning Unhealthy 5s (x3 over 15s) kubelet Liveness probe failed: cat: can't open '/tmp/healthy': No such file or directory
Normal Killing 5s kubelet Container liveness failed liveness probe, will be restarted
同样的,我们还可以使用 HTTP GET 请求来配置我们的存活探针,我们这里使用一个 liveness 镜像来验证演示下:
# liveness-http.yaml
apiVersion: v1
kind: Pod
metadata:
name: liveness-http
spec:
containers:
- name: liveness
image: cnych/liveness
args:
- /server
livenessProbe:
httpGet: # 使用 HTTP GET 请求检查健康
path: /healthz # 访问容器内部的 /healthz 接口
port: 8080 # HTTP 服务端口
httpHeaders: # 自定义 HTTP 头
- name: X-Custom-Header
value: Awesome
initialDelaySeconds: 3 # 容器启动后 3 秒才开始探测
periodSeconds: 3 # 每 3 秒探测一次
cnych/liveness
镜像启动后会运行一个 HTTP 服务器,监听在 /healthz
路径上,返回 HTTP 200 状态码,常用于测试 Kubernetes 的存活探针(liveness probe)配置。(前10秒成功,后面失败)
healthz 的实现:
http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
duration := time.Now().Sub(started)
if duration.Seconds() > 10 {
w.WriteHeader(500)
w.Write([]byte(fmt.Sprintf("error: %v", duration.Seconds())))
} else {
w.WriteHeader(200)
w.Write([]byte("ok"))
}
})
说明:
HTTP 类型存活探针
- 容器健康检查通过 HTTP 请求
/healthz
接口返回 200 判断。 - 可以加自定义请求头,如上例中的
X-Custom-Header: Awesome
。
探针逻辑
initialDelaySeconds=3
:容器启动 3 秒后才开始探测,避免应用启动慢被误判。periodSeconds=3
:每隔 3 秒探测一次,如果连续失败(failureThreshold,默认 3)就重启容器。
适用场景
- 适合有 HTTP 服务接口 的应用。
- 可以快速发现服务挂掉或者接口异常,并自动重启容器。
验证:
ubuntu@ubuntu:~/example$ kubectl apply -f ./liveness-http.yaml
pod/liveness-http created
ubuntu@ubuntu:~/example$ kubectl describe pod liveness-http
# 观察事件
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 107s default-scheduler Successfully assigned default/liveness-http to node1
Normal Pulled 96s kubelet Successfully pulled image "cnych/liveness" in 10.195s (10.195s including waiting). Image size: 1281426 bytes.
Normal Pulled 77s kubelet Successfully pulled image "cnych/liveness" in 2.48s (2.48s including waiting). Image size: 1281426 bytes.
Normal Pulled 55s kubelet Successfully pulled image "cnych/liveness" in 2.835s (2.835s including waiting). Image size: 1281426 bytes.
Normal Pulled 35s kubelet Successfully pulled image "cnych/liveness" in 2.413s (2.413s including waiting). Image size: 1281426 bytes.
Normal Pulling 16s (x5 over 106s) kubelet Pulling image "cnych/liveness"
Normal Killing 16s (x4 over 79s) kubelet Container liveness failed liveness probe, will be restarted
Normal Created 14s (x5 over 96s) kubelet Created container: liveness
Normal Pulled 14s kubelet Successfully pulled image "cnych/liveness" in 2.797s (2.797s including waiting). Image size: 1281426 bytes.
Normal Started 13s (x5 over 96s) kubelet Started container liveness
Warning Unhealthy 1s (x13 over 85s) kubelet Liveness probe failed: HTTP probe failed with statuscode: 500
有时候,会有一些现有的应用在启动时需要较长的初始化时间,前面我们提到了探针里面有一个 initialDelaySeconds的属性,可以来配置第一次执行探针的等待时间,对于启动非常慢的应用这个参数非常有用,比如 Jenkins、Gitlab 这类应用,但是如何设置一个合适的初始延迟时间呢?这个就和应用具体的环境有关系了,所以这个值往往不是通用的,这样的话可能就会导致一个问题,我们的资源清单在别的环境下可能就会健康检查失败了。这个时候我们就可以使用startupProbe(启动探针),该探针将推迟所有其他探针,直到 Pod 完成启动为止,使用方法和存活探针一样。技巧就是使用相同的命令来设置启动探测,针对 HTTP 或 TCP 检测,可以通过将 failureThreshold * periodSeconds参数设置为足够长的时间来应对糟糕情况下的启动时间。
ports:
- name: liveness-port
containerPort: 8080
hostPort: 8080
livenessProbe: # 存活探针(判断容器是否“死掉”)
httpGet:
path: /healthz
port: liveness-port
failureThreshold: 1 # 探针失败 1 次就认为容器不健康,立即重启
periodSeconds: 10 # 每 10 秒探测一次
startupProbe: # 启动探针(防止应用启动慢被误杀)
httpGet:
path: /healthz
port: liveness-port
failureThreshold: 30 # 连续失败 30 次才判定启动失败(启动慢可设置大)
periodSeconds: 10 # 每 10 秒探测一次
比如上面这里的配置表示我们的慢速容器最多可以有 5 分钟(30 个检查 * 10 秒= 300s)来完成启动。
有的时候,应用程序可能暂时无法对外提供服务,例如,应用程序可能需要在启动期间加载大量数据或配置文件。在这种情况下,你不想杀死应用程序,也不想对外提供服务。那么这个时候我们就可以使用 readiness probe 来检测和减轻这些情况,Pod 中的容器可以报告自己还没有准备,不能处理 Kubernetes 服务发送过来的流量。存活探针和就绪探针如果同时使用的话就可以确保流量不会到达还未准备好的容器,准备好过后,如果应用程序出现了错误,则会重新启动容器。
就绪探针的配置和存活探针的配置相似,唯一区别就是要使用 readinessProbe 字段,而不是 livenessProbe 字段。
readinessProbe: # 就绪探针,用于判断容器是否可以对外提供服务
exec:
command:
- cat
- /tmp/healthy
# 如果能 cat 成功,Pod 就认为“就绪”
# 如果文件不存在,Pod 不就绪,Service 不会将流量路由到该 Pod
initialDelaySeconds: 5 # 容器启动 5 秒后开始探测
periodSeconds: 5 # 每 5 秒探测一次
另外除了上面的 initialDelaySeconds 和 periodSeconds 属性外,探针还可以配置如下几个参数:
- timeoutSeconds:探测超时时间,默认 1 秒,最小 1 秒。
- successThreshold:探测失败后,最少连续探测成功多少次才被认定为成功,默认是 1,但是如果是 liveness则必须是 1。最小值是 1。
- failureThreshold:探测成功后,最少连续探测失败多少次才被认定为失败,默认是 3,最小值是 1。
Pod 的终止
Terminating(移除中,可能仍在运行) -> preStopHook(钩子函数,可选) ->Sigterm(发送信号) -> TerminationGracePeriodSeconds(优雅退出时间窗口) -> sigKill(强制终止)
-
用户发出删除 Pod 指令,Pod 被删除,状态变为 Terminating,从 API 层面看就是 Pod metadata 中的deletionTimestamp 字段会被标记上删除时间。
-
kube-proxy watch 到了就开始更新转发规则,将 Pod 从 service 的 endpoints 列表中摘除掉,新的流量不再转发到该 Pod。
-
kubelet watch 到了就开始销毁 Pod。
3.1 如果 Pod 中有 container 配置了 preStop Hook ,则 Pod 被标记为 Terminating 状态时以同步的方式启动执行;若宽限期结束后 preStop 仍未执行结束,则会额外获得一个 2 秒的小宽限期。
3.2 发送 SIGTERM 信号给容器内主进程以通知容器进程开始优雅停止。
3.3 等待 container 中的主进程完全停止,如果在宽限期结束后还未完全停止,就发送 SIGKILL 信号将其强制杀死。
3.4 所有容器进程终止,清理 Pod 资源。
3.5 通知 APIServer Pod 销毁完成,完成 Pod 删除。
对于长连接类型的业务,比如游戏类应用,我们可以将 terminationGracePeriodSeconds 设置大一点,避免过早的被 SIGKILL 杀死,但是具体多长时间是不好预估的,所以最好在业务层面进行优化,比如 Pod 销毁时的优雅终止逻辑里面主动通知下客户端,让客户端连到新的后端,然后客户端来保证这两个连接的平滑切换。等旧 Pod 上所有客户端连接都连切换到了新 Pod 上,才最终退出。
强制中止Pod
默认情况下,所有的删除操作都会有 30 秒钟的宽限期限。kubectl delete 命令支持 -- grace-period=
将宽限期限强制设置为 0 意味着立即从 APIServer 删除 Pod,如果 Pod 仍然运行于某节点上,强制删除操作会触发kubelet 立即执行清理操作。
你必须在设置 -- grace-period=0 的同时额外设置 -- force 参数才能发起强制删除请求。
执行强制删除操作时,APIServer 不再等待来自 kubelet 的、关于 Pod 已经在原来运行的节点上终止执行的确认消息。APIServer 直接删除 Pod 对象,这样新的与之同名的 Pod 即可以被创建。在节点侧,被设置为立即终止的 Pod 仍然会在被强行杀死之前获得一点点的宽限时间。
对于已失败的 Pod 而言,对应的 API 对象仍然会保留在集群的 API 服务器上,直到用户或者控制器进程显式地将其删除。控制面组件会在 Pod 个数超出所配置的阈值 (根据 kube-controller-manager 的 terminated-pod-gcthreshold设置)时删除已终止的 Pod(phase 值为 Succeeded 或 Failed)。这一行为会避免随着时间不断创建和终止 Pod 而引起的资源泄露问题。
业务代码处理 SIGTERM 信号
要实现优雅退出,我们需要业务代码得支持下优雅退出的逻辑,在业务代码里面处理下 SIGTERM 信号,一般主要逻辑就是"排水",即等待存量的任务或连接完全结束,再退出进程。下面我们给出几种常用编程语言实现优雅退出的示例。
public class ExampleSignalHandler {
public static void main(String"". args) throws InterruptedException {
final long start = System.nanoTime();
Signal.handle(new Signal("TERM"), new SignalHandler() {
public void handle(Signal sig) {
System.out.format("\nProgram execution took %f seconds\n",
(System.nanoTime() - start) / 1e9f);
System.exit(0);
}
});
int counter = 0;
while(true) {
System.out.println(counter"2);
Thread.sleep(500);
}
}
}
收不到 SIGTERM 信号
上面我们给出了几种常见的捕捉 SIGTERM 信号的代码,然后我们就可以执行停止逻辑以实现优雅退出了。在 Kubernetes环境中,业务发版时经常会对工作负载进行滚动更新,当旧版本 Pod 被删除时,K8s 会对 Pod 中各个容器中的主进程发送 SIGTERM 信号,当达到退出宽限期后进程还未完全停止的话,就会发送 SIGKILL 信号将其强制杀死。但是有的场景下在 Kubernetes 环境中实际运行时,有时候可能会发现在滚动更新时,我们业务的优雅终止逻辑并没有被执行,现象是在等了较长时间后,业务进程直接被 SIGKILL 强制杀死了。
这是什么原因造成的呢?通常情况下这都是因为容器启动入口使用了 shell,比如使用了类似 /bin/sh -c my-app 这样的启动入口。或者使用 /entrypoint.sh 这样的脚本文件作为入口,在脚本中再启动业务进程,比如下面的entrypoint.sh 文件:
#! /bin/bash
/webserver
这就可能就会导致容器内的业务进程收不到 SIGTERM 信号,原因是:
- 容器主进程是 shell,业务进程是在 shell 中启动的,变成了 shell 进程的子进程了。
- shell 进程默认是不会处理 SIGTERM 信号的,自己不会退出,也不会将信号传递给子进程,所以就导致了业务进程不会触发停止逻辑。
- 当等到 K8s 优雅停止宽限时间 ( terminationGracePeriodSeconds,默认 30s),就只能发送 SIGKILL 强制杀死 shell 及其子进程了。
那么我们应该如何让我们的业务进程收到 SIGTERM 信号呢?当然如果可以的话,尽量不使用 shell 启动业务进程,这样当然最好了,此外我们还有其他解决方案。
-
使用 exec 启动
在 shell 中启动二进制的命令前加一个 exec 命令即可让该二进制启动的进程代替当前 shell 进程,即让新启动的进程成为主进程:
#! /bin/bash ... exec /webserver # 脚本中执行二进制
-
多进程场景
通常我们一个容器只会有一个进程,但有些时候我们不得不启动多个进程,比如从传统部署迁移到 Kubernetes 的过渡期间,使用了富容器,即单个容器中需要启动多个业务进程,这时候我们可以通过 shell 来启动,但却无法使用上面的exec 方式来传递信号了,因为 exec 只能让一个进程替代当前 shell 成为主进程。
这个时候我们可以在 shell 中使用 trap 来捕获信号,当收到信号后触发回调函数来将信号通过 kill 命令传递给业
务进程,脚本示例:#! /bin/bash /bin/app1 & pid1="$!" # 启动第一个业务进程并记录 pid echo "app1 started with pid $pid1" /bin/app2 & pid2="$!" # 启动第二个业务进程并记录 pid echo "app2 started with pid $pid2" handle_sigterm() { echo "[INFO] Received SIGTERM" kill -SIGTERM $pid1 $pid2 # 传递 SIGTERM 给业务进程 wait $pid1 $pid2 # 等待所有业务进程完全终止 } trap handle_sigterm SIGTERM # 捕获 SIGTERM 信号并回调 handle_sigterm 函数 wait # 等待回调执行完,主进程再退出
-
使用 init 系统(强烈推荐)
前面一种方案实际是用脚本实现了一个极简的 init 系统 (或 supervisor ) 来管理所有子进程,只不过它的逻辑很简陋,仅仅简单的透传指定信号给子进程,其实社区有更完善的方案,dumb-init 和 tini 都可以作为 init 进程,作为主进程 (PID 1) 在容器中启动,然后它再运行 shell 来执行我们指定的脚本 (shell 作为子进程),shell 中启动的业务进程也成为它的子进程,当它收到信号时会将其传递给所有的子进程,从而也能完美解决 shell 无法传递信号问题,并且还有回收僵尸进程的能力,这也是我们强烈推荐的一种方式。
如下所示是一个以 dumb-init 制作镜像的 Dockerfile 示例:
FROM ubuntu:22.04 RUN apt-get update "5 apt-get install -y dumb-init ADD start.sh / ADD app1 /bin/app1 ADD app2 /bin/app2 ENTRYPOINT ["dumb-init", ""*"] CMD ["/start.sh"]
下面则是以 tini 为例制作镜像的 Dockerfile 示例:
FROM ubuntu:22.04 ENV TINI_VERSION v0.19.0 ADD https:"#github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini COPY entrypoint.sh /entrypoint.sh RUN chmod +x /tini /entrypoint.sh ENTRYPOINT ["/tini", ""*"] CMD [ "/start.sh" ]
kind类型(附录):
序号 | 类型 | 简述 |
---|---|---|
1 | Pod | 一个Kubernetes中最基本的资源类型,它用于定义一个或多个容器的共同运行环境。 |
2 | Deployment | 用于定义应用程序的声明式更新。 |
3 | Service | 用于定义一组pod的逻辑集合,以及访问这些pod的方式。 |
4 | DaemonSet | 用于在集群中运行一个pod的声明式更新和管理。 |
5 | ReplicaSet | 用于确保在集群中运行指定数量的pod的声明式更新和管理。 |
6 | ServiceAccount | 定义一个ServiceAccount对象,用于给Pod分配身份与访问权限。 |
7 | PodDisruptionBudget | 用于定义维护期间可以安全中断的pod的最小数量,以确保Kubernetes集群的高可用性。 |
8 | PersistentVolumeClaim | PersistentVolumeClaim(PVC)是Kubernetes中用于声明持久化存储资源的对象。 |
9 | PersistentVolume | 用于定义持久化存储卷,并使它们在Kubernetes集群中可用。 |
10 | Job | 定义一个Job对象,用于定义一个运行一次性任务的作业。 |
11 | CronJob | 定义一个CronJob对象,用于定义一个周期性运行任务的作业。 |
12 | StatefulSet | 用于有状态应用程序的声明式更新和管理。 |
13 | ConfigMap | 用于存储非敏感数据(如配置文件)的声明式更新和管理。 |
14 | Secret | 用于存储敏感数据(如密码和密钥)的声明式更新和管理。 |
15 | Ingress | 定义一个Ingress对象,用于配置集群中的HTTP和HTTPS路由规则。 |
16 | StorageClass | 用于定义不同类型的存储,例如云存储、本地存储等,并为这些存储类型指定默认的参数和策略。 |
17 | Namespace | 用于在Kubernetes集群中创建逻辑分区,从而将资源隔离开来,以提高安全性和可维护性。 |
18 | ServiceMonitor | 用于自动发现和监控在Kubernetes集群中运行的服务。 |
19 | HorizontalPodAutoscaler | 定义一个HorizontalPodAutoscaler对象,用于自动调整Pod副本数量以适应负载。 |
20 | NetworkPolicy | 定义一个NetworkPolicy对象,用于在Pod之间定义网络流量规则。 |
21 | CustomResourceDefinition | 用于定义自定义资源,以扩展Kubernetes API和自定义资源类型。 |
22 | Role | 用于定义对Kubernetes资源的操作权限,例如读、写、更新、删除等。 |
23 | ClusterRole | 与Role类似,但是可以在整个Kubernetes集群中使用。 |
24 | ClusterRoleBinding | 定义一个集群角色绑定对象,将集群角色与用户或ServiceAccount关联。 |
25 | RoleBinding | 定义一个角色绑定对象,将角色与用户或ServiceAccount关联。 |
26 | Endpoint | 定义一个Endpoint对象,用于指定Service的后端IP地址和端口。 |
27 | Volume | 定义一个Volume对象,用于将存储挂载到Pod中。 |
28 | PodSecurityPolicy | 定义一个PodSecurityPolicy对象,用于定义Pod的安全策略。 |
29 | Event | 定义一个Event对象,用于记录集群中发生的事件。 |
30 | ResourceQuota | 定义一个ResourceQuota对象,用于限制命名空间中资源的使用量。 |
31 | PriorityClass | 定义一个PriorityClass对象,用于设置Pod的优先级。 |
32 | VolumeSnapshot | 定义一个VolumeSnapshot对象,用于创建和管理存储卷的快照。 |