systemd journal 的保留范围需要明确约定
systemd journal 把消息与结构化元数据一起保存在日志文件中,因此可以查询服务输出,不必先为每种应用文本设计一套解析器。但让这些消息保留足够长时间,是另一项需要明确的要求。存储位置、空间策略、轮转方式与转发路径,决定了事故发生一段时间后还有哪些证据可查。查询方便,不等于历史永远完整,也不等于数据已经离开这台机器安全保存。 坏掉的样子 默认命名空间通常使用 Storage=auto:存在 /var/log/journal 时选择持久存储,否则使用运行时存储。Storage=volatile 把文件放在 /run/log/journal 下,并不是简单借用内核的文本环形缓冲区。这些运行时文件通常会在整台机器重新启动后消失,但重启 journald 服务是不同的事件,并不必然清空文件。排查“重启后日志去哪了”之前,必须明确重启的是哪个对象。 Storage=persistent 优先使用 /var/log/journal,早期启动阶段或持久位置不可写时仍可能使用运行时存储。满足文档规定条件时,flush 操作可以转移运行时数据。因此应检查真实文件和最终生效配置,不能只因为某份配置片段里出现了 persistent,就推定所有消息已经写在预期磁盘上。路径、挂载状态与执行时点都需要对应到实际记录。 journald 本身有默认空间限制和轮转行为,不会仅仅因为管理员没有写一个轮转任务就无限增长。不过,SystemMaxUse 也不是保证文件系统永不写满的精确硬配额。运行时存储有另一组设置,可用空间限制同时起作用,其他程序也会占用磁盘,而清理时只能删除已经归档的 journal 文件。把这些条件压缩成“设置一个上限就绝对安全”,会掩盖真正的磁盘压力来源。 快速验证 在由 systemd 管理的一次性 Linux 系统中,以管理员身份运行下面的 Python 脚本。记录环境为 Debian 12、systemd 252.39。脚本创建唯一日志命名空间,显式配置持久存储,通过临时服务写入两条无害消息,最后只删除该命名空间的数据。实验中的 vacuum 会有意删除自己刚写入的旧消息: from pathlib import Path import json,shutil,subprocess,uuid name='xbcatm-journal-'+uuid.uuid4().hex[:8] config=Path('/etc/systemd')/('journald@'+name+'.conf') directory=Path('/var/log/journal')/(Path('/etc/machine-id').read_text().strip()+'.'+name) assert not config.exists() and not directory.exists() def run(*args): result=subprocess.run(args,text=True,capture_output=True,timeout=15) assert result.returncode==0,(args,result.stderr) return result.stdout def journal(*args):return run('journalctl','--namespace='+name,*args) def emit(message): run('systemd-run','--quiet','--wait','--collect','--unit='+name+'-'+message, '--property=LogNamespace='+name,'--property=StandardOutput=journal', '--property=SyslogIdentifier='+name,'/usr/bin/printf',message+'\n') journal('--sync') def messages(): rows=journal('--no-pager','-o','json','SYSLOG_IDENTIFIER='+name) return [json.loads(line)['MESSAGE'] for line in rows.splitlines()] config.write_text('[Journal]\nStorage=persistent\nSystemMaxUse=16M\nRuntimeMaxUse=8M\n') try: emit('old');journal('--flush');journal('--sync') assert messages()==['old'] and list(directory.glob('*.journal')) run('systemctl','restart','systemd-journald@'+name+'.service') assert messages()==['old'] print('persistent namespace: message survives journald service restart') journal('--vacuum-size=1') assert messages()==['old'] print('vacuum cannot remove the active journal file to satisfy a one-byte target') journal('--rotate');emit('new') assert messages()==['old','new'] assert list(directory.glob('*@*.journal')) journal('--vacuum-size=1') assert messages()==['new'] and not list(directory.glob('*@*.journal')) print('after rotation: archived old message removed; active new message remains') print('persistent files observed under /var/log/journal; no reboot durability claim') finally: run('systemctl','stop','systemd-journald@'+name+'.service', 'systemd-journald@'+name+'.socket','systemd-journald-varlink@'+name+'.socket') config.unlink(missing_ok=True) for path in (directory,Path('/run/log/journal')/directory.name,Path('/run/systemd')/('journal.'+name)): if path.exists():shutil.rmtree(path) 第一条消息位于该命名空间的 /var/log/journal 目录中,重启对应 journald 服务后仍然能够查到。这验证了选中的存储位置,以及消息跨越日志守护进程重启仍然存在;它没有模拟突然断电,也没有证明存储设备的持久性,更没有执行整台机器重启。说明验证范围,可以避免把一个真实但有限的结果扩大成并未做过的耐久性测试。 ...