1. 先搞清楚 Linux 上 atime、mtime、ctime 到底是什么
1.1 三个时间存在哪:从 inode 说起
Linux 上每个文件在磁盘上都有一个 inode,文件的元数据几乎都塞在里面:权限、属主、大小、指向数据块的指针,以及三个时间戳。文件名本身不在 inode 里,inode 里存的只是"这个数据对象"的信息。理解这一点非常重要,因为后面讲到的很多"反直觉"现象——比如重命名文件后 mtime 没变、chmod 之后 mtime 没变但 ctime 变了——根源都在 inode 这一层。
三个字段的名字分别是st_atime、st_mtime、st_ctime,在struct stat里都是秒级的时间戳(现代内核还提供纳秒精度的st_atim.tv_nsec等扩展字段)。它们存的都是 UTC 时间戳(epoch 秒数),ls和stat显示出来的是本地时区换算后的结果。这一点必须先说清楚,因为我见过太多人把"时区显示不对"当成了"文件时间被改坏了",方向一错,后面全是白折腾。
1.2 三个时间的准确定义与触发条件
先把定义列清楚,这是后面所有命令的基础。
- atime(Access Time):文件内容最后一次被"读"的时间。注意是内容被读取,不是你自己用肉眼看到文件。
cat、grep、less、cp(作为源文件被读)都会更新它。 - mtime(Modify Time):文件内容最后一次被修改的时间。写入、追加、截断、用编辑器保存,都会更新 mtime。你在
ls -l里看到的那一列默认就是 mtime。 - ctime(Change Time):inode 状态最后一次发生变化的时间,也叫状态变更时间。内容变了会更新它,但权限、属主、硬链接数、甚至文件名所在目录项的变化,也会更新它。
这里的关键差异是:mtime 只认内容,ctime 认的是 inode 上的一切变化。所以chmod、chown、ln这些不碰内容只碰元数据的操作,会让 ctime 动、mtime 不动。下表把这个关系总结得更直观。
| 操作 | atime | mtime | ctime |
|---|---|---|---|
cat file | 变(取决于挂载选项) | 不变 | 不变 |
echo x >> file | 不变 | 变 | 变 |
chmod 600 file | 不变 | 不变 | 变 |
chown user file | 不变 | 不变 | 变 |
ln file file2 | 不变 | 不变 | 变 |
同设备内mv | 不变 | 不变 | 变 |
touch -a file | 变 | 不变 | 变 |
touch -m file | 不变 | 变 | 变 |
最后两行值得单独说一句:touch无论改的是 atime 还是 mtime,ctime 一定会跟着变成当前时间。原因是touch走的是utimensat系统调用,修改 inode 元数据这件事本身就会让 ctime 更新。这个特性决定了 ctime 无法被"还原",后面第 5 章会展开。
1.3 ctime 不是创建时间,这个误解至少要坑三类人
把 ctime 当成 Create Time,不是一个冷知识层面的小错误,它会直接影响业务判断。我至少见过三类人被它坑:
第一类是写清理脚本的。有人想"删除 30 天前创建的文件",于是用find -ctime +30,结果把老文件里刚chmod过的全删了,因为chmod刷新了 ctime。
第二类是做审计的。审计要求"文件最后变更时间",用ctime确实更严格,但如果你在中间做过一次chown -R,整个目录树的 ctime 会被刷成同一时刻,这时候再拿 ctime 去判断"谁什么时候改的",结论就完全失真了。
第三类是做同步的。有人发现rsync之后 ctime 全变了,以为是同步工具出了问题,其实新建文件天然就有新的 ctime,这是无法"保留"的字段。
真正的创建时间在 Linux 上是另一个字段:birth time。ext4 从比较新的 e2fsprogs 版本开始支持记录它,XFS 也支持,但并不是所有文件系统和内核组合都能读出来。你可以先用stat看一眼:
stat /etc/hostname如果输出里有一行Birth:且有具体时间,说明这个文件系统支持;如果显示-,那就是拿不到。也可以用指定格式直接取:
stat -c '%n 创建=%w 访问=%x 修改=%y 变更=%z' /etc/hostnameGNU coreutils 较新版本的ls也支持按创建时间排序显示:
ls -l --time=birth /data需要提醒的是,%w这个字段在很多环境里返回的是-,脚本里不要直接拿它做判断而不做空值处理,否则会得到一堆无效时间。这是我在写巡检脚本时踩过的坑:本地测试机是 XFS 能拿到,线上老环境是 ext4 拿不到,脚本直接输出了一列-,还以为是数据坏了。
1.4 为什么面试爱问这三个时间
面试里问atime/mtime/ctime,往往不是想听定义,而是想看你有没有踩过真实的坑。常见的追问有三个:backup 工具为什么默认只保留 mtime?noatime挂载选项能带来什么收益?find -mtime +7到底删的是哪批文件?
这三个问题的答案分别是:因为 ctime 根本无法被用户态还原,而 atime 的保留在很多文件系统上意义不大且代价高;因为每次读操作都要更新 inode,在高频读取场景下会带来额外的元数据写入,SSD 上就是实打实的写放大;至于第三个,答案是"8 天以上",这个整除逻辑第 4 章会专门拆开算。
2. 用 stat 和 ls 把三个时间看清楚
2.1 stat 输出逐字段拆解
stat是查看三个时间最直接的工具,没有之一。默认输出长这样:
$ stat /var/log/syslog File: /var/log/syslog Size: 1085377 Blocks: 2136 IO Block: 4096 regular file Device: fd00h/64768d Inode: 262155 Links: 1 Access: (0640/-rw-r-----) Uid: ( 0/ root) Gid: ( 4/ adm) Access: 2024-06-10 09:12:33.412000000 +0800 Modify: 2024-06-10 10:45:02.118000000 +0800 Change: 2024-06-10 10:45:02.118000000 +0800 Birth: 2024-06-01 08:00:11.771000000 +0800四行时间的顺序是固定的:Access 对应 atime,Modify 对应 mtime,Change 对应 ctime,Birth 对应创建时间。注意 Access 那一行出现了两次,一次是权限字符串(Access: (0640/...)),一次是时间,看的时候别混淆。
stat的价值在于它是"原始数据"。ls的输出会受参数、别名、LS_COLORS 影响,而stat输出的就是 inode 里存的东西。排查时间类问题,我的第一反应永远是先stat一下,确认到底哪个字段对不上。
stat还支持自定义格式,脚本里特别好用:
stat -c '%n | atime=%x | mtime=%y | ctime=%z' /data/*如果要取 epoch 秒做计算,用大写的%X/%Y/%Z:
stat -c '%Y' /data/report.csv2.2 ls 的时间选项:-u、-c 和 --time
ls -l默认显示 mtime。加上不同参数就能切换:
ls -lu /data # 显示 atime ls -lc /data # 显示 ctime ls -l --time=atime /data ls -l --time=ctime /data排序参数也可以叠加,这个组合在排查"到底是谁在读"的时候很有用:
ls -ltu /data # 按 atime 从新到旧排序 ls -ltc /data # 按 ctime 从新到旧排序 ls -ltr /data # 按 mtime 从旧到新排序有一点要注意:ls显示的时间精度是分钟级的,对于一秒内多次变化的文件根本分辨不出来。做时间对比实验时不要用ls,直接用stat或者find -printf输出高精度时间:
find /data -maxdepth 1 -type f -printf '%TY-%Tm-%Td %TH:%TM:%TS %p\n'2.3 一组可复现的小实验:亲眼看谁在动时间
光看定义记不住,动手做一遍最清楚。准备一个测试文件,然后按顺序操作,每步之后用stat看变化:
cd /tmp echo hello > t1.txt stat t1.txt # 记下三个时间的初值 chmod 600 t1.txt stat t1.txt # 预期:ctime 变了,mtime 没变 echo world >> t1.txt stat t1.txt # 预期:mtime 和 ctime 都变了 cat t1.txt stat t1.txt # 预期:atime 可能变,可能不变(取决于挂载选项)第三步之后你会发现 atime 有的机器变、有的机器不变,这正是relatime在起作用。默认的relatime有两条更新规则:atime 比 mtime 或 ctime 旧时更新一次,或者 atime 距上次更新已经超过 24 小时时更新一次。所以连续cat两次,第二次的 atime 往往就不动了。
这个实验在同一台机器上做两遍,如果结果不一样,先别怀疑人生,去查挂载参数。这也是排查时间类问题时我最强调的一步:先确认文件系统挂载选项,再怀疑命令。
2.4 时间戳和可读时间互转,别在时区上栽跟头
脚本里常需要在 epoch 和可读时间之间来回转,这几个用法要记住:
date -d @1718000000 # epoch 转可读时间 date -r /data/report.csv # 取文件 mtime date +%s # 当前 epoch date -d '2024-06-01 08:00:00' +%s # 指定时间转 epoch时区只影响显示,不影响 inode 里存的值。想验证这一点,可以指定 TZ 再看一遍:
TZ=UTC stat -c '%y' /data/report.csv TZ=Asia/Shanghai stat -c '%y' /data/report.csv同一个文件,两行显示的时间会差 8 小时,但%Y取出来的 epoch 秒完全一样。很多人在容器里看到文件时间是 UTC,就以为同步出了问题,其实只是容器镜像里没配/etc/localtime,纯粹是显示层面的事情。
3. atime 的性能代价与挂载选项调优
3.1 relatime 为什么成了事实默认
每次读文件都要写一次 inode,这个代价在机械盘时代就不小,到了 SSD 时代更是直接变成写放大。所以内核很早就引入了折中方案:relatime。
它的规则简单说就是"能省则省"——只有当 atime 比 mtime 或 ctime 旧,或者距上次更新超过 24 小时,才真正把 atime 落盘。这样既能保证mut这类需要判断"是否读过"的工具基本可用,又把绝大多数无意义的 inode 写入砍掉了。现在主流发行版默认挂的就是relatime,所以你很少看到 atime 频繁变动。
理解这一点之后,很多现象就顺了:刚cat完的文件 atime 没变,不是命令坏了,是因为本次更新被relatime省掉了;而一个 mtime 很旧的文件,你第一次读它,atime 一定会更新,因为"atime 比 mtime 旧"这个条件满足了。
3.2 noatime、nodiratime、strictatime、lazytime 怎么选
几个相关选项的取舍如下:
| 选项 | 行为 | 适用场景 |
|---|---|---|
strictatime | 每次读都更新 atime | 有严格访问审计需求 |
relatime | 满足条件才更新(默认) | 绝大多数通用服务器 |
noatime | 完全不更新 atime | 高并发读、数据库、构建机、SSD |
nodiratime | 只对目录不更新,文件仍按 relatime | 目录遍历极多且不需要目录访问时间 |
lazytime | atime 只在内存维护,需要时才落盘 | 读多写少、想兼顾 atime 和性能 |
noatime会隐含nodiratime的效果。在构建机、Web 静态资源服务器、日志分析机上,把它打开通常能省下可观的元数据写入。但代价是 atime 彻底失效,任何依赖它做判断的程序都会失灵。
3.3 查看和修改挂载参数,两步就够
先看当前挂的是什么:
findmnt -o TARGET,SOURCE,FSTYPE,OPTIONS / mount | grep ' / ' cat /proc/mounts | grep ' / 'findmnt -no OPTIONS /最适合塞进脚本,只输出参数。改参数分临时和永久两种。
临时生效,重启后失效:
mount -o remount,noatime /永久生效,改/etc/fstab第四列:
UUID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx / ext4 defaults,noatime 0 1改完fstab之后建议先跑findmnt --verify检查语法,确认没问题再重启,否则写错一个字段就是开机进不了系统。
注意:容器里的挂载点通常不允许 remount,宿主机上改了参数对容器内的绑定挂载也可能不生效。排查容器里的 atime 行为时要看宿主机实际挂载参数。
3.4 关掉 atime 之前,先确认这些程序不依赖它
noatime不是无脑优化,动手前最好过一遍清单:
- 邮件客户端(mutt、mail 命令)靠 atime 判断"已读/未读",关了会全部显示未读。
tmpreaper、老版本tmpwatch会按 atime 清理/tmp里的陈旧文件,关了之后清理逻辑会退化到 mtime 或直接失效。- 某些备份和同步工具在同步 atime(比如 rsync 的
--atimes),关了会看到大量 atime 差异,白白增加扫描开销。 - 合规审计如果明确要求保留"最后访问时间",那 atime 是不能省的。
如果这些都不涉及,noatime基本是稳赚不赔的。我个人的习惯是先在测试环境打开跑一周,用iostat和sar对比一下元数据写入量,确认没有程序报异常再推到生产。
4. find 按时间查找:-mtime 的整除逻辑最容易坑人
4.1 -atime、-mtime、-ctime 与 -amin、-mmin、-cmin
find提供两组时间条件,一组以天为单位,一组以分钟为单位,一一对应:
| 天 | 分钟 | 比较的字段 |
|---|---|---|
-atime | -amin | atime |
-mtime | -mmin | mtime |
-ctime | -cmin | ctime |
分钟版的语义更直观,-mmin -30就是"30 分钟内改过",没有整除的歧义。所以做精确时间窗口的过滤,我一般优先用分钟版,或者干脆用-newermt配合自然语言时间。
天版的坑在于它是"先把时间差除以 86400 再向下取整,然后拿取整结果去比"。下面把它算清楚。
4.2 -mtime +n / -n 到底怎么算,手算一遍
find内部做的事是:
n_days = floor( (now - file_mtime) / 86400 ) -mtime n → n_days == n -mtime +n → n_days > n,等价于 n_days >= n+1 -mtime -n → n_days < n,等价于 n_days <= n-1用具体数字算一遍最清楚。假设当前时间是 6 月 10 日 12:00。
- 文件 mtime 是 6 月 7 日 11:00。时间差是 3 天 1 小时 = 3.04 天,向下取整是 3。所以
-mtime 3命中,-mtime +3不命中,-mtime -3也不命中。 - 文件 mtime 是 6 月 6 日 11:00。时间差是 4 天 1 小时,取整是 4。此时
-mtime +3命中,-mtime +4不命中。 - 文件 mtime 是 6 月 10 日 11:50。时间差 10 分钟,取整是 0。
-mtime 0和-mtime -1都会命中。
由此可以得到几个必须记住的结论:
-mtime +7真正匹配的是至少 8 天前修改的文件,不是"超过 7 天"。-mtime -7匹配的是不到 7 天,即时间差小于 7×86400 秒。-mtime +0匹配的是至少 1 天前,这是"清理今天之前所有旧文件"的常用写法。
想精确表达"7 天前这个时间点之前",用-newermt更靠谱:
find /var/log -type f -not -newermt '-7 days' -print-not -newermt '-7 days'表示 mtime 不晚于 7 天前,语义清楚,不会因为整除产生边界争议。这是我现在的默认写法,能避免跟同事解释半天"+7 是 8 天"。
4.3 -newer、-newermt、-newerXY、-daystart 的用法
-newer拿一个参考文件来比较被测文件的 mtime:
find /data -type f -newer /data/template.txt-newermt后面直接跟时间字符串,支持 GNU date 能解析的绝大多数格式:
find /data -type f -newermt '2024-06-01 08:00' find /data -type f -newermt '-1 hour' find /data -type f -newermt 'last monday'-newerXY是最灵活的形态。规则是:X 表示被测文件的时间类型,Y 表示参考对象的时间类型,Y 写成t时后面直接跟时间字符串。可用的字母是a(atime)、m(mtime)、c(ctime)、t(时间字符串)。所以:
find /data -newermt '2024-06-01' # 被测 mtime 晚于指定时间 find /data -newerat '2024-06-01' # 被测 atime 晚于指定时间 find /data -neweram /data/ref.txt # 被测 atime 晚于 ref.txt 的 mtime-daystart是个修饰符,加上它之后,-mtime这类条件的计算起点从"当前时刻"变成"今天 00:00",适合做"包含今天在内"的统计:
find /data -daystart -mtime -1 -type f4.4 三个能直接抄的实战案例
案例一:清理 7 天前的日志但不删今天的
find /var/log/myapp -type f -name '*.log' -not -newermt '-7 days' -print确认输出没问题后,把-print换成-delete。先用-print试跑是铁律,少一次手抖就少一次事故。
案例二:找出最近 2 小时改过的配置文件
find /etc -type f -mmin -120 -printf '%TY-%Tm-%Td %TH:%TM %p\n' | sort输出里带上时间,方便直接定位是哪次变更。
案例三:增量备份用 mtime 卡时间点
TAG=/data/.backup_tag find /data -type f -newer "$TAG" -print0 | tar --null -T - -czf /backup/inc-$(date +%F-%H%M).tar.gz touch "$TAG"思路是维护一个标记文件,每次备份完就更新它的时间,下次以它为新基准。这种方案的可靠性依赖 mtime 的单调性,所以如果机器时间被回拨过,必须重建标记文件,否则会漏备份。
5. 修改文件时间:touch 能改什么,不能改什么
5.1 touch 的参数全解
touch最常用的表象是"创建空文件",但它真正的本职是修改时间戳。
touch file # 把 atime 和 mtime 都设为当前时间 touch -a file # 只改 atime touch -m file # 只改 mtime touch -c file # 文件不存在时不创建 touch -h link # 作用于符号链接本身而不是目标指定时间有两种方式。-t是紧凑格式,YYYYMMDDhhmm[.ss]:
touch -t 202406010800.00 file touch -t 202406010800 file-d更友好,接受自然语言:
touch -d '2024-06-01 08:00:00' file touch -d '2 days ago' file touch -d @1718000000 file-r是以另一个文件为模板:
touch -r /data/template.txt file组合起来最常用的两条:
touch -a -d '2024-06-01 08:00' file # 只把 atime 设成指定值 touch -m -r ref.txt file # 把 mtime 对齐到 ref.txt5.2 为什么 ctime 你改不动
touch没有-c之外的任何选项能设置 ctime,原因在第 1 章已经说过:ctime 是 inode 元数据的变更时间,而修改时间戳这个动作本身就是一次元数据变更,所以 ctime 必然被刷成"现在"。你想让它变成"过去",等于要求 inode 记录一次"元数据在未来的某个过去时间被改过",逻辑上自相矛盾。
所以只要碰了chmod、chown、touch、重命名中的任意一个,ctime 就是当前时间,无法回退。用户态没有任何合法途径把 ctime 写成指定值,备份工具也保留不了它。凡是在时间戳上做文章的操作,一定会在 ctime 上留下痕迹,这一点在审计合规场景里要特别清楚,别想着靠touch去"抹平"变更记录,那是不可能的。
5.3 批量修正时间戳的思路
实际项目里偶尔需要把一批文件的 mtime 对齐成某个时间点,比如发布包里的资源文件希望时间统一,减少后续校验时的差异。思路是两段式:
find /release/assets -type f -exec touch -d '2024-06-01 00:00:00' {} +也可以用-print0加xargs -0,文件名里有空格和换行时更安全:
find /release/assets -type f -print0 | xargs -0 touch -d '2024-06-01 00:00:00'如果想保留原有 mtime 只改 atime,那就得先记下来再改,脚本会复杂一些:
find /data -type f | while read -r f; do ts=$(stat -c '%y' "$f") touch -a -d "$ts" "$f" done注意:
touch会更新 ctime,所以"批量对齐时间戳"之后,用find -ctime做增量判断的方案会整体失效。改时间之前先确认没有下游逻辑依赖 ctime。
5.4 改时间戳的合理用途与风险边界
合理的用途其实不多,主要是三类:统一发布产物的时间戳以减少校验噪音;为测试构造特定的时间条件,验证清理脚本逻辑;恢复误操作导致的时间偏移(比如从备份里取回文件后,把 mtime 对齐到原值)。
风险则集中在两点:一是干扰基于时间的增量逻辑,二是破坏审计线索。这两点都不是"技术能不能做到"的问题,而是"做了之后下游能不能承受"的问题。我的习惯是,任何批量touch操作前先做一次文件列表快照:
find /data -type f -printf '%p\t%T@\n' > /tmp/timestamp-before.txt改完之后可以对比,出问题至少能有个回溯依据。
6. 真实场景里的时间坑:备份、同步、构建、容器
6.1 cp、rsync、tar 对三个时间的处理差异
这几个工具的时间保留行为差别很大,做数据迁移和备份时特别容易出错。
| 工具/参数 | atime | mtime | ctime |
|---|---|---|---|
cp | 不保留(新文件为当前时间) | 不保留 | 新建,当前时间 |
cp -p | 尽量保留 | 保留 | 当前时间 |
cp -a | 尽量保留 | 保留 | 当前时间 |
rsync(默认不带 -t) | 不保留 | 不保留 | 当前时间 |
rsync -a | 不保留 | 保留 | 当前时间 |
rsync -a --atimes | 保留 | 保留 | 当前时间 |
tar -cf(打包) | 不保留 | 保留 | 当前时间 |
tar -x(默认) | 不保留 | 保留 | 当前时间 |
tar -x -m | 不保留 | 不保留 | 当前时间 |
几个要点值得单独强调。第一,ctime 谁都保留不了,任何复制或解包出来的文件,ctime 都是新文件被创建的时刻,没有例外。第二,rsync -a隐含了-t,所以 mtime 是保留的,但 atime 要显式加--atimes(接收端还需要支持相应扩展属性)。第三,tar默认解包会保留 mtime,如果用一个 mtime 老的包去覆盖线上文件,覆盖后的 mtime 会变成包里的老时间,基于 mtime 的存量校验会误判为"没更新"。这在版本发布里是个经典坑,解决方案是解包后统一touch或者使用--mtime参数显式指定。
6.2 构建系统与时钟偏移
make判断是否重新编译,靠的就是源文件和目标文件的 mtime 比较。源文件比目标文件新,就触发重编。所以一旦出现下面两种情况,构建就会出怪事:
一是源文件 mtime 落在未来。这通常是因为机器时间不准、或者从别的机器拷贝了带未来时间戳的文件。结果是make认为目标文件"永远是旧的",或者反过来,即使改了代码也不重编。排查方法很直接:
find . -type f -newermt 'now' -printf '%TY-%Tm-%Td %TH:%TM %p\n'任何输出都值得怀疑。
二是构建过程中时区或系统时间被调整。Docker 构建、CI 流水线里调过date的脚本都会引发这个问题。所以构建机一定要配 NTP 时间同步,这条不是可有可无的规范,而是构建正确性的前提。
6.3 容器、虚拟机、NFS 挂载盘里的时间异常
容器和宿主机共用同一个内核时钟,容器里date输出 UTC 只是没有/etc/localtime或者TZ没设,不代表时间戳有问题。验证方法前面说过,用%Y取 epoch 对比。
虚拟机的情况稍有不同。快照恢复之后,虚拟机的时钟可能落后于宿主机,新写入的文件 mtime 小于旧文件,find -newer这类比较就会漏掉文件。恢复快照后手动同步一次时间是必要的收尾动作。
NFS 挂载盘更麻烦一点。客户端和服务端时钟不一致时,同一份数据在两边看到的 mtime 可能不一样;服务端的挂载参数(noatime与否)决定了 atime 是不是会更新,客户端本地是看不到这个差异的。排查这类问题,要在客户端和服务端各stat一次同一文件做对照,光在一边猜是没用的。
6.4 一套顺手的排查顺序
时间类问题看似杂乱,实际排查路径很固定,我一般按这个顺序走:
stat看三个字段的实际值,确认到底哪个字段不符合预期。findmnt看挂载参数,确认 atime 的更新策略。date和timedatectl看系统时间与同步状态,排除时钟偏移。- 对比参照物:和同目录其他文件比、和备份比、和另一台机器比。
- 最后才怀疑命令本身或者工具行为。
顺序反过来做,就是在一堆假设里瞎试,效率差很多。这个顺序也是我在几次"文件时间莫名其妙变了"的故障里总结出来的,前面四步基本能覆盖九成以上的情况。
7. 常见问题速查与避坑清单
7.1 现象、原因、处理对照表
| 现象 | 常见原因 | 处理方式 |
|---|---|---|
cat之后 atime 没变 | 挂载了relatime或noatime | 属正常,查看findmnt -no OPTIONS / |
chmod后 mtime 没变 | mtime 只看内容 | 属正常,用 ctime 判断元数据变更 |
| ctime 和创建时间对不上 | ctime 是变更时间,不是创建时间 | 用stat -c %w看 birth time |
find -mtime +7删多了 | +n实际是n+1天起 | 改用-not -newermt '-7 days' |
| mtime 是未来时间 | 时钟偏移或跨机复制 | find -newermt now找出后修正 |
| 容器里时间差 8 小时 | 时区未配置,纯粹显示问题 | 挂载/etc/localtime或设TZ |
rsync后 ctime 全变 | ctime 无法保留 | 属正常,不要用它做同步判断 |
| 备份恢复后 mtime 是老时间 | 打包时保留了原 mtime | 解包后统一touch或指定--mtime |
7.2 我实际踩过的几个坑
第一个坑是拿-mtime +30当"30 天前"用,写进清理脚本跑了半年才发现实际删的是 31 天以上,边界上一直有文件留着。后来统一改成-not -newermt '-30 days',语义清楚,也方便和同事对齐。
第二个坑是在构建机上没关 atime,一轮全量构建下来 inode 写入量比预期高一截。后来加了noatime,同样的构建任务耗时和磁盘写入都下来了。这个收益在大规模并发编译的场景下更明显。
第三个坑是容器里做定时任务时,用 mtime 做"当天是否已执行"的判断,跨时区之后判断全乱。最后的做法是把所有时间判断统一换成 epoch 比较,彻底绕开时区和本地化格式的问题。
第四个坑是touch -d '2024-06-01'之类的操作之后,用find -ctime做的增量巡检脚本把全量文件都报了一遍。原因是 ctime 被刷新了,而下拉逻辑并没有考虑这一点。修正的方式是巡检改用 mtime,并在脚本注释里写清楚为什么不能用 ctime。
如果只让我留一条经验,那就是:任何基于时间的自动化逻辑,先明确它依赖的是哪个时间字段,再明确这个字段会被哪些操作刷新。atime 会被读刷新,mtime 会被写刷新,ctime 会被元数据操作刷新,三个字段的"刷新面"完全不同。心里有这张表,上面列的大部分坑其实都能提前避开。