临时存储声明对节点磁盘的挤占,核心在于emptyDir等临时存储卷默认与宿主机系统盘共用空间,且不设上限,容器大规模写入时节点磁盘被瞬间打满,引发Pod驱逐、节点NotReady甚至整个集群瘫痪。
作为处理过多次K8s节点故障的从业者,每次看到磁盘告警,第一反应就是先看有没有容器在疯狂写临时数据,这问题看似简单,但坑极深,临时存储不是云盘,它是实打实写在你机器硬盘上的,要搞懂它怎么挤占磁盘、怎么排查、怎么根治,下面直接拆开讲。
k8s临时存储声明如何一步步占满节点磁盘
临时存储声明在Kubernetes里最常见的形态就是emptyDir,它创建时是空目录,Pod删除时连根拔起,听着很干净,但它有两个致命本性。
临时存储卷与系统盘共享生存空间
emptyDir的默认行为,是直接挂在宿主机kubelet的工作目录下,通常落在/var/lib/kubelet路径里,这个路径属于谁的磁盘分区?在没有单独挂载数据盘或单独划分kubelet分区的情况下,它就是系统盘的一部分。
行业共识是,节点系统盘既要跑操作系统关键日志,又要承担容器镜像层、kubelet的Pod状态文件,现在还要被emptyDir当作数据仓库,三股写入流量汇到同一块盘上,磁盘IO和空间双双承压。
很多团队在初始化集群时,压根没给kubelet或容器数据单独划卷,结果是临时存储声明在毫无感知的情况下,把系统盘最核心的根分区慢慢塞满。
emptyDir写入量不受配额硬性约束
更关键的是,emptyDir没有默认大小限制,你可以在Pod里声明一个挂载了容量限制的持久卷,但emptyDir从创建起就不带任何配额属性,除非你在Pod spec里显式配置sizeLimit,否则一个应用往临时目录里写100GB数据,kubelet不会说一个”不”字。
emptyDir也没有内存型那种hugepages的隔离性,没法通过cgroup的磁盘配额直接掐住它对空间的占用,它就像住你家的一个不交租的租客,能占多大地方完全看它心情。
容器日志与镜像层是额外的隐性挤占
磁盘挤占不会只来自声明本身,容器运行时往/var/log/pods写入的日志文件、镜像拉取后的解压层,同样落在kubelet的数据目录里,即便挂载了emptyDir,日志暴涨时,节点磁盘依然会告急。
这里有个极易忽视的叠加效应:容器写满了emptyDir声明的限定值,应用本身还在不停输出日志,节点Proxy组件还要写缓存,这些写入路径交织在一起,磁盘就像被三四个水管同时注水的水池,早晚漫出来。
节点磁盘被临时存储声明挤占后的典型故障现象
磁盘满这件事有一个特点:它不会第一时间表现为Pod失败,而是表现为紊乱,你看到的往往是连锁故障。
故障从Pod驱逐触发,雪崩到节点失联
当节点磁盘使用率逼近kubelet的驱逐阈值(默认通常为85%或90%),kubelet会开始执行驱逐操作,先是标记节点磁盘压力,然后对使用临时存储超过limit的Pod发出终止信号。
典型的现象是Pod卡在Terminating状态长达几分钟不动,这大概率是磁盘写入已经卡死,容器进程无法正常退出,此时节点上的Pod开始大面积重启,但不是因为资源不够,而是因为活不下去了。
系统关键组件因写盘失败而罢工
当临时存储声明把磁盘占满后,最先崩溃的不是业务应用,而是那些依赖写盘的系统组件。容器运行时在创建新容器时,需要写入容器配置文件和日志,一旦磁盘没有inode可用,Pod创建请求直接报错。
节点本身也会进入NotReady状态,kubelet要定期上报心跳给控制面,它需要把节点状态写进本地缓存,磁盘满意味着这个动作失败,控制平面收不到心跳,就会把节点标记为不可用,进而触发上面业务的二次驱逐,形成雪崩。
排查时需要看清的指标与命令
排查节点磁盘到底是谁在占,通用的操作路径是SSH登录节点后,用du命令按目录排查,先看根目录整体占用:
- 先执行
df -h /看根分区剩余空间 - 然后执行
du -sh /var/lib/docker/或/var/lib/containerd/定位是镜像还是容器层爆了 - 再执行
du -sh /var/lib/kubelet/pods/,逐个找到体积最大的Pod目录
找到目标容器后,进入该Pod目录下的volumes/kubernetes.io~empty-dir文件夹,ls -lh 看哪个volume文件最大,这是最朴素也最有效的定位方法。
如何根治临时存储声明对节点磁盘的挤占
问题发生后,单纯的删除大文件只是解燃眉之急,要从机制上防止临时存储声明把节点磁盘挤爆,几个方案按推荐程度排列。
为emptyDir场景设置sizeLimit并配合驱逐
如果业务确实适合用emptyDir,给每个Pod的临时卷设置sizeLimit是第一步,这相当于给租客规定了居住面积上限,配置方式是在Pod的spec.volumes下设置:
- 通过
emptyDir.sizeLimit声明最大可用空间,比如500Mi或1Gi - 当写入超过该限制时,kubelet会判定该Pod超出临时存储配额,主动驱逐它
这是最直接的防止节点磁盘整体被打满的手段,代价是没有自动缩容能力,写满就重启,业务需要容忍这种方式的故障转移。
调整kubelet驱逐阈值并分离存储路径
把kubelet数据目录放到独立分区,是治本的关键一步,如果你的节点是云虚拟机,务必挂载一块独立数据盘,并把容器数据和kubelet数据目录放到新盘上。
具体操作是通过容器运行时或kubelet的启动参数指定数据根目录:
- containerd:通过
/etc/containerd/config.toml里的root参数指向新盘 - kubelet:通过
--root-dir参数把默认的/var/lib/kubelet改到新分区
同时给kubelet加上imagefs和nodefs分离的配置,这样即使临时存储声明把kubelet所在的分区写满,系统盘上的操作系统核心文件和日志还能正常工作,系统盘和数据盘被炸了,至少机器还能SSH上去救火。
用持久化卷声明代替临时存储声明
对写入量较大且要求数据不丢的业务,应该在根因上就放弃emptyDir,行业共识是,有状态的数据写入应当使用PersistentVolumeClaim以及对应的持久化存储,而不是赌节点本地盘够大。
替换操作路径很简单:在Deployment里将原emptyDir占用的路径改挂PVC,后端接云厂商的块存储或网络文件存储,随之而来的优势是容量可弹性扩展,不用跟节点宿主机磁盘较劲。
容器日志通过logrotate和配置做限额
临时文件挤占往往和日志混在一起,给容器运行时配置日志轮转参数,限制单个容器日志文件大小和最多保留个数,能避免日志把磁盘余量吞干净,以containerd为例,需要在配置里设置max_size和max_file参数。
更进一步的动作是在节点上配置/etc/logrotate.d/下的轮转规则,每天压缩历史日志,并清理超过N天的归档文件,这属于运维日常保养,但能保证磁盘占用处于一个稳定水位。
价格与性能视角下的方案取舍:临时存储与持久卷的成本博弈
挑解决方案的时候,成本和性能逃不开,emptyDir之所以受欢迎,核心是免费且快,它直接写宿主机本地盘,不经过网络,延迟极低,但免费的代价就是承载着节点磁盘爆掉的风险。
持久化存储按使用量计费,云厂商的云盘价格通常按容量与IOPS收取费用,有的团队为了省一点钱,把日志和临时缓存数据全压在emptyDir上,结果生产节点磁盘满导致事故,人工排查成本远大于云盘费用,这里需要做个权衡:
- 若业务日志量极大且非核心,用emptyDir配合sizeLimit即可,省成本
- 若临时数据丢失会影响业务恢复流程,则必须上PVC,哪怕多花点存储费用
- 若数据必须持久化且需要快速恢复,文件存储或块存储是合理选择
价格上没有绝对的便宜,只有场景是否匹配。
临时存储声明挤占节点磁盘的典型误操作与风险规避
说了这么多方案,有些坑仍然值得专门拎出来讲,用错emptyDir的两种姿势最容易出事。
把emptyDir当成跨节点共享存储用
emptyDir的生命周期绑定Pod,且无法跨节点共享数据,但有人试图通过hostPath配合节点亲和性来实现共享,这属于高危操作。
hostPath直接把宿主机目录暴露给Pod,且不具备任何配额能力,一旦Pod崩溃或容器被删除,如果声明的是清理策略,数据直接没了,更重要的是,hostPath会让Pod权限直接映射到宿主机目录,安全风险极大,它就是临时存储声明失控和节点磁盘被占的升级版陷阱。
不使用临时存储声明性能反而下降的情形
有些应用需要高频读写大量小文件,比如数据处理类的中间件,其内部临时排序文件就是通过临时目录写的,如果把这类路径从emptyDir改成网络持久卷,延迟会陡增,吞吐量大幅下降,这种情况下,反而是保留emptyDir更合适。
要规避风险,需要注意以下两点:
- 为这类高I/O应用单独准备本地SSD节点池,让emptyDir写在高速盘上
- 控制节点池上Pod部署密度,防止多个高写入应用共享同块磁盘
针对这个问题运行中的多节点集群,还有个细节容易被漏掉:在节点上运行 du 排查时,容易混淆overlay2镜像层与emptyDir卷的占用比例,如果发现某个containerd的mount目录极大,需仔细核对是镜像本身肥大,还是容器在运行中写入过大,镜像层占用的空间可以通过清理废弃镜像来回收,而emptyDir卷的占用则必须动Pod本身,两个不同的动作,别搞混。
最终结论与临时存储管理的闭环建议
临时存储声明对节点磁盘的挤占,本质是无边界写入与有限磁盘容量之间的矛盾,解决的起点是承认emptyDir是不可控的,所有挂载emptyDir的路径都必须有对应的配额或日志轮转兜底,用独立数据盘分离kubelet数据与系统盘,是陌生环境下最稳妥的防御策略。
Kubernetes临时存储挤占节点磁盘的常见问题
问题1:设置了emptyDir的sizeLimit后,超过限制一定会被驱逐吗?
不一定立即驱逐,kubelet的驱逐机制是周期性检查存储使用量,并且驱逐行为遵循--eviction-minimum-reclaim和垃圾回收时间间隔,当Pod超过limit时,kubelet会在下一轮检查中发出驱逐信号,但如果磁盘压力还有其他资源干扰,驱逐动作可能稍有延迟,更大的不确定性在于,sizeLimit只作用于emptyDir声明目录,容器日志不在这个限制范围内,日志依旧可能打满节点磁盘。
问题2:节点磁盘被临时存储打满后,为什么很难SSH上去删除文件?
磁盘空间满和inode满可能导致这条行为路径完全不同,当inode耗尽时,连创建临时文件都失败,SSH的登录进程无法写入必要的session文件,登录直接被拒,当空间满但inode充足时,SSH可以登录,但命令行工具如ls、rm都可能因为需要写临时缓存而卡住,解决思路是预留一点空间缓冲区,确保系统分区永远有几百MB的硬预留,使用LVM时可以通过设置reserve预留空间来实现。
问题3:本地持久卷(local PV)和emptyDir在节点磁盘挤占上有什么区别?
本地持久卷和emptyDir的文件落盘机制相似,但local PV多了一层生命周期隔离,emptyDir随Pod销毁而删除,看起来自动释放了空间,但Pod频繁重建时会不断进行大块空间分配与回收,容易产生磁盘碎片,在极端场景下反而抬高磁盘IO压力,local PV的数据生命周期由PVC管理,即使容器和Pod集体挂掉,数据依然存在,节点磁盘的使用率保持平稳,不会出现emptyDir瞬间填满又瞬间清零的剧烈波动,如果业务必须频繁重启且需要稳定磁盘资源,local PV的隔离性,会在可用性上明显优于emptyDir。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/776419.html

