临时存储声明是节点磁盘被K8s写满的头号隐性元凶,根因在于emptyDir默认不设配额,Pod在节点上随便写,写满后kubelet只能在磁盘被挤爆后被动驱逐。大多数时候,你以为是日志惹的祸,真正把磁盘逼到绝路的,是那个从未被限制过的临时目录。
节点磁盘被占满怎么排查:先分清临时存储的三种身份
线上节点告警时,第一反应通常是看容器日志或镜像大小,但临时存储声明往往藏在最深处,Kubernetes里的临时存储不以持久卷的形式出现,而是直接烧节点的本地文件系统,这个特性让它在df -h里显得格外刺眼。
临时存储的三种常见身份
- emptyDir:生命周期跟随Pod,Pod删了它才清空,默认不设大小上限,Pod在运行期间往里面写大文件,写多少占多少。
- hostPath:直接挂载节点目录,比如
/var/log或/data,容器里对路径的写入等价于在节点上写入,防护意识薄弱时,一晚上就能占满整块盘。 - configMap与secret:挂载进容器时默认只读,但如果容器把内容复制到临时目录再处理,同样会在nodefs上再吃一份空间。
多数情况下,磁盘被挤占的链条是这样的:容器不断产生临时文件或缓存,写入emptyDir目录,而emptyDir没有大小限制,写满nodefs后触发节点DiskPressure,随后调度器停止调度新Pod,已有Pod开始被驱逐。
确认元凶的排查路径
第一步看节点状态:
kubectl describe node <node-name>
重点关注Conditions里的DiskPressure是否为True,如果为True,说明kubelet认为节点本地存储即将耗尽。
第二步看磁盘挂载:
df -h
确认是根分区还是/var/lib/docker(或/var/lib/containerd)所在分区告急,K8s的临时存储统计的是/var/lib/kubelet所在文件系统,通常就是根分区。
第三步定位大目录:
du -xh --max-depth=3 /var/lib/kubelet 2>/dev/null | sort -rh | head -20
在

/var/lib/kubelet/pods下,每个Pod目录里都有一个volumes子目录,kubernetes.io~empty-dir开头的就是临时存储的物理路径,找到体积异常的那几个,再kubectl get pod -A -o wide反查Pod归属,基本就能锁定肇事者。
emptyDir磁盘爆掉如何处理:配额和驱逐要一起管
定位到是emptyDir惹的祸之后,处理思路不是简单删文件,而是给所有可能写临时存储的 Pod 补上配额,如果你问“emptyDir磁盘爆掉怎么处理”,答案只有十六个字:显式声明配额,控制写入速率,依赖驱逐兜底,监控提前预警。
给emptyDir戴上sizeLimit口罩
在Pod的spec里加上emptyDir.sizeLimit,相当于给临时目录焊死一块天花板:
volumes:
- name: scratch
emptyDir:
sizeLimit: 1Gi
写满1Gi后,kubelet会尝试驱逐该Pod,但要注意,驱逐动作有延迟,而且容器的/dev/shm(emptyDir的medium: Memory模式)不受这个限制影响,如果你用了内存型emptyDir,它走的是/dev/shm,占的是内存而非磁盘,但体积过大会触发容器OOM,同样要设limit。
调度阶段的占位声明
光设sizeLimit不够,还需要在容器里声明本地临时存储的request和limit:
resources:
requests:
ephemeral-storage: "1Gi"
limits:
ephemeral-storage: "2Gi"
这个声明的作用比sizeLimit更前置,调度器看到requests后,会把它当作调度依据,确保节点可用临时存储足够容纳新Pod。limits则控制写入上限,超过后Pod被驱逐。
业内专家指出,生产环境里多数Pod根本不声明ephemeral-storage,等于让调度器对临时存储失明,这个字段是K8s从1.10版本引入的,至今仍有相当一部分集群没有启用,根因是应用开发者对临时存储没有归属感。
驱逐阈值和节点压力
kubelet本身有一套驱逐阈值,比如nodefs.available低于一定百分比就触发软驱逐或硬驱逐,临时存储声明的作用是让kubelet在单个Pod层面提前行动,而不是等整个nodefs被拖垮后做全局清理。

实操里可以把驱逐阈值调得激进一点,在/var/lib/kubelet/config.yaml里设置:
evictionHard: nodefs.available: "5%"
然后重启kubelet,阈值设得越低,磁盘被完全写满的风险越大,但节点可用率越高,这个参数要按业务容忍度调整。
一次节点磁盘被占满的现场复盘
光讲概念不好理解,说个真实场景,某天晚上收到的告警是“K8s磁盘空间不足挂载点只读”,登录节点后发现根分区已满,docker ps都跑不动了,Pod卡在ContainerCreating状态。
排查流程是这样的:
df -h确认根分区100%。du -sh /var/lib/kubelet/pods/逐个查看,发现一个目录占了近两百G。- 查看
/var/lib/kubelet/pods/xxx/volumes/kubernetes.io~empty-dir,里面堆满了临时渲染缓存。 - 用
kubectl get pod -n <namespace> -o wide | grep <node>反查,确认是某个后端的渲染服务。
这个服务在写emptyDir,而且没设sizeLimit,代码里也没有定时清理逻辑,最后处理方式是:先删掉临时文件止血,再修改Pod的ephemeral-storage配额,同时让研发把缓存清理逻辑加上,最后给Prometheus加了临时存储使用率的监控指标。
整个过程验证了一个道理:临时存储声明不是运行时保护,而是发布前就该写进清单的硬约束。
避免临时存储挤占节点磁盘的落地配置
如果你看过kubelet日志里那些eviction manager: attempting to reclaim nodefs的记录,就会明白光靠清理文件永远解决不了问题,配置层面需要一套组合拳。
| 存储类型 | 适用场景 | 风险等级 | 推荐做法 |
|---|---|---|---|
| emptyDir(普通) | 临时缓存、中间文件 | 高 | 必须设置sizeLimit,同时声明ephemeral-storage |
| emptyDir(medium: Memory) | 共享内存、乱序处理 | 中 | 限制/dev/shm大小,防止OOM连带节点故障 |
| hostPath | 日志采集、设备挂载 | 极高 | 尽量改用PVC或emptyDir,必须用则限定只读 |
| configMap/secret | 配置注入 | 低 | 确认容器未复制到本地目录二次写入 |
行业共识认为,临时存储要按一次性用品来管理,用完就丢,丢了不心疼,用emptyDir的业务代码必须设计成可容忍数据丢失的状态,比如中间结果可重算、缓存可重建。
再补充三个实战建议:
- 日志轮转配置要在容器层面就做好,别指望日志文件写出默认写满节点,Docker的json-file支持
max-size和max-file,containerd也有类似的配置,默认情况下不加限制,需要手动开启。 - 把大目录从emptyDir挪到PVC,PVC绑定的是云盘或分布式存储,单Pod写满也不会影响节点整体可用性。
- 给临时存储加监控告警,指标用
kubelet_volume_stats_used_bytes和container_fs_usage_bytes,阈值设在80%就该人工介入。
临时存储声明挤占节点磁盘相关问题
Q1:设置了sizeLimit之后,Pod被驱逐时数据会丢吗?
会丢,而且必须丢,emptyDir的生命周期跟随Pod,Pod被驱逐或删除时目录会被kubelet清空,这个行为是设计使然,如果业务需要Pod重建后数据还在,应该用PVC,而不是emptyDir。
Q2:emptyDir和hostPath,哪个更容易导致节点磁盘被占满?
从概率上看是hostPath更危险,因为它直连节点目录,容器写入就是节点写入,没有中间层过滤,但从默认配置看,emptyDir没有配额限制且使用频率更高,实际生产环境中emptyDir把磁盘写满的案例更多,因为大多数Pod用emptyDir就像用收银抽屉一样随意,从不关心里面塞了多少没用的东西。
Q3:临时存储告警时,应该先扩容还是先清理?
先定位再处理,不要急着扩容,用df和du找到具体占用路径,如果是临时文件的堆积,清理后就能恢复,扩容只是掩盖了配额缺失的问题,如果确认是业务快速增长导致的真实存储需求,再考虑给Pod调大限额,给节点加磁盘,顺序反了会让问题越滚越大。
