相反方向的箭头区分消费者通过 Requires 请求提供方,与提供方的 READY=1 启动边界放行有序消费者。Requires 和 After 的方向与用途不同。

systemd 依赖要区分激活、顺序与就绪

依赖图需要表达的不只是“先启动数据库”。一种关系决定是否把另一个单元拉进本次任务,一种关系决定启动任务的先后,服务类型则决定怎样才算启动完成。应用就绪还是程序必须真正兑现的承诺。把这些层次混在一起,得到的开机流程可能看起来很有顺序,依赖方实际却仍然在所需条件成立之前就开始运行。 激活、顺序和就绪是独立契约。Requires 加 After 会在提供方激活失败时阻止消费者;通知机制可将有序启动保持到 READY=1。图解基于原文,不是本轮 systemd 实测,也不是持续健康保证。 坏掉的样子 After=provider.service 在双方都有启动任务时,把当前单元排在提供方之后,但它不会主动请求启动提供方。如果本次只激活消费者,单靠这条排序关系,提供方完全可能继续保持非活动。只有消费者确实应当带着提供方一起启动时,才需要另外声明合适的激活依赖;“排在它后面”与“把它启动起来”不能互相替代。 Wants 会请求启动另一个单元,但容忍对方启动失败。Requires 表达更强的关系;与 After 配合时,如果提供方激活失败,依赖它的单元就不会启动。但这两个关键字各自都不会自动建立启动顺序。应根据服务的真实需要选择组合,而不是把所有可选组件都变成硬性前提,也不能把弱依赖误当作一定成功的就绪屏障。 有序启动的强度取决于提供方怎样报告完成。Type=exec 表示程序已经成功执行,不表示数据库完成初始化;Type=notify 可以把启动任务保持到程序发送 READY=1;oneshot 则可以等待有限的设置工作结束。应用必须在承诺的就绪条件真的满足后才发送通知,管理器无法替它判断某个业务表或外部依赖是否已经可用。 快速验证 在一次性的 Linux systemd 环境中,以管理员身份运行下面的完整 Python 脚本。它安装两个临时运行时单元,依次验证六种配置。恢复文本记载的环境为 Debian 12、systemd 252.39,但原始运行日志不可用。本轮没有执行此样例;以下结果说明其预期断言,不能视为本轮实测。确认两个实验单元名尚未使用;临时文件分别记录提供方是否进入程序、是否就绪,以及消费者实际观察到了什么: from pathlib import Path from tempfile import TemporaryDirectory import subprocess,time provider='xbcatm-provider-lab.service';consumer='xbcatm-consumer-lab.service' paths={name:Path('/run/systemd/system')/name for name in (provider,consumer)} assert all(not p.exists() for p in paths.values()) def ctl(*args,success=True): result=subprocess.run(['systemctl',*args],text=True,capture_output=True,timeout=10) assert (result.returncode==0)==success,(args,result.stderr) return result.stdout.strip() def value(name,key):return ctl('show',name,'-p',key,'--value') def wait_for(check): deadline=time.monotonic()+5 while not check(): assert time.monotonic()<deadline,'state transition timed out' time.sleep(0.02) with TemporaryDirectory(prefix='xbcatm-dependency-',dir='/run') as directory: root=Path(directory) (root/'provider.py').write_text(r"""from pathlib import Path import os,socket,sys,time root=Path(__file__).parent;mode=(root/'mode').read_text() if mode=='fail': sys.exit(7) (root/'entered').touch() if mode in ('exec','notify'): while not (root/'gate').exists(): time.sleep(0.01) (root/'ready').touch() if mode=='notify': address=os.environ['NOTIFY_SOCKET'] if address.startswith('@'): address='\0'+address[1:] with socket.socket(socket.AF_UNIX,socket.SOCK_DGRAM) as notification: notification.connect(address);notification.sendall(b'READY=1') if mode in ('exec','notify'): time.sleep(60) """) (root/'consumer.py').write_text("from pathlib import Path\nr=Path(__file__).parent\n(r/'observed').write_text(str((r/'ready').exists()))\n") def install(mode,requirement): (root/'mode').write_text(mode) kind='oneshot' if mode in ('oneshot','fail') else mode paths[provider].write_text(f'[Service]\nType={kind}\nExecStart=/usr/bin/python3 {root}/provider.py\nTimeoutStartSec=5s\n'+('RemainAfterExit=yes\n' if kind=='oneshot' else '')) paths[consumer].write_text(f'[Unit]\nAfter={provider}\n'+(f'{requirement}={provider}\n' if requirement else '')+f'[Service]\nType=oneshot\nExecStart=/usr/bin/python3 {root}/consumer.py\n') subprocess.run(['systemd-analyze','verify',*[str(p) for p in paths.values()]],check=True) ctl('daemon-reload') def reset(): ctl('stop',consumer,provider) for name in paths: if value(name,'ActiveState')=='failed':ctl('reset-failed',name) for name in ('entered','gate','ready','observed'):(root/name).unlink(missing_ok=True) pending=None install('oneshot','') try: ctl('start',consumer) assert (root/'observed').read_text()=='False' and value(provider,'ActiveState')=='inactive' print('After alone: provider not pulled in; consumer ran before readiness') reset();install('fail','Wants');ctl('start',consumer) assert value(provider,'ActiveState')=='failed' and (root/'observed').read_text()=='False' print('Wants plus After: consumer runs despite provider failure') reset();install('fail','Requires');ctl('start',consumer,success=False) assert value(provider,'ActiveState')=='failed' and not (root/'observed').exists() print('Requires plus After: failed provider prevents consumer execution') reset();install('oneshot','Requires');ctl('start',consumer) assert (root/'observed').read_text()=='True' print('oneshot provider: consumer sees completed setup') reset();install('exec','Requires');ctl('start',consumer) assert (root/'observed').read_text()=='False' and value(provider,'ActiveState')=='active' (root/'gate').touch();wait_for(lambda:(root/'ready').exists()) print('Type=exec provider: process started before application readiness') reset();install('notify','Requires') pending=subprocess.Popen(['systemctl','start',consumer],text=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE) wait_for(lambda:(root/'entered').exists()) assert pending.poll() is None and not (root/'observed').exists() assert value(provider,'ActiveState')=='activating' (root/'gate').touch();out,err=pending.communicate(timeout=10) assert pending.returncode==0,err assert (root/'observed').read_text()=='True' print('Type=notify provider: dependent waits for actual READY=1') finally: reset() if pending is not None and pending.poll() is None:pending.communicate(timeout=5) for path in paths.values():path.unlink() ctl('daemon-reload') 只有 After 时,消费者运行并记录尚未就绪,而提供方保持非活动。这不是提供方启动得慢,而是没有任何激活关系把它拉进来。使用 Wants 加 After 时,提供方故意以状态七退出,消费者仍然执行并记录否。样本程序接受这个观察结果;真实消费者如果确实必须依赖它,随后可能在自己的业务操作中失败,弱依赖不会替业务消除这个事实。 ...

二月 18, 2028 · 2 分钟 · XB Field Notes
冷启动从连接开始,经过兼容服务启动,直到首个应用响应。应将这段延迟与客户端截止时间及所节省的常驻资源比较。仅示意有条件的机制;原文仍为搁置/待审。

socket 激活让服务按需启动

一个 socket 单元可以在兼容应用停止时继续提供监听端点,从而减少少用服务的常驻内存与周期性工作。但套接字、内核队列和管理器仍然占用资源,已经安装的软件也仍然需要安全更新。监听可用还取决于绑定成功、权限正确以及 socket 单元保持活动。端口正在监听,只表示客户端有机会请求工作,并不能证明应用已经初始化或能够返回正确结果;这些条件必须分别观察。 交接方式由配置决定。Accept=no 把监听描述符交给守护进程,由它自己接受连接;Accept=yes 则先接受连接,再启动适合处理该连接的服务实例。客户端可能在应用初始化之前完成连接,随后等待响应,甚至等到超时。应测量从连接到应用回复的完整延迟,不能因为交接对客户端接口透明,就推断冷启动和常驻服务在时延、失败诊断或实际资源成本上没有区别。 socket 单元活动时,请求可以启动兼容服务。应用明确定义的空闲退出可使进程停止,后续需求再触发启动;没有空闲退出时,服务仍继续运行。应在真实负载下测量连接至首个响应的冷启动延迟、客户端截止时间和常驻资源。监控可能同时唤醒多个服务,因此需要验证聚合启动负载。仅示意所选机制;原文仍为搁置/待审,不为其错误泛化背书。 仅示意选定的原文机制;本轮没有执行示例。 让监听器先等着 socket 单元启动时,systemd 打开 ListenStream 指定的端点。Accept=no 通常由 foo.socket 激活 foo.service 并传递监听器,Service= 可以指定其他服务;Accept=yes 通常使用 foo@.service 模板,为每条接受的流连接生成实例。数据报套接字不采用这种逐连接模型。程序必须匹配选定的描述符契约,不能只因为两个单元名称相近,就认为普通绑定逻辑可以原样使用。 这种分离可以减少启动顺序上的耦合,但不会消除依赖。客户端使用套接字之前,socket 单元必须已经活动并监听,所需的服务、挂载、设备与先后顺序依赖仍然适用。兼容服务启动时,连接可以在有限的内核积压队列中等待,但客户端仍可能在应用就绪前失败或超时。当然积压长度也要调:启动慢的服务遇上开机早期的连接洪峰,溢出起来和普通监听队列一样。 服务按需唤醒 按需激活可以在 socket 保持活动时推迟应用的首次启动,但不会让已运行的守护进程自动休眠,也不会自动停止进入空闲的程序。管理服务、罕用协议处理器是否适合这种方式,要看激活接口、请求截止时间与资源画像。其他依赖或人工启动仍然可能提前激活服务,因此不能把首次连接当成所有环境中唯一的启动入口,更不能从文章标题推导永久的运行限制。 要形成反复节省资源的循环,应用必须明确实现安全的空闲退出,先完成或转交进行中的工作,并让管理器持有的监听器继续可用。socket 单元仍活动时,后续需求可以请求再次启动;没有空闲退出逻辑,守护进程可能在第一次激活后一直常驻。这两种情况都不是零成本状态,也不保证套接字永远不会关闭。应验证待处理请求和退出边界,而不是仅设置一个空闲秒数就认定节省已经成立。 用速度换内存 冷启动预算应从连接开始,一直计算到首个有效应用响应,并在有代表性的负载下比较常驻内存。假设启动三百毫秒能够节省八百 MB,在一种工作负载中可能合适,在另一种场景却可能超过请求期限。二十秒预热对异步任务也许可接受,对交互请求则可能不可用。这些数字只是说明取舍,并不是实测结果或通用决策阈值。 有些工作无法承受启动间隙。健康检查本身可能唤醒服务,也可能在初始化时把端点判为不可用;客户端截止时间和负载均衡策略必须匹配实际启动行为。若稳定响应时间比空闲内存更重要,保持守护进程运行可能更简单。开机预热一次并不会自动实现之后的空闲退出,也不能替代对长时间停用后恢复状态的检查。 坏掉的样子 一轮监控可能同时唤醒很多服务,引发总体 CPU 或内存尖峰。错峰检查可以减少同时到来的需求,单服务启动速率限制则约束该单元反复启动的次数,并不能限制整机并发数量或总内存。过短的空闲超时还可能增加启停抖动。应测试聚合启动负载,并使用合适资源控制,而不是把几个独立单元上的限制误认为全机容量保证。 ...

二月 1, 2028 · 1 分钟 · XB Field Notes
/work 目录分支到标记删除的旧条目、保留的新文件,以及由 x 规则递归保留的子树。

systemd-tmpfiles 执行创建与年龄策略

systemd-tmpfiles 用配置文件描述文件系统对象与维护规则。一条规则可以按指定模式创建目录,年龄规则则可以让旧条目符合清理条件,但只有命令实际执行对应操作,效果才会发生。这是一种有用的生命周期策略,不是连续监视器,也不是硬性存储配额。目录已经声明、清理命令曾经运行和磁盘空间始终有界,是三种不能互相替代的事实。 树形图描述原文特定的 d 与 x 年龄清理样例。x 排除不能覆盖显式 r 或 R 删除。执行清理、删除路径名与释放存储是不同事件;本轮绘图没有运行清理。 坏掉的样子 年龄不只是“创建文件多久了”,也不只是“多久没有修改内容”。文档中的默认规则考虑多个时间戳,包括访问与修改时间、文件状态变化时间,以及可获得的创建时间。某个参与判断的时间戳较新,就可能保留一个看起来 mtime 很老的条目。因此,为了测试而改写时间戳,也可能以意外方式改变结果;不能只观察其中一个字段就断定工具应该删除文件。 --create 和 --clean 执行不同操作。创建配置中的目录,不证明里面的旧内容已经清除。下面使用的 d 规则保留目录本身,同时让符合年龄条件的内容参与清理;其他类型有不同删除语义。不能因为都叫“清理”,就随意换成递归移除规则,否则可能把按条件删除变成完全不同的破坏性操作。 年龄清理中的 x 排除规则可以保留匹配目录及其内容,X 则只排除目录本身,没有相同的递归保护。它们也不会覆盖显式 r 或 R 删除规则。判断一次维护会删什么,必须同时阅读最终生效配置与执行操作,而不是只找到某条看起来写着“忽略”的记录,就认定其他规则都不可能影响该路径。 快速验证 在装有 systemd-tmpfiles 的一次性 Linux 环境中,以管理员身份运行下面的 Python 脚本。恢复文本记载的环境为 Debian 12、systemd 252.39,但原始运行日志不可用。本轮没有执行此样例;以下结果说明其预期断言,不能视为本轮实测。脚本传入一份明确配置和临时 --root,所有创建对象与删除目标都属于本次实验,不会使用当前系统平时执行的 tmpfiles 配置: ...

十二月 10, 2027 · 2 分钟 · XB Field Notes
三个编号命令形成有限的向前执行序列,大幅重启箭头从完成处返回第一条命令,说明新的激活可以重复工作。

systemd oneshot 完成的是一次激活

Type=oneshot 服务表达一段有限工作,启动命令成功结束后,本次启动才算完成。因此,它适合初始化步骤,以及依赖方需要等待任务结束的定时工作。oneshot 描述的是一次激活的进程模型,不会建立永久记录来阻止任务在重启、再次激活或机器重新开机后重跑。名字中的“一次”,不能直接推导为业务意义上的“永远只做一次”。 oneshot 完成的是一次激活。后续命令失败不会撤销先前副作用,保留的 active/exited 状态也可以没有存活主进程。三个命令和 PID 0 来自原文样例;本轮绘图没有执行服务。 坏掉的样子 成功的 oneshot 通常在命令完成后变为非活动状态,这是任务已经结束的正常表现,不是守护进程崩溃的证据。设置 RemainAfterExit=yes 后,管理器会在命令退出后仍把单元记为活动。因此,active (exited) 可以完全没有主进程;如果监控假定任何活动服务都必须有一个存活 PID,就会错误判断这种单元类型。 oneshot 允许多条 ExecStart,并按顺序执行。采用普通成功规则时,某条命令失败会阻止后续启动命令运行,并让本次激活失败。但前面的命令可能已经修改文件或数据库,systemd 不会回滚这些副作用。重新尝试整个激活,就可能重复之前已经成功的部分,不能把管理器的失败状态误当作业务事务已经全部撤销。 RemainAfterExit 保存的是管理器状态,不是持久的恰好一次执行保证。再次启动已经活动的单元不会重跑,重启却会。定时器指向一个仍然活动的 oneshot 时,也不会把每个到达的时间点都变成另一次执行。只有它确实代表一个持续成立的状态时,才适合保留活动标记;需要反复触发的定时工作,应在各次执行之间回到非活动状态。 ...

十一月 28, 2027 · 1 分钟 · XB Field Notes
只要 socket 单元仍活动,管理器便在本例工作进程回复并主动退出后保留监听器。Accept=no 传递监听器,并不强制每连接创建一个进程。

Socket 激活把监听器交给兼容的服务

Socket 激活让 systemd 在应用进程启动之前先打开监听端点。端点上的活动随后可以触发兼容服务,并把管理器已经持有的套接字描述符传给它。这里真正分开的,是监听器所有权与应用生命周期。端点已经开放,还不证明应用完成初始化、接受了请求或生成了正确响应;传输层能连接与业务层能工作,需要分别验证。 socket 单元先活动,对应服务的 MainPID 为 0。首个连接触发工作进程,它校验 LISTEN_PID、LISTEN_FDS 1 及监听中的 FD 3,处理有完整边界的请求并回复。工作进程主动退出,管理器保留监听器;后续连接启动不同的进程,仍使用 FD 3。重复绑定会得到 EADDRINUSE。停止 socket 后移除路径,是因为本例明确配置了 RemoveOnStop=yes。 仅示意选定的原文机制;本轮没有执行示例。 坏掉的样子 应用必须理解激活协议。如果守护进程无条件再次绑定同一个端点,就可能因为管理器已经占用而失败。激活机制把绑定责任移到 socket 单元,并不会消除地址冲突、接口缺失、文件系统权限错误,或其他导致首次绑定失败的条件。因此不能把“端口先开好”写成所有启动竞态已经自动消失,真正的监听建立仍然可能失败。 使用 Accept=no 时,服务接收监听套接字并自行接受连接;Accept=yes 则为合适的流套接字支持按连接启动服务实例的模式。它们对应不同的应用契约,不能互换。任何一种模式都不意味着任意守护进程会自动判断空闲、完成工作后退出,或者只在安全时重新启动。是否退出,以及退出时哪些请求已经完成,仍然由应用行为决定。 ...

十一月 8, 2027 · 2 分钟 · XB Field Notes
三个启动位于同一个括起的十秒窗口内。等长重启间隔指向下一次启动,粗停止栏在上限三次处挡住第四次请求。这些数值是原文样例配置。

systemd 重启策略要明确失败与限流规则

重启策略决定当前进程结束后,服务管理器是否应该再启动一个进程。它可以帮助恢复短暂故障,但修不好缺失配置或必然重复的程序错误。一个有用的契约需要分别回答三个问题:哪些结束方式应该重试、重试前等多久、一个时间窗口内允许启动多少次。这些设置各管一件事,不能把它们笼统理解为“失败后会自己恢复”。 先分类结束原因,再应用固定延迟和窗口启动上限。100 毫秒、十秒、三次启动来自原文 systemd 252.39 样例,图形不按时间比例,也不是本轮实测或生产推荐值。清除限流前先读取并修复失败原因。 坏掉的样子 Restart=on-failure 不代表任何信号结束都会重启。对于 oneshot 以外的服务类型,通常的 SIGHUP、SIGINT、SIGTERM 和 SIGPIPE 终止,默认被视为干净结束;非零退出码,以及 SIGKILL 这样的异常信号,才符合默认失败规则。SuccessExitStatus 和专门的重启例外指令还能改变分类,因此要检查最终生效的单元,不能只看其中一行策略名字。 on-abnormal 也不是 on-failure 的“更严格同义词”:它排除了普通的非零进程退出码,保留异常信号以及部分超时或看门狗故障。选择依据应是程序怎样报告可恢复失败与主动结束。如果进程始终存活,却已经不再提供有用工作,还需要单独配置发现这种状态的机制;基于退出的重启逻辑不会自动推断业务是否健康。 RestartSec 提供下一次重启前的等待时间。在下面的 systemd 252 实验里,它是固定延迟,不是越来越长的指数退避。StartLimitIntervalSec 和 StartLimitBurst 限制窗口内的启动次数,手动启动也算在内;它们既不是整个生命周期的重试预算,也不会在超限后自动预约下一窗口边界再尝试一次。限流与恢复触发应分别理解。 ...

十一月 7, 2027 · 2 分钟 · XB Field Notes
日志文件经过轮转从活动变为归档,之后 vacuum 可以删除归档文件。

systemd journal 的保留范围需要明确约定

systemd journal 把消息与结构化元数据一起保存在日志文件中,因此可以查询服务输出,不必先为每种应用文本设计一套解析器。但让这些消息保留足够长时间,是另一项需要明确的要求。存储位置、空间策略、轮转方式与转发路径,决定了事故发生一段时间后还有哪些证据可查。查询方便,不等于历史永远完整,也不等于数据已经离开这台机器安全保存。 存储路径与保留范围是不同问题。轮转将旧文件关闭成归档并打开替代文件;vacuum 可以删除归档,不能删除活动文件。journald 服务重启不能证明整机重启或断电耐久性,字节目标也不保证固定证据窗口。本图是原文模型,不是新的执行结果。 坏掉的样子 默认命名空间通常使用 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 服务后仍然能够查到。这验证了选中的存储位置,以及消息跨越日志守护进程重启仍然存在;它没有模拟突然断电,也没有证明存储设备的持久性,更没有执行整台机器重启。说明验证范围,可以避免把一个真实但有限的结果扩大成并未做过的耐久性测试。 ...

十月 26, 2027 · 1 分钟 · XB Field Notes
日志记录条经过明确筛选条件,进入更小的匹配集合。

按明确的字段与优先级规则查询 journal

有效的 journal 查询应该选出能够回答具体事故问题的记录。单元身份、运行实例、时间、优先级和应用字段提供了不同维度;正确组合它们,比在整台机器的历史里找到一个熟悉词语更重要。journald 可以附加进程元数据,但不会自动收集应用写出的每个文件,也不会自动理解每条消息的业务含义。查询条件和日志实际采集路径必须相互对应。 不同字段按 AND 组合,同字段多个取值按 OR 组合,独立 + 将条件组按 OR 连接。在原文首次运行示例中,-p err 选择 API 的二、三级,而 PRIORITY=3 只选三级。单元身份比一次运行实例更宽。空输出可能是成功查询,不证明事件未发生。本图不新增运行验证结论。 坏掉的样子 字段匹配有明确的布尔组合规则:不同字段之间是 AND,同一字段的多个取值之间是 OR,单独一个 + 则把两组条件按 OR 连接。因此,再添加一个字段值,有时会扩大结果集,而不是进一步缩小。应把查询看成一个明确的条件表达式,先用已知记录验证它,再放进事故脚本,避免因为“参数更多了”就误认为结果必然更精确。 -p err 选择错误及更严重的级别,并不是只选择数字三。如果真正需要精确的三级记录,应使用 PRIORITY=3 字段匹配。应用在文本中写了“error”,本身也不等于设置了错误优先级;标准输出的采集使用配置好的优先级规则,除非应用通过受支持的表示方式提供级别。消息文字、日志字段和筛选阈值不能互相混同。 命令成功但没有记录,不证明服务从未运行。所选启动周期、时间窗口、命名空间、读取权限、保留策略或输出路由,都可能解释空结果。反过来,查询一个单元的全部历史,又可能找到以前的成功启动,然后错误归因到当前进程。应区分服务长期身份与某一次具体运行,不能只因为单元名称相同就把证据拼接成一次执行。 快速验证 在 PID 1 为 systemd 的一次性 Linux 系统中,以管理员身份运行下面的 Python 脚本。恢复文本记载的环境为 Debian 12、systemd 252.39,但原始运行日志不可用。本轮没有执行此样例;以下结果说明其预期断言,不能视为本轮实测。脚本安装一个唯一命名的临时服务,并激活两次。每个工作进程向原生日志套接字发送四条较短的结构化消息,分别包含两个应用组件和几个明确的优先级: ...

七月 30, 2027 · 2 分钟 · XB Field Notes