由云原生平台管理的容器化应用无法控制其生命周期,为了成为优秀的云原生公民,它们必须监听管理平台发出的事件,并相应地调整其生命周期。托管生命周期(Managed Lifecycle)模式描述了应用程序如何能够并且应该对这些生命周期事件做出反应。
问题描述
在第 4 章 “健康探针” 中,我们解释了为什么容器要为不同的健康检查提供 API。健康检查 API 是平台不断探测以获取应用洞察力的只读端点。它是平台从应用中提取信息的一种机制。
除了监控容器的状态外,平台有时可能会发出命令,并期望应用程序对这些命令做出反应。在策略和外部因素的驱动下,云原生平台可能会在任何时刻决定启动或停止其管理的应用程序。容器化应用要决定哪些事件是重要的,要做出反应以及如何反应。但实际上,这是一个 API,平台是用来和应用进行通信和发送命令的。另外,如果应用程序不需要这项服务,他们可以自由地从生命周期管理中获益,或者忽略它。
解决方案
我们看到,只检查进程状态并不能很好地显示应用程序的健康状况。这就是为什么有不同的 API 用于监控容器的健康状况。同样,只使用进程模型来运行和停止一个进程是不够好的。现实世界中的应用需要更多的细粒度交互和生命周期管理能力。有些应用需要帮助预热,有些应用需要一个温和而干净的关闭程序。对于这种和其他用例,如图 5-1 所示,一些事件由平台发出,容器可以监听并在需要时做出反应。
图 5-1 托管的容器生命周期
应用程序的部署单元是一个 Pod。正如你已经知道的,一个 Pod 是由一个或多个容器组成的。在 Pod 级别,还有其他的构造,如 Init 容器,我们将在第 14 章 “Init 容器” 中介绍,这些构造可以帮助管理容器生命周期。我们在本章描述的事件和钩子都是在单个容器级别而不是 Pod 级别应用的。
SIGTERM 信号
每当 Kubernetes 决定关闭一个容器时,无论是因为它所属的 Pod 正在关闭,还是仅仅是一个失败的存活探针导致容器被重新启动,容器都会收到一个 SIGTERM 信号。SIGTERM 是在 Kubernetes 发出更粗暴的 SIGKILL 信号之前,温柔地戳一下容器,让它干净利落地关闭。一旦收到 SIGTERM 信号,应用程序应该尽快关闭。对于一些应用程序来说,这可能是一个快速的关闭,而其他一些应用程序可能必须完成它们正在处理中的请求,释放开放的连接,并清理临时文件,这可能需要稍长的时间。在所有情况下,对 SIGTERM 做出反应是以干净的方式关闭容器的正确时刻。
SIGKILL 信号
如果容器进程在发出 SIGTERM 信号后还没有关闭,则会被下面的 SIGKILL 信号强行关闭。Kubernetes 不会立即发送 SIGKILL 信号,而是在发出 SIGTERM 信号后默认等待 30 秒的宽限期。这个宽限期可以使用 .spec.terminalGracePeriodSeconds
字段为每个 Pod 定义,但不能保证,因为它可以在向 Kubernetes 发出命令时被覆盖。这里的目的应该是设计和实现容器化应用,使其具有快速启动和关闭过程的短暂性。
Poststart 钩子
只使用过程信号来管理生命周期是有一定局限性的。这就是为什么 Kubernetes 提供了额外的生命周期钩子,如 postStart
和 preStop
。一个包含 postStart
钩子的 Pod 清单看起来像例 5-1 中的那个。
# 例 5-1 一个带有 postStart 钩子的容器
---
apiVersion: v1
kind: Pod
metadata:
name: post-start-hook
spec:
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
lifecycle:
postStart:
exec:
# postStart 命令在这里等待 30 秒,sleep 只是模拟这里可能运行的任何冗长的启动代码
# 另外,它在这里使用一个触发器文件与主应用程序同步,主应用程序是并行启动的
command:
- sh
- -c
- sleep 30 && echo "Wake up!" > /tmp/postStart_done
:::tips
postStart
命令在容器创建后执行,与主容器的进程异步。即使许多应用程序初始化和预热逻辑可以作为容器启动步骤的一部分来实现,postStart
仍然涵盖了一些用例。postStart
动作是一个阻塞调用,容器状态保持为 Waiting
,直到 postStart
处理程序完成,这又使 Pod 状态保持为 Pending
状态。postStart
的这种性质可以用来延迟容器的启动状态,同时给主容器进程初始化的时间。
:::
postStart
的另一个用途是当 Pod 不满足某些前提条件时,防止容器启动。例如,当 postStart
钩子通过返回一个非零的退出代码来指示错误时,主容器进程会被 Kubernetes 杀死。
postStart
和 preStop
钩子调用机制与第 4 章中描述的健康探针类似,并支持这些处理程序类型:
exec
:直接在容器中运行命令httpGet
:对一个 Pod 中容器打开的端口执行 HTTP GET 请求
:::danger
您必须非常小心地执行 postStart
钩子中的关键逻辑,因为它的执行没有保证。因为钩子是与容器进程并行运行的,所以钩子有可能在容器启动之前就被执行了。另外,钩子的目的是至少有一次语义,所以实现必须照顾到重复的执行。另一个需要注意的方面是,平台不会对没有到达处理程序的失败的 HTTP 请求进行任何重试尝试。
:::
Prestop 钩子
preStop
钩子是在容器被终止之前发送到容器的阻塞调用。它与 SIGTERM 信号具有相同的语义,在无法对 SIGTERM 作出反应时,应使用它来启动容器的优雅关闭。例 5-2 中的 preStop
动作必须在删除容器的调用被发送到容器运行时之前完成,后者会触发 SIGTERM 通知。
# 例 5-2 一个带有 preStop 钩子的容器
---
apiVersion: v1
kind: Pod
metadata:
name: pre-stop-hook
spec:
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
lifecycle:
preStop:
# 调用运行在应用程序中的 /shutdown 端点
httpGet:
port: 8080
path: /shutdown
即使 preStop
是阻塞的,按住它或返回一个不成功的结果并不能阻止容器被删除和进程被杀死。preStop
只是一个方便的替代 SIGTERM 信号来优雅地关闭应用程序,而不是更多。它还提供了与我们之前介绍的 postStart
钩子相同的处理程序类型和保证。
生命周期的其它控制手段
在本章中,到目前为止,我们已经关注了当容器生命周期事件发生时允许执行命令的钩子。但另一种机制不是在容器层面,而是在 Pod 层面,允许执行初始化指令。
我们在第 14 章 “Init 容器” 中会深入描述,但这里我们简单描述一下,将其与生命周期钩子进行比较。与常规的应用容器不同,Init 容器按顺序运行,一直运行到完成,并且在 Pod 中的任何应用容器启动之前运行。这些保证允许使用 Init 容器来完成 Pod 级的初始化任务。生命周期钩子和 Init 容器的运行粒度不同(分别在容器级和 Pod 级),可以在某些情况下相互变化使用,或者在其他情况下相互补充。表 5-1 总结了两者的主要区别。
表 5-1 生命周期钩子与 Init 容器的区别
除了当你需要特定的时间保证时,使用哪种机制没有严格的规定。我们可以完全跳过生命周期钩子和初始化容器,使用 Bash 脚本来执行特定的操作,作为容器启动或关闭命令的一部分。这是有可能的,但它会将容器与脚本紧密耦合,并将其变成维护的噩梦。
我们还可以使用 Kubernetes 生命周期钩子来执行本章所述的一些操作。另外,我们还可以更进一步,运行使用 Init 容器执行各个动作的容器。在这个序列中,这些选项越来越需要更多的努力,但同时也提供了更强的保障,并实现了重用。
理解容器和 Pod 生命周期的阶段和可用钩子对于创建受益于 Kubernetes 管理的应用程序是至关重要的。
一些讨论
云原生平台提供的主要好处之一是能够在潜在的不可靠的云基础设施之上可靠地、可预测地运行和扩展应用程序。这些平台为在其上运行的应用程序提供了一系列约束和合同。遵守这些合同以从云原生平台提供的所有功能中获益,符合应用程序的利益。处理和响应这些事件可确保您的应用程序可以优雅地启动和关闭,并将对消费服务的影响降到最低。目前,在其基本形式下,这意味着容器应该像任何设计良好的 POSIX 进程一样行为。在未来,可能会有更多的事件给应用程序提示,当它即将被放大,或要求释放资源以防止被关闭。必须要进入这样的思维模式,应用的生命周期不再由人控制,而是由平台完全自动化。
除了管理应用生命周期,像 Kubernetes 这样的编排平台的另一大职责就是将容器分布在节点群上。接下来的自动化部署(Automated Placement)模式,解释了从外部影响调度决策的选项。