本文发表于一年多前。旧文章可能包含过时内容。请检查页面中的信息自发布以来是否已变得不正确。

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

运行/操作 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)。这被称为可写层。本地写入也可以使用一个可写的文件系统,该文件系统以容器镜像的完整克隆进行初始化;这用于某些基于虚拟机管理程序虚拟化的运行时。

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

容器运行时配置

CRI-O

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

[storage]
# Default storage driver
driver = "overlay"
# Temporary storage location
runroot = "/var/run/containers/storage"
# Primary read/write location of container 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 <YOUR-STORAGE-PATH>
restorecon -R -v <YOUR-STORAGE-PATH>

containerd

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

containerd 存储的相关字段是 rootstate

  • root
    • containerd 元数据的根目录
    • 默认为 /var/lib/containerd
    • 如果你的操作系统需要,Root 也需要 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 项目建议你要么使用驱逐的默认设置,要么设置驱逐的所有字段。你可以使用默认设置或指定自己的 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