线上有个 Spring Boot 服务退出后没有再起来,第一反应通常是补一句 Restart=always。我见过这么改的,结果是发版时正常停掉的进程也被拉回来了,现场更乱。
systemd 不是看到进程没了就无脑重启。它至少要过三道判断:这次退出算不算失败,重启次数有没有撞限额,当前追踪到的到底是不是你以为的那个主进程。

先看退出为什么没有被算成失败
对常驻服务,Restart=on-failure 通常是比 always 更稳的默认选择。systemd 的官方说明也把它列为长运行服务的推荐项。
但它只会在这些情况重启:
- 进程返回非零退出码
- 进程异常被信号终止
- 服务启动或运行超时
退出码是 0,或者是有意停止的 SIGTERM、SIGINT,都不属于它要补救的故障。
所以排查时别只盯着「进程没了」。有些启动脚本把 Java 异常吃掉以后又 exit 0,systemd 会认为这次服务正常结束。手动执行 systemctl stop 后不自动拉起也完全正常。
先看这一组信息:
bash
systemctl status demo-api
systemctl show demo-api -p Result -p ExecMainCode -p ExecMainStatus -p NRestarts
journalctl -u demo-api -b --no-pager -n 100
我一般先找两件事:最后一次退出的 code 和 status 是什么,日志里失败发生在 Java 进程里还是发生在启动命令本身。
重启策略生效了,也可能被限频拦住
服务连续崩溃时,systemd 会遵守 RestartSec= 的等待时间,但不会无限次地拉起。它还会按 StartLimitIntervalSec= 和 StartLimitBurst= 做启动限频。
看到 start-limit-hit,不要急着把阈值调大。那只是在让错误更快刷屏。先把导致崩溃的配置、依赖或端口问题修掉,之后再清理失败状态:
bash
sudo systemctl reset-failed demo-api
sudo systemctl restart demo-api
下面这份 unit 文件适合先把行为讲清,不是照抄到所有机器上的万能模板:
ini
[Unit]
Description=demo-api
StartLimitIntervalSec=60
StartLimitBurst=3
[Service]
User=app
WorkingDirectory=/srv/demo-api
Type=exec
ExecStart=/usr/bin/java -jar /srv/demo-api/app.jar
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target
这里的 60 秒和 3 次只是一个明确的保护边界。实际值要看服务启动成本、依赖恢复速度和报警承受能力。把 RestartSec=0 当成「恢复更快」往往不是好事,它会让崩溃循环更快撞上限频。
Type=exec 解决的不是应用启动失败
很多 unit 文件完全不写 Type=,默认是 simple。对于长期运行的 Java 进程,Type=exec 有个很实在的好处:可执行文件不存在、用户不存在这类 execve 之前的启动错误,systemd 能更可靠地记录为启动失败。
但别把它神化。JAR 已经被 Java 拉起来,应用随后因为数据库连不上而退出,这仍然要靠退出状态和日志来判断。它不会替 Spring Boot 判断「业务已就绪」。

面试里怎么收这题
被问「为什么不用 Restart=always」,我会先说清目标:守住异常退出,不干预运维的正常停止。
然后补一句,真正可靠的配置不只是一个 Restart。要同时确认退出状态能反映失败,给重启留出间隔,再让限频把持续崩溃挡住。这样面试官继续问服务为什么没拉起,才有具体的排查顺序,不会只剩一句「加个重启策略」。
资料核对:systemd.service(5)、systemd.unit(5)、Spring Boot Reference Documentation 的 Linux systemd service 示例。