声明依赖以让初始化系统调度启动顺序

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