一次 Systemd 服务启动失败排查:StandardOutput 与 Python 自动创建目录的时序陷阱

背景

最近在部署一个 Streamlit 应用,使用 Systemd 管理服务,希望它能够开机自启并在崩溃后自动恢复。应用的代码中有一个常见设计:在启动时自动创建 log/ 目录,并写入按天轮转的应用日志文件(如 app_20260630.log)。Systemd service 文件中配置了 StandardOutputStandardError 将 stdout/stderr 重定向到该目录下的 streamlit.logstreamlit-error.log,方便统一管理。

然而,在配置完 service 文件并执行 systemctl start 后,服务却一直处于不断重启的状态,始终无法正常运行。

现象

查看服务状态:

bash 复制代码
$ systemctl status submit_branch_approval
● submit_branch_approval.service - Streamlit submit_branch_approval App
     Loaded: loaded (/etc/systemd/system/submit_branch_approval.service; enabled; preset: enabled)
     Active: activating (auto-restart) (Result: exit-code) since Tue 2026-06-30 08:12:38 UTC; 5s ago
    Process: 293988 ExecStart=/home/vod/code/feishu/submit_branch_approval/.venv/bin/streamlit run ... --server.port 8502 ...
   Main PID: 293988 (code=exited, status=209/STDOUT)
        CPU: 929us

关键信息:

  • Active: activating (auto-restart) -- 服务不断自动重启;
  • code=exited, status=209/STDOUT -- 退出码 209,表示标准输出设置失败。

查看 journal 日志得到更明确的错误:

bash 复制代码
$ sudo journalctl -u submit_branch_approval -f
Jun 30 08:15:22 vod systemd[1]: Started submit_branch_approval.service - Streamlit submit_branch_approval App.
Jun 30 08:15:22 vod (treamlit)[294979]: submit_branch_approval.service: Failed to set up standard output: No such file or directory
Jun 30 08:15:22 vod systemd[1]: Main process exited, code=exited, status=209/STDOUT

日志直指问题:Failed to set up standard output: No such file or directory

排查过程

1. 确认日志目录是否存在

我首先想到可能是目录权限或路径问题,于是检查了应用目录:

bash 复制代码
$ ls -ld /home/vod/code/feishu/submit_branch_approval/log
ls: cannot access '/home/vod/code/feishu/submit_branch_approval/log': No such file or directory

目录确实不存在。

2. 为什么应用没有自动创建?

代码中明明有自动创建逻辑:

python 复制代码
# demo_app.py
LOG_DIR = Path(__file__).parent / "log"
LOG_DIR.mkdir(exist_ok=True)

按道理,应用启动时应该会创建这个目录。但为什么它没执行?

3. 重新审视 systemd 启动流程

Systemd 在启动服务时,会先做一系列准备工作,其中就包括打开标准输出和标准错误所指定的文件 。它必须在 fork() 之后、execve() 之前完成这些 I/O 重定向。这意味着:如果 StandardOutputStandardError 指向的文件无法打开(例如目录不存在),systemd 就会直接失败,并不会去执行你指定的程序

也就是说,你的 Python 代码根本没有获得执行机会------它在启动前就被 systemd 拦下了。因此,LOG_DIR.mkdir() 永远没机会运行。

原因分析

问题的根源在于时序依赖

  • Python 代码希望在运行时创建目录;
  • Systemd 却期望在启动前目录已经存在,以便打开日志文件。

这种设计冲突导致启动失败。Systemd 的 StandardOutputStandardError 配置是在进程启动前由 systemd 自身处理的,而不是由应用进程处理。如果目录不存在,systemd 会报错并退出,根本不进入应用执行阶段。

退出码 209/STDOUT 正是 systemd 内部用于表示标准输出设置失败的代码。

解决方案

解决思路有两种:

方案一:手动创建目录,保留 systemd 重定向

既然 systemd 要求目录提前存在,我们就提前创建它:

bash 复制代码
sudo mkdir -p /home/vod/code/feishu/submit_branch_approval/log
sudo chown vod:vod /home/vod/code/feishu/submit_branch_approval/log

然后重启服务即可。应用运行后,既可以写入自己的 app_*.log,systemd 也会将 stdout/stderr 写入指定的 streamlit.log

方案二:移除 systemd 重定向,改用 journald(推荐)

在很多场景下,我们并不需要单独存放 systemd 的 stdout/stderr 日志,尤其是应用自身已经写了详细的日志文件。此时,可以直接在 service 文件中注释掉(或删除)StandardOutputStandardError 这两行,让所有输出都进入 systemd journal。

编辑 service 文件:

bash 复制代码
sudo systemctl edit --full submit_branch_approval

/etc/systemd/system/submit_branch_approval.service:

bash 复制代码
[Unit]
Description=Streamlit submit_branch_approval App
After=network.target

[Service]
Type=simple
User=vod
WorkingDirectory=/home/vod/code/feishu/submit_branch_approval

# 使用共享的虚拟环境,端口 8502
ExecStart=/home/vod/code/feishu/submit_branch_approval/.venv/bin/streamlit run /home/vod/code/feishu/submit_branch_approval/demo_app.py --server.port 8502 --server.address 0.0.0.0 --server.headless true

# 自动重启配置
Restart=always
RestartSec=10

# 日志输出(每个服务独立的日志文件)
StandardOutput=append:/home/vod/code/feishu/submit_branch_approval/log/streamlit.log
StandardError=append:/home/vod/code/feishu/submit_branch_approval/log/streamlit-error.log

[Install]
WantedBy=multi-user.target

注释掉这两行:

复制代码
# StandardOutput=append:/home/vod/code/feishu/submit_branch_approval/log/streamlit.log
# StandardError=append:/home/vod/code/feishu/submit_branch_approval/log/streamlit-error.log

保存后执行:

bash 复制代码
sudo systemctl daemon-reload
sudo systemctl restart submit_branch_approval

之后查看日志使用 journalctl -u submit_branch_approval -f,既方便又避免了目录权限问题。

验证结果

执行方案二后,服务正常启动:

复制代码
$ systemctl status submit_branch_approval
● submit_branch_approval.service - Streamlit submit_branch_approval App
     Active: active (running) since Tue 2026-06-30 08:18:57 UTC; 3s ago
   Main PID: 296405 (streamlit)
      Tasks: 8 (limit: 9329)
     Memory: 44.9M
     CGroup: /system.slice/submit_branch_approval.service
             └─296405 /home/vod/code/feishu/submit_branch_approval/.venv/bin/python3 ...

Jun 30 08:18:58 vod streamlit[296405]: Uvicorn server started on 0.0.0.0:8502
Jun 30 08:18:58 vod streamlit[296405]: You can now view your Streamlit app in your browser.

应用日志(app_20260630.log)依然由 Python 代码自行写入新建的 log/ 目录,两者互不干扰。

经验教训与总结

  1. Systemd 的 I/O 重定向发生在进程启动之前。任何依赖程序内部创建目录/文件的做法,都无法满足 systemd 提前打开文件的需求。

  2. 退出码 209/STDOUT 是一个重要信号,它直接指向标准输出设置失败,排查时应优先检查 StandardOutputStandardError 指定的路径是否存在且可写。

  3. 最佳实践

    • 如果应用本身已具备完善的日志系统(如 RotatingFileHandler、TimedRotatingFileHandler),可以不在 systemd 层面再重定向到文件,而是让输出进入 journald,既减少配置复杂度,也便于集中查看。
    • 若确实需要 systemd 日志文件,请在部署脚本或安装步骤中提前创建好相应目录,并设置正确的权限。
  4. 自动化创建目录的代码仍然有价值 ,它可以保证应用在手动运行(如直接 python demo_app.py)时也能正常工作,但在 Systemd 服务中不能依赖它来满足 systemd 的启动前需求。

总之,理解 Systemd 的启动时序,是避免此类"坑"的关键。希望这篇记录能帮助遇到类似问题的朋友快速定位并解决。


欢迎留言讨论,如果有更好的实践,也请不吝赐教。