镜像文件系统:配置 Kubernetes 将容器存储在独立的文件系统上

作者: Kevin Hannon (Red Hat)

译者: Michael Yao

磁盘空间不足是运行或操作 Kubernetes 集群时的一个常见问题。 在制备节点时,你应该为容器镜像和正在运行的容器留足够的存储空间。 容器运行时通常会向 /var 目录写入数据。 此目录可以位于单独的分区或根文件系统上。CRI-O 默认将其容器和镜像写入 /var/lib/containers, 而 containerd 将其容器和镜像写入 /var/lib/containerd

在这篇博文中,我们想要关注的是几种不同方式,用来配置容器运行时将其内容存储到别的位置而非默认分区。 这些配置允许我们更灵活地配置 Kubernetes,支持在保持默认文件系统不受影响的情况下为容器存储添加更大的磁盘。

需要额外讲述的是 Kubernetes 向磁盘在写入数据的具体位置及内容。

了解 Kubernetes 磁盘使用情况

Kubernetes 有持久数据和临时数据。kubelet 和特定于 Kubernetes 的本地存储的基础路径是可配置的, 但通常假定为 /var/lib/kubelet。在 Kubernetes 文档中, 这一位置有时被称为根文件系统或节点文件系统。写入的数据可以大致分类为:

  • 临时存储
  • 日志
  • 容器运行时

与大多数 POSIX 系统不同,这里的根/节点文件系统不是 /,而是 /var/lib/kubelet 所在的磁盘。

临时存储

Pod 和容器的某些操作可能需要临时或瞬态的本地存储。 临时存储的生命周期短于 Pod 的生命周期,且临时存储不能被多个 Pod 共享。

日志

默认情况下,Kubernetes 将每个运行容器的日志存储为 /var/log 中的文件。 这些日志是临时性质的,并由 kubelet 负责监控以确保不会在 Pod 运行时变得过大。

你可以为每个节点自定义日志轮换设置, 以管控这些日志的大小,并(使用第三方解决方案)配置日志转储以避免对节点本地存储形成依赖。

容器运行时

容器运行时针对容器和镜像使用两个不同的存储区域。

  • 只读层:镜像通常被表示为只读层,因为镜像在容器处于运行状态期间不会被修改。 只读层可以由多个层组成,这些层组合到一起形成最终的只读层。 如果容器要向文件系统中写入数据,则在容器层之上会存在一个薄层为容器提供临时存储。
  • 可写层:取决于容器运行时的不同实现,本地写入可能会用分层写入机制来实现 (例如 Linux 上的 overlayfs 或 Windows 上的 CimFS)。这一机制被称为可写层。 本地写入也可以使用一个可写文件系统来实现,该文件系统使用容器镜像的完整克隆来初始化; 这种方式适用于某些基于 Hypervisor 虚拟化的运行时。

容器运行时文件系统包含只读层和可写层。在 Kubernetes 文档中,这一文件系统被称为 imagefs

容器运行时配置

CRI-O

CRI-O 使用 TOML 格式的存储配置文件,让你控制容器运行时如何存储持久数据和临时数据。 CRI-O 使用了 containers-storage 库。 某些 Linux 发行版为 containers-storage 提供了帮助手册条目(man 5 containers-storage.conf)。 存储的主要配置位于 /etc/containers/storage.conf 中,你可以控制临时数据和根目录的位置。 根目录是 CRI-O 存储持久数据的位置。

[storage]
# 默认存储驱动
driver = "overlay"
# 临时存储位置
runroot = "/var/run/containers/storage"
# 容器存储的主要读/写位置
graphroot = "/var/lib/containers/storage"
  • graphroot
    • 存储来自容器运行时的持久数据
    • 如果 SELinux 被启用,则此项必须是 /var/lib/containers/storage
  • runroot
    • 容器的临时读/写访问
    • 建议将其放在某个临时文件系统上

以下是为你的 graphroot 目录快速重新打标签以匹配 /var/lib/containers/storage 的方法:

semanage fcontext -a -e /var/lib/containers/storage <你的存储路径>
restorecon -R -v <你的存储路径>

containerd

containerd 运行时使用 TOML 配置文件来控制存储持久数据和临时数据的位置。 配置文件的默认路径位于 /etc/containerd/config.toml

与 containerd 存储的相关字段是 rootstate

  • root
    • containerd 元数据的根目录
    • 默认为 /var/lib/containerd
    • 如果你的操作系统要求,需要为根目录设置 SELinux 标签
  • state
    • containerd 的临时数据
    • 默认为 /run/containerd

Kubernetes 节点压力驱逐

Kubernetes 将自动检测容器文件系统是否与节点文件系统分离。 当你分离文件系统时,Kubernetes 负责同时监视节点文件系统和容器运行时文件系统。 Kubernetes 文档将节点文件系统称为 nodefs,将容器运行时文件系统称为 imagefs。 如果 nodefs 或 imagefs 中有一个磁盘空间不足,则整个节点被视为有磁盘压力。 这种情况下,Kubernetes 先通过删除未使用的容器和镜像来回收空间,之后会尝试驱逐 Pod。 在同时具有 nodefs 和 imagefs 的节点上,kubelet 将在 imagefs 上对未使用的容器镜像执行垃圾回收, 并从 nodefs 中移除死掉的 Pod 及其容器。 如果只有 nodefs,则 Kubernetes 垃圾回收将包括死掉的容器、死掉的 Pod 和未使用的镜像。

Kubernetes 提供额外的配置方法来确定磁盘是否已满。kubelet 中的驱逐管理器有一些让你可以控制相关阈值的配置项。 对于文件系统,相关测量值有 nodefs.availablenodefs.inodesfreeimagefs.availableimagefs.inodesfree。如果容器运行时没有专用磁盘,则 imagefs 被忽略。

用户可以使用现有的默认值:

  • memory.available < 100MiB
  • nodefs.available < 10%
  • imagefs.available < 15%
  • nodefs.inodesFree < 5%(Linux 节点)

Kubernetes 允许你在 kubelet 配置文件中将 EvictionHardEvictionSoft 设置为用户定义的值。

EvictionHard
定义限制;一旦超出这些限制,Pod 将被立即驱逐,没有任何宽限期。
EvictionSoft
定义限制;一旦超出这些限制,Pod 将在按各信号所设置的宽限期后被驱逐。

如果你为 EvictionHard 指定了值,所设置的值将取代默认值。 这意味着在你的配置中设置所有信号非常重要。

例如,以下 kubelet 配置可用于配置驱逐信号和宽限期选项。

apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
address: "192.168.0.8"
port: 20250
serializeImagePulls: false
evictionHard:
    memory.available:  "100Mi"
    nodefs.available:  "10%"
    nodefs.inodesFree: "5%"
    imagefs.available: "15%"
    imagefs.inodesFree: "5%"
evictionSoft:
    memory.available:  "100Mi"
    nodefs.available:  "10%"
    nodefs.inodesFree: "5%"
    imagefs.available: "15%"
    imagefs.inodesFree: "5%"
evictionSoftGracePeriod:
    memory.available:  "1m30s"
    nodefs.available:  "2m"
    nodefs.inodesFree: "2m"
    imagefs.available: "2m"
    imagefs.inodesFree: "2m"
evictionMaxPodGracePeriod: 60s

问题

Kubernetes 项目建议你针对 Pod 驱逐要么使用其默认设置,要么设置与之相关的所有字段。 你可以使用默认设置或指定你自己的 evictionHard 设置。 如果你漏掉一个信号,那么 Kubernetes 将不会监视该资源。 管理员或用户可能会遇到的一个常见误配是将新的文件系统挂载到 /var/lib/containers/storage/var/lib/containerd。 Kubernetes 将检测到一个单独的文件系统,因此你要确保 imagefs.inodesfreeimagefs.available 符合你的需要。

另一个令人困惑的地方是,如果你为节点定义了镜像文件系统,则临时存储报告不会发生变化。 镜像文件系统(imagefs)用于存储容器镜像层;如果容器向自己的根文件系统写入, 那么这种本地写入不会计入容器镜像的大小。容器运行时存储这些本地修改的位置是由运行时定义的,但通常是镜像文件系统。 如果 Pod 中的容器正在向基于文件系统的 emptyDir 卷写入,所写入的数据将使用 nodefs 文件系统的空间。 kubelet 始终根据 nodefs 所表示的文件系统来报告临时存储容量和分配情况; 当临时写入操作实际上是写到镜像文件系统时,这种差别可能会让人困惑。

后续工作

为了解决临时存储报告相关的限制并为容器运行时提供更多配置选项,SIG Node 正在处理 KEP-4191。在 KEP-4191 中, Kubernetes 将检测可写层是否与只读层(镜像)分离。 这种检测使我们可以将包括可写层在内的所有临时存储放在同一磁盘上,同时也可以为镜像使用单独的磁盘。

参与其中

如果你想参与其中,可以加入 Kubernetes Node 特别兴趣小组(SIG)。

如果你想分享反馈,可以分享到我们的 #sig-node Slack 频道。 如果你还没有加入该 Slack 工作区,可以访问 https://slack.k8s.io/ 获取邀请。

特别感谢所有提供出色评审、分享宝贵见解或建议主题想法的贡献者。

  • Peter Hunt
  • Mrunal Patel
  • Ryan Phillips
  • Gaurav Singh