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

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

<!--more-->

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

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

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

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

`emptyDir.medium: Memory` 的数据会计入相应内存使用；把缓存从磁盘改成 tmpfs，可能把磁盘问题转为 OOM 问题。这个区别与[内存 OOM 和驱逐文章](/posts/kubernetes-memory-oom-eviction/)中的 memory limit 对应。

资源定义与文件系统布局要求见 Kubernetes v1.35 的[本地临时存储管理文档](https://v1-35.docs.kubernetes.io/docs/concepts/storage/ephemeral-storage/)。节点采用不受支持的挂载布局时，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`](https://github.com/kubernetes/kubernetes/blob/v1.35.1/pkg/kubelet/eviction/eviction_manager.go#L508-L538) 对每个有统计数据的活跃 Pod，依次检查：

{{< github-file url="https://github.com/kubernetes/kubernetes/blob/v1.35.1/pkg/kubelet/eviction/eviction_manager.go#L508-L538" >}}

```text
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-file url="https://github.com/kubernetes/kubernetes/blob/v1.35.1/pkg/volume/metrics_du.go#L65-L72" >}}

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

{{< github-file url="https://github.com/kubernetes/kubernetes/blob/v1.35.1/pkg/volume/util/fs/fs.go#L73-L105" >}}

在满足文件系统、用户命名空间和相关 feature gate 条件时，project quota 监测可以更准确地跟踪这类仍占空间的文件；`LocalStorageCapacityIsolationFSQuotaMonitoring` 控制相关能力。这里的 quota 用于监测，不会把 Kubernetes 的临时存储 limit 自动变成每次写入都由内核拒绝的硬限额。本文未启用或运行验证这条分支，条件与限制以[官方计量说明](https://v1-35.docs.kubernetes.io/docs/concepts/storage/ephemeral-storage/#resource-ephemeralstorage-consumption)为依据。

{{< github-file url="https://github.com/kubernetes/kubernetes/blob/v1.35.1/pkg/volume/util/fsquota/quota.go#L30-L55" >}}

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

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

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

```mermaid
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`](https://github.com/kubernetes/kubernetes/blob/v1.35.1/pkg/kubelet/eviction/helpers.go#L1151-L1198) 根据文件系统布局选择参与排序的统计项。例如有独立 imagefs 的布局中，nodefs 压力主要关联日志和本地卷，imagefs 压力则对应另一组统计。不能只按 Pod 总磁盘占用排序，也不能照搬内存 QoS 的顺序判断哪个 Pod 一定先被驱逐。

{{< github-file url="https://github.com/kubernetes/kubernetes/blob/v1.35.1/pkg/kubelet/eviction/helpers.go#L1151-L1198" >}}

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

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

## 固定小写入量验证卷超限

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

配置、校验、结果和清理说明见 [k8sdev/article-resources-storage](https://github.com/jimyag/k8sdev/tree/3cfa654905a750b1716de8c32aa42d7573220954/article-resources-storage)。先按该目录 README 的固定提交准备步骤创建独立工作目录，再运行实验。

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

{{< github-file url="https://github.com/jimyag/k8sdev/blob/3cfa654905a750b1716de8c32aa42d7573220954/article-resources-storage/run.py#L26-L36" >}}

实际结果为：

```text
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 耗尽。这样才能把调整限额、控制日志、清理无用镜像和扩展节点存储，分别对应到真正起作用的位置。

