Linux logrotate copytruncate 与信号重开日志有什么区别
来源:17golang原创
时间:2026-11-14 06:45:47 438浏览 收藏
我在生产环境第一次遇到“日志已经轮转,服务却还在往旧文件写”的问题时,真正卡住的不是 logrotate 语法,而是把“文件名”误当成了“进程正在写的文件”。copytruncate 和“重命名后让服务重新打开日志”解决的是两条不同路径:前者保留原 inode 并原地清空,后者让进程关闭旧文件描述符,再按新路径打开新 inode。
- 应用支持可靠的日志重开时,优先使用普通 rename/create,再在
postrotate中通知服务;这样不会让进程长期持有旧 inode。 - 应用无法配合重开时,
copytruncate是实用的兼容方案,但复制和截断之间存在极短窗口,繁忙日志可能丢记录。 HUP不是 logrotate 自动施加的魔法,它只有在应用明确实现“收到信号后重开日志”时才有意义。
先看文件句柄为什么决定轮转结果
进程写日志时通常已经打开了一个文件描述符。这个描述符指向文件对象和 inode,而不是每次写入都重新按字符串查找 /var/log/app.log。因此,轮转时只改变路径名,并不会自动把已有描述符切到新文件。
# 只读观察路径、inode 和打开文件,先确认服务到底写向哪个文件
LOG=/var/log/app.log
PID=1234
# 路径当前对应的 inode;轮转前后对比这个数字
stat -c 'path=%n inode=%i size=%s' "$LOG"
# 查看进程打开的文件描述符,实际环境中请按服务 PID 替换 1234
ls -l /proc/"$PID"/fd | grep '/var/log/'
如果 logrotate 先把旧文件改名,又创建了一个同名新文件,路径名会指向新 inode,但服务原来的 fd 仍指向已经改名的旧 inode。此时磁盘上可能同时出现“新路径文件”和“deleted 但仍被进程占用的旧文件”。下面的结构图只解释这种关系,是静态说明图,不是运行截图。

copytruncate 的便利与丢日志窗口
copytruncate 的思路是先把当前日志复制为轮转文件,再对原文件执行原地截断。因为原路径和原 inode 仍然存在,进程不需要关闭 fd,也不需要认识任何信号;这正是它在无法控制第三方程序时的价值。
/var/log/app.log {
# 保留 7 份轮转文件,按周轮转
weekly
rotate 7
compress
delaycompress
# 复制当前内容后,原地把正在使用的文件截断为 0
copytruncate
missingok
notifempty
}
它的代价也很具体:复制和截断不是一个不可分割的瞬间。服务可能在复制已经读过的范围后又写入新记录,随后截断动作把这段尚未进入副本的内容清掉;日志越大、写入越密集,越应该认真评估这个窗口。复制本身还会带来额外 I/O,压缩则可能继续放大轮转时的磁盘压力。
所以我通常只把 copytruncate 当作兼容性方案,而不是默认最佳方案。它适合低到中等日志量、应用不能重开、少量丢失可接受的场景;对审计日志、计费日志或不能缺行的流水,不能只因为配置简单就选择它。
用 postrotate 和 HUP 让服务重新打开日志
当应用提供“重新打开日志”的能力时,更干净的路径是让 logrotate 先完成 rename/create,再在 postrotate 中通知服务。服务收到约定信号后,应关闭旧 fd、重新按路径打开日志,并继续写入新 inode。这里的关键是应用行为,而不是信号名称本身。
/var/log/app.log {
# 先重命名旧文件并创建新文件,保留服务需要的属主和权限
daily
rotate 14
create 0640 appuser appgroup
compress
delaycompress
missingok
notifempty
# 只有 app 明确支持 HUP 重开日志时才这样通知
postrotate
/bin/systemctl kill -s HUP --kill-who=main app.service
endscript
}
如果服务由 systemd 管理,也可以把重开动作写入单元的 ExecReload,再让 logrotate 调用对应的 reload 命令。不要把“发送了 HUP”当成完成证明:应用可能没有安装处理器,也可能异步处理信号。生产上应让服务自己记录“日志已重新打开”的状态,至少在轮转后用文件描述符和新旧 inode 做一次有计划的检查。
[Service]
# 下面的信号只适用于应用约定 HUP 表示重新加载或重开日志
ExecReload=/bin/kill -HUP $MAINPID
按生产约束选择并保留回退路径
这两种方式不是“新旧语法”的比较,而是“进程是否能协作”的选择。下面这张图把轮转动作、信号通知和回退条件放在同一条时间线上,方便在上线前和服务负责人对齐。

| 约束 | 优先方案 | 主要风险 |
|---|---|---|
| 应用支持可靠重开,日志不能长期写旧文件 | rename/create + postrotate 通知 | 信号未处理、权限错误或重开失败 |
| 第三方程序无法控制,少量丢失可接受 | copytruncate | 复制与截断之间的短暂丢失窗口、额外 I/O |
| 审计、计费等不能缺记录 | 优先使用应用原生轮转或集中日志方案 | 不要把 copytruncate 当成无损保证 |
| 多文件共用一个服务 | 结合 sharedscripts 与服务重开协议 | 脚本重复执行或只重开了部分文件 |
我的上线清单通常只有四项:第一,确认应用文档对重开信号的约定;第二,确认 create 的属主和权限不会让服务下一次打开失败;第三,确认轮转脚本的执行用户有权改名、创建和通知服务;第四,保留一次可回退的配置变更,不要在高峰期第一次验证信号行为。
相关问题
copytruncate 会不会让进程自动写到轮转后的新文件?
不会。它保留原 inode,只是把内容截断,再让同一个 fd 继续写。这里的“新文件”只是轮转副本,服务并没有重新打开路径。
发送 HUP 后为什么旧日志仍在增长?
常见原因是应用没有把 HUP 定义为重开日志、信号发给了错误的进程,或服务有多个 worker 而只重开了主进程。应回到应用的信号协议和实际 fd 检查,不要只看 logrotate 命令返回码。
systemd 的 reload 和重启是一回事吗?
不是。reload 通常要求服务继续运行并重新加载配置或重开资源;重启会引入进程退出和重新启动的影响。是否支持 reload、哪个信号有效,必须以具体服务的单元和应用文档为准。
-
260 收藏
-
339 收藏
-
388 收藏
-
253 收藏
-
182 收藏
-
263 收藏
-
226 收藏
-
331 收藏
-
418 收藏
-
288 收藏
-
418 收藏
-
493 收藏
-
443 收藏
-
300 收藏
-
299 收藏
-
293 收藏
-
185 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习