跳到主要内容

2、Pod生命周期

更新于:2024年3月10日

Pod 生命周期

img

前面我们已经了解了 Pod 的设计原理,接下来我们来了解下 Pod 的生命周期。下图展示了一个 Pod 的完整生命周期过程,其中包含 Init ContainerPod Hook健康检查 三个主要部分,接下来我们就来分别介绍影响 Pod 生命周期的部分:

首先在介绍 Pod 的生命周期之前,我们先了解下 Pod 的状态,因为 Pod 状态可以反应出当前我们的 Pod 的具体状态信息,也是我们分析排错的一个必备的方式。

目录

[toc]

推荐文章

我的开源项目:

https://onedayxyy.cn/docs/MyOpenSourceProject

image-20240305125631154

1、Pod 状态

Kubernetes pod遵循已定义的生命周期。这些是不同的阶段:

  • pod首次创建后,从**pending(挂起)**阶段开始。调度器试图搞清楚把pod放置在哪里。如果调度器找不到放置pod的节点,它将保持挂起状态。(要检查pod为何处于挂起状态,运行kubectl describe pod <pod name>命令)。
  • 一旦pod被调度,它进入容器**creating(创建)**阶段,在此阶段拉取应用程序所需的镜像,随后容器启动。
  • 一旦容器在pod中,它进入**running(运行)**阶段,在此阶段它继续运行,直至程序成功完成或终止。

要检查pod的状态,请运行kubectl get pod命令,检查STATUS列。如你所见,在这种情况下,所有pod都处于运行状态。此外,READY列表明pod已准备好接受用户流量

# kubectl get pod
NAME READY STATUS RESTARTS AGE
my-nginx-6b74b79f57-fldq6 1/1 Running 0 20s
my-nginx-6b74b79f57-n67wp 1/1 Running 0 20s
my-nginx-6b74b79f57-r6pcq 1/1 Running 0 20s

首先先了解下 Pod 的状态值,我们可以通过 kubectl explain pod.status 命令来了解关于 Pod 状态的一些信息,Pod 的状态定义在 PodStatus 对象中,其中有一个 phase 字段,下面是 phase 的可能取值:

[root@master1 ~]#kubectl explain pod.status
phase <string>
The phase of a Pod is a simple, high-level summary of where the Pod is in
its lifecycle. The conditions array, the reason and message fields, and the
individual container status arrays contain more detail about the pod's
status. There are five possible phase values:

Pending: The pod has been accepted by the Kubernetes system, but one or
more of the container images has not been created. This includes time
before being scheduled as well as time spent downloading images over the
network, which could take a while.

Running: The pod has been bound to a
node, and all of the containers have been created. At least one container
is still running, or is in the process of starting or restarting.

Succeeded: All containers in the pod have terminated in success, and will
not be restarted.

Failed: All containers in the pod have terminated, and at
least one container has terminated in failure. The container either exited
with non-zero status or was terminated by the system.

Unknown: For some
reason the state of the pod could not be obtained, typically due to an
error in communicating with the host of the pod.

Pod 状态(5种状态)

  • 挂起(Pending):Pod 信息已经提交给了集群,但是还没有被调度器调度到合适的节点或者 Pod 里的镜像正在下载

  • 运行中(Running):该 Pod 已经绑定到了一个节点上,Pod 中所有的容器都已被创建。至少有一个容器正在运行,或者正处于启动或重启状态**。**

  • 成功(Succeeded):Pod 中的所有容器都被成功终止,并且不会再重启

  • 失败(Failed):Pod 中的所有容器都已终止了,并且至少有一个容器是因为失败终止。也就是说,容器以非0状态退出或者被系统终止。

  • 未知(Unknown):因为某些原因无法取得 Pod 的状态,通常是因为与 Pod 所在主机通信失败导致的

注意:这些状态都是集群帮我们维护的。

除此之外,PodStatus 对象中还包含一个 PodCondition 的数组,里面包含的属性有:

[root@master1 ~]#kubectl explain pod.status.conditions
FIELDS:
lastProbeTime <string>
Last time we probed the condition.

lastTransitionTime <string>
Last time the condition transitioned from one status to another.

message <string>
Human-readable message indicating details about last transition.

reason <string>
Unique, one-word, CamelCase reason for the condition's last transition.

status <string> -required-
Status is the status of the condition. Can be True, False, Unknown. More
info:
https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#pod-conditions

type <string> -required-
Type is the type of the condition. More info:
https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#pod-conditions

[root@master1 ~]#
  • lastProbeTime:最后一次探测 Pod Condition 的时间戳。

  • lastTransitionTime:上次 Condition 从一种状态转换到另一种状态的时间。

  • message:上次 Condition 状态转换的详细描述。

  • reason:Condition 最后一次转换的原因。

  • status:Condition 状态类型,可以为 “True”, “False”, and “Unknown”.

  • type:Condition 类型,包括以下方面:

    • PodScheduled(Pod 已经被调度到其他 node 里)
    • Ready(Pod 能够提供服务请求,可以被添加到所有可匹配服务的负载平衡池中)
    • Initialized(所有的init containers已经启动成功)
    • Unschedulable(调度程序现在无法调度 Pod,例如由于缺乏资源或其他限制)
    • ContainersReady(Pod 里的所有容器都是 ready 状态)

注意:这个pod.status.conditions在排错上还是很有用的

案例:
[root@master1 ~]#kubectl get po coredns-7b884d5cb7-6jzn6 -nkube-system -oyaml
……
status:
conditions:
- lastProbeTime: null
lastTransitionTime: "2022-12-04T22:53:44Z"
status: "True"
type: Initialized
- lastProbeTime: null
lastTransitionTime: "2022-12-04T22:53:45Z"
status: "True"
type: Ready
- lastProbeTime: null
lastTransitionTime: "2022-12-04T22:53:45Z"
status: "True"
type: ContainersReady
- lastProbeTime: null
lastTransitionTime: "2022-12-04T22:53:44Z"
status: "True"
type: PodScheduled
containerStatuses:
- containerID: containerd://5bb2371166b77499496cb9d3e3c0387179c248497cc8df637aefc92f769dd62b
image: docker.io/coredns/coredns:1.9.3
imageID: sha256:5185b96f0becf59032b8e3646e99f84d9655dff3ac9e2605e0dc77f9c441ae4a
lastState: {}
name: coredns
ready: true
restartCount: 0
started: true
state:
running:
startedAt: "2022-12-04T22:53:45Z"
hostIP: 172.29.9.61
phase: Running
podIP: 10.244.0.3
podIPs:
- ip: 10.244.0.3
qosClass: Burstable
startTime: "2022-12-04T22:53:44Z"

2、重启策略

Pod对象:重启策略+健康检查(应用自修复)。

我们可以通过配置 restartPolicy 字段来设置 Pod 中所有容器的重启策略,其可能值为 AlwaysOnFailureNever,默认值为 Always

重启策略(restartPolicy):

• Always:当容器终止退出后,总是重启容器,默认策略。 #所有的应用服务;守护进程:nginx、tomcat;

• OnFailure:当容器异常退出(退出状态码非0)时,才重启容器。 #定时任务:备份数据库

• Never:当容器终止退出,从不重启容器。 # 临时任务:处理数据库数据

restartPolicy 指通过 kubelet 在同一节点上重新启动容器通过 kubelet 重新启动的退出容器将以指数增加延迟(10s,20s,40s…)重新启动,上限为 5 分钟,并在成功执行 10 分钟后重置。(这个是针对于**always**策略)

那么问题来了:

OnFailure策略是容器异常退出后pod只重启一次吗?如果重启多次的话那不是和always一样的效果了?

不同类型的的控制器可以控制 Pod 的重启策略:

  • Job:适用于一次性任务如批量计算,任务结束后 Pod 会被此类控制器清除。Job 的重启策略只能是"OnFailure"或者"Never"

  • ReplicaSetDeployment:此类控制器希望 Pod 一直运行下去,它们的重启策略只能是"Always"

  • DaemonSet:每个节点上启动一个 Pod,很明显此类控制器的重启策略也应该是"Always"

注意:在k8s中没有重启的概念,删除pod后会被重建。重启策略==重建。

3、初始化容器(InitCongtainer)

Init Container:顾名思义,用于初始化工作,执行完就结束,可以理解为一次性任务。

  • 支持大部分应用容器配置,但不支持健康检查

  • 优先应用容器执行

了解了 Pod 状态后,首先来了解下 Pod 中最先启动的 Init Container,也就是我们平时常说的初始化容器Init Container就是用来做初始化工作的容器,可以是一个或者多个,如果有多个的话,这些容器会按定义的顺序依次执行。我们知道一个 Pod 里面的所有容器是共享数据卷和 Network Namespace的,所以 Init Container 里面产生的数据可以被主容器使用到。从上面的 Pod 生命周期的图中可以看出初始化容器是独立与主容器之外的,只有所有的`初始化容器执行完之后,主容器才会被启动

那么初始化容器有哪些应用场景呢:

  • 环境检查(等待其他模块 Ready) 例如确保应用容器依赖的服务启动后再启动应用容器。这个可以用来解决服务之间的依赖问题,比如我们有一个 Web 服务,该服务又依赖于另外一个数据库服务,但是在我们启动这个 Web 服务的时候我们并不能保证依赖的这个数据库服务就已经启动起来了,所以可能会出现一段时间内 Web 服务连接数据库异常。要解决这个问题的话我们就可以在 Web 服务的 Pod 中使用一个 InitContainer,在这个初始化容器中去检查数据库是否已经准备好了,准备好了过后初始化容器就结束退出,然后我们主容器的 Web 服务才被启动起来,这个时候去连接数据库就不会有问题了。

  • 初始化配置 例如给应用容器准备配置文件。比如集群里检测所有已经存在的成员节点,为主容器准备好集群的配置信息,这样主容器起来后就能用这个配置信息加入集群。

  • 其它场景:如将 Pod 注册到一个中央数据库、配置中心等。

日志采集也不能放到init container里去,可以放到一个pod里去。


==💘 实战:初始化容器测试-2022.12.9(成功测试)==

  • 实验环境
1、win10,vmwrokstation虚机;
2、k8s集群:3台centos7.6 1810虚机,2个master节点,1个node节点
k8s version:v1.20
CONTAINER-RUNTIME:docker://20.10.7
  • 实验软件(无)

  • 创建pod资源清单

比如现在我们来实现一个功能,在 Nginx Pod 启动之前去重新初始化首页内容,如下所示的资源清单:

#init-demo.yaml
apiVersion: v1
kind: Pod
metadata:
name: init-demo
spec:
volumes:
- name: workdir
emptyDir: {} #注意:emptyDir{},这个是一个临时的目录,数据会保存在 kubelet 的工作目录下面,生命周期等同于 Pod 的生命周期。
initContainers:
- name: install
image: busybox
command:
- wget
- "-O"
- "/work-dir/index.html"
- http://www.baidu.com # https
volumeMounts:
- name: workdir
mountPath: "/work-dir"
containers:
- name: web
image: nginx
ports:
- containerPort: 80
volumeMounts:
- name: workdir
mountPath: /usr/share/nginx/html # nginx容器默认的首页index.html就位于/usr/share/nginx/html

上面的资源清单中我们首先在 Pod 顶层声明了一个名为 workdir 的 Volume,前面我们用了 hostPath 的模式,这里我们使用的是 emptyDir{}这个是一个临时的目录,数据会保存在 kubelet 的工作目录下面,生命周期等同于 Pod 的生命周期。

然后我们定义了一个初始化容器,该容器会下载一个 html 文件到 /work-dir 目录下面,但是由于我们又将该目录声明挂载到了全局的 Volume,同样的主容器 nginx 也将目录 /usr/share/nginx/html 声明挂载到了全局的 Volume,所以在主容器的该目录下面会同步初始化容器中创建的 index.html 文件。

这里需要注意一点的是:

init和main 2个容器挂载到同一个volume,main container原来的文件会默认被init container覆盖掉的。

  • 部署并测试

直接创建上面的 Pod:

[root@master1 ~]#kubectl apply -f init-demo.yaml
pod/init-demo created

创建完成后可以查看该 Pod 的状态:

[root@master1 ~]#kubectl get po -owide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
init-demo 0/1 Init:0/1 0 11s <none> node1 <none> <none>

可以发现 Pod 现在的状态处于 Init:0/1 状态,意思就是现在第一个初始化容器还在执行过程中,此时我们可以查看 Pod 的详细信息:

[root@master1 ~]#kubectl describe pod init-demo
Name: init-demo
Namespace: default
Priority: 0
Node: node1/172.29.9.52
Start Time: Sat, 06 Nov 2021 17:36:21 +0800
Labels: <none>
Annotations: <none>
Status: Running
IP: 10.244.1.6
IPs:
IP: 10.244.1.6
Init Containers:
install:
Container ID: containerd://7b8ae516ae003458e439f9efe9d31c9e33b44593e827b2518007a7dbf103f69e
Image: busybox
Image ID: docker.io/library/busybox@sha256:15e927f78df2cc772b70713543d6b651e3cd8370abf86b2ea4644a9fba21107f
Port: <none>
Host Port: <none>
Command:
wget
-O
/work-dir/index.html
http://www.baidu.com
State: Terminated
Reason: Completed
Exit Code: 0
Started: Sat, 06 Nov 2021 17:36:39 +0800
Finished: Sat, 06 Nov 2021 17:36:39 +0800
Ready: True
Restart Count: 0
Environment: <none>
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-wzm78 (ro)
/work-dir from workdir (rw)
Containers:
web:
Container ID: containerd://194aaf1d7c003029d1d89bafd946de1d919efa15f0e1032184fc4b95d4e05ae8
Image: nginx
Image ID: docker.io/library/nginx@sha256:644a70516a26004c97d0d85c7fe1d0c3a67ea8ab7ddf4aff193d9f301670cf36
Port: 80/TCP
Host Port: 0/TCP
State: Running
Started: Sat, 06 Nov 2021 17:36:55 +0800
Ready: True
Restart Count: 0
Environment: <none>
Mounts:
/usr/share/nginx/html from workdir (rw)
/var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-wzm78 (ro)
Conditions:
Type Status
Initialized True
Ready True
ContainersReady True
PodScheduled True
Volumes:
workdir:
Type: EmptyDir (a temporary directory that shares a pod's lifetime)
Medium:
SizeLimit: <unset>
kube-api-access-wzm78:
Type: Projected (a volume that contains injected data from multiple sources)
TokenExpirationSeconds: 3607
ConfigMapName: kube-root-ca.crt
ConfigMapOptional: <nil>
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 119s default-scheduler Successfully assigned default/init-demo to node1
Normal Pulling 118s kubelet Pulling image "busybox"
Normal Pulled 101s kubelet Successfully pulled image "busybox" in 16.310146437s
Normal Created 101s kubelet Created container install
Normal Started 101s kubelet Started container install
Normal Pulling 100s kubelet Pulling image "nginx"
Normal Pulled 85s kubelet Successfully pulled image "nginx" in 15.212796527s
Normal Created 85s kubelet Created container web
Normal Started 85s kubelet Started container web
[root@master1 ~]#

从上面的描述信息里面可以看到初始化容器已经启动了,现在处于 Running 状态,所以还需要稍等,到初始化容器执行完成后退出初始化容器会变成 Complete状态,然后才会启动主容器。待到主容器也启动完成后,Pod 就会变成Running` 状态,然后我们去访问下 Pod 主页,验证下是否有我们初始化容器中下载的页面信息:

  • 访问init-demo pod ip测试:
[root@master1 ~]#kubectl get po -owide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
init-demo 1/1 Running 0 3m3s 10.244.1.6 node1 <none> <none>

[root@master1 ~]#curl 10.244.1.6
<!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&amp;tpl=mn&amp;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>&copy;2017&nbsp;Baidu&nbsp;<a href=http://www.baidu.com/duty/>使用百度前必读</a>&nbsp; <a href=http://jianyi.baidu.com/ class=cp-feedback>意见反馈</a>&nbsp;京ICP证030173号&nbsp; <img src=//www.baidu.com/img/gs.gif> </p> </div> </div> </div> </body> </html>
[root@master1 ~]#

可以看到,这里访问到的是百度首页内容,符合预期。

实验结束。😘

4、Pod Hook

我们知道 Pod 是 Kubernetes 集群中的最小单元,而 Pod 是由容器组成的,所以在讨论 Pod 的生命周期的时候我们可以先来讨论下容器的生命周期。实际上 Kubernetes 为我们的容器提供了生命周期的钩子,就是我们说的 Pod HookPod Hook 是由 kubelet 发起的,当容器中的进程启动前或者容器中的进程终止之前运行,这是包含在容器的生命周期之中我们可以同时为 Pod 中的所有容器都配置 hook。

Kubernetes 为我们提供了两种钩子函数:

  • PostStart:Kubernetes 在容器创建后立即发送 postStart 事件,然而 postStart 处理函数的调用不保证早于容器的入口点(entrypoint) 的执行。 postStart 处理函数与容器的代码是异步执行的,但 Kubernetes的容器管理逻辑会一直阻塞等待 postStart 处理函数执行完毕。只有 postStart 处理函数执行完毕,容器的状态才会变成 RUNNING

  • PreStop:这个钩子在容器终止之前立即被调用。它是阻塞的,所以它必须在删除容器的调用发出之前完成。主要用于优雅关闭应用程序、通知其他系统等。

注意:PostStart用的不是很多,而PreStop用的相对很多

所以我们应该让钩子函数尽可能的轻量。当然有些情况下,长时间运行命令是合理的, 比如在停止容器之前预先保存状态

Kubernetes 只有在 Pod 结束(Terminated)的时候才会发送 preStop 事件,这意味着在 Pod 完成(Completed)时 preStop 的事件处理逻辑不会被触发,可以查看 issue #55087 https://github.com/kubernetes/kubernetes/issues/55807这个 issue 进行了解。

img

另外我们有两种方式来实现上面的钩子函数:

  • Exec - 用于执行一段特定的命令,不过要注意的是该命令消耗的资源会被计入容器。

  • HTTP - 对容器上的特定的端点执行 HTTP 请求。

1.PostStart

💘 实战:PostStart钩子函数-2022.12.9(成功测试)

  • 实验环境
1、win10,vmwrokstation虚机;
2、k8s集群:3台centos7.6 1810虚机,2个master节点,1个node节点
k8s version:v1.20
CONTAINER-RUNTIME:docker://20.10.7
  • 实验软件(无)

  • 创建pod资源清单

以下示例中,定义了一个 Nginx Pod,其中设置了 PostStart 钩子函数,即在容器创建成功后,写入一句话到 /usr/share/message 文件中:

[root@master1 ~]#vim pod-postStart.yaml

# 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 from the postStart handler > /usr/share/message"]
  • 部署
[root@master1 ~]#kubectl apply -f pod-postStart.yaml
pod/hook-demo1 created

[root@master1 ~]#kubectl describe pod hook-demo1
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 19s default-scheduler Successfully assigned default/hook-demo1 to node2
Normal Pulling 18s kubelet Pulling image "nginx"
Normal Pulled 3s kubelet Successfully pulled image "nginx" in 15.461233038s
Normal Created 3s kubelet Created container hook-demo1
Normal Started 3s kubelet Started container hook-demo1

[root@master1 ~]#kubectl get po
NAME READY STATUS RESTARTS AGE
hook-demo1 1/1 Running 0 27s
  • 测试

创建成功后可以查看容器中 /usr/share/message 文件是否内容正确:

[root@master1 ~]#kubectl exec -it hook-demo1 -- cat /usr/share/message
Hello form postStart handler

测试完成。😘

2.PreStop

💘 实战:PreStop钩子函数-2022.12.9(成功测试)

  • 实验环境
1、win10,vmwrokstation虚机;
2、k8s集群:3台centos7.6 1810虚机,2个master节点,1个node节点
k8s version:v1.20
CONTAINER-RUNTIME:docker://20.10.7
  • 实验软件(无)

  • 创建pod.yaml

以下示例中,定义了一个 Nginx Pod,其中设置了 PreStop 钩子函数,即在容器退出之前,优雅的关闭 Nginx:

[root@master1 ~]#vim pod-prestop.yaml

# 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"] # 优雅退出,注意,如果程序有这个优雅退出的命令,应用才可能使用这个preStop hook功能;

---
apiVersion: v1
kind: Pod
metadata:
name: hook-demo3 #这边使用这个demo来测试验证下preStop hook的效果
spec:
volumes:
- name: message
hostPath:
path: /tmp
containers:
- name: hook-demo2
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']

上面定义的两个 Pod,一个是利用 preStop 来进行优雅删除,另外一个是利用 preStop 来做一些信息记录的事情。

  • 部署pod

同样直接创建上面的 Pod:

[root@master1 ~]#kubectl apply -f pod-prestop.yaml
pod/hook-demo2 created
pod/hook-demo3 created
[root@master1 ~]#kubectl get po -owide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
hook-demo2 1/1 Running 0 23s 10.244.1.7 node1 <none> <none>
hook-demo3 1/1 Running 0 23s 10.244.2.7 node2 <none> <none>
  • 测试

创建完成后,我们可以直接删除 hook-demo2 这个 Pod,在容器删除之前会执行 preStop 里面的优雅关闭命令,这个用法在后面我们的滚动更新的时候用来保证我们的应用零宕机非常有用。

第二个 Pod 我们声明了一个 hostPath 类型的 Volume,在容器里面声明挂载到了这个 Volume,所以当我们删除 Pod,退出容器之前,在容器里面输出的信息也会同样的保存到宿主机(一定要是 Pod 被调度到的目标节点)的 /tmp 目录下面,我们可以查看 hook-demo3 这个 Pod 被调度的节点:

[root@master1 ~]#kubectl describe pod hook-demo3
Name: hook-demo3
Namespace: default
Priority: 0
Node: node2/172.29.9.53

可以看到这个 Pod 被调度到了 node2 这个节点上,我们可以先到该节点上查看 /tmp 目录下面目前没有任何内容:

[root@node2 ~]#ls /tmp/
systemd-private-f1bc2b719ba648949a0f4f4f95c61219-chronyd.service-fuobuq vmware-root_6166-1002616931
[root@node2 ~]#

现在我们来删除 hook-demo3 这个 Pod,安装我们的设定在容器退出之前会执行 preStop 里面的命令,也就是会往 message 文件中输出一些信息:

[root@master1 ~]#kubectl delete pod hook-demo3
pod "hook-demo3" deleted
[root@master1 ~]#kubectl get po
NAME READY STATUS RESTARTS AGE
hook-demo1 1/1 Running 0 22m
hook-demo2 1/1 Running 0 3m13s
init-demo 1/1 Running 0 14h
[root@master1 ~]#

[root@node2 ~]#ls /tmp/
message systemd-private-f1bc2b719ba648949a0f4f4f95c61219-chronyd.service-fuobuq vmware-root_6166-1002616931
[root@node2 ~]#cat /tmp/message
Hello from the preStop Handler
[root@node2 ~]#

实验结束。😘

另外 Hook 调用的日志没有暴露给 Pod,如果处理程序由于某种原因失败,它将产生一个事件。对于 PostStart,这是FailedPostStartHook 事件,对于 PreStop,是 FailedPreStopHook 事件,比如我们修改下面的 lifecycleevents.yaml 文件,将 postStart 命令更改为 badcommand 并应用它。

vim lifecycle-events.yaml

#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: ["/usr/sbin/nginx","-s","quit"] # 优雅退出,注意,如果程序有这个优雅退出的命令,应用才可能使用这个preStop hook功能;

应用后我们可以通过运行 kubectl describe pod lifecycle-demo 后来查看一些结果事件的示例输出:

img

5、Pod 健康检查

Pod对象:重启策略+健康检查(应用自修复)。

健康检查是一种让系统知道应用程序实例是否正常运行的简单方法。如果你的应用程序实例未正常运行,其他服务不会访问它或向它发送请求。相反,会将请求发送到另一个准备就绪的实例,或者你应该重试发送请求。

系统应该能够使你的应用程序处于健康状态。默认情况下,pod内的所有容器都已启动后,Kubernetes会开始向该pod发送流量。容器崩溃时,Kubernetes会重启容器。这个默认行为应该足以开始上手。由于Kubernetes有助于创建自定义健康检查,提高部署稳健性就变得比较简单。

任何分布式系统都需要检查健康,Kubernetes也不例外。使用健康检查为你的Kubernetes服务提供稳固的基础、更高的可靠性和更长的正常运行时间。

在Kubernetes中,探针(Probes)是Kubelet用来定期诊断容器健康状况的工具。

1.探针类型

现在在 Pod 的整个生命周期中,能影响到 Pod 的就只剩下健康检查这一部分了。在 Kubernetes 集群当中,我们可以通过配置 liveness probe(存活探针)、readiness probe(就绪探针) 以及 startupProbe(启动探针) 来影响容器的生命周期。

  1. Liveness Probe(存活探针)用来确定容器是否正在运行。如果存活探针失败,kubelet会杀死容器,并且容器将根据其重启策略进行重启。---(通俗点将就是是否还活着。)
  2. Readiness Probe(就绪探针)用来确定容器是否准备好为请求提供服务。如果就绪探针失败,Endpoint控制器将从与Pod关联的所有Service的Endpoints中移除该Pod的IP地址。--(通俗点讲就是说是是否准否准备好了
  3. Startup Probe(启动探针)用来确定应用程序在容器内是否已经启动完毕。如果启动探针失败,kubelet会杀死容器,容器将根据其重启策略进行重启。启动探针的主要目的是为了处理启动时间较长的应用程序当设置了启动探针时,其他探针(存活和就绪探针)将不会开始,直到启动探针成功为止。(如果容器没有提供启动探测,则默认状态为 Success。)(这是Kubernetes 1.16版本引入的特性)

🚩 各探针使用场景

==(1)存活探针 使用场景:==

不妨看另一种场景:你的应用程序因代码错误而崩溃(可能是极端情况),并且无限期挂起,停止为请求提供服务。由于你的进程默认继续运行,Kubernetes会将流量发送到坏掉的pod。使用存活探针,Kubernetes将检测到应用程序不再为请求提供服务,默认情况下重启出故障的pod。

==(2)就绪探针 使用场景:==

不妨以实际场景为例:你的应用程序需要一些时间来预热,或从GitHub等某个外部源下载应用程序内容。除非完全准备好,否则你的应用程序不会接受流量。默认情况下,一旦容器内的进程启动,Kubernetes就会开始发送流量。使用就绪探针,Kubernetes会等到应用程序完全启动后,才允许服务将流量发送到新副本。

请注意,如果你只是想在 Pod 被删除时能够排空请求,则不一定需要使用就绪态探针;在删除 Pod 时,Pod 会自动将自身置于未就绪状态,无论就绪态探针是否存在。 等待 Pod 中的容器停止期间,Pod 会一直处于未就绪状态。

例如,应用程序可能需要在启动期间加载大量数据或配置文件

==(3)启动探针 使用场景:==

如果你的容器需要在启动期间加载大型数据、配置文件等操作,那么这个时候我们可以使用启动探针。

有时候,会有一些现有的应用在启动时需要较长的初始化时间,前面我们提到了探针里面有一个 initialDelaySeconds的属性,可以来配置第一次执行探针的等待时间,对于启动非常慢的应用这个参数非常有用,比如 Jenkins、 Gitlab 这类应用,但是如何设置一个合适的初始延迟时间呢?这个就和应用具体的环境有关系了,所以这个值往往不是通用的。

这样的话可能就会导致一个问题,我们的资源清单在别的环境下可能就会健康检查失败了

这个时候我们就可以使用startupProbe(启动探针) ,该探针将推迟所有其他探针,直到 Pod 完成启动为止,使用方法和存活探针一样。技巧就是使用相同的命令来设置启动探测。

针对 HTTP 或 TCP 检测,可以通过将 failureThreshold * periodSeconds参数设置为足够长的时间来应对糟糕情况下的启动时间。

如果你的容器启动时间通常超出 initialDelaySeconds + failureThreshold × periodSeconds 总值,你应该设置一个启动探针,对存活态探针所使用的同一端点执行检查。 periodSeconds 的默认值是 10 秒,还应该将其failureThreshold 设置得足够高,以便容器有充足的时间完成启动,并且避免更改存活态探针所使用的默认值。 这一设置有助于减少死锁状况的发生。

ports:
- name: liveness-port
containerPort: 8080
hostPort: 8080

livenessProbe:
httpGet:
path: /healthz
port: liveness-port
failureThreshold: 1
periodSeconds: 10

startupProbe:
httpGet:
path: /healthz
port: liveness-port
failureThreshold: 30 # 尽量设置大点
periodSeconds: 10

img

比如上面这里的配置表示我们的慢速容器最多可以有 5 分钟(30 个检查 * 10 秒= 300s)来完成启动。

🚩

就绪探针的配置和存活探针的配置相似,唯一区别就是要使用 readinessProbe 字段,而不是 livenessProbe 字段。

就绪探针和存活探针是同步的;

存活探针和就绪探针如果同时使用的话就可以确保流量不会到达还未准备好的容器,准备好过后,如果应用程序出现了错误,则会重新启动容器。

对于一般的应用提供一个健康检查的 URL 会更好

一般情况下,我们的livenessProbe和readinessProbe都是会去配置;

线上业务,存活探针可以不一定加,但是就绪探针应该尽可能地加上

为什么呢?这里其实是考虑到线上错误排查的一个问题,如果当我们的应用出现了问题,然后就自动重启去掩盖错误的话,可能这个错误就会被永远忽略掉了,所以其实这是一个折衷的做法。不使用存活性探针,而是结合监控报警,保留错误现场,方便错误排查,但是可读写探针是一定需要添加的

2.探测方式

probe 是由 kubelet 对容器执行的定期诊断,要执行诊断,kubelet 既可以在容器内执行代码,也可以发出一个网络请求。

使用探针来检查容器有四种不同的方法,每个探针都必须准确定义为这四种机制中的一种:

  • exec :在容器内执行指定命令,如果命令退出时返回码为 0, 则认为诊断成功。
  • grpc :使用 gRPC 执行一个远程过程调用,目标应该实现 gRPC 健康检查。如果响应的状态是 SERVING,则认为诊断成功。不过需要注意 gRPC 探针是一个 Alpha 特性,只有在启用了 GRPCContainerProbe 特性门时才能使用。
  • httpGet:对容器的 IP 地址上指定端口和路径执行 HTTP GET 请求,如果响应的状态码大于等于 200 且小于400,则诊断被认为是成功的。
  • tcpSocket :使用此配置,kubelet 将尝试在指定端口上打开容器的套接字。如果可以建立连接,容器被认为是健康的,如果不能就认为是失败的。(实际上就是检查端口)

每次探测都将获得以下三种结果之一:

  • Success(成功):容器通过了诊断。
  • Failure(失败):容器未通过诊断。
  • Unknown(未知):诊断失败,因此不会采取任何行动。

==1、命令探针(exec命令)==

命令探针(exec命令):在命令探针情况下,Kubernetes将在你的容器内运行命令。如果命令返回退出代码0,容器将被标记为健康。否则,将被标记为不健康。当你不能或不想运行HTTP服务器时,这种类型的探针很有用,但你可以运行命令来检查应用程序是否健康。在下面示例中,我们检查文件/tmp/healthy是否存在;如果命令返回退出代码0,容器将被标记为健康;否则,它将被标记为不健康。

livenessProbe:
exec:
command:
- cat
- /tmp/healthy

==2、HTTP探针(httpGet)==

HTTP探针(httpGet):这是最常见的探针类型。即使你的应用程序不是HTTP服务器,通常也可以在应用程序内创建轻量级 HTTP服务器以响应存活探针。Kubernetes将在特定端口(本例中端口8080)测式ping路径(比如/healthz)。如果它收到200或300范围内的HTTP响应,将被标记为健康。(想进一步了解HTTP响应代码,请参阅该链接https://developer.mozilla.org/en-US/docs/Web/HTTP/Status)。否则,它会被标记为不健康。

以下是定义HTTP存活探针的方法:

livenessProbe:
httpGet:
path: /healthz
port: 8080

HTTP就绪探针的定义与HTTP存活探针类似,你只需要把liveness换成readiness。

readinessProbe:
httpGet:
path: /healthz
port: 8080
建议开发也做一个这样的网页:专门用于健康检查用的

img

img

==3、TCP探针(tcpSocket)==

TCP探针(tcpSocket):使用TCP探针,Kubernetes会尝试在指定端口(比如下例中的8080端口)建立TCP连接。如果可以建立连接,则认为容器是健康的。如果不能,视为失败。在HTTP或命令探针无法正常工作的情况下,这些探针会很方便。比如说,FTP服务将能够使用这种类型的探针。

readinessProbe:
tcpSocket:
port: 8080

🚩 以多种方式配置探针:

可根据需要运行的频率、成功和失败阈值以及等待响应的时间,以多种方式配置探针。

initialDelaySeconds(默认值0):如果你知道应用程序需要n秒(比如30秒)来预热,可以使用initialDelaySeconds来添加延迟(以秒为单位),直至执行第一次检查。

periodSeconds(默认值10):如果你想要指定执行检查的频率,可以使用periodSeconds来定义。

• timeoutSeconds(默认值1):探测超时时间,默认 1 秒,最小 1 秒。

• successThreshold(默认值1):探测失败后,最少连续探测成功多少次才被认定为成功,默认是 1,但是如果是 liveness则必须是 1。最小值是 1。

• failureThreshold(默认值 3):探测成功后,最少连续探测失败多少次才被认定为失败,默认是 3,最小值是 1。

有关探针配置的更多信息,请参阅该链接:Configure Liveness, Readiness and Startup Probeshttps://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#configure-probes

🚩 此外对于 HTTP 和 TCP 存活检测可以使用命名的 port,例如:

ports:
- name: liveness-port
containerPort: 8080
hostPort: 8080

livenessProbe:
httpGet:
path: /healthz
port: liveness-port

🚩 应尽量避免使用 TCP 探测

不过需要注意应尽量避免使用 TCP 探测,因为 TCP 探测实际就是 kubelet 向指定端口发送 TCP SYN 握手包,当端口被监听内核就会直接响应 ACK,探测就会成功。当程序死锁hang 住的情况,这些并不影响端口监听,所以探测结果还是健康,流量打到表面健康但实际不健康的 Pod 上,就无法处理请求,从而引发业务故障。

3.案例

存活探针(liveness probe)

我们先来给大家演示下存活探针的使用方法,首先我们用 exec 执行命令的方式来检测容器的存活,如下:

==💘 实战:存活探针-exec 执行命令的方式来检测容器的存活-2022.12.10(成功测试)==

  • 实验环境
1、win10,vmwrokstation虚机;
2、k8s集群:3台centos7.6 1810虚机,2个master节点,1个node节点
k8s version:v1.20
CONTAINER-RUNTIME:docker://20.10.7
  • 实验软件(无)

  • 编写yaml

# 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
initialDelaySeconds: 5
periodSeconds: 5

我们这里需要用到一个新的属性 livenessProbe,下面通过 exec 执行一段命令:

  • periodSeconds :表示让 kubelet 每隔 5 秒执行一次存活探针,也就是每 5 秒执行一次上面的 cat/tmp/healthy 命令,如果命令执行成功了,将返回 0,那么 kubelet 就会认为当前这个容器是存活的,如果返回的是非 0 值,那么 kubelet 就会把该容器杀掉然后重启它。默认是 10 秒,最小 1 秒
  • initialDelaySeconds :表示在第一次执行探针的时候要等待 5 秒,这样能够确保我们的容器能够有足够的时间启动起来。大家可以想象下,如果你的第一次执行探针等候的时间太短,是不是很有可能容器还没正常启动起来,所以存活探针很可能始终都是失败的,这样就会无休止的重启下去了,对吧?

我们在容器启动的时候,执行了如下命令:

/bin/sh -c "touch /tmp/healthy; sleep 30; rm -rf /tmp/healthy; sleep 600"

意思是说在容器最开始的 30 秒内创建了一个 /tmp/healthy 文件,在这 30 秒内执行 cat /tmp/healthy 命令都会返回一个成功的返回码。30 秒后,我们删除这个文件,现在执行 cat /tmp/healthy 是不是就会失败了(默认检测失败 3 次才认为失败),所以这个时候就会重启容器了。

  • 我们来创建下该 Pod,然后在 30 秒内,查看 Pod 的 Event:
[root@master1 ~]# kubectl apply -f liveness-exec.yaml

img`

我们可以观察到容器是正常启动的,再隔一会儿查看下 Pod 的 Event,在最下面有一条信息显示 liveness probe 失败了,容器将要重启。然后可以查看到 Pod 的 RESTARTS 值加 1 了:

img

==💘 实战:存活探针-使用 HTTP GET 请求来配置我们的存活探针-2022.12.10(成功测试)==

  • 实验环境
1、win10,vmwrokstation虚机;
2、k8s集群:3台centos7.6 1810虚机,2个master节点,1个node节点
k8s version:v1.20
CONTAINER-RUNTIME:docker://20.10.7
  • 实验软件(无)

同样的,我们还可以使用 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:
path: /healthz
port: 8080
httpHeaders:
- name: X-Custom-Header
value: Awesome
initialDelaySeconds: 3
periodSeconds: 3

img

同样的,根据 periodSeconds 属性我们可以知道 kubelet 需要每隔 3 秒执行一次 liveness Probe ,该探针将向容器中的 server 的 8080 端口发送一个 HTTP GET 请求。如果 server 的 /healthz 路径的 handler 返回一个成功的返回码,kubelet 就会认定该容器是活着的并且很健康,如果返回失败的返回码,kubelet 将杀掉该容器并重启它。 initialDelaySeconds 指定 kubelet 在该执行第一次探测之前需要等待 3 秒钟。通常来说,任何大于 200小于 400 的状态码都会认定是成功的返回码。其他返回码都会被认为是失败的返回码。

  • 我们可以来查看下上面的 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"))
}
})

img

大概意思就是最开始前 10s 返回状态码200,10s 过后就返回状态码500。所以当容器启动3秒后,kubelet 开始执行健康检查。第一次健康检查会成功,因为是在 10s 之内,但是 10 秒后,健康检查将失败,因为现在返回的是一个错误的状态码了,所以 kubelet 将会杀掉和重启容器。

同样的,我们来创建下该 Pod 测试下效果,10 秒后,查看 Pod 的 event,确认 liveness probe 失败并重启了容器:

img

测试结束。😘

==💘 实战:pod健康检查测试(就绪探针+存放探针)-2022.12.11(成功测试)==

  • 实验环境
1、win10,vmwrokstation虚机;
2、k8s集群:3台centos7.6 1810虚机,2个master节点,1个node节点
k8s version:v1.20
CONTAINER-RUNTIME:docker://20.10.7
  • 实验软件(无)

img

#示例:端口探测
apiVersion: v1
kind: Pod
metadata:
name: probe-demo

spec:
containers:
- name: web
image: nginx
ports:
- containerPort: 80

livenessProbe:
tcpSocket:
port: 80
initialDelaySeconds: 30 #启动容器后多少秒健康检查
periodSeconds: 10 #以后每间隔多少秒检查一次
readinessProbe:
tcpSocket:
port: 80
initialDelaySeconds: 30
periodSeconds: 10

#示例:执行Shell命令
livenessProbe:
exec:
command:
- cat
- /tmp/healthy

#示例:HTTP请求
livenessProbe:
httpGet:
path: /healthz
port: 8080
httpHeaders:
- name: Custom-Header
value: Awesome
  • 创建pod.yaml

现在我们来测试下:

创建pod-probe.yaml文件

我们导出一个yaml:

[root@k8s-master ~]#kubectl run nginx --image=nginx --dry-run=client -o yaml > pod-probe.yaml #在此基础上编写yaml文件
apiVersion: v1
kind: Pod
metadata:
labels:
run: nginx
name: nginx
spec:
containers:
- image: nginx
name: nginx
livenessProbe: #存活性检查
httpGet:
path: /index.html
port: 80
initialDelaySeconds: 5 #启动容器后多少秒健康检查
periodSeconds: 10 #以后每间隔多少秒检查一次

readinessProbe: #就绪检查
httpGet:
path: /index.html
port: 80
initialDelaySeconds: 5
periodSeconds: 10
dnsPolicy: ClusterFirst
restartPolicy: Always
  • 部署并测试

apply下

[root@k8s-master ~]#kubectl apply -f pod-probe.yaml
pod/nginx unchanged
  • 查看效果
[root@k8s-master ~]#kubectl get pod
NAME READY STATUS RESTARTS AGE
nginx 1/1 Running 0 84m
web-96d5df5c8-2h8vm 1/1 Running 0 93m
  • 我们来观察下pod日志
[root@k8s-master ~]#kubectl logs nginx -f

img

这里的日志探测时间需要注意下,因为yaml里配置了2个检查项,所以这里显示看起来会有些问题,但其实是没问题的。

  • 模拟nginx容器故障情况

我们再来模拟下nginx容器故障情况

进到nginx容器,把index.html文件删除,观察日志输出及poddescrible的事件信息。

先查看一下原来的pod状态

img

  • 进到nginx容器,把index.html文件删除

img

  • 观察pod日志:报/usr/share/nginx/html/index.html" failed (2: No such file or directory),404

img

  • 观察pod事件:报kubelet的健康检查失败

img

  • 因此,根据健康检查规则,这里将重启pod

img

img

img

  • 再次观察日志输出,发现一切都正常了

img

  • 怎么看出来是移除的?

我们将这一块添加到deployment中,模拟故障,然后用kubectl get svc就可以看出来的。(ep列表)

img

实验结束。😘

就绪探针(readness probe)

==💘 实战:测试readinessProbe与httpGet的使用-2022.12.10(成功测试)==

  • 实验环境
1、win10,vmwrokstation虚机;
2、k8s集群:3台centos7.6 1810虚机,2个master节点,1个node节点
k8s version:v1.20
CONTAINER-RUNTIME:docker://20.10.7
  • 实验软件(无)

  • 创建pod.yaml

这里要注意的关键是readinessProbe与httpGet的使用。第一次检查将在10秒后执行,然后每5秒重复一次。

#readinessProbe-httpget.yaml
apiVersion: v1
kind: Pod
metadata:
labels:
run: nginx
name: nginx
spec:
containers:
- image: nginx
name: nginx
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 10
periodSeconds: 5
  • 部署并测试

部署pod

[root@k8s-master ~]#kubectl apply -f readinessProbe-httpget.yaml 
pod/nginx created
  • 如果你现在检查pod的状态,它应该在STATUS列下显示状态为Running。但如果你检查READY列,它仍然会显示0/1,这意味着它没有准备好接受新的连接。
[root@k8s-master ~]#kubectl get po
NAME READY STATUS RESTARTS AGE
nginx 0/1 Running 0 8s
  • 由于我们将初始延迟设置为10秒,几秒钟后验证状态。现在,pod应该在运行。
[root@k8s-master ~]#kubectl get po
NAME READY STATUS RESTARTS AGE
nginx 1/1 Running 0 18s #这里明显可以看到18-8=10s的就绪时间
  • 要检查定义就绪探针时使用的所有参数(比如initialDelaySeconds、periodSeconds等)的详细状态,运行kubectl describe命令。
[root@k8s-master ~]#kubectl describe pod nginx |grep -i readiness
Readiness: http-get http://:80/ delay=10s timeout=1s period=5s #success=1 #failure=3
  • 我们在部署pod之前也是用-w选项来持续观察pod的状态
[root@k8s-master ~]#kubectl get po -w
NAME READY STATUS RESTARTS AGE
nginx 0/1 Pending 0 0s
nginx 0/1 Pending 0 0s
nginx 0/1 ContainerCreating 0 0s
nginx 0/1 ContainerCreating 0 5s
nginx 0/1 Running 0 7s #这里也可以看到就绪探针的时间
nginx 1/1 Running 0 18s

测试结束。😘

==💘 实战:测试readinessProbe与exec的使用-2022.12.10(成功测试)==

  • 实验环境
1、win10,vmwrokstation虚机;
2、k8s集群:3台centos7.6 1810虚机,2个master节点,1个node节点
k8s version:v1.20
CONTAINER-RUNTIME:docker://20.10.7
  • 实验软件(无)

  • 创建pod.yaml

现在你已了解存活探针的工作原理,不妨调整上面的例子将其定义为就绪探针,以此了解就绪探针的工作原理。在下面例子中,我们在容器内执行命令(sleep 20; touch healthy; sleep 600),它先休眠20秒,创建一个文件,最后休眠600秒。由于初始延迟设置为15秒,第一次检查以15 秒延迟来执行。

#readiness-probe-exec.yaml
apiVersion: v1
kind: Pod
metadata:
labels:
test: readinessProbe-exec
name: readiness-probe-exec
spec:
containers:
- name: readiness-probe
image: busybox
args:
- /bin/sh
- -c
- sleep 20;touch healthy;sleep 600
readinessProbe:
exec:
command:
- cat
- healthy
initialDelaySeconds: 15
periodSeconds: 5
  • 部署pod
[root@k8s-master ~]#kubectl apply -f readiness-probe-exec.yaml 
pod/readiness-probe-exec created
  • 如果你在这里执行kubectl get events,会看到探针失败,因为文件不存在。
[root@k8s-master ~]#kubectl describe po readiness-probe-exec
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 66s default-scheduler Successfully assigned default/readiness-probe-exec to k8s-node2
Normal Pulling 65s kubelet Pulling image "busybox"
Normal Pulled 53s kubelet Successfully pulled image "busybox" in 11.426079872s
Normal Created 53s kubelet Created container readiness-probe
Normal Started 53s kubelet Started container readiness-probe
Warning Unhealthy 36s kubelet Readiness probe failed: cat: can't open 'healthy': No such file or directory
  • 但是如果你在20秒后检查它,它应该处于运行状态。
[root@k8s-master ~]#kubectl get po
NAME READY STATUS RESTARTS AGE
readiness-probe-exec 1/1 Running 0 9m27s

测试结束。😘

6、Pod的终止

1.Pod的终止

img

\1. 用户发出删除 Pod 指令,Pod 被删除,状态变为 Terminating,从 API 层面看就是 Pod metadata 中的deletionTimestamp 字段会被标记上删除时间。

\2. kube-proxy watch 到了就开始更新转发规则,将 Pod 从 service 的 endpoints 列表中摘除掉,新的流量不再转发到该 Pod。

\3. 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 上,才最终退出。

2.强制终止Pod

默认情况下,所有的删除操作都会有 30 秒钟的宽限期限。kubectl delete 命令支持 grace-period=<seconds> 选项,允许你重载默认值,设定自己希望的期限值。

将宽限期限强制设置为 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-gc-threshold 设置)时删除已终止的 Pod(phase 值为 Succeeded 或 Failed)。这一行为会避免随着时间不断创建和终止 Pod 而引起的资源泄露问题

能不用强制删除Pod的命令就不要用这个命令。

3.业务代码处理 SIGTERM 信信号

要实现优雅退出,我们需要业务代码得支持下优雅退出的逻辑,在业务代码里面处理下 SIGTERM 信号,一般主要逻辑就是"排水",即等待存量的任务或连接完全结束,再退出进程。下面我们给出几种常用编程语言实现优雅退出的示例。

\1. Golang:

imgimg

package main
import (
"fmt"
"os"
"os/signal"
"syscall"
)
func main() {
sigs := make(chan os.Signal, 1)
done := make(chan bool, 1)
"#registers the channel
signal.Notify(sigs, syscall.SIGTERM)
go func() {
sig := "-sigs
fmt.Println("Caught SIGTERM, shutting down")
"# Finish any outstanding requests, then"".
done "- true
}()
fmt.Println("Starting application")
"# Main logic goes here
"-done
fmt.Println("exiting")
}

2.Python

img

import signal, time, os
def shutdown(signum, frame):
print('Caught SIGTERM, shutting down')
# Finish any outstanding requests, then"".
exit(0)
if "/name"/ "0 '"/main"/':
# Register handler
signal.signal(signal.SIGTERM, shutdown)
# Main logic goes here

3.Node Js

img

process.on("SIGTERM", () "1 {
console.log("The service is about to shut down!");
"# Finish any outstanding requests, then"".
process.exit(0);
});

4.Java

img

import sun.misc.Signal;
import sun.misc.SignalHandler;
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);
}
}
}

5.Shell

img

img

"3/bin/sh
"4 Redirecting Filehanders
ln -sf /proc/$$/fd/1 /log/stdout.log
ln -sf /proc/$$/fd/2 /log/stderr.log
"4 Pre execution handler
pre_execution_handler() {
"4 Pre Execution
# TODO: put your pre execution steps here
: # delete this nop
}
"4 Post execution handler
post_execution_handler() {
"4 Post Execution
# TODO: put your post execution steps here
: # delete this nop
}
"4 Sigterm Handler
sigterm_handler() {
if [ $pid -ne 0 ]; then
# the above if statement is important because it ensures
# that the application has already started. without it you
# could attempt cleanup steps if the application failed to
# start, causing errors.
kill -15 "$pid"
wait "$pid"
post_execution_handler
fi
exit 143; # 128 + 15 "* SIGTERM
}
"4 Setup signal trap
# on callback execute the specified handler
trap 'sigterm_handler' SIGTERM
"4 Initialization
pre_execution_handler
"4 Start Process
# run process in background and record PID
>/log/stdout.log 2>/log/stderr.log "$@" &
pid="$!"
# Application can log to stdout/stderr, /log/stdout.log or /log/stderr.log
"4 Wait forever until app dies
wait "$pid"
return_code="$?"
"4 Cleanup
post_execution_handler
# echo the return code of the application
exit $return_code

4.收不到 SIGTERM 信信号

上面我们给出了几种常见的捕捉 SIGTERM 信号的代码,然后我们就可以执行停止逻辑以实现优雅退出了。在 Kubernetes环境中,业务发版时经常会对工作负载进行滚动更新,当旧版本 Pod 被删除时,K8s 会对 Pod 中各个容器中的主进程发

送 SIGTERM 信号,当达到退出宽限期后进程还未完全停止的话,就会发送 SIGKILL 信号将其强制杀死。但是有的场景下在 Kubernetes 环境中实际运行时,有时候可能会发现在滚动更新时,我们业务的优雅终止逻辑并没有被执行,现象是在等了较长时间后,业务进程直接被 SIGKILL 强制杀死了。

这是什么原因造成的呢?通常情况下这都是因为容器启动入口使用了 shell,比如使用了类似 /bin/sh -c my-app 这样的启动入口。或者使用 /entrypoint.sh 这样的脚本文件作为入口,在脚本中再启动业务进程,比如下面的entrypoint.sh 文件:

#! /bin/bash
/webserver

这就可能就会导致容器内的业务进程收不到 SIGTERM 信号,原因是:

\1. 容器主进程是 shell,业务进程是在 shell 中启动的,变成了 shell 进程的子进程了。

img

\2. shell 进程默认是不会处理 SIGTERM 信号的,自己不会退出,也不会将信号传递给子进程,所以就导致了业务进程不会触发停止逻辑。

\3. 当等到 K8s 优雅停止宽限时间 ( terminationGracePeriodSeconds,默认 30s),就只能发送 SIGKILL 强制杀死 shell 及其子进程了。

那么我们应该如何让我们的业务进程收到 SIGTERM 信号呢?当然如果可以的话,尽量不使用 shell 启动业务进程,这样当然最好了,此外我们还有其他解决方案。

1. 使用 exec 启动

在 shell 中启动二进制的命令前加一个 exec 命令即可让该二进制启动的进程代替当前 shell 进程,即让新启动的进程成为主进程:

#! /bin/bash
……
exec /webserver # 脚本中执行二进制

然后业务进程就可以正常接收所有信号了,实现优雅退出当然也可以了。

2. 多进程场景

通常我们一个容器只会有一个进程,但有些时候我们不得不启动多个进程,比如从传统部署迁移到 Kubernetes 的过渡期间,使用了富容器,即单个容器中需要启动多个业务进程,这时候我们可以通过 shell 来启动,但却无法使用上面的

exec 方式来传递信号了,因为 exec 只能让一个进程替代当前 shell 成为主进程

这个时候我们可以在 shell 中使用 trap 来捕获信号,当收到信号后触发回调函数来将信号通过 kill 命令传递给业务进程,脚本示例:

img

#! /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 # 等待回调执行完,主进程再退出

3. 使用 init 系统

前面一种方案实际是用脚本实现了一个极简的 init 系统 (或 supervisor ) 来管理所有子进程,只不过它的逻辑很简陋,仅仅简单的透传指定信号给子进程,其实社区有更完善的方案,**dumb-init (https://github.com/Yelp/dumb-init**)和 tini (https://github.com/krallin/tini)都可以作为 init 进程,作为主进程 (PID 1) 在容器中启动,然后它再运行 shell 来执行我们指定的脚本 (shell 作为子进程),shell 中启动的业务进程也成为它的子进程,当它收到信号时会将其传递给所有的子进程,从而也能完美解决 shell 无法传递信号问题,并且还有回收僵尸进程的力,这也是我们强烈推荐的一种方式

如下所示是一个以 dumb-init 制作镜像的 Dockerfile 示例:

img

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 示例:

img

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" ]

start.sh 脚本中当然也可以是多个进程:

#! /bin/bash
/bin/app1 &
/bin/app2 &
wait

优雅退出是 K8s 中非常重要的一个特性,对于实现我们的应用零宕机滚动更新非常重要,所以一定要掌握这部分知识点。

关于我

我的博客主旨:

  • 排版美观,语言精炼;
  • 文档即手册,步骤明细,拒绝埋坑,提供源码;
  • 本人实战文档都是亲测成功的,各位小伙伴在实际操作过程中如有什么疑问,可随时联系本人帮您解决问题,让我们一起进步!

🍀 微信二维码

x2675263825 (舍得), qq:2675263825。

img

🍀 微信公众号

《云原生架构师实战》

img

🍀 语雀

https://www.yuque.com/xyy-onlyone

https://www.yuque.com/xyy-onlyone/exkgza?# 《语雀博客》

img

🍀 博客

www.onlyyou520.com

img

img

🍀 csdn

https://blog.csdn.net/weixin_39246554?spm=1010.2135.3001.5421

img

🍀 知乎

https://www.zhihu.com/people/foryouone

img

最后

好了,关于本次就到这里了,感谢大家阅读,最后祝大家生活快乐,每天都过的有意义哦,我们下期见!

img

1