速读#
nohup 只解决关终端不退出,systemd 才解决崩溃恢复、开机自启、统一日志和服务管理。一个你睡觉时挂了不会自己回来的进程,不算真正上线;但 systemd 只拉进程级故障,整机卡死还要另看。
裸跑不算上线#
我的判定标准:一个我睡觉时挂了、不会自己回来的进程,不叫服务,叫「还没跑稳的程序」。从能用到能信赖,中间差的就是守护。
| nohup 裸跑 | systemd | |
|---|---|---|
| 崩溃恢复 | 不会 | 可自动重启 |
| 开机自启 | 不会 | 支持 |
| 日志 | 自己重定向 | journalctl 统一收 |
| 管理 | 靠 kill | start / stop / restart |
nohup 只解决「关终端别杀我」;systemd 解决「崩了能回来、重启能起来、日志有人收、管理有命令」一整面。不是同一量级。
一份最小 unit,核心就那两行#
完整的最小 unit 长这样(/etc/systemd/system/myapp.service):
[Unit]Description=myappAfter=network.target
[Service]User=s1oopxWorkingDirectory=/opt/myappExecStart=/usr/bin/python3 main.pyRestart=alwaysRestartSec=3
[Install]WantedBy=multi-user.target其中真正承担「守护」语义的是两行:
| 行 | 保什么 |
|---|---|
Restart=always | 崩了回来 |
WantedBy=multi-user.target | 重启了回来 |
其余是交代清楚身份和位置:用哪个用户跑(别用 root)、在哪个目录跑、执行什么。ExecStart 要写绝对路径——systemd 不继承你 shell 的 PATH,这是从裸跑迁移过来最容易撞的一下。还有一处容易望文生义:After=network.target 只保证启动顺序排在网络配置之后,不保证网络真的通了;真要「等到网络可用」,得用 network-online.target。练手服务差别不大,但排查「开机后第一次请求失败」这类问题时,知道这层区别就不会走错方向。
systemctl enable --now myapp # 开机自启 + 立即启动systemctl status myapp # 第一眼:活着吗、重启过几次journalctl -u myapp -f # 看日志一个容易误解的点:Restart=always 拉的是异常退出,systemctl stop 停的进程不会被拉起来。手动停是意图,崩溃才是故障,systemd 分得清这两件事。另外崩得太密会触发 start limit,systemd 直接放弃重启——这其实是对的:三秒一崩的服务,无限重启只是把故障刷成日志噪音,该做的是去查根因。
改完要 reload#
改完 unit 一定 systemctl daemon-reload,否则 systemd 还在用旧配置。踩过:改了 ExecStart 没 reload,重启发现跑的还是旧命令。
配置改了 ≠ 生效,要触发加载。 Nginx 改完要 reload、fail2ban 改完要 restart——「改」和「生效」之间那一步,最容易漏。
边界:它拉的是进程退出#
Restart=always 拉的是进程崩了、被杀了。拉不起更彻底的挂:系统卡死、硬件故障、内核 panic——那是 watchdog 这一层的事。
我不假装 systemd 万能,也不假装已经上了 watchdog。守护是分层的,每层挡什么级别的挂,照实写。
守护要配可观测#
systemd 负责拉回来,探针负责通知我它挂过。自愈是好事,自愈了还不知道,就少了一次复盘机会。 我用哪吒探针补这一环。
事后翻 journalctl -u myapp --since yesterday,能看到它什么时候退、退出码多少、几秒后被拉起——自愈的每一次现场都留了底,这是裸跑时代想都不敢想的。
守护是「上线」的分水岭,且它是分层的:systemd 挡进程级,watchdog 挡更底层。
