从手动重启到自动守护:Systemd 的解决路径
在日常运维中,最让人头疼的场景之一就是某个关键进程毫无征兆地崩溃,或者更隐蔽的——进程还在,但陷入死循环、死锁或无限等待,对外无任何响应。传统的做法是写 crontab 定时检查 + kill + 启动脚本,但这种方式粗糙、延迟高,且容易在并发操作下产生 race condition。Systemd 作为现代 Linux 的主流 init 系统,早已内置了成熟的进程监控与自动重启机制。本文直接聚焦如何利用 Systemd 的单元文件,实现进程崩溃后的秒级自动拉起,以及针对进程挂死(hang)的无响应检测与重启。
Systemd 服务单元文件基础
在接触自动重启参数之前,需要先理解 Systemd 服务单元文件(.service)的基本结构。每个.services 文件通常包含 [Unit]、[Service]、[Install] 三个区块。[Service] 中的 Type= 字段决定了 Systemd 如何判断进程的启动完成与状态,这直接影响重启行为的触发条件。
Type 类型的选择
- simple:默认值。ExecStart 启动后即认为服务已运行,适合持续运行的前台进程。缺点是无法感知进程初始化是否完成。
- forking:父进程 fork 子进程后父进程退出,Systemd 等待子进程 PID 成为主进程。适用于传统 daemon(如 Nginx 的 master-worker 模式)。 li>oneshot:适用于执行一次就退出的任务(如初始化脚本)。
- notify:进程通过 sd_notify() 通知 Systemd 启动完成,配合 WatchdogSec= 实现心跳保活。
- dbus:等待 D-Bus 服务就绪。
对于大多数 Web 服务器、中间件和自定义后台进程,推荐使用 simple 或 notify。其中 notify 类型可以主动报告自己的存活状态,是处理挂死场景的最佳搭档。
自动重启核心配置参数
Systemd 重启行为由 [Service] 区块中的三个主要参数控制:Restart=、RestartSec=、StartLimitInterval= / StartLimitBurst=。理解它们的组合含义,才能避免“重启-崩溃-再重启”的死循环。
Restart= 设定重启条件
- no:不重启(默认)。
- on-success:仅当进程正常退出(exit code 0 或信号为 SIGHUP、SIGINT 等)时重启。
- on-failure:仅当进程非正常退出(exit code 非零或收到 SIGKILL、SIGSEGV 等)时重启。
- on-abnormal:当进程被信号终止、超时或看门狗触发时重启。
- on-watchdog:仅在看门狗超时(WatchdogSec=)时重启。
- on-abort:仅当进程因未捕获到信号异常中止时重启。
- always:无论何种退出原因(包括正常退出、用户手动 systemctl stop)都重启。通常不建议使用 always,以免手动停止服务后又被拉起。
实际生产推荐:对于需要高可用的后台服务,使用 Restart=on-failure;对于需要检测挂死的服务,结合 Restart=on-watchdog。
RestartSec= 重启延迟
单位秒,默认 0.1 秒。设置合理的延迟可避免崩溃后立即重启导致日志来不及收集,或者给文件锁的释放留出时间。一般建议设为 5-10 秒,例如 RestartSec=5。如果要让延迟递增(类似 backoff),可以配合 RestartSteps= 和 RestartMaxDelaySec=(Systemd v250+)实现指数退避。
StartLimitInterval= 与 StartLimitBurst= 防止重启风暴
当一个服务在短时间内反复崩溃,Systemd 需要限制重启次数。默认情况下,在 10 秒内启动失败超过 5 次,服务将进入 failed 状态,不再自动重启。可通过 StartLimitIntervalSec=(时间窗口,默认 10s)和 StartLimitBurst=(最多次数,默认 5)调整。注意这个限制是对所有启动尝试(包括首次启动和重启)的总计。如果希望禁用限制,可以设置 StartLimitIntervalSec=0,但这可能导致无限重启循环,应谨慎使用。
处理进程挂起(无响应)
进程崩溃可以被 Systemd 捕获(因为主进程退出),更棘手的是进程挂死——PID 还在,但不再处理任何请求(如内存泄漏后的 GC 停顿、死锁、无限循环)。Systemd 的 Watchdog 机制可以解决这一问题。
WatchdogSec= 与 Type=notify
工作流程:服务进程需调用 sd_notify("WATCHDOG=1") 定期向 Systemd 发送心跳。如果超过 WatchdogSec= 设定的秒数未收到心跳,Systemd 认为服务已无响应,默认先发送 SIGABRT 杀死进程(可通过 WatchdogSignal= 修改),然后根据 Restart= 策略决定是否重启。
配置示例:
[Service]nType=notifynWatchdogSec=30nRestart=on-watchdognRestartSec=10nStartLimitIntervalSec=60nStartLimitBurst=3n
注意:Type=modify 不是有效值,必须使用 Type=notify。应用代码如果使用标准库(如 Go 的 x/sdnotify、Python 的 systemd-daemon),只需在循环中每隔 WatchdogSec/2 时间调用一次 notify 函数即可。
无需修改代码的替代:Watchdog+Restart=on-failure 组合
如果无法修改源码,可以设置 Type=simple 配合 Restart=on-failure,再外挂一个监控脚本(如 ExecStopPost= 或单独的定时 service)来检测应用层响应。但这种方式本质仍靠进程退出触发,对挂死无效。一个折中方案是使用 systemd-run 临时启动一个 watchdog 服务,监控指定进程的文件描述符或 TCP 端口,但不如内置 watchdog 优雅。
完整配置示例:守护 Nginx 并检测工作进程挂死
以 Nginx 为例(虽然 Nginx 自带 master-worker 管理,但演示思路通用)。确保 Nginx 以 forking 模式运行,并设置合理的重启策略:
[Unit]nDescription=NGINX High Availability ServicenAfter=network.targetnn[Service]nType=forkingnPIDFile=/run/nginx.pidnExecStart=/usr/sbin/nginx -c /etc/nginx/nginx.confnExecReload=/usr/sbin/nginx -s reloadnExecStop=/usr/sbin/nginx -s quitnRestart=on-failurenRestartSec=10nStartLimitIntervalSec=30nStartLimitBurst=3nn[Install]nWantedBy=multi-user.targetn
这个配置会在 nginx master 退出(即使 worker 还在时),或者收到异常信号后 10 秒重启,最多每 30 秒内重启 3 次。对于 nginx worker 单独挂死的情况,由于 master 会监控 worker,一般不另处理。如果你的自定义服务没有内部监控,考虑使用 notify 模式。
自定义后台进程(假设 /usr/local/bin/myapp 支持 sd_notify):
[Service]nType=notifynExecStart=/usr/local/bin/myappnWatchdogSec=20nRestart=on-watchdognRestartSec=5nStartLimitIntervalSec=60nStartLimitBurst=5n
注意:Restart=on-watchdog 只会在 watchdog 超时后触发重启,如果进程主动退出(exit 0)反而不会重启。如果你希望同时处理崩溃和挂死,可以将 Restart=on-failure 配合 WatchdogSec=,但此时 watchdog 超时被视为 failure 的一种。
验证与回滚操作
单元文件生效与即时测试
- 编写或修改 .service 文件后,执行
systemctl daemon-reload使 Systemd 重新加载。 - 启动服务:
systemctl start your-service。 - 检查状态:
systemctl status your-service可见 “Active: active (running)” 及最近启动时间。 - 手动模拟崩溃:找到进程 PID,执行
kill -9 PID。观察systemctl status是否显示 “Active: activating (auto-restart)” 并在几秒后重新运行。 - 查看 journald 日志:
journalctl -u your-service -f能看到 “Unit entered failed state” 和 “started, now restarting” 记录。
回滚方案
如果新的重启策略导致服务频繁重启或引发其他问题,最快的回滚是还原之前的单元文件并执行 systemctl daemon-reload && systemctl restart your-service。建议在配置前备份原文件 (cp /etc/systemd/system/your-service.service{,.bak})。若不想立即恢复,可以临时禁用自动重启:systemctl stop your-service && systemctl disable your-service,待修复后再重新启用。
常见陷阱与风险
重启风暴导致系统资源耗尽
若错误配置 Restart=always 且 StartLimitIntervalSec=0,进程在每次启动后立即崩溃,CPU 和 IO 会飙升。务必保留 StartLimitBurst 的默认限制,或设置为合理的值。如果服务启动时需要加载大量数据(如缓存预热),频繁重启会加重磁盘压力。
依赖与顺序错误
自动重启的进程可能依赖其他服务(数据库、网络)。在 [Unit] 区块中正确添加 After= 和 Requires= 可确保重启时不会因依赖未就绪而反复失败。例如 Nginx 需要网络和日志服务,应写 After=network-online.target rsyslog.service。
watchdog 心跳丢失误判
如果进程执行的某些长时间操作(如复杂计算、大批量数据库查询)超过 WatchdogSec 但并未挂死,会导致服务被误杀。建议将 WatchdogSec 设置为正常最大响应时间的 3-5 倍,同时在应用代码的长操作前刷新心跳 (sd_notify("WATCHDOG=1"))。
手动 systemctl stop 后被错误重启
如果使用 Restart=always,你手动 systemctl stop 后 Systemd 会记录 stop 请求,但若进程被异步杀除,仍可能触发重启。解决方案是使用 Restart=on-failure 并避免 STOP 信号被误认为 failure。如果需要临时停止服务并防止重启,使用 systemctl stop 后立刻 systemctl mask your-service,或设置 ExecStopPost= 来清除状态。
结语
Systemd 的自动重启能力远不止一个简单的活检查机制。通过精细配置 Restart、WatchdogSec、StartLimitBurst 以及 Type 选择,可以构建从“进程退出恢复”到“无响应自愈”的完整防线。配置时始终牢记:先验证再上线,设定合理的启动限制,并保证依赖顺序正确。掌握这些参数,你的服务运维将更加稳健,告别深夜手动重启的噩梦。
延伸阅读
