陈旧(孤儿)告警回收器(alert-sweep.py)¶
仓库路径:
deploy/nfos/alert-sweep.py| 线上路径:/opt/Cy/services/system.d/nfos/alert-sweep.py语言:python3 | 定时器:nfos-alert-sweep.timer
用途¶
与守卫解耦的独立小工具,用来回收「守卫已不再跟踪」的陈旧告警。
守卫(security-guard.sh)用 state/security-guard.state 记着「我现在正在跟踪哪些 dedup_key」:本轮命中的键写进状态,不再命中的键调 /alerts/recover 恢复。问题出在状态文件曾经被重建 / 迁移的那些时刻——库里已经有告警行,但状态文件里没有对应键,守卫再也不知道要回收它们,这些行就永久留在 recovered_at IS NULL。
副作用不止「看着脏」:
- Prometheus 的「活动告警数超阈值」元告警永远无法恢复(活动数下不去);
- 12h 汇总窗口长期被这些陈旧条目占满,真正的信号被稀释。
判定口径(保守,只碰「守卫自己拥有、且守卫已不再跟踪」的行)¶
候选必须同时满足:
recovered_at IS NULL(还挂着);source = 'security'(守卫自己上报的)——绝不碰prometheus/...等由 Prometheus / Alertmanager 管理生命周期的告警(误回收会让它反复抖动);dedup_key LIKE 'nfos.sec:%'(守卫的键命名空间);- 该键不在当前状态文件里(守卫已不再跟踪它);
created_at早于now - grace(默认 600s,避免和「正在上报的这一轮」抢跑)。
状态文件格式按命名空间分开处理(守卫是 TSV、log-audit 是 JSON):
name |
前缀 | 状态文件 | kind |
|---|---|---|---|
security-guard |
nfos.sec: |
/opt/Cy/services/system.d/nfos/state/security-guard.state(NFOS_GUARD_STATE) |
tsv(key\tseverity\tts) |
log-audit |
nfos.log: |
/opt/Cy/services/system.d/nfos/state/log-audit.state.json(NFOS_LOGAUDIT_STATE) |
JSON |
只回收这里列出的命名空间,其它(尤其 prometheus/ Alertmanager 托管的)一律不碰。
何时用¶
- 不用手工跑:
nfos-alert-sweep.timer每小时:02、:17、:32、:47各一次(另有 30 秒随机延迟),ExecStart带--apply。 - 想先看候选、绝不动库:
--dry-run(默认行为;只打印候选与将发出的请求,绝不调 API)。 - 排查「活动告警数降不下来」:先
--dry-run看候选数;若候选为 0 而活动数仍高,说明根因不在孤儿条目(可能是「命中永不过期」,见docs/CHANGELOG-2026-09-26.md「§11.1 订正」)。 - 改判据之后:
--selftest(离线自检:临时库 + 假 transport,不碰网络)。 - 调整保守度:
--grace 3600(只回收 1 小时前创建的)、--max 50(单轮上限)。
命令与参数¶
以下参数逐字取自 main() 的 argparse 定义。
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
--db |
路径 | /opt/Cy/services/system.d/nfarmsd/data/nfarmsd.db(NFARMSD_DB) |
无 help |
--state |
路径 | /opt/Cy/services/system.d/nfos/state/security-guard.state(NFOS_GUARD_STATE) |
无 help。只覆盖「守卫那一条命名空间」的状态文件路径,log-audit 那条保持不变 |
--log |
路径 | /opt/Cy/services/logs/backend/nfos/alert-sweep.log(NFOS_SWEEP_LOG) |
无 help |
--api |
字符串 | http://127.0.0.1:9990(NFARMSD_API) |
无 help |
--grace |
float | 600.0 |
只回收创建时间早于 now-grace 的(秒) |
--max |
int | 200 |
单轮最多回收条数 |
--apply |
开关 | False |
真正回收(缺省为 dry-run) |
--dry-run |
开关 | False |
只打印(默认) |
--selftest |
开关 | False |
离线自检 |
共 9 个参数。
不带参数时的默认行为¶
只读、绝不写库、绝不调 API。 缺省即 dry-run(--apply 是唯一的「真回收」开关)。它会:
- 打印
候选(孤儿)告警 = N 条;状态文件缺失的命名空间 = …; - 逐条打印前 20 条的
level/age/owner/dedup_key/rule,超出部分打印… 其余 N 条; - 打印一行 JSON 汇总
{"ok":true,"candidates":N,"recovered":0,"failed":0,"grace":…,"missing_state":[…],"dry_run":true}; - 往
--log追加一条{"event":"alert-sweep",…}的 JSON Lines 记录。
missing_state非空是重要信号:说明某个命名空间的状态文件不存在,此时该命名空间的判据不成立(脚本对状态文件缺失的命名空间不会据此回收)。
示例¶
D=/opt/Cy/services/system.d/nfos
# 1) 离线自检(临时库 + 假 transport,不碰网络、不碰生产库)
python3 "$D/alert-sweep.py" --selftest
# 2) 只读查看候选(等价于线上默认行为;绝不写库、绝不调 API)
sudo python3 "$D/alert-sweep.py" --dry-run
# 3) 保守一档:只回收 1 小时前创建的、单轮最多 50 条
sudo python3 "$D/alert-sweep.py" --dry-run --grace 3600 --max 50
# 4) 真正回收(timer 的线上形式)
/usr/bin/python3 "$D/alert-sweep.py" --apply
# 5) 收窄到更保守的口径再真回收
sudo python3 "$D/alert-sweep.py" --apply --grace 3600 --max 50
# 6) 排查:看日志里最近几轮的候选/回收/失败计数
tail -n 20 /opt/Cy/services/logs/backend/nfos/alert-sweep.log
依赖与权限¶
| 项 | 内容 |
|---|---|
| 解释器 | python3 |
| 第三方模块 | 无(标准库 argparse / datetime / json / os / sqlite3 / sys / tempfile / urllib) |
| 外部命令 | 无 |
| root | 需要。timer 单元 User=root,注释写明「需要 root:只读打开 nfarmsd 库(mode=ro)+ 读守卫状态文件(640 root)」 |
sudo -n |
不需要(脚本自身不调 sudo) |
| 读取 | nfarmsd SQLite(alerts 表)、守卫状态文件 TSV(640 root)、state/log-audit.state.json |
| 写入 | --log 指定路径(JSON Lines 追加)。--apply 时不直接写库——它逐条 POST /alerts/recover 由 nfarmsd 更新 recovered_at |
| 网络 | 仅本机回环 HTTP:POST {api}/alerts/recover(带身份头 IDent_ID="nfos-alert-sweep" / IDENT_ROLES="service") |
| 幂等保障 | 回收是幂等的:已恢复的行(recovered_at 非空)不再进候选 |
幂等与失败语义¶
- 幂等:候选判据包含
recovered_at IS NULL,已恢复的行下一轮自然出局;重复执行--apply不会重复回收。 - 状态文件:本脚本自己没有。它的「判据来源」是别人的状态文件(守卫的 TSV / log-audit 的 JSON)与库里的
recovered_at。 - 一半状态的风险:有,且是设计取向——
--apply逐条回收,回收失败不中断其余条目,只在日志里记failed计数({"event":"alert-sweep.failed",…})。所以一轮可能「一部分成功了、一部分失败」,下一轮会对失败项重试。 - 状态文件缺失时的行为:
missing_state会把缺失的命名空间列出来;对该命名空间不建立「已不再跟踪」的判断,因此不会误回收。 - 退出码(docstring 说「0 = 正常(含「没有候选」);1 = 自检失败或参数错误」,实现如下):
| 退出码 | 含义 |
|---|---|
0 |
--selftest 全部 PASS;或 run() 返回(注意:main() 对 run() 的结果做了 0 if run(...) else 0 的处理,因此正常/--apply 路径恒为 0,即使有 failed > 0) |
1 |
--selftest 有 FAIL 项;或 argparse 参数错误(argparse 自身为 2) |
重要:判断「这一轮回收有没有问题」不能看退出码——要看 stdout 的
{"recovered":N,"failed":M}与日志里的alert-sweep.failed事件。
常见错误与排查¶
| 现象 | 代码里的对应分支 | 排查步骤 |
|---|---|---|
候选(孤儿)告警 = 0 条 但活动告警数居高不下 |
判据保守(只碰 source='security' + nfos.sec:% + 状态文件里没有的键) |
① 先看 missing_state 是否非空(状态文件不在 → 该命名空间不回收);② 若候选确为 0,根因不在孤儿条目:对照 docs/CHANGELOG-2026-09-26.md「§11.1 订正:25 条 high 一直 firing 不是『孤儿告警』,是『命中永不过期』」 |
状态文件缺失的命名空间 = ['security-guard'] |
candidates.last_missing |
不要急着让它回收:状态文件不存在时判据不成立。先确认守卫是否在正常写状态(systemctl status nfos-security.service) |
明明 --apply 了,recovered 却是 0 |
候选为 0(allc 为空)或全部被 --max 截断 |
先 --dry-run 看候选;候选为 0 就不会有 recovered。--max 默认 200 |
failed 大于 0 |
recover() 返回非 2xx 或非 "code":0 |
日志有 alert-sweep.failed(含 dedup_key / rule / result)。对照是 nfarmsd 未启动(systemctl status nfarmsd-api.service)还是 dedup_key 不匹配 |
| Prometheus 里「活动告警数超阈值」一直 firing | 见第一行 | 用 sudo python3 "$D/alert-sweep.py" --dry-run 拿候选数;再配合 check-pipeline.md 判断链路整体是否可信任 |
| 误回收了 Prometheus 托管的告警 | 不应发生(source = 'security' + nfos.sec:% 双重限制) |
若发生,说明判据被改坏。用 --selftest 复现,并检查 NAMESPACES 常量是否被加了 prometheus 前缀 |
--selftest 返回 1 |
selftest() 的逐项断言 |
它逐行打印 PASS / FAIL (got=… want=…) 与 alert-sweep selftest: N/M PASS;失败项就在输出里 |
回滚方式¶
本脚本不直接改库、不改任何文件(除往 --log 追加 JSON Lines)。它的「副作用」是经 nfarmsd 的 /alerts/recover 把告警置为已恢复。
| 改动面 | 还原方式 |
|---|---|
被回收的告警行(recovered_at 被置上) |
这是平台状态,没有一键逆向接口。要么按 dedup_key 让守卫/检测器重新上报(条件仍在时会自然重新开),要么经平台接口人工处理。先在 --dry-run 阶段把好关 |
--log 追加的 JSON Lines |
日志可滚动 / 可截断;无需「回滚」 |
| 脚本本体 | 用版本化备份覆盖回去 |
D=/opt/Cy/services/system.d/nfos
BK=$D/state/unit-backups
# A) 最有效的一步:停掉回收(因为回收本身不可逆)
sudo systemctl stop nfos-alert-sweep.timer
sudo systemctl disable nfos-alert-sweep.timer
# B) 回滚脚本本体
TS=$(date +%Y%m%d-%H%M%S)
sudo install -d -m 755 "$BK"
sudo cp -a "$D/alert-sweep.py" "$BK/alert-sweep.py.bak-$TS"
sudo cp -a "$BK/alert-sweep.py.bak-<时间戳>" "$D/alert-sweep.py"
sudo chown root:root "$D/alert-sweep.py"
sudo chmod 644 "$D/alert-sweep.py"
# C) 验收(离线,零副作用)
python3 "$D/alert-sweep.py" --selftest
sudo python3 "$D/alert-sweep.py" --dry-run
# D) 若确实误回收:条件仍在的告警会由守卫下一轮重新上报;
# 必要时人工核对后按规范重开(不要直接改库)
备份落点铁律:脚本/单元的任何备份放
/opt/Cy/services/system.d/nfos/state/unit-backups/(命名<文件名>.bak-YYYYMMDD-HHMMSS)或仓库侧/opt/Cy/nfarms/.backup/。 绝不能放在/etc/systemd/system/—— 会触发 PS-002 完整性告警。见 timers.md 第 11 节。
相关 CHANGELOG¶
| CHANGELOG | 章节标题 |
|---|---|
docs/CHANGELOG-2026-09-26.md |
§11.3 新增:陈旧(孤儿)告警回收器 deploy/nfos/alert-sweep.py |
docs/CHANGELOG-2026-09-26.md |
§11.1 订正:25 条 high 一直 firing 不是「孤儿告警」,是「命中永不过期」 |
docs/CHANGELOG-2026-09-26.md |
§11.2 修复:WA-001/WA-002 检测加「时间窗」(deploy/nfos/security-guard.sh) |
docs/CHANGELOG-2026-09-26.md |
§11.4 当前系统终态(2026-09-26 16:42 CST) |
docs/CHANGELOG-2026-09-26.md |
§13.6 最后一公里:日志审计的告警改「近期口径」(SL-004 不再复报) |
定时器节奏与日志落点见 timers.md 第 3 节;
nfos.sec:键的来源见 log-audit.md 与security-guard.sh。