面试知识库

V4 Layer: L2 预处理 + 拓扑级联关联#

目录: app/preprocessing/, app/topology/

上游: L1 接入层(NormalizedAlert) → 下游: L3 调查引擎(IncidentContext)

参考: Alertmanager group_wait/group_interval/inhibit_rules;BigPanda/Moogsoft 告警关联;PagerDuty Intelligent Alert Grouping

2.1 设计立场:纯规则,不进 LLM#

L2 的所有操作都是确定性规则,零 LLM 调用。这不是”还没做到用 LLM”,而是刻意的架构选择

  • 对百炼 RPM/TPM 限流的第一道闸:越多告警在 L2 被消化,越少 LLM 出站调用。
  • 优雅降级的基石:LLM 完全不可用时,L2 仍然工作——系统从”智能诊断”退化为”确定性降噪 + 指纹匹配自愈”,而不是整体瘫痪。
  • 可测试性:纯规则可以用 unit test 覆盖每一条路径,不存在 LLM 输出的非确定性。

2.2 五步处理链路#

NormalizedAlert

   ├─ 1. 去重(fingerprint + 时间窗)──► 重复 → 直接丢弃,不入队

   ├─ 2. 抑制(inhibit_rules)──► 被抑制 → 标记 suppressed

   ├─ 3. 时间窗聚合(同 service + N 秒)──► 归入已有 Incident 或新建

   ├─ 4. 抖动检测(firing↔resolved 翻转频率)──► 抖动 → impact 折减

   └─ 5. 拓扑关联 + Impact 评分 ──► 产出 IncidentContext
plaintext

去重#

fingerprint = hash(labels + startsAt),Redis SET NX + dedup_window_sec(默认 600 秒)。同 fingerprint 的告警在窗口内视为重复,不入队——这是降噪率指标的第一个来源。

抑制#

对标 Alertmanager 的 inhibit_rules:当高优先级告警(source_match)存在时,抑制低优先级告警(target_match),要求 equal 列表里的 label 值相同。自抑制护栏:source 和 target 匹配到同一条告警时跳过。

时间窗聚合#

首条告警后等待 group_wait_sec(默认 30 秒),同 service 的后续告警归入同一个 Incident。同一 group 两次推送的最小间隔 group_interval_sec(默认 300 秒)。

抖动检测#

flapping_window_secfiring → resolved 翻转次数 ≥ flapping_threshold(默认 5)→ 标记 is_flapping = True,Impact 评分折减(抖动告警不应该抬高影响面)。

Impact 评分#

class ImpactScore(BaseModel):
    score: float = Field(ge=0.0, le=1.0)   # 0=低影响, 1=高影响
    factors: dict[str, float]               # severity_weight, service_criticality,
                                            # alert_count_weight, is_flapping_penalty,
                                            # blast_radius (跨域则高)
python

Impact 只调门槛不改结构:影响面越大,Tier 1/2 的置信门槛越高——极端情况强制走 Tier 3 复核。影响面越大,越不敢便宜地停。

2.3 拓扑级联关联#

为什么不只做标签匹配#

标签匹配(同 service / 同 namespace)只能发现”症状聚合”,发现不了”因果关联”。举例:MySQL 主从切换导致上游 web-api 502,两者 service 不同、namespace 不同,标签匹配不到。要做因果关联必须有拓扑图。

typed 资源图#

# app/topology/graph.py
# 两种边类型,方向统一指向"因"
EdgeType.DEPENDS_ON  # 调用关系:web-api depends_on mysql
EdgeType.RUNS_ON     # 承载关系:mysql runs_on host-1
python

旧设计只有 depends_on 一种边,是把 K8s/微服务世界观搬过来了。小企业 90% 的级联是垂直的(磁盘满 → MySQL 拒写 → 业务 502),runs_on 承载边正是为此设计。

same_node 这种”共因”关系不再是独立边类型——被物化成合成主机节点:service_a runs_on host-1, service_b runs_on host-1

群体级操作#

不是逐条告警做拓扑关联,而是窗口内所有 firing 节点一起算:

  1. Tarjan SCC 缩点:环里分不出先后,报成一个整体
  2. 多跳反向可达:从告警节点沿边反向遍历,置信度衰减 Π边置信度 × decay^跳数
  3. 因果时序门:上游晚于下游超过 causality_skew_sec → 不认它是因(“MySQL 在 web-api 挂了三分钟后才报警”——它多半是被打死的受害者)
  4. 层级裁决:同分时基础设施 > 中间件 > 应用 > 业务指标

静默根因#

根因节点常常自己不告警(那块盘没配监控)。旧算法只在”正在告警的节点”里挑根因,这类故障必然挑错。

新增静默根因判定:在残差集(互相独立的直接根因)上找共同祖先,两个指标同时达标才成立:

  • explains = |残差 ∩ 辖区| / |残差|:它解释了多少个本该互相独立的根因
  • precision = |告警 ∩ 辖区| / |辖区|:它的辖区烧得有多彻底

残差集是关键——用全部告警的话,“根因所在的那台主机”永远抢走根因位。两个分子刻意不同口径:只有 explains 会让”整个机房”永远满分;只有 precision 会让任何叶子的父节点都是 1.0。

拓扑数据从哪来#

来源方式人工成本
compose / nginx provider从 docker-compose.yml / nginx.conf 解析 depends_on/links/DB_HOST/upstream
sniffss -tnp 连接表自动发现零(无侵入)
mined从告警历史挖共现(条件概率 + lift)零(自动)
手写 YAML补充推不出来的东西(主机/分区/DNS/证书/第三方 API)

自动发现的边落 topology_edges 表,带 observations/last_seen_at/TTL——服务下线后边会自己过期消失,静态拓扑腐烂的问题由此自愈。

降噪的三道锁#

拓扑级联抑制会吞告警,所以三道锁:

  1. topology_cascade_suppress 默认关闭
  2. 归因链上每条边必须 status=active(人审过的)
  3. 静默根因永不触发抑制——没人会去调查一个没有告警的节点

身份解析#

同一个 MySQL 在不同告警里可能是 service=mysql-master / instance=10.0.0.5:9104 / container_name=prod-mysql-1resolver.py 负责跨命名空间的身份对齐。解析不到就返回 None,让该告警退化成”自己是根因”,绝不猜——猜错的拓扑归因比没有拓扑归因更有害。

模拟面试问答#

🔥 热点拷问#

面试官:你说 L2 是纯规则不进 LLM,但告警语义理解不需要 NLP 吗?比如两条告警描述的是同一件事但用词不同——“disk full” 和 “no space left on device”,纯规则怎么聚合?

不靠 NLP,靠 Alertmanager 的标签体系。运维告警不是自然语言——它是结构化的 {alertname, service, severity, instance} 标签集。“disk full” 和 “no space left on device” 如果 alertname 不同就是不同的告警,聚合靠 service + 时间窗,不靠文本相似度。这是运维告警和工单系统的根本区别:告警天然有标签,工单只有自然语言描述。如果真要做文本级语义聚合,那应该在告警源(Prometheus rules)层面统一 alertname,而不是在接收端用 LLM 做。

追问:那你的拓扑关联算法,Tarjan 缩点 + 多跳反向可达 + 因果时序门——这一套对于”本地跑的小项目”来说是不是过度设计?你有几个服务?

目前自动发现的拓扑约 10-15 个服务节点。单看规模确实用不上 Tarjan 缩点——10 个节点不可能有需要缩点的 SCC。但设计的出发点不是当前规模,而是正确性:即使只有两个节点形成环(A depends_on B, B depends_on A,这在现实中很常见——比如服务互相健康检查),不做缩点就会在”谁是根因”上死循环。Tarjan 是 O(V+E) 的,对小图来说开销可以忽略。因果时序门才是真正重要的——它解决的是”上游挂了三分钟后才被下游打死”这种顺序颠倒的场景,这与规模无关。


面试官:告警风暴来了,L2 降噪率 80%——这个数字是怎么测的?80% 够吗?Alertmanager 自己的 group_wait 不是已经做了一层聚合了吗?

真机验证时连推 5 条同 fingerprint 告警,4 条被去重,降噪率 80%。这个数字只是去重的贡献,还没算抑制和时间窗聚合——真实告警风暴下多种规则叠加,降噪率应该更高。Alertmanager 的 group_wait 做的是推送聚合(减少 webhook 次数),不是事件聚合(把多条告警归进同一个 Incident)。两者是不同层次的降噪:Alertmanager 减少的是网络请求数,L2 减少的是 LLM 调用数。叠加使用是正确的。至于 80% 够不够——目标不是”越高越好”,而是”不该合并的不能合并”。误合并(把两个独立故障合成一个 Incident)比漏合并危害更大。

追问:静默根因——根因节点自己不告警,你怎么保证找到的”共同祖先”真的是根因而不只是一个巧合?

两个指标的门槛就是为此设计的。explains 要求”大部分残差节点都在它的辖区”——如果只是巧合的共同祖先(比如两个完全无关的服务碰巧部署在同一台主机),残差集覆盖率不会高。precision 要求”它辖区内的节点大部分都在告警”——如果只有一个叶子节点告警但它的父节点管了 20 个叶子,precision 只有 5%,不会误判。两个指标同时达标才成立,是交叉验证。当然这仍然不是 ground truth——拓扑关联给出的是候选,最终确认在 Tier 3 假设验证环节。静默根因永不触发级联抑制也是兜底:即使判错了也不会吞掉本该调查的告警。

深度追问链#

面试官:你说自动发现的拓扑边一律 pending_review 落库,人审过才变 active——如果没人审怎么办?pending 的边堆了一大堆,都没参与级联关联,拓扑功能岂不是名存实亡?

pending_review 的边不参与级联抑制,但参与根因打分。区别在于:打分只是给调查链路一个提示(“可能是上游的问题”),不会吞告警,判错了成本很低;抑制会直接丢弃下游告警,判错了成本极高。所以 pending 边堆了没人审,拓扑关联的降噪功能确实打折,但根因候选推荐仍然在工作。这是”保守设计”和”有用设计”之间的平衡——宁可降噪率低一点,不能误吞告警。审核队列目前只有 CLI,没有前端,这是已知的产品短板。

继续追问:告警历史挖共现用的条件概率 + lift,但告警数据天然有大量”巧合共现”——两个无关告警碰巧在同一分钟触发。你怎么过滤噪声?

两个措施。一是用 lift(提升度)而不是只看条件概率。lift = P(A∧B) / (P(A)·P(B)),如果 A 和 B 独立则 lift=1,显著共现时 lift 远大于 1。只看条件概率 P(B|A) 会被”B 本来就一直在告警”骗过去。二是设置最小共现次数阈值——观测次数太少时统计量不可靠,不生成边。挖出来的边也是 pending_review,只参与打分不参与抑制。

常规问题#

面试官:去重窗口 600 秒、时间窗聚合 30 秒——这些默认值怎么定的?可以按 service 不同设置不同值吗?

600 秒是 Alertmanager 默认 resolve_timeout 的常见值——一条告警在 10 分钟内重推被视为同一次故障。30 秒对标 Alertmanager 的 group_wait。当前实现是全局配置,不支持按 service 差异化,这是已知的局限。如果要做差异化,应该引入类似 Alertmanager route 树的层级配置,但当前规模下全局值够用,过早引入层级配置增加运维复杂度。

面试官:抖动检测——一个告警 10 分钟内翻转 5 次就判抖动,但有些场景就是会频繁翻转(比如磁盘使用率在阈值附近波动),这不是 false positive 吗?

是 false positive 的高发场景。但抖动检测的处理不是”丢弃告警”,而是”Impact 折减”——抖动告警仍然会进入调查链路,只是不会因为数量多就抬高影响面评分。真正的解决方案在告警源——Prometheus 应该配 for 持续时间(比如 for: 5m),在阈值附近波动时不触发告警。L2 的抖动检测是针对”告警源没配好”的兜底。

反思与改进#

面试官:L2 预处理这块最大的遗憾是什么?

没有做告警关联的可视化。拓扑图、归并逻辑、降噪过程——这些在后端全部有数据,但前端看不到。运维人员看到的是”100 条告警变成了 3 个 Incident”,但不知道为什么。可视化需要展示:哪些告警被去重了(fingerprint)、哪些被抑制了(被谁抑制)、哪些通过拓扑关联归入了同一个 Incident(边是怎么走的)。这不仅是用户体验问题,更是调试问题——如果降噪规则误杀了一条重要告警,运维人员需要看到规则链才能定位原因。

面试官:拓扑这块最大的技术挑战是什么?

身份解析。同一个服务在告警标签里可能有三五种叫法,resolver.py 需要在没有 CMDB 的情况下做跨命名空间的身份对齐。当前实现是手写的别名规则 + compose/nginx 解析出来的映射。这比算法层面的 SCC 缩点或时序门难得多——算法是确定性的,写对一次就行;身份解析是脏活,每个环境的命名习惯不同,永远有新的 edge case。如果重来,我会优先投资一个轻量的服务注册表(不是完整的 CMDB),让每个服务有且仅有一个 canonical name,告警归一化时就对齐到它。