Linux 服务自动化故障恢复方案

1次阅读

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

Linux 服务自动化故障恢复方案

systemd 服务崩溃后自动重启不生效?检查 RestartStartLimitIntervalSec

很多服务加了 Restart=always 却还是“挂一次就躺平”,根本原因是 systemd 默认启用了启动频率限制。它会在 StartLimitIntervalSec(默认 10 秒)内限制最多启动 StartLimitBurst(默认 5 次),超限后直接拒绝后续启动,且不会报错提示。

实操建议:

  • 确认服务单元文件中是否显式设置了 StartLimitIntervalSec=0(禁用限制)或调高数值,比如 StartLimitIntervalSec=60
  • Restart=on-failure 只对非 0 退出码生效,而进程被 SIGKILL 或 OOM killer 杀掉时退出码为 0,此时得用 Restart=alwaysRestart=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。

text=ZqhQzanResources