一个 Pod 从创建到销毁会经历若干阶段:先由各组件协作完成调度与创建,随后可能运行 init 容器完成初始化,主容器启动后由探针持续做健康检查,其状态通过 phase 对外呈现,最终经历优雅终止流程被删除。下面依次展开。
Pod 的创建过程
用户通过 kubectl 客户端提交 Pod Spec 给 API Server。
API Server 尝试将 Pod 对象的相关信息存储到 etcd 中,等待写入操作完成后,API Server 返回确认信息到客户端。
API Server 开始反映 etcd 中的状态变化。
所有 Kubernetes 组件通过 watch 机制跟踪检查 API Server 上的相关信息变动。
kube-scheduler(调度器)通过其 watcher 检测到 API Server 创建了新的 Pod 对象,但还没有绑定到任何工作节点。
kube-scheduler 为 Pod 对象挑选一个工作节点,并将结果信息更新到 API Server。
调度结果由 API Server 更新到 etcd,并且 API Server 也开始反馈该 Pod 对象的调度结果。
Pod 被调度到的目标工作节点上的 kubelet 尝试在当前节点上调用 docker engine 启动容器,并将容器的状态结果返回给 API Server。
API Server 将 Pod 信息存储到 etcd 系统中。
etcd 确认写入操作完成后,API Server 将确认信息发送给相关的 kubelet。
init 容器 init 容器与普通的容器非常像,除了:
init 容器总是运行到成功完成为止;
每个 init 容器都必须在下一个 init 容器启动前完成。
tips :实际上最先生成的是 pause 容器。
如果 Pod 的 init 容器失败,kubernetes 会不断地重启该 Pod,直到 init 容器成功为止。如果 Pod 对应的 restartPolicy 为 Never,则 Pod 启动失败。
init 容器的优势 init 容器具有与应用程序容器分离的单独镜像,因此带来如下优势:
它们可以包含并运行实用工具,而出于安全考虑,这些工具并不适合放进应用程序容器镜像中。
它们可以包含用于安装的工具和定制化代码,而这些不必出现在应用程序镜像中。例如,创建镜像没必要 FROM 另一个镜像,只需要在安装过程中使用类似 sed、awk、python 或 dig 这样的工具。
应用程序镜像可以分离出创建和部署的角色,而没有必要联合它们构建一个单独的镜像。
init 容器使用 Linux Namespace,所以相对应用程序容器来说具有不同的文件系统视图。因此,它们能够具有访问 Secret 的权限,而应用程序容器则不能。
它们必须在应用程序容器启动之前运行完成,而应用程序容器是并行运行的,所以 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 容器依赖的 myservice、mydb 尚未创建,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 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 NAME READY STATUS RESTARTS AGE myapp-pod 0/1 Init:1/2 0 2m2s [root@k8s01 ~]# kubectl get pods 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 提供三种探针:
livenessProbe(存活探针):指示容器是否正在运行。如果存活探测失败,kubelet 会杀死容器,容器将受到其重启策略的影响。如果容器不提供存活探针,则默认状态为 Success。
readinessProbe(就绪探针):指示容器是否准备好服务请求。如果就绪探测失败,端点控制器会将该 Pod 的 IP 从与之匹配的所有 Service 的 endpoint 中删除。初始延迟之前的就绪状态默认为 Failure;如果容器不提供就绪探针,则默认状态为 Success。
startupProbe(启动探针,v1.16 新增):用于判断容器是否已启动完成。startupProbe 通过后,前两个探针才会开始检测 。如果启动探测失败,kubelet 会杀死容器,容器将受到其重启策略的影响。如果容器不提供此探针,则默认状态为 Success。
要执行诊断,kubelet 调用由容器实现的 Handler。k8s 内置了三种类型的处理程序:
ExecAction:在容器内执行命令。命令退出返回码为 0 则认为诊断成功。
TCPSocketAction:对指定端口上的容器 IP 地址进行 TCP 检查。端口打开则诊断成功。
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 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 [root@k8s01 ~]# kubectl get pod 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 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 次才会重启;通过之后才开始 livenessProbe 与 readinessProbe:
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 apiVersion: v1 kind: Pod metadata: name: liveness-readiness-pod namespace: default spec: containers: - name: liveness-readiness-container image: nginx:1.7.9 imagePullPolicy: IfNotPresent ports: - name: http containerPort: 80 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: httpGet: path: /index.html port: 80 failureThreshold: 3 periodSeconds: 2
一旦容器通过了 startupProbe,kubelet 便按各探针各自的 periodSeconds 间隔,分别进行存活检测(livenessProbe)和就绪检测(readinessProbe)。
重启策略 PodSpec 中的 restartPolicy 字段适用于 Pod 中的所有容器,取值为 Always、OnFailure、Never,没有此字段时默认为 Always。
restartPolicy 仅指由同一节点上的 kubelet 重新启动容器。
Pod phase Pod 的 status 字段是一个 PodStatus 对象,其中的 phase(相位)字段是对 Pod 在其生命周期中所处状态的简单宏观概述。phase 仅有以下取值:
Pending:Pod 已被 Kubernetes 系统接受,但有一个或多个容器镜像尚未创建。
Running:该 Pod 已绑定到一个节点,Pod 中所有容器都已被创建,且至少有一个容器正在运行,或者正处于启动或重启状态。
Succeeded:Pod 中所有容器均已成功终止(退出码为 0),并且不会再被重启。
Failed:Pod 中所有容器都已终止,但至少有一个容器以非 0 状态退出或被系统终止。
Unknown:因为某些原因无法取得 Pod 的状态,通常是与 Pod 所在主机通信失败。
常见场景分析 下面列举在不同 restartPolicy 下,几类典型场景对 Pod phase 的影响。
单容器 Pod,容器成功退出
记录完成事件;
restartPolicy 的影响:
Always:重启容器,Pod phase 仍为 Running;
OnFailure:Pod phase 变为 Succeeded;
Never:Pod phase 变为 Succeeded。
单容器 Pod,容器退出失败
记录失败事件;
restartPolicy 的影响:
Always:重启容器,Pod phase 仍为 Running;
OnFailure:Pod phase 仍为 Running;
Never:Pod phase 变为 Failed。
双容器 Pod,其中一个容器退出失败
记录失败事件;
restartPolicy 的影响:
Always:重启容器,Pod phase 仍为 Running;
OnFailure:Pod phase 仍为 Running;
Never:Pod phase 仍为 Running。
若在此基础上另一个容器也退出,导致没有容器处于运行状态:
Always:重启容器,Pod phase 仍为 Running;
OnFailure:Pod phase 仍为 Running;
Never:Pod phase 变为 Failed。
单容器 Pod,容器运行时内存超限(OOM)
容器以失败状态终止;
记录 OOM 事件;
restartPolicy 的影响:
Always:重启容器,Pod phase 仍为 Running;
OnFailure:Pod phase 仍为 Running;
Never:Pod phase 变为 Failed。
Pod 正在运行,磁盘故障
杀掉所有容器,记录适当事件;
Pod phase 变为 Failed;
如果使用控制器来运行,Pod 将在别处重建。
Pod 正在运行,其节点被分区(partition)
节点控制器等待直到超时;
节点控制器将 Pod phase 设置为 Failed;
如果使用控制器来运行,Pod 将在别处重建。
Pod hook(生命周期钩子) Pod hook(钩子)由 kubelet 发起,在容器进程启动前或容器进程终止前运行,包含在容器的生命周期之中,可以为 Pod 中所有容器都配置 hook。
支持的 hook 类型:
postStart:容器创建后立即执行。注意由于是异步执行,它无法保证一定在 ENTRYPOINT 之前运行。如果执行失败,容器会被杀死,并根据 restartPolicy 决定是否重启。
preStop:容器终止前执行,常用于资源清理与优雅退出(例如执行 nginx -s quit)。如果执行失败,容器同样会被杀死。
钩子的回调函数支持两种方式:
exec:执行一段命令;
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
Pod 的终止过程
用户发送删除 Pod 的命令,默认宽限期是 30 秒;
API Server 立刻 给对象写上 deletionTimestamp——不用等宽限期。此刻 Pod 状态就变成 Terminating,endpoint controller 也同时把它从 Service 的 endpoint 列表里摘掉,宽限期这才开始倒计时;
kubelet watch 到这个变化后开始停止 Pod:
如果定义了 preStop hook,先调用它;
然后向容器里的进程发送 TERM 信号;
宽限期到了还没退出的进程,收到 SIGKILL 被强杀;
kubelet 以优雅周期 0 向 API Server 确认删除,对象从 API 中消失,客户端不再可见。
有一个容易踩的竞态值得留意:摘 endpoint 和发 SIGTERM 是并发发生的 ,而 kube-proxy、ingress 更新自己那份转发规则又有延迟,所以收到 SIGTERM 之后仍然会有新连接被打进来。这也是为什么优雅退出的标准姿势是在 preStop 里先 sleep 个 5~10 秒、让规则传播完,再让应用退出,而不是一收到信号就关监听。另外 terminationGracePeriodSeconds 是 preStop 和 SIGTERM 共享 的总预算,不是各给一份。
宽限期由如下字段控制:
1 2 spec: terminationGracePeriodSeconds: 30
kubectl delete 命令支持 --grace-period=<seconds> 选项,允许用户设置自己的宽限期,也可以使用 --force --grace-period=0 来强制删除 Pod。
Pod 的强制删除是通过在 cluster 和 etcd 中将其标记为删除状态实现的。执行强制删除命令时,API Server 不会等待该 Pod 所在节点上的 kubelet 确认,就立即将该 Pod 从 API 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,还需要删除对应的控制器才行。