“linux中保留最新3份文件的shell脚本”,这个标题我太熟了。干运维的头三年,几乎每个星期都要为“磁盘满了”、“日志文件把inode吃光了”、“备份脚本往外卖时把目录里的历史包全删了”这类破事擦屁股。后来我总结出一个规律:运维的很多痛点,本质上是缺少一个简单、可靠、可解释的“文件轮转策略”——而“只保留最新N份”正是其中最典型的场景,没有之一。
这篇文章我准备把话说明白:为什么你最需要的就是“保留最新3份”(而不是按天、按月、按大小)?为了做到这个“最新”,Linux里到底有哪几条技术路线?各有什么坑?最后给你一个可以直接抄作业的脚本,加上部署到cron里之后那些文档里永远不会写清楚的坑。没有一句废话,全是拿真实服务器换来的教训。
1. 为什么“保留最新3份”是Linux上需求最硬的清理逻辑
很多刚入行的朋友看到“保留最新3份”第一反应是:这有什么好写的?一个ls -t | tail -n +4 | xargs rm不就完事了吗?确实,这句话就是那个脚本的灵魂,但真让你写成一个能上生产、不出事故、别人能维护的脚本,你会发现里面全是细节。
先说清楚需求来源。我在这几年里,“保留最新3份”主要出现在三种场景里,逻辑细节其实并不一样:
-
日志文件轮转。比如Nginx访问日志、Java应用日志、消息队列的消费位点日志。这类文件通常是按天、按小时滚动生成的,比如
app.log.20250601、app.log.20250602。磁盘空间有限,日志不能无限堆。保留最近3份的意思是:最多只有3个日志文件躺在历史记录里,第4个老文件出现后就会被清掉。这样即使磁盘不富裕,写满3份日志需要的空间也是上下可控的。 -
备份文件的轮转。比如每天凌晨用mysqldump导出一份数据库备份,文件名带日期时间戳,放在
/data/backup/目录。保留3份的意思就是:最多存在3个备份点——你今天导出的、昨天的、前天的,大前天的自动删除。万一今天导出的备份有问题,你还能回退到昨天或者前天。超过3天就没必要留了,再老的备份基本没人在意,只会悄悄吃满磁盘。 -
应用发布包/安装包的保留。很多公司在服务器上发布新版本前,会把上一版的jar包、war包、二进制文件留在本地作为回滚退路。这些文件往往是带版本号的,比如
order-service-2.3.4.jar、order-service-2.3.5.jar。保留最新3份,就是在服务器上始终留最近3个可回滚版本,老版本自动清掉。这在发布系统没做集中管理、全靠手工的服务器上尤其常见。
这三个场景的共同特点是什么?它们都要求“永远只留最近N个”,而不是“删除某个死期之前的老文件”。 这决定了脚本逻辑的核心:不是“找多老的文件删”,而是“找出文件集合里的最新3个,把其余全部干掉”。
另外一个小型运维经验:为什么偏偏是3?不是2、不是5?我的经验是,3是一个运维上很舒服的数字——日志场景下,如果今天的日志有问题,你至少还有昨天和前天的两份可以对比;备份场景下,3个备份点能覆盖“昨天还能用,今天这个坏了”的大多数翻车;发布包场景下,3个版本能覆盖“新版本有大坑,回退1个版本还不够,得回退2个”的情况。设置成5个、10个当然也可以,但磁盘成本和对“历史包袱”的容忍度就完全不同了。3是性价比最高的默认值,所以这个脚本的需求天然是“保留最新3份”。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 实现“保留最新N份”的三条技术路线:原理、适用场景与致命短板
既然需求明确了,实现思路就非常关键。很多人写这个脚本的时候,脑子里只有“ls -t + tail -n +4”这一个组合,但实际在Linux上有几种主流路线,每一种都有各自的适用边界和致命短板。我按从“直觉”到“工程化”的顺序挨个讲。
2.1 路线一:ls -t 按修改时间排序 + tail 定位旧文件
这条路线是最直觉的。思路是这样的:ls -t 会把当前目录下的文件按修改时间从新到旧排列,tail -n +4 会从第4行开始输出,也就是跳过最新的3个文件,剩下的都是要删的。然后把它们交给xargs rm。
bash复制#!/bin/bash
ls -t /data/logs/*.log | tail -n +4 | xargs rm -f
这个方案能跑通吗?能。但这只是“能跑”而已。它有几个难以忽视的工程短板:
- 文件名的空格问题。
ls默认输出是“以空白分割的列”,如果文件名里有空格(比如app log 2025.log),xargs会把它当成两个文件来处理,删除时找不到文件反而报错,甚至误删其他文件。 - 管道中丢失特殊字符。文件名里如果有单引号、双引号、反斜杠、换行符,
ls的文本输出方式根本没法正确处理。 - 目录里文件非常多时,ls的文本排序不可控。虽然加了
-t是按mtime,但mtime精度到秒时,同一秒生成的文件顺序可能乱掉。 - 脚本可读性一般,而且处理不了“如果目录里文件不足3个”的边界情况(这时候
tail -n +4输出为空,xargs会报“no arguments”或者干脆什么都不干,得加-r参数)。
所以这条路线适合什么场景?临时手动清一下、目录里文件命名规律且干净、你能肉眼确认目录里没有诡异文件名的场景。 用在生产脚本里,风险太大。
2.2 路线二:find -mtime 按修改时间过滤老文件
这也是很常见的一种写法:
bash复制#!/bin/bash
find /data/logs -name "*.log" -mtime +3 -delete
这条命令的意思是:找到修改时间超过3天的log文件,然后删掉。听起来是不是很符合需求?完全不是一回事。 这个逻辑是“按时间窗口过滤”,不是“按数量保留”。
如果有一天你忘了跑备份,连续5天没有新日志生成,那么第6天来清理时,-mtime +3会瞬间把所有日志全删掉,留下的不是“最新的3份”而是“0份”。你原本想要的是“给我的历史留3个保底”,结果在日志中断的场景下全部丢光。这就是按时间过滤和按数量保留的本质区别:前者受不了任务空窗期,后者永远有保底。
那find -mtime完全没用吗?也不是。它适合那种“文件本身按天生成、且生成严格稳定、你对缺失无所谓”的场景。但绝不是“保留最新3份”的最佳实现。如果你非要用find的思路,至少得加个“留够3个”的保底逻辑,让脚本先数一下文件数量,不足3个就退出——但这种变通代码写起来已经比用sort -V还复杂了。
2.3 路线三:sort -V 按版本号排序(真正能解决生产问题的一条线)
sort -V 是GNU coreutils提供的“自然版本号排序”功能。它会识别文件名里的数字和点号,并按人类直觉的版本号前后顺序排序,而不是按纯字典序。
bash复制ls -1 /data/app/order-service-*.jar | sort -V | head -n -3
sort -V会把order-service-2.3.10.jar排在order-service-2.3.9.jar后面,而字典序排序会把2.3.10排在2.3.9前面(因为1在ASCII码上比9小)。这是版本号文件保留场景下最常见的排序陷阱。 大多数文件名里都带数字,如果直接ls | sort,第10个文件永远排在老前面,你的“最新3份”就会永远选错。
所以我对这三条路线的结论是:
- 临时手动清文件:
ls -t | tail能接受。 - 按天日志、且接受“任务断档会误删”的风险:
find -mtime可以。 - 生产环境、发布包、备份文件、含数字的日志文件:必须用
sort -V或find -printf '%T@ %p\n' | sort -rn这类能精确表达“时间顺序”的方式。
下面我详细展开 sort -V 的原理,并给出可落地的完整脚本。
3. 文件名里的“数字大小”骗了你:版本号排序原理与字典序陷阱
我专门开一章讲这个,是因为这个问题特别隐蔽,而且会造成看起来没问题、实际删错文件的最严重后果。我见过不止一次有人把order-service-2.3.9.jar删了,留下order-service-2.3.10.jar,然后回滚时找不到可用的旧版本,最后只能从备份系统里拉文件,丢人丢到运维群里。
3.1 字典序 vs 自然版本序
默认的sort命令用的是字典序(lexicographical order),也就是按字符的ASCII码一个一个比。我们来看两组典型的文件名:
code复制order-service-2.3.9.jar
order-service-2.3.10.jar
在字典序排序下,2.3.9和2.3.10的比较过程是这样的:前几个字符order-service-2.3.完全一样,然后开始比较9和1这两个字符。在ASCII码表里,'9'的码值是57,'1'的码值是49,因此'1'排在'9'前面。所以字典序会认为2.3.10比2.3.9小,排序结果是:
code复制order-service-2.3.10.jar
order-service-2.3.9.jar
也就是说,在字典序的“最新”概念里,2.3.10反而是旧文件。如果你用ls | sort | tail -n +4来保留“最新3份”,第4个被你删除的文件恰恰是真正的2.3.9,而2.3.10会被错误地当作旧文件保留下。这就是一个典型的“文件越多,时间越长,删得越错”的bug。
而sort -V则不一样,它内部会把数字字段解析成真正的数值进行比较,10 > 9,所以排序结果是:
code复制order-service-2.3.9.jar
order-service-2.3.10.jar
这样2.3.9在旧文件一侧,2.3.10在新文件一侧,保留最新的3份就能正确选到2.3.10、2.3.9的前面一个版本。
3.2 版本号排序的实际执行细节
如果你在终端里试一下:
bash复制mkdir /tmp/sort-test && cd /tmp/sort-test
touch order-service-2.3.9.jar order-service-2.3.10.jar order-service-2.3.11.jar
ls -1 | sort # 输出: order-service-2.3.10.jar ... (顺序错)
ls -1 | sort -V # 输出: order-service-2.3.9.jar ... order-service-2.3.11.jar (正确)
你就能直观看到区别。sort -V不仅能处理数字版本,还能处理前置0的情况,比如2.03和2.3它会视作相等,这在某些特殊的命名规则下会有影响,但绝大多数业务场景里,sort -V就是最稳妥的选择。
另外还需要注意一点:中文环境可能影响排序规则。 如果你的系统locale不是C或者POSIX,sort的比较行为可能受LC_COLLATE影响。建议在脚本开头显式设置LC_ALL=C,让排序规则回到最原始的字节序,避免引入了中文locale的拼音排序规则,导致版本号排序结果变得难以预测。关于这个坑,后面我会再提。
3.3 什么场景不该用 sort -V
sort -V 并非万能,它的短板在哪里?如果文件名里的数字不是版本号,而是日期时间戳,比如backup-202506011200.sql,这种文件名用sort -V也可以按数字顺序排出来,逻辑上其实是对的——因为日期时间戳本身就是“数值越大,时间越新”。所以日期场景下sort -V也能用。
但真正的短板是:如果文件名里没有数字,或者文件名里的数字和业务时间顺序无关(比如a-filename-1.dat生成时间比a-filename-2.dat晚,但1在数字上小于2),那就不能靠文件名排序来判断新旧了。这时候就得回到真实文件时间属性上,即find -printf '%T@ %p\n' | sort -rn这一路。
所以文件名排序这条路的基本原则是:只有文件名里的数字顺序与时间顺序一致,才能用sort -V。如果不一致,就老老实实用find -printf按mtime排。
4. 可直接落地的“保留最新3份”脚本:从单目录到多目录的工程化写法
下面进入正题,给你一个可以直接放到生产环境使用的脚本。这个脚本我已经在好几台服务器上跑过,处理过几十万份文件,不敢说完美,但踩过的坑基本都填平了。
4.1 完整脚本:Bash版本
bash复制#!/bin/bash
# description: 保留指定目录下指定模式的最新N份文件,删除其余旧文件
# usage: ./keep_latest_files.sh <directory> <file_pattern> [keep_count]
set -u
DIR="$1"
PATTERN="$2"
KEEP="${3:-3}"
if [ ! -d "$DIR" ]; then
echo "[ERROR] Directory not found: $DIR" >&2
exit 1
fi
# 强制使用C locale,避免排序规则受系统语言环境影响
export LC_ALL=C
# 找到匹配的文件,按版本号排序,然后跳过最新的KEEP份
mapfile -t TO_DELETE < <(
find "$DIR" -maxdepth 1 -type f -name "$PATTERN" -printf '%f\n' \
| sort -V \
| head -n -"$KEEP"
)
if [ "${#TO_DELETE[@]}" -eq 0 ]; then
echo "[INFO] No old file to delete in $DIR"
exit 0
fi
echo "[INFO] Will delete the following ${#TO_DELETE[@]} file(s):"
for f in "${TO_DELETE[@]}"; do
echo " - $DIR/$f"
done
# 安全删除:先放入回收站目录,确认无误后再清理
STAMP=$(date +%Y%m%d%H%M%S)
TRASH="$DIR/.trash_$STAMP"
mkdir -p "$TRASH"
for f in "${TO_DELETE[@]}"; do
mv "$DIR/$f" "$TRASH/"
done
echo "[INFO] Moved to trash: $TRASH"
echo "[INFO] If you need to restore, move files back from $TRASH"
echo "[INFO] To permanently delete, run: rm -rf $TRASH"
如果把这套“先移入回收站”的方式换成直接rm,脚本长度能减半,但我强烈不建议直接删。按我的经验,一个清理脚本真正的风险不是“多删了文件”,而是“删错了文件,甚至没法恢复”。多花两行代码做“软删除”,是运维对自己最大的善意。
4.2 脚本的关键设计和为什么这样写
set -u:遇到未定义变量直接报错退出。这个开关比你想象中重要,尤其在cron环境下,某些环境变量没赋值时,${3:-3}这类写法才能安全兜底。我这里没有用set -e,因为find在“目录为空”情况下也会返回0,不会误触发退出;而用set -e反而会在某些管道型命令的非零返回时把脚本弄挂。这类细节,只有吃过亏才知道。find -maxdepth 1 -type f -name "$PATTERN":只搜索当前目录层级下的普通文件,不递归,避免误删子目录中的同名文件。-printf '%f\n':只输出文件名,不带目录路径前缀。这样后面sort -V排序时不会因为路径里包含其他数字而干扰版本号排序。而且“输出文件名、删除时再由脚本拼回完整路径”这个写法,能避免find带路径输出时对路径中特殊字符的额外解析麻烦。head -n -"$KEEP":GNU head支持-n -3这种“取除了最后3行以外的所有行”的语法。注意,macOS自带的BSD head不支持负数行号,如果你只在Linux上跑,没问题;但如果你的脚本要跨平台,请用head -n -3 | tail -n +1的兼容写法,或者直接用head -n -3并在异常时退化为sed处理。mapfile -t:把head输出的每一行读入数组。这比for f in $(find ...)要好得多,因为$(...)会把输出按空格拆词,文件名里的空格会直接炸裂。- 回收站机制:把要删的文件移动到一个带时间戳的临时目录,而不是直接删除。这样即使脚本逻辑有bug、或者某个文件被误判为旧文件,也能在几分钟内恢复。实际使用中,你可以跑完以后检查回收站目录内容,确认无误,再手动
rm -rf。
4.3 测试整个脚本,先别正式跑
上线一个清理脚本,最忌讳的就是直接跑。我建议的完整测试流程是:
bash复制# 1. 建测试目录
mkdir -p /tmp/keep-test && cd /tmp/keep-test
# 2. 生成一批测试文件(模拟版本号)
for v in 1.0.1 1.0.2 1.0.3 1.0.10 1.0.11; do
touch app-$v.jar.dat
done
# 3. 先dry-run:直接用find + sort + head看看会选出哪些文件
find /tmp/keep-test -maxdepth 1 -type f -name 'app-*.jar.dat' -printf '%f\n' | sort -V
find /tmp/keep-test -maxdepth 1 -type f -name 'app-*.jar.dat' -printf '%f\n' | sort -V | head -n -3
# 4. 执行正式脚本,观察输出
bash /path/to/keep_latest_files.sh /tmp/keep-test 'app-*.jar.dat' 3
# 5. 检查回收站目录里的文件是否符合预期
ls -la /tmp/keep-test/.trash_*
这个测试流程走下来,你基本能确认脚本行为符合预期。尤其第一步的find | sort -V输出,可以帮你提前看到sort -V的排序结果,避免真正执行时才发现选错了文件。
5. 生产环境部署的坑,全在cron里:环境变量、绝对路径与日志
脚本写好了,大概率是要放到cron里每天或每小时执行。这一段很多人忽略,但它恰恰是“脚本在终端能跑、在cron里全废”的根源。
5.1 cron环境的PATH和locale问题
cron执行脚本时,环境非常干净,PATH默认可能只有/usr/bin:/bin,而你的脚本里用到的find、sort、head如果装在/usr/local/bin下,就可能直接报“command not found”。更隐蔽的是locale问题:某些系统在cron环境下LC_ALL没有被设置,sort会去读/etc/locale.gen或/etc/default/locale下的配置,一旦这个配置被破坏或缺失,sort的行为就会退化到字典序甚至直接报错。
所以我建议:
- 脚本里用绝对路径写命令,或者至少在脚本开头明确设置
PATH:
bash复制export PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
-
脚本里显式设置
LC_ALL=C,避免排序和多语言环境的不确定性(我前面已经写了)。 -
cron调用时,使用
bash /path/to/script的完整调用方式,不要依赖脚本的shebang或相对路径。
5.2 crontab配置示例
假设你的脚本放在/opt/scripts/keep_latest_files.sh,要给/data/logs目录下的app-*.log保留最新的3份,每小时执行一次,那么cron配置可以这样写:
cron复制# 每小时的第5分钟执行一次
5 * * * * /bin/bash /opt/scripts/keep_latest_files.sh /data/logs 'app-*.log' 3 >> /var/log/keep_latest_files.log 2>&1
这里的输出重定向>> /var/log/keep_latest_files.log 2>&1很重要。cron执行脚本时的标准输出默认会以邮件的形式发给root用户(如果你的系统没配置邮件服务,这些输出就全丢了),把输出重定向到日志文件里,既能保留执行记录,也方便排查问题。
同时,日志文件本身也需要轮转。如果你的清理脚本频率是每小时一次,那这个日志文件也建议用logrotate或者再嵌套一个保留脚本清理一下,别让日志文件本身成为磁盘占用大户。我的经验是:给这个日志文件挂一条logrotate规则,让它保留最近7天的日志就够了。
5.3 脚本上线前必须执行的“试运行三板斧”
正式部署到cron前,我强烈建议以手动方式至少做三轮:
第一轮:dry-run检查选择逻辑。 先不实际删除,用脚本里的find | sort -V | head -n -KEEP命令输出看看,你选的模式匹配到了多少文件,哪些会被删。这一步能帮你验证PATTERN是否写对。比如你要保留app-*.log,结果忘了加引号,在shell里直接展开成了当前目录下的具体文件,那cron里执行时就会因找不到文件而什么都匹配不到。
第二轮:备份模式跑一次。 使用脚本的默认回收站模式,故意让它处理一批测试文件。然后检查回收站目录里的文件是不是你预期的“旧文件”。如果发现预期错误,可以从回收站恢复,不至于造成数据损失。
第三轮:正式上线后连续三天人工检查。 看cron日志和目录里的文件数量,确认脚本三天都执行成功,且目录中始终只有3份文件。如果系统里还有别的进程也在生成同名文件,也要确认不会造成竞争条件——比如你这边刚把app-1.0.10.log移入回收站,新进程又基于旧文件名创建了新的日志,这会对业务产生表面上的“丢文件”影响。
5.4 多目录扩展:用配置文件或循环处理
如果服务器上不止一个目录需要保留策略,我建议写成“配置文件驱动”的形式,而不是复制多个脚本。简单做法:
bash复制#!/bin/bash
# keep_multiple_dirs.sh
set -u
PATH=/usr/local/bin:/usr/bin:/bin
export LC_ALL=C
# 每行格式: 目录 文件名模式 保留份数
while read -r dir pattern keep; do
[ -z "$dir" ] && continue
echo ">>> Processing: $dir $pattern $keep"
/opt/scripts/keep_latest_files.sh "$dir" "$pattern" "${keep:-3}"
done < /opt/scripts/keep_latest_files.conf
配置文件/opt/scripts/keep_latest_files.conf只需要几行:
code复制/data/logs/nginx access-*.log 3
/data/backup/mysql backup-*.sql.gz 5
/data/release order-service-*.jar 3
这样加目录、加规则的时候,不用改脚本,只改配置文件,别人接手时看规则也一目了然。运维脚本最大的敌人不是实现难,而是维护难——任何能让维护者一眼看懂的抽象,都非常值得做。
6. 那些文档不会写的真实事故:符号链接、权限、文件名里的换行符
脚本可以写得很漂亮,但真实服务器上总有一些你想不到的脏数据。这一节分享三个我真实踩过的坑,每一个都足以让清理脚本从“正确”变成“灾难”。
6.1 符号链接与-type f的隐藏陷阱
find -type f会匹配符号链接指向的普通文件吗?答案是不会。在GNU find里,-type f只匹配普通文件本身,不匹配symlink。但很多清理脚本用find列出“要删的文件”后,会直接交给xargs rm——而rm是跟着符号链接走的,rm symlink删的是符号链接本身,不是它指向的目标文件。
这听起来问题不大,但实际生产里有个非常坑的场景:如果目录里有config.log这个符号链接指向/data/shared/config.log,你的保留逻辑只统计了目录下的普通文件,没统计symlink,但在清理其他文件时,symlink不会删掉,它永远占着那个名字。 这样可能在用户感知上出现“新文件没生成、旧文件还在”的错觉。更危险的是,如果程序在写日志时,日志落盘路径是一个指向别处的symlink,而你的清理脚本又把type f选出来的文件给删了,那程序接下来在原有路径上写文件时,就会因路径不存在而报错。
我现在的做法是:清理前先统计目录下-type f和-type l的数量,如果有symlink,要么显式排除,要么明确告诉维护者目录里有符号链接。 绝对不要把symlink当成普通文件来统计配额。
6.2 权限导致的“假删除失败”
如果你用回收站方案(mv到临时目录),那么删除时涉及的不只是文件本身的权限,还有目标目录的写权限。有些场景下,脚本以root运行,那没问题;但如果你为了安全用普通运维账号跑脚本,而那个账号对目标目录只有读权限,没有写权限,那么mv会直接失败,文件原地不动,脚本可能会在循环里反复报错。
这事的坑在于:清理脚本本身没报错,但实际文件都没删掉,磁盘还是满的。 由于日志文件本身可能是root创建的,普通账号没法写入目录,这个问题在真实服务器上太常见了。
建议脚本中在移动文件前显式判断权限:
bash复制if [ ! -w "$DIR" ]; then
echo "[ERROR] No write permission on $DIR" >&2
exit 1
fi
或者干脆在crontab里用root账号跑,但做之前一定要确认脚本里没有会把全盘搞坏的逻辑。权限不足比权限过大好处理,但脚本一定要对权限不足有显式报错。
6.3 文件名里的换行符和不可见字符
这是最极端但也是最容易出现的一类问题。某些程序在生成文件名时,会意外地带上换行符(比如把\n拼进了文件名)。这种文件你用ls看几乎看不出来,但一进find输出,它就会被head、mapfile之类的按“行”处理的工具当成两个文件——一个正常名字,一个空行,或者一个截断的名字。这会导致清理脚本出现以下两种后果之一:
- 文件名被截断,脚本试图删除一个不存在的文件,报错;
- 更糟糕的是,整个文件名输出被换行符打断,
mapfile切数组时把一行拆成两行,后续mv会先处理前半段名字,再单独处理换行符后面的残段,造成删除错误。
这个问题最稳妥的解法是:用find -print0代替-printf '%f\n',用空字符\0作为文件名分隔符,然后配合xargs -0或while IFS= read -r -d ''来处理。空字符在文件名中是不允许出现的,所以用它做分隔符可以完整保留文件名里的换行符、空格等一切特殊字符。
比如删文件这一段可以改成:
bash复制find "$DIR" -maxdepth 1 -type f -name "$PATTERN" -print0 \
| sort -zV \
| head -z -n -"$KEEP" \
| xargs -0 -r rm -f
这里用到sort -z、head -z和xargs -0这套“zoo”组合,就是专门为了处理不可预测的文件名而生的。虽然阅读门槛高一点,但生产脚本里建议直接上这个写法,宁可写难看一点,也别踩到文件名特殊字符的雷。
7. 从“保留最新3份”到“完整备份轮转策略”的进阶思考
写到最后,我想跳出脚本本身,聊点方法论层面的东西。“保留最新3份”看似只是一个简单的工具需求,但它背后是一整套备份和日志的轮转策略思想。 理解了它,你就能理解为什么很多大型系统的日志系统会对“分片数量”、“保留周期”做精细控制。
在实际运维里,“保留最新3份”往往是“保留策略”的简化版。如果文件是按天生成的,你往往还想保留“近7天”的,加上“最近3份”的双重控制;如果文件是跨机器备份的,你可能还想把旧文件打包压缩后放到低频存储里,再在本地只留3份,形成“本地快照+远端归档”的两级结构。这些扩展思路,本质上都是在“保留最新3份”的核心思想上叠加策略维度。
我个人建议,如果你管理的是生产服务器,可以设计三层清理策略:
- 本地快速清理:即本文的脚本,保证磁盘不会被历史文件吃满。
- 归档策略:对有价值的备份文件,打包后定期移至对象存储或另一台低频磁盘挂载点,而不是直接删除。这样即使本地只剩3份新文件,远端还留有更长时间跨度的历史。
- 审计与告警:清理脚本执行的每次输出都应写入日志。一旦目录中文件数量异常增多,或者某个目录的清理任务连续失败,就要有告警通知(比如通过cron脚本监控procs文件数量)。
这套体系跑起来以后,“保留最新3份”这种小脚本就不再是一个孤立工具,而是整个运维可观测性的一环。
拿我自己的经验打个比方:这套脚本和策略,我用了一年多。有一次线上某个应用疯狂生成日志,到下午4点的时候磁盘直接满了,所有写入报错。等我登上去的时候,发现应用日志目录里本来应该只剩3份日志,但那个目录里堆了几十份——原因不是脚本没跑,而是这个应用写日志时用的是一个随机后缀的文件名,我的PATTERN写的是app-*.log,结果它生成的文件名是app-20250612-R2D2.log这类,没匹配上。那一次事故给我的教训是:保留策略的服务对象是真实的文件命名规律,脚本只是策略的执行者。上线前一定要和业务开发确认“文件到底怎么命名、多久生成一个”。
所以最后送大家一句话:“保留最新3份”不是记住一个命令,而是搞定一套“筛选、排序、保护、清理、可观测”的完整链路。看懂这个链路,你写出的不只是一个脚本,而是无论遇到什么乱七八糟的线上磁盘问题都能稳住的底气。如果你已经在生产环境跑了类似的脚本,欢迎在评论区分享你还踩过什么坑——我至今还在收集关于文件名安全性的诡异案例。
