把 Python 脚本交给 systemd:一份可直接抄的生产级单元文件笔记
后台跑一个 Python 脚本,很多人第一反应是 nohup python main.py &。我最初也这么干,直到某次机器重启后发现服务没起来,日志还躺在被清空的 /tmp 里——从那次开始,我把所有常驻脚本都改成了 systemd 服务。这篇文章记录我踩过的坑和最后稳定用的那套写法。
一、最小可用的 unit 文件
以 /opt/fetcher 下的采集脚本为例,新建 /etc/systemd/system/fetcher.service:
[Unit]
Description=Fetcher worker
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=fetcher
Group=fetcher
WorkingDirectory=/opt/fetcher
Environment="PYTHONUNBUFFERED=1"
ExecStart=/opt/fetcher/.venv/bin/python /opt/fetcher/main.py
Restart=on-failure
RestartSec=5
TimeoutStopSec=20
[Install]
WantedBy=multi-user.target
然后 systemctl daemon-reload && systemctl enable --now fetcher。
几个参数值得单独说:
WorkingDirectory必须写。脚本里所有相对路径都以它为基准,不写就会落到/,读取配置文件时直接报FileNotFoundError。Environment="PYTHONUNBUFFERED=1"是关键。Python 默认对非终端输出做块缓冲,日志会憋在内存里,journalctl -f看着像卡死。Restart=on-failure配合RestartSec做退避重试就够了,不建议用Restart=always,否则脚本自身逻辑正常退出时也会被反复拉起,日志刷得很难看。
二、环境变量和虚拟环境的两处常见坑
第一坑:PATH 不一样。登录 shell 里能跑的命令,在 unit 里可能找不到。原因很简单——systemd 不读 /etc/profile。要么在 ExecStart 里写绝对路径,要么显式声明:
Environment="PATH=/opt/fetcher/.venv/bin:/usr/local/bin:/usr/bin:/bin"
第二坑:User 与文件权限不匹配。用 User=fetcher 启动后,脚本写入 ~/.cache 或者 /opt/fetcher/data 时可能没有写权限。排查时先 sudo -u fetcher ls -l /opt/fetcher/data 确认,再决定是改属主还是加 ReadWritePaths。
还有一个小细节:如果脚本里用了 input() 或任何需要 TTY 的逻辑,Type=simple 下会直接报错退出。常驻服务一律不应该依赖交互输入。
三、日志:交给 journald,或者交给 logrotate
最简单的做法是不在 Python 里写文件,统一 print 到 stdout,由 journald 收集:
journalctl -u fetcher -f # 实时
journalctl -u fetcher --since today
好处是自动轮转、带时间戳、能按服务过滤。如果一定要写文件,就别自己搞轮转,配 /etc/logrotate.d/fetcher:
/var/log/fetcher/*.log {
daily
rotate 14
compress
missingok
notifempty
copytruncate
}
copytruncate 是为了兼容那些不会重新打开文件句柄的程序(比如用 logging.FileHandler 打开后一直持有着)。代价是轮转瞬间可能丢几行日志,但对采集类服务可以接受。
四、上线与排错顺序
我现在的固定流程是:
- 改 unit 文件后先
systemd-analyze verify fetcher.service,能提前发现拼写和语法问题。 systemctl daemon-reload,再restart,不要只start,否则旧进程不会被替换。- 立刻
systemctl status fetcher --no-pager -l,看Active和最近几行 journal 输出。 - 如果起不来,
journalctl -u fetcher -n 50 --no-pager看真实报错,而不是猜。
补充一个经验:Type=simple 下 systemd 认为 ExecStart 一执行就算启动成功,脚本内部初始化失败它并不知道。如果启动阶段有比较重的准备工作(建连接池、加载模型),可以换成 Type=notify 配合 sd_notify,或者退一步用 ExecStartPre 做前置检查。
五、小结
把脚本服务化本身不复杂,复杂的是那些"在终端里能跑、放到 systemd 里就出问题"的隐性差异:工作目录、PATH、缓冲、权限、TTY。把这些逐项显式写进 unit 文件,比在脚本里到处 chdir 和 os.environ 打补丁要干净得多。
现在我的机器上所有常驻任务都只有一条 systemctl status 就能看明白状态,重启后也不会再有"服务没起来"的意外。
- 点赞
- 收藏
- 关注作者
评论(0)