Linux /tmp 被定时清理致 Java 应用建不了临时文件

问题背景

公司微服务集群部署在 CentOS 7 服务器上,Java 应用(Spring Boot)运行时需要在 /tmp 下创建临时目录用于嵌入式 Tomcat 的文件上传缓存和 JNA 的 native 库解压。此前该环境稳定运行超过一年。

故障现象

3 月 10 日下午 4:00,运维团队完成了一批安全加固操作(包括配置 systemd-tmpfiles 清理规则)后,陆续有监控告警:订单服务、支付服务和文件上传服务的 error rate 开始上升。

日志中出现大量:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Caused by: java.io.IOException: Permission denied
    at java.io.UnixFileSystem.createFileExclusively(Native Method)
    at java.io.File.createTempFile(File.java:2024)
    ...
    at org.apache.tomcat.util.http.fileupload.disk.DiskFileItem.write()

Caused by: java.io.IOException: No such file or directory
    at java.io.UnixFileSystem.createFileExclusively(Native Method)
    ...
    at org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory

所有涉及临时文件创建的操作——文件上传、PDF 导出、Excel 生成——全部失败。

排查过程

第一步:检查 /tmp 状态

1
2
3
4
5
$ ls -la /tmp/
total 8
drwxrwxrwt.  3 root root   60 Mar 10 16:01 .
dr-xr-xr-x. 19 root root  280 Mar 10 15:58 ..
drwx------   2 app  app    40 Mar 10 15:58 hsperfdata_app

Spring Boot 默认创建的 /tmp/tomcat.* 目录全部不存在!

第二步:检查 /tmp 的挂载选项

1
2
$ mount | grep /tmp
tmpfs on /tmp type tmpfs (rw,nosuid,nodev,noexec,relatime)

noexec 标志——/tmp 被挂载为禁止执行。Java 的 JNA(Java Native Access)需要在 /tmp 下解压 .so 文件并加载,noexec 直接导致这个操作失败。

第三步:追溯变更

在安全加固中新增了 systemd-tmpfiles 配置:

1
2
3
$ cat /etc/tmpfiles.d/security-cleanup.conf
# 安全加固:每天清理/tmp下超过24小时未访问的文件
d /tmp 1777 root root 1d

关键问题在这一行 d /tmp 1777 root root 1d——它告诉 systemd-tmpfiles:

如果 /tmp 不存在,用权限 1777 创建;如果存在,清空所有超过 1 天的文件和目录

更致命的是,临时目录规则并不是简单的 rm——它在创建 /tmp不会自动加上 exec 选项,而运维恰好在 /etc/fstab 中也添加了 noexec

第四步:验证清理动作

1
2
3
4
5
$ systemd-tmpfiles --clean /etc/tmpfiles.d/security-cleanup.conf --dry-run
Would remove /tmp/tomcat.8080.1234567890
Would remove /tmp/tomcat.8443.1234567891
Would remove /tmp/jna-3506402
...

一次清理会删掉所有正在运行的 Java 应用创建的临时目录。由于 Spring Boot 的 Embedded Tomcat 在启动时创建了 /tmp/tomcat.* 目录,运行时持续使用。当 systemd-tmpfiles 在每天的定时任务中执行清理时,会连同正在使用的临时目录一起删除。

而且 Java 应用尝试重新创建时,因为 noexec 导致 JNA 的解压-加载流程失败,进而引发连锁失败。

解决方案

紧急恢复

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 1. 移除noexec
mount -o remount,exec /tmp

# 2. 修正systemd-tmpfiles规则
cat > /etc/tmpfiles.d/security-cleanup.conf << 'EOF'
# 只清理/tmp下的临时文件,不删除目录结构
# 排除Java和Tomcat的临时目录
q /tmp 1777 root root 1d
x /tmp/tomcat.*
x /tmp/jna-*
x /tmp/hsperfdata_*
EOF

# 3. 重启受影响的服务
systemctl restart order-service payment-service upload-service

长期方案

1
2
3
4
5
6
7
8
9
// 应用层指定独立的临时目录,不依赖/tmp
@Bean
public TomcatServletWebServerFactory tomcatFactory() {
    TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
    factory.addContextCustomizers(context -> {
        context.setTempDir(new File("/opt/app/temp"));
    });
    return factory;
}

在服务器上为每个应用创建独立的临时目录 /opt/<app>/temp/,脱离 /tmp 依赖。

根因分析

直接原因:安全加固中同时做了两件事——配置 systemd-tmpfiles 清空 /tmp 并给 /tmp 加了 noexec——导致 Java 应用的临时目录被删除后无法重新创建和加载 native 库。

深层原因:安全策略变更未在测试环境验证对业务应用的影响/tmp 目录被大量 Linux 应用隐式依赖,任何对其的变更都应该做充分的影响评估。

预防措施

  1. 应用临时目录独立化:每个微服务使用独立的 /opt/<app>/temp/,不共用 /tmp
  2. 安全变更影响评估:对基础环境(/tmp/etc/fstab、内核参数等)的变更,必须先在测试环境验证 48 小时
  3. 禁止盲加 noexecnoexec/tmp 更安全,但必须确认所有应用兼容后才上线
  4. 监控 /tmp 异常:添加 Prometheus 指标监控 Java 应用是否能正常创建临时文件
  5. 变更窗口制度:基础环境变更必须在低峰窗口执行,且保留回滚 SOP

总结

/tmp 是 Linux 上最"透明"又最危险的目录——几乎所有应用都在用它,但没人专门管理它。运维想做安全加固,方向没错,但 noexec + 定时清理这组组合拳在非 JNA 环境下可能没事,一碰到 Java 的 native 库加载就炸了。教训是:对共享资源的变更,影响的不是你自己用的那个应用,而是所有依赖它的应用。

使用 Hugo 构建
主题 StackJimmy 设计