用 systemd 让你的脚本在服务器上稳定跑一整年
很多人第一次让脚本在服务器上长期跑,用的都是同一种写法:
nohup python job.py > out.log 2>&1 &
然后关掉终端,心里默念"应该还在跑吧"。
过两天再连上去,ps -ef | grep job.py 什么都没有。日志最后一行停在某个网络超时,脚本半夜就死了,没人知道。
这篇文章讲清楚一件事:把"靠人盯着"换成"靠系统盯着"。用 systemd 三个最常用的单元,让脚本自己活、自己重启、自己记录,出事你还能查到原因。
一、为什么 nohup 不够用
nohup ... & 只解决了"关终端不退出"这一个问题,剩下的六个它都不管:
- 进程崩了不会重启
- 机器重启后不会自动起来
- 没有统一的日志落盘和轮转
- 不知道它是"正常运行"还是"卡死"
- 启动顺序、依赖关系全靠手写 sleep
- 多个服务之间没法做依赖编排
这些在 systemd 里都是配置项,写一次管一辈子。
二、service:把一个脚本变成受管服务
最常用的形态是"常驻进程"。新建一个单元文件:
[Unit]
Description=My long running job
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=appuser
WorkingDirectory=/srv/myapp
ExecStart=/usr/bin/node /srv/myapp/worker.js
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
几个字段值得单独说:
Restart=on-failure 是核心。进程非零退出就重拉,这是 nohup 永远做不到的事。配合 RestartSec 避免疯狂重启把机器拖垮。
User=appuser 一定要写。用 root 跑业务进程是最常见的坏习惯,一旦脚本有漏洞,攻击者直接拿到整台机器。
WorkingDirectory 别省。很多人脚本里用了相对路径,手动跑没事,装成服务就找不到文件——因为 systemd 默认的工作目录是根目录。
Type=simple 适合绝大多数场景。只有当你需要 systemd 等待"启动完成"信号时,才用 Type=notify 并在程序里主动通知。
写完后三步上线:
sudo systemctl daemon-reload
sudo systemctl enable --now myjob.service
systemctl status myjob.service
enable 和 start 是两件事,前者管开机自启,后者管现在就跑。--now 是两个一起做。
三、timer:比 cron 更值得用的定时器
如果你的活儿是"每隔一段时间跑一次"而不是常驻,别急着写 crontab。systemd 的 timer 有三个 cron 没有的好处:
- 错过补偿:机器关机期间错过的任务,开机后补跑一次。cron 不会。
- 日志统一:跟 service 一样进 journal,不用再去翻邮件和 syslog。
- 依赖清晰:可以声明"跑之前必须等网络就绪"。
一个 timer 由两个文件组成,服务本体加定时器本体:
# myjob.service
[Unit]
Description=Periodic job
[Service]
Type=oneshot
User=appuser
WorkingDirectory=/srv/myapp
ExecStart=/usr/bin/node /srv/myapp/job.js
TimeoutStartSec=300
# myjob.timer
[Unit]
Description=Run myjob every 20 minutes
[Timer]
OnBootSec=3min
OnUnitActiveSec=20min
AccuracySec=30s
Persistent=true
[Install]
WantedBy=timers.target
注意 Type=oneshot——定时任务的进程本来就该跑完就退,不是常驻。很多人在这里写成 simple,结果 systemd 一直认为服务还在启动中,后续的 timer 调度就乱了。
OnUnitActiveSec 是"上一次跑完之后再等 20 分钟",适合耗时不固定的任务;OnCalendar 是"每天几点几分",适合和人类作息对齐的任务,比如凌晨跑批。
Persistent=true 就是前面说的错过补偿开关,配合 OnCalendar 用才有意义。
启用和查看:
sudo systemctl enable --now myjob.timer
systemctl list-timers --all
第二个命令会列出所有定时器的下次触发时间和上次执行时间,比翻 crontab 直观得多。
四、日志:出事时能一眼看出问题
不要把日志往文件里 >> 追加然后不管。用 journal:
journalctl -u myjob.service -n 100 --no-pager
journalctl -u myjob.service --since "1 hour ago"
journalctl -u myjob.service -f
-f 是实时跟踪,等价于 tail -f,但不用你记住日志文件在哪、有没有被切走。
长期日志建议在 /etc/systemd/journald.conf 里限制体积:
[Journal]
SystemMaxUse=500M
MaxRetentionSec=2week
否则 journal 会一路涨到把磁盘吃满,这是新手最容易踩的坑之一。
五、几个真会咬人的坑
坑一:环境变量不继承。 你在 .bashrc 里 export 的变量,systemd 一个都看不到。要么在单元里写 Environment=,要么 EnvironmentFile= 指向一个文件。排查时先怀疑这个。
坑二:PATH 不一样。 交互式 shell 的 PATH 很长,systemd 的 PATH 很短。脚本里调用了 node、ffmpeg 这类命令却报"找不到",八成是这个原因。用绝对路径最稳。
坑三:改了 unit 文件没生效。 必须 systemctl daemon-reload,否则 systemd 用的还是内存里的旧配置。这个坑几乎人人踩过一次。
坑四:Type 写错。 常驻写 oneshot 会让 systemd 一直等它退出;一次性任务写 simple 会让 systemd 以为它还在跑。表现都是"状态不对但看不出错"。
六、小结
把判断标准记成一句话:
- 活儿一直在跑 → 用
service,配Restart=on-failure - 活儿隔一阵跑一次 → 用
timer加Type=oneshot
配好这两样,脚本就从"我盯着它"变成"系统盯着它"。半夜崩了会自己起来,机器重启了会自己上线,出错有日志可查。
真正省下的不是那几行命令,是你每天惦记它的那点心力。
- 点赞
- 收藏
- 关注作者
评论(0)