用 systemd 把 Python 脚本做成开机自启服务:从 nohup 到崩溃自动重启
背景
我在一台 2 核 4G 的云主机上跑一个 Python 采集脚本:每隔一分钟请求一次接口,把结果写进本地 SQLite,偶尔还要往上游推一条汇总。脚本逻辑本身不难,难的是「怎么让它一直活着」。
我经历过三代做法:
nohup python3 collector.py &——进程崩了就没了,机器重启更是什么都不剩,还得自己ps找 PID;crontab里的@reboot——能开机拉起,但脚本第二天 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
六、常见问题
status=203/EXEC:ExecStart路径不对,或者脚本没有可执行权限。用ls -l确认虚拟环境解释器路径,必要时chmod +x。status=200/CHDIR:WorkingDirectory指向的目录不存在。目录要真实存在,且User有权限进入。- 脚本里读不到环境变量:systemd 不继承登录 shell 的环境。把变量写进
/opt/collector/env,再在 unit 里加一行EnvironmentFile=/opt/collector/env。 - 改了代码但行为没变:
ExecStart启动的是已经在跑的进程,改完要sudo systemctl restart collector。 - 开机启动时依赖还没就绪:加
After=和Wants=network-online.target;如果还不行,用ExecStartPre=加一段等待脚本。
小结
把 Python 脚本交给 systemd,收益就三句话:开机不用管、崩了自己起、日志一处查。写 unit 文件时把绝对路径、最小权限账号、重启策略和日志上限这四点做对,剩下的就是一次 enable --now 的事。
如果你更愿意用容器,同一套目标也可以由 compose 的 restart: always 加 logging 配置来实现——但在单机单进程的场景里,systemd 是改动最小、依赖最少的那条路。
- 点赞
- 收藏
- 关注作者
评论(0)