systemd-tmpfiles 清理规则实战:d/e/v 的区别、Age 判定与站点临时目录的正确配法

有一类问题特别烦人:磁盘没满、日志也清了、内存也够,但站点每天凌晨三四点准时卡两分钟,或者 /tmp 里的东西莫名消失导致上传失败。翻遍 Nginx 和 PHP 的配置都找不到原因,最后发现是 systemd 的系统定时任务在半夜清了一堆你以为还在的文件。

systemd 时代,清理临时文件这件事不再只是 crontab 里那条 find /tmp -mtime +7 -delete。它有一套自己的规则文件和三个触发的 timer。这套机制好用,但默认值对"跑着网站和容器的机器"来说偏激进,而且它的规则分散在七八个文件里,出问题时很难一眼看出是谁干的。

这篇文章把 systemd-tmpfiles 的三件事讲清楚:规则怎么写、什么时候执行、怎么在不动系统默认值的前提下管好自己那几个目录。

一、三个 timer 到底谁在清什么

很多人以为 tmpfiles 就是"清 /tmp",实际上它被拆成了三个独立单元,职责不一样:

  • systemd-tmpfiles-clean.timer:只管按规则删除过期文件,按 Age 参数判断。默认每天跑一次,启动后延迟 15 分钟。
  • systemd-tmpfiles-setup.timer:启动时创建规则里声明的目录、文件、符号链接,并修正属主和权限。不删东西。
  • systemd-tmpfiles-setup-dev.timer:只管 /dev 下的设备节点,一般不用管。

先确认它们当前的状态和上一次执行时间,这是排查的起点:

systemctl list-timers 'systemd-tmpfiles*' --all
systemctl status systemd-tmpfiles-clean.timer
journalctl -u systemd-tmpfiles-clean --since "3 days ago" --no-pager

list-timers 能直接看到 NEXT 和 LAST 两列,如果 LAST 是很久以前,说明 timer 没在跑(常见于被 systemctl mask 过、或者容器里没有 systemd 的 init)。journalctl 里能看到每次执行刷了哪些路径,这是事后追溯"谁删了我的文件"最直接的证据。

要看全部生效的规则(包含发行版自带的),用:

systemd-tmpfiles --cat-config | less

这个命令会把 /usr/lib/tmpfiles.d、/run/tmpfiles.d、/etc/tmpfiles.d 三个位置的所有配置按优先级拼接后输出。同名文件后者覆盖前者,这是覆盖发行版默认值的关键,也是很多人改错的根源——直接改 /usr/lib 下的文件,包一升级就被覆盖回去了。

二、规则文件的一行到底在写什么

规则格式是空格分隔的若干字段,前两个是类型和路径,后面全是可选。一条完整规则长这样:

# 类型 路径                 模式   属主  属组  年龄   参数
d    /var/cache/myapp       0755   www    www   -      -
d    /var/tmp/myapp-upload  0750   www    www   7d     -
e    /var/log/myapp         0755   root   root  14d    -

各字段的含义:

  • 类型:单字母。d 建目录,f 建文件,e 保留目录但清理里面过期的内容,v 建目录但只清空子目录(不删子目录本身),z 只改属主权限不创建,X 只在目录不存在时创建。日常最常用的是 d、e、z 三个。
  • 模式:八进制权限。写了就会强制 chmod,哪怕目录已经存在。这一条踩过坑:如果你给上传目录写了 0775,而程序自己把某个子目录设成 0700,每次 timer 跑完权限就变回去,然后程序下一次写入行为异常。
  • 属主 / 属组:同样会强制 chown,写上 - 表示不改。用 - 比写死 root 更安全,避免误改用户。
  • 年龄:7d、12h、30min、2w 这类写法。只有 Age 非 - 时才会清理,写 - 就是永不删。
  • 参数:逗号分隔的开关,subvol=、xattr=、nosuid、nodev 等,日常很少用。

d、e、v 三个类型的差别必须分清

这是最容易出错的地方,用一个实际场景说明。假设 /var/www/uploads 下有一堆用户的子目录,每个子目录里是文件:

  • d /var/www/uploads 0755 www www -:只保证这个目录存在、权限对。不会删任何东西,因为没有 Age。这是安全的。
  • e /var/www/uploads 0755 www www 30d -:会遍历目录下所有一级内容,把修改时间超过 30 天的条目删掉,包括整个子目录。所以如果用户子目录本身的 mtime 很老(子目录只有在内部增删文件时 mtime 才更新,改文件内容不更新),即使里面有新文件,整个子目录也会被删。这是最典型的"数据莫名其妙少了"的原因。
  • v /var/www/uploads 0755 www www 30d -:会删掉过期的文件,但子目录只清空不删除。相对温和一些。

给内容站的上传目录配 e 加一个很短的 Age,是事故的经典配方。稳妥的做法是:上传目录只用 d 不带 Age;确实需要清理的目录(比如缓存、会话、缩略图临时文件)单独开一个目录,配一个足够长的 Age(比如 30d 或 60d),并且确保清理脚本能区分"该删的"和"不该删的"。

三、默认值里那些会咬人的地方

Debian/Ubuntu 的 /usr/lib/tmpfiles.d/tmp.conf 大意是把 /tmp 设成 10 天、/var/tmp 设成 30 天。这看起来挺宽松,但有两个细节要留意:

第一,Age 的判断依据是 mtime(在较新版本里是 mtime/atime/ctime 里最新的那个,取决于 --clean 的实现)。一个上传目录如果放了文件但一直没改,10 天后 /tmp 里那些东西会被清掉。PHP 默认的 upload_tmp_dir 和 session.save_path 如果没指定,就是走的系统临时目录,所以"用户传完文件过一会儿再提交表单,提示找不到临时文件"这类问题,跟 tmpfiles 有直接关系。

第二,PrivateTmp=true 的服务有自己独立的 /tmp 命名空间(实际挂在 /tmp/systemd-private-xxxx-<unit>/tmp),它同样受 tmp.conf 管辖,但你在主机 /tmp 里看不到。所以排查"临时文件消失"要先确认进程看到的 /tmp 到底是哪个。

# 看某进程实际的 /tmp
sudo ls -l /proc/<pid>/root/tmp | head
# 看该进程的挂载命名空间
sudo lsns -t mnt -p <pid>

推荐的处置是把站点的临时目录从系统 /tmp 移出来,明确定义:

# /etc/tmpfiles.d/myapp.conf
d /var/lib/myapp/tmp      0750  www  www  -   -
d /var/cache/myapp/sess   0700  www  www  -   -
d /var/cache/myapp/thumbs 0755  www  www  30d -

前两个不带 Age,永不清理;第三个是缩略图缓存,30 天清一次。然后在 php-fpm 的 pool 配置里改 php_admin_value[session.save_path] = /var/cache/myapp/sess、php_admin_value[upload_tmp_dir] = /var/lib/myapp/tmp,并且把 session 的 gc 关掉或调长:php_admin_value[session.gc_maxlifetime] = 86400、php_admin_flag[session.gc_probability] = 0,交给 tmpfiles 统一管。这样两个清理器不会互相打架。

改完立刻生效,不需要重启:

systemd-tmpfiles --create /etc/tmpfiles.d/myapp.conf
systemd-tmpfiles --clean  /etc/tmpfiles.d/myapp.conf --dry-run   # 先看会删什么

--dry-run 会打印它打算做的每一个动作,这个习惯能省掉很多后悔。

四、和 logrotate、容器、cron 的关系

这里有一个边界必须划清:tmpfiles 不负责日志轮转。它按 Age 直接删文件或目录,没有"改名、压缩、保留 N 份"的能力。日志该由 logrotate 管,tmpfiles 只管"临时产物"。两者的 Age 容易冲突,比如 logrotate 保留 30 天、tmpfiles 对同一目录配了 7d,结果就是你以为有 30 天历史,实际每天被清到只剩 7 天。所以配置时一定要交叉检查:

# 看 logrotate 管哪些日志、保留多久
grep -rhE "^\s*(/|rotate |maxage)" /etc/logrotate.d/ | head -40
# 看 tmpfiles 里对 /var/log 有没有 Age
systemd-tmpfiles --cat-config | grep -E "^\s*[devzX]\s+/var/log"

容器场景另有一套规则。Docker 的容器层不走主机 tmpfiles,但挂载进容器的 /tmp 如果在主机上有对应目录,就照样会被清。更常见的容器问题是 json-file 日志涨满磁盘,那属于 Docker 自身的 max-size 配置,跟 tmpfiles 无关,别混在一起查。

还有一类机器上 tmpfiles 完全不起作用:精简容器、Alpine、以及任何没有 systemd 作为 init 的环境。这时候 systemctl 会报 "System has not been booted with systemd",别以为是坏了,直接退回 cron 方案:

# 每小时的清理,注意排除你不想删的东西
17 * * * * root find /var/cache/myapp/thumbs -mindepth 1 -mtime +30 -delete 2>/dev/null

五、"文件自己消失了"的排查顺序

这是运维里最常见的一类投诉,下面这个顺序比"东看西看"有效得多,核心原则是先确认是谁干的,再决定要不要阻止它:

  1. 查审计日志。如果开了 auditd,ausearch -f /path/to/dir -ts recent 能直接看到哪个进程 unlink 了文件。这是唯一能给出"凶手进程名"的证据,比任何猜测都靠得住。
  2. 查 tmpfiles 的历史执行。journalctl -u systemd-tmpfiles-clean --since "7 days ago" | grep <路径>,被它删的都会记日志(较新版本会打印 "Removing ...")。
  3. 查 cron 与 at。grep -r <路径> /etc/cron* /var/spool/cron/ 2>/dev/null,再加上 atq。自定义清理脚本是第二大来源。
  4. 查应用自身。很多框架有自己的缓存 GC,比如 Laravel 的 cache:prune-stale-tags、各种 CMS 的"清理临时附件"。这类通常有日志。
  5. 查是不是根本就没删。文件可能被 mv 到了别的目录,或者进程用 rename 挪走了。用 inotifywait -m -r --format '%T %e %w%f' --timefmt '%F %T' <dir> 挂一天,能抓到所有动作。

前四步都没结果、又确实反复发生时,inotifywait 是性价比最高的一招。它不改任何配置,只观察,抓到记录后再对症处理。

六、一份可以照着抄的配置模板

最后给一个给单机内容站用的完整模板,涵盖常见需求,注释写清了每条为什么这么配:

# /etc/tmpfiles.d/myapp.conf
# 类型 路径                       模式  属主 属组 年龄  参数

# 上传目录:只保证存在与权限,永不自动删除
d   /var/www/uploads             0755  www  www  -     -

# 会话目录:由 PHP 自己 GC 或由下面的定时任务按需清理
d   /var/cache/myapp/sess        0700  www  www  -     -

# 临时上传中转:比系统 /tmp 短,1 天足够(程序用完即删,这里是兜底)
d   /var/lib/myapp/tmp           0750  www  www  1d    -

# 缩略图:按天生成,30 天足够,可重建,删了不心疼
d   /var/cache/myapp/thumbs      0755  www  www  30d   -

# Nginx 缓存目录:只改权限,不清理(清理由 cache purge 负责)
z   /var/cache/nginx             0700  root root  -    -

注意最后一条用 z 而不是 d:Nginx 的 cache 目录结构由 Nginx 自己维护,tmpfiles 只帮忙保证权限对,不插手内容。这类"别人有自己的生命周期"的目录,一律用 z。

配套的验收清单,改完配置后跑一遍:

# 1. 语法与 dry-run
systemd-tmpfiles --create /etc/tmpfiles.d/myapp.conf
systemd-tmpfiles --clean  /etc/tmpfiles.d/myapp.conf --dry-run

# 2. 确认目录存在且权限正确
ls -ld /var/www/uploads /var/cache/myapp/sess /var/lib/myapp/tmp

# 3. 确认没有别的规则在动同一个路径
systemd-tmpfiles --cat-config | grep -E "uploads|myapp"

# 4. 确认 timer 是 active 的
systemctl is-active systemd-tmpfiles-clean.timer

第 3 步特别值得坚持做。站点上出过的清除事故,八成不是"配置写错了",而是"两条规则指向了同一个目录,一条是你写的,一条是发的或者装包带的"。把生效规则全打出来 grep 一遍,两分钟的事,能避免掉一整天的数据恢复。

说到底,tmpfiles 的默认值对一个跑着业务的机器来说是"够用但不够稳"的。它假设 /tmp 里的东西都是可丢弃的临时产物,而真实站点里经常把上传、缓存、会话都放进去。花半小时把这些目录挪到明确的位置、配上明确的 Age,后面就能省下无数次"文件怎么没了"的排查。

Last modification:November 14th, 2026 at 08:24 pm

Leave a Comment