跳转至

陈旧(孤儿)告警回收器(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 汇总窗口长期被这些陈旧条目占满,真正的信号被稀释。

判定口径(保守,只碰「守卫自己拥有、且守卫已不再跟踪」的行)

候选必须同时满足:

  1. recovered_at IS NULL(还挂着);
  2. source = 'security'(守卫自己上报的)——绝不碰 prometheus/... 等由 Prometheus / Alertmanager 管理生命周期的告警(误回收会让它反复抖动);
  3. dedup_key LIKE 'nfos.sec:%'(守卫的键命名空间);
  4. 该键不在当前状态文件里(守卫已不再跟踪它);
  5. 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 是唯一的「真回收」开关)。它会:

  1. 打印 候选(孤儿)告警 = N 条;状态文件缺失的命名空间 = …;
  2. 逐条打印前 20 条的 level / age / owner / dedup_key / rule,超出部分打印 … 其余 N 条;
  3. 打印一行 JSON 汇总 {"ok":true,"candidates":N,"recovered":0,"failed":0,"grace":…,"missing_state":[…],"dry_run":true};
  4. 往 --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。