用 systemd 把 Python 脚本做成开机自启服务:从 nohup 到崩溃自动重启

举报
shijiu_dev 发表于 2026/10/23 08:06:15 2026/10/23
【摘要】 背景我在一台 2 核 4G 的云主机上跑一个 Python 采集脚本:每隔一分钟请求一次接口,把结果写进本地 SQLite,偶尔还要往上游推一条汇总。脚本逻辑本身不难,难的是「怎么让它一直活着」。我经历过三代做法:nohup python3 collector.py &——进程崩了就没了,机器重启更是什么都不剩,还得自己 ps 找 PID;crontab 里的 @reboot——能开机拉起...

背景

我在一台 2 核 4G 的云主机上跑一个 Python 采集脚本:每隔一分钟请求一次接口,把结果写进本地 SQLite,偶尔还要往上游推一条汇总。脚本逻辑本身不难,难的是「怎么让它一直活着」。

我经历过三代做法:

  1. nohup python3 collector.py &——进程崩了就没了,机器重启更是什么都不剩,还得自己 ps 找 PID;
  2. crontab 里的 @reboot——能开机拉起,但脚本第二天 3 点崩掉之后再也没起来,日志被重定向到一个没人看的文件里;
  3. systemd——开机自启、崩溃自动重启、日志集中管理,一次配好,后面省心。

这篇写的就是第 3 种:把任意一个 Python 脚本变成一个正经的常驻服务。

一、先备好一个能跑的脚本

假设脚本放在 /opt/collector/collector.py,运行时不需要任何命令行参数:

#!/usr/bin/env python3
"""每 60 秒采集一次,写进 SQLite。"""
import sqlite3
import time
import datetime

DB = "/opt/collector/data/metrics.db"


def init_db(conn):
    conn.execute(
        "CREATE TABLE IF NOT EXISTS metrics ("
        "  ts TEXT PRIMARY KEY, value REAL)"
    )
    conn.commit()


def collect():
    # 这里换成真实的采集逻辑
    return 42.0


def main():
    conn = sqlite3.connect(DB)
    init_db(conn)
    while True:
        now = datetime.datetime.now().isoformat(timespec="seconds")
        conn.execute(
            "INSERT OR REPLACE INTO metrics (ts, value) VALUES (?, ?)",
            (now, collect()),
        )
        conn.commit()
        time.sleep(60)


if __name__ == "__main__":
    main()

顺手建好目录和虚拟环境,注意虚拟环境里解释器的绝对路径后面要写进配置文件:

sudo mkdir -p /opt/collector/data
sudo python3 -m venv /opt/collector/venv
sudo /opt/collector/venv/bin/pip install -r requirements.txt

二、三种「后台运行」的取舍

方案 开机自启 崩溃重启 日志 适合场景
nohup … & 否 否 自己重定向 临时跑一次
crontab @reboot 是 否 自己重定向 一次性初始化任务
systemd unit 是 是(可配次数与间隔) journald 统一收 常驻服务

判据很简单:只要这个进程挂了你会心疼,就用 systemd。

三、写一个 unit 文件

新建 /etc/systemd/system/collector.service:

[Unit]
Description=Collector metrics daemon
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=collector
Group=collector
WorkingDirectory=/opt/collector
Environment=PYTHONUNBUFFERED=1
ExecStart=/opt/collector/venv/bin/python /opt/collector/collector.py
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

几个容易踩的细节:

  • ExecStart 必须写绝对路径,python3 简写 systemd 不认;
  • Type=simple 表示主进程一直前台运行,Python 脚本属于这一类;
  • Restart=always 配合 RestartSec=5,进程异常退出 5 秒后自动拉起;
  • Environment=PYTHONUNBUFFERED=1 让 print 立刻进日志而不是卡在缓冲区;
  • User 不是 root,跑业务的最小权限账号更安全。

四、启用并自检

sudo systemctl daemon-reload
sudo systemctl enable --now collector
systemctl status collector --no-pager

看到 Active: active (running) 就成功了。再确认两件事:

# 看最近的输出
journalctl -u collector -n 50 --no-pager

# 手动 kill 验证自动重启
sudo systemctl kill -s SIGKILL collector
sleep 6
systemctl is-active collector   # 期望输出 active

最后一条是很多人省掉的步骤:不验证重启策略,等于没配。

五、别让日志把磁盘写满

journald 默认按容量滚动,但云主机磁盘普遍不大,建议显式设上限。编辑 /etc/systemd/journald.conf:

[Journal]
SystemMaxUse=500M
SystemMaxFileSize=50M

改完重启日志服务:

sudo systemctl restart systemd-journald
journalctl --disk-usage

六、常见问题

  1. status=203/EXEC:ExecStart 路径不对,或者脚本没有可执行权限。用 ls -l 确认虚拟环境解释器路径,必要时 chmod +x。
  2. status=200/CHDIR:WorkingDirectory 指向的目录不存在。目录要真实存在,且 User 有权限进入。
  3. 脚本里读不到环境变量:systemd 不继承登录 shell 的环境。把变量写进 /opt/collector/env,再在 unit 里加一行 EnvironmentFile=/opt/collector/env。
  4. 改了代码但行为没变:ExecStart 启动的是已经在跑的进程,改完要 sudo systemctl restart collector。
  5. 开机启动时依赖还没就绪:加 After= 和 Wants=network-online.target;如果还不行,用 ExecStartPre= 加一段等待脚本。

小结

把 Python 脚本交给 systemd,收益就三句话:开机不用管、崩了自己起、日志一处查。写 unit 文件时把绝对路径、最小权限账号、重启策略和日志上限这四点做对,剩下的就是一次 enable --now 的事。

如果你更愿意用容器,同一套目标也可以由 compose 的 restart: always 加 logging 配置来实现——但在单机单进程的场景里,systemd 是改动最小、依赖最少的那条路。

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

评论(0)

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

全部回复

上滑加载中

设置昵称

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

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

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