日志不切割,是在给服务器埋雷
很多站长对日志的态度是"能记就行",直到某天磁盘被一个几十 GB 的 access.log 撑满,服务全线报错,才回头去查这个文件是什么时候长成这样的。日志本身不复杂,难的是让它"可控地增长、可控地轮转、可控地删除"。Linux 世界里干这件事的标准工具是 logrotate,它几乎是所有发行版默认自带的,但默认配置只覆盖了系统日志,你自己的 Nginx、MySQL、应用日志基本没被管起来——这才是隐患的根源。
这篇文章不堆手册,而是从"一个真实站长会遇到什么"出发:日志怎么切才不丢数据、切完怎么让 Nginx 重新打开文件、压缩保留策略怎么定、以及为什么"切割成功但磁盘没变小"这种最气人的情况会发生。
先理解 logrotate 的运行机制
logrotate 不是常驻进程,而是被 cron 或 systemd timer 每天唤起一次的批处理工具。Debian/Ubuntu 上它由 /etc/cron.daily/logrotate 触发,CentOS/RHEL 上由 logrotate.timer 触发。每次运行时,它读取 /etc/logrotate.conf 以及 /etc/logrotate.d/ 目录下的所有片段,逐个判断每个日志是否满足轮转条件(按天、按周、按大小)。
所以排查 logrotate 问题的第一句话永远是:它到底跑了没有。查看运行记录:
cat /var/lib/logrotate/status
# 或
cat /var/lib/logrotate.status
systemctl status logrotate.timer
journalctl -u logrotate --since todaystatus 文件记录了每个日志"上次轮转的日期",这是判断"为什么没切"的直接证据。如果某个日志的 last rotated 一直没更新,说明它根本没被任何配置匹配到。
一份给 Nginx 用的 logrotate 配置
Nginx 默认会在 /var/log/nginx/ 下写 access.log 和 error.log。给它配一份靠谱的轮转规则,放在 /etc/logrotate.d/nginx:
/var/log/nginx/*.log {
daily
missingok
rotate 14
compress
delaycompress
notifempty
create 0640 www-data adm
sharedscripts
postrotate
[ -f /run/nginx.pid ] && kill -USR1 $(cat /run/nginx.pid)
endscript
}逐项解释这些指令为什么重要。daily 表示每天一次;rotate 14 表示保留 14 份历史;compress 开启 gzip 压缩,日志压缩比通常能到 10:1 以上,对磁盘是实打实的节省;delaycompress 是"延迟一轮再压缩",因为刚切出来的那份可能还有进程正在写;notifempty 表示空文件不轮转,避免产生一堆 0 字节的垃圾;create 0640 www-data adm 保证新文件权限正确,否则 Nginx 可能因权限问题写不进去。
最关键的两条是 sharedscripts 和 postrotate。Nginx 是"文件句柄"模型:进程打开 access.log 后一直持有这个 fd,你就算把文件改名,它依然往旧 inode 写。所以切割后必须通知 Nginx 重新打开日志文件——kill -USR1 就是干这个的。sharedscripts 保证这段脚本所有匹配的日志只执行一次,而不是每个文件跑一遍。
切割了但磁盘没变小?inode 与 fd 的锅
这是最经典、也最让人抓狂的现象:明明 logrotate 执行了,日志也改名压缩了,df 一看磁盘占用纹丝不动。原因基本有两个。
第一个是"进程仍持有旧文件句柄"。切割时把 access.log 重命名成 access.log.1,但 Nginx 没收到信号,继续往已经改名的旧 inode 里写。你在目录里看不到这个文件在增长(因为名字变了),但它占用的空间一点没释放。用 lsof 一看就现形:
lsof +L1 | grep -i deleted
lsof | grep nginx | grep -i log第二类原因是分布式文件系统的延迟回收,或者被 du 的统计口径误导——du 默认按硬链接只算一次,而 df 按块统计。这种情况下用 du --apparent-size 和 du -sh 对比一下,往往能看出差异。
解决第一个问题的通用手法是"复制截断法"(copytruncate)。它先复制一份日志,再把原文件清零,进程的 fd 依然指向同一个文件,因此不需要发信号:
/var/log/myapp/*.log {
daily
rotate 7
copytruncate
compress
delaycompress
missingok
notifempty
}copytruncate 的代价是有极短的时间窗,窗口内新写入的日志可能丢失。所以它适合"不支持重新打开文件"的应用,而 Nginx、Apache 这类支持 USR1 信号的,优先用 postrotate 方案,更严谨。
按大小切割:搞定"一天跑出 10G"的日志
daily 只保证一天一次,但如果你的站点被打或爬虫狂刷,一天就能跑出 10GB,等到午夜才切,磁盘早就报警了。这时应该用 size 触发,或者 daily 与 size 并用(logrotate 里同时写两者时,只要满足任一条件就触发——注意这一点和很多人直觉相反):
/var/log/nginx/access.log {
size 500M
rotate 20
compress
delaycompress
missingok
notifempty
create 0640 www-data adm
sharedscripts
postrotate
[ -f /run/nginx.pid ] && kill -USR1 $(cat /run/nginx.pid)
endscript
}但 size 有个已知限制:logrotate 是由每日定时任务唤起的,如果它一天只跑一次,size 也只在那一刻被检查。想做到"接近实时"的按大小切割,得额外加一个更高频的触发。可以单独起一个每小时运行的任务:
# /etc/cron.hourly/logrotate-size
#!/bin/sh
/usr/sbin/logrotate /etc/logrotate.d/nginx-highfreq
# 或者强制以 size 判断,忽略时间
/usr/sbin/logrotate -f -s /var/lib/logrotate/status /etc/logrotate.d/nginx-highfreq注意 -s 指定独立的 status 文件,避免和高频和每日任务互相干扰"上次轮转时间"的判断。
测试与调试:-d 和 -f 怎么用
logrotate 配置里有个拼写错误,可能几个月都不会报错——因为它只在真正轮到该文件时才解析那一行。所以每次改完配置,务必用 debug 模式预演:
logrotate -d /etc/logrotate.d/nginx
# -d 是 dry-run,只打印"打算做什么",不实际执行确认逻辑无误后,再强制跑一次验证真实效果:
logrotate -f -v /etc/logrotate.d/nginx
ls -lh /var/log/nginx/-v 会输出详细过程,包括每个文件为什么被切或没被切。如果看到 "log does not need rotating",说明轮转条件(时间或大小)没满足;如果是 "skipping ... because parent directory has insecure permissions",那就要去修目录权限——这是 logrotate 一个安全特性,父目录不能被组或其他用户写。
保留策略与合规:不是越久越好
日志保留两周是常见默认,但实际该保留多久,取决于三个现实约束:磁盘容量、故障复盘的追溯窗口、以及合规要求。给几条经验值。
访问日志(access.log)通常保留 14~30 天就够,爬虫和攻击的追溯用不到更久,而且压缩后占用很小。错误日志(error.log)建议保留 30~90 天,因为它记录了 5xx、超时、上游异常这些需要复盘的线索。审计类日志(如 SSH 登录、sudo 操作)在部分场景下有合规要求,可能需要保留 6 个月到 1 年,这类应该单独配置并考虑异地归档,而不是堆在本机磁盘上。数据库慢查询日志保留 30 天通常足够支撑性能调优。
特殊保留需求可以用 olddir 把历史日志挪到另一个盘,或用 maxage 按天数而非份数控制:
/var/log/myapp/*.log {
daily
rotate 30
maxage 90
olddir /data/log-archive/myapp
compress
delaycompress
missingok
notifempty
copytruncate
}olddir 目录必须和日志同分区,否则 logrotate 无法用 rename 挪动文件,会退化成复制,效率骤降还可能失败。
和 journald 的分工,别让日志写两遍
现代发行版上,systemd 的 journald 也在收集日志。很多人踩过一个坑:应用同时把日志写进 /var/log/(被 logrotate 管)和 journald(被 journald 自己的容量上限管),结果磁盘被两份副本占着。先看 journald 实际占了多少:
journalctl --disk-usage
# /etc/systemd/journald.conf 中限制
# SystemMaxUse=500M
# MaxRetentionSec=2week如果你只打算用文件日志加 logrotate,就把应用的 stdout 日志输入关掉或降低 journald 的上限;反之若依赖 journalctl 检索,就确保 SystemMaxUse 有个明确封顶,别让它无限增长。两条路选一条走通,比"两套都开着、谁都没管"稳得多。
日志管理听起来琐碎,却是服务器稳定性里"回报率最高"的一环:一份到位的 logrotate 配置,可能就帮你避免了一次磁盘写满导致的整站宕机。花十分钟把你的应用日志纳入 logrotate,值得。