十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Linux atime、mtime、ctime 三时间戳详解与实战避坑

Linux atime、mtime、ctime 三时间戳详解与实战避坑 Linux 文件的 atime、mtime、ctime 这三个时间戳坑人的地方不在于它们有多难懂而在于你自以为懂了。很多人第一次被它绊倒都不是在高深场景里而是在一条看起来人畜无害的清理命令上写完find /var/log -name *.log -mtime 7 -delete一跑第二天发现刚调整过参数的配置文件不见了或者某个部署脚本因为源文件的时间和产物对不上把已经编译好的二进制又覆盖了一遍。问题最后都指向同一个地方——你以为文件只有一个时间实际上至少有三个在同时跳动而且它们的脾气完全不同。这篇内容想解决的就是三件事把 atime、mtime、ctime 各自的职责彻底讲清把 stat、ls、find 这几个工具读时间的差异说明白把「按时间查找」和「按时间修改」这两类操作中真正会出事的地方逐个拆开。刚接触 Linux 的读者可以从头顺着看已经用了几年命令、但一直靠感觉写 find 的同学建议重点看第 3 章和第 5 章。1. 三个时间戳各自的职责边界1.1 atime最后一次读的时间但它远比你想的沉默atime 全称 access time记录的是文件内容最后一次被读取的时刻。注意这里的关键词是内容——cat、less、grep、head、编辑器打开文件读取内容理论上都会更新 atime。但如果你现在去做个实验cat一下某个文件然后立刻stat看 atime多半会发现它没变。这不是命令坏了而是挂载选项在起作用。现代绝大多数发行版默认用relatime挂载它把 atime 的更新频率压得极低目的是避免读一个文件就要写一次元数据这种昂贵操作。所以在生产机器上atime 往往是最不可靠的一个时间戳——它可能停留在几天前也可能因为一次备份扫描而集体刷成今天。把它当成这文件被人看过的证据是很容易翻车的。真正会关注 atime 的场景其实很集中备份工具判断哪些文件被读过、缓存系统决定淘汰谁、以及一些老式的审计需求。如果你手上没有这类需求atime 对你来说基本可以忽略。1.2 mtime唯一能代表内容变了的时间mtime 全称 modify time记录文件内容最后一次被修改的时间。这是日常运维里用得最多的那个——ls -l默认显示的就是它find -mtime操作的也是它rsync 判断要不要同步这个文件比较的还是它。备份、部署、日志轮转、增量同步几乎所有判断新旧的逻辑都建立在 mtime 上。有个细节值得单独拎出来mtime 变了不代表文件被重写过。追加一行内容是 mtime 变改一个字节也是 mtime 变用echo file加个换行还是 mtime 变。对于备份工具来说它不关心你改了多少只看这个值动没动这也是为什么 rsync 这类工具在没有--checksum的情况下会漏掉一些内容变了但大小和时间都没变的极端情况。还有个容易被忽略的文件内容一改文件大小通常也跟着变而大小属于 inode 的元数据所以mtime 变化几乎必然连带着 ctime 一起变。这个连带关系是后面理解 ctime 的关键。1.3 ctime名字叫 change但和改内容不是一回事ctime 是三个词里最容易误解的一个很多人把它当成 create time创建时间这是彻头彻尾的错。ctime 的全称是 change time准确翻译是inode 状态变更时间。它记录的是文件的元数据或内容最后一次发生变化的时间——只要 inode 里任何一个字段动了ctime 就刷新。具体包括哪些操作改内容mtime 变ctime 跟着变、chmod改权限、chown改属主属组、mv在同目录内改名、建立或删除硬链接导致链接计数变化、改变文件大小……全部会刷新 ctime。反过来ctime 变了不代表内容变了——你只是chmod 644一下mtime 纹丝不动ctime 却已经跳到当前时刻。这就带来一个非常实用的判断技巧如果某个文件的 mtime 是三个月前ctime 却是今天基本可以确定有人动过它的权限或者挪过它的位置但没碰内容。这在排查权限莫名其妙变了的问题时特别好用。至于真正的创建时间Linux 传统stat是不显示的。较新的内核和 coreutils 支持通过 statx 拿到 birth timestat -c %w file可以试着看一眼但很多文件系统上这个字段是空的显示为-。这一点后面第 2 章会细说。1.4 一张表看清哪些操作会动哪个时间光靠文字记容易混我整理了一张对照表建议直接收藏。这里的前提是文件系统挂载了默认的 relatime如果挂载了 noatimeatime 那一列全部不生效。操作atimemtimectime新建文件当前时间当前时间当前时间cat/grep读取内容可能变受挂载选项影响不变不变echo x file追加内容不变变变vim保存默认方式不变变变chmod/chown不变不变变同目录内mv改名不变不变变建立硬链接不变不变变touch不带参数当前时间当前时间当前时间touch -d ... file按参数设置按参数设置当前时间cp -p复制保留保留变为复制时刻rm删除文件———最后两行是重点touch -d无论怎么设置ctime 一定会变成当前时间这是很多人以为改不掉的。而cp -p能保留 atime 和 mtime却永远保留不了 ctime因为新文件的 inode 是全新的。2. 把时间戳读出来stat、ls、find -printf 的差异2.1 stat 输出逐行拆解要看清三个时间stat是首选工具输出最完整、精度最高。直接跑stat /etc/hosts你会看到类似这样的内容File: /etc/hosts Size: 221 Blocks: 8 IO Block: 4096 regular file Device: fd01h/64769d Inode: 131075 Links: 1 Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root) Access: 2025-01-08 09:12:33.418225104 0800 Modify: 2024-12-19 14:02:11.000000000 0800 Change: 2024-12-19 14:02:11.000000000 0800 Birth: 2024-11-02 08:30:45.123456789 0800Access对应 atimeModify对应 mtimeChange对应 ctimeBirth就是前面提到的创建时间有可能显示为-。注意每一行末尾那串纳秒精度的时间ext4、xfs 这类现代文件系统都支持亚秒精度但 NFS 挂载或者某些老式文件系统可能只精确到秒导出到别的机器上差异就出来了。如果只想拿特定字段用stat -c格式化输出更省事尤其适合写脚本stat -c name%n atime%x mtime%y ctime%z /etc/hosts stat -c mtime_epoch%Y atime_epoch%X ctime_epoch%Z /etc/hosts大写的%X %Y %Z输出的是 Unix 时间戳1970 年以来的秒数适合做数值比较小写的%x %y %z输出人类可读格式适合打印给人看。这一对大写小写的区分我见过不少人写脚本时混用过结果比较出来的逻辑全错。2.2 ls 的三个开关 -l / -lu / -lcls -l显示 mtime这是大家都知道的事。但很多人不知道ls还有两个开关能切换显示的时间类型ls -lu显示 atimels -lc显示 ctime。注意是显示哪一类时间不是按时间排序。要按时间排序得用-t默认按 mtime 排。ls -ltu是按 atime 排序并显示 atimels -ltc是按 ctime 排序并显示 ctime。这两个组合在排查谁最后动过这个目录时非常高效比一个个stat快得多。有一个坑必须提一下ls默认的时间精度只到分钟一年以上的文件甚至只显示年份和日期。如果你要靠肉眼比对两个文件的时间差ls给的信息量是不够的直接上stat或者ls --full-time更靠谱。2.3 时间格式与时区--time-style 和 TZls和stat显示的时间默认受当前时区影响也就是TZ环境变量或者系统时区配置。同一台机器上TZUTC stat file和TZAsia/Shanghai stat file看到的时间会差 8 小时但底层存的 epoch 值是一样的。跨时区排查问题的时候这个细节能救命——对方看到的8 小时前和你看到的刚刚可能指的是同一个时刻。ls的--time-style有几个常用取值各有各的用途--time-stylefull-iso输出到纳秒和最详细的 stat 对齐排查精确到毫秒的问题时必用。--time-stylelong-iso只到分钟比默认多显示年份适合日常记录。--time-style%Y-%m-%d_%H:%M:%S自定义格式写脚本归档时方便解析。我个人习惯是在做时间比对前先统一下时区和格式尤其是要写进表格或者跟别人对数据的时候避免时间对不上这种纯沟通成本。2.4 批量导出与排序find -printf 配合 sort要一次性看一批文件的时间ls就不够用了find -printf才是正解。它能打印任意字段并且天然支持递归# 按 mtime 从旧到新排列当前目录下所有普通文件 find . -type f -printf %T %TY-%Tm-%Td %TH:%TM %p\n | sort -n # 只输出路径和 mtime方便喂给别的脚本 find /data -type f -printf %T\t%p\n | sort -n%T是 mtime 的 epoch 秒带小数%TY %Tm %Td是年月日%TH %TM是时分。把T换成A就是 atime%A换成C就是 ctime%C。这套写法比find -exec stat {} \;快一个数量级因为不用为每个文件 fork 一个进程。要按秒级排序再加个稳定排序可以用sort -k1,1n如果文件多到几十万记得配合xargs -0而不是管道直接传路径否则遇到文件名带空格就会断掉。3. 按时间找文件find 的时间谓词有多少人用错3.1 -mtime n 的取整逻辑与 n 的真实含义find -mtime是整个时间查找里误解率最高的一个参数因为它用的是向下取整的 24 小时单位而不是自然日。它的计算方式是先算出「现在时刻减去文件 mtime」的秒数差再除以 86400 并向下取整得到一个整数 n然后拿这个 n 去匹配你写的条件。具体规则写法实际含义-mtime 0时间差取整后为 0即过去 24 小时内修改过-mtime 1时间差在 24 到 48 小时之间-mtime 7时间差取整后大于 7即8 天以上不是 7 天以上-mtime -7时间差取整后小于 7即 7 天以内-mtime 7恰好落在 7 到 8 天那个 24 小时区间内7是 8 天以上这一点坑过太多人。日志清理场景里写-mtime 7本意是保留 7 天实际保留的是 8 天多因为第 8 天的文件还没满足条件。而如果你写-mtime 6才是真正意义上的超过 6 天也就是第 7 天开始删。想精确控制最好直接用-mmin分钟单位或者-newermt。还要注意一个隐含前提find计算时的现在是命令启动的那一刻不是文件被扫描到的那一刻。对文件量极大的目录来说扫描过程可能持续几分钟边界上的文件会因此产生偏差。要求严格的话把基准时间显式固定下来。3.2 -mmin、-newermt、-newer 三种更精确的写法-mmin是分钟版本对应还有-amin、-cmin。规则和-mtime完全一样只是单位从 24 小时变成 1 分钟。所以最近 30 分钟内改过的文件就是-mmin -30超过 30 分钟没动过是-mmin 30。-newermt是我个人最推荐的写法因为它直接跟你写的时间字符串比较不需要在脑子里做除法# 找出 2025-01-01 00:00 之后修改过的文件 find /data -type f -newermt 2025-01-01 00:00:00 # 找出今天凌晨之后修改过的文件 find /data -type f -newermt today 00:00 # 找出 3 天前之后、1 天前之前的文件区间查找 find /data -type f -newermt 3 days ago ! -newermt 1 day ago时间字符串支持 GNU date 的绝大多数写法2 hours ago、yesterday、2024-12-01 08:00、1735700000epoch 秒都能用。要按 atime 或 ctime 比较把-newermt换成-newerat、-newerct即可。-newer则是拿另一个文件当参照物find -newer /tmp/marker表示找出比 marker 文件 mtime 更新的文件。这个在增量备份里极其好用——先touch一个标记文件同步完之后再挪它下轮就以它为分界点。3.3 组合条件、目录剪枝与特殊文件名find的条件默认是与关系多个条件写在一起就自动叠加。但有个优先级陷阱!、-a、-o混用时-a的优先级高于-o必须用括号显式分组而且括号要转义因为 shell 会自己解析一遍。# 找出 /data 下超过 7 天没改过的 .log 文件排除 archive 目录 find /data -type f -name *.log -mtime 7 \ -not -path */archive/* # 排除整个目录树的更高效写法命中目录直接剪枝不再深入 find /data -type d -name archive -prune -o \ -type f -name *.log -mtime 7 -print第二种写法用-prune把目录剪掉性能比-not -path好很多因为-prune直接阻止 find 进入那个目录而-not -path只是过滤结果find 还是会把整个目录遍历一遍。目录层级深、文件量大的时候这个差别非常明显。文件名安全也是老问题。删除、批量处理的场景里一定要用-print0配合xargs -0或者直接用-exec ... {} 别用$(find ...)往管道里塞路径遇到带空格、换行、引号的文件名就会炸。3.4 时间查找里最容易出事的三类命令第一类是清理命令跑在生产目录。find /var/log -type f -delete这种写法如果在没有-name限定的情况下敲错路径删掉的可能不只是日志。我自己的习惯是无论多简单的清理都先跑一遍不带-delete的版本把输出重定向到一个文件里 count 一下确认数量级对不对再补上删除动作。多花十秒省一次事故。第二类是用-mtime N做保留策略。就像前面说的N是 N1 天以上跟保留 N 天不是一回事。要么改用-mmin精确到分钟要么把策略写成-mmin $((N*1440))让含义一眼可见。第三类是在 NFS 或者挂载共享目录上跑时间查找。跨机器的时间同步、文件系统精度差异、目录缓存过期都会让find的结果出现延迟。这类环境建议先用一个小目录验证结果是否符合预期再放开到全量。4. 改时间touch 能改什么、改不了什么4.1 touch 的四个维度-a -m -d -rtouch这个命令绝大多数人只用过它的一个功能——创建一个空文件。其实它的本职工作是修改文件的时间戳创建文件只是顺带的结果。它的行为由几个参数控制不带参数把 atime 和 mtime 都设成当前时间。如果文件不存在就创建一个空的。-a只改 atimemtime 保持不动。-m只改 mtimeatime 保持不动。-d 时间字符串按指定时间设置配合-a或-m可以分别指定。-t 时间戳紧凑格式[[CC]YY]MMDDhhmm[.ss]。-r 参照文件把参照文件的 atime 和 mtime 复制过来ctime 除外。-c文件不存在时不创建只对已存在的文件生效。-h符号链接场景下操作链接本身而不是它指向的目标。实际工作中-r的出场率比想象的高。比如你重新生成了一份和旧文件应当时间一致的产物用touch -r old new就能把时间对齐避免下游脚本误判成新文件。4.2 时间字符串怎么写才不报错-d后面跟的字符串由系统的日期解析库处理支持的范围很广但写法不统一的时候确实会报invalid date format。下面这几种是我实测最稳的# 标准格式最推荐 touch -d 2025-01-08 10:30:45 file.txt # ISO8601 带时区偏移 touch -d 2025-01-08T10:30:4508:00 file.txt # 相对时间 touch -d 2 hours ago file.txt touch -d yesterday file.txt touch -d next Monday file.txt # epoch 秒前面必须加 touch -d 1735700000 file.txt # 紧凑格式注意是点号分隔秒不是冒号 touch -t 202501081030.45 file.txt几个容易踩的坑-t格式里秒前面是点号写成冒号会解析失败-d用 epoch 时必须加不加会被当成别的意思月份和日期的英文写法依赖 locale在LC_ALLC之外的环境里可能解析行为不同。如果时间字符串是从变量拼出来的建议先date -d $input验证一遍再喂给touch能提前发现问题。另外注意时区问题touch -d 2025-01-08 10:30:45用的是本地时区。要在 UTC 环境下设置得显式带上偏移或者设TZUTC否则实际写入的 epoch 值会差出时区那几个小时。4.3 touch 之后 ctime 为什么变成现在这是新手最常困惑的一点。前面表格里写过touch -d无论设置成什么时间ctime 都会变成当前时刻。原因在 inode 层面ctime 的语义是这个 inode 的状态最后一次被改变的时刻而touch设置时间戳这件事本身就是一次对 inode 的修改所以 ctime 必然更新。换句话说ctime 有个特点你永远无法让一个刚被操作过的文件的 ctime 回到过去。它只会随着你的操作往后走。这不是 bug而是设计使然——它保证了这个 inode 最近被动过这件事有据可查。理解了这一点很多疑问就通了。比如我明明把时间改成了去年为什么 rsync 还是认为它是新文件——因为 rsync 在-a模式下虽然主要看 mtime但某些校验逻辑也会参考其他因素再比如我改了时间为什么备份系统还是能识别出来——那是因为备份系统可能在别的地方记录了状态不完全依赖文件时间。所以不要把修改时间戳当成隐身术它解决的是让工具按我想要的方式判断不是让所有痕迹消失。4.4 目录、符号链接与递归修改目录本身也有三个时间戳含义和文件一致目录的 mtime 在目录内容发生变化时更新——新建、删除、重命名直接子项都会刷新它但修改子项的内容不会。所以一个目录的 mtime 反映的是这个目录的结构最后一次变的时刻而不是里面文件最后被改的时刻。这是排查目录为什么看起来很旧时常见的一个思考误区。touch dir只会改这个目录自己的时间不会递归下去。要递归处理整棵树得配合find# 把整个目录树里所有文件和目录的 mtime 统一设成某个时间 find /path -exec touch -m -d 2025-01-01 00:00:00 {} # 只处理文件不碰目录 find /path -type f -exec touch -m -d 2025-01-01 00:00:00 {} 用-exec ... {} 而不是{} \;是因为前者会把多个文件一次传给touch少 fork 成百上千次进程。文件量上万的时候速度差别能到十倍以上。符号链接要特别小心。默认情况下touch link会跟随链接、修改目标文件的时间。要操作链接本身加-h。同理stat link默认显示的是目标文件的信息stat -L反而才是跟随链接这个参数名很容易记反建议用之前先验证。批量处理含链接的目录树时如果想跳过链接用find -type f天然就把符号链接排除了因为符号链接的类型是l不是f。5. ctime 改不动的背后inode 才是主角5.1 inode 变更时间的内在逻辑要真正理解 ctime得先接受一个事实在 Linux 的文件模型里文件有两个部分——文件名和数据。文件名挂在目录项上数据体和对它的所有描述信息都放在 inode 里。inode 记录了权限、所有者、大小、链接计数、数据块指针以及我们讨论的这三个时间。ctime 就是这个 inode 的最后修改时刻。它不区分你改的是内容还是元数据只要 inode 里任何一个字节动了它就刷新。这也是为什么chmod会改 ctime——权限位就在 inode 里。反过来说任何不触碰 inode 的操作都不会改 ctime。比如只读打开文件读内容只可能改 atimeatime 也在 inode 里所以严格说也在 inode 里但 relatime 下大多数时候不写回又比如对文件加一个指向它的软链接——软链接是独立的新 inode原文件的链接计数不变所以原文件的 ctime 也不变。这一点跟硬链接正好相反硬链接会让原文件的链接计数加一属于 inode 变更ctime 会刷新。软链接和硬链接在时间戳上的区别是很多人没意识到的。5.2 想动 ctime 只有几条歪路以及为什么不该走touch、utime这类标准接口在设计上就只能设置 atime 和 mtime没有给你设置 ctime 的入口。所以从正常命令层面ctime 是改不了的。有人会尝试用离线文件系统工具直接改写 inode 里的字段理论上确实能改但代价很大必须卸载文件系统、用 root 权限操作原始设备、操作失误可能直接损坏文件系统。更重要的是这么做会破坏系统自身的状态可信度。文件时间戳是备份、审计、变更追踪的基础依据人为篡改会让所有基于它的机制失效——你会分不清哪些文件真的被改过增量备份会漏文件配置核对会得出错误结论。所以在生产环境里正确的做法不是想办法改 ctime而是接受它、然后利用它。ctime 的价值恰恰在于它改不动。它提供了一个无法通过常规手段伪造的信号只要一个文件被动过ctime 就会留下痕迹。我在做配置基线核对时经常这么干——对比 mtime 判断内容有没有变对比 ctime 判断有没有人碰过它。这两个信息合起来判断力比单看一个强得多。5.3 vim 保存、sed -i、rsync 对 inode 和时间的连带影响有几个日常操作会以不太直观的方式影响 inode 和时间戳值得单独讲。vim 的保存策略。默认配置下vim 写文件的方式取决于backupcopy选项。在很多系统上它是写临时文件、然后 rename 覆盖原文件。这种方式的后果是原文件的 inode 被替换了新文件的 atime、mtime 都是保存时刻ctime 也是。这意味着原文件上挂的硬链接会断掉权限可能被重置属主也可能变化。如果你发现编辑一个配置文件之后其他指向它的硬链接失效了、权限也变了就是这个原因。解决办法是把backupcopy设成yes让 vim 直接原地写。sed -i的行为。GNU sed 的-i默认也是写临时文件 rename和 vim 类似。它有一个不太为人知的细节GNU sed 会尝试把原文件的权限和属主复制到新文件上但inode 是新的所以所有硬链接都会断ctime 一定变。批量sed -i处理大目录时这个副作用会积累成一堆看起来像新文件的产物对增量备份不友好。如果在意这一点可以用sed -i的--follow-symlinks选项它会改为原地修改保留 inode。rsync 的时间处理。rsync -a会保留 mtime所以目标端的文件时间和源端一致。它默认不保留 atime因为要读源文件源文件 atime 会被影响保留它意义不大也不保留 ctime因为目标端是新写入的 inodectime 必然是同步时刻。如果你确实需要保留 atime加--atimes。要特别提醒的是任何一次rsync -a执行完目标目录下的所有文件 ctime 都指向同步那一刻这是判断这个目录最近被同步过的一个快捷方式。5.4 硬链接场景下的时间传染硬链接本质上是同一个 inode 的多个名字所以对其中任何一个名字做修改都是对同一个 inode 的修改——三个时间戳全部共享。改一个名字的内容另一个名字的 mtime 也变了chmod一个名字另一个名字的权限和 ctime 也一起变。这符合直觉。反直觉的是删除操作。rm掉一个硬链接名字时如果链接计数还没归零原 inode 依然存活但剩余名字的 ctime 会因为链接计数变化而刷新。也就是说你删的是 AB 的 ctime 变了。这个现象在排查文件明明没动过时间却是今天的问题时能解释掉一部分看起来灵异的情况。同理重命名也会刷新。在同一个目录内把一个文件mv A Binode 不变数据没动但目录项变了所以文件本身的 ctime 会刷新而 mtime 和内容都不变。这也是为什么移动文件经常会让基于 ctime 的判断出现噪音——它确实动了只是没动内容。6. atime 的代价relatime、noatime 与实际取舍6.1 每次读都写一次元数据意味着什么假设挂载选项是strictatime每次都更新 atime那么每读一个文件内核都要把这个文件的 inode 写回磁盘一次。一次两次无所谓但想想这些场景备份工具要把几百万个文件扫一遍、Web 服务器每秒读取几千个静态文件、日志采集器不停地 tail 日志——每一次读取都意味着一次元数据写入。代价体现在三个方面。第一是吞吐元数据写入会占用磁盘 IOPS机械盘上尤其明显一次随机写就够让队列排起来。第二是持久性风险大量无关的元数据写会和业务写入竞争还可能顺带触发日志提交。第三是寿命对闪存介质来说元数据写同样是写长期高频的 atime 更新会白白消耗擦写次数。正是因为这些代价从内核 2.6.30 左右开始relatime 逐渐成为主流发行版的默认选项。它本质上是一个折中既保留了文件被读过这个信息的大致可用性又把更新频率压到了可以接受的水平。6.2 relatime 的判定规则relatime 的更新条件可以用一句话概括只有当 atime 已经落后于 mtime 或 ctime或者 atime 距今超过 24 小时才会真正把新的 atime 写回磁盘。其他情况下的读取只在内存里更新不落盘。这个规则解决了两类场景。一是文件刚创建、还没被读过此时 atime 等于 mtime第一次读会因为atime mtime而触发更新所以新文件的 atime 能正常记录。二是文件被读了之后又被改了此时 mtime 领先 atime下一次读又会触发更新保证 atime 不会永远停在很久以前。而它放弃的是同一个文件在 24 小时内被反复读取只有第一次会写回。这正好对应了备份扫描、Web 服务这类高频读取场景——每次都记其实没意义记个大概就够了。判断当前挂载用的哪种策略最直接的是这两条命令# 看某个挂载点的选项 findmnt -no TARGET,FSTYPE,OPTIONS / # 或者从 /proc/mounts 里捞 awk $2/ {print $4} /proc/mounts输出里如果没有明确写atime相关的词可能就是默认值看到relatime就说明是相对模式看到noatime说明完全不更新看到strictatime才是每次更新。6.3 不同负载下怎么选这不是一个哪个更先进就用哪个的问题得看你的负载形态。我按经验分几类说数据库数据目录。这类目录的读取模式是随机页读取atime 没有任何业务价值更新它纯属浪费。建议显式挂noatime能给元数据层面减不少负担。日志目录和高频读取的静态资源目录。同样建议noatime。日志文件本来就只写不读静态资源读一次就够了atime 更新带来的收益为零。备份和归档目录。这里要分情况。如果备份工具靠 atime 判断这个文件被读过没有那必须保留 atime 更新至少用relatime如果工具本身有独立的索引数据库比如大多数现代备份方案完全不依赖 atime那noatime更省事。开发和调试环境。我一般保留默认的relatime因为排查问题时偶尔会想看一眼 atime虽然它不精确但有没有人读过这种粗粒度判断还是有点用。需要考虑目录 atime的情况。有些场景下目录的 atime 也有意义比如判断某个目录最近有没有被ls过。noatime会连目录的 atime 一起关掉而nodiratime只关目录、保留文件的。如果只想优化目录遍历的开销又不想丢掉文件的 atimenodiratime是个更细的选择。负载类型建议选项理由数据库数据目录noatime随机读为主atime 无业务价值日志/静态资源noatime高频读取更新纯属开销依赖 atime 的备份工具relatime保留粗粒度信息控制写入量有独立索引的备份noatime工具不依赖 atime开发调试机relatime保留排查时的一点参考价值只想去掉目录开销nodiratime保留文件 atime关闭目录 atime6.4 改挂载参数的完整操作与回退改挂载选项有两种方式临时生效和永久生效。临时生效用 remount# 把根分区重挂为 noatime立即生效重启后失效 mount -o remount,noatime / # 确认结果 findmnt -no OPTIONS /这里有个坑remount 时如果不带完整选项可能会丢掉原有的重要选项。稳妥的做法是先把当前选项完整抄下来再加新选项findmnt -no OPTIONS / # 假设输出是 rw,relatime,errorsremount-ro mount -o remount,rw,relatime,noatime,errorsremount-ro /永久生效要改/etc/fstabUUIDxxxx-xxxx / ext4 defaults,noatime 0 1改完之后不要急着重启。先跑mount -a验证语法如果 fstab 写错了mount -a会报错但不会影响当前挂载这是最安全的验证时机。真要等到重启时才发现 fstab 有问题那就可能是进不了系统的局面了。回退很简单把noatime从选项里去掉重新 remount 即可。注意 remount 之后 atime 会从当时的时刻开始恢复更新中间这段时间的访问记录是缺失的这个没法补。7. 四类真实场景的时间戳组合用法7.1 日志与临时文件清理这是最常见的使用场景也是最容易出事的地方。一个相对稳妥的模板长这样# 第一步先预览确认数量和路径都符合预期 find /var/log/myapp -type f -name *.log -mmin $((7*1440)) -print | wc -l # 第二步抽查几个文件的实际时间 find /var/log/myapp -type f -name *.log -mmin $((7*1440)) \ -printf %TY-%Tm-%Td %TH:%TM %s %p\n | head -20 # 第三步确认无误后再删 find /var/log/myapp -type f -name *.log -mmin $((7*1440)) -delete用-mmin $((7*1440))而不是-mtime 7是为了让7 天这个语义直接写在表达式里一眼能看出是 7×1440 分钟避免取整带来的困惑。先预览再执行是我这些年养成的最值钱的习惯之一。还要注意-delete有个隐含行为它会自动启用深度优先并且必须在表达式最后。如果前面有-prune之类的操作可能会互相干扰。所以能不用-delete就不用改成-exec rm -f {} 更可控。7.2 基于 mtime 的增量备份用find -newer配合标记文件能搭出一个最小可用的增量备份MARKER/var/lib/backup/last_run.marker TARGET/backup/incremental/$(date %Y%m%d%H%M) # 第一次运行前先建立标记 [ -f $MARKER ] || touch -d 1970-01-01 00:00:00 $MARKER mkdir -p $TARGET find /data -type f -newer $MARKER -print0 | \ tar --null -T - -czf $TARGET/files.tar.gz # 归档成功后再推进标记 touch $MARKER这里的关键点是标记文件的推进顺序一定要在归档成功之后再touch否则如果 tar 中途失败标记已经推进下一轮就会漏掉这批文件。顺序写反了备份就会出现静默丢失而且很难发现。另外find -newer比较的是 mtime所以只改过权限、没改内容的文件不会被同步——这在备份内容的语义下是对的如果你的场景需要连权限变更一起同步那就得换成比较 ctime用-cnewer代价是每次权限调整都会带来一次同步。7.3 部署校验文件到底有没有被换掉发布完之后想确认线上文件和我预期的一致时间戳能帮上忙但要会用。我的做法是把发布产物的时间基准记录下来然后对比# 记录基准 stat -c %n %Y %Z /opt/app/bin/* /tmp/baseline.txt # 之后核对 stat -c %n %Y %Z /opt/app/bin/* /tmp/current.txt diff /tmp/baseline.txt /tmp/current.txt如果 mtime 变了说明文件内容被改过如果只有 ctime 变了说明有人动过权限或者挪过位置内容没变。这个区分在追查配置为什么被还原了这类问题时特别有用。要注意stat -c的输出顺序要保持一致文件多的时候最好先sort一下否则diff出来的都是噪音。还有一点不要依赖 ctime 来证明文件没被动过因为任何一次chmod、任何一次同目录重命名都会刷新它噪音很大。ctime 的正确用法是看到它变了去查为什么变而不是它没变所以一切正常。7.4 时间看起来不对的常见成因排查最后说几个我实际遇到的问题几乎覆盖了九成时间对不上的疑惑。时区差。文件本身存的是 epoch显示出来才带时区。容器里如果是 UTC宿主机是东八区两边看到的时间会差 8 小时。判断方法date看两边当前时间stat对比 epoch 值。epoch 一样就说明文件没问题是显示层的问题。NFS 或网络文件系统的精度。某些 NFS 实现只保存到秒本地文件是纳秒比较时会出现差几毫秒的情况导致基于纳秒的比对脚本误报。做跨文件系统的时间比较先把精度统一到秒。系统时钟被校正过。如果机器的时钟被向前或向后调整过会留下一批mtime 在未来的文件。这类文件用-mtime系列条件是匹配不出来的因为时间差算出来是负数无论n还是-n都不成立。排查办法是显式找一个未来的基准# 找出 mtime 在未来的文件 find /data -type f -newermt $(date -d 1 hour %Y-%m-%d %H:%M:%S)挂载选项导致 atime 不动。前面讲过 relatime 和 noatime 的区别你手动cat一个文件之后 atime 没变绝大多数情况下就是这个原因不是文件系统坏了。文件系统本身不支持某项精度。某些存储介质或者特殊的挂载方式下秒以下精度会被丢弃导致同一秒内创建的两个文件时间看起来完全一样。这类情况下比较时间要改用内容校验别硬靠时间戳。容器里看到的文件和宿主不是一份。绑定挂载、overlay 文件系统都可能让你看到的时间与预期不同。排查的第一步永远是确认我看到的到底是不是那个文件用 inode 号对比最直接ls -i两边各跑一次对不上就说明路径下挂的压根不是同一个东西。把这些成因理清楚之后你会发现时间戳本身其实很老实它只是精确地反映了自己的语义。绝大多数时间不对的困惑最后都归结到两件事上要么是把三个时间的语义记混了要么是把显示层的时区和存储层的 epoch 搞混了。区分清楚这两层剩下的就都是熟练度问题。
返回列表