systemd 的 StartLimitBurst:一个能让自愈服务彻底停摆的隐形开关
systemd 的 StartLimitBurst:一个能让"自愈服务"彻底停摆的隐形开关
背景
一个自建服务,配置了 Restart=always,本来指望它断了能自己爬起来。某天它真的断了——然后就再也没起来,一直卡在 failed,等了两小时也不动。手动 systemctl start 一秒就恢复了。
翻日志才看到一行关键提示:
Start request repeated too quickly.
这不是服务本身死了,而是 systemd 主动拒绝再拉起它。这个机制叫启动限流,由 StartLimitIntervalSec 和 StartLimitBurst 两个参数控制,默认值是 10 秒内最多启动 5 次。一旦超限,systemd 把服务标记为 failed 并停止重试——Restart=always 在这个状态下完全失效。
一、为什么这个设计会让自愈失效
启动限流的初衷是防止崩溃循环:如果一个服务一启动就崩,无限重启会把 CPU 吃满、把日志刷爆。这个防护本身是对的。
问题在于阈值和真实故障情景不匹配。设想这样一个场景:
RestartSec=65——每次重启间隔 65 秒,看起来挺克制StartLimitIntervalSec=900(15 分钟窗口)StartLimitBurst=4(窗口内最多 4 次)
如果有人把这个理解成"65 秒重试、15 分钟内重试 4 次",那是对的。但真正的坑在于判断条件用的是"启动次数",不是"失败原因"。只要底层依赖还没恢复,这 4 次机会耗尽得很快:
4 次 × 65 秒 ≈ 4 分多钟。之后服务就进入 failed,哪怕第 5 次尝试就能成功。
更糟的是,进入 failed 之后没有任何自动恢复路径。systemd 不会在某个时间点解冻它,必须有人执行 systemctl reset-failed 再 start。对于一个"希望无人值守自动恢复"的服务来说,等于自己给自己装了个定时炸弹。
二、怎么确认是这个原因
看三个地方,按这个顺序:
# 1. 服务状态与退出码
systemctl status my-service
# 2. 是否出现限流提示(关键)
journalctl -u my-service --since "2 hours ago" | grep -i "start request repeated"
# 3. 当前计数值
systemctl show my-service -p StartLimitIntervalUSec -p StartLimitBurst -p NRestarts
status 输出里如果出现 start request repeated too quickly,配合 Active: failed,基本可以定型。此时服务进程本身可能完全正常。
这里有个容易误判的地方:status 里显示的 “failed” 会让人去查服务代码、查配置文件、查依赖进程,但实际上真正的障碍在 systemd 的管理层,跟服务自己没关系。所以 journalctl 里那行 grep 是最快的分诊手段,值得每次排查先跑一遍。
三、正确配置长周期重试
如果确实需要"一直重试直到成功",配置要成对地改,不能只调 RestartSec。
方案 A:放宽窗口,让重试次数足够多
[Service]
Restart=always
RestartSec=30
[Unit]
StartLimitIntervalSec=0
StartLimitIntervalSec=0 表示关闭启动限流,服务会无限重试。适合那些依赖是"临时不可用、一定会恢复"的场景。代价是:如果服务真的代码有问题,它会一直重启下去,日志会很长——所以最好同时配 RateLimitIntervalSec 之类的日志限流,别让 journal 被刷爆。
方案 B:保留防护,但把窗口拉长
[Unit]
StartLimitIntervalSec=3600
StartLimitBurst=120
[Service]
Restart=always
RestartSec=30
一小时 120 次,相当于平均 30 秒一次且能容忍突发。既保留了崩溃循环的保护,又给了依赖足够的恢复时间。
方案 C:加一个外部兜底
配置再好也有极限。一个每 5 分钟跑一次的 timer,检查服务是否处于 failed 且不是"配置错误",是就 reset-failed 再 start:
# my-service-recover.timer
[Timer]
OnBootSec=5min
OnUnitActiveSec=5min
[Install]
WantedBy=timers.target
# my-service-recover.service
[Service]
Type=oneshot
ExecStart=/bin/sh -c 'systemctl is-failed my-service && systemctl reset-failed my-service && systemctl start my-service || true'
这是"保险的保险"。生产环境里,一个关键服务同时具备自动重启、合理限流和外部巡检,才算完整。
四、这次排查里更值得记的一条方法论
起因是上游返回 502。直觉会去查上游服务——但逐层往下走,发现真正断掉的是中间某一段 TCP 连接。
排查顺序是:
- 先看错误信息指向哪一跳。502 是"上游不可达",不等于"上游服务死了"。
- 分层验证。到底是本机到中间节点不通,还是中间节点到上游不通?在每一段两端各测一次,把范围收缩到最小。
- 找到断点后,先确认服务是否还活着。结果是:远端服务在跑、容器在跑、本地回环请求返回预期错误码——说明服务正常,问题在连接路径上。
- 确认是进程问题还是管理问题。发现目标进程的 systemd unit 处于 failed 状态、且提示
start request repeated too quickly——问题在管理层的限流,不在进程。
reset-failed 加 start 之后,一切恢复。
整个过程中最贵的一步是第 2 步:多花十分钟分辨"哪一段不通",能省掉后面所有瞎猜。反过来,如果一上来就重启上游服务,可能碰巧恢复,但根本原因还埋在那里,下次照样复现。
五、一个细节:验证要看到真实生成,不只看健康检查
恢复之后,健康检查返回 200 并不能证明整条链路都好了。健康检查往往只检查进程存活、端口监听,不碰真实业务路径。
有用的是发一次最小的、端到端的真实请求,并且检查:
- HTTP 状态码
- 首段响应的耗时(区分"能连上"和"能出结果")
- 返回内容是否与预期完全一致
- 结束原因字段(是否正常结束,而非截断)
这次是发一条固定内容的诊断请求,期望返回一个固定字符串,实际返回完全一致、end_turn、耗时 7.8 秒。这比任何 curl -I 都有说服力。
小结
Restart=always不是万能的:启动限流一旦触发,systemd 会彻底停止重试- 看到服务莫名 failed,先
journalctl | grep "Start request repeated" - 要无限重试就设
StartLimitIntervalSec=0,要保留防护就大幅放宽 - 关键服务应该有外部巡检兜底,不能只依赖 systemd 自己的重启
- 分层排查时,"哪一段不通"比"哪里报错"更重要
- 恢复后要发一次真实请求验证,健康检查不算
- 点赞
- 收藏
- 关注作者
评论(0)