jimyag's Blog

Kubernetes 临时存储:容器日志、emptyDir 与磁盘压力

PVC 还有很多空间,Pod 却因存储问题被驱逐;或者 emptyDir.sizeLimit 只设了 16Mi,应用仍然成功写入了 32Mi,随后才退出。这些现象需要从不同的存储边界解释:PVC 容量、Pod 本地临时存储额度、卷大小限制和节点磁盘压力,彼此不是一个配额。

在 zj 的 kind 实验中,磁盘型 emptyDir 成功接收固定的 32Mi 写入,随后 kubelet 把 Pod 标记为 Failed / Evicted,消息明确指出 emptyDir 超过 16Mi。采集结果中的节点 DiskPressure=False,因此不能把本次驱逐解释成节点满盘。

哪些数据占用本地临时存储

常规 Linux 节点上的本地临时存储,主要包括容器可写层、容器日志,以及磁盘型 emptyDir。它们共享节点上的实际存储资源,但具有不同的生命周期与计量入口。

数据常见归属生命周期与限制
容器根文件系统的可写层容器随容器实例变化,不应依赖它持久保存业务数据
容器 stdout / stderr 日志节点上的容器日志文件由 kubelet、运行时和日志轮转配置共同管理
磁盘型 emptyDirPod同 Pod 内容器可共享,容器重启后仍可存在,Pod 移除后清理
emptyDir.medium: Memorytmpfs / 内存使用内存,不能当作普通磁盘临时存储预算
PVCPersistentVolume 所承载的存储容量与回收策略由卷及存储实现决定

镜像只读层也会占用节点磁盘,但它不是简单地按完整镜像大小计入每个 Pod 的临时存储使用量。镜像文件系统压力与垃圾回收需要结合节点布局单独观察。hostPath 也不能作为“挂载后自动纳入 Pod 临时存储 limit”的保证。

emptyDir.medium: Memory 的数据会计入相应内存使用;把缓存从磁盘改成 tmpfs,可能把磁盘问题转为 OOM 问题。这个区别与内存 OOM 和驱逐文章中的 memory limit 对应。

资源定义与文件系统布局要求见 Kubernetes v1.35 的本地临时存储管理文档。节点采用不受支持的挂载布局时,kubelet 的计量可能无法覆盖预期路径;不能仅凭配置了 limit 就认定所有文件都受约束。

request、容器 limit 与卷 sizeLimit 是三层判断

requests.ephemeral-storage 参与 scheduler 的容量核算。它表示调度承诺,不会在文件系统中立即创建一块专属空间。limits.ephemeral-storage 用于 kubelet 的超限判断;磁盘型 emptyDir 的 sizeLimit 则限制该卷的使用量。

这些字段使用资源数量语法。400Mi 表示约 400 MiB,400m 则是 0.4 字节,不能把小写 m 当作 MB。单位错误可能导致远小于预期的限制。

本文分析 Kubernetes v1.35.1(8fea90b45245ef5c8ba54e7ae044d3e777c22500),实际运行环境是 kind v0.33.0、containerd v2.2.1。没有调整本地存储隔离或文件系统 quota 的 feature gate。

localStorageEviction 对每个有统计数据的活跃 Pod,依次检查:

正在加载 GitHub 文件…
1
2
3
emptyDirLimitEviction
→ podEphemeralStorageLimitEviction
→ containerEphemeralStorageLimitEviction

某一步已经驱逐该 Pod,就不再用后面的原因再次驱逐它。因此一个 Pod 同时超出多个限制时,最后显示的消息不等于所有超限项的完整列表。

emptyDirLimitEviction 从卷统计中按卷名查使用量,比较 used > sizeLimit。podEphemeralStorageLimitEviction 则读取 Pod 的临时存储汇总使用量,与资源 helper 计算的 Pod limit 比较。它们不是把每个 emptyDir 强行平均分配给所有容器。

容器级检查另行从容器统计中提取日志等使用量,并根据文件系统布局处理 rootfs 统计。独立 imagefs 与共享文件系统会走不同分支。排查时应确认压力和限制对应的是哪一层,不能只把一个目录的 du 结果与任意一个 limit 对比。

sizeLimit 超限是异步驱逐条件

计量方式还有一个实际排障盲区。目录扫描统计路径下仍可见的文件;文件已经删除、进程却仍持有打开的描述符时,文件系统的空间仍被占用,扫描可能不再把这部分空间记入卷使用量。因此 du、Pod 用量和 df 可以不同步,删除大日志后也可能没有马上归还磁盘空间。metricsDu.getDiskUsage 调用的 fs.DiskUsage 是核对扫描与 quota 统计入口的位置。

正在加载 GitHub 文件…

DiskUsage 先尝试 quota 统计;不能取得相应结果时继续遍历目录。两种计量方式不是同时无条件启用的两份使用量。

正在加载 GitHub 文件…

在满足文件系统、用户命名空间和相关 feature gate 条件时,project quota 监测可以更准确地跟踪这类仍占空间的文件;LocalStorageCapacityIsolationFSQuotaMonitoring 控制相关能力。这里的 quota 用于监测,不会把 Kubernetes 的临时存储 limit 自动变成每次写入都由内核拒绝的硬限额。本文未启用或运行验证这条分支,条件与限制以官方计量说明为依据。

正在加载 GitHub 文件…

对于本文的磁盘型 emptyDir,sizeLimit 不等价于“写到指定字节时,每一次 write 都立即返回 ENOSPC”。kubelet 需要先收集卷使用量,再在驱逐循环中判断并终止 Pod。这段时间里,应用可能已经写入超过限制的数据。

这也意味着临时存储 limit 无法替代文件系统容量规划。写入速率很高、统计延迟较大或其他工作负载同时写盘时,节点仍可能先遇到空间不足。应用必须处理写入失败,不能依赖 kubelet 一定先以干净的方式终止进程。

源码中,如果本地存储隔离启用,synchronize 会在“没有节点压力阈值需要处理就返回”的分支之前调用 localStorageEviction。因此 Pod 卷超限驱逐不以节点出现 DiskPressure 为前提。

  flowchart TB
    A[kubelet 取得节点与 Pod 统计] --> B{Pod 的本地存储是否超限}
    B -->|是| C[记录具体超限原因并驱逐该 Pod]
    B -->|否| D{节点文件系统压力达到驱逐条件}
    D -->|否| E[本轮不因磁盘驱逐]
    D -->|是| F[按文件系统尝试节点级回收]
    F --> G{压力是否解除}
    G -->|是| E
    G -->|否| H[按该压力源的使用量与优先级排序]
    H --> I[选择 Pod 并执行压力驱逐]
    style B fill:#e1efff,color:#15365a,stroke:#366a9c
    style D fill:#fff1de,color:#4b2e05,stroke:#9f6a1d

图把卷或 Pod 自身超限,与节点整体压力分开。统计缺失时实现可能暂时跳过对应判断;这不是“未统计就允许永久超限”的保证,也不能把统计未出现误认为使用量为零。

DiskPressure 取决于哪个文件系统缺什么

节点磁盘压力既可能来自可用字节,也可能来自可用 inode。大量小文件可能在 df -h 仍有空间时耗尽 inode,因此空间与 inode 都要核对。

kubelet 根据运行时报告区分 nodefs、imagefs 以及可选的 containerfs;这些是资源统计与压力信号对应的文件系统,不是要求每个节点一定有三块磁盘。是否分盘、运行时支持与 feature gate 都会改变可用信号及回收路径。

buildSignalToRankFunc 根据文件系统布局选择参与排序的统计项。例如有独立 imagefs 的布局中,nodefs 压力主要关联日志和本地卷,imagefs 压力则对应另一组统计。不能只按 Pod 总磁盘占用排序,也不能照搬内存 QoS 的顺序判断哪个 Pod 一定先被驱逐。

正在加载 GitHub 文件…

在节点压力路径中,kubelet 先尝试相应的节点级回收,例如清理无用容器或镜像;压力仍未解除时,才选择 Pod。删 PVC、调大 emptyDir sizeLimit 或重启业务,不一定能解决镜像文件系统的压力,需要对齐实际耗尽的存储与数据归属。

本文没有制造 nodefs、imagefs 或 inode 耗尽,也没有验证独立 containerfs 的运行行为;这些路径依据固定版本源码解释。实验主机还承载其他任务,节点满盘不适合作为本次取证手段。

固定小写入量验证卷超限

实验配置仅创建一个独立 Pod:普通磁盘型 emptyDir 的 sizeLimit 为 16Mi,Python 程序向卷写入固定 32Mi 后关闭文件并输出完成日志,再保持进程运行。容器 memory limit 设置为 128Mi,避免把这次写缓冲误测成 memory OOM。

配置、校验、结果和清理说明见 k8sdev/article-resources-storage。先按该目录 README 的固定提交准备步骤创建独立工作目录,再运行实验。

存储实验核对固定写入完成、具体卷超限消息与节点压力条件,关键取证代码如下。

正在加载 GitHub 文件…

实际结果为:

1
2
3
4
phase: Failed
reason: Evicted
message: Usage of EmptyDir volume "data" exceeds the limit "16Mi".
node DiskPressure: False

容器最后的退出码也是 137,但容器终止 reason 为 Error,Pod reason 明确是 Evicted。这与内存实验中的 OOMKilled 再次说明:不能仅凭退出码判断故障类型。这里由 kubelet 根据卷统计驱逐 Pod,节点条件快照没有报告磁盘压力。

脚本等待明确的 Pod Evicted 原因,并核对错误消息中的卷名与限制值。这个检查能够排除镜像错误、调度失败和容器 OOM,不能证明整个观察期间任何瞬间都没有磁盘压力,也不提供“超限后多少秒内一定被驱逐”的时效保证。

将数据生命周期放回排障判断

emptyDir 可以跨同一个 Pod 内的容器重启保留数据,但 Pod 被删除、驱逐后由控制器创建的新 Pod,不会自动继承旧 Pod 的 emptyDir 内容。需要持久保存的状态应使用适合该数据一致性和恢复要求的存储,而不是靠提高临时存储限制避免回收。

对于一次临时存储事故,先读 Pod 的 reason、message 和事件,再核对节点压力信号以及实际文件系统;确认是单个卷超限、容器日志或可写层增长,还是节点空间 / inode 耗尽。这样才能把调整限额、控制日志、清理无用镜像和扩展节点存储,分别对应到真正起作用的位置。

#Kubernetes #源码分析 #Kind