systemd服务崩溃后自动重启不生效,主因是默认启用启动频率限制(startlimitintervalsec=10s内最多启动5次),需显式设置startlimitintervalsec=0或调高数值,并确保restart=always等策略匹配退出场景。

systemd 服务崩溃后自动重启不生效?检查 Restart 和 StartLimitIntervalSec
很多服务加了 Restart=always 却还是“挂一次就躺平”,根本原因是 systemd 默认启用了启动频率限制。它会在 StartLimitIntervalSec(默认 10 秒)内限制最多启动 StartLimitBurst(默认 5 次),超限后直接拒绝后续启动,且不会报错提示。
实操建议:
- 确认服务单元文件中是否显式设置了
StartLimitIntervalSec=0(禁用限制)或调高数值,比如StartLimitIntervalSec=60 -
Restart=on-failure只对非 0 退出码生效,而进程被SIGKILL或 OOM killer 杀掉时退出码为 0,此时得用Restart=always或Restart=on-abort - 用
systemctl show <service-name> | grep -E "(Restart|StartLimit)"</service-name>查看实际生效值,别只信自己写的配置——systemctl daemon-reload忘了执行的话,改了也白改
进程活着但功能失效,HealthCheck 怎么写才靠谱?
单纯依赖进程存在(ps aux | grep myapp)做恢复判断,90% 的场景会漏掉“假活”:比如 Go 程序 panic 后 goroutine 全挂,但主进程还在;或 Python 服务卡死在某个锁上,http 端口开着却无响应。
实操建议:
- 健康检查必须走业务语义层,比如
curl -f http://localhost:8080/healthz,且返回 HTTP 200 + 非空 json{"status":"ok"},不能只看端口通不通 - 避免在
ExecStartPre里写复杂检查逻辑——它只在启动前跑一次;要用ExecStartPost或独立的 watchdog 脚本配合Restart=on-success - 检查命令必须带超时:
timeout 3s curl -f http://localhost:8080/healthz,否则一个卡住的curl会让整个 restart 流程阻塞
恢复动作要干净:别让旧进程残留变成僵尸
常见错误是写个 shell 脚本 kill 掉旧进程再 start,结果 kill -9 太暴力,子进程没回收,或者服务本身有守护模式(double-fork),systemctl stop 发送信号后主进程退出但子进程继续跑。
实操建议:
- 优先用
Type=notify+systemd-notify --ready,让服务主动告知 systemd “我准备好了”,这样systemctl restart才能精准等它 clean shutdown - 如果必须用
Type=simple,在ExecStop里明确指定killall -u <user><binary-name></binary-name></user>,并加KillMode=mixed,确保主进程退出时子进程也被带进棺材 - 检查残留:重启后立刻运行
ps auxf | grep <service-name></service-name>,看有没有同名但 PPID 不是 1 的进程——那是漏杀的孤儿
日志和触发边界必须可追溯
自动化恢复最怕“悄无声息地修好了又坏了”,因为没人知道它什么时候触发、依据什么判断、执行了哪些动作。等真出事,只能翻三天前的日志猜。
实操建议:
- 所有自定义 check 脚本必须输出到
journalctl -u <service-name></service-name>,用logger -t "myapp-watchdog"打点,别只写本地文件 - 在
ExecStartPost里加一行/bin/sh -c 'logger -t "$0" "restarted at $(date -Iseconds)"' %n,让每次重启都有时间戳锚点 - 不要把恢复逻辑塞进
RestartSec=5这种简单参数里——它只控制延迟,不记录原因。真正的问题定位靠的是“哪条健康检查失败了”“上次退出码是多少”“OOM Killer 是否介入过”
最麻烦的永远不是写个脚本能重启,而是当它第 7 次半夜重启时,你能三秒内从日志里定位到是磁盘满导致 DB 连接池耗尽,而不是又去重试一遍 systemctl restart。