一、先说背景:什么时候该用临时存储卷?
我见过不少团队上来就想给每个Pod挂PV,其实很多场景根本不需要持久化。缓存服务、排序时的临时文件、多容器间共享的临时数据------这些东西Pod一删就没了,用持久卷完全是杀鸡用牛刀。
Kubernetes的临时卷(Ephemeral Volume) 就是干这个的:它跟Pod同生命周期,Pod创建它创建,Pod删除它删除。容器重启不影响卷里的数据,但Pod被删就什么都没了。
典型适用场景:
- 同Pod内多个容器共享临时文件
- 缓存空间(比如磁盘归并排序)
- 耗时计算任务的检查点存储
- 高频读写的临时数据
临时卷里最常用的两种,就是emptyDir 和已经被弃用的gitRepo。下面一个一个讲。
二、emptyDir:最基础的临时存储卷
2.1 核心概念
emptyDir这名字起得很直白------一开始是空的目录。Pod被调度到节点上时,kubelet会创建一个空目录,Pod里的容器通过volumeMounts挂上去用。
生命周期就一句话:Pod在,卷就在;Pod删,卷就删。
有个细节容易搞混:容器崩溃重启不会丢数据,因为容器重启不等于Pod删除。只有Pod被彻底移除,emptyDir才会被清理。
2.2 基础配置:最小可用示例
apiVersion: v1
kind: Pod
metadata:
name: redis
spec:
containers:
- name: redis
image: redis:7.2
volumeMounts:
- name: redis-storage
mountPath: /data/redis # 容器内挂载路径
volumes:
- name: redis-storage
emptyDir: {} # 空配置,使用节点默认存储(通常是磁盘)
这是官方文档里的标准示例。emptyDir: {}表示使用节点磁盘,不限大小(实际受节点磁盘容量限制)。
2.3 进阶配置:sizeLimit限制容量
这是我强烈建议生产环境必配的参数。不设sizeLimit的emptyDir能把节点磁盘写爆,触发DiskPressure导致节点上所有Pod被驱逐。
volumes:
- name: cache
emptyDir:
sizeLimit: 10Gi # 限制最大10GB
超过限制会怎样?kubelet会在其周期性资源回收检测(约1分钟一个周期) 中发现超限,随后触发Pod驱逐。实测通常在超限后1分钟内看到Pod变成Evicted,报错类似:
Usage of EmptyDir volume "cache" exceeds the limit "10Gi"
社区里因为这个被坑的人不少------Redpanda、Trino、Crossplane等项目都有过类似issue。
2.4 内存模式:medium: Memory
如果你需要极高性能的临时存储,可以把emptyDir放到内存(tmpfs) 里:
volumes:
- name: fast-cache
emptyDir:
medium: Memory
sizeLimit: 2Gi # 内存模式下强烈建议设limit
三个关键注意事项:
- 数据存在内存里。如果Pod被驱逐或所在节点重启,tmpfs里的数据会彻底丢失;但单纯的容器进程崩溃重启不影响数据。
- tmpfs占用的是Pod的内存配额,会计入Pod的memory limit。
- sizeLimit必须小于Pod的memory limit,否则可能OOM。
我一般只在两种场景用内存模式:超高频率的缓存 ,或者需要修改/dev/shm大小(有些应用对共享内存有要求)。
补充说明 :默认情况下emptyDir使用节点的/var/lib/kubelet所在磁盘。如果你希望将emptyDir放到特定存储介质(如SSD),可以通过调整kubelet的--root-dir参数改变整体路径,但原生不支持按卷粒度指定不同的存储介质。
2.5 调度陷阱:sizeLimit不影响Pod调度
这是一个很容易被忽略的坑。
Kubernetes调度器(Scheduler)在决定Pod调度到哪个节点时,默认完全不检查节点剩余磁盘空间是否大于 emptyDir.sizeLimit,它只检查CPU和内存是否充足。
举个例子:节点A只剩1Gi可用磁盘,但你的Pod声明了sizeLimit: 10Gi。调度器一看CPU和内存都够,嗖一下就把Pod调度到节点A上了。然后kubelet发现磁盘空间不足,立刻把Pod驱逐(Evicted)。Pod进入Pending状态,重新调度,如果集群里所有节点磁盘都不够,这Pod就永远Pending在那里。
特别澄清 :上述说法有一个例外------如果Pod同时显式声明了resources.requests.ephemeral-storage,调度器会 将此请求值纳入节点磁盘容量的评估。但emptyDir.sizeLimit本身不作为调度依据,两者是独立配置的。
怎么破? 目前只能靠外部手段:
- 监控节点磁盘使用率,及时扩容或清理
- 使用StorageClass + 动态PV替代大容量emptyDir(注意:标准PV调度仅检查PV是否存在,不检查容量匹配,需配合外部调度扩展或容量感知调度特性)
- 在应用层控制写入量,避免打满磁盘
想自己验证这个行为? 可以在测试集群里创建一个sizeLimit: 100Gi的Pod,然后观察它是否能调度到一个实际磁盘剩余空间远小于100Gi的节点上。如果节点能成功运行该Pod(尽管随后会被驱逐),就说明调度时确实没把sizeLimit作为筛选条件。这是一个很好的动手实验,能帮你直观理解调度器的行为边界。
2.6 两个容易混淆的概念
|--------------------------------------|------------------------------------------------------------|
| 概念 | 说明 |
| emptyDir.sizeLimit | 单个emptyDir卷的容量上限,超了Pod被驱逐 |
| resources.limits.ephemeral-storage | 容器级别临时存储总限额,包含:容器可写层(rootfs的写入)、容器日志、以及挂载的emptyDir卷的总和 |
两个可以同时用。如果配了ResourceQuota,用户必须在Pod里显式声明ephemeral-storage limit,否则配额不生效。
emptyDir虽然好用,但它本身是空的。早期Kubernetes为了让开发者省事,搞了一个gitRepo卷,想帮你自动把代码clone到emptyDir里。听上去很美好,但接下来我要讲的,是这个功能为何成为Kubernetes历史上最大的"坑"之一。
三、gitRepo:已被弃用的存储卷类型(重要!)
3.1 它曾经是什么
gitRepo卷本质上就是emptyDir + 自动git clone。Pod启动时,Kubernetes帮你把Git仓库克隆到一个emptyDir里挂载给容器。
配置长这样(注意:这是 ❌ 错误示例,别抄):
# ❌ 已弃用,不要再用了
volumes:
- name: config
gitRepo:
repository: "https://github.com/example/config.git"
revision: "abc123"
3.2 为什么弃用?(三个硬伤)
第一,严重安全漏洞 。gitRepo在执行git clone时用的是节点上的root权限。攻击者可以通过构造恶意Git仓库的hooks目录,执行容器外的任意命令。
相关CVE:
- CVE-2024-10220:任意命令执行,影响kubelet 1.28.11及以下、1.29.0-1.29.6、1.30.0-1.30.2
- CVE-2025-1767:可访问同节点其他Pod的本地Git仓库
第二,功能残缺。不支持SSH密钥认证、不支持私有仓库、不支持分支选择、不支持稀疏检出、不支持LFS,克隆完就再也不更新。
第三,官方已放弃维护 。自Kubernetes v1.33起默认禁用 ;v1.36将永久移除,无法重新启用。API层面仍然接受带gitRepo的Pod,但kubelet会直接拒绝运行并报错。
3.3 如何发现集群中还在用gitRepo
先用命令扫一遍,心里有个数:
# 查找所有使用gitRepo的Pod
kubectl get pods -A -o json | jq -r '.items[] | select(.spec.volumes[]?.gitRepo != null) | "\(.metadata.namespace)/\(.metadata.name)"'
# 在YAML文件中搜索
grep -rn "gitRepo:" --include="*.yaml" --include="*.yml" .
找到的赶紧改,别等v1.36集群升级时Pod起不来才慌。
3.4 迁移方案:InitContainer + emptyDir(✅推荐)
官方推荐的替代方案是InitContainer + emptyDir:
apiVersion: v1
kind: Pod
metadata:
name: app-with-git
spec:
initContainers:
- name: git-clone
image: bitnami/git:2.45 # 轻量Git镜像
command:
- git
- clone
- --depth=1 # 浅克隆,省空间省时间
- --branch=main
- https://github.com/example/config.git
- /repo # 克隆目标目录,InitContainer执行后代码落在这里
volumeMounts:
- name: git-repo
mountPath: /repo
securityContext:
runAsNonRoot: true
runAsUser: 1000 # 非root运行,更安全
containers:
- name: app
image: registry.example.com/app:v2.0
volumeMounts:
- name: git-repo
mountPath: /config
readOnly: true # 生产环境建议只读
volumes:
- name: git-repo
emptyDir: {} # 底层还是emptyDir
这个方案的核心逻辑:InitContainer先跑,把仓库克隆到emptyDir里;主容器启动时,emptyDir里已经有代码了。
私有仓库认证也不怕,用Secret存OAuth Token或个人访问令牌(PAT)即可:
initContainers:
- name: git-clone
image: bitnami/git:2.45
command:
- /bin/sh
- -c
- git clone --depth=1 https://${GIT_TOKEN}@github.com/example/private-config.git /repo
env:
- name: GIT_TOKEN
valueFrom:
secretKeyRef:
name: git-credentials
key: token
# volumes和volumeMounts同上
认证小贴士 :强烈建议使用OAuth Token或PAT拼接到URL中。如果必须使用含@、#等特殊字符的密码,直接拼接会导致Illegal character报错,此时应改用GIT_ASKPASS环境变量方式传递。
四、验证方法
4.1 验证emptyDir是否正常挂载
# 创建测试Pod
kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
name: emptydir-test
spec:
containers:
- name: writer
image: busybox
command: ["/bin/sh", "-c", "echo 'hello' > /data/test.txt && sleep 3600"]
volumeMounts:
- name: data
mountPath: /data
- name: reader
image: busybox
command: ["/bin/sh", "-c", "sleep 3600"]
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
emptyDir: {}
EOF
# 验证reader容器能看到writer写的文件
kubectl exec emptydir-test -c reader -- cat /data/test.txt
# 预期输出: hello
4.2 验证sizeLimit是否生效
# 创建带sizeLimit的Pod
kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
name: size-limit-test
spec:
restartPolicy: Never
containers:
- name: writer
image: busybox
command: ["/bin/sh", "-c", "dd if=/dev/zero of=/data/bigfile bs=1M count=600 && sleep 3600"]
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
emptyDir:
sizeLimit: 500Mi
EOF
# 观察Pod状态------约1分钟内会被驱逐
kubectl get pod size-limit-test -w
# 最终会看到状态变为Evicted
4.3 验证gitRepo迁移方案
# 用上面3.4节的YAML创建Pod
kubectl apply -f app-with-git.yaml
# 检查InitContainer是否成功
kubectl logs app-with-git -c git-clone
# 预期输出: Cloning into '/repo'...
# 检查主容器是否能看到文件
kubectl exec app-with-git -- ls -la /config
五、常见问题(真实报错)
Q1:Pod被驱逐,报"Usage of EmptyDir volume exceeds the limit"
Usage of EmptyDir volume "cache" exceeds the limit "10Gi"
原因:emptyDir.sizeLimit设得太小,或者应用写入了超出预期的数据量。kubelet在周期性检测(约1分钟)时发现超限,触发了驱逐。
解决:
- 检查应用是否有日志/缓存无限增长的问题
- 适当调大sizeLimit
- 如果无法预估,考虑换用持久卷或增加日志轮转策略
Q2:内存模式下Pod被OOM Kill
原因 :medium: Memory的emptyDir计入Pod内存配额。sizeLimit + 容器本身内存使用 > Pod memory limit。
解决 :确保sizeLimit < Pod memory limit,预留足够内存给应用本身。
Q3:Pod一直Pending,节点磁盘明明还有空间
原因 :这就是我在2.5节提到的调度陷阱 。调度器只看CPU和内存,不看emptyDir.sizeLimit。但kubelet在启动Pod时发现磁盘空间不足,将其驱逐,导致Pod反复Pending-Evicted。
解决:
- 清理节点上的无用镜像和日志:
docker system prune -f(或nerdctl对应命令) - 扩容节点磁盘
- 减小emptyDir的sizeLimit
- 考虑使用PVC代替大容量emptyDir(需配合外部调度方案)
Q4:升级到Kubernetes v1.33+后,带gitRepo的Pod起不来
The gitRepo volume plugin is deprecated and is disabled by default
原因:v1.33起gitRepo默认禁用,v1.36彻底移除。
解决:按3.4节方案迁移到InitContainer。
Q5:gitRepo安全漏洞扫描报CVE-2024-10220
The Kubernetes kubelet component allows arbitrary command execution via specially crafted gitRepo volumes
原因:集群中还有Pod在使用gitRepo卷。
解决:立即按3.4节迁移。如果暂时无法迁移,至少升级到已修复的kubelet版本(1.28.12+、1.29.7+、1.30.3+)。
六、总结
三个核心要点:
- emptyDir是临时存储的首选 ------轻量、简单、跟Pod走。生产环境务必配
sizeLimit,别让一个Pod把节点磁盘写爆。同时记住:sizeLimit默认不影响调度 ,别指望它帮你做节点磁盘容量筛选(除非配合ephemeral-storagerequest使用)。 medium: Memory是把双刃剑------性能极高但占内存配额,sizeLimit必须小于Pod memory limit。容器崩溃重启不影响数据,但Pod被驱逐或节点重启数据就没了。- gitRepo已死,有事烧纸------安全漏洞+功能残缺+官方弃用,赶紧迁移到InitContainer+emptyDir方案。先用命令扫一遍集群,把还在用gitRepo的Pod找出来,按本文方案逐个整改。别等到v1.36升级时才发现Pod起不来。
如果觉得有用,欢迎分享给更多被gitRepo坑过的同事。
有问题或想补充?欢迎在评论区交流。