问题背景
凌晨 02:40,生产机房一批跑核心 API 与内部网关的 Linux 物理机突然告警:load average 从日常 2~4 冲到 80 以上,监控 agent 间歇超时,跳板机 SSH 登录要等十几秒才出提示符。业务侧同步反馈:网关 502/504 大面积出现,部分接口偶发能通、大多直接拒连。
当晚刚做过一次「小变更」——网关上游的配置中心里,某台 API 机监听端口从 8080 改到 18080,并同步改了 systemd 的 unit 覆盖文件。变更窗口短、灰度一台后观测几分钟「进程有起来」,就扩到同角色其余节点。谁也没想到,配置写错 + Restart=always 无节制重试,会在十几分钟内把整机拖进「重启风暴」:服务永远起不来,CPU、磁盘 I/O、journal 却一起被打满,连排障通道都变慢。
这类故障的危险处在于:表象是「机器负载高、像被打爆」,根因却常常是管控平面(systemd)在合法地、疯狂地、毫无冷却地重拉一个必失败的 unit。
故障现象
-
业务侧
- 网关对多台后端健康检查连续失败,摘掉后上游仍探测「偶有成功」,流量在摘挂间抖动。
- 客户端日志里大量
connection refused与短连接超时并存,不像单纯的慢查询或连接池耗尽。
-
主机侧
top/htop里%sy与短生命周期进程刷屏,反复出现同名服务二进制一闪而过。load average1/5/15 分钟全线飙高,但单进程 CPU 并不持久占高——典型的「大量短命进程 + 上下文切换」。- 根分区写入猛增:
/var/log/journal或 rsyslog 落盘的服务日志在几分钟内暴涨数 GB。 systemctl status xxx.service显示activating (auto-restart),Active 在failed与重启间隙来回跳。
-
控制面侧
journalctl -u xxx.service -f几乎刷不完:Failed with result 'exit-code'、Scheduled restart job成对出现,间隔往往只有 100ms 量级。- SSH 可登但卡顿;
systemctl本身偶发变慢,因为 dbus/systemd 也在被重启风暴挤占。
-
时间线特征
- 告警与变更完成时间高度吻合(扩到全量后 5~15 分钟)。
- 回滚应用配置不一定立刻降负载——只要 unit 仍在 auto-restart,风暴就还在转。
排查过程
1. 先分清「被打」还是「自己打自己」
第一反应是 DDoS 或上游重试风暴。快速核对:
- 出口/入口网卡 PPS 与连接跟踪:有升高,但远不到历史攻击水位。
- 本机
ss -s/conntrack -C:未出现本地端口耗尽或 conntrack 打满那类经典网络型告警。 - 同网段未变更机器正常——故障与「刚改 unit 的角色」强相关,更像本机自激。
结论:优先查本机进程生命周期与 systemd 单元状态,而不是先扩容、先限流。
2. 锁定「刷屏」的单元
|
|
关键字段:
NRestarts已经是几千甚至上万。SubState=auto-restart。Result=exit-code或start-limit-hit(若已撞上限制;本批机器没配限制,所以只见无限 auto-restart)。
再配合:
|
|
能看到同二进制的大量僵尸/极短寿命子进程,父进程链最终回到 systemd。
3. 读清「为什么起不来」——而不是只看 Restart
服务日志里反复出现(示例):
|
|
本轮根因落地到 drop-in 覆盖写错:
|
|
变更本意:
|
|
实际写成了:
|
|
配置文件路径笔误 / YAML 非法后,进程启动即非 0 退出。应用本身没进入 listen 阶段,健康检查当然全红。
另有一台更隐蔽:端口改到 18080,但 旧进程未停干净 或 socket 单元仍占用,新进程 bind 失败,同样秒退。表现与「配置文件坏了」在 systemd 层完全一致——都是 start 失败 + 自动拉起。
4. 为何一台配置错会变成「整机打满」
核对 unit 默认策略:
|
|
现场读数(问题模式):
| 项 | 值 | 含义 |
|---|---|---|
| Restart | always | 任何退出都重启,含配置错误 |
| RestartSec | 100ms(或默认很短) | 失败后几乎立刻再拉 |
| StartLimitBurst | 0 / 未生效 | 没有滑动窗口熔断 |
| StartLimitAction | none | 触顶也不停 |
于是形成正反馈:
- ExecStart 必失败 → 进程秒退
- systemd 记一次失败并
auto-restart - 100ms 后再 start → 再失败
- 每次失败写 journal、写应用日志、fork/exec、安全模块检查……
- CPU、磁盘、日志子系统先于业务被拖死,排障变慢,窗口越拖越长
这与「应用逻辑死循环占 CPU」不同:这里是 PID 1 的策略在放大一次变更事故。
5. 与其他「负载高」故障的快速鉴别
| 表象 | 更像 | 一眼区别 |
|---|---|---|
| NRestarts 暴涨 + auto-restart | systemd 重启风暴 | systemctl show -p NRestarts |
| 单 Java 进程 CPU 100% | 代码/GC/死循环 | top 稳定同一 PID |
| Too many open files | fd 泄漏 | lsof//proc/pid/fd |
| 本地端口耗尽 | 短连接打满 | ss -s、EADDRNOTAVAIL |
| journal 涨、服务本身正常 | 日志级别/访问日志 | unit 稳定 active (running) |
本轮三项同时命中:NRestarts 疯狂涨、服务从未 stable running、journal 与变更同窗暴涨。
6. 变更链核实
- GitOps/配置仓库:override 与配置中心端口一致,但 主机上
systemctl daemon-reload后的实际 cat 与仓库有一行不可见字符/换行差异(或人工在机上「临时改一下」未回写)。 - 灰度判定失误:只看了
systemctl start返回和进程表「闪过」,没等 ActiveState=active (running) 且端口 listen,也没看NRestarts是否为 0。 - 监控缺项:有进程存活/端口拨测,但 没有「unit 重启次数」与「StartLimit 触顶」告警,风暴在业务大面积 5xx 前已转了几分钟。
解决方案
止血(按顺序,越前面越快降负载)
- 立刻停掉自动拉起(治标,但必须先做)
|
|
对已经 auto-restart 的风暴,stop 比继续改配置更重要——先让 systemd 停止 fork。
- 确认负载下降
|
|
- 修正配置后再受控启动
- 修正
override.conf/ 应用 YAML / 端口占用(ss -lntp | grep 18080)。 systemctl daemon-reloadsystemctl unmask api-gateway.service- 先单台
systemctl start,确认:
|
|
-
业务侧
- 网关把已恢复后端加回;对已雪崩的连接池/线程池视情况滚动重启下游客户端,避免「半开连接」长时间拖尾。
-
日志磁盘
- 若 journal 撑满根分区:按规范
journalctl --vacuum-size=或清理应用错误日志(先 stop 风暴再清理,否则边清边写)。
- 若 journal 撑满根分区:按规范
根治配置模板(写进角色 baseline)
|
|
要点:
Restart=always不等于高可用;配置错误、依赖未就绪时它是自攻击。优先on-failure,并对「正常退出码」语义定义清楚(SuccessExitStatus=)。RestartSec至少秒级,给依赖与排障留呼吸空间。StartLimitIntervalSec+StartLimitBurst是重启风暴的安全带,默认不配等于裸奔。- 变更后验收清单必须包含:
ActiveState、listen端口、NRestarts、健康检查连续成功 N 次——而不是「进程列表里看见过」。
根因分析
-
直接原因
unit / 应用配置错误(路径笔误、端口冲突、环境变量未生效)导致进程启动即退出,服务从未进入稳定running。 -
放大原因
Restart=always+ 极短RestartSec+ 无 StartLimit,使 systemd 以极高频率反复 fork/exec 同一失败路径,CPU、日志 I/O、磁盘被合法管控动作打满,形成「重启风暴」。 -
过程原因
- 灰度验收只看「有没有进程/命令是否返回」,不看 unit 稳态与重启计数。
- 监控缺少
systemd unit NRestarts/ActiveState!=active的及时告警。 - 变更把「端口/路径」与「重启策略」视为无关项,未评估失败路径下的控制面行为。
-
本质
高可用策略(自动拉起)在不可恢复的配置错误上没有熔断,把应用故障升级成主机级可用性事故。
预防措施
-
Unit 基线强制带熔断
所有生产 service 模板审查:Restart语义、RestartSec、StartLimit*、必要时CPUQuota/MemoryMax。禁止裸Restart=always无 StartLimit 上线。 -
变更验收 Checklist(发布门禁)
-
systemctl cat与仓库一致,daemon-reload已执行 -
ActiveState=active且持续 ≥ 1~2 分钟 - 端口
LISTEN与配置端口一致 -
NRestarts在观察窗内不增长 - 本地 + 经负载均衡的健康检查连续成功
- journal 无秒级
Scheduled restart job刷屏
-
-
监控与告警
- 采集
node_systemd_unit_state、systemd_unit_restarts_total(或等价 exporter)。 - 规则示例:5 分钟内重启次数 > 3 告警;
activating持续 > 1 分钟告警;journal 错误速率突增联动。
- 采集
-
发布策略
- 先改配置再 reload 的路径,串行单台,观察 Restart 指标后再下一批。
- 端口变更坚持「先起新端口旁路验证 → 再切流量 → 再停旧」,避免 bind 冲突秒退。
- 配置渲染进 CI:校验 YAML/端口区间/文件存在性,拒绝明显坏配置进仓库。
-
排障预案
- Runbook 第一条写清:怀疑重启风暴时 先
systemctl stop/mask停叉,再查配置。 - 演练:故意提交坏 ExecStart,验证 StartLimit 与告警是否在业务大面积 5xx 前触发。
- Runbook 第一条写清:怀疑重启风暴时 先
-
与日志系统协同
- 失败启动日志限速(应用侧)、journald rate limit 复核,避免风暴二次填满磁盘导致连环故障。
总结
这是一起典型的「管控平面放大配置错误」事故:应用其实已经「明确失败」,但 Restart=always 在无 StartLimit、短 RestartSec 下把失败变成每秒数十次的自攻击,主机 load、journal 与排障通道一起垮掉,业务表现为大面积 502 与间歇恢复的假象。
处理顺序应当是 停风暴 → 修配置 → 受控拉起 → 补熔断与监控,而不是在 auto-restart 刷屏时继续叠变更。预防上,把 StartLimit、重启语义、NRestarts 验收和 unit 状态告警写进基线,比再写一百行业务重试更管用。
一句话:自动重启是保险丝,不是永动机;该熔断时必须熔断,否则 systemd 会比攻击者更早打满你的机器。