把 Python 脚本交给 systemd:一份可直接抄的生产级单元文件笔记

举报
yd_289814845 发表于 2026/11/11 08:59:20 2026/11/11
【摘要】 把 nohup 常驻脚本迁移到 systemd 的完整笔记:unit 文件逐项说明、PATH 与虚拟环境坑、journald 与 logrotate 的日志治理,以及上线排错的固定顺序。

后台跑一个 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 打开后一直持有着)。代价是轮转瞬间可能丢几行日志,但对采集类服务可以接受。

四、上线与排错顺序

我现在的固定流程是:

  1. 改 unit 文件后先 systemd-analyze verify fetcher.service,能提前发现拼写和语法问题。
  2. systemctl daemon-reload,再 restart,不要只 start,否则旧进程不会被替换。
  3. 立刻 systemctl status fetcher --no-pager -l,看 Active 和最近几行 journal 输出。
  4. 如果起不来,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 就能看明白状态,重启后也不会再有"服务没起来"的意外。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。