科技 · 实用指南
Python 日志轮转丢行?验证 inode 切换与记录并集
只保留一个轮转所有者,对比打开描述符和路径身份,并验证轮转文件与活动文件中的编号记录恰好出现一次。

logrotate 成功退出且出现新的 app.log,并不能证明 Python 进程已停止向旧 inode 写入。WatchedFileHandler 会在类 Unix 系统的每次 emit 前检查设备号和 inode,因此必须从描述符、路径和记录三个层面验证。
只使用一个轮转所有者:外部 rename-create 配合 WatchedFileHandler,或只使用一个内部轮转处理器,不能叠加。围绕边界给记录编号,旧文件与新文件必须组成无缺口、无重复的精确序列。
1. 只选择一个轮转所有者
若 logrotate 会重命名 app.log 并创建新文件,就只给 logger 配置一个 WatchedFileHandler;不要再叠加 RotatingFileHandler 或 TimedRotatingFileHandler。
Windows 或设备号、inode 语义不同的文件系统不能直接套用;必须在真实挂载点和服务账号下实测。
2. 记录描述符和路径身份
轮转前先 flush,分别记录 os.fstat(handler.stream.fileno()) 与 os.stat(log_path) 的设备号和 inode;两者应指向同一对象,同时记录目录权限与新文件所有者。
这能区分旧描述符和不可访问的新路径。CPython 公开问题表明,目标目录消失时重新打开可能失败。
3. 重命名并创建后写入一条
把 app.log 重命名为 app.log.1,在下一次写入前按审核后的所有者和权限创建新的 app.log,并保留轮转文件直到审计结束。
写入一条编号哨兵并 flush;只有新路径 inode 等于处理器描述符 inode,且轮转文件保持旧 inode,才通过这一层。
4. 验证每条编号记录恰好一次
轮转前写 seq-001 至 seq-003,轮转后写 seq-004 至 seq-006,完整读取两个文件。并集必须等于预期集合,每个值只出现一次,且不得出现未知值。
本地 CPython 3.12.14 夹具中,前三条位于旧 inode,后三条位于新 inode;缺失和重复集合为空,轮转后描述符与新路径 inode 一致。
5. 拒绝掩盖边界的捷径
不要把 copytruncate 当成无损等价方案;logrotate 明确警告复制与截断之间可能丢数据。若为兼容性使用,必须单独接受并测试该风险。
不要只比总行数;一条缺失加一条重复仍可得到相同行数,也不要仅凭新路径存在判断成功。
6. 记录结论边界
该测试只证明指定 Python 版本、文件系统、服务账号和配置下观察到的边界,不证明未来轮转、远程文件系统、容器卷或 Windows。
更改 Python、处理器、轮转策略、挂载、权限或账号后都要重测,并监控重新打开异常和描述符路径不匹配。
