ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

定时删除超过天数文件:Linux与Windows安全清理实战指南

定时删除超过天数文件:Linux与Windows安全清理实战指南 1. 定时删除超过天数的文件为什么看着简单做起来却总翻车很多朋友第一次接触自动化需求接到的就是定时删除超过天数的文件日志目录快满了、备份盘要爆了、临时文件随便堆积领导一句写个脚本每天清一下就把任务扔过来了。我当时也是这么想的——这不就是到期就删四个字吗真上手才发现凡是和删除沾边的事默认都带着风险凡是涉及定时的事默认都有环境差异。这个需求真正的价值不在删这个动作而在三个容易被忽略的地方怎么定义超过天数、怎么保证删除安全、怎么让脚本在无人值守的情况下长期可靠运行。所以这篇文章我不想只丢给你一条 find 命令而是按我实际做过的一个清理项目为主线把需求决策、Linux 和 Windows 两套实现、定时触发、日志审计、误删回滚完整串一遍。适合刚接触运维自动化的开发、想系统化做日志/备份清理的运维以及所有被磁盘空间报警逼疯的同行。1.1 什么场景最需要按天数清理先说场景。这类需求最集中的地方有三个应用日志目录Tomcat、Nginx、Spring Boot、Python 服务按天切割的日志文件只增不减。我见过一台线上机器跑三个月没管光/var/log/app就占了 80GB。备份文件目录数据库逻辑备份、配置文件备份、云盘同步的旧版本通常业务要求保留 7 天到 30 天不等到期必须清理否则备份策略迟早被磁盘容量卡死。临时目录和构建产物/tmp、CI 服务器的工作目录、上传文件的暂存目录。这些文件的生命周期很短不清理既占空间又有合规风险。不同场景对天的容忍度完全不同。日志可以少留一点但绝对不能删到正在写入的热文件备份文件一旦误删就涉及恢复必须多重保障临时目录可以激进但也要防止有人把还没处理的文件放在里面。1.2 我见过的三种翻车现场先说反面案例因为定时删除这个需求真正的坑几乎都藏在你以为懂了的地方。翻车 A用 atime 判断活跃文件结果热日志被清掉。当时同事写了一个find -atime 7 -delete本意是清理 7 天没被访问的文件。结果应用日志每天都被 logrotate 重命名、被监控进程读取atime 反而不断刷新真正该删的旧日志没删掉误删了另一批只在启动时读一次的配置文件。atime 在现代 Linux 上默认还开了 relatime它的含义远比你以为的复杂。翻车 B备份文件生成 1 分钟就被删除。备份脚本每天凌晨生成文件清理脚本也是凌晨跑。原脚本用ctime判断创建时间但ctime实际上是 inode 变化时间备份文件生成后被复制、被修改权限、被移动目录都会刷新 ctime。结果刚生成的备份被判定为超期文件直接删掉那次事故直接导致恢复演练失败。翻车 CWindows 任务计划静默失败。PowerShell 脚本在手动执行时一切正常但放进任务计划后每天都不跑。后来发现是脚本用了用户环境变量而任务计划运行时没有加载完整用户环境还有一个更隐蔽的问题脚本保存在 UTF-8 无 BOM 格式PowerShell 5.1 把它当成了 ANSI 解析中文路径匹配全乱。这些都是定时删除真实发生过的事。你可以说它们低级但如果不提前把机制搞清楚谁都有可能再踩一次。2. 动手写脚本前必须先拍板的四个决策我见过很多团队上来就写find /data -mtime 30 -delete跑两周后出问题才回头补设计。其实这个需求有四个决策必须在写代码前定清楚它们直接决定脚本是安全工具还是定时炸弹。2.1 用哪个时间戳定义天数这是最核心的一个决策文件的年龄到底按哪个时间算。Linux 下有mtime内容修改时间、atime访问时间、ctime状态变更时间Windows 下对应的是LastWriteTime、LastAccessTime、CreationTime。场景建议用原因日志文件清理mtime / LastWriteTime日志不再写入mtime 就停住了最符合停止更新超过 N 天备份文件清理mtime 或 CreationTime备份生成后不再改动两种都行但要注意备份脚本是否在生成后有 touch / 拷贝操作缓存临时文件atime如果有把握想表达最近没人用时可以用但 Linux 的 relatime 会稀释精度不建议新手用不允许使用 ctime 判断年龄勿用ctime 改变不等于文件内容变更任何权限变化、链接数变化都会刷新它实际经验是业务文件统一看 mtime / LastWriteTime不要自己发明时间维度。Windows 的 CreationTime 在某些场景下文件被还原、被复制会重置稳定性不如 LastWriteTime。2.2 删除前要不要先归档删和移走是两回事。归档成本低但能极大降低误删的灾难级别。决策思路是如果文件可以随时重新生成比如日志、缓存直接删做好日志记录即可。如果文件不可再生比如备份、导出数据强烈建议先移动到待删除区延迟 3~7 天再真正删除。如果目录很大、文件很多删除和归档都要考虑 IO 压力。同一文件系统下mv只是改目录项速度极快跨文件系统移动则是真正的拷贝反而可能在业务高峰期造成性能抖动。我经手的备份清理项目最后全部改成移动 延迟删除后面第 5 章会详细说这个方案。2.3 按目录范围还是按文件名匹配很多人习惯把删除范围写成某个目录下所有文件但这会带来一个隐患目录里可能混着不该删的配置、许可证文件、正在用的临时文件。写清理脚本前必须把匹配规则从宽到窄排一遍目录白名单只扫描明确列出的目录绝不用/*这种根目录通配。文件名模式比如*.log、*.bak、backup_*.sql用-name或-regex过滤。排除列表某些子目录和文件名必须保留比如keep/、latest.zip。时间范围mtime超过阈值。正是因为把全目录删除改成模式匹配 排除列表我那个项目才敢让脚本每天自动跑不用每次人工 review。2.4 脚本要不要幂等、要不要可恢复定时任务最怕的一件事是跑了一半断了或者被误伤后没有重试机会。所以写脚本前要规定两点幂等性同样的脚本在同一状态下重复跑结果一致。删除脚本天然幂等文件没了就是没了但如果加了归档到另一个目录的逻辑重复跑就可能导致同一文件被归档两次必须用归档前检查目标是否存在来保证幂等。可恢复性最好有干跑模式只打印要删的文件不实际删并且每次删除都输出日志。日志不是给机器看的是给第二天发现出问题的人类看的——没有日志你连回滚的依据都没有。这套设计不复杂但能帮你避开 90% 的定时删除事故。3. Linux 下最稳的组合bash find cronLinux 环境下的定时清理最经典的组合就是 bash 脚本 find 命令 cron 任务。但经典不等于可以裸奔下面这套是我验证过、可以直接套用的方案。3.1 一个够用但有点危险的入门脚本先看你可能在网上搜到的最简版find /var/log/myapp -type f -mtime 30 -delete这条命令的意思是在/var/log/myapp下找普通文件修改时间超过 30 天直接删除。能跑但我绝对不会直接把它放进 crontab。问题有三个没有日志你不知道今天删了哪些文件、删了多少。没有排除一旦目录里混入重要文件30 天后直接没了。没有干跑机制想先看看效果必须改命令重新跑。还有个更隐蔽的问题-delete和某些find参数组合在部分环境下优先级不同可能误删。3.2 逐步加上安全措施的完整清理脚本我现在的日志清理脚本长这样你可以按需改目录和天数后直接用#!/usr/bin/env bash # # clean_old_files.sh - 定时删除超过天数的文件 # 用法: # ./clean_old_files.sh --dry-run 干跑只输出不删除 # ./clean_old_files.sh 正式执行 # TARGET_DIRS( /var/log/app1 /var/log/app2 ) RETENTION_DAYS30 FILE_PATTERN*.log EXCLUDE_DIRS(keep archive) DRY_RUN0 LOG_FILE/var/log/cleanup/clean_$(date %Y%m%d).log if [[ $1 --dry-run ]]; then DRY_RUN1 fi mkdir -p $(dirname $LOG_FILE) for dir in ${TARGET_DIRS[]}; do if [[ ! -d $dir ]]; then echo [$(date %F %T)] [WARN] 目录不存在: $dir $LOG_FILE continue fi # 这里特意用 print0 while read 而不是 -exec是为了安全处理文件名里的空格和换行 while IFS read -r -d file; do [[ -z $file ]] continue # 跳过排除目录 for ex in ${EXCLUDE_DIRS[]}; do if [[ $file *$ex* ]]; then continue 2 fi done # 跳过大小为零的空文件可选 if [[ ! -s $file ]]; then continue fi if [[ $DRY_RUN -eq 1 ]]; then echo [$(date %F %T)] [DRY_RUN] 将删除: $file $LOG_FILE else rm -f $file echo [$(date %F %T)] [DELETED] $file $LOG_FILE fi done (find $dir -type f -name $FILE_PATTERN -mtime ${RETENTION_DAYS} -print0) done exit 0几个关键点我解释一下-mtime ${RETENTION_DAYS}注意引号不能去掉否则当 RETENTION_DAYS 以30形式传入时传给 find 的参数就是 30会报错。while IFS read -r -d 配合find -print0这是处理文件名中空格、换行符最安全的方式。不要用find ... | xargs rm这种裸奔写法遇到带空格的文件名会当场裂开。DRY_RUN先跑--dry-run看输出确认没有误匹配再正式执行。这一步价值极高。日志写到独立文件不要只依赖 cron 的输出重定向因为脚本内部的警告和删除详情必须和 cron 的系统日志分离。3.3 cron 定时任务里的环境细节脚本准备好了接下来是 cron 配置。很多人在这里栽跟头因为 cron 环境和手动执行环境完全不同。给你一个经过坑出来的 crontab 写法# 每天凌晨 2 点 30 分执行清理脚本 30 2 * * * /bin/bash /usr/local/bin/clean_old_files.sh /var/log/cleanup/cron.log 21三个细节必须注意脚本内不要依赖用户环境变量。cron 的 PATH 极简/usr/local/bin下的一些命令有时候找不到。所以脚本开头最好export PATH/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin。如果需要防止上一次还没跑完、下一次重复启动可以在脚本开头加文件锁。我常用flockexec 99${LOCK_FILE:-/tmp/clean_old_files.lock} flock -n 99 || { echo 另一个清理实例还在运行退出; exit 1; }触发时间避开业务高峰。删除大量小文件时盘 I/O 会短暂升高我一般安排在凌晨 2 点到 4 点之间且和备份任务错开。另外如果你有多个清理脚本尽量分开不同时刻执行否则同一时间一起扫目录CPU 和 I/O 峰值会叠加。3.4 符号链接、目录权限和隐藏文件这三个是我在 Linux 环境下的补充经验符号链接find默认不跟随符号链接-type f只会匹配真实文件。如果你加了-L参数-type f会检查链接指向的目标类型这时一旦删除操作没有正确调用可能删掉目标文件而不是链接本身风险极大。清理脚本里不要加-L。目录权限扫描时如果遇到权限不足的子目录find会继续扫其他目录但会往 stderr 打错误。脚本里最好加2/dev/null或把这部分单独记录避免错误刷爆 cron 日志。隐藏文件find默认会扫到隐藏文件如果你只想清*.log隐藏文件通常不会匹配但如果用-name *.tmp这种宽泛模式注意隐藏文件也可能命中比如.foo.tmp。别问我是怎么知道的。4. Windows 环境PowerShell 脚本 任务计划程序Windows 上的实现思路和 Linux 一致但工具链完全不同。我们生产环境有一批 Windows 文件服务器用的就是 PowerShell 任务计划程序下面是完整方案。4.1 用一条 PowerShell 命令完成扫描-过滤-删除最核心的 PowerShell 命令是Get-ChildItem配合Where-Object过滤然后用Remove-Item删除$cutoff (Get-Date).AddDays(-30) Get-ChildItem -Path D:\Logs\MyApp -File -Recurse | Where-Object { $_.LastWriteTime -lt $cutoff } | Remove-Item -WhatIf执行前先看一眼-WhatIf的输出确认筛选结果符合预期再把-WhatIf去掉。这里有几个关键点-File参数只匹配文件不匹配目录避免误删整个目录结构。LastWriteTime对应 Linux 的mtime是判断文件是否超期的最可靠时间戳。Remove-Item默认没有输出如果要日志需要自己记录每个删除的文件。4.2 PowerShell 脚本的健壮性改造生产用的脚本不能只有一行我一般写成这样$TargetDir D:\Logs\MyApp $ExcludeDirs (keep, archive) $RetentionDays 30 $CutoffDate (Get-Date).AddDays(-$RetentionDays) $LogFile D:\Logs\Cleanup\clean_$(Get-Date -Format yyyyMMdd).log if (!(Test-Path (Split-Path $LogFile))) { New-Item -ItemType Directory -Path (Split-Path $LogFile) -Force | Out-Null } try { Get-ChildItem -Path $TargetDir -File -Recurse | Where-Object { $_.LastWriteTime -lt $CutoffDate -and $_.DirectoryName -notmatch ($ExcludeDirs -join |) } | ForEach-Object { Remove-Item -LiteralPath $_.FullName -Force -ErrorAction Continue Add-Content -Path $LogFile -Value [$(Get-Date -Format yyyy-MM-dd HH:mm:ss)] DELETED $($_.FullName) } } catch { Add-Content -Path $LogFile -Value [$(Get-Date -Format yyyy-MM-dd HH:mm:ss)] ERROR $($_.Exception.Message) throw }说明几个细节用-LiteralPath而不是-Path可避免文件路径里有[、]等通配符字符时误解析。-ErrorAction Continue表示单个文件删除失败时不要中断整个循环而是继续处理下一个文件错误会输出到控制台可以通过任务计划捕获。排除目录用正则^keep$|archive将多个词合并匹配目录名。Add-Content是追加写入不会像Out-File那样覆盖。4.3 任务计划程序设置要点PowerShell 脚本写好之后在任务计划程序里创建任务重点配置四项常规勾选不管用户是否登录都要运行使用具有目标目录读取/删除权限的专用账户。注意这个账户的密码变更后任务计划里的密码也要更新否则任务会静默失败。触发器选择按预定计划每天一次例如凌晨 03:00。如果服务器很多最好随机错开 5~15 分钟避免所有机器同时清理造成文件服务器抖动。操作程序填powershell.exe添加参数填-ExecutionPolicy Bypass -File D:\Scripts\clean_old_files.ps1。条件取消只有在计算机使用交流电源时才启动此任务否则在经常断电的机器上任务会被跳过服务器的话还要取消唤醒计算机运行此任务以避免计划外的唤醒当然偶尔需要。4.4 中文路径、编码和文件占用Windows 环境有几个独有的坑我踩过之后总结如下脚本编码PowerShell 5.1 默认按 ANSI 解析无 BOM 的.ps1文件。只要脚本里出现中文路径就必须把文件保存为UTF-8 with BOM或 UTF-16 LE否则字符会乱码Get-ChildItem根本找不到目录。文件占用Windows 上被进程占用的文件Remove-Item会报另一个程序正在使用。如果你的清理目标和某个常驻服务有关建议脚本里先尝试删除失败则记录文件名等下一次运行再重试。不建议用-Force强行删可能会把正被写入的文件删出一个残缺状态。回收站Remove-Item是永久删除不进回收站。如果担心误删可以用 PowerShell 调用Microsoft.VisualBasic.FileIO.FileSystem的DeleteFile并指定RecycleOption.SendToRecycleBin。这个设计适合备份目录后续我会单独说延迟删除方案。5. 让清理任务看得见、能回滚的三个工程化习惯定时删除的脚本本身不难难的是出问题之后你能不能快速定位、快速恢复。我见过太多清理脚本跑了半年没出问题一出问题就是事故级。下面三个习惯是我坚持了多年、救过我命的做法。5.1 干跑模式和双阶段提交删除操作一定要支持干跑dry run。Linux 脚本里用--dry-run参数Windows 脚本里用Remove-Item -WhatIf。这不是给测试用的而是给正式运行时的巡检用的。我一般的做法是每周手动跑一次干跑输出将要删除的文件列表发给相关人员。确认无误后由定时任务在当天夜里正式执行删除。更进一步把符合删除条件的文件先写到一个列表文件第二天的定时任务读取这个列表删除。这样如果你发现列表里有不该删的文件还有 24 小时窗口去处理。这就是双阶段提交的思路虽然说不上多高级但能保证你在自动化删除面前始终保留人工确认的机会。5.2 删除日志要记什么日志不是简单地记一下删了多少文件而是要满足两个目标出事时能回溯、误删时能恢复。我常用的删除日志字段如下字段示例说明操作时间2025-06-12 02:30:01记录删除发生的精确时间操作类型DRY_RUN / DELETED / ERROR区分干跑和实际删除完整文件路径/data/backup/db_20250501.sql绝对路径不能是相对路径文件大小1.2 GB用于评估清理量文件最后修改时间2025-05-01 01:00:00验证是否满足时间边界脚本名称与参数clean.sh --prod快速定位版本如果条件允许日志文件本身也要做轮转免得清理脚本把自己的日志堆成新的超期文件。5.3 用回收站式目录做延迟删除这一条主要针对备份文件、不可再生文件。做法很简单清理脚本不直接删而是用mvLinux或Move-ItemWindows把超期文件移到同一个磁盘下的一个隐藏目录比如/data/.trash/然后另一个脚本 7 天后再把.trash里的文件彻底删除。为什么在同磁盘移动因为同文件系统下mv只是修改目录项不涉及数据拷贝速度极快删除大量小文件也不会有 IO 峰值。跨文件系统的mv则等效于复制删除在大文件场景下会把磁盘 IO 拉满反而影响业务。延迟删除的时间我一般设为 3~7 天。这个窗口期内如果发现误删还能手动把文件从.trash移回来超过窗口期文件再进一次日常备份流程即可。成本极低但心理安全感非常高。5.4 失败通知和磁盘监控定时任务最大的特点是无人值守所以它必须有能力自己发现问题。我坚持两条线脚本失败通知Linux 下可以用sendmail或直接调用企业微信/钉钉 webhookWindows 下可以让任务计划把运行结果发送到自己的监控系统。注意不要只通知失败还要定时汇报今天删了多少、剩余磁盘多少因为一个脚本长期不删除文件也可能是出问题了。磁盘水位监控清理脚本只是治标真正要紧的是磁盘使用率。我一般会在脚本开头先取一次磁盘使用率写入日志如果超过 85% 就在删除结束后额外输出一条 WARN方便后续扩容或调整保留天数。6. 踩坑复盘与可以直接抄的最终方案这一节我把我自己实际踩过的坑和最终模板都整理出来。不是理论分析是真正在生产的服务器和文件服务器上跑过的方案。6.1 时间边界30、30、-30的真意find -mtime的参数经常让人困惑我详细解释一次-mtime 30修改时间超过30 天即 720 小时以上也就是第 31 天及更早的文件。-mtime 30修改时间正好落在 30~31 天之间的文件。-mtime -30修改时间在 30 天以内。所以保留 30 天删除更早文件的正确写法是-mtime 29还是-mtime 30这取决于你定义的第 30 天是包含还是不包含。严格按英文文档的说法30是超过 30 天也就是从第 31 天开始删第 30 天当天的不删。如果你的业务明确说超过 30 天就用30如果内部算法是保留 30 个自然日按整日取可能需要29。我的建议是把保留天数定义成至少保留 N×24 小时写进注释这样就不会被边界坑到。更精确的写法可以这样find $dir -type f ! -newermt 30 days ago -delete这句话的含义是修改时间早于 30 天前的文件语义更直观但对大部分场景-mtime 30已经够用。6.2 文件正在使用和权限不足的坑Linux 下删除文件不需要文件可写权限只需要父目录写权限。但find在扫描时如果遇到无权限的子目录会不断报Permission denied即使你可以删除其中个别文件。我常用2/dev/null屏蔽掉这些噪音但要注意屏蔽后如果目录权限出问题扫描结果不完整你会误以为没有可删文件所以日志里最好还是记录一份扫描错误数量。Windows 上则是另一套逻辑。文件被进程独占时Remove-Item会抛异常在脚本里我建议把失败的路径单独写到一个failed_YYYYMMDD.log等下一次任务启动时再重试一次。不要用-Force去硬删可能把正在读写的文件删出问题。6.3 文件名中的空格和换行符怎样安全删除一个真实案例某应用的日志文件名包含日期和随机串如log - 20250501 - abc.log中间有空格。用最粗暴的find ... | xargs rm处理xargs 会把空格当成分隔符一条路径可能被拆成三段最后删了个寂寞甚至误删到其他文件。正确姿势有两个# 方式一print0 while read推荐 find $dir -type f -mtime 30 -print0 | while IFS read -r -d file; do rm -f $file done # 方式二直接使用 -delete find $dir -type f -mtime 30 -deletefind -delete自己就安全地处理了文件名边界不需要额外做字符转义。所以如果你只是简单清理-delete是最稳妥的。需要更复杂的排除或归档逻辑时再用print0方式。6.4 可以直接抄的最终脚本模板Linux Windows最后放两个我目前生产环境在用的精简模板。Linux 版本把干跑、日志、文件锁、排除目录都集成在一起Windows 版本就是第 4 章的增强版加了失败重试列表。Linux/usr/local/bin/clean_old_files.sh#!/usr/bin/env bash export PATH/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin TARGET_DIRS(/var/log/app1 /data/backup) RETENTION_DAYS30 LOG_DIR/var/log/cleanup LOCK_FILE/tmp/clean_old_files.lock exec 99$LOCK_FILE flock -n 99 || { echo $(date %F %T) 已有清理任务在运行退出 $LOG_DIR/error.log; exit 1; } mkdir -p $LOG_DIR LOG_FILE$LOG_DIR/clean_$(date %Y%m%d).log for dir in ${TARGET_DIRS[]}; do if [[ ! -d $dir ]]; then echo $(date %F %T) [WARN] 目录不存在 $dir $LOG_FILE continue fi find $dir -type f -mtime ${RETENTION_DAYS} -print0 | while IFS read -r -d file; do case $file in *keep*|*archive*) continue ;; esac echo $(date %F %T) [DELETED] $file $LOG_FILE rm -f $file done doneWindowsD:\Scripts\clean_old_files.ps1$TargetDir D:\Backup $RetentionDays 30 $CutoffDate (Get-Date).AddDays(-$RetentionDays) $LogDir D:\Logs\Cleanup $LogFile Join-Path $LogDir (clean_{0}.log -f (Get-Date -Format yyyyMMdd)) $FailedFile Join-Path $LogDir (failed_{0}.log -f (Get-Date -Format yyyyMMdd)) if (!(Test-Path $LogDir)) { New-Item -ItemType Directory -Path $LogDir -Force | Out-Null } Get-ChildItem -Path $TargetDir -File -Recurse | Where-Object { $_.LastWriteTime -lt $CutoffDate -and $_.DirectoryName -notmatch keep|archive } | ForEach-Object { try { Remove-Item -LiteralPath $_.FullName -Force -ErrorAction Stop Add-Content -Path $LogFile -Value [$(Get-Date -Format yyyy-MM-dd HH:mm:ss)] DELETED $($_.FullName) } catch { Add-Content -Path $FailedFile -Value [$(Get-Date -Format yyyy-MM-dd HH:mm:ss)] FAILED $($_.FullName) : $($_.Exception.Message) } }测试步骤永远是同一套先在测试目录放 20 个不同时间戳的文件分别设定保留天数为 0 / 1 / 30跑干跑模式核对输出再跑正式模式最后检查日志内容。一定要模拟文件名带空格、带中文、带方括号的情况这一步能帮你挡掉很多线上事故。最后分享一个小技巧每次清理脚本上线之前我会先在目录里放一个锚点文件比如forever_keep_marker.txt内容写上如果这个文件被删除说明脚本匹配规则有问题。然后观察一周确认它安然无恙才敢放心让脚本全自动运行。这个做法很土但在无人值守的定时任务面前它比任何代码 review 都更直观。
返回列表