背景
最近在部署一个 Streamlit 应用,使用 Systemd 管理服务,希望它能够开机自启并在崩溃后自动恢复。应用的代码中有一个常见设计:在启动时自动创建 log/ 目录,并写入按天轮转的应用日志文件(如 app_20260630.log)。Systemd service 文件中配置了 StandardOutput 和 StandardError 将 stdout/stderr 重定向到该目录下的 streamlit.log 和 streamlit-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 重定向。这意味着:如果 StandardOutput 或 StandardError 指向的文件无法打开(例如目录不存在),systemd 就会直接失败,并不会去执行你指定的程序。
也就是说,你的 Python 代码根本没有获得执行机会------它在启动前就被 systemd 拦下了。因此,LOG_DIR.mkdir() 永远没机会运行。
原因分析
问题的根源在于时序依赖:
- Python 代码希望在运行时创建目录;
- Systemd 却期望在启动前目录已经存在,以便打开日志文件。
这种设计冲突导致启动失败。Systemd 的 StandardOutput 和 StandardError 配置是在进程启动前由 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 文件中注释掉(或删除)StandardOutput 和 StandardError 这两行,让所有输出都进入 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/ 目录,两者互不干扰。
经验教训与总结
-
Systemd 的 I/O 重定向发生在进程启动之前。任何依赖程序内部创建目录/文件的做法,都无法满足 systemd 提前打开文件的需求。
-
退出码 209/STDOUT 是一个重要信号,它直接指向标准输出设置失败,排查时应优先检查
StandardOutput和StandardError指定的路径是否存在且可写。 -
最佳实践:
- 如果应用本身已具备完善的日志系统(如 RotatingFileHandler、TimedRotatingFileHandler),可以不在 systemd 层面再重定向到文件,而是让输出进入 journald,既减少配置复杂度,也便于集中查看。
- 若确实需要 systemd 日志文件,请在部署脚本或安装步骤中提前创建好相应目录,并设置正确的权限。
-
自动化创建目录的代码仍然有价值 ,它可以保证应用在手动运行(如直接
python demo_app.py)时也能正常工作,但在 Systemd 服务中不能依赖它来满足 systemd 的启动前需求。
总之,理解 Systemd 的启动时序,是避免此类"坑"的关键。希望这篇记录能帮助遇到类似问题的朋友快速定位并解决。
欢迎留言讨论,如果有更好的实践,也请不吝赐教。