日志改名后,既有写者仍然可能继续增长旧对象。

日志轮转需要交接写入者并核算保留预算

日志轮转把持续的数据流分成可以保留、检索、压缩并最终删除的文件。有用策略既定义何时轮转,也定义需要保留哪段历史,还必须考虑应用如何持有输出文件。成功改名与成功把之后日志转入新文件,是运维契约中的不同步骤。只检查归档文件出现,无法证明写入交接已经完成。 坏掉的样子 写者通常长期保留描述符,而不是每写一条记录都重新解析日志路径。改名旧文件并在原位置创建新文件,不会移动已有描述符。因此活动路径可能看起来为空,归档却持续增长;只监控新名字,会误以为应用停止记录。排查时需要把路径状态与进程实际打开对象对应起来。 删除归档名字,也不会关闭仍然引用该 inode 的写者。文件可以在没有普通目录项的情况下继续打开、占用资源并接受写入。因此清理任务报告删除成功,不一定意味着已经回收预期空间,更不代表应用日志已转移。名字消失和最后一个使用者结束,是需要分别确认的条件。 复制后截断可以避免要求写者重开,但复制和截断是两个独立动作。在复制完成之后、截断之前写入的记录,可能从归档和活动文件中同时消失。选择低峰时段可以降低这种交错的概率,却不能把它变成原子无损交接。是否允许这个窗口,应由日志保留要求明确决定。 快速验证 把脚本保存为 rotation.py,在一次性 Linux 环境中运行 python3 rotation.py。实测使用 Linux 6.12.76、Python 3.11.2 和 ext4。样本只操作临时文件,并明确控制写入顺序;copytruncate 案例是底层操作的确定性交错,不是声称偶然碰到了一次竞争: from pathlib import Path from tempfile import TemporaryDirectory import os,shutil with TemporaryDirectory(prefix='xbcatm-rotation-') as directory: root=Path(directory);active=root/'app.log';archive=root/'app.log.1' with active.open('a+b',buffering=0) as writer: writer.write(b'before\n');identity=os.fstat(writer.fileno()).st_ino active.rename(archive);active.touch();writer.write(b'after-rename\n') assert active.read_bytes()==b'' and archive.read_bytes()==b'before\nafter-rename\n' assert archive.stat().st_ino==identity and active.stat().st_ino!=identity print('renaming and creating a replacement path leaves an existing writer appending to the archive inode') archive.unlink();assert os.fstat(writer.fileno()).st_nlink==0 writer.write(b'after-unlink\n');writer.seek(0) assert b'after-unlink\n' in writer.read() and not archive.exists() print('deleting the archive name does not close the writer; its unlinked file remains open and writable') with active.open('ab',buffering=0) as reopened:reopened.write(b'new-active\n') assert active.read_bytes()==b'new-active\n' print('closing and reopening the active path switches subsequent writes to the replacement inode') source=root/'copytruncate.log';copied=root/'copytruncate.log.1' with source.open('a+b',buffering=0) as writer: writer.write(b'before-copy\n');shutil.copyfile(source,copied) writer.write(b'between-copy-and-truncate\n') os.ftruncate(writer.fileno(),0);writer.write(b'after-truncate\n') assert copied.read_bytes()==b'before-copy\n' and source.read_bytes()==b'after-truncate\n' assert b'between-copy-and-truncate' not in copied.read_bytes()+source.read_bytes() print('deterministic copy-then-truncate interleaving loses the write placed in that gap') 第一个写者追加 before,控制流程再把日志改名并创建新的活动路径。同一打开写者继续追加 after-rename,两条记录都出现在归档中,替代文件仍然为空。inode 检查确认写者保留原对象,而活动路径已经标识另一个文件。这样直接验证名字改变不会自动重定向进程手里的引用。 ...

七月 11, 2026 · 1 分钟 · XB Field Notes