一文学会 Kubernetes Pod 的生命周期管理(转载)
使用探活(liveness)探针(Probe)检查容器的健康状况
使用 poststart hook 在容器启动时执行初始化操作
使用 preStop hook 在容器终止之前运行自定义程序
检查 Pod 的状态
- 使用活性探针保持容器健康
- 使用生命周期挂钩在容器启动和关闭时执行操作
- 了解 Pod 及其容器的完整生命周期
了解 Pod 的状态(Status)
Pod 的状态部分包含以下信息:
- Pod 和托管它的工作节点的 IP 地址
- Pod 何时启动
- Pod 的服务质量 (QoS) 等级
- Pod 处于哪个阶段,
- 集群的条件,以及其各个容器的状态。
Pod Phase | 描述 |
Pending | 创建 Pod 对象后,这是其初始阶段。 在 pod 被调度到节点并且拉取并启动其容器的镜像之前,它一直处于此阶段。 |
Running | 该 Pod 的至少一个容器正在运行。因此 Pod 包含多个容器,只要有一个正在运行,Pod 的状态就是 Running |
Succeeded | 当所有容器成功完成时,不打算无限期运行的 Pod 会被标记为“成功”。注意:Pod 的生命周期很多时候由工作负载(workload)来管理,因此会有不同的策略 |
Failed | 当 pod 未配置为无限期运行并且其至少一个容器未成功终止时,该 pod 会被标记为“失败”。 |
Unknown | Pod 的状态未知,因为 Kubelet 已停止报告与 API 服务器的通信。 工作节点可能出现故障或与网络断开连接。 |
Pod 的条件(conditions)指示 Pod 是否已达到特定状态以及原因。与阶段相反,Pod 同时具有多个条件。
Pod Condition | 描述 |
PodScheduled | 指示pod是否已调度到节点 |
Initialized | Pod 的初始化容器已全部成功完成。 |
ContainersReady | Pod 中的所有容器都表明它们已准备就绪。 这是整个 Pod 准备就绪的必要但非充分条件。 |
Ready | 该 Pod 已准备好为其客户提供服务。 Pod 中的容器和 Pod 的就绪门(Readiness Gates)都报告它们已准备就绪。 |
每个条件要么满足,要么不满足。 如下图所示,PodScheduled 和 Initialized 条件一开始未满足,但很快就满足,并且在 Pod 的整个生命周期中保持这种状态。 相比之下,Ready 和 ContainersReady 条件在 Pod 的生命周期内可能会发生多次变化。
容器状态包含几个字段。 state 字段指示容器的当前状态,而 lastState 字段显示前一个容器终止后的状态。 容器状态还指示容器的内部ID(containerID)、容器正在运行的镜像和imageID、容器是否准备好以及重新启动的频率(restartCount)。
容器状态最重要的部分是它的状态。 容器可以处于下图所示的状态之一。
容器状态 | 描述 |
Waiting | 容器正在等待启动。 原因(reason)和消息(message)字段指示容器处于此状态的原因。 |
Running | 容器已创建并且进程正在其中运行。 startedAt 字段指示该容器启动的时间。 |
Terminated | 容器中运行的进程已终止。 startedAt 和 finishedAt 字段指示容器何时启动以及何时终止。 主进程终止的退出代码位于 exitCode 字段中。 |
Unknown | 无法确定容器的状态。例如失联了 |
如果 Pod 中的一个容器死掉了怎么办? 如果 pod 中的所有容器都死掉了怎么办? 如何保持 Pod 健康及其容器运行?
当 pod 被调度到某个节点时,该节点上的 Kubelet 会启动其容器,并从那时起,只要 pod 对象存在,它们就会保持运行。 如果容器中的主进程因任何原因终止,Kubelet 会重新启动容器。 如果应用程序中的错误导致其崩溃,Kubernetes 会自动重新启动它,因此即使应用程序本身没有执行任何特殊操作,在 Kubernetes 中运行它也会自动赋予其自我修复的能力。
默认情况下,无论容器中的进程是否以零或非零退出代码退出,Kubernetes 都会重新启动容器 - 换句话说,无论容器成功完成还是失败。 可以通过在 Pod 规范中设置 restartPolicy 字段来更改此行为。
以下是三种重启策略:
重启策略 | 描述 |
Always | 无论容器中的进程以什么退出代码终止,容器都会重新启动。 这是默认的重启策略。 |
OnFailure | 仅当进程以非零退出代码终止时,容器才会重新启动,按照惯例,这表示失败。 |
Never | 容器永远不会重新启动 - 即使它失败了。 |
如下图所示,容器第一次终止时,会立即重新启动。 然而,下一次,Kubernetes 会等待十秒钟,然后再次重新启动。 然后,在每次后续终止后,此延迟会加倍到 20、40、80 秒,然后是 160 秒。 从此以后,延迟时间保持在五分钟。 这种在尝试之间加倍的延迟称为指数退避。
在最坏的情况下,容器可能会被阻止启动长达五分钟。
使用探活(liveness)探针(Probe)检查容器的健康状况
Liveness 探针
我们通过定义活性探针来检查应用程序是否仍然“活着”(正常运行且响应请求)。 我们可以为 pod 中的每个容器指定一个活性探针。 Kubernetes 定期运行探测器来询问应用程序是否仍然存在且运行良好。 如果应用程序没有响应、发生错误或响应是否定的,则容器被认为不健康并被终止。 如果重启策略允许,容器就会重新启动。
Kubernetes 可以使用以下三种机制之一来探测容器:
- HTTP GET 探针:通过指定的网络端口和路径上向容器的 IP 地址发送 GET 请求。 如果探针收到响应,并且响应代码不代表错误(换句话说,如果 HTTP 响应代码为 2xx 或 3xx),则认为探针成功。 如果服务器返回错误响应码,或者没有及时响应,则认为探测失败。
- TCP 套接字探针:通过尝试打开到容器指定端口的TCP 连接。 如果连接成功建立,则认为探测成功。 如果不能及时建立连接,则认为探测失败。
- Exec 探针:在容器内执行命令并检查其终止的退出代码。 如果退出代码为零,则探测成功。 非零退出代码被视为失败。 如果命令未能及时终止,则探测也被视为失败。
Liveness 有几个参数:
- initialDelaySeconds 决定了Kubernetes在启动容器后应该延迟多长时间执行第一个探测;
- periodSeconds 字段指定两次连续探测执行之间的时间量;
- timeoutSeconds 字段指定在探测尝试计数为失败之前等待响应的时间;
- failureThreshold 字段指定探测器必须失败多少次才能将容器视为不健康并可能重新启动。
对于没有实现 HTTP 运行状况检查端点的应用程序,应使用 tcpSocket 或 exec liveness 探针。
exec 探针
exec 的命令在容器内执行,因此必须在容器的文件系统上可用。如果该命令返回退出代码为零,则容器被认为是健康的。 如果它返回非零退出代码或未能在 timeoutSeconds 字段中指定的时间内完成,则认为探测失败;如过失败次数超过 failureThreshold 字段中的配置,则容器将立即终止(并根据重启策略重启或者停止),我们认为单个探测失败可以将容器视为不健康。
另外 exec 会启动一个临时容器,因此建议尽量使用 HTTP 或者 TCP Probe,减少因为健康检查带来的额外开销。同时 exec 脚本实现也应该相对比较简单,响应时间控制在 1s 以内,不适宜做业务测的复杂检查。
默认的 liveness probe 设置为应用程序提供了 20 到 30 秒的时间来开始响应探针请求。 如果应用程序启动时间较长,则会重新启动并且必须重新启动。 如果第二次启动也需要同样长的时间,则会再次重新启动。 如果这种情况持续下去,容器将永远不会达到 liveness probe 探测成功的状态,并陷入无限重启的循环。
为了防止这种情况,可以增加 initialDelaySeconds、periodSeconds 或 failureThreshold 设置来解决较长的启动时间,但这会对应用程序的正常运行产生负面影响。 periodSeconds * failureThreshold 的结果越高,如果应用程序变得不健康,重新启动应用程序所需的时间就越长(不再灵敏)。
为了处理应用程序的启动和稳态运行之间的差异,Kubernetes 还提供了startup probe。
如上图所示,与活性探针不同,启动探针失败是完全正常的。失败仅表明应用程序尚未完全启动。成功的启动探针表明应用程序已成功启动,Kubernetes 应该切换到活性探针。然后,通常会使用更短的时间执行活性探测,这样可以更快地检测无响应的应用程序。
通常 startup probe 和 liveness probe 配置为使用相同的 HTTP endpoint,当然了也可以使用不同的端点。 我们还可以将启动探针配置为 exec 或 tcpSocket 探针。
我们应该为所有 pod 定义 Liveness 探针。如果没有它,Kubernetes 除了检查应用程序进程是否已终止之外,无法知道您的应用程序是否仍然存活(进程在不代表运行健康)。
例如为了提供更好的活性检查,Web 应用程序通常会公开特定的运行状况检查端点,例如 /healthz。 调用此端点时,应用程序会对应用程序中运行的所有主要组件执行内部状态检查,以确保它们都没有死亡或不再执行其应做的事情。
/healthz 不应该使用鉴权,否则鉴权系统一旦故障,所有 Pod 的 liveness 检测都会失败,从而导致重启。
确保应用程序仅检查其内部组件的操作,而不检查受外部因素影响的情况。 例如,当前端服务的运行状况检查端点无法连接到后端服务时,它永远不应该响应失败。 如果后端服务出现故障,重启前端也无法解决问题。 这样的 liveness 探针重启后会再次失败,因此容器会反复重启,直到后端修复。 如果许多服务以这种方式相互依赖,那么单个服务的故障可能会导致整个系统的级联故障。
Liveness 探针调用的处理程序应当尽量轻量化,也不应花费太长时间才能完成。 默认情况下,探测会频繁地被执行,建议要在 1 秒钟内完成执行。
使用消耗大量 CPU 或内存的处理程序可能会严重影响容器的主进程。 探测处理程序调用消耗的 CPU 和内存会计入容器的资源配额,从而可能影响主程序的运行(CPU 节流或者容器被 OOM)。
探针的失败阈值是可配置的。因此不要在探针处理程序中实现重试,而应保持简单,可以使用 failureThreshold 来实现重试。
如果我们需要在容器启动和停止时执行自定义操作,可以使用如下钩子程序:
- PostStop hook,在容器启动时执行
- PreStop hook,在容器停止前执行
这些生命周期挂钩是按容器指定的,而不是在 pod 级别指定的 init 容器。下图可以帮助我们直观地了解这些钩子程序如何在容器的生命周期不同阶段执行。
与 Liveness 探针一样,lifecycle hook 可由如下方式实现:
- 在容器内执行命令(exec)
- 向容器中的应用程序发送 HTTP GET 请求
与 Liveness 探针相同,lifecycle hook 只能应用于常规容器,而不能应用于 init 容器。 与 liveness 探针不同,lifecycle hook 不支持 tcpSocket 方式。
使用 poststart hook 在容器启动时执行初始化操作
创建容器后将立即调用 poststart hook。 我们可以使用 exec 类型的 hook 在主进程启动时执行附加进程,也可以使用 httpGet 挂钩向容器中运行的应用程序发送 HTTP 请求以执行某种类型的初始化或预热过程 。
例如这里的例子中,我们使用 postStart hook (exec 方式)执行软件包的安装。
尽管 postStart hook 与主容器进程异步运行,但它以两种方式影响容器。
首先,容器保持在 Waiting 状态,reason 是 ContainerCreating,直到 postStart hook 调用完成。 Pod 的阶段为 Pending。 如果此时运行 kubectl logs 命令,即使容器正在运行,它也会拒绝显示日志。 kubectl port-forward 命令也拒绝将端口转发到 pod。
其次当 postStart hook 中使用的命令无法执行或返回非零退出代码时,整个容器将被重新启动。
使用 preStop hook 在容器终止之前运行自定义程序
相关内容:Grissom:容器技术回顾 - 什么是优雅关闭以及如何实现
preStop hook 会在容器终止之前立即执行。 要终止进程,通常会向该进程发送 TERM 信号。 这告诉应用程序完成它正在做的事情并关闭。 容器也是一样的方式。 每当容器需要停止或重新启动时,都会向容器中的主进程发送 TERM 信号。 不过在此之前,Kubernetes 首先执行 preStop hook(如果定义了)。 在 preStop hook 完成之前不会发送 TERM 信号,除非进程由于调用 preStop hook 处理程序本身而终止。
preStop hook 可用于启动容器的正常关闭或执行其他操作,而无需在应用程序中实现。 与 postStart hook 一样,可以在容器内执行命令,也可以向在容器中运行的应用程序发送 HTTP 请求。
Pod 生命周期包含三个阶段:
- 初始化阶段,Pod 的 init 容器运行。
- 运行阶段,Pod 的常规容器在该阶段运行。
- 终止阶段,Pod 的容器被终止。
- 拉取镜像
- 容器重启,如果失败,retry 时间会随次数加倍
- 容器重启后,根据镜像拉取策略,可能会再次拉取镜像
- 如果镜像拉取失败,会重试
如果 init 容器因错误而终止,并且 pod 的重启策略设置为 Always 或 OnFailure,则失败的 init 容器将重新启动。 如果该策略设置为 Never,则后续的 init 容器和 pod 的常规容器永远不会启动。 Pod 的状态无限期地显示为 Init:Error。我们必须删除并重新创建 pod 对象才能重新启动应用程序。
三种拉取镜像的策略
当所有 init 容器成功完成后,pod 的常规容器将全部并行创建。 理论上,每个容器的生命周期应该独立于 Pod 中的其他容器,但事实并非如此。
容器的 postStart hook 会阻止后续容器的创建
Kubelet 不会同时启动 pod 中的所有容器。 它按照 Pod 规范中定义的顺序同步创建和启动容器。 如果为容器定义了 postStart hook,则它与主容器进程异步运行,但 postStart hook 处理程序的执行会阻止后续容器的创建和启动(这个实现细节将来可能会改变).
相反,容器的终止是并行执行的。 长时间运行的 postStop hook 确实会阻止定义它的容器的关闭,但不会阻止其他容器的关闭。 容器的预停止钩子全部同时调用。
- 镜像从镜像仓库拉取成功
- 如果探活探针检测失败,会触发容器重启
- 如果容器不能优雅关闭,则在一定时间后会被强制关闭
- 容器重启后,会根据镜像拉取策略决定释放重新拉取镜像
- 当 Pod 被删除发起后,Pod 进入终止过程,等所有容器停止或者删除后,Pod 对象也被删除.
Pod 的容器会继续运行,直到 Pod 对象被删除,此时将启动 pod 中所有容器的终止过程,并将其状态更改为"Terminating"。
- 当执行 kubectl delete pod 开始后,Pod 终止过程开始了;
- 容器 A 的 preStop hook 停止了容器内的进程,因此就不需要 Kubelet 来发送 TERM 信号;
- 容器 B 没有定义 preStop hook,因此 Kubelet 直接发送了 TERM 信号;
- 容器 C 没有及时退出,因此会被直接杀死;
- 所有容器结束后,Pod 也结束了,随后会被杀死。
- Pod 的状态包含有关 Pod 的阶段(phase)、其条件(conditions)及其每个容器的状态(status)信息。 我们可以通过运行 kubectl describe 命令或使用命令 kubectl get -o yaml 获取完整的信息。
- 根据 Pod 的重启策略(restartPolicy),容器可以在终止后重新启动。当然了容器永远不会真正重新启动:旧容器将被销毁,并创建新容器。
- 如果容器被重复终止,则在每次重新启动之前都会插入指数增加的延迟。 第一次重新启动没有延迟,然后延迟 10 秒,在后续每次重新启动之前延迟加倍。 最大延迟时间为 5 分钟,当容器正常运行至少两次后,延迟时间会重置为零。
- 每次尝试下载容器映像失败后,也会让启动增加指数级的延迟。
- 将 liveness probe 添加到容器可确保容器在停止响应时重新启动。 liveness probe 通过 HTTP GET 请求、在容器中执行命令(exec)或打开 TCP 连接来检查应用程序的状态。
- 如果应用程序需要很长时间才能启动,则可以使用比 liveness probe 中更宽松的设置来定义 startup probe,以防止容器过早重新启动。
- 我们可以为每个 Pod 的主容器定义 lifecycle hook。 postStart hook 在容器启动时调用,而 preStop hook 在容器停止时调用。 Lifecycle hook 可以发送 HTTP GET 请求或在容器内执行命令。
- 如果在容器中定义了 preStop hook,在容器停止时,则首先调用该钩子程序。 然后 TERM 信号被发送到容器中的主进程。 如果进程在终止序列开始后的 terminationGracePeriodSeconds 内没有停止,则进程将被强制终止(kill -9)。
- 当我们删除 Pod 对象时,其所有容器将被同时终止。 Pod 的删除 GracePeriodSeconds 是给予容器关闭的时间。 默认情况下,它设置为 termination grace period,但可以使用 kubectl delete 命令覆盖。
- 如果关闭 pod 需要很长时间,则可能是其中运行的进程之一无法处理 TERM 信号。 添加 TERM 信号处理程序是比缩短终止或删除宽限期更好的解决方案。
原文:《Kubernetes In Action 2nd》,预计将于明年出版,本人已经购买,目前的版本是 MEAP Edition,上述内容来自原文 《6. Manging the Pod lifecycleUnderstanding the pod lifecycleManging the Pod lifecycleManging the Pod lifecycle》。