搞运维和开发的朋友,十有八九都被日志坑过。不是磁盘被一堆几百MB甚至几个G的日志文件塞爆,就是排查问题的时候发现关键日志早就被覆盖了。我自己就经历过一次:一台业务服务器半夜告警,登录上去一看,/var/log 目录占了整整40多个G,连 df -h 都要卡好几秒。从那次之后,我彻底把“日志自动管理”这件事当成正经项目来做,而不是随手写个 > /tmp/app.log 就糊弄过去。
这篇内容适合所有在用 Linux 做服务器、跑应用、维护系统的朋友。不管你是刚入行的运维新手,还是自己折腾个人项目的开发者,只要服务器上会产出日志,就离不开这套东西。我会从整体思路讲起,把自动切割、压缩、按时间清理到定时任务整个链路拆开讲透,全程都有可以直接抄走的配置和脚本。
1. 日志管理的整体设计与方案选型
日志自动管理这件事,看着简单,做起来容易踩坑。核心原因在于:日志文件是动态增长的,而磁盘空间是固定有限的。如果不给它上一套“自动机制”,它迟早会把你坑一次。
1.1 要解决的核心问题
我们把日志管理的需求拆开来看,其实是四个问题的组合:
- 日志文件无限增长:一个 Java 服务如果开了 debug 级别日志,一天产出几个 G 是很正常的事。如果完全不限制,磁盘迟早被写满。
- 单文件过大导致读取困难:几个 G 的日志文件,你用
vi打开直接卡死,用tail -f倒是能跟,但想往回翻就很痛苦,grep一次也要等半天。 - 历史日志没有一个合理的清理周期:有些日志可能要留 180 天用于安全审计,有些日志留 7 天就足够。如果一刀切全删,出了问题没法追溯;如果全留着,磁盘又扛不住。
- 操作完全依赖人工:今天记得手动清理了,明天忘了怎么办?必须靠系统机制自动完成,不能靠人的自觉性。
设计这套方案的时候,我没有绕弯子,直接采用了 Linux 生态里面最成熟的两个组合:logrotate 做轮转切割,find + crontab 做定期清理。这两个工具都是系统自带的,不需要额外装任何东西,也没有性能损耗,稳定可靠。
1.2 为什么选 logrotate 而不是纯脚本
肯定有人会问:我直接写个 shell 脚本,用 mv 把日志改名,再用 kill -HUP 让进程重新打开日志文件,不也能实现吗?
可以,但没必要。logrotate 本身已经把这些事封装好了,而且处理了很多边缘情况:
- 它是通过 cron 每天定时触发的,不需要你额外维护一个定时任务。
- 支持按大小切割(size 参数)、按时间切割(daily、weekly、monthly)。
- 支持压缩(compress)、归档(olddir)、自定义日期后缀(dateext)。
- 支持在轮转完成后执行命令(postrotate),这对需要发信号给进程重新打开日志的场景特别重要。
- 它的配置文件语法清晰,字段含义明确,几行就能完成一个服务的日志轮转配置。
更重要的是,logrotate 是绝大多数 Linux 发行版预装好的,几乎所有主流软件包在安装时会自动往 /etc/logrotate.d/ 里丢一份自己的轮转配置。你只需要看懂并修改它,而不是从零造轮子。
1.3 整体方案架构
这套日志自动管理方案一共分为三层:
- 轮转切割层:用 logrotate 把正在写的日志按天或按大小切分,保留最近 N 份,再对历史文件做压缩。
- 清理回收层:用一个独立的清理脚本,对超过保留期限的日志文件做删除,或者对某些不需要进 logrotate 的目录做粗暴清理。
- 定时执行层:用 crontab 把清理脚本挂上去,每天凌晨低峰期跑一次,同时给 logrotate 也留出执行窗口。
换句话说,logrotate 负责“把大文件切成小文件”,find 脚本负责“把过期的小文件删掉”。两者分工明确,互不冲突。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 日志轮转核心配置与参数详解
logrotate 的主配置在 /etc/logrotate.conf,针对具体服务的轮转规则放在 /etc/logrotate.d/ 下面,比如 /etc/logrotate.d/nginx、/etc/logrotate.d/syslog。我实际使用中发现,直接在 /etc/logrotate.d/ 下新建独立配置文件,比改动全局配置要清晰得多,也好维护。
2.1 一份可直接落地的 logrotate 配置
下面这份配置是我在线上环境用了一年多的模板,适用范围很广,你只需要改掉路径和保留份数就能用:
bash复制cat > /etc/logrotate.d/myapp <<'EOF'
/var/log/myapp/*.log {
daily
rotate 30
compress
delaycompress
missingok
notifempty
dateext
dateformat -%Y%m%d
copytruncate
}
EOF
逐项参数说明一下:
daily:每天轮转一次。如果你愿意,也可以写成weekly或size 500M,按日志产出速度实际情况来。rotate 30:保留最近 30 份轮转后的日志文件。因为是按天的,所以相当于保留一个月。compress:轮转完成后对历史文件做 gzip 压缩。注意它压缩的是“轮转下来之后”的文件,正在写入的日志文件不会被压缩。delaycompress:延迟一层压缩。意思是本轮轮转出来的文件先不压缩,等下一天轮转时再压缩。这个参数配合copytruncate用特别重要,原因我在后面“踩坑”里讲。missingok:如果日志文件不存在,或日志目录没匹配到文件,不会报错。这个必须加,否则某天日志目录被清空,cron 会疯狂往 root 邮箱发报错。notifempty:文件为空就不轮转。避免生成一堆无意义的空压缩包。dateext+dateformat -%Y%m%d:轮转出来的文件加上日期后缀,形式是app.log-20250213,而不是默认的app.log.1。日期后缀一眼就能看出是哪天的日志,排查问题的时候体验好得多。copytruncate:复制日志文件内容到新文件后,把原文件截断为 0 字节。这意味着正在写日志的进程不需要重启,文件句柄也不会断。
2.2 copytruncate 和 postrotate 的选择逻辑
logrotate 处理正在被进程写入的日志文件,有两条路线:
路线 A:rename + create + postrotate
传统做法是先把 app.log 改名成 app.log.1,然后新建一个 app.log,再通过 postrotate 脚本给进程发送信号(如 kill -USR1 $(cat /var/run/nginx.pid)),让进程重新打开日志文件。nginx、syslog 这类标准的守护进程都支持这种方式,好处是日志文件不会丢失任何数据。
路线 B:copytruncate
不移动文件,直接用 cp 把当前内容复制一份出来,再 truncate -s 0 把原文件清空。进程完全感知不到变化,依然往原来的文件句柄里写内容。
我个人的经验是:如果你的应用没有处理“日志文件被重命名”这种情况的能力(很多自研的小服务根本没做这个处理),就用 copytruncate,它容错率最高。但要注意两个副作用:第一,复制和截断之间存在极短的时间窗口,极端情况下可能丢一点点日志;第二,文件特别大时复制有 I/O 开销。所以对 nginx 这类支持信号重开日志的标准服务,我更推荐走 create + postrotate 路线:
bash复制cat > /etc/logrotate.d/nginx <<'EOF'
/var/log/nginx/*.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
sharedscripts
postrotate
if [ -f /var/run/nginx.pid ]; then
kill -USR1 $(cat /var/run/nginx.pid)
fi
endscript
}
EOF
这里有个细节:postrotate 脚本是可选的,但如果你用了它,建议同时加上 sharedscripts。这个参数的含义是“所有匹配的日志文件整体轮转完成后,只执行一次脚本”,而不是“每个文件轮转后都执行一次”。不写的话,如果你的通配符匹配了 3 个日志文件,脚本就会执行 3 次,白折腾进程。
3. 日志清理脚本与定时任务落地
logrotate 解决的是“日志太多、单个文件太大”的问题,但它有自己的边界:它只管自己配置文件里列出来的那些日志。如果你有一个程序把日志写在了一个非标准路径下,或者你只是临时想对某个目录做一次大扫除,logrotate 就管不到了。这时候就轮到 find + crontab 组合登场。
3.1 核心清理命令写法和参数说明
日志清理命令的核心就是 find,按修改时间找文件并删除:
bash复制find /var/log/myapp -type f -name "*.log" -mtime +15 -delete
这条命令的意思是:在 /var/log/myapp 目录下,找到所有以 .log 结尾的普通文件,而且最后修改时间在 15 天之前,直接删除。
-mtime +15 这个参数要注意,它表示“超过 15 天”而不是“正好 15 天”。find 的计算方式是按“24 小时”为单位的整数天来算的,-mtime +15 的意思是找那些最后一次修改时间距今超过 15 整天(即 360 小时以上)的文件,-mtime 15 是正好 15 天前那个时间点附近 24 小时内修改的文件,-mtime -15 是 15 天内修改过的文件。
如果你需要更精准的控制,可以用 -mmin,单位是分钟。比如 -mmin +720 就是 12 小时之前。这个在测试清理脚本的时候非常有用,不需要等一天才能看到效果。
再强调一点:-delete 这个动作尽量放在最末尾,而且在正式执行前,先把 -delete 去掉,换成 -print 或 -ls 跑一遍,看看 find 找出来的文件到底是不是你想删的。线上环境我做任何批量删除之前,一定会先“打印名单”确认,再真正动手。
3.2 一个更完整的清理脚本
实际场景往往比一条命令复杂,单独一条 find 命令可能不够。比如你可能需要同时清理多个目录、排除某些重要子目录、并且把清理动作记到日志里方便日后审计。
这里分享一个我在多台服务器上用的清理脚本,它做了三件事:清理指定目录下超过 N 天的 *.log 文件、清理临时目录下的过期文件、把每次执行的结果写入审计日志。
bash复制#!/bin/bash
# description: 日志清理脚本,建议每天凌晨执行一次
CLEAN_LOG="/var/log/cleanup_history.log"
RETENTION_DAYS=15
echo "=========== 清理任务开始:$(date '+%F %T') ===========" >> ${CLEAN_LOG}
# 清理主应用日志目录
find /var/log/myapp -type f -name "*.log" -mtime +${RETENTION_DAYS} -print >> ${CLEAN_LOG} -delete
# 清理临时目录,排除我们还需要保留的文件
find /opt/app/tmp -type f -mtime +7 ! -name "*.lock" ! -name "*.pid" -print >> ${CLEAN_LOG} -delete
# 清理可能存在的旧压缩包
find /var/log -type f -name "*.gz" -mtime +30 -print >> ${CLEAN_LOG} -delete
echo "=========== 清理任务结束:$(date '+%F %T') ===========" >> ${CLEAN_LOG}
写这个脚本有几个注意点:
-print和-delete同时出现时,-print在前,-delete在后,执行顺序是先打印再删除,这样审计日志里能看到删了哪个文件。! -name "*.lock"表示取反,把不想删除的 .lock 文件排除掉。注意!在 bash 里可能有历史扩展问题,在脚本文件里没问题,但如果直接在命令行粘贴执行,建议用!前加反斜杠转义,或者写成-not -name。- 清理动作一定要加上目录限制,比如
-type f只处理普通文件,不能匹配到目录,防止误伤。
3.3 用 crontab 把清理任务挂起来
脚本写好后,给它加执行权限,再扔到 crontab 里。先看现有 crontab 配置:
bash复制crontab -l
然后编辑追加任务:
bash复制crontab -e
在里面加这一行:
code复制15 3 * * * /opt/scripts/clean_old_logs.sh >/dev/null 2>&1
这行的意思是:每天凌晨 3 点 15 分执行一次清理脚本,标准输出和错误输出全部丢到 /dev/null,避免 cron 因为脚本有输出而往 root 邮箱发垃圾邮件。
选凌晨 3 点而不是半夜 12 点,是我个人的习惯。凌晨 1 点到 3 点一般是业务最低谷,而且很多系统自身的 crontab 任务(比如 logrotate 的那个 daily 定时)通常也在凌晨两三点左右执行,稍微错开一点时间窗口,能避免 I/O 高峰撞车。
这里要特别推荐一个检查 crontab 配置是否生效的小技巧:如果机器上安装了 cronie,直接用 crontab -l 能看、crontab -e 能改。不少发行版现在的 cron 服务名称是 crond 或 cron,实际查看可以用 systemctl status crond 或 systemctl status cron,不确认的话 ps aux | grep cron 看一下最直接。
4. 实战场景:为常见应用配置日志管理方案
上面讲的是通用配置,这个部分我专门挑了几个典型场景来拆解。线上的问题总是比教科书上更复杂,每个应用写日志的方式都不一样,需要“对症下药”。
4.1 Nginx 日志切割与按天归档
Nginx 日志是我们运维中最常见的一种。默认情况下,access.log 和 error.log 写在 /var/log/nginx/ 下,如果不做处理,一个高流量站点一天能写下几个 G 的 access 日志。
配置中需要注意的一个细节是,postrotate 里给 master 进程发的是 USR1 信号,这个信号是 Nginx 约定好用来重新打开日志文件的。还有别忘了 sharedscripts,否则匹配到两个日志文件时,信号会发两次,浪费资源不说,极端情况下可能导致日志句柄被重复打开。
如果你要对 access_log 做更精细化的处理,比如按域名分目录、让不同站点保留不同天数,可以在 nginx 配置里就用变量指定日志路径,然后给 logrotate 写对应规则。这个方案我在多站点虚拟主机环境里用过,效果很好,缺点是要给每个域名或站点组写一份 logrotate 配置,管理成本高一些。量力而行。
4.2 Java 服务日志(Spring Boot)的实战配置
Java 服务是日志管理的重灾区。Spring Boot 默认是 Logback,很多项目图省事直接让它输出到一个文件,然后这个文件就无限膨胀。遇到这种情况,我的标准做法是:应用自身的 logback-spring.xml 里配置好滚动策略(按大小和时间),同时在外部再套一层 logrotate,双保险。
如果应用实在不好改配置文件,就在 logrotate 上用 copytruncate 方案硬切,虽然理论上可能丢最后一点点日志,但配合 delaycompress 可以把丢数据的影响压到极低。为什么这里一定建议 delaycompress?
这里展开说一下:如果你同时配置了 compress 和 copytruncate,logrotate 会在复制完文件后立刻对该文件做 gzip 压缩。但此刻应用进程极有可能还在往原文件句柄里写内容,复制出来的文件本身可能还在变动中,压缩出来的压缩包有概率是损坏的。而 delaycompress 会让本轮轮转出来的文件先放在原地不压缩,等下一轮轮转时再压缩,那时候这个文件已经固定不再变化了,压缩结果必然是完整的。这个坑我踩过,压缩包损坏后 grep 不到内容,排查问题的时候差点以为是日志真的没写。
4.3 系统日志 /var/log 下的通用保护策略
系统层的日志(/var/log/messages、/var/log/cron、/var/log/secure 等)最好不要自己乱动,绝大多数发行版已经装好了 logrotate 配置,在 /etc/logrotate.d/ 下面有 syslog 或 rsyslog 对应的规则。你要做的只是检查一下它们的 rotate 参数,确认保留天数是不是符合公司安全策略要求。
很多新手容易犯的一个错误是:为了清理空间,直接 rm -rf /var/log/*,把正在被 syslog 进程写入的文件句柄给删了。在 Linux 上,删除正在写入的文件不会立即释放磁盘空间,因为进程仍然持有这个文件句柄,空间要等进程关掉文件描述符或者重启进程才会真正释放。结果就是:你删了半天,df -h 一看空间没变,等进程重启的时候磁盘才突然释放。这是一种非常误导人的现象,只有正确理解了“文件句柄”这个概念才能想通。
如果你遇到删了文件但空间没释放的情况,可以用 lsof | grep deleted 找到那些“已删除但还开着”的文件,确认对应的进程后,重启该进程即可彻底释放空间。注意,如果是 nginx、php-fpm 这类能平滑重载的服务,用 nginx -s reload 或 systemctl reload 就行,不需要粗暴 restart。
5. 常见问题与排查技巧实录
写日志管理方案容易,真正跑起来会遇到各种问题。我把这一年多来遇到的典型问题整理成了速查表,很多问题不是看文档能发现的,需要实际踩过或者是看到错误日志才能定位。
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 日志文件越来越大,但 size 选项不生效 | logrotate 每天只跑一次,size 只在 cron 触发时才检查 | 检查 /etc/anacrontab 或 logrotate 自己的 cron 时间;如果要求严格准时就自己加 crontab 半小时触发一次 |
轮转时提示 error: skipping because parent directory has insecure permissions |
日志目录权限过于开放(如 777),logrotate 出于安全限制跳过处理 | 修改目录权限为 755 或 750,确保属主正确 |
| 压缩包解压出来内容不全或损坏 | 同时用了 compress 和 copytruncate |
加上 delaycompress,让文件不再写入后再压缩 |
| 日志文件被删但磁盘空间没释放 | 进程仍持有已删除文件句柄 | 执行 lsof | grep deleted 定位进程并重启/reload |
| 多个日志匹配项时 postrotate 重复执行 | 缺少 sharedscripts 参数 |
在配置里加上 sharedscripts,让脚本只执行一次 |
日志目录里出现大量 .1 这种旧格式文件 |
dateext 没启用 | 加上 dateext 和 dateformat -%Y%m%d |
| logrotate 手动执行没问题,cron 自动执行却报错 | cron 环境变量 PATH 和手工 shell 不同 | 在 logrotate 配置的脚本里写全路径,避免依赖 PATH |
| 日志轮转后应用输出中断或重复 | 应用不支持 rename 后信号重开日志 | 改用 copytruncate,或者给 postrotate 增加正确的 signal 逻辑 |
5.1 手工触发 logrotate 验证配置是否正常
配置写好后,不可能等一天再看效果,必须手工触发来验证。logrotate 支持 debug 模式,它会模拟执行,告诉你“如果现在轮转会做什么”,但不会真的动文件:
bash复制logrotate -d /etc/logrotate.d/myapp
-d 是 debug,输出信息非常详细,能看到它匹配了哪些文件、将执行什么动作,非常适合写配置时候用来自查。
确认没问题后,用 -v 强制执行一次:
bash复制logrotate -vf /etc/logrotate.d/myapp
-v 是 verbose,-f 是 force,强制轮转。这个命令会真的切割日志文件,注意执行前确认应用能接受这种操作,别在用户正在用系统的时候乱试。
执行完看下目录:
bash复制ls -lh /var/log/myapp/
正常情况下你能看到类似 app.log-20250213 这样的文件,并且原 app.log 还在正常增长。
5.2 一个典型的 find 清理误删问题
最后分享一个比较典型的踩坑经历。我曾经写过一个清理脚本,用 find 删过期日志文件,参数是:
bash复制find /data/app/logs -mtime +7 -name "*.log" -exec rm -f {} \;
看着没问题,实际跑起来却把当天的日志也删了。排查了半天,问题出在服务器时间上。那台机器系统时间被人调快了将近一周,导致文件的 mtime 比真实时间“看起来”老很多,find 判断超过 7 天就直接删了。
从那以后,我在所有清理脚本里加了两条铁律:一是在删除前先 -print 到审计日志里,二是定期做一次 NTP 时间同步检查,timedatectl status 看看时间是否正常。服务器时间不准,受影响的远不止日志清理一个环节,但日志清理是最容易“秒级删光”的。
5.3 如果日志目录已经爆炸了怎么救急
有时候问题已经发生了,磁盘满了,数据库写不进去。这时候等 logrotate 慢慢跑来不及,得立刻救急。我的建议顺序是这样:
- 先别急着删,用
du -sh /var/log/* | sort -rh | head -20看看哪些目录和文件最大,确定“罪魁祸首”。 - 如果单个日志文件已经几个 G,而且这个应用日志不重要,直接
truncate -s 0 /路径/日志文件清空。注意是truncate而不是rm,truncate能保持文件句柄不变,进程不用重启。 - 对确实要保留的历史压缩包,先压缩再挪走:
tar czf /backup/logs_$(date +%F).tar.gz /var/log/yourapp/*.gz。 - 清理出空间后,立刻把 logrotate 配置和 crontab 清理脚本部署好,避免下一次爆炸。
救急的时候能理解“句柄”和“文件大小”的关系特别关键,这决定了你用 truncate 还是 rm,也决定了释放空间是否是“即时生效”。
6. 日志管理的进一步扩展
前面介绍的内容已经能解决 90% 的日志自动管理需求,但实际工作里还有一些场景需要更细的处理方式。这个部分我补充几个高阶用法,算是我自己在项目中验证过、觉得值得留作备份的经验。
6.1 用 ulimit 或 systemd 限制单个日志文件体积
有些情况下,应用本身的日志库行为不可控,比如公司采购的商业软件、外包团队留下的遗留系统,它们可能自管自地写一个巨型文件,根本不读你的 logrotate 配置。遇到这类黑盒,能做的就是从一开始限制文件体积上限。
如果是 systemd 托管的服务,可以通过 systemd 的 StandardOutput=file: 或 LogLevelMax 参数来限制输出,也可以在 service 文件里加 LimitFSIZE=1073741824 限制单个文件大小上限。这个参数是给进程设置文件大小限制,超过 1G 后进程写入文件会收到 SIGXFSZ 信号,可能直接崩掉,所以生产环境要谨慎使用,最好先在测试环境验证应用对这个信号的反应。
如果是直接跑脚本或二进制,可以用 ulimit -f 设置文件大小限制:
bash复制ulimit -f 1048576
./start_your_app.sh
ulimit -f 的单位是“块”而不是字节,在不同系统上块的大小不同,一般 512 字节一块,所以 1048576 块大约等于 512MB。注意这个限制对日志文件的写入同样生效,如果应用没做异常处理,会直接异常退出。只用在自己有把握的短平快场景里。
6.2 日志文件权限的合理规划
日志很多是敏感信息,毕竟里面可能记录了用户 IP、操作路径、甚至部分业务数据。如果日志目录权限是 777,任何人都能读取或修改,安全风险非常大。
我一般建议按“目录 755 / 文件 640”的基准来设置。755 表示目录允许所有用户进入和列出文件,640 表示文件属主可读写、属组可读、其他人无权限。这样运维组内的同事都能查日志,外人无法读取。
logrotate 轮转后的文件权限默认会继承原文件的权限设置,但在 copytruncate 场景下,新创建的压缩包权限取决于 logrotate 创建文件时的 umask。为了保证权限一致,可以在 logrotate 配置里显式指定 create 640 root root 或 create 0640 xxx xxx,这样无论原文件权限怎么变,轮转出来的历史文件权限是确定安全的。
6.3 条件触发与告警结合
有时候没有出现磁盘告警,你就不知道日志管理出故障了。我在生产上给 logrotate 和日志清理脚本都加了“结果校验”:脚本执行完后,如果发现清理出空间但还有日志目录超过磁盘阈值,就发一条告警。
最简单的方式是,在 crontab 命令的最后追加一个判断:
bash复制15 3 * * * /opt/scripts/clean_old_logs.sh && /opt/scripts/check_log_disk.sh >/dev/null 2>&1
check_log_disk.sh 里可以用 df -h /var/log | awk 'NR==2{if($5+0>85) exit 1}' 判断使用率是否超过 85%,一旦超了就通过 mail 或 webhook 发通知。这个判断触发逻辑写得稍微繁复,但能让你在磁盘彻底爆之前收到警告,救命的概率很高。
日志管理这件事,你说它难,它不涉及高深算法;你说它简单,做得不仔细,总会在某个夜里用磁盘满的方式“教育”你。我自己在踩过各种坑之后的最大体会是:日志管理最怕的不是工具不好,而是配置好之后觉得“完事大吉”,然后彻底不再看它。再好的配置,也需要在业务变化后及时调整——日志量翻倍了、保留周期变了、新应用上线了,这些都会让旧方案快速失效。
所以我现在的日常习惯是:每个季度检查一次所有生产服务器的 /var/log 占用情况和 logrotate 执行记录,顺便看看有没有新上线的应用还没纳入自动管理。这套“季度巡检 + 每日自动执行”的组合,才是我真正想分享的东西。日志管理本质上不是一次配置,而是一个持续维护的轻量级系统。
