K8S学习之旅(2)容器

225 阅读35分钟

镜像

容器镜像(Image)所承载的是封装了应用程序及其所有软件依赖的二进制数据。 容器镜像是可执行的软件包,可以单独运行;该软件包对所处的运行时环境具有明确定义的运行时环境假定。

镜像名称

容器镜像通常会被赋予 pauseexample/mycontainer 或者 kube-apiserver 这类的名称。 镜像名称也可以包含所在仓库的主机名。例如:fictional.registry.example/imagename。 还可以包含仓库的端口号,例如:fictional.registry.example:10443/imagename。 在镜像名称之后,你可以添加一个标签(Tag)  或 摘要(digest)  (与使用 docker 或 podman 等命令时的方式相同)。 使用标签能让你辨识同一镜像序列中的不同版本。 摘要是特定版本镜像的唯一标识符,是镜像内容的哈希值,不可变。 镜像标签可以包含小写字母、大写字母、数字、下划线(_)、句点(.)和连字符(-)。 它的长度最多为 128 个字符,并且必须遵循正则表达式模式:[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}。 你可以在 OCI 分发规范中阅读有关并找到验证正则表达式的更多信息。 如果你不指定标签,Kubernetes 认为你想使用标签 latest。 Kubernetes 可以使用的一些镜像名称示例包括:

  • busybox - 仅包含镜像名称,没有标签或摘要,Kubernetes 将使用 Docker 公共镜像仓库和 latest 标签。 (例如 docker.io/library/busybox:latest
  • busybox:1.32.0 - 带标签的镜像名称,Kubernetes 将使用 Docker 公共镜像仓库。 (例如 docker.io/library/busybox:1.32.0
  • registry.k8s.io/pause:latest - 带有自定义镜像仓库和 latest 标签的镜像名称。
  • registry.k8s.io/pause:3.5 - 带有自定义镜像仓库和非 latest 标签的镜像名称。
  • registry.k8s.io/pause@sha256:1ff6c18fbef2045af6b9c16bf034cc421a29027b800e4f9b68ae9b1cb3e9ae07 - 带摘要的镜像名称。
  • registry.k8s.io/pause:3.5@sha256:1ff6c18fbef2045af6b9c16bf034cc421a29027b800e4f9b68ae9b1cb3e9ae07 - 带有标签和摘要的镜像名称,镜像拉取仅参考摘要。

更新镜像

当你最初创建一个 Deployment、 StatefulSet、Pod 或者其他包含 Pod 模板的对象时,如果没有显式设定的话, Pod 中所有容器的默认镜像拉取策略是 IfNotPresent。这一策略会使得kubelet在镜像已经存在的情况下直接略过拉取镜像的操作。

镜像拉取策略

容器的 imagePullPolicy 和镜像的标签会影响kubelet尝试拉取(下载)指定的镜像。

以下列表包含了 imagePullPolicy 可以设置的值,以及这些值的效果:

  • IfNotPresent

  • 只有当镜像在本地不存在时才会拉取。

  • Always

  • 每当 kubelet 启动一个容器时,kubelet 会查询容器的镜像仓库, 将名称解析为一个镜像。 如果 kubelet 有一个容器镜像,并且对应的摘要已在本地缓存,kubelet 就会使用其缓存的镜像; 否则,kubelet 就会使用解析后的摘要拉取镜像,并使用该镜像来启动容器。

  • Never

  • kubelet 不会尝试获取镜像。如果镜像已经以某种方式存在本地, kubelet 会尝试启动容器;否则,会启动失败。 为了确保 Pod 总是使用相同版本的容器镜像,你可以指定镜像的摘要; 将 <image-name>:<tag> 替换为 <image-name>@<digest>,例如 image@sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2

当使用镜像标签时,如果镜像仓库修改了代码所对应的镜像标签,可能会出现新旧代码混杂在 Pod 中运行的情况。 镜像摘要唯一标识了镜像的特定版本,因此 Kubernetes 每次启动具有指定镜像名称和摘要的容器时,都会运行相同的代码。 通过摘要指定镜像可固定你运行的代码,这样镜像仓库的变化就不会导致版本的混杂。 有一些第三方的准入控制器在创建 Pod(和 Pod 模板)时产生变更,这样运行的工作负载就是根据镜像摘要,而不是标签来定义的。 无论镜像仓库上的标签发生什么变化,你都想确保你所有的工作负载都运行相同的代码,那么指定镜像摘要会很有用。

默认镜像拉取策略

当你(或控制器)向 API 服务器提交一个新的 Pod 时,你的集群会在满足特定条件时设置 imagePullPolicy 字段:

  • 如果你省略了 imagePullPolicy 字段,并且你为容器镜像指定了摘要, 那么 imagePullPolicy 会自动设置为 IfNotPresent

  • 如果你省略了 imagePullPolicy 字段,并且容器镜像的标签是 :latest, imagePullPolicy 会自动设置为 Always

  • 如果你省略了 imagePullPolicy 字段,并且没有指定容器镜像的标签, imagePullPolicy 会自动设置为 Always

  • 如果你省略了 imagePullPolicy 字段,并且为容器镜像指定了非 :latest 的标签, imagePullPolicy 就会自动设置为 IfNotPresent

说明:

容器的 imagePullPolicy 的值总是在对象初次创建时设置的, 如果后来镜像的标签或摘要发生变化,则不会更新。

例如,如果你用一个 :latest 的镜像标签创建一个 Deployment, 并在随后更新该 Deployment 的镜像标签为 :latest,则 imagePullPolicy 字段不会变成 Always。 你必须手动更改已经创建的资源的拉取策略。

必要的镜像拉取

如果你想总是强制执行拉取,你可以使用下述的一种方式:

  • 设置容器的 imagePullPolicy 为 Always
  • 省略 imagePullPolicy,并使用 :latest 作为镜像标签; 当你提交 Pod 时,Kubernetes 会将策略设置为 Always
  • 省略 imagePullPolicy 和镜像的标签; 当你提交 Pod 时,Kubernetes 会将策略设置为 Always
  • 启用准入控制器 AlwaysPullImages。

ImagePullBackOff

当 kubelet 使用容器运行时创建 Pod 时,容器可能因为 ImagePullBackOff 导致状态为 Waiting

ImagePullBackOff 状态意味着容器无法启动, 因为 Kubernetes 无法拉取容器镜像(原因包括无效的镜像名称,或从私有仓库拉取而没有 imagePullSecret)。 BackOff 部分表示 Kubernetes 将继续尝试拉取镜像,并增加回退延迟。

Kubernetes 会增加每次尝试之间的延迟,直到达到编译限制,即 300 秒(5 分钟)。

基于运行时类的镜像拉取

特性状态: Kubernetes v1.29 [alpha] (enabled by default: false)

Kubernetes 包含了根据 Pod 的 RuntimeClass 来执行镜像拉取的 Alpha 支持。

如果你启用了 RuntimeClassInImageCriApi 特性门控, kubelet 会通过一个元组(镜像名称,运行时处理程序)而不仅仅是镜像名称或镜像摘要来引用容器镜像。 你的容器运行时可能会根据选定的运行时处理程序调整其行为。 基于运行时类来拉取镜像对于基于 VM 的容器(如 Windows Hyper-V 容器)会有帮助。

串行和并行镜像拉取

默认情况下,kubelet 以串行方式拉取镜像。 也就是说,kubelet 一次只向镜像服务发送一个镜像拉取请求。 其他镜像拉取请求必须等待,直到正在处理的那个请求完成。

节点独立地做出镜像拉取的决策。即使你使用串行的镜像拉取,两个不同的节点也可以并行拉取相同的镜像。

如果你想启用并行镜像拉取,可以在 kubelet 配置中将字段 serializeImagePulls 设置为 false。

serializeImagePulls 设置为 false 时,kubelet 会立即向镜像服务发送镜像拉取请求,多个镜像将同时被拉动。

启用并行镜像拉取时,请确保你的容器运行时的镜像服务可以处理并行镜像拉取。

kubelet 从不代表一个 Pod 并行地拉取多个镜像。

例如,如果你有一个 Pod,它有一个初始容器和一个应用容器,那么这两个容器的镜像拉取将不会并行。 但是,如果你有两个使用不同镜像的 Pod,当启用并行镜像拉取时,kubelet 会代表两个不同的 Pod 并行拉取镜像。

最大并行镜像拉取数量

特性状态: Kubernetes v1.32 [beta]

当 serializeImagePulls 被设置为 false 时,kubelet 默认对同时拉取的最大镜像数量没有限制。 如果你想限制并行镜像拉取的数量,可以在 kubelet 配置中设置字段 maxParallelImagePulls。 当 maxParallelImagePulls 设置为 n 时,只能同时拉取 n 个镜像, 超过 n 的任何镜像都必须等到至少一个正在进行拉取的镜像拉取完成后,才能拉取。

当启用并行镜像拉取时,限制并行镜像拉取的数量可以防止镜像拉取消耗过多的网络带宽或磁盘 I/O。

你可以将 maxParallelImagePulls 设置为大于或等于 1 的正数。 如果将 maxParallelImagePulls 设置为大于等于 2,则必须将 serializeImagePulls 设置为 false。 kubelet 在无效的 maxParallelImagePulls 设置下会启动失败。

带镜像索引的多架构镜像

除了提供二进制的镜像之外, 容器仓库也可以提供容器镜像索引。 镜像索引可以指向镜像的多个镜像清单, 提供特定于体系结构版本的容器。 这背后的理念是让你可以为镜像命名(例如:pauseexample/mycontainerkube-apiserver) 的同时,允许不同的系统基于它们所使用的机器体系结构获取正确的二进制镜像。

Kubernetes 自身通常在命名容器镜像时添加后缀 -$(ARCH)。 为了向前兼容,请在生成较老的镜像时也提供后缀。 这里的理念是为某镜像(如 pause)生成针对所有平台都适用的清单时, 生成 pause-amd64 这类镜像,以便较老的配置文件或者将镜像后缀硬编码到其中的 YAML 文件也能兼容。

使用私有仓库

从私有仓库读取镜像时可能需要密钥。 凭据可以用以下方式提供:

  • 配置节点向私有仓库进行身份验证

    • 所有 Pod 均可读取任何已配置的私有仓库
    • 需要集群管理员配置节点
  • kubelet 凭据提供程序,动态获取私有仓库的凭据

    • kubelet 可以被配置为使用凭据提供程序 exec 插件来访问对应的私有镜像库
  • 预拉镜像

    • 所有 Pod 都可以使用节点上缓存的所有镜像
    • 需要所有节点的 root 访问权限才能进行设置
  • 在 Pod 中设置 ImagePullSecrets

    • 只有提供自己密钥的 Pod 才能访问私有仓库
  • 特定于厂商的扩展或者本地扩展

    • 如果你在使用定制的节点配置,你(或者云平台提供商)可以实现让节点向容器仓库认证的机制

下面将详细描述每一项。

用于认证镜像拉取的 kubelet 凭据提供程序

说明:

此方法尤其适合 kubelet 需要动态获取仓库凭据时。 最常用于由云提供商提供的仓库,其中身份认证令牌的生命期是短暂的。

你可以配置 kubelet,以调用插件可执行文件的方式来动态获取容器镜像的仓库凭据。 这是为私有仓库获取凭据最稳健和最通用的方法,但也需要 kubelet 级别的配置才能启用。

config.json 说明

对于 config.json 的解释在原始 Docker 实现和 Kubernetes 的解释之间有所不同。 在 Docker 中,auths 键只能指定根 URL,而 Kubernetes 允许 glob URL 以及前缀匹配的路径。 唯一的限制是 glob 模式(*)必须为每个子域名包括点(.)。 匹配的子域名数量必须等于 glob 模式(*.)的数量,例如:

  • *.kubernetes.io 会匹配 kubernetes.io,但会匹配 abc.kubernetes.io
  • *.*.kubernetes.io 会匹配 abc.kubernetes.io,但会匹配 abc.def.kubernetes.io
  • prefix.*.io 将匹配 prefix.kubernetes.io
  • *-good.kubernetes.io 将匹配 prefix-good.kubernetes.io

这意味着,像这样的 config.json 是有效的:

{
    "auths": {
        "my-registry.io/images": { "auth": "…" },
        "*.my-registry.io/images": { "auth": "…" }
    }
}

现在镜像拉取操作会将每种有效模式的凭据都传递给 CRI 容器运行时。例如下面的容器镜像名称会匹配成功:

  • my-registry.io/images
  • my-registry.io/images/my-image
  • my-registry.io/images/another-image
  • sub.my-registry.io/images/my-image

但这些不会匹配成功:

  • a.sub.my-registry.io/images/my-image
  • a.b.sub.my-registry.io/images/my-image

kubelet 为每个找到的凭据的镜像按顺序拉取。这意味着对于不同的路径在 config.json 中也可能有多项:

{
    "auths": {
        "my-registry.io/images": {
            "auth": "…"
        },
        "my-registry.io/images/subpath": {
            "auth": "…"
        }
    }
}

如果一个容器指定了要拉取的镜像 my-registry.io/images/subpath/my-image, 并且其中一个失败,kubelet 将尝试从另一个身份验证源下载镜像。

提前拉取镜像

说明:

该方法适用于你能够控制节点配置的场合。 如果你的云供应商负责管理节点并自动置换节点,这一方案无法可靠地工作。

默认情况下,kubelet 会尝试从指定的仓库拉取每个镜像。 但是,如果容器属性 imagePullPolicy 设置为 IfNotPresent 或者 Never, 则会优先使用(对应 IfNotPresent)或者一定使用(对应 Never)本地镜像。

如果你希望使用提前拉取镜像的方法代替仓库认证,就必须保证集群中所有节点提前拉取的镜像是相同的。

这一方案可以用来提前载入指定的镜像以提高速度,或者作为向私有仓库执行身份认证的一种替代方案。

所有的 Pod 都可以使用节点上提前拉取的镜像。

在 Pod 上指定 ImagePullSecrets

说明:

运行使用私有仓库中镜像的容器时,建议使用这种方法。

Kubernetes 支持在 Pod 中设置容器镜像仓库的密钥。 imagePullSecrets 必须全部与 Pod 位于同一个名字空间中。 引用的 Secret 必须是 kubernetes.io/dockercfg 或 kubernetes.io/dockerconfigjson 类型。

使用 Docker Config 创建 Secret

你需要知道用于向仓库进行身份验证的用户名、密码和客户端电子邮件地址,以及它的主机名。 运行以下命令,注意替换适当的大写值:

kubectl create secret docker-registry <name> \
  --docker-server=DOCKER_REGISTRY_SERVER \
  --docker-username=DOCKER_USER \
  --docker-password=DOCKER_PASSWORD \
  --docker-email=DOCKER_EMAIL

如果你已经有 Docker 凭据文件,则可以将凭据文件导入为 Kubernetes Secret, 而不是执行上面的命令。 基于已有的 Docker 凭据创建 Secret 解释了如何完成这一操作。

如果你在使用多个私有容器仓库,这种技术将特别有用。 原因是 kubectl create secret docker-registry 创建的是仅适用于某个私有仓库的 Secret。

说明:

Pod 只能引用位于自身所在名字空间中的 Secret,因此需要针对每个名字空间重复执行上述过程。

在 Pod 中引用 ImagePullSecrets

现在,在创建 Pod 时,可以在 Pod 定义中增加 imagePullSecrets 部分来引用该 Secret。 imagePullSecrets 数组中的每一项只能引用同一名字空间中的 Secret。

例如:

cat <<EOF > pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: foo
  namespace: awesomeapps
spec:
  containers:
    - name: foo
      image: janedoe/awesomeapp:v1
  imagePullSecrets:
    - name: myregistrykey
EOF

cat <<EOF >> ./kustomization.yaml
resources:
- pod.yaml
EOF

你需要对使用私有仓库的每个 Pod 执行以上操作。不过, 设置该字段的过程也可以通过为服务账号资源设置 imagePullSecrets 来自动完成。

你也可以将此方法与节点级别的 .docker/config.json 配置结合使用。 来自不同来源的凭据会被合并。

使用案例

配置私有仓库有多种方案,以下是一些常用场景和建议的解决方案。

  1. 集群运行非专有镜像(例如,开源镜像)。镜像不需要隐藏。

    • 使用来自公共仓库的公共镜像

      • 无需配置
      • 某些云厂商会自动为公开镜像提供高速缓存,以便提升可用性并缩短拉取镜像所需时间
  1. 集群运行一些专有镜像,这些镜像需要对公司外部隐藏,对所有集群用户可见

    • 使用托管的私有仓库

      • 在需要访问私有仓库的节点上可能需要手动配置
    • 或者,在防火墙内运行一个组织内部的私有仓库,并开放读取权限

      • 不需要配置 Kubernetes
    • 使用控制镜像访问的托管容器镜像仓库服务

      • 与手动配置节点相比,这种方案能更好地处理集群自动扩缩容
    • 或者,在不方便更改节点配置的集群中,使用 imagePullSecrets

  1. 集群使用专有镜像,且有些镜像需要更严格的访问控制

    • 确保 AlwaysPullImages 准入控制器被启用。 否则,所有 Pod 都可以使用所有镜像。
    • 确保将敏感数据存储在 Secret 资源中,而不是将其打包在镜像里。
  1. 集群是多租户的并且每个租户需要自己的私有仓库

    • 确保 AlwaysPullImages 准入控制器。 否则,所有租户的所有的 Pod 都可以使用所有镜像。
    • 为私有仓库启用鉴权。
    • 为每个租户生成访问仓库的凭据,放置在 Secret 中,并将 Secret 发布到各租户的名字空间下。
    • 租户将 Secret 添加到每个名字空间中的 imagePullSecrets。

如果你需要访问多个仓库,可以为每个仓库创建一个 Secret。

容器环境

本页描述了在容器环境里容器可用的资源。

容器环境

Kubernetes 的容器环境给容器提供了几个重要的资源:

  • 文件系统,其中包含一个镜像和一个或多个的卷
  • 容器自身的信息
  • 集群中其他对象的信息

容器信息

一个容器的 hostname 是该容器运行所在的 Pod 的名称。通过 hostname 命令或者调用 libc 中的 gethostname 函数可以获取该名称。

Pod 名称和命名空间可以通过下行 API转换为环境变量。

Pod 定义中的用户所定义的环境变量也可在容器中使用,就像在 container 镜像中静态指定的任何环境变量一样。

集群信息

创建容器时正在运行的所有服务都可用作该容器的环境变量。 这里的服务仅限于新容器的 Pod 所在的名字空间中的服务,以及 Kubernetes 控制面的服务。

对于名为 foo 的服务,当映射到名为 bar 的容器时,定义了以下变量:

FOO_SERVICE_HOST=<其上服务正运行的主机>
FOO_SERVICE_PORT=<其上服务正运行的端口>

服务具有专用的 IP 地址。如果启用了DNS 插件, 可以在容器中通过 DNS 来访问服务。

容器运行时类(Runtime Class)

特性状态: Kubernetes v1.20 [stable]

本页面描述了 RuntimeClass 资源和运行时的选择机制。

RuntimeClass 是一个用于选择容器运行时配置的特性,容器运行时配置用于运行 Pod 中的容器。

动机

你可以在不同的 Pod 设置不同的 RuntimeClass,以提供性能与安全性之间的平衡。 例如,如果你的部分工作负载需要高级别的信息安全保证,你可以决定在调度这些 Pod 时尽量使它们在使用硬件虚拟化的容器运行时中运行。 这样,你将从这些不同运行时所提供的额外隔离中获益,代价是一些额外的开销。

你还可以使用 RuntimeClass 运行具有相同容器运行时但具有不同设置的 Pod。

设置

  1. 在节点上配置 CRI 的实现(取决于所选用的运行时)
  2. 创建相应的 RuntimeClass 资源

1. 在节点上配置 CRI 实现

RuntimeClass 的配置依赖于运行时接口(CRI)的实现。

说明:

RuntimeClass 假设集群中的节点配置是同构的(换言之,所有的节点在容器运行时方面的配置是相同的)。 如果需要支持异构节点,配置方法请参阅下面的调度。

所有这些配置都具有相应的 handler 名,并被 RuntimeClass 引用。 handler 必须是有效的DNS 标签名

2. 创建相应的 RuntimeClass 资源

在上面步骤 1 中,每个配置都需要有一个用于标识配置的 handler。 针对每个 handler 需要创建一个 RuntimeClass 对象。

RuntimeClass 资源当前只有两个重要的字段:RuntimeClass 名 (metadata.name) 和 handler (handler)。 对象定义如下所示:

# RuntimeClass 定义于 node.k8s.io API 组
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
  # 用来引用 RuntimeClass 的名字
  # RuntimeClass 是一个集群层面的资源
  name: myclass
# 对应的 CRI 配置的名称
handler: myconfiguration

RuntimeClass 对象的名称必须是有效的DNS 子域名。

说明:

建议将 RuntimeClass 写操作(create、update、patch 和 delete)限定于集群管理员使用。 通常这是默认配置。参阅授权概述了解更多信息。

使用说明

一旦完成集群中 RuntimeClasses 的配置, 你可以在 Pod spec 中指定 runtimeClassName 来使用它。例如:

apiVersion: v1
kind: Pod
metadata:
  name: mypod
spec:
  runtimeClassName: myclass
  # ...

这一设置会告诉 kubelet 使用所指的 RuntimeClass 来运行该 Pod。 如果所指的 RuntimeClass 不存在或者 CRI 无法运行相应的 handler, 那么 Pod 将会进入 Failed 终止阶段。 你可以查看相应的事件, 获取执行过程中的错误信息。

如果未指定 runtimeClassName,则将使用默认的 RuntimeHandler,相当于禁用 RuntimeClass 功能特性。

CRI 配置

containerd

通过 containerd 的 /etc/containerd/config.toml 配置文件来配置运行时 handler。 handler 需要配置在 runtimes 块中:

[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.${HANDLER_NAME}]
CRI-O

通过 CRI-O 的 /etc/crio/crio.conf 配置文件来配置运行时 handler。 handler 需要配置在 crio.runtime 表之下:

[crio.runtime.runtimes.${HANDLER_NAME}]
  runtime_path = "${PATH_TO_BINARY}"

调度

特性状态: Kubernetes v1.16 [beta]

通过为 RuntimeClass 指定 scheduling 字段, 你可以通过设置约束,确保运行该 RuntimeClass 的 Pod 被调度到支持该 RuntimeClass 的节点上。 如果未设置 scheduling,则假定所有节点均支持此 RuntimeClass。

为了确保 pod 会被调度到支持指定运行时的 node 上,每个 node 需要设置一个通用的 label 用于被 runtimeclass.scheduling.nodeSelector 挑选。在 admission 阶段,RuntimeClass 的 nodeSelector 将会与 Pod 的 nodeSelector 合并,取二者的交集。如果有冲突,Pod 将会被拒绝。

如果 node 需要阻止某些需要特定 RuntimeClass 的 Pod,可以在 tolerations 中指定。 与 nodeSelector 一样,tolerations 也在 admission 阶段与 Pod 的 tolerations 合并,取二者的并集。

更多有关 node selector 和 tolerations 的配置信息,请查阅将 Pod 分派到节点。

Pod 开销

特性状态: Kubernetes v1.24 [stable]

你可以指定与运行 Pod 相关的开销资源。声明开销即允许集群(包括调度器)在决策 Pod 和资源时将其考虑在内。

Pod 开销通过 RuntimeClass 的 overhead 字段定义。 通过使用这个字段,你可以指定使用该 RuntimeClass 运行 Pod 时的开销并确保 Kubernetes 将这些开销计算在内。

容器生命周期回调

这个页面描述了 kubelet 管理的容器如何使用容器生命周期回调框架, 藉由其管理生命周期中的事件触发,运行指定代码。

概述

类似于许多具有生命周期回调组件的编程语言框架,例如 Angular、Kubernetes 为容器提供了生命周期回调。 回调使容器能够了解其管理生命周期中的事件,并在执行相应的生命周期回调时运行在处理程序中实现的代码。

容器回调

有两个回调暴露给容器:

PostStart

这个回调在容器被创建之后立即被执行。 但是,不能保证回调会在容器入口点(ENTRYPOINT)之前执行。 没有参数传递给处理程序。

PreStop

在容器因 API 请求或者管理事件(诸如存活态探针、启动探针失败、资源抢占、资源竞争等) 而被终止之前,此回调会被调用。 如果容器已经处于已终止或者已完成状态,则对 preStop 回调的调用将失败。 在用来停止容器的 TERM 信号被发出之前,回调必须执行结束。 Pod 的终止宽限周期在 PreStop 回调被执行之前即开始计数, 所以无论回调函数的执行结果如何,容器最终都会在 Pod 的终止宽限期内被终止。 没有参数会被传递给处理程序。

有关终止行为的更详细描述, 请参见终止 Pod。

回调处理程序的实现

容器可以通过实现和注册该回调的处理程序来访问该回调。 针对容器,有三种类型的回调处理程序可供实现:

  • Exec - 在容器的 cgroups 和名字空间中执行特定的命令(例如 pre-stop.sh)。 命令所消耗的资源计入容器的资源消耗。
  • HTTP - 对容器上的特定端点执行 HTTP 请求。
  • Sleep - 将容器暂停一段指定的时间。 这是由 PodLifecycleSleepAction 特性门控默认启用的 Beta 级特性。
说明:

如果你想为 Sleep 生命周期回调设置零秒的睡眠持续时间(实际上是一个 no-op), 可以启用 PodLifecycleSleepActionAllowZero 特性门控。

回调处理程序执行

当调用容器生命周期管理回调时,Kubernetes 管理系统根据回调动作执行其处理程序, httpGettcpSocket 和 sleep 由 kubelet 进程执行,而 exec 在容器中执行。

当容器创建时,会调用 PostStart 回调程序, 这意味着容器的 ENTRYPOINT 和 PostStart 回调会同时触发。然而, 如果 PostStart 回调程序执行时间过长或挂起,它可能会阻止容器进入 running 状态。

PreStop 回调并不会与停止容器的信号处理程序异步执行;回调必须在可以发送信号之前完成执行。 如果 PreStop 回调在执行期间停滞不前,Pod 的阶段会变成 Terminating并且一直处于该状态, 直到其 terminationGracePeriodSeconds 耗尽为止,这时 Pod 会被杀死。 这一宽限期是针对 PreStop 回调的执行时间及容器正常停止时间的总和而言的。 例如,如果 terminationGracePeriodSeconds 是 60,回调函数花了 55 秒钟完成执行, 而容器在收到信号之后花了 10 秒钟来正常结束,那么容器会在其能够正常结束之前即被杀死, 因为 terminationGracePeriodSeconds 的值小于后面两件事情所花费的总时间(55+10)。

如果 PostStart 或 PreStop 回调失败,它会杀死容器。

用户应该使他们的回调处理程序尽可能的轻量级。 但也需要考虑长时间运行的命令也很有用的情况,比如在停止容器之前保存状态。

回调递送保证

回调的递送应该是至少一次,这意味着对于任何给定的事件, 例如 PostStart 或 PreStop,回调可以被调用多次。 如何正确处理被多次调用的情况,是回调实现所要考虑的问题。

通常情况下,只会进行单次递送。 例如,如果 HTTP 回调接收器宕机,无法接收流量,则不会尝试重新发送。 然而,偶尔也会发生重复递送的可能。 例如,如果 kubelet 在发送回调的过程中重新启动,回调可能会在 kubelet 恢复后重新发送。

调试回调处理程序

回调处理程序的日志不会在 Pod 事件中公开。 如果处理程序由于某种原因失败,它将播放一个事件。 对于 PostStart,这是 FailedPostStartHook 事件,对于 PreStop,这是 FailedPreStopHook 事件。 要自己生成失败的 FailedPostStartHook 事件,请修改lifecycle-events.yaml文件将 postStart 命令更改为 “badcommand” 并应用它。 以下是通过运行 kubectl describe pod lifecycle-demo 后你看到的一些结果事件的示例输出:

Events:
  Type     Reason               Age              From               Message
  ----     ------               ----             ----               -------
  Normal   Scheduled            7s               default-scheduler  Successfully assigned default/lifecycle-demo to ip-XXX-XXX-XX-XX.us-east-2...
  Normal   Pulled               6s               kubelet            Successfully pulled image "nginx" in 229.604315ms
  Normal   Pulling              4s (x2 over 6s)  kubelet            Pulling image "nginx"
  Normal   Created              4s (x2 over 5s)  kubelet            Created container lifecycle-demo-container
  Normal   Started              4s (x2 over 5s)  kubelet            Started container lifecycle-demo-container
  Warning  FailedPostStartHook  4s (x2 over 5s)  kubelet            Exec lifecycle hook ([badcommand]) for Container "lifecycle-demo-container" in Pod "lifecycle-demo_default(30229739-9651-4e5a-9a32-a8f1688862db)" failed - error: command 'badcommand' exited with 126: , message: "OCI runtime exec failed: exec failed: container_linux.go:380: starting container process caused: exec: "badcommand": executable file not found in $PATH: unknown\r\n"
  Normal   Killing              4s (x2 over 5s)  kubelet            FailedPostStartHook
  Normal   Pulled               4s               kubelet            Successfully pulled image "nginx" in 215.66395ms
  Warning  BackOff              2s (x2 over 3s)  kubelet            Back-off restarting failed container

Pod 的生命周期

本页面讲述 Pod 的生命周期。 Pod 遵循预定义的生命周期,起始于 Pending 阶段, 如果至少其中有一个主要容器正常启动,则进入 Running,之后取决于 Pod 中是否有容器以失败状态结束而进入 Succeeded 或者 Failed 阶段。

和一个个独立的应用容器一样,Pod 也被认为是相对临时性(而不是长期存在)的实体。 Pod 会被创建、赋予一个唯一的 ID(UID), 并被调度到节点,并在终止(根据重启策略)或删除之前一直运行在该节点。 如果一个节点死掉了,调度到该节点的 Pod 也被计划在给定超时期限结束后删除

Pod 生命期

在 Pod 运行期间,kubelet 能够重启容器以处理一些失效场景。 在 Pod 内部,Kubernetes 跟踪不同容器的状态并确定使 Pod 重新变得健康所需要采取的动作。

在 Kubernetes API 中,Pod 包含规约部分和实际状态部分。 Pod 对象的状态包含了一组 Pod 状况(Conditions)。 如果应用需要的话,你也可以向其中注入自定义的就绪态信息

Pod 在其生命周期中只会被调度一次。 将 Pod 分配到特定节点的过程称为绑定,而选择使用哪个节点的过程称为调度。 一旦 Pod 被调度并绑定到某个节点,Kubernetes 会尝试在该节点上运行 Pod。 Pod 会在该节点上运行,直到 Pod 停止或者被终止; 如果 Kubernetes 无法在选定的节点上启动 Pod(例如,如果节点在 Pod 启动前崩溃), 那么特定的 Pod 将永远不会启动。

你可以使用 Pod 调度就绪态来延迟 Pod 的调度,直到所有的调度门控都被移除。 例如,你可能想要定义一组 Pod,但只有在所有 Pod 都被创建完成后才会触发调度。

Pod 和故障恢复

如果 Pod 中的某个容器失败,Kubernetes 可能会尝试重启特定的容器。 有关细节参阅 Pod 如何处理容器问题

然而,Pod 也可能以集群无法恢复的方式失败,在这种情况下,Kubernetes 不会进一步尝试修复 Pod; 相反,Kubernetes 会删除 Pod 并依赖其他组件提供自动修复。

如果 Pod 被调度到某个节点而该节点之后失效, Pod 会被视为不健康,最终 Kubernetes 会删除 Pod。 Pod 无法在因节点资源耗尽或者节点维护而被驱逐期间继续存活。

Kubernetes 使用一种高级抽象来管理这些相对而言可随时丢弃的 Pod 实例, 称作控制器

任何给定的 Pod (由 UID 定义)从不会被“重新调度(rescheduled)”到不同的节点; 相反,这一 Pod 可以被一个新的、几乎完全相同的 Pod 替换掉。 如果你创建一个替换 Pod,它甚至可以拥有与旧 Pod 相同的名称(如 .metadata.name), 但替换 Pod 将具有与旧 Pod 不同的 .metadata.uid

Kubernetes 不保证现有 Pod 的替换 Pod 会被调度到与被替换的旧 Pod 相同的节点。

关联的生命期

如果某物声称其生命期与某 Pod 相同,例如存储, 这就意味着该对象在此 Pod (UID 亦相同)存在期间也一直存在。 如果 Pod 因为任何原因被删除,甚至某完全相同的替代 Pod 被创建时, 这个相关的对象(例如这里的卷)也会被删除并重建。

一个包含文件拉取程序 Sidecar(边车) 和 Web 服务器的多容器 Pod。此 Pod 使用临时 emptyDir 卷作为容器之间的共享存储。

图 1

一个包含文件拉取程序 Sidecar(边车) 和 Web 服务器的多容器 Pod。此 Pod 使用临时 emptyDir 卷作为容器之间的共享存储。

Pod 阶段

Pod 的 status 字段是一个 PodStatus 对象,其中包含一个 phase 字段。

Pod 的阶段(Phase)是 Pod 在其生命周期中所处位置的简单宏观概述。 该阶段并不是对容器或 Pod 状态的综合汇总,也不是为了成为完整的状态机。

Pod 阶段的数量和含义是严格定义的。 除了本文档中列举的内容外,不应该再假定 Pod 有其他的 phase 值。

下面是 phase 可能的值:

取值描述
Pending(悬决)Pod 已被 Kubernetes 系统接受,但有一个或者多个容器尚未创建亦未运行。此阶段包括等待 Pod 被调度的时间和通过网络下载镜像的时间。
Running(运行中)Pod 已经绑定到了某个节点,Pod 中所有的容器都已被创建。至少有一个容器仍在运行,或者正处于启动或重启状态。
Succeeded(成功)Pod 中的所有容器都已成功结束,并且不会再重启。
Failed(失败)Pod 中的所有容器都已终止,并且至少有一个容器是因为失败终止。也就是说,容器以非 0 状态退出或者被系统终止,且未被设置为自动重启。
Unknown(未知)因为某些原因无法取得 Pod 的状态。这种情况通常是因为与 Pod 所在主机通信失败。
说明:

当 Pod 反复启动失败时,某些 kubectl 命令的 Status 字段中可能会出现 CrashLoopBackOff。 同样,当 Pod 被删除时,某些 kubectl 命令的 Status 字段中可能会出现 Terminating

确保不要将 Status(kubectl 用于用户直觉的显示字段)与 Pod 的 phase 混淆。 Pod 阶段(phase)是 Kubernetes 数据模型和 Pod API 的一个明确的部分。

NAMESPACE               NAME               READY   STATUS             RESTARTS   AGE
alessandras-namespace   alessandras-pod    0/1     CrashLoopBackOff   200        2d9h

Pod 被赋予一个可以体面终止的期限,默认为 30 秒。 你可以使用 --force 参数来强制终止 Pod

从 Kubernetes 1.27 开始,除了静态 Pod 和没有 Finalizer 的强制终止 Pod 之外,kubelet 会将已删除的 Pod 转换到终止阶段 (Failed 或 Succeeded 具体取决于 Pod 容器的退出状态),然后再从 API 服务器中删除。

如果某节点死掉或者与集群中其他节点失联,Kubernetes 会实施一种策略,将失去的节点上运行的所有 Pod 的 phase 设置为 Failed

容器状态

Kubernetes 会跟踪 Pod 中每个容器的状态,就像它跟踪 Pod 总体上的阶段一样。 你可以使用容器生命周期回调 来在容器生命周期中的特定时间点触发事件。

一旦调度器将 Pod 分派给某个节点,kubelet 就通过容器运行时开始为 Pod 创建容器。容器的状态有三种:Waiting(等待)、Running(运行中)和 Terminated(已终止)。

要检查 Pod 中容器的状态,你可以使用 kubectl describe pod <pod 名称>。 其输出中包含 Pod 中每个容器的状态。

每种状态都有特定的含义:

Waiting(等待)

如果容器并不处在 Running 或 Terminated 状态之一,它就处在 Waiting 状态。 处于 Waiting 状态的容器仍在运行它完成启动所需要的操作:例如, 从某个容器镜像仓库拉取容器镜像,或者向容器应用 Secret 数据等等。 当你使用 kubectl 来查询包含 Waiting 状态的容器的 Pod 时,你也会看到一个 Reason 字段,其中给出了容器处于等待状态的原因。

Running(运行中)

Running 状态表明容器正在执行状态并且没有问题发生。 如果配置了 postStart 回调,那么该回调已经执行且已完成。 如果你使用 kubectl 来查询包含 Running 状态的容器的 Pod 时, 你也会看到关于容器进入 Running 状态的信息。

Terminated(已终止)

处于 Terminated 状态的容器开始执行后,或者运行至正常结束或者因为某些原因失败。 如果你使用 kubectl 来查询包含 Terminated 状态的容器的 Pod 时, 你会看到容器进入此状态的原因、退出代码以及容器执行期间的起止时间。

如果容器配置了 preStop 回调,则该回调会在容器进入 Terminated 状态之前执行。

Pod 如何处理容器问题

Kubernetes 通过在 Pod spec 中定义的 restartPolicy 管理 Pod 内容器出现的失效。 该策略决定了 Kubernetes 如何对由于错误或其他原因而退出的容器做出反应,其顺序如下:

  1. 最初的崩溃:Kubernetes 尝试根据 Pod 的 restartPolicy 立即重新启动。
  2. 反复的崩溃:在最初的崩溃之后,Kubernetes 对于后续重新启动的容器采用指数级回退延迟机制, 如 restartPolicy 中所述。 这一机制可以防止快速、重复的重新启动尝试导致系统过载。
  3. CrashLoopBackOff 状态:这一状态表明,对于一个给定的、处于崩溃循环、反复失效并重启的容器, 回退延迟机制目前正在生效。
  4. 回退重置:如果容器成功运行了一定时间(如 10 分钟), Kubernetes 会重置回退延迟机制,将新的崩溃视为第一次崩溃。

在实际部署中,CrashLoopBackOff 是在描述或列出 Pod 时从 kubectl 命令输出的一种状况或事件。 当 Pod 中的容器无法正常启动,并反复进入尝试与失败的循环时就会出现。

换句话说,当容器进入崩溃循环时,Kubernetes 会应用容器重启策略 中提到的指数级回退延迟机制。这种机制可以防止有问题的容器因不断进行启动失败尝试而导致系统不堪重负。

下列问题可以导致 CrashLoopBackOff

  • 应用程序错误导致的容器退出。
  • 配置错误,如环境变量不正确或配置文件丢失。
  • 资源限制,容器可能没有足够的内存或 CPU 正常启动。
  • 如果应用程序没有在预期时间内启动服务,健康检查就会失败。
  • 容器的存活探针或者启动探针返回 失败 结果,如探针部分所述。

要调查 CrashLoopBackOff 问题的根本原因,用户可以:

  1. 检查日志:使用 kubectl logs <pod名称> 检查容器的日志。 这通常是诊断导致崩溃的问题的最直接方法。
  2. 检查事件:使用 kubectl describe pod <pod名称> 查看 Pod 的事件, 这可以提供有关配置或资源问题的提示。
  3. 审查配置:确保 Pod 配置正确无误,包括环境变量和挂载卷,并且所有必需的外部资源都可用。
  4. 检查资源限制: 确保容器被分配了足够的 CPU 和内存。有时,增加 Pod 定义中的资源可以解决问题。
  5. 调试应用程序:应用程序代码中可能存在错误或配置不当。 在本地或开发环境中运行此容器镜像有助于诊断应用程序的特定问题。

容器重启策略

Pod 的 spec 中包含一个 restartPolicy 字段,其可能取值包括 Always、OnFailure 和 Never。默认值是 Always。

restartPolicy 应用于 Pod 中的应用容器和常规的 Init 容器。 Sidecar 容器忽略 Pod 级别的 restartPolicy 字段:在 Kubernetes 中,Sidecar 被定义为 initContainers 内的一个条目,其容器级别的 restartPolicy 被设置为 Always。 对于因错误而退出的 Init 容器,如果 Pod 级别 restartPolicy 为 OnFailure 或 Always, 则 kubelet 会重新启动 Init 容器。

  • Always:只要容器终止就自动重启容器。
  • OnFailure:只有在容器错误退出(退出状态非零)时才重新启动容器。
  • Never:不会自动重启已终止的容器。

当 kubelet 根据配置的重启策略处理容器重启时,仅适用于同一 Pod 内替换容器并在同一节点上运行的重启。当 Pod 中的容器退出时,kubelet 会以指数级回退延迟机制(10 秒、20 秒、40 秒......)重启容器, 上限为 300 秒(5 分钟)。一旦容器顺利执行了 10 分钟, kubelet 就会重置该容器的重启延迟计时器。 Sidecar 容器和 Pod 生命周期中解释了 init containers 在指定 restartpolicy 字段时的行为。

可配置的容器重启延迟

特性状态: Kubernetes v1.32 [alpha] (enabled by default: false)

启用 Alpha 特性门控 KubeletCrashLoopBackOffMax 后, 你可以重新配置容器启动重试之间的最大延迟,默认值为 300 秒(5 分钟)。 此配置是针对每个节点使用 kubelet 配置进行设置的。 在你的 kubelet 配置中, 在 crashLoopBackOff 下设置 maxContainerRestartPeriod 字段,取值范围在 "1s" 到 "300s" 之间。 如上文容器重启策略所述,该节点上的延迟仍将从 10 秒开始,并在每次重启后以指数方式增加 2 倍,但现在其上限将被限制为你所配置的最大值。如果你配置的 maxContainerRestartPeriod 小于默认初始值 10 秒, 则初始延迟将被设置为配置的最大值。

参见以下 kubelet 配置示例:

# 容器重启延迟将从 10 秒开始,每次重启增加 2 倍
# 最高达到 100 秒
kind: KubeletConfiguration
crashLoopBackOff:
    maxContainerRestartPeriod: "100s"
# 容器重启之间的延迟将始终为 2 秒
kind: KubeletConfiguration
crashLoopBackOff:
    maxContainerRestartPeriod: "2s"

Pod 状况

Pod 有一个 PodStatus 对象,其中包含一个 PodConditions 数组。Pod 可能通过也可能未通过其中的一些状况测试。 Kubelet 管理以下 PodCondition:

  • PodScheduled:Pod 已经被调度到某节点;
  • PodReadyToStartContainers:Pod 沙箱被成功创建并且配置了网络(Beta 特性,默认启用);
  • ContainersReady:Pod 中所有容器都已就绪;
  • Initialized:所有的 Init 容器都已成功完成;
  • Ready:Pod 可以为请求提供服务,并且应该被添加到对应服务的负载均衡池中。
字段名称描述
typePod 状况的名称
status表明该状况是否适用,可能的取值有 "True"、"False" 或 "Unknown"
lastProbeTime上次探测 Pod 状况时的时间戳
lastTransitionTimePod 上次从一种状态转换到另一种状态时的时间戳
reason机器可读的、驼峰编码(UpperCamelCase)的文字,表述上次状况变化的原因
message人类可读的消息,给出上次状态转换的详细信息

Pod 就绪态

特性状态: Kubernetes v1.29 [beta]

你的应用可以向 PodStatus 中注入额外的反馈或者信号:Pod Readiness(Pod 就绪态) 。 要使用这一特性,可以设置 Pod 规约中的 readinessGates 列表,为 kubelet 提供一组额外的状况供其评估 Pod 就绪态时使用。

就绪态门控基于 Pod 的 status.conditions 字段的当前值来做决定。 如果 Kubernetes 无法在 status.conditions 字段中找到某状况, 则该状况的状态值默认为 "False"。

这里是一个例子:

kind: Pod
...
spec:
  readinessGates:
    - conditionType: "www.example.com/feature-1"
status:
  conditions:
    - type: Ready                              # 内置的 Pod 状况
      status: "False"
      lastProbeTime: null
      lastTransitionTime: 2018-01-01T00:00:00Z
    - type: "www.example.com/feature-1"        # 额外的 Pod 状况
      status: "False"
      lastProbeTime: null
      lastTransitionTime: 2018-01-01T00:00:00Z
  containerStatuses:
    - containerID: docker://abcd...
      ready: true
...

你所添加的 Pod 状况名称必须满足 Kubernetes 标签键名格式

Pod 就绪态的状态

命令 kubectl patch 不支持修改对象的状态。 如果需要设置 Pod 的 status.conditions,应用或者 Operators 需要使用 PATCH 操作。你可以使用 Kubernetes 客户端库之一来编写代码, 针对 Pod 就绪态设置定制的 Pod 状况。

对于使用定制状况的 Pod 而言,只有当下面的陈述都适用时,该 Pod 才会被评估为就绪:

  • Pod 中所有容器都已就绪;
  • readinessGates 中的所有状况都为 True 值。

当 Pod 的容器都已就绪,但至少一个定制状况没有取值或者取值为 False, kubelet 将 Pod 的状况设置为 ContainersReady

Pod 网络就绪

特性状态: Kubernetes v1.25 [alpha]

说明:

在其早期开发过程中,这种状况被命名为 PodHasNetwork

在 Pod 被调度到某节点后,它需要被 kubelet 接受并且挂载所需的存储卷。 一旦这些阶段完成,Kubelet 将与容器运行时(使用容器运行时接口(Container Runtime Interface;CRI)) 一起为 Pod 生成运行时沙箱并配置网络。如果启用了 PodReadyToStartContainersCondition 特性门控 (Kubernetes 1.32 版本中默认启用), PodReadyToStartContainers 状况会被添加到 Pod 的 status.conditions 字段中。

当 kubelet 检测到 Pod 不具备配置了网络的运行时沙箱时,PodReadyToStartContainers 状况将被设置为 False。以下场景中将会发生这种状况:

  • 在 Pod 生命周期的早期阶段,kubelet 还没有开始使用容器运行时为 Pod 设置沙箱时。

  • 在 Pod 生命周期的末期阶段,Pod 的沙箱由于以下原因被销毁时:

    • 节点重启时 Pod 没有被驱逐
    • 对于使用虚拟机进行隔离的容器运行时,Pod 沙箱虚拟机重启时,需要创建一个新的沙箱和全新的容器网络配置。

在运行时插件成功完成 Pod 的沙箱创建和网络配置后, kubelet 会将 PodReadyToStartContainers 状况设置为 True。 当 PodReadyToStartContainers 状况设置为 True 后, Kubelet 可以开始拉取容器镜像和创建容器。

对于带有 Init 容器的 Pod,kubelet 会在 Init 容器成功完成后将 Initialized 状况设置为 True (这发生在运行时成功创建沙箱和配置网络之后), 对于没有 Init 容器的 Pod,kubelet 会在创建沙箱和网络配置开始之前将 Initialized 状况设置为 True

容器探针

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

检查机制

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

  • exec

    在容器内执行指定命令。如果命令退出时返回码为 0 则认为诊断成功。

  • grpc

    使用 gRPC 执行一个远程过程调用。 目标应该实现 gRPC 健康检查。 如果响应的状态是 "SERVING",则认为诊断成功。

  • httpGet

    对容器的 IP 地址上指定端口和路径执行 HTTP GET 请求。如果响应的状态码大于等于 200 且小于 400,则诊断被认为是成功的。

  • tcpSocket

    对容器的 IP 地址上的指定端口执行 TCP 检查。如果端口打开,则诊断被认为是成功的。 如果远程系统(容器)在打开连接后立即将其关闭,这算作是健康的。

注意:

和其他机制不同,exec 探针的实现涉及每次执行时创建/复制多个进程。 因此,在集群中具有较高 pod 密度、较低的 initialDelaySeconds 和 periodSeconds 时长的时候, 配置任何使用 exec 机制的探针可能会增加节点的 CPU 负载。 这种场景下,请考虑使用其他探针机制以避免额外的开销。

探测结果

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

  • Success(成功)

    容器通过了诊断。

  • Failure(失败)

    容器未通过诊断。

  • Unknown(未知)

    诊断失败,因此不会采取任何行动。

探测类型

针对运行中的容器,kubelet 可以选择是否执行以下三种探针,以及如何针对探测结果作出反应:

  • livenessProbe

    指示容器是否正在运行。如果存活态探测失败,则 kubelet 会杀死容器, 并且容器将根据其重启策略决定未来。如果容器不提供存活探针, 则默认状态为 Success

  • readinessProbe

    指示容器是否准备好为请求提供服务。如果就绪态探测失败, 端点控制器将从与 Pod 匹配的所有服务的端点列表中删除该 Pod 的 IP 地址。 初始延迟之前的就绪态的状态值默认为 Failure。 如果容器不提供就绪态探针,则默认状态为 Success

  • startupProbe

    指示容器中的应用是否已经启动。如果提供了启动探针,则所有其他探针都会被 禁用,直到此探针成功为止。如果启动探测失败,kubelet 将杀死容器, 而容器依其重启策略进行重启。 如果容器没有提供启动探测,则默认状态为 Success

如欲了解如何设置存活态、就绪态和启动探针的进一步细节, 可以参阅配置存活态、就绪态和启动探针

何时该使用存活态探针?

如果容器中的进程能够在遇到问题或不健康的情况下自行崩溃,则不一定需要存活态探针; kubelet 将根据 Pod 的 restartPolicy 自动执行修复操作。

如果你希望容器在探测失败时被杀死并重新启动,那么请指定一个存活态探针, 并指定 restartPolicy 为 "Always" 或 "OnFailure"。

何时该使用就绪态探针?

如果要仅在探测成功时才开始向 Pod 发送请求流量,请指定就绪态探针。 在这种情况下,就绪态探针可能与存活态探针相同,但是规约中的就绪态探针的存在意味着 Pod 将在启动阶段不接收任何数据,并且只有在探针探测成功后才开始接收数据。

如果你希望容器能够自行进入维护状态,也可以指定一个就绪态探针, 检查某个特定于就绪态的因此不同于存活态探测的端点。

如果你的应用程序对后端服务有严格的依赖性,你可以同时实现存活态和就绪态探针。 当应用程序本身是健康的,存活态探针检测通过后,就绪态探针会额外检查每个所需的后端服务是否可用。 这可以帮助你避免将流量导向只能返回错误信息的 Pod。

如果你的容器需要在启动期间加载大型数据、配置文件或执行迁移, 你可以使用启动探针。 然而,如果你想区分已经失败的应用和仍在处理其启动数据的应用,你可能更倾向于使用就绪探针。

说明:

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

何时该使用启动探针?

对于所包含的容器需要较长时间才能启动就绪的 Pod 而言,启动探针是有用的。 你不再需要配置一个较长的存活态探测时间间隔,只需要设置另一个独立的配置选定, 对启动期间的容器执行探测,从而允许使用远远超出存活态时间间隔所允许的时长。

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

Pod 的终止

由于 Pod 所代表的是在集群中节点上运行的进程,当不再需要这些进程时允许其体面地终止是很重要的。 一般不应武断地使用 KILL 信号终止它们,导致这些进程没有机会完成清理操作。

设计的目标是令你能够请求删除进程,并且知道进程何时被终止,同时也能够确保删除操作终将完成。 当你请求删除某个 Pod 时,集群会记录并跟踪 Pod 的体面终止周期, 而不是直接强制地杀死 Pod。在存在强制关闭设施的前提下, kubelet 会尝试体面地终止 Pod。

通常 Pod 体面终止的过程为:kubelet 先发送一个带有体面超时限期的 TERM(又名 SIGTERM) 信号到每个容器中的主进程,将请求发送到容器运行时来尝试停止 Pod 中的容器。 停止容器的这些请求由容器运行时以异步方式处理。 这些请求的处理顺序无法被保证。许多容器运行时遵循容器镜像内定义的 STOPSIGNAL 值, 如果不同,则发送容器镜像中配置的 STOPSIGNAL,而不是 TERM 信号。 一旦超出了体面终止限期,容器运行时会向所有剩余进程发送 KILL 信号,之后 Pod 就会被从 API 服务器上移除。 如果 kubelet 或者容器运行时的管理服务在等待进程终止期间被重启, 集群会从头开始重试,赋予 Pod 完整的体面终止限期。

Pod 终止流程,如下例所示:

  1. 你使用 kubectl 工具手动删除某个特定的 Pod,而该 Pod 的体面终止限期是默认值(30 秒)。

  2. API 服务器中的 Pod 对象被更新,记录涵盖体面终止限期在内 Pod 的最终死期,超出所计算时间点则认为 Pod 已死(dead)。 如果你使用 kubectl describe 来查验你正在删除的 Pod,该 Pod 会显示为 "Terminating" (正在终止)。 在 Pod 运行所在的节点上:kubelet 一旦看到 Pod 被标记为正在终止(已经设置了体面终止限期),kubelet 即开始本地的 Pod 关闭过程。

    1. 如果 Pod 中的容器之一定义了 preStop 回调, kubelet 开始在容器内运行该回调逻辑。如果超出体面终止限期时, preStop 回调逻辑仍在运行,kubelet 会请求给予该 Pod 的宽限期一次性增加 2 秒钟。

      如果 preStop 回调在体面期结束后仍在运行,kubelet 将请求短暂的、一次性的体面期延长 2 秒。

    说明:

    如果 preStop 回调所需要的时间长于默认的体面终止限期,你必须修改 terminationGracePeriodSeconds 属性值来使其正常工作。

    1. kubelet 接下来触发容器运行时发送 TERM 信号给每个容器中的进程 1。

      如果 Pod 中定义了Sidecar 容器, 则存在特殊排序。否则,Pod 中的容器会在不同的时间和任意的顺序接收 TERM 信号。如果关闭顺序很重要,考虑使用 preStop 钩子进行同步(或者切换为使用 Sidecar 容器)。

  1. 在 kubelet 启动 Pod 的体面关闭逻辑的同时,控制平面会评估是否将关闭的 Pod 从对应的 EndpointSlice(和端点)对象中移除,过滤条件是 Pod 被对应的服务以某 选择算符选定。 ReplicaSet 和其他工作负载资源不再将关闭进程中的 Pod 视为合法的、能够提供服务的副本。

    关闭动作很慢的 Pod 不应继续处理常规服务请求,而应开始终止并完成对打开的连接的处理。 一些应用程序不仅需要完成对打开的连接的处理,还需要更进一步的体面终止逻辑 - 比如:排空和完成会话。

    任何正在终止的 Pod 所对应的端点都不会立即从 EndpointSlice 中被删除,EndpointSlice API(以及传统的 Endpoints API)会公开一个状态来指示其处于 终止状态。 正在终止的端点始终将其 ready 状态设置为 false(为了向后兼容 1.26 之前的版本), 因此负载均衡器不会将其用于常规流量。

    如果需要排空正被终止的 Pod 上的流量,可以将 serving 状况作为实际的就绪状态。你可以在教程 探索 Pod 及其端点的终止行为 中找到有关如何实现连接排空的更多详细信息。

  1. kubelet 确保 Pod 被关闭和终止

    1. 超出终止宽限期限时,如果 Pod 中仍有容器在运行,kubelet 会触发强制关闭过程。 容器运行时会向 Pod 中所有容器内仍在运行的进程发送 SIGKILL 信号。 kubelet 也会清理隐藏的 pause 容器,如果容器运行时使用了这种容器的话。
    2. kubelet 将 Pod 转换到终止阶段(Failed 或 Succeeded,具体取决于其容器的结束状态)。
    3. kubelet 通过将宽限期设置为 0(立即删除),触发从 API 服务器强制移除 Pod 对象的操作。
    4. API 服务器删除 Pod 的 API 对象,从任何客户端都无法再看到该对象。

强制终止 Pod

注意:

对于某些工作负载及其 Pod 而言,强制删除很可能会带来某种破坏。

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

将宽限期限强制设置为 0 意味着立即从 API 服务器删除 Pod。 如果 Pod 仍然运行于某节点上,强制删除操作会触发 kubelet 立即执行清理操作。

使用 kubectl 时,你必须在设置 --grace-period=0 的同时额外设置 --force 参数才能发起强制删除请求。

执行强制删除操作时,API 服务器不再等待来自 kubelet 的、关于 Pod 已经在原来运行的节点上终止执行的确认消息。 API 服务器直接删除 Pod 对象,这样新的与之同名的 Pod 即可以被创建。 在节点侧,被设置为立即终止的 Pod 仍然会在被强行杀死之前获得一点点的宽限时间。

注意:

马上删除时不等待确认正在运行的资源已被终止。这些资源可能会无限期地继续在集群上运行。

如果你需要强制删除 StatefulSet 的 Pod, 请参阅从 StatefulSet 中删除 Pod 的任务文档。

Pod 关闭和 Sidecar 容器

如果你的 Pod 包含一个或多个 Sidecar 容器 (重启策略为 Always 的 Init 容器),kubelet 将延迟向这些 Sidecar 容器发送 TERM 信号, 直到最后一个主容器已完全终止。Sidecar 容器将按照它们在 Pod 规约中被定义的相反顺序被终止。 这样确保了 Sidecar 容器继续为 Pod 中的其他容器提供服务,直到完全不再需要为止。

这意味着主容器的慢终止也会延迟 Sidecar 容器的终止。 如果在终止过程完成之前宽限期已到,Pod 可能会进入强制终止阶段。 在这种情况下,Pod 中所有剩余的容器将在某个短宽限期内被同时终止。

同样地,如果 Pod 有一个 preStop 钩子超过了终止宽限期,可能会发生紧急终止。 总体而言,如果你以前使用 preStop 钩子来控制没有 Sidecar 的 Pod 中容器的终止顺序, 你现在可以移除这些钩子,允许 kubelet 自动管理 Sidecar 的终止。

Pod 的垃圾收集

对于已失败的 Pod 而言,对应的 API 对象仍然会保留在集群的 API 服务器上, 直到用户或者控制器进程显式地将其删除。

Pod 的垃圾收集器(PodGC)是控制平面的控制器,它会在 Pod 个数超出所配置的阈值 (根据 kube-controller-manager 的 terminated-pod-gc-threshold 设置)时删除已终止的 Pod(阶段值为 Succeeded 或 Failed)。 这一行为会避免随着时间演进不断创建和终止 Pod 而引起的资源泄露问题。

此外,PodGC 会清理满足以下任一条件的所有 Pod:

  1. 孤儿 Pod - 绑定到不再存在的节点,
  2. 计划外终止的 Pod
  3. 终止过程中的 Pod,绑定到有 node.kubernetes.io/out-of-service 污点的未就绪节点。

在清理 Pod 的同时,如果它们处于非终止状态阶段,PodGC 也会将它们标记为失败。 此外,PodGC 在清理孤儿 Pod 时会添加 Pod 干扰状况。参阅 Pod 干扰状况 了解更多详情。

Init 容器

本页提供了 Init 容器的概览。Init 容器是一种特殊容器,在 Pod 内的应用容器启动之前运行。Init 容器可以包括一些应用镜像中不存在的实用工具和安装脚本。

你可以在 Pod 的规约中与用来描述应用容器的 containers 数组平行的位置指定 Init 容器。

在 Kubernetes 中,边车容器 是在主应用容器之前启动并持续运行的容器。本文介绍 Init 容器:在 Pod 初始化期间完成运行的容器。

理解 Init 容器

每个 Pod 中可以包含多个容器, 应用运行在这些容器里面,同时 Pod 也可以有一个或多个先于应用容器启动的 Init 容器。

Init 容器与普通的容器非常像,除了如下两点:

  • 它们总是运行到完成。
  • 每个都必须在下一个启动之前成功完成。

如果 Pod 的 Init 容器失败,kubelet 会不断地重启该 Init 容器直到该容器成功为止。 然而,如果 Pod 对应的 restartPolicy 值为 "Never",并且 Pod 的 Init 容器失败, 则 Kubernetes 会将整个 Pod 状态设置为失败。

为 Pod 设置 Init 容器需要在 Pod 规约中添加 initContainers 字段, 该字段以 Container 类型对象数组的形式组织,和应用的 containers 数组同级相邻。 参阅 API 参考的容器章节了解详情。

Init 容器的状态在 status.initContainerStatuses 字段中以容器状态数组的格式返回 (类似 status.containerStatuses 字段)。

与普通容器的不同之处

Init 容器支持应用容器的全部字段和特性,包括资源限制、 数据卷和安全设置。 然而,Init 容器对资源请求和限制的处理稍有不同, 在下面容器内的资源共享节有说明。

常规的 Init 容器(即不包括边车容器)不支持 lifecyclelivenessProbereadinessProbe 或 startupProbe 字段。Init 容器必须在 Pod 准备就绪之前完成运行;而边车容器在 Pod 的生命周期内继续运行, 它支持一些探针。有关边车容器的细节请参阅边车容器

如果为一个 Pod 指定了多个 Init 容器,这些容器会按顺序逐个运行。 每个 Init 容器必须运行成功,下一个才能够运行。当所有的 Init 容器运行完成时, Kubernetes 才会为 Pod 初始化应用容器并像平常一样运行。

与边车容器的不同之处

Init 容器在主应用容器启动之前运行并完成其任务。 与边车容器不同, Init 容器不会持续与主容器一起运行。

Init 容器按顺序完成运行,等到所有 Init 容器成功完成之后,主容器才会启动。

Init 容器不支持 lifecyclelivenessProbereadinessProbe 或 startupProbe, 而边车容器支持所有这些探针以控制其生命周期。

Init 容器与主应用容器共享资源(CPU、内存、网络),但不直接与主应用容器进行交互。 不过这些容器可以使用共享卷进行数据交换。

使用 Init 容器

因为 Init 容器具有与应用容器分离的单独镜像,其启动相关代码具有如下优势:

  • Init 容器可以包含一些安装过程中应用容器中不存在的实用工具或个性化代码。 例如,没有必要仅为了在安装过程中使用类似 sedawkpython 或 dig 这样的工具而去 FROM 一个镜像来生成一个新的镜像。
  • 应用镜像的创建者和部署者可以各自独立工作,而没有必要联合构建一个单独的应用镜像。
  • 与同一 Pod 中的多个应用容器相比,Init 容器能以不同的文件系统视图运行。因此,Init 容器可以被赋予访问应用容器不能访问的 Secret 的权限。
  • 由于 Init 容器必须在应用容器启动之前运行完成,因此 Init 容器提供了一种机制来阻塞或延迟应用容器的启动,直到满足了一组先决条件。 一旦前置条件满足,Pod 内的所有的应用容器会并行启动。
  • Init 容器可以安全地运行实用程序或自定义代码,而在其他方式下运行这些实用程序或自定义代码可能会降低应用容器镜像的安全性。 通过将不必要的工具分开,你可以限制应用容器镜像的被攻击范围。

示例

下面是一些如何使用 Init 容器的想法:

  • 等待一个 Service 完成创建,通过类似如下 Shell 命令:

    for i in {1..100}; do sleep 1; if nslookup myservice; then exit 0; fi; done; exit 1
    
  • 注册这个 Pod 到远程服务器,通过在命令中调用 API,类似如下:

    curl -X POST http://$MANAGEMENT_SERVICE_HOST:$MANAGEMENT_SERVICE_PORT/register -d 'instance=$(<POD_NAME>)&ip=$(<POD_IP>)'
    
  • 在启动应用容器之前等一段时间,使用类似命令:

    sleep 60
    
  • 克隆 Git 仓库到中。
  • 将配置值放到配置文件中,运行模板工具为主应用容器动态地生成配置文件。 例如,在配置文件中存放 POD_IP 值,并使用 Jinja 生成主应用配置文件。

使用 Init 容器的情况

下面的例子定义了一个具有 2 个 Init 容器的简单 Pod。 第一个等待 myservice 启动, 第二个等待 mydb 启动。 一旦这两个 Init 容器都启动完成,Pod 将启动 spec 节中的应用容器。

apiVersion: v1
kind: Pod
metadata:
  name: myapp-pod
  labels:
    app.kubernetes.io/name: MyApp
spec:
  containers:
  - name: myapp-container
    image: busybox:1.28
    command: ['sh', '-c', 'echo The app is running! && sleep 3600']
  initContainers:
  - name: init-myservice
    image: busybox:1.28
    command: ['sh', '-c', "until nslookup myservice.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local; do echo waiting for myservice; sleep 2; done"]
  - name: init-mydb
    image: busybox:1.28
    command: ['sh', '-c', "until nslookup mydb.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local; do echo waiting for mydb; sleep 2; done"]

你通过运行下面的命令启动 Pod:

kubectl apply -f myapp.yaml

输出类似于:

pod/myapp-pod created

使用下面的命令检查其状态:

kubectl get -f myapp.yaml

输出类似于:

NAME        READY     STATUS     RESTARTS   AGE
myapp-pod   0/1       Init:0/2   0          6m

或者查看更多详细信息:

kubectl describe -f myapp.yaml

输出类似于:

Name:          myapp-pod
Namespace:     default
[...]
Labels:        app.kubernetes.io/name=MyApp
Status:        Pending
[...]
Init Containers:
  init-myservice:
[...]
    State:         Running
[...]
  init-mydb:
[...]
    State:         Waiting
      Reason:      PodInitializing
    Ready:         False
[...]
Containers:
  myapp-container:
[...]
    State:         Waiting
      Reason:      PodInitializing
    Ready:         False
[...]
Events:
  FirstSeen    LastSeen    Count    From                      SubObjectPath                           Type          Reason        Message
  ---------    --------    -----    ----                      -------------                           --------      ------        -------
  16s          16s         1        {default-scheduler }                                              Normal        Scheduled     Successfully assigned myapp-pod to 172.17.4.201
  16s          16s         1        {kubelet 172.17.4.201}    spec.initContainers{init-myservice}     Normal        Pulling       pulling image "busybox"
  13s          13s         1        {kubelet 172.17.4.201}    spec.initContainers{init-myservice}     Normal        Pulled        Successfully pulled image "busybox"
  13s          13s         1        {kubelet 172.17.4.201}    spec.initContainers{init-myservice}     Normal        Created       Created container init-myservice
  13s          13s         1        {kubelet 172.17.4.201}    spec.initContainers{init-myservice}     Normal        Started       Started container init-myservice

如需查看 Pod 内 Init 容器的日志,请执行:

kubectl logs myapp-pod -c init-myservice # 查看第一个 Init 容器
kubectl logs myapp-pod -c init-mydb      # 查看第二个 Init 容器

在这一刻,Init 容器将会等待至发现名称为 mydb 和 myservice 的服务

如下为创建这些 Service 的配置文件:

---
apiVersion: v1
kind: Service
metadata:
  name: myservice
spec:
  ports:
  - protocol: TCP
    port: 80
    targetPort: 9376
---
apiVersion: v1
kind: Service
metadata:
  name: mydb
spec:
  ports:
  - protocol: TCP
    port: 80
    targetPort: 9377

创建 mydb 和 myservice 服务的命令:

kubectl apply -f services.yaml

输出类似于:

service/myservice created
service/mydb created

这样你将能看到这些 Init 容器执行完毕,随后 my-app 的 Pod 进入 Running 状态:

kubectl get -f myapp.yaml

输出类似于:

NAME        READY     STATUS    RESTARTS   AGE
myapp-pod   1/1       Running   0          9m

这个简单例子应该能为你创建自己的 Init 容器提供一些启发。 接下来节提供了更详细例子的链接。

具体行为

在 Pod 启动过程中,每个 Init 容器会在网络和数据卷初始化之后按顺序启动。 kubelet 运行依据 Init 容器在 Pod 规约中的出现顺序依次运行之。

每个 Init 容器成功退出后才会启动下一个 Init 容器。 如果某容器因为容器运行时的原因无法启动,或以错误状态退出,kubelet 会根据 Pod 的 restartPolicy 策略进行重试。 然而,如果 Pod 的 restartPolicy 设置为 "Always",Init 容器失败时会使用 restartPolicy 的 "OnFailure" 策略。

在所有的 Init 容器没有成功之前,Pod 将不会变成 Ready 状态。 Init 容器的端口将不会在 Service 中进行聚集。正在初始化中的 Pod 处于 Pending 状态, 但会将状况 Initializing 设置为 false。

如果 Pod 重启,所有 Init 容器必须重新执行。

对 Init 容器规约的修改仅限于容器的 image 字段。 直接更改 Init 容器的 image 字段不会重启该 Pod 或触发其重新创建。如果该 Pod 尚未启动,则该更改可能会影响 Pod 的启动方式。

对于 Pod 模板,你通常可以更改 Init 容器的任何字段;更改的影响取决于 Pod 模板的使用位置。

因为 Init 容器可能会被重启、重试或者重新执行,所以 Init 容器的代码应该是幂等的。 特别地,向任何 emptyDir 卷写入数据的代码应该对输出文件可能已经存在做好准备。

Init 容器具有应用容器的所有字段。然而 Kubernetes 禁止使用 readinessProbe, 因为 Init 容器不能定义不同于完成态(Completion)的就绪态(Readiness)。 Kubernetes 会在校验时强制执行此检查。

在 Pod 上使用 activeDeadlineSeconds 和在容器上使用 livenessProbe 可以避免 Init 容器一直重复失败。 activeDeadlineSeconds 时间包含了 Init 容器启动的时间。 但建议仅在团队将其应用程序部署为 Job 时才使用 activeDeadlineSeconds, 因为 activeDeadlineSeconds 在 Init 容器结束后仍有效果。 如果你设置了 activeDeadlineSeconds,已经在正常运行的 Pod 会被杀死。

在 Pod 中的每个应用容器和 Init 容器的名称必须唯一; 与任何其它容器共享同一个名称,会在校验时抛出错误。

容器内的资源共享

在给定的 Init、边车和应用容器执行顺序下,资源使用适用于如下规则:

  • 所有 Init 容器上定义的任何特定资源的 limit 或 request 的最大值,作为 Pod 有效初始 request/limit。 如果任何资源没有指定资源限制,这被视为最高限制。

  • Pod 对资源的 有效 limit/request 是如下两者中的较大者:

    • 所有应用容器对某个资源的 limit/request 之和
    • 对某个资源的有效初始 limit/request
  • 基于有效 limit/request 完成调度,这意味着 Init 容器能够为初始化过程预留资源, 这些资源在 Pod 生命周期过程中并没有被使用。

  • Pod 的 有效 QoS 层,与 Init 容器和应用容器的一样。

配额和限制适用于有效 Pod 的请求和限制值。

Init 容器和 Linux cgroup

在 Linux 上,Pod 级别的 CGroup 资源分配基于 Pod 的有效请求和限制值,与调度程序相同。

Pod 重启的原因

Pod 重启会导致 Init 容器重新执行,主要有如下几个原因:

  • Pod 的基础设施容器 (译者注:如 pause 容器) 被重启。这种情况不多见, 必须由具备 root 权限访问节点的人员来完成。
  • 当 restartPolicy 设置为 Always,Pod 中所有容器会终止而强制重启。 由于垃圾回收机制的原因, Init 容器的完成记录将会丢失。

当 Init 容器的镜像发生改变或者 Init 容器的完成记录因为垃圾收集等原因被丢失时, Pod 不会被重启。这一行为适用于 Kubernetes v1.20 及更新版本。 如果你在使用较早版本的 Kubernetes,可查阅你所使用的版本对应的文档。

边车容器

特性状态: Kubernetes v1.29 [beta]

边车容器是与主应用容器在同一个 Pod 中运行的辅助容器。 这些容器通过提供额外的服务或功能(如日志记录、监控、安全性或数据同步)来增强或扩展主应用容器的功能, 而无需直接修改主应用代码。

通常,一个 Pod 中只有一个应用容器。 例如,如果你有一个需要本地 Web 服务器的 Web 应用, 则本地 Web 服务器以边车容器形式运行,而 Web 应用本身以应用容器形式运行。

Kubernetes 中的边车容器

Kubernetes 将边车容器作为 Init 容器的一个特例来实现, Pod 启动后,边车容器仍保持运行状态。 本文档使用术语"常规 Init 容器"来明确指代仅在 Pod 启动期间运行的容器。

如果你的集群启用了 SidecarContainers 特性门控 (该特性自 Kubernetes v1.29 起默认启用),你可以为 Pod 的 initContainers 字段中列出的容器指定 restartPolicy。 这些可重新启动的边车(Sidecar)  容器独立于其他 Init 容器以及同一 Pod 内的主应用容器, 这些容器可以启动、停止和重新启动,而不会影响主应用容器和其他 Init 容器。

你还可以运行包含多个未标记为 Init 或边车容器的 Pod。 如果作为一个整体而言,某个 Pod 中的所有容器都要运行,但你不需要控制哪些容器先启动或停止,那么这种设置是合适的。 如果你使用的是不支持容器级 restartPolicy 字段的旧版本 Kubernetes,你也可以这样做。

应用示例

下面是一个包含两个容器的 Deployment 示例,其中一个容器是边车形式:

application/deployment-sidecar.yaml 

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
  labels:
    app: myapp
spec:
  replicas: 1
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
        - name: myapp
          image: alpine:latest
          command: ['sh', '-c', 'while true; do echo "logging" >> /opt/logs.txt; sleep 1; done']
          volumeMounts:
            - name: data
              mountPath: /opt
      initContainers:
        - name: logshipper
          image: alpine:latest
          restartPolicy: Always
          command: ['sh', '-c', 'tail -F /opt/logs.txt']
          volumeMounts:
            - name: data
              mountPath: /opt
      volumes:
        - name: data
          emptyDir: {}

边车容器和 Pod 生命周期

如果创建 Init 容器时将 restartPolicy 设置为 Always, 则它将在整个 Pod 的生命周期内启动并持续运行。这对于运行与主应用容器分离的支持服务非常有帮助。

如果为此 Init 容器指定了 readinessProbe,其结果将用于确定 Pod 的 ready 状态。

由于这些容器被定义为 Init 容器,所以它们享有与其他 Init 容器相同的顺序和按序执行保证, 从而允许将边车容器与常规 Init 容器混合使用,支持复杂的 Pod 初始化流程。

与常规 Init 容器相比,在 initContainers 中定义的边车容器在启动后继续运行。 当 Pod 的 .spec.initContainers 中有多个条目时,这一点非常重要。 在边车风格的 Init 容器运行后(kubelet 将该 Init 容器的 started 状态设置为 true), kubelet 启动 .spec.initContainers 这一有序列表中的下一个 Init 容器。 该状态要么因为容器中有一个正在运行的进程且没有定义启动探针而变为 true, 要么是其 startupProbe 成功而返回的结果。

在 Pod 终止时, kubelet 会推迟终止边车容器,直到主应用容器已完全停止。边车容器随后将按照它们在 Pod 规约中出现的相反顺序被关闭。 这种方法确保了在不再需要边车服务之前这些边车继续发挥作用,以支持 Pod 内的其他容器。

带边车容器的 Job

如果你定义 Job 时使用基于 Kubernetes 风格 Init 容器的边车容器, 各个 Pod 中的边车容器不会阻止 Job 在主容器结束后进入完成状态。

以下是一个具有两个容器的 Job 示例,其中一个是边车:

application/job/job-sidecar.yaml 

apiVersion: batch/v1
kind: Job
metadata:
  name: myjob
spec:
  template:
    spec:
      containers:
        - name: myjob
          image: alpine:latest
          command: ['sh', '-c', 'echo "logging" > /opt/logs.txt']
          volumeMounts:
            - name: data
              mountPath: /opt
      initContainers:
        - name: logshipper
          image: alpine:latest
          restartPolicy: Always
          command: ['sh', '-c', 'tail -F /opt/logs.txt']
          volumeMounts:
            - name: data
              mountPath: /opt
      restartPolicy: Never
      volumes:
        - name: data
          emptyDir: {}

与应用容器的区别

边车容器与同一 Pod 中的应用容器并行运行。不过边车容器不执行主应用逻辑,而是为主应用提供支持功能。

边车容器具有独立的生命周期。它们可以独立于应用容器启动、停止和重启。 这意味着你可以更新、扩展或维护边车容器,而不影响主应用。

边车容器与主容器共享相同的网络和存储命名空间。这种共存使它们能够紧密交互并共享资源。

从 Kubernetes 的角度来看,边车容器的体面终止(Graceful Termination)相对不那么重要。 当其他容器耗尽了分配的体面终止时间后,边车容器将会比预期更快地接收到 SIGTERM,随后是 SIGKILL。 因此,在 Pod 终止时,边车容器退出码不为 00 表示成功退出)是正常的, 通常应该被外部工具忽略。

与 Init 容器的区别

边车容器与主容器并行工作,扩展其功能并提供附加服务。

边车容器与主应用容器同时运行。它们在整个 Pod 的生命周期中都处于活动状态,并且可以独立于主容器启动和停止。 与 Init 容器不同, 边车容器支持探针来控制其生命周期。

边车容器可以直接与主应用容器交互,因为与 Init 容器一样, 它们总是与应用容器共享相同的网络,并且还可以选择共享卷(文件系统)。

Init 容器在主容器启动之前停止,因此 Init 容器无法与 Pod 中的应用容器交换消息。 所有数据传递都是单向的(例如,Init 容器可以将信息放入 emptyDir 卷中)。

容器内的资源共享

假如执行顺序为 Init 容器、边车容器和应用容器,则关于资源用量适用以下规则:

  • 所有 Init 容器上定义的任何特定资源的 limit 或 request 的最大值,作为 Pod 有效初始 request/limit。 如果任何资源没有指定资源限制,则被视为最高限制。

  • Pod 对资源的 有效 limit/request 是如下两者中的较大者:

    • 所有应用容器对某个资源的 limit/request 之和
    • Init 容器中对某个资源的有效 limit/request
  • 系统基于有效的 limit/request 完成调度,这意味着 Init 容器能够为初始化过程预留资源, 而这些资源在 Pod 的生命周期中不再被使用。

  • Pod 的 有效 QoS 级别,对于 Init 容器和应用容器而言是相同的。

配额和限制适用于 Pod 的有效请求和限制值。

边车容器和 Linux Cgroup

在 Linux 上,Pod Cgroup 的资源分配基于 Pod 级别的有效资源请求和限制,这一点与调度器相同。

临时容器

特性状态: Kubernetes v1.25 [stable]

本页面概述了临时容器:一种特殊的容器,该容器在现有 Pod 中临时运行,以便完成用户发起的操作,例如故障排查。 你会使用临时容器来检查服务,而不是用它来构建应用程序。

了解临时容器

Pod 是 Kubernetes 应用程序的基本构建块。 由于 Pod 是一次性且可替换的,因此一旦 Pod 创建,就无法将容器加入到 Pod 中。 取而代之的是,通常使用 Deployment 以受控的方式来删除并替换 Pod。

有时有必要检查现有 Pod 的状态。例如,对于难以复现的故障进行排查。 在这些场景中,可以在现有 Pod 中运行临时容器来检查其状态并运行任意命令。

什么是临时容器?

临时容器与其他容器的不同之处在于,它们缺少对资源或执行的保证,并且永远不会自动重启, 因此不适用于构建应用程序。 临时容器使用与常规容器相同的 ContainerSpec 节来描述,但许多字段是不兼容和不允许的。

  • 临时容器没有端口配置,因此像 portslivenessProbereadinessProbe 这样的字段是不允许的。
  • Pod 资源分配是不可变的,因此 resources 配置是不允许的。
  • 有关允许字段的完整列表,请参见 EphemeralContainer 参考文档

临时容器是使用 API 中的一种特殊的 ephemeralcontainers 处理器进行创建的, 而不是直接添加到 pod.spec 段,因此无法使用 kubectl edit 来添加一个临时容器。

与常规容器一样,将临时容器添加到 Pod 后,将不能更改或删除临时容器。

说明:

临时容器不被静态 Pod 支持。

临时容器的用途

当由于容器崩溃或容器镜像不包含调试工具而导致 kubectl exec 无用时, 临时容器对于交互式故障排查很有用。

尤其是,Distroless 镜像 允许用户部署最小的容器镜像,从而减少攻击面并减少故障和漏洞的暴露。 由于 distroless 镜像不包含 Shell 或任何的调试工具,因此很难单独使用 kubectl exec 命令进行故障排查。

使用临时容器时, 启用进程名字空间共享很有帮助, 可以查看其他容器中的进程。

用户命名空间

特性状态: Kubernetes v1.30 [beta]

本页解释了在 Kubernetes Pod 中如何使用用户命名空间。 用户命名空间将容器内运行的用户与主机中的用户隔离开来。

在容器中以 root 身份运行的进程可以在主机中以不同的(非 root)用户身份运行; 换句话说,该进程在用户命名空间内的操作具有完全的权限, 但在命名空间外的操作是无特权的。

你可以使用这个功能来减少被破坏的容器对主机或同一节点中的其他 Pod 的破坏。 有几个安全漏洞被评为  或 重要, 当用户命名空间处于激活状态时,这些漏洞是无法被利用的。 预计用户命名空间也会减轻一些未来的漏洞。

准备开始

说明: 本部分链接到提供 Kubernetes 所需功能的第三方项目。Kubernetes 项目作者不负责这些项目。此页面遵循CNCF 网站指南,按字母顺序列出项目。要将项目添加到此列表中,请在提交更改之前阅读内容指南

这是一个只对 Linux 有效的功能特性,且需要 Linux 支持在所用文件系统上挂载 idmap。 这意味着:

  • 在节点上,你用于 /var/lib/kubelet/pods/ 的文件系统,或你为此配置的自定义目录, 需要支持 idmap 挂载。
  • Pod 卷中使用的所有文件系统都必须支持 idmap 挂载。

在实践中,这意味着你最低需要 Linux 6.3,因为 tmpfs 在该版本中开始支持 idmap 挂载。 这通常是需要的,因为有几个 Kubernetes 功能特性使用 tmpfs (默认情况下挂载的服务账号令牌使用 tmpfs、Secret 使用 tmpfs 等等)。

Linux 6.3 中支持 idmap 挂载的一些比较流行的文件系统是:btrfs、ext4、xfs、fat、 tmpfs、overlayfs。

此外,容器运行时及其底层 OCI 运行时必须支持用户命名空间。以下 OCI 运行时提供支持:

  • crun 1.9 或更高版本(推荐 1.13+ 版本)。
  • runc 1.2 或更高版本。
说明:

一些 OCI 运行时不包含在 Linux Pod 中使用用户命名空间所需的支持。 如果你使用托管 Kubernetes,或者使用软件包下载并安装 Kubernetes 集群, 则集群中的节点可能使用不包含支持此特性的运行时。

此外,需要在容器运行时提供支持, 才能在 Kubernetes Pod 中使用这一功能:

  • containerd:2.0(及更高版本)支持容器使用用户命名空间。
  • CRI-O:1.25(及更高)版本支持配置容器的用户命名空间。

你可以在 GitHub 上的 [issue][CRI-dockerd-issue] 中查看 cri-dockerd 中用户命名空间支持的状态。

介绍

用户命名空间是一个 Linux 功能,允许将容器中的用户映射到主机中的不同用户。 此外,在某用户命名空间中授予 Pod 的权能只在该命名空间中有效,在该命名空间之外无效。

一个 Pod 可以通过将 pod.spec.hostUsers 字段设置为 false 来选择使用用户命名空间。

kubelet 将挑选 Pod 所映射的主机 UID/GID, 并以此保证同一节点上没有两个 Pod 使用相同的方式进行映射。

pod.spec 中的 runAsUserrunAsGroupfsGroup 等字段总是指的是容器内的用户。 启用该功能时,有效的 UID/GID 在 0-65535 范围内。这以限制适用于文件和进程(runAsUserrunAsGroup 等)。

使用这个范围之外的 UID/GID 的文件将被视为属于溢出 ID, 通常是 65534(配置在 /proc/sys/kernel/overflowuid和/proc/sys/kernel/overflowgid)。 然而,即使以 65534 用户/组的身份运行,也不可能修改这些文件。

大多数需要以 root 身份运行但不访问其他主机命名空间或资源的应用程序, 在用户命名空间被启用时,应该可以继续正常运行,不需要做任何改变。

了解 Pod 的用户命名空间

一些容器运行时的默认配置(如 Docker Engine、containerd、CRI-O)使用 Linux 命名空间进行隔离。 其他技术也存在,也可以与这些运行时(例如,Kata Containers 使用虚拟机而不是 Linux 命名空间)结合使用。 本页适用于使用 Linux 命名空间进行隔离的容器运行时。

在创建 Pod 时,默认情况下会使用几个新的命名空间进行隔离: 一个网络命名空间来隔离容器网络,一个 PID 命名空间来隔离进程视图等等。 如果使用了一个用户命名空间,这将把容器中的用户与节点中的用户隔离开来。

这意味着容器可以以 root 身份运行,并将该身份映射到主机上的一个非 root 用户。 在容器内,进程会认为它是以 root 身份运行的(因此像 aptyum 等工具可以正常工作), 而实际上该进程在主机上没有权限。 你可以验证这一点,例如,如果你从主机上执行 ps aux 来检查容器进程是以哪个用户运行的。 ps 显示的用户与你在容器内执行 id 命令时看到的用户是不一样的。

这种抽象限制了可能发生的情况,例如,容器设法逃逸到主机上时的后果。 鉴于容器是作为主机上的一个非特权用户运行的,它能对主机做的事情是有限的。

此外,由于每个 Pod 上的用户将被映射到主机中不同的非重叠用户, 他们对其他 Pod 可以执行的操作也是有限的。

授予一个 Pod 的权能也被限制在 Pod 的用户命名空间内, 并且在这一命名空间之外大多无效,有些甚至完全无效。这里有两个例子:

  • CAP_SYS_MODULE 若被授予一个使用用户命名空间的 Pod 则没有任何效果,这个 Pod 不能加载内核模块。
  • CAP_SYS_ADMIN 只限于 Pod 所在的用户命名空间,在该命名空间之外无效。

在不使用用户命名空间的情况下,以 root 账号运行的容器,在容器逃逸时,在节点上有 root 权限。 而且如果某些权能被授予了某容器,这些权能在宿主机上也是有效的。 当我们使用用户命名空间时,这些都不再成立。

如果你想知道关于使用用户命名空间时的更多变化细节,请参见 man 7 user_namespaces

设置一个节点以支持用户命名空间

默认情况下,kubelet 会分配 0-65535 范围以上的 Pod UID/GID, 这是基于主机的文件和进程使用此范围内的 UID/GID 的假设,也是大多数 Linux 发行版的标准。 此方法可防止主机的 UID/GID 与 Pod 的 UID/GID 之间出现重叠。

避免重叠对于减轻 CVE-2021-25741 等漏洞的影响非常重要, 其中 Pod 可能会读取主机中的任意文件。 如果 Pod 和主机的 UID/GID 不重叠,则 Pod 的功能将受到限制: Pod UID/GID 将与主机的文件所有者/组不匹配。

kubelet 可以对 Pod 的用户 ID 和组 ID 使用自定义范围。要配置自定义范围,节点需要具有:

  • 系统中的用户 kubelet(此处不能使用任何其他用户名)。
  • 已安装二进制文件 getsubidsshadow-utils 的一部分)并位于 kubelet 二进制文件的 PATH 中。
  • kubelet 用户的从属 UID/GID 配置 (请参阅 man 5 subuid 和 man 5 subgid

此设置仅收集 UID/GID 范围配置,不会更改执行 kubelet 的用户。

对于分配给 kubelet 用户的从属 ID 范围, 你必须遵循一些限制:

  • 启动 Pod 的 UID 范围的从属用户 ID 必须是 65536 的倍数,并且还必须大于或等于 65536。 换句话说,Pod 不能使用 0-65535 范围内的任何 ID;kubelet 施加此限制是为了使创建意外不安全的配置变得困难。
  • 从属 ID 计数必须是 65536 的倍数
  • 从属 ID 计数必须至少为 65536 x <maxPods>,其中 <maxPods> 是节点上可以运行的最大 Pod 数量。
  • 你必须为用户 ID 和组 ID 分配相同的范围。如果其他用户的用户 ID 范围与组 ID 范围不一致也没关系。
  • 所分配的范围不得与任何其他分配重叠。
  • 从属配置必须只有一行。换句话说,你不能有多个范围。

例如,你可以定义 /etc/subuid 和 /etc/subgid 来为 kubelet 用户定义以下条目:

# 格式为:
#   name:firstID:count of IDs
# 在哪里:
# - firstID 是 65536 (可能的最小值)
# - IDs 的数量是 110(默认数量限制)* 65536
kubelet:65536:7208960

与 Pod 安全准入检查的集成

特性状态: Kubernetes v1.29 [alpha]

对于启用了用户命名空间的 Linux Pod,Kubernetes 会以受控方式放宽 Pod 安全性标准的应用。 这种行为可以通过特性门控 UserNamespacesPodSecurityStandards 进行控制,可以让最终用户提前尝试此特性。 如果管理员启用此特性门控,必须确保群集中的所有节点都启用了用户命名空间。

如果你启用相关特性门控并创建了使用用户命名空间的 Pod,以下的字段不会被限制, 即使在执行了 Baseline 或 Restricted Pod 安全性标准的上下文中。这种行为不会带来安全问题, 因为带有用户命名空间的 Pod 内的 root 实际上指的是容器内的用户,绝不会映射到主机上的特权用户。 以下是在这种情况下不进行检查的 Pod 字段列表:

  • spec.securityContext.runAsNonRoot
  • spec.containers[*].securityContext.runAsNonRoot
  • spec.initContainers[*].securityContext.runAsNonRoot
  • spec.ephemeralContainers[*].securityContext.runAsNonRoot
  • spec.securityContext.runAsUser
  • spec.containers[*].securityContext.runAsUser
  • spec.initContainers[*].securityContext.runAsUser

限制

当 Pod 使用用户命名空间时,不允许 Pod 使用其他主机命名空间。 特别是,如果你设置了 hostUsers: false,那么你就不可以设置如下属性:

  • hostNetwork: true
  • hostIPC: true
  • hostPID: true