登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  linux

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 但仍被进程占用的旧文件”。下面的结构图只解释这种关系,是静态说明图,不是运行截图。

Linux logrotate 中路径名、inode、进程文件描述符以及 rename、copytruncate 两条路径的关系说明图
图1:路径名、inode 和进程文件描述符的关系说明图。

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

按生产约束选择并保留回退路径

这两种方式不是“新旧语法”的比较,而是“进程是否能协作”的选择。下面这张图把轮转动作、信号通知和回退条件放在同一条时间线上,方便在上线前和服务负责人对齐。

Linux logrotate copytruncate 与 rename、postrotate、HUP 重开日志的流程和选择条件说明图
图2:copytruncate 与信号重开日志的流程对比说明图。
约束优先方案主要风险
应用支持可靠重开,日志不能长期写旧文件rename/create + postrotate 通知信号未处理、权限错误或重开失败
第三方程序无法控制,少量丢失可接受copytruncate复制与截断之间的短暂丢失窗口、额外 I/O
审计、计费等不能缺记录优先使用应用原生轮转或集中日志方案不要把 copytruncate 当成无损保证
多文件共用一个服务结合 sharedscripts 与服务重开协议脚本重复执行或只重开了部分文件

我的上线清单通常只有四项:第一,确认应用文档对重开信号的约定;第二,确认 create 的属主和权限不会让服务下一次打开失败;第三,确认轮转脚本的执行用户有权改名、创建和通知服务;第四,保留一次可回退的配置变更,不要在高峰期第一次验证信号行为。

相关问题

copytruncate 会不会让进程自动写到轮转后的新文件?

不会。它保留原 inode,只是把内容截断,再让同一个 fd 继续写。这里的“新文件”只是轮转副本,服务并没有重新打开路径。

发送 HUP 后为什么旧日志仍在增长?

常见原因是应用没有把 HUP 定义为重开日志、信号发给了错误的进程,或服务有多个 worker 而只重开了主进程。应回到应用的信号协议和实际 fd 检查,不要只看 logrotate 命令返回码。

systemd 的 reload 和重启是一回事吗?

不是。reload 通常要求服务继续运行并重新加载配置或重开资源;重启会引入进程退出和重新启动的影响。是否支持 reload、哪个信号有效,必须以具体服务的单元和应用文档为准。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>