systemd 守护进程:服务挂了自己重启

2026 年 09 月 28 日 · 约 8 分钟读完

网站在服务器上跑起来了,然后呢?你关掉 SSH 窗口,服务就跟着没了。新手最常见的三件套是:命令后面加 & 放后台、nohup 挂起、tmux 开个窗口养着。这三样有三个共同的毛病:第一,机器重启就全没了,半夜机房断电一次你就得爬起来手动恢复;第二,进程挂了没人知道,更没人帮你重启;第三,日志没人管,输出重定向到一个文件里越滚越大。正道是 systemd:它是 Linux 的服务管家,你告诉它怎么启动你的网站,它负责开机自启、挂了重启、日志归档。这篇写一个能直接用的 .service 文件,每个字段都讲人话。

systemd 状态流转图:服务崩溃后 systemd 自动按 Restart=always 重启,回到运行中服务运行中Active: active (running)进程崩溃退出systemd 发现进程没了一直盯着,不用你看着Restart=always自动重启等 5 秒再拉起来(RestartSec)恢复运行配合 journalctl -u 服务名 -f 看日志:先看清为什么挂,再决定要不要改 Restart 策略
systemd 状态流转:服务崩溃后自动按 Restart=always 重启,回到运行中

先看一个能直接用的例子

假设你的网站在 /var/www/myapp,用 node server.js 启动,跑在 deploy 用户下。新建 /etc/systemd/system/myapp.service,内容如下:

[Unit]
Description=我的网站
After=network.target

[Service]
Type=simple
User=deploy
WorkingDirectory=/var/www/myapp
ExecStart=/usr/bin/node server.js
Restart=always
RestartSec=5
Environment=NODE_ENV=production

[Install]
WantedBy=multi-user.target

每个字段讲人话

  • Description:这个服务是干嘛的,自己看的名字,随便写清楚就行。
  • After=network.target:等网络好了再启动,别一开机就跑,那时候网还没通。
  • Type=simple:启动命令就是服务本身(node server.js 一直跑着),最常用就选它。
  • User=deploy:用哪个用户跑。别用 root,用前面建好的普通用户。
  • WorkingDirectory:先 cd 到这个目录再启动,相对路径的配置都靠它。
  • ExecStart:真正的启动命令,必须写绝对路径(/usr/bin/node,用 which node 查)。
  • Restart=always:核心。进程不管怎么退出都重启,挂了自己拉起来。注意它是退出就重启,包括你手动 systemctl stop——stop 是告诉 systemd 别管了,它不会跟你作对去重启,只有意外退出才触发。
  • RestartSec=5:重启前等 5 秒,别崩溃瞬间疯狂重启把机器拖死。如果服务一启动就崩(比如配置写错了),systemd 会按这个间隔反复试几次,你有足够时间去看日志。
  • Environment:环境变量,一行一个,生产环境 NODE_ENV=production 别忘。变量多了别全堆在这里,用 EnvironmentFile=/var/www/myapp/.env 指向一个文件,清爽得多,注意那个文件权限别让别人可读,里面可能有密钥。
  • WantedBy=multi-user.target:允许开机自启,不写这行 enable 会报错。multi-user.target 就是命令行多用户模式,服务器一般都进这个模式,开机进到这一步时你的服务跟着起来。

Type 还有哪些:一句话带过

Type=simple 是最常用的,但还有两个会碰到:Type=forking 给那种启动后自己退到后台的老式程序(比如 nginx),systemd 靠 PID 文件找它;Type=oneshot 给跑完就退的一次性任务,配合 RemainAfterExit=yes 用。90% 的场景 simple 就够,看到别人的 service 文件里写 forking 别慌,知道有这回事就行。

上线四步:放进去、生效

sudo cp myapp.service /etc/systemd/system/  # 放进系统目录
sudo systemctl daemon-reload              # 告诉 systemd 有新服务了
sudo systemctl enable --now myapp         # 开机自启 + 现在就启动
sudo systemctl status myapp               # 看状态

status 看到 Active: active (running) 就是活了。注意第二步 daemon-reload 每次改完 .service 文件都要跑一遍,不然改的是旧版本——这是新手最常忘的一步,改完配置发现没生效,九成是这个原因,心里先打个问号:我 reload 了吗。enable 和 start 是两回事:start 是现在就跑,enable 是下次开机还跑,--now 把两个合在一起。可以用 systemctl is-enabled myapp 确认开机自启真的开上了,显示 enabled 才算。

ExecStart 必须写绝对路径。用 which node 查出完整路径再填,写 node server.js 这种相对命令,systemd 会告诉你找不到。

systemctl 常用命令一张表

  • systemctl status myapp:看状态,active 还是 failed,第一时间看它。
  • systemctl start/stop/restart myapp:启动、停止、重启,重启是改完配置最常用的。
  • systemctl enable/disable myapp:开/关开机自启,不影响现在跑没跑。
  • systemctl is-enabled myapp:查开机自启到底开上没,返回 enabled 才算。
  • systemctl daemon-reload:改完 .service 文件必须跑,不然改了个寂寞。
  • systemctl cat myapp:看当前生效的完整配置,排查到底用的是哪个文件。

这六个记住,日常运维够用了。systemctl cat 专门提一句:它显示的是 systemd 实际读到的合并后配置,怀疑配置没生效时先 cat 一眼,比猜快得多。另外 tab 补全对服务名是生效的,systemctl status my 按 tab 能补全,服务名记不全也不怕,少打很多字。

服务文件放哪:两个目录别搞混

系统里有两个放 .service 文件的地方:/lib/systemd/system 是软件包自带的(比如 nginx.service),/etc/systemd/system 是你自己写的。自己写的放 /etc 下,升级系统不会被覆盖,优先级也更高。两边有同名文件时,/etc 的胜出。记住这个规则,排查问题时才知道到底生效的是哪个文件——systemctl status 里 Loaded 那一行会写清楚用的是哪个路径。

看日志:journalctl

服务的输出(console.log 那些)systemd 都帮你收着,不用自己重定向到文件。查日志就两句:

journalctl -u myapp -n 50 --no-pager   # 看最近 50 行
journalctl -u myapp -f                  # 实时追,Ctrl+C 退出

服务起不来先看日志,别急着重启。十次有九次是路径写错、端口被占、环境变量没配,日志里都写得明明白白。journalctl 还有几个实用组合:--since '1 hour ago' 只看最近一小时的,-p err 只看报错级别的,两个可以叠着用。日志是 systemd 帮你收着的,存在 /var/log/journal 里,默认最多占磁盘 10%,不用担心它把你的盘吃满。注意:如果 /var/log/journal 这个目录不存在,日志只存在内存里,重启就丢了。跑 sudo mkdir -p /var/log/journal 再重启服务,日志才能持久化,小机器上建议检查一下。

三个最常见的报错,对号入座

  • status 显示 failed,日志里 permission denied:八成是 User=deploy 但文件是 root 的,deploy 读不了。用 chown -R deploy:deploy /var/www/myapp 把目录还给它,或者检查 EnvironmentFile 指向的文件权限。
  • 203/EXEC 报错:ExecStart 写的路径不对,systemd 找不到那个可执行文件。which node 查出完整路径再填,别凭记忆写 /usr/local/bin/node,版本管理器装的 node 路径千奇百怪。
  • EADDRINUSE 端口被占:这个端口已经有个进程在用了。ss -tlnp | grep 端口号 找到它,要么杀掉旧的,要么改新服务的端口。常见原因是之前用 nohup 起的旧进程还活着,两个抢一个端口。

什么时候别用 Restart=always

Restart=always 适合一直跑着的服务(网站、API)。但有一种服务不适合:跑完一次就退出的定时任务,比如每天凌晨跑一遍的备份脚本。这种用 systemd 的 timer 或者 cron 就行,硬套 Restart=always 会变成死循环——跑完退出、systemd 拉起来、又跑完、又拉起来,你的备份一天会被跑几千遍。另外调试阶段也别开 always,先用 Restart=no,确认服务能稳定跑起来再换成 always,不然它在你改配置的时候反复重启,日志刷得没法看。

优雅停机:别让重启丢数据

Restart=always 管的是崩溃,但正常的重启和关机也得体面。systemd stop 服务时默认发 SIGTERM,给进程一点时间自己收尾(关连接、刷缓存),等 TimeoutStopSec(默认 90 秒)还没退出才发 SIGKILL 强杀。你的程序要处理 SIGTERM 信号,该落盘的落盘,Node 里监听 process.on('SIGTERM') 做清理,Python 里用 signal 模块接。如果程序需要特殊的停止命令(比如先通知主进程),可以加 ExecStop=/usr/bin/kill -TERM $MAINPID,不过大多数 Node/Python 程序默认行为就够用了。记住:kill -9 是核武器,只在进程僵死时用,日常重启走 systemctl restart,让它体面地结束。

资源限制:别让一个服务拖死整台机器

1 核 2G 的小机器上,一个内存泄漏的服务能把整台机器拖进 OOM,SSH 都连不上。systemd 可以给服务加上限:

[Service]
MemoryMax=512M
CPUQuota=80%

MemoryMax=512M 是内存超过 512M 就杀掉这个服务(然后 Restart=always 再拉起来),CPUQuota=80% 是最多用 0.8 核。被杀了会在日志里留下记录,你能看到是它在作怪,而不是整台机器莫名其妙卡死,连 SSH 都连不上。小机器上给每个服务都加上限,这是花 2 分钟避免半夜被叫醒的操作,值。

验证自动重启真的管用

别信文档,自己动手验一次:ps 找到进程 kill 掉,等几秒再 systemctl status,看 Main PID 变了、启动时间更新了——这才是真的会自己重启。验证完你才敢半夜睡觉。还有个细节:服务如果反复崩溃,status 会显示 failed,这时候修好配置后跑 systemctl reset-failed myapp 清掉失败计数,再 start,不然它还记着上次的 failed 状态。

ps aux | grep "node server.js"   # 找到 PID,比如 812
kill 812                          # 模拟崩溃
sleep 8
sudo systemctl status myapp       # PID 变了、时间更新了 = 自动重启生效

如果 kill 之后服务没起来,先别慌:看 status 是不是 failed,是的话 journalctl -u myapp -n 30 找原因。常见的是 RestartSec 还没到(多等几秒),或者服务连续崩溃太多次被 systemd 限流了——这是保护机制,修好根本问题再 reset-failed 就行,别跟它较劲硬重启。

到这里,你的网站就有了管家:开机自己起,挂了自己重启,日志有人收着,资源还有上限兜底,停机也体面,半夜能睡安稳觉。前面没做服务器初始化的回去看《VPS 到手先做这 10 件事》,反代和 HTTPS 看《Caddy 反代:5 分钟给网站加上 HTTPS》。systemd 相关的命令(systemctl、journalctl)记不清就去站里的 Shell 命令速查页(站内 /shell),都在日志排查分组里,复制粘贴就能用。

systemd 守护进程:服务挂了自己重启 · EazyOPC 博客