兄弟们,今天聊一个特别蛋疼的问题。
我们团队上个月做安全合规,K8s 集群全面启用 readOnlyRootFilesystem: true。按道理这是标准操作,安全扫描一跑,绿油油的,完美。结果?Spring Boot 3.0 应用直接崩了,Pod 疯狂 CrashLoopBackOff。
日志一看,就一句话:java.io.IOException: No space left on device。
等等,这不是空间不足,而是文件系统不让写。
症状:Pod 起不来,日志全是临时文件写入失败
你打开 kubectl logs,看到的不是业务异常,而是 Tomcat 启动时的 IO 错误:
org.apache.catalina.startup.Catalina.start: Error starting Tomcat context
java.io.IOException: The temporary directory is not writable
at org.apache.catalina.startup.Tomcat.init(Tomcat.java:467)
Spring Boot 内嵌的 Tomcat 在启动时,需要往 /tmp 或者 java.io.tmpdir 指向的目录写临时文件——JSP 编译、session 序列化、上传文件解析,全都要写。
但 readOnlyRootFilesystem: true 把整个根文件系统锁死了。/tmp 挂载的是 rootfs 的一部分,自然也没法写。
根因分析:Spring Boot 的临时目录依赖
这事说白了,Spring Boot 的锅?不完全是。
Spring Boot 本身没有做只读文件系统的适配。Tomcat、Undertow、Jetty 这些内嵌容器,默认都会在 java.io.tmpdir 下创建临时目录。如果你把整个容器文件系统设成只读,又不给它们一个可写的临时目录,那就直接翻车。
更坑的是,有些应用还会在运行时写日志、写缓存、写 Hibernate 二级缓存。你不给写权限,应用就炸。
修复步骤:三步走,不改代码
第一步:给 Pod 挂载 emptyDir 到 /tmp
这是最直接的方案。在 Deployment 里加一个 emptyDir 卷,挂到 /tmp 上。
apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-boot-app
spec:
template:
spec:
containers:
- name: app
image: myapp:latest
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
emptyDir 本质上是 Pod 生命周期内的临时存储,默认挂载为可读写。K8s 不会对这个目录做只读限制,因为它是独立挂载点,不是 rootfs 的一部分。
第二步:显式设置 java.io.tmpdir
有些场景下,Spring Boot 可能不认 /tmp,或者你的应用自己定义了其他临时路径。保险起见,通过环境变量强制指定:
env:
- name: JAVA_TOOL_OPTIONS
value: "-Djava.io.tmpdir=/tmp"
或者直接在 Dockerfile 里加:
ENV JAVA_TOOL_OPTIONS="-Djava.io.tmpdir=/tmp"
第三步(可选):给其他需要写的路径也挂 emptyDir
你的应用可能还需要写别的地方——比如日志目录、上传目录、缓存目录。
volumeMounts:
- name: tmp
mountPath: /tmp
- name: logs
mountPath: /var/log/app
- name: uploads
mountPath: /var/uploads
volumes:
- name: tmp
emptyDir: {}
- name: logs
emptyDir: {}
- name: uploads
emptyDir: {}
注意:emptyDir 的数据不会持久化,Pod 删除就丢了。如果你需要持久化日志,改用 hostPath 或者 PVC。
对比:不同临时存储方案
| 方案 | 持久化 | 性能 | 适用场景 |
|---|---|---|---|
| emptyDir | 否(Pod 删除即丢) | 高(本地盘) | 临时文件、缓存 |
| hostPath | 是(节点级) | 高(本地盘) | 日志采集、监控 |
| PVC | 是(集群级) | 中(依赖后端存储) | 需要持久化的数据 |
| tmpfs | 否(内存) | 最高 | 敏感临时数据 |
社区吐槽:这问题早该修了
Reddit 上有人发帖说,Spring Boot 3.0 发布这么久,对只读文件系统的支持依然稀烂。“We stumbled upon a problem, that we can’t run applications in Docker in a read-only filesystem.” 这条帖子在 Docker 官方 repo 里挂了两年都没解决。
还有人直接开喷:“The docs are garbage on this part.” 确实,Spring Boot 官方文档对只读文件系统的适配建议写得非常模糊,只说“确保临时目录可写”,但怎么确保?没写。
FAQ
Q: 为什么 readOnlyRootFilesystem 会导致 Spring Boot 启动失败?
A: Spring Boot 内嵌的 Tomcat 在启动时需要在 java.io.tmpdir(默认 /tmp)创建临时文件。如果 rootfs 是只读的,写入操作会抛出 IOException,导致 Pod CrashLoopBackOff。
Q: emptyDir 和 tmpfs 有什么区别?
A: emptyDir 默认使用节点本地磁盘,数据会占用磁盘空间;tmpfs 使用内存,速度更快但受内存限制,且数据不持久。对于临时文件,emptyDir 足够,tmpfs 适合对性能敏感的场景。
Q: 不修改代码能解决吗?
A: 可以。通过挂载 emptyDir 到 /tmp 即可解决,不需要改一行 Java 代码。如果需要写其他目录,同理挂载 emptyDir。
Q: 这个方案安全吗?
A: 安全。emptyDir 的写入权限只限于挂载的目录,不影响 rootfs 的只读策略。攻击者无法修改系统文件,只能写临时目录,风险可控。
总结
readOnlyRootFilesystem 是 K8s 安全加固的标配,但 Spring Boot 对它的支持确实拉胯。解决方案不复杂:挂 emptyDir 到 /tmp,顺手设个 java.io.tmpdir 环境变量。
别指望 Spring Boot 官方在短期内修复这个兼容性问题——社区吐槽了两年都没动静。自己动手,丰衣足食。
社区灵感与参考 (References & Community Insights)
本文探讨的架构演进与技术实现方案,深度提炼自 Hacker News、Reddit 等极客社区的真实工程师讨论、线上事故复盘(Post-mortems)以及一线技术博客的实战经验分享。