Rails日志删除后仍占用磁盘空间,如何优化日志轮转方案?
这个问题我之前维护Rails+Resque集群时也碰到过,核心原因就是Linux下进程持有已删除文件的句柄时,磁盘空间不会被释放——你的Resque Worker还在往已经被logrotate删掉的旧日志文件句柄里写数据,所以df看空间还是被占着,只有进程退出才会释放句柄。
下面是几个从优到次的解决方案,亲测有效:
1. 让logrotate通知Worker刷新日志句柄(最优方案)
这是最推荐的做法,不用重启进程,就能让Worker切换到新的日志文件,同时释放旧句柄。有两种实现方式:
方式A:使用copytruncate(简单但有极小风险)
在你的logrotate配置文件(比如/etc/logrotate.d/rails-app)里加上copytruncate指令:
/home/log/*.log { daily rotate 7 compress copytruncate # 关键:复制日志内容到新文件后截断原文件 missingok notifempty delaycompress }
原理是:logrotate会先把当前日志文件的内容复制到轮转后的文件(比如production_database.log.1),然后清空原日志文件。Worker还是往原文件句柄写,不会中断,也不会丢失日志(除非在复制和截断的极短时间内有日志写入,概率极低)。
方式B:发送USR1信号让Worker重新打开日志(更可靠)
大部分Ruby日志库(包括Rails默认的Logger)都支持接收USR1信号,收到后会自动关闭旧日志文件,重新打开新的。在logrotate配置里用postrotate脚本发送信号:
/home/log/*.log { daily rotate 7 compress missingok notifempty postrotate # 精准匹配Resque Worker进程,发送USR1信号 pgrep -u fadmin -f "resque" | xargs kill -USR1 endscript }
这里要注意:
- 替换
fadmin为你的Worker运行用户 -f "resque"要匹配你的Worker进程名,如果进程名里有更独特的标识(比如resque: Worker),可以写得更精准,避免误杀其他Ruby进程- 如果用systemd管理Worker,也可以用
systemctl kill -s USR1 resque-workers.service来发送信号
执行完logrotate后,再跑lsof | grep deleted,应该就看不到那些已删除的日志文件了,磁盘空间也会立刻释放。
2. 让Worker自身处理日志轮转(备选)
如果系统级的logrotate不好调整,可以让Ruby进程自己处理日志轮转。比如用Rails的Logger配置:
# config/environments/production.rb config.logger = Logger.new( Rails.root.join('log', 'production_database.log'), daily: true, # 每日轮转 max_size: 100.megabytes, keep: 7 )
但这种方式有个问题:多个Worker进程写同一个日志文件时,可能会出现日志内容混乱或者轮转冲突,不如系统级logrotate稳定,所以只适合单Worker场景。
3. 重启Worker(不推荐,万不得已才用)
如果上面的方法都不行,只能在logrotate之后重启Resque Worker,但这会中断正在执行的任务,影响业务:
/home/log/*.log { # 其他配置... postrotate systemctl restart resque-workers.service endscript }
除非你的任务是幂等的,或者可以接受短暂中断,否则不建议用这个方法。
验证方法
- 手动执行logrotate测试:
logrotate -v /etc/logrotate.d/rails-app - 检查已删除文件:
lsof | grep deleted,确认没有旧日志文件的条目 - 查看磁盘空间:
df -h,确认空间已经释放
内容的提问来源于stack exchange,提问作者jayesh

