运维笔记

Spring Boot 只读文件系统 Pod 崩溃?三步修复 Kubernetes 安全加固后的临时目录问题

Cloud & DevOps 技术可视化

兄弟们,今天聊一个特别蛋疼的问题。

我们团队上个月做安全合规,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)以及一线技术博客的实战经验分享。

Elvin Hui

关于作者:Elvin Hui

Elvin 拥有 10+ 年企业级数据中心、云原生架构和网络安全经验。持有 CCNA、AWS 解决方案架构师认证。我致力于将一线的“踩坑”经验沉淀为真实、硬核的技术指南,拒绝空洞理论。