ARTICLE DETAIL

资讯详情

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

Linux定时清理过期文件:find+cron脚本实战与避坑指南

Linux定时清理过期文件:find+cron脚本实战与避坑指南 1. 需求拆解这个任务到底在做什么先说说这类需求的来源。很多人第一次遇到定时删除超过天数的文件大概率是服务器日志把磁盘塞满了。我去过不少公司看到过一堆让人哭笑不得的处理方式有人手动SSH上去rm -rf不敢删多只敢删昨天的有人在crontab里硬写一行find /var/log -name *.log -exec rm -rf {} \;结果连带把正在写入的日志文件也断了句柄还有人干脆不管等磁盘满了再停机拉数据重建。定时删除超过天数的文件这需求听着简单本质上是一个带策略的数据生命周期管理问题。你真正要解决的问题不是删文件而是让磁盘上的数据保持在一个可控、可预期、可回溯的状态。核心由三个动作组合而成筛选找出超过N天的文件、定时周期性地执行删除、留痕知道删了什么、什么时候删的。任何只做了其中一部分的方案后续都会出问题。我把这个需求拆开看其实隐藏着几个维度第一时间判定维度。文件有修改时间mtime、访问时间atime、状态变更时间ctime。大多数人的需求是按最后修改时间算但也不排除有人按最后访问时间或创建时间来算。时间判定选错了删除结果会完全偏离预期。比如你想清理30天没动过的临时文件用的是-mtime 30按修改时间筛结果发现下载目录每隔几天就有人访问一次文件永远清不掉——这时候你就知道该用-atime了。第二路径范围维度。是清理某个应用日志目录还是全盘扫描范围越界会带来灾难性后果我见过有人写find / -name *.log -mtime 7结果把系统日志、其他服务日志全删了好在大多数Linux发行版对正在写的日志文件持有句柄删掉后还能继续写但服务一旦重启日志文件就断档了。第三执行周期维度。是每天清理还是每周、每月不同的业务对数据保留周期要求完全不同日志可能保留7天备份可能要留3090天CI构建产物可能只留3天。这个周期要可配置而不是写死在脚本里。第四安全降级维度。删除不可逆所以你至少需要一个演练模式只打印不删除一个正式模式一个日志输出。我第一次写这种脚本时没有演练模式直接上生产结果把一堆3个月前的缓存文件全清了幸好缓存可以重新生成不然就是事故。这个需求最典型的落地场景是应用日志服务器、备份文件服务器、CI制品库、临时下载目录、以及各类容器的挂载数据卷。只要你的数据目录里会积累过期数据这个需求就有效。在执行方案选型前先把需求拆成明确的输入参数目标目录、保留天数、执行频率、是否递归、是否处理空目录、日志输出位置。把这些参数列成一个表格后面写脚本时直接对应填值即可。2. 方案选型Linux、Windows以及第三方工具的取舍2.1 Linux 环境find cron 是绝对主流在Linux服务器上最主流的方案是find命令配合cron。为什么不是你自己写遍历程序因为find已经足够成熟而且它原生支持基于时间、类型、大小、权限等多维筛选性能也不错。真要自己去遍历目录树你是和文件系统较劲没必要。find核心语法其实是这一条find /你的目录 -type f -mtime 7 -name *.log -delete这里面有三个关键参数用的时候一定要搞清楚-type f只匹配普通文件避免误删目录或符号链接。-mtime 7匹配最后修改时间超过7×24小时的文件。注意是有个号含义是大于7天不是7天以内。-name *.log按文件名模式筛选可选但强烈建议加上能防止误删非目标文件。-mtime的时间单位是天它接受n、n、-n三种写法我稍后详细讲。只有把时间参数彻底搞懂这个脚本才算真正站稳。2.2 Windows 环境计划任务 PowerShell 是可行方案Windows 上的习惯不太一样很多人喜欢用批处理.bat但我会推荐PowerShell因为它的Get-ChildItem加Where-Object可以非常自然地过滤文件时间然后Remove-Item删除文件。PowerShell对日期比较、路径处理比批处理强太多了。一条典型的清理命令是这样的$target D:\data\logs $days 7 $cutoff (Get-Date).AddDays(-$days) Get-ChildItem -Path $target -File -Recurse | Where-Object { $_.LastWriteTime -lt $cutoff } | Remove-Item -Force然后把这个脚本放到任务计划程序中设定触发时间即可。Windows任务计划程序的GUI界面比较直观但注意要设置使用最高权限运行否则有些受保护目录删不动。2.3 第三方工具该不该用市面上有一些专门的旧文件清理工具比如rmcache、clean_old_files这类命令行工具它们封装好了参数开箱即用。用还是不用我的建议是如果只是单机、单目录、规则固定直接用find或PowerShell脚本就够了没必要引入一个不熟悉的工具这需要额外维护。如果是多台机器、多套规则、需要统一配置管理那么可以考虑用cron脚本配置文件的模式或者用Ansible推送脚本。如果公司已经有配置管理平台比如SaltStack、Puppet、Ansible那就顺着平台来不要单独造轮子。2.4 选型对比表方案优点缺点适用场景Linux find cron命令简洁、进程开销小、无需额外依赖参数有学习成本时间判定容易搞混绝大多数Linux服务器Windows PowerShell 计划任务与系统集成好、脚本灵活首次配置路径较绕权限坑多Windows Server业务机第三方命令行工具上手快、参数友好依赖外部维护、团队要额外学习临时机器、不常维护的节点配置管理平台分发统一管理、可审计需要先搭建平台规则变更要走流程多机房、多节点、规范化运维从我自己实践的角度我倾向于自写脚本cron。原因是这类需求太基础了基础到任何第三方工具都可能因为版本更新、行为变化带来不可控的地方。自己写脚本自己维护反而最稳妥。3. 核心参数与脚本骨架动手前必须搞懂的三组概念3.1 find时间参数这部分很多人一错就是错一年find的时间参数我建议你把它当成一个小表格来记写法含义类比-mtime 7文件最后修改时间恰好是7天前那一档精确到天正好过期7天-mtime 7文件最后修改时间早于7天前也就是超过7天放了7天往上的垃圾-mtime -7文件最后修改时间在7天以内最近7天动过的文件-mmin 120最后修改时间超过120分钟前两小时前的老文件注意7才是超过7天这是最基础也最重要的一个边界。我见过有人删文件时写-mtime 7结果什么也没删掉因为恰好精确落在7天前那一档的文件几乎没有于是误以为脚本没生效也有人想保留7天以内的文件用了-mtime -7去执行删除结果把7天以来的全删了幸好当天还有备份。如果需求是超过7天的文件请一定、务必、每次都用7。另外两个容易混淆的时间参数-atime按最后访问时间筛选。适合清理很久没被访问过的文件比如临时目录。缺点是每次访问文件都会更新atime如果你的文件系统开启了relatime现在大多数Linux默认atime的更新会有限制实际效果和-mtime差别可能没那么大。-ctime按inode状态变更时间筛选。状态变更包括权限、属主、硬链接数改变等。对普通文件来说ctime的变化经常和mtime同步但如果你想判断这个文件被改过权限可以用它。日常清理用得较少。3.2 删除的边界目录、符号链接与管道文件很多人在find里只写-type f没有考虑目录本身。如果你只删文件那目录会一直存在着可能挂载了上万个空目录这虽然不占多少磁盘空间但会让目录树非常凌乱也影响ls、备份工具的性能。更好的做法是分两步先删文件再处理空目录。Linux下删除空目录可以用rmdir但遇到嵌套空目录比较麻烦。用find可以一次性解决find $TARGET_DIR -depth -type d -empty -delete注意这里有两个关键点-depth先处理子目录再处理父目录-empty保证只删空的。否则如果你在删除目录时会遇到目录非空的报错。如果目录里还有需要保留的文件不要着急删目录先把文件的过滤条件做好就行。3.3 文件名特殊字符带来的坑我遇到过文件名里带空格、带中括号、带换行符的情况。大多数新手直接写find ... -delete没有问题因为find -delete对文件名是安全的。但如果你想在删除前做日志、做二次判断比如用-exec或管道传给while read就要小心了。安全的写法是使用-print0配合while IFS read -r -d 这样文件名里的空格和换行符都能正确处理find $TARGET_DIR -type f -mtime 7 -print0 | while IFS read -r -d file; do echo $file done这个写法比find ... -print | grep或者for循环稳妥得多。日志文件名一般不会带换行但备份文件名常带空格生产环境必须按最坏情况准备。3.4 脚本骨架变量、配置、日志、dry-run一个不能少我建议所有清理脚本都至少包含这些模块可配置变量目标路径、保留天数、日志路径、是否只测试。日志函数记录每次执行的时间、删除的文件、错误信息。dry-run模式控制是否真实删除先跑一遍看效果。错误处理目标目录不存在、没有权限时明确报错退出。下面是一个我常用、也非常适合直接上生产的脚本骨架你把它保存为cleanup_old_files.sh就行#!/bin/bash # 清理超过指定天数的文件 # 用法: ./cleanup_old_files.sh [目标目录] [保留天数] [dry-run] set -u TARGET_DIR${1:-/data/app/logs} THRESHOLD_DAYS${2:-7} DRY_RUN${3:-0} # 1只输出删除计划0真实删除 LOG_FILE/var/log/cleanup_files.log log() { echo $(date %Y-%m-%d %H:%M:%S) $* $LOG_FILE } if [ ! -d $TARGET_DIR ]; then log ERROR: directory $TARGET_DIR not exist exit 1 fi log -------------- START --------------- log target$TARGET_DIR threshold${THRESHOLD_DAYS} days dry_run$DRY_RUN find $TARGET_DIR -type f -mtime ${THRESHOLD_DAYS} -print0 2$LOG_FILE | while IFS read -r -d file; do if [ $DRY_RUN 1 ]; then log [DRY] would delete: $file else if rm -f $file 2$LOG_FILE; then log deleted: $file else log fail to delete: $file fi fi done # 删除过期空目录注意按深度优先 if [ $DRY_RUN 1 ]; then find $TARGET_DIR -depth -type d -empty -mtime ${THRESHOLD_DAYS} -print 2$LOG_FILE | while IFS read -r d; do log [DRY] would rmdir: $d done else find $TARGET_DIR -depth -type d -empty -mtime ${THRESHOLD_DAYS} -delete 2$LOG_FILE fi log -------------- FINISH --------------这个脚本的优点是参数可配传目标目录和天数就行不用改代码。默认带dry-run能力上线前先演练。删除动作单独记录日志出了问题能回溯。set -u预防变量为空造成的意外行为。实际生产时我会把脚本放到/usr/local/bin/cleanup_old_files.sh然后chmod x。4. 实操过程从测试到定时上线一个完整案例4.1 先构造一个实验环境我最开始测试这种脚本时不想拿生产目录试于是自己造了一个测试目录。你可以这样操作mkdir -p /tmp/cleanup_test/{logs,backup} touch -d 15 days ago /tmp/cleanup_test/logs/old1.log touch -d 15 days ago /tmp/cleanup_test/logs/old2.log touch -d 2 days ago /tmp/cleanup_test/logs/new1.log touch -d 10 days ago /tmp/cleanup_test/backup/old_bak.tar.gz touch -d 2 days ago /tmp/cleanup_test/backup/new_bak.tar.gz这里用touch -d来伪造文件的修改时间是测试清理脚本最核心的手段。你要是有一个文件需要保留千万别手滑改错了时间。然后先跑dry-runbash cleanup_old_files.sh /tmp/cleanup_test 7 1预期结果是logs/old1.log、logs/old2.log被标记为待删除。logs/new1.log不删除。backup/old_bak.tar.gz被标记。backup/new_bak.tar.gz不删除。跑完dry-run看日志里每个文件的标记是否符合预期。如果符合再跑真实删除bash cleanup_old_files.sh /tmp/cleanup_test 7然后再次用find /tmp/cleanup_test -type f验证。4.2 从零到定时任务的完整步骤把调试好的脚本放到正式路径加入cron把脚本放到/usr/local/bin/下赋予执行权限。编辑crontabcrontab -e先理解一下crontab的五个字段分钟 小时 日 月 星期。比如每天凌晨3点执行0 3 * * * /usr/local/bin/cleanup_old_files.sh /data/app/logs 7如果希望每周六凌晨3点20分执行20 3 * * 6 /usr/local/bin/cleanup_old_files.sh /data/app/logs 7保存后验证cron配置是否生效。最粗暴有效的方法是等它跑一次看日志但更快的验证方法是临时把执行时间设到下一分钟例如现在的系统时间是14:05你就写成6 14 * * *等它执行完看日志输出确认无误后再改回正式时间。注意cron进程的环境变量极少脚本里最好全部使用绝对路径而且脚本本身要可执行、有正确的#!行。4.3 验证cron服务是否正常运行有些基础不牢的机器crond服务压根没启动。检查方法systemctl status crond # 或 systemctl status cron如果没启动systemctl enable --now crond日志是验证执行效果的最有力证据我每次执行完都会打开/var/log/cleanup_files.log看一眼尾部确认S-T-A-R-T和F-I-N-I-S-H记录完整。4.4 Windows下的完整实操Windows环境我还是建议用PowerShell脚本加计划任务的组合。脚本保存为cleanup_old_files.ps1param( [string]$TargetDir D:\data\logs, [int]$Days 7, [switch]$DryRun ) $cutoff (Get-Date).AddDays(-$Days) $logFile D:\logs\cleanup_files.log function Write-Log($message) { $(Get-Date -Format yyyy-MM-dd HH:mm:ss) $message | Out-File -FilePath $logFile -Append -Encoding utf8 } Write-Log START target$TargetDir days$Days Get-ChildItem -Path $TargetDir -File -Recurse | Where-Object { $_.LastWriteTime -lt $cutoff } | ForEach-Object { if ($DryRun) { Write-Log [DRY] $($_.FullName) } else { Remove-Item -LiteralPath $_.FullName -Force -ErrorAction Stop Write-Log deleted $($_.FullName) } } Write-Log FINISH然后在任务计划程序中创建一个基本任务触发器选择每天或每周操作选择启动程序程序填powershell.exe参数填-ExecutionPolicy Bypass -File D:\scripts\cleanup_old_files.ps1 -TargetDir D:\data\logs -Days 7注意在任务计划程序的常规页签里勾选使用最高权限运行否则很多系统目录你删不动甚至访问都会失败。实操下来我最深的体会是Windows计划任务调试比Linux cron更绕因为没有人会在计划任务里帮你捕获脚本报错。所以PowerShell脚本里一定要写日志函数把每一步丢到文件里否则执行失败你根本不知道问题出在哪。5. 常见问题与排查思路5.1 常见问题速查表症状可能原因解决方法文件没有被删除find时间参数写错比如用了-mtime 7而非7确认使用-mtime 7文件没有被删除目标目录不存在脚本直接退出先手动执行ls -ld确认路径文件没有被删除cron服务未启动systemctl status crond文件没有被删除cron环境变量中找不到命令脚本使用绝对路径文件没有被删除文件正在被进程占用删除失败查看日志中的fail信息排除进程占用日志文件越来越大脚本本身弹性日志没做轮转对清理日志也做轮转或定期清理误删了不该删的文件没先跑dry-run直接生产每次上线前先跑dry-run路径严格限定目录一大堆空文件夹只删文件不删目录增加-depth -type d -empty -deletefind找到文件太多删除耗时太长目录文件数量极大单线程find不够快分批次清理按时间范围切片或考虑用-mmin缩短单次范围5.2 独家避坑经验为什么你的脚本总是差一分钟这里分享一个我踩过几次的坑cron的分钟字段是精确匹配不是每过X分钟。很多人想写每5分钟跑一次结果写成了5 * * * *以为5点、6点、7点的第5分钟各跑一次。但这个写法确实只会在每个小时的第五分钟执行一天24次。如果你真的想每5分钟要写*/5 * * * *。在清理任务里这种差别可能没那么致命但如果你还写了0 0 * * *这样的整点任务务必意识到它只代表每天00:00执行一次。还有时区问题。cron和find判断时间用的都是系统本地时间。如果你的机器时区不是北京时间而你的业务预期是今天凌晨3点执行那最终删除的文件边界会跟着系统时间走。我曾经在一台UTC时区的机器上配了每天凌晨1点清理7天前的日志结果是北京时间早上9点才执行日志保留时间看似是7天实际成了7天又8小时。这个偏差如果不注意审计的时候会很困惑。关于日志轮转我的建议是给清理脚本自己的日志也加一个简单的轮转。一种最低成本的做法是让cron定期把这个日志文件打包后清空0 4 * * 0 /usr/bin/gzip -S cleanup_files.log.$(date \%W).gz /var/log/cleanup_files.log这样每周一自动把上一周的清理日志压缩归档。你也可以用logrotate但单脚本场景我觉得cron打包够用。5.3 误删之后的补救思路误删文件这种事谁都希望不发生但万一发生最有效的补救手段是提前设置好备份策略。你可以在清理脚本里对即将删除的文件做一个可配置的移动而不是删除的过渡方案。比如先移动到/data/trash/目录保留48小时后二次清理这样即便发现误删还有一个恢复窗口。这种延迟删除策略在生产环境里特别实用很多云厂商的对象存储也是这么做的删除后先进回收站保留几天。我在管理备份目录时就喜欢先执行移动到.trash模式的脚本确认一周无误后再跑真正的删除任务。trash目录本身也要纳入清理范围不然就变成只进不出的垃圾场了find /data/trash -type f -mtime 2 -delete5.4 处理特殊目录的技巧如果目录里文件名带空格或特殊符号推荐使用-print0与空字符分隔。find命令中还有一个参数经常被忽略-xdev。加上它之后find不会跨文件系统查找。什么意思呢比如你的目标目录是/data但/data下面挂载了一个独立的磁盘到/data/disk2如果没加-xdevfind会顺着目录树爬到另一块磁盘去清理这很可能不是你期望的。加上-xdev只清理当前挂载点下的内容更安全。另外一个参数-maxdepth也很实用。有些目录结构特别深而你只清理前两层用-maxdepth 2限制查找深度可以显著提升find的速度也避免误入一些深层临时目录。6. 最后分享一点实战体会做了这么久运维我对这类定时清理任务最大的体会是它不是一次性工作而是一个需要持续观察、定期复盘的小系统。我见过太多人把脚本部署到cron里就再也不管了直到磁盘报警才发现脚本因为权限问题已经静默失败了好几个月。原因往往是脚本里没有日志或者日志没有告警出错了也没人知道。所以我的习惯是清理脚本必须留日志且日志必须能通过监控系统感知到。哪怕只是每天看一眼/var/log/cleanup_files.log的尾部也比直觉觉得它在跑可靠得多。对于重要目录我还会把清理量做成一个简单的报表每周过一眼本周删了多少文件、释放了多少空间、有没有异常的大幅波动。这样既可以提前发现业务异常也能验证清理策略是否还合理。另外清理策略不是一层不变的。你的业务在涨日志产生速度在变快原定保留7天的日志可能改成保留3天更合适或者某些目录的访问模式变了原来按修改时间清理现在得按访问时间清理。这条基本都是每个季度要重新检查一遍。如果你想把这件事做得更自动化一点可以把脚本的参数放到一个配置文件里让cron每天读配置规则变更时只需要改配置不动脚本。但具体怎么设计配置格式这取决于你的脚本习惯没有标准答案。我的建议是先用最简单、明确的参数化脚本跑起来跑出日志和数据再逐步增加灵活性。
返回列表