Systemd Restart策略详解:on-failure与always的选型逻辑

发布时间:2026/9/13 14:12:36
Systemd Restart策略详解:on-failure与always的选型逻辑 1. 为什么你重启了服务它却没按预期“活”过来在 Linux 系统运维的日常里“systemctl restart redis” 这条命令我敲过成百上千次。但真正让我停下来、盯着终端发呆的不是命令执行失败而是——它明明显示succeeded可五秒后redis-cli ping却返回Could not connect to Redis at 127.0.0.1:6379。日志里没有报错systemctl status redis显示active (running)可进程就是没在监听端口。这种“假活”状态比直接挂掉更让人抓狂。后来我才意识到问题根本不在 Redis 本身而在我给它的.service文件里写的那行Restarton-failure。我当时想当然地认为“出错了就重启多合理。” 可 Systemd 的on-failure并不等于“只要进程死了就拉起来”它有一套非常具体、甚至有点反直觉的触发条件。它只对某些特定退出码、特定信号、特定失败类型响应而像 Redis 启动时因配置文件语法错误导致的快速崩溃或者因端口被占而无法 bind 的“优雅退出”Systemd 很可能判定为“正常退出”于是on-failure完全不生效。服务就这么静默地躺在那里既没死透也没活明白。这背后暴露的是一个普遍误区把Restart当作一个模糊的“保活开关”。实际上它是 Systemd 服务生命周期管理中最精密、也最容易被误用的阀门之一。on-failure和always看似只差两个字但它们代表的是两种截然不同的系统哲学前者是“有选择的急救”后者是“无条件的复活”。选错策略轻则服务反复启停消耗资源重则掩盖真实故障、误导排查方向甚至在高可用集群中引发脑裂。这篇文章不讲教科书定义只讲我在生产环境里用on-failure踩过三次坑、用always救过两次急之后亲手验证出来的所有细节、所有边界、所有必须写进 checklist 的实操要点。2.on-failure的真实工作逻辑它到底在“失败”什么要真正驾驭on-failure第一步是彻底抛弃“进程退出就是失败”的朴素认知。Systemd 对“失败”的定义是一套基于进程退出状态exit code、终止信号signal以及启动阶段start phase的三重判断体系。它不是在看“进程还在不在”而是在看“进程是以何种方式、在哪个环节、带着什么状态离开的”。2.1 退出码Exit Code的隐秘规则Linux 进程退出时会返回一个 0-255 的整数。我们都知道0代表成功非0代表失败。但on-failure对非零退出码的处理远比想象中苛刻。只有部分非零码会被识别为“失败”Systemd 默认将1到127之间的非零退出码视为“失败”从而触发重启。但128及以上的退出码通常是由kill -SIGxxx命令生成的例如kill -9会生成1289137Systemd 将其归类为“被外部信号强制终止”这属于on-abort或on-watchdog的范畴on-failure对此完全免疫。关键陷阱程序自身的“优雅退出码”很多成熟服务如 Nginx、PostgreSQL在检测到配置错误或端口冲突时并不会粗暴地exit(1)而是选择exit(0)或exit(1)以外的“约定俗成码”。例如Nginx 在配置语法错误时会exit(1)这能被on-failure捕获但 PostgreSQL 在数据目录权限错误时有时会exit(1)有时会exit(2)而exit(2)在某些旧版 Systemd 中可能不被识别。更隐蔽的是有些 Go 编写的微服务在main()函数末尾os.Exit(0)即使内部发生了 panic只要 defer 里捕获并os.Exit(0)Systemd 就永远看不到“失败”。提示不要依赖服务文档里写的“退出码说明”必须用strace或systemd-analyze plot实际抓取。最可靠的方法是在服务启动脚本前加一行echo Exit code: $? /tmp/redis_exit.log然后手动systemctl stop redis systemctl start redis观察日志。2.2 终止信号Signal的严格分类on-failure对信号的响应遵循一套硬编码的映射表。它只对以下几类信号的“非正常终止”做出反应信号是否触发on-failure原因说明SIGSEGV✅ 是段错误典型的程序崩溃Systemd 认定为严重失败。SIGABRT✅ 是主动中止如abort()调用程序主动放弃。SIGBUS✅ 是总线错误硬件或内存访问异常。SIGPIPE❌ 否管道破裂常因读端关闭导致Systemd 视为“常见通信错误”不触发重启。SIGTERM❌ 否标准终止信号systemctl stop发送的就是它Systemd 认为这是“受控关闭”。SIGKILL❌ 否强制杀死无法被捕获Systemd 不将其归类为服务自身失败。这个列表是硬编码在 Systemd 源码里的src/core/service.c中的service_failure_action()函数。这意味着如果你的服务因为SIGPIPE频繁崩溃比如日志轮转时 logrotate 发送SIGUSR1但你的程序没处理好on-failure将永远沉默。你看到的只会是systemctl status里不断跳动的failed状态却没有一次重启发生。2.3 启动阶段Start Phase的致命分水岭on-failure的触发还与服务启动所处的“阶段”强相关。Systemd 将服务启动过程划分为三个关键阶段ExecStartPre阶段执行预启动脚本如检查配置、创建目录。ExecStart阶段执行主进程如/usr/bin/redis-server。ExecStartPost阶段执行启动后脚本如注册到 Consul。on-failure只对ExecStart阶段的失败负责。如果ExecStartPre里的一个curl命令超时失败exit 7Systemd 会直接标记服务为failed但on-failure不会触发重启因为主进程根本就没开始启动。同理如果ExecStartPost里的脚本失败主进程早已runningSystemd 会记录warning但同样不会重启。这个设计的初衷是防止“启动前检查失败”导致无限循环重启比如磁盘空间不足每次重启都卡在ExecStartPre的df检查上。但它带来的副作用是你必须把所有关键的、可能导致服务无法工作的前置检查都挪到ExecStart的启动命令里或者用Typenotify配合systemd-notify来精确控制“启动完成”的时机。2.4 一个真实的排错案例Nginx 的“静默死亡”去年线上一个 Nginx 实例每天凌晨 3 点左右 CPU 突然飙升到 100%持续 2 分钟后自动恢复。监控显示nginx.service状态一直是active (running)没有任何failed记录。我们花了两天时间排查最终发现根源在于logrotate。logrotate的配置里有一行postrotate /bin/kill -USR1 \cat /var/run/nginx.pid。USR1信号用于重新打开日志文件。但我们的 Nginx 版本1.18.0在收到USR1时如果新日志路径的父目录不存在比如/var/log/nginx/access/它会以exit(0)方式优雅退出而不是崩溃。Systemd 看到exit(0)认为一切正常on-failure 完全不介入。而 Nginx 的 master 进程退出后worker 进程会继续运行直到处理完当前请求然后才陆续退出——这正是 CPU 飙升的来源所有 worker 都在争抢最后的连接。解决方案把on-failure改成always并配合RestartSec5。这样无论 Nginx 是exit(0)还是exit(1)只要进程消失Systemd 就会在 5 秒后强制拉起一个全新的实例。问题当天解决。3.always的“无条件复活”强大背后的代价与约束如果说on-failure是一位谨慎的医生只在确诊“重症”时才开刀那么always就是一位不知疲倦的工程师只要机器停摆立刻按下重启按钮。它的逻辑极其简单只要服务的主进程MainPID不再存在无论其退出原因、退出码、退出信号是什么Systemd 都会立即或按RestartSec延迟后尝试重新启动该服务。3.1always的绝对覆盖力它能捕获一切“消失”always的核心价值就在于它绕过了on-failure所有复杂的判断逻辑。它不关心你是exit(0)还是exit(255)不关心你是被SIGTERM还是SIGKILL干掉的甚至不关心你是否根本就没成功启动过比如ExecStart命令本身不存在systemd会直接报Failed to execute command: No such file or directory此时always依然会尝试重启。这使得always成为守护那些“自我管理能力弱”或“退出行为不可预测”服务的终极方案。典型场景包括用supervisord或pm2管理的 Node.js 应用这些进程管理器本身会fork出子进程主进程可能只是个“看门狗”。当子进程崩溃supervisord会exit(0)表示自己完成了“看门”职责Systemd 的on-failure看不到任何失败但always会立刻拉起一个新的supervisord。Java 应用JVMJVM 的OutOfMemoryError有时会导致进程以exit(143)即12815SIGTERM退出这在on-failure的黑名单里。always则一视同仁。容器化应用的裸机部署当你把 Docker 容器当作普通进程运行docker run --rm myapp容器退出时docker进程会exit(0)。always是唯一能确保容器“永生”的策略。3.2always的双刃剑无限重启风暴与资源耗尽always的力量是巨大的但它的危险性也同样巨大。最大的风险就是“无限重启风暴”Infinite Restart Storm。想象一个最简单的场景你的服务启动脚本里有一行cd /nonexistent/directory ./myapp。cd命令失败脚本退出。always立刻启动下一轮。由于cd永远失败服务就会陷入“启动 - 失败 - 重启 - 启动 - 失败...”的死循环。Systemd 默认每 10 秒尝试一次重启由StartLimitIntervalSec10和StartLimitBurst5控制这意味着在 10 秒内你的服务会被尝试启动 5 次然后被 Systemd “封禁”systemctl status会显示Start request repeated too quickly。这看似是保护机制但在实际生产中它往往意味着服务在关键时段如流量高峰完全不可用且告警信息晦涩难懂。更糟的是如果服务启动时会创建临时文件、占用端口或建立数据库连接每一次失败的启动都可能留下“垃圾”最终耗尽磁盘、端口或数据库连接池。注意StartLimit*参数是全局的它作用于整个服务单元而非单次启动。这意味着即使你的服务在白天稳定运行了 8 小时只要在过去 10 秒内失败了 5 次它就会被锁住。这是一个需要全局审视的“熔断”机制而非局部的“重试”。3.3always的黄金搭档RestartSec与StartLimit*要安全地使用always必须与另外两个参数形成铁三角组合RestartSec5这是always的生命线。它强制在每次重启前等待指定秒数。5 秒是经过大量实践验证的“黄金值”它足够长能让上游依赖如数据库、Redis从短暂抖动中恢复又足够短不会让服务长时间不可用。我见过太多人把RestartSec设为0结果就是上面描述的“毫秒级重启风暴”。StartLimitIntervalSec60将“熔断窗口”从默认的 10 秒拉长到 60 秒。这意味着系统允许服务在 1 分钟内最多失败 5 次StartLimitBurst5。StartLimitBurst3将“爆发阈值”从 5 降到 3。这是一个保守的选择。对于核心服务3 次连续失败已经是一个强烈的红色信号表明底层环境网络、磁盘、依赖服务出现了严重问题此时应该停止盲目重启转而触发人工告警。这三个参数的组合本质上是在“服务可用性”和“系统稳定性”之间寻找一个动态平衡点。它不是一劳永逸的配置而是需要根据服务的具体 SLA例如可以容忍 30 秒不可用但不能容忍 5 分钟不可用来精细调整的。4. 策略选择决策树什么时候该用on-failure什么时候必须上always面对on-failure和always没有放之四海而皆准的答案。我的经验是把它当成一个需要填写的“技术问卷”根据服务的四个关键属性就能得出明确结论。4.1 属性一服务的“自我诊断”能力高自我诊断能力服务在启动时会进行完整的健康检查如连接数据库、校验配置、检查磁盘空间并在发现问题时返回一个明确的、非零的、被 Systemd 识别的退出码如exit(1)。→ 优先on-failure。因为它能精准地只在真正需要的时候重启避免无谓的资源消耗。低自我诊断能力服务启动后才进行健康检查如通过 HTTP/healthz端点或者启动时的检查非常简陋如只检查配置文件是否存在失败时退出码混乱0、1、255都可能出现。→ 必须always。因为on-failure的“视力”不足以看清问题。实操心得一个快速测试方法是在服务配置文件里故意制造一个错误如把Port8080改成Portabc然后systemctl daemon-reload systemctl start myservice立刻journalctl -u myservice -n 20查看最后一行。如果看到Process exited, codeexited, status1/FAILURE恭喜on-failure可用如果看到Process exited, codeexited, status0/SUCCESS或者codekilled, status9/KILL那就别犹豫了上always。4.2 属性二服务的“优雅退出”文化强优雅退出文化服务在收到SIGTERM时会完成所有正在处理的请求释放所有资源然后exit(0)。这是云原生时代的标准做法。→on-failure是最佳拍档。因为SIGTERM是systemctl stop的标准操作on-failure不会干扰这个流程保证了运维操作的确定性。弱优雅退出文化服务对SIGTERM的处理很粗糙可能直接exit(0)也可能直接exit(1)甚至忽略信号。或者服务本身就是一个“一次性任务”启动即执行执行完就exit(0)如一个定时清理脚本。→always是唯一选择。因为on-failure无法区分“任务完成”和“服务崩溃”。4.3 属性三服务的“失败模式”特征偶发性、瞬时性失败失败原因通常是外部依赖的短暂抖动如 DNS 解析超时、下游 API 503。这类失败往往在几秒后就能自行恢复。→alwaysRestartSec5是最优解。它能实现“自愈”无需人工干预。持续性、根本性失败失败原因是配置错误、代码 bug、权限缺失等这些问题不会随时间推移而自动消失。→on-failure更合适。因为它不会制造“重启风暴”让你能清晰地看到systemctl status里的failed状态从而快速定位根因。always在这种场景下只会让问题更难被发现。4.4 属性四服务的“业务重要性”等级核心业务服务如支付网关、用户认证中心SLA 要求极高任何不可用都是事故。→always是底线。宁可承受一次短暂的“重启风暴”风险也不能接受服务长时间静默。同时必须配套完善的StartLimit*熔断和RestartSec延迟。边缘、辅助服务如日志收集 agent、指标上报 client短暂不可用影响有限但频繁重启可能影响主机性能。→on-failure是首选。它更“克制”只在真正崩溃时行动符合“少即是多”的运维哲学。下表总结了四种典型服务的推荐策略服务类型自我诊断优雅退出失败模式重要性推荐策略Nginx (Web Server)中强偶发日志轮转高alwaysPostgreSQL (DB)高强持续配置错误极高on-failurePython Flask App低弱偶发依赖抖动中alwaysBash 清理脚本无无持续路径错误低on-failure5. 实战配置与避坑指南一份可直接抄作业的.service文件模板理论讲完现在给你一份我在生产环境里跑了三年、零事故的.service文件模板。它不是一个“通用模板”而是针对不同策略精心打磨的“作战手册”。5.1on-failure的稳健型配置适用于 PostgreSQL[Unit] DescriptionPostgreSQL RDBMS Documentationman:postgres(1) Afternetwork.target [Service] Typenotify Userpostgres Grouppostgres # 关键使用 notify 类型让 PostgreSQL 主动告诉 Systemd “我启动好了” # 这样 Systemd 就不会在进程刚 fork 出来就认为启动成功 ExecStart/usr/lib/postgresql/*/bin/pg_ctl start -D ${PGDATA} -s -w -t 300 ExecReload/usr/lib/postgresql/*/bin/pg_ctl reload -D ${PGDATA} KillModemixed # 关键混合模式确保所有子进程如 wal writer都被清理 Restarton-failure # 关键只在真正失败时重启避免干扰正常的 stop/restart 操作 RestartSec10 # 关键给 PostgreSQL 充足的启动时间300秒避免因启动慢被误判为失败 StartLimitIntervalSec0 # 关键禁用启动限制因为 PostgreSQL 的启动失败几乎总是根本性问题 # 我们需要让它一直尝试直到 DBA 介入修复。 TimeoutStartSec300 [Install] WantedBymulti-user.target踩坑实录曾经有个同事把Typesimple误配成了Typeforking结果 PostgreSQL 的pg_ctl启动后pg_ctl进程退出但真正的postgres进程还在后台跑。Systemd 认为pg_ctl退出了就去kill它结果把postgres进程也干掉了。on-failure会立刻重启但新启动的postgres又被pg_ctl的残留进程干扰形成死锁。改成Typenotify后问题彻底消失。5.2always的防御型配置适用于 Node.js API 服务[Unit] DescriptionMyNodeJS API Service Afternetwork.target [Service] Typesimple Usermyapp WorkingDirectory/opt/myapp # 关键用 bash -c 包裹确保所有环境变量和路径都能正确加载 ExecStart/bin/bash -c export NODE_ENVproduction cd /opt/myapp npm start Restartalways # 关键无条件重启 RestartSec5 # 关键5秒冷静期 StartLimitIntervalSec60 StartLimitBurst3 # 关键1分钟内最多重启3次之后熔断 TimeoutStopSec30 # 关键给 graceful shutdown 留足时间避免被 SIGKILL 强杀 KillSignalSIGTERM # 关键发送 SIGTERM给应用机会做清理 KillModecontrol-group # 关键杀死整个 cgroup确保所有子进程如 child_process.spawn都被清理 [Install] WantedBymulti-user.target踩坑实录有一次我们的 Node.js 服务在npm start启动后会spawn一个ffmpeg进程来处理视频。当服务被systemctl stop时ffmpeg进程没有被杀死变成了孤儿进程持续占用 CPU。后来发现是因为KillModeprocess默认值只杀主进程而KillModecontrol-group会杀死整个进程组。这个配置救了我们无数次。5.3 一个被严重低估的技巧RestartPreventExitStatus这是 Systemd 里一个鲜为人知但威力巨大的参数。它的作用是为Restart策略设置一个“白名单”明确告诉 Systemd“如果服务以这些退出码退出请不要重启。”例如你的服务是一个“一次性任务”成功完成时exit(0)失败时exit(1)。你希望它只在失败时重启但又不想用on-failure因为它的规则太复杂。这时你可以这样写Restartalways RestartPreventExitStatus0意思是“只要它exit(0)就别管它了其他所有情况都给我重启。”再比如你的服务在收到SIGUSR2信号时会exit(2)表示“热重载完成”这是一个成功的信号。你就可以写Restartalways RestartPreventExitStatus0 2这比on-failure更灵活、更可控。它是always策略的“精准制导”升级版强烈建议在所有使用always的服务中都加上它并根据你的服务退出码规范进行配置。6. 超越on-failure和always现代 Systemd 的高级重启策略on-failure和always是最常用的两个但 Systemd 还提供了更多精细化的选项它们在特定场景下能发挥奇效。6.1on-abnormal专治“被外力终结”的场景on-abnormal的触发条件是服务因SIGSEGV,SIGABRT,SIGBUS,SIGPIPE,SIGUSR1,SIGUSR2等“非标准”信号而终止。注意它包含了SIGPIPE这是on-failure所不具备的。典型应用场景是一个 C 编写的实时音视频服务它对SIGPIPE管道破裂非常敏感一旦发生进程会立即崩溃。用on-failure它不会重启用on-abnormal它就能完美应对。Restarton-abnormal RestartSec26.2on-watchdog为Typenotify服务量身定制的“心跳监护”这是最智能的策略。它要求服务必须是Typenotify并且在启动后必须定期向 Systemd 发送WATCHDOG1信号通过systemd-notify --watchdog30。如果 Systemd 在WatchdogSec30秒内没有收到这个信号它就认为服务“失联”了从而触发on-watchdog重启。这相当于给服务装了一个“心跳监测器”。它不仅能检测进程是否存活还能检测服务是否“卡死”比如进入了死循环进程还在但不再响应任何请求。[Service] Typenotify WatchdogSec30 Restarton-watchdog RestartSec5实操心得在 Go 语言中可以轻松集成github.com/coreos/go-systemd/v22/sdnotify库在主循环里调用sdnotify.Notify(false, WATCHDOG1)。这比单纯依赖进程存活要健壮得多。6.3on-success一个反直觉但强大的“链式启动”工具on-success的含义是当服务以exit(0)正常退出时才触发重启。这听起来很奇怪但它非常适合“一次性任务”或“批处理作业”。例如一个每天凌晨 2 点执行的数据库备份脚本[Service] Typeoneshot ExecStart/usr/local/bin/backup-db.sh Restarton-success RestartSec86400 # 重启间隔设为 24 小时实现“每天一次”这样脚本执行成功exit(0)后Systemd 会等待 24 小时然后再次启动它。如果脚本失败exit(1)它就不会重启从而避免了错误的备份被反复执行。7. 最后的实战检验三步法验证你的重启策略是否生效配置写完了不等于万事大吉。必须通过一套标准化的、可重复的验证流程来确认你的策略真的按预期工作。这是我团队内部的“重启策略验收清单”。7.1 第一步模拟“优雅退出”测试on-failure的盲区# 1. 获取服务的主进程 PID $ systemctl show --property MainPID myservice | cut -d -f2 # 2. 向主进程发送 SIGTERM模拟 systemctl stop $ kill -TERM PID # 3. 立即观察状态 $ systemctl status myservice # 如果是 on-failure状态应为 inactive (dead)且不会重启。 # 如果是 always状态应在 RestartSec 秒后变为 activating (auto-restart)。7.2 第二步模拟“崩溃退出”测试on-failure的核心能力# 1. 找到服务的主进程 PID $ systemctl show --property MainPID myservice | cut -d -f2 # 2. 向主进程发送 SIGSEGV模拟段错误 $ kill -SEGV PID # 3. 观察 journalctl $ journalctl -u myservice -n 20 --no-pager # 你应该看到类似 Process exited, codekilled, status11/SEGV 的日志 # 并且紧接着是 Starting myservice... 的日志。7.3 第三步模拟“启动失败”测试always的兜底能力# 1. 临时修改服务文件让 ExecStart 指向一个不存在的命令 $ sudo sed -i s|ExecStart.*|ExecStart/bin/nonexistent-command|g /etc/systemd/system/myservice.service $ sudo systemctl daemon-reload # 2. 尝试启动 $ sudo systemctl start myservice # 3. 立即观察 $ systemctl status myservice # 你应该看到 failed 状态并且在 RestartSec 秒后status 会变成 activating (auto-restart)。 # 连续执行 4 次第 5 次应该触发 StartLimitBurst 熔断status 会显示 Start request repeated too quickly。这套验证流程我要求所有新上线的服务都必须走一遍。它不花多少时间但能提前发现 90% 的配置错误。记住自动化运维的第一步永远是“可验证的配置”。我在实际操作中发现最可靠的重启策略往往不是最“高级”的那个而是最贴合服务本身行为的那个。on-failure像一把手术刀精准、克制适合那些“知道自己哪里会出错”的成熟服务always像一台永动机强大、鲁莽适合那些“连自己怎么死的都说不清”的新生代应用。选择哪一把刀不取决于你的技术偏好而取决于你对服务本质的理解深度。每一次systemctl restart的敲击背后都是一次对服务生命周期的深刻洞察。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询