Pod 生命周期

Pod 生命周期

容器生命周期

一个 Pod 从创建到销毁会经历若干阶段:先由各组件协作完成调度与创建,随后可能运行 init 容器完成初始化,主容器启动后由探针持续做健康检查,其状态通过 phase 对外呈现,最终经历优雅终止流程被删除。下面依次展开。

Pod 的创建过程

  1. 用户通过 kubectl 客户端提交 Pod SpecAPI Server
  2. API Server 尝试将 Pod 对象的相关信息存储到 etcd 中,等待写入操作完成后,API Server 返回确认信息到客户端。
  3. API Server 开始反映 etcd 中的状态变化。
  4. 所有 Kubernetes 组件通过 watch 机制跟踪检查 API Server 上的相关信息变动。
  5. kube-scheduler(调度器)通过其 watcher 检测到 API Server 创建了新的 Pod 对象,但还没有绑定到任何工作节点。
  6. kube-schedulerPod 对象挑选一个工作节点,并将结果信息更新到 API Server
  7. 调度结果由 API Server 更新到 etcd,并且 API Server 也开始反馈该 Pod 对象的调度结果。
  8. Pod 被调度到的目标工作节点上的 kubelet 尝试在当前节点上调用 docker engine 启动容器,并将容器的状态结果返回给 API Server
  9. API ServerPod 信息存储到 etcd 系统中。
  10. etcd 确认写入操作完成后,API Server 将确认信息发送给相关的 kubelet

init 容器

init 容器与普通的容器非常像,除了:

  1. init 容器总是运行到成功完成为止;
  2. 每个 init 容器都必须在下一个 init 容器启动前完成。

tips:实际上最先生成的是 pause 容器。

如果 Podinit 容器失败,kubernetes 会不断地重启该 Pod,直到 init 容器成功为止。如果 Pod 对应的 restartPolicyNever,则 Pod 启动失败。

init 容器的优势

init 容器具有与应用程序容器分离的单独镜像,因此带来如下优势:

  1. 它们可以包含并运行实用工具,而出于安全考虑,这些工具并不适合放进应用程序容器镜像中。
  2. 它们可以包含用于安装的工具和定制化代码,而这些不必出现在应用程序镜像中。例如,创建镜像没必要 FROM 另一个镜像,只需要在安装过程中使用类似 sedawkpythondig 这样的工具。
  3. 应用程序镜像可以分离出创建和部署的角色,而没有必要联合它们构建一个单独的镜像。
  4. init 容器使用 Linux Namespace,所以相对应用程序容器来说具有不同的文件系统视图。因此,它们能够具有访问 Secret 的权限,而应用程序容器则不能。
  5. 它们必须在应用程序容器启动之前运行完成,而应用程序容器是并行运行的,所以 init 容器能够提供一种简单的阻塞或延迟应用容器启动的方法,直到满足一组先决条件。

init 容器示例

init-example.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: v1
kind: Pod
metadata:
name: myapp-pod
labels:
app: myapp
spec:
containers:
- name: myapp-container
image: busybox
command: ['sh','-c','echo The app is running! && sleep 3600']
initContainers: # 关键字
- name: init-myservice
image: busybox
command: ['sh','-c','until nslookup myservice; do echo waiting for myservice; sleep 2;done;']
- name: init-mydb
image: busybox
command: ['sh','-c','until nslookup mydb; do echo waiting for mydb; sleep 2; done;']

应用后,由于 init 容器依赖的 myservicemydb 尚未创建,Pod 会停留在 Init 状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[root@k8s01 ~]# kubectl apply -f init-example.yaml
pod/myapp-pod created

[root@k8s01 ~]# kubectl get pods # init 没成功
NAME READY STATUS RESTARTS AGE
myapp-pod 0/1 Init:0/2 0 13s

[root@k8s01 ~]# kubectl logs myapp-pod -c init-myservice
waiting for myservice
Server: 10.96.0.10
Address: 10.96.0.10:53

** server can't find myservice.default.svc.cluster.local: NXDOMAIN
*** Can't find myservice.svc.cluster.local: No answer
*** Can't find myservice.cluster.local: No answer

waiting for myservice
## init 未就绪

创建 init 容器所等待的 Service:service-init-example.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
kind: Service
apiVersion: v1
metadata:
name: myservice
spec:
ports:
- protocol: TCP
port: 80
targetPort: 9376

---
kind: Service
apiVersion: v1
metadata:
name: mydb
spec:
ports:
- protocol: TCP
port: 80
targetPort: 9377
1
kubectl apply -f service-init-example.yaml

此时 init 容器依次成功,Pod 最终进入 Running

1
2
3
4
5
6
7
8
9
10
11
[root@k8s01 ~]# kubectl get pods  # 第一个 init 容器启动
NAME READY STATUS RESTARTS AGE
myapp-pod 0/1 Init:1/2 0 2m2s
[root@k8s01 ~]# kubectl get pods # Pod 内主容器开始启动
NAME READY STATUS RESTARTS AGE
myapp-pod 0/1 PodInitializing 0 2m44s
[root@k8s01 ~]# kubectl get pods
NAME READY STATUS RESTARTS AGE
myapp-pod 1/1 Running 0 2m49s
[root@k8s01 ~]# kubectl logs myapp-pod
The app is running!

通过 kubectl describe pod myapp-pod 可以看到两个 init 容器先后 Terminated / Completed,主容器随后 Running,Events 也清晰反映了这一先后顺序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
[root@k8s01 ~]# kubectl describe pod myapp-pod
Name: myapp-pod
Namespace: default
Node: k8s02/192.168.43.102
Labels: app=myapp
Status: Running
IP: 172.18.236.154
Init Containers:
init-myservice:
Image: busybox
State: Terminated
Reason: Completed
Exit Code: 0
Ready: True
init-mydb:
Image: busybox
State: Terminated
Reason: Completed
Exit Code: 0
Ready: True
Containers:
myapp-container:
Image: busybox
State: Running
Ready: True
Conditions:
Type Status
Initialized True
Ready True
ContainersReady True
PodScheduled True
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 2m59s default-scheduler Successfully assigned default/myapp-pod to k8s02
Normal Created 2m41s kubelet, k8s02 Created container init-myservice
Normal Started 2m41s kubelet, k8s02 Started container init-myservice
Normal Created 63s kubelet, k8s02 Created container init-mydb
Normal Started 63s kubelet, k8s02 Started container init-mydb
Normal Created 13s kubelet, k8s02 Created container myapp-container
Normal Started 12s kubelet, k8s02 Started container myapp-container

容器探针

探针是由 kubelet 对容器执行的定期诊断。Kubernetes 提供三种探针:

  1. livenessProbe(存活探针):指示容器是否正在运行。如果存活探测失败,kubelet 会杀死容器,容器将受到其重启策略的影响。如果容器不提供存活探针,则默认状态为 Success
  2. readinessProbe(就绪探针):指示容器是否准备好服务请求。如果就绪探测失败,端点控制器会将该 PodIP 从与之匹配的所有 Serviceendpoint 中删除。初始延迟之前的就绪状态默认为 Failure;如果容器不提供就绪探针,则默认状态为 Success
  3. startupProbe(启动探针,v1.16 新增):用于判断容器是否已启动完成。startupProbe 通过后,前两个探针才会开始检测。如果启动探测失败,kubelet 会杀死容器,容器将受到其重启策略的影响。如果容器不提供此探针,则默认状态为 Success

要执行诊断,kubelet 调用由容器实现的 Handlerk8s 内置了三种类型的处理程序:

  1. ExecAction:在容器内执行命令。命令退出返回码为 0 则认为诊断成功。
  2. TCPSocketAction:对指定端口上的容器 IP 地址进行 TCP 检查。端口打开则诊断成功。
  3. HTTPGetAction:对指定端口和路径上的容器 IP 地址执行 HTTP GET 请求。响应码为 200~400(不包含 400)则诊断成功。

探测结果有三种:

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

就绪检测(readinessProbe)

readinessProbe-http-get.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: v1
kind: Pod
metadata:
name: readiness-httpget-pod
namespace: default
spec:
containers:
- name: readiness-httpget-container
image: nginx:1.7.9
imagePullPolicy: IfNotPresent
readinessProbe: # 关键字
httpGet:
port: 80
path: /index1.html
initialDelaySeconds: 1 # 触发延时
periodSeconds: 3 # 重试间隔时间

由于 /index1.html 不存在,就绪探测返回 404 而失败;手动创建该文件后返回 200,探测成功,Pod 变为就绪:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[root@k8s01 ~]# kubectl describe pod readiness-httpget-pod
## 省略部分
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 5s default-scheduler Successfully assigned default/readiness-httpget-pod to k8s03
Normal Pulled 4s kubelet, k8s03 Container image "nginx:1.7.9" already present on machine
Normal Created 4s kubelet, k8s03 Created container readiness-httpget-container
Normal Started 3s kubelet, k8s03 Started container readiness-httpget-container
Warning Unhealthy 0s kubelet, k8s03 Readiness probe failed: HTTP probe failed with statuscode: 404
## 探测失败

[root@k8s01 ~]# kubectl exec readiness-httpget-pod -it -- /bin/bash
root@readiness-httpget-pod:/# echo "123" > /usr/share/nginx/html/index1.html
root@readiness-httpget-pod:/# exit
exit
[root@k8s01 ~]# kubectl get pod
NAME READY STATUS RESTARTS AGE
myapp-pod 1/1 Running 0 26m
readiness-httpget-pod 1/1 Running 0 3m34s

当存在 index1.html 时会返回 200,探测成功。

存活检测(livenessProbe)

存活检测支持 exec、httpGet、tcpSocket 三种方式,下面分别举例。

exec 方式

livenessProbe-exec.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: v1
kind: Pod
metadata:
name: liveness-exec-pod
namespace: default
spec:
containers:
- name: liveness-exec-container
image: busybox:1.32.0
imagePullPolicy: IfNotPresent
command: ["/bin/sh","-c","touch /tmp/live ; sleep 60; rm -rf /tmp/live; sleep 3600"]
livenessProbe:
exec:
command: ["test","-e","/tmp/live"]
initialDelaySeconds: 1
periodSeconds: 3

容器启动 60 秒后删除 /tmp/live,此后存活探测持续失败,Pod 周期性重启:

1
2
3
4
5
[root@k8s01 ~]# kubectl get pod  ## liveness-exec-pod 会周期性 restart
NAME READY STATUS RESTARTS AGE
liveness-exec-pod 0/1 CrashLoopBackOff 6 10m
myapp-pod 1/1 Running 0 7h31m
readiness-httpget-pod 1/1 Running 0 7h1m

httpGet 方式

livenessProbe-httpget.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: v1
kind: Pod
metadata:
name: liveness-httpget-pod
namespace: default
spec:
containers:
- name: liveness-httpget-container
image: nginx:1.7.9
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 80
livenessProbe:
httpGet:
port: 80
path: /index.html
initialDelaySeconds: 1
periodSeconds: 3
timeoutSeconds: 10

/index.html 改名后,存活探测失败,容器会 restart:

1
2
3
4
5
6
7
8
9
[root@k8s01 ~]# kubectl exec liveness-httpget-pod -it -- /bin/bash
## 将 index.html 改名为 index1.html

[root@k8s01 ~]# kubectl get pod ## liveness-httpget-pod 会 restart
NAME READY STATUS RESTARTS AGE
liveness-exec-pod 0/1 CrashLoopBackOff 6 12m
liveness-httpget-pod 1/1 Running 1 2m59s
myapp-pod 1/1 Running 0 7h31m
readiness-httpget-pod 1/1 Running 0 7h1m

tcpSocket 方式

livenessProbe-tcp.yaml(探测监听中的 80 端口):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: v1
kind: Pod
metadata:
name: probe-tcp
spec:
containers:
- name: nginx
image: nginx:1.7.9
livenessProbe:
initialDelaySeconds: 5
timeoutSeconds: 1
tcpSocket:
port: 80
periodSeconds: 3

livenessProbe-tcp-81.yaml(探测未监听的 81 端口):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: v1
kind: Pod
metadata:
name: probe-tcp1
spec:
containers:
- name: nginx
image: nginx:1.7.9
livenessProbe:
initialDelaySeconds: 5
timeoutSeconds: 1
tcpSocket:
port: 81
periodSeconds: 3

80 端口可以探测到,81 端口因无人监听导致连接被拒绝,容器一直重启:

1
2
3
4
[root@k8s01 ~]# kubectl get pod # tcp 80 端口可以检测到,81 端口一直重启
NAME READY STATUS RESTARTS AGE
probe-tcp80 1/1 Running 0 19s
probe-tcp81 0/1 CrashLoopBackOff 4 2m48s

查看日志可见 Liveness probe failed: dial tcp ...:81: connect: connection refused

1
2
3
4
5
6
7
8
9
10
11
# 节选
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled <unknown> default-scheduler Successfully assigned default/probe-tcp81 to k8s03
Normal Created 6h39m (x4 over 6h41m) kubelet, k8s03 Created container nginx
Normal Started 6h39m (x4 over 6h41m) kubelet, k8s03 Started container nginx
Normal Killing 6h39m (x3 over 6h40m) kubelet, k8s03 Container nginx failed liveness probe, will be restarted
Normal Pulled 6h39m (x4 over 6h41m) kubelet, k8s03 Container image "nginx:1.7.9" already present on machine
Warning Unhealthy 6h39m (x10 over 6h41m) kubelet, k8s03 Liveness probe failed: dial tcp 172.18.235.163:81: connect: connection refused
Warning BackOff 6h36m (x10 over 6h38m) kubelet, k8s03 Back-off restarting failed container

存活 + 就绪 + 启动检测(startupProbe,v1.16+)

liveness-readiness.yaml 同时配置三种探针。startupProbe 最先执行,连续失败达到 failureThreshold 次才会重启;通过之后才开始 livenessProbereadinessProbe

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
apiVersion: v1
kind: Pod
metadata:
name: liveness-readiness-pod
namespace: default
spec:
containers:
- name: liveness-readiness-container
image: nginx:1.7.9
imagePullPolicy: IfNotPresent
readinessProbe: # 关键字
httpGet:
port: 80
path: /index1.html
initialDelaySeconds: 1 # 触发延时
periodSeconds: 3 # 重试间隔时间
livenessProbe:
httpGet:
port: http
path: /index.html
initialDelaySeconds: 1
periodSeconds: 3
timeoutSeconds: 10
startupProbe: # 最先进行 startupProbe,3 次没通过就重启,通过后再进行另外两个检测
httpGet:
path: /index.html
port: 80
failureThreshold: 3 # 失败阈值
periodSeconds: 2

一旦容器通过了 startupProbekubelet 便按各探针各自的 periodSeconds 间隔,分别进行存活检测(livenessProbe)和就绪检测(readinessProbe)。

重启策略

PodSpec 中的 restartPolicy 字段适用于 Pod 中的所有容器,取值为 AlwaysOnFailureNever,没有此字段时默认为 Always

restartPolicy 仅指由同一节点上的 kubelet 重新启动容器。

Pod phase

Podstatus 字段是一个 PodStatus 对象,其中的 phase(相位)字段是对 Pod 在其生命周期中所处状态的简单宏观概述。phase 仅有以下取值:

  1. PendingPod 已被 Kubernetes 系统接受,但有一个或多个容器镜像尚未创建。
  2. Running:该 Pod 已绑定到一个节点,Pod 中所有容器都已被创建,且至少有一个容器正在运行,或者正处于启动或重启状态。
  3. SucceededPod 中所有容器均已成功终止(退出码为 0),并且不会再被重启。
  4. FailedPod 中所有容器都已终止,但至少有一个容器以非 0 状态退出或被系统终止。
  5. Unknown:因为某些原因无法取得 Pod 的状态,通常是与 Pod 所在主机通信失败。

常见场景分析

下面列举在不同 restartPolicy 下,几类典型场景对 Pod phase 的影响。

单容器 Pod,容器成功退出

  1. 记录完成事件;
  2. restartPolicy 的影响:
    • Always:重启容器,Pod phase 仍为 Running
    • OnFailure:Pod phase 变为 Succeeded
    • Never:Pod phase 变为 Succeeded

单容器 Pod,容器退出失败

  1. 记录失败事件;
  2. restartPolicy 的影响:
    • Always:重启容器,Pod phase 仍为 Running
    • OnFailure:Pod phase 仍为 Running
    • Never:Pod phase 变为 Failed

双容器 Pod,其中一个容器退出失败

  1. 记录失败事件;
  2. restartPolicy 的影响:
    • Always:重启容器,Pod phase 仍为 Running
    • OnFailure:Pod phase 仍为 Running
    • Never:Pod phase 仍为 Running
  3. 若在此基础上另一个容器也退出,导致没有容器处于运行状态:
    • Always:重启容器,Pod phase 仍为 Running
    • OnFailure:Pod phase 仍为 Running
    • Never:Pod phase 变为 Failed

单容器 Pod,容器运行时内存超限(OOM)

  1. 容器以失败状态终止;
  2. 记录 OOM 事件;
  3. restartPolicy 的影响:
    • Always:重启容器,Pod phase 仍为 Running
    • OnFailure:Pod phase 仍为 Running
    • Never:Pod phase 变为 Failed

Pod 正在运行,磁盘故障

  1. 杀掉所有容器,记录适当事件;
  2. Pod phase 变为 Failed
  3. 如果使用控制器来运行,Pod 将在别处重建。

Pod 正在运行,其节点被分区(partition)

  1. 节点控制器等待直到超时;
  2. 节点控制器将 Pod phase 设置为 Failed
  3. 如果使用控制器来运行,Pod 将在别处重建。

Pod hook(生命周期钩子)

Pod hook(钩子)由 kubelet 发起,在容器进程启动前或容器进程终止前运行,包含在容器的生命周期之中,可以为 Pod 中所有容器都配置 hook

支持的 hook 类型:

  • postStart:容器创建后立即执行。注意由于是异步执行,它无法保证一定在 ENTRYPOINT 之前运行。如果执行失败,容器会被杀死,并根据 restartPolicy 决定是否重启。
  • preStop:容器终止前执行,常用于资源清理与优雅退出(例如执行 nginx -s quit)。如果执行失败,容器同样会被杀死。

钩子的回调函数支持两种方式:

  1. exec:执行一段命令;
  2. HTTPGet:发送 HTTP 请求。

启动、退出动作示例

start_stop.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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", "echo Hello from the poststop handler >/usr/share/message"]

进入容器可以看到 postStart 已成功写入文件:

1
2
3
[root@k8s01 ~]# kubectl exec lifecycle-demo -it -- /bin/bash
root@lifecycle-demo:/# cat /usr/share/message
Hello from the postStart handler # postStart 成功

Pod 的终止过程

  1. 用户发送删除 Pod 的命令,默认宽限期是 30 秒;
  2. 在超过宽限期后,API Server 会更新 Pod 的状态为 dead
  3. 命令行上显示的 Pod 状态变为 Terminating,同时 svc 会从 endpoint 中移除该 Pod;
  4. 与第 3 步同时,当 kubelet 发现 Pod 被标记为 Terminating 状态时,开始停止 Pod 进程:
    • 如果在 Pod 中定义了 preStop hook,会在停止 Pod 前调用它;如果宽限期过后 preStop hook 仍在运行,则会再增加 2 秒宽限期;
    • Pod 中的进程发送 TERM 信号;
  5. Pod 从对应 Serviceendpoint 列表中删除,不再是 RS 的一部分;关闭较慢的 Pod 将继续处理已由 load balancer 转发来的流量;
  6. 过了宽限期后,向 Pod 中依然运行的进程发送 SIGKILL 信号将其杀掉;
  7. kubeletAPI Server 中完成 Pod 的删除(将优雅周期设置为 0,即立即删除)。PodAPI 中消失,客户端也不再可见。

宽限期由如下字段控制:

1
2
spec:
terminationGracePeriodSeconds: 30 # 删除宽限期,默认 30 秒

kubectl delete 命令支持 --grace-period=<seconds> 选项,允许用户设置自己的宽限期,也可以使用 --force --grace-period=0 来强制删除 Pod

Pod 的强制删除是通过在 clusteretcd 中将其标记为删除状态实现的。执行强制删除命令时,API Server 不会等待该 Pod 所在节点上的 kubelet 确认,就立即将该 PodAPI Server 中移除。这时节点上的 Pod 会被立即设置为 Terminating 状态,不过在被真正强制删除之前依然有一小段优雅删除周期。

1
2
3
[root@k8s01 ~]# kubectl delete pod redis-master-0 --force --grace-period=0
warning: Immediate deletion does not wait for confirmation that the running resource has been terminated. The resource may continue to run on the cluster indefinitely.
pod "redis-master-0" deleted

如果删除一个 Pod 后再次查看发现它还在,这是因为在控制器中定义了 replicas,还需要删除对应的控制器才行。