一、引言

在可观测性平台里,一次故障常会触发多条告警,告警散落在应用、集群、服务、主机乃至机房等不同层级对象上。如果只按告警数量或资源层级排序,排在前面的通常是“告警最明显”的配置项(Configuration Item,CI),却未必是最早发生异常、最值得优先排查的对象。方案调研最初评估过 PC、GES、CCDr 等统计因果发现算法,但现场没有可支撑统计因果发现的多变量观测样本,只有节点告警数量、告警等级和既有 CMDB 拓扑。在这一数据条件下,统计因果发现不适配当前任务,问题因此被重新定义为 Graph RCA,也就是图传播式根因排序。eoi-root-cause-analysis 随后把告警作为事件的触发证据,从 Nebula 查询对应业务范围内的 CMDB 依赖子图,再为图中的 CI 实例生成根因候选排序。本文讨论的重点不是把 PageRank 原样搬进告警平台,而是解释如何借用 Personalized PageRank 的核心思想,将事件先验、拓扑方向、节点度数和推荐范围组合成一套可落地的故障传播算法。

二、正文

(一)告警不等于根因

旧算法主要根据节点自身属性和固定范围内的告警邻居打分:

$$ \operatorname{OldScore}(i) = \operatorname{LayerScore}(i) + \operatorname{CiTypeScore}(i) + \operatorname{Depth1Weight} \times \operatorname{AlarmedUpperCount1}(i) + \operatorname{Depth2Weight} \times \operatorname{AlarmedUpperCount2}(i) $$

此外,下游节点还会固定加 10 分。这套规则直观,也容易解释:节点所在层级越重要、CI 类型权重越高、附近告警越多,得分就越高。

问题在于,它隐含了两个并不总是成立的前提。

第一,告警挂载对象与实际故障 CI 足够接近。工程现场中,监控指标通常采集自某个明确对象,但故障可能发生在它依赖的上游,也可能通过共享基础设施影响多个下游。告警告诉我们“哪里观察到了异常”,不直接等于“异常从哪里开始”。

第二,告警实例数量能够代表证据强度。旧算法会把同一实例上的多条告警折叠成一个实例 ID,严重度和发生时间不会累积进入得分。一条刚发生的高严重度告警,与一条较早发生的低严重度告警,在实例级折叠之后可能没有明显差别。

固定查看一、二级邻居也限制了算法对长链路故障的表达能力。只要根因与告警对象之间隔了更多层,或者证据需要经过多个节点汇聚,固定深度计数就难以继续传递信息。由于旧分数没有跨轮传播和归一化,不同拓扑规模下的结果也不容易放在同一尺度上比较。

旧算法与传播算法的差异可以概括为:

对比项旧算法告警先验驱动的传播算法
告警处理同实例多告警折叠为一个实例 ID同实例告警按严重度和时间累计
拓扑范围固定一、二级邻居在查询得到的 CMDB 子图内多轮传播
节点证据层级分、CI 类型分、邻居告警计数层级分、CI 类型分、严重度、时间衰减
传播方式邻居计数和固定加分按方向、阻尼系数和节点度数迭代
分数尺度缺少统一归一化每轮归一化到 $[0,1000]$
输出约束更偏向直接告警节点传播后按 scorableLayers 过滤候选层
主要局限难以表达跨层故障链路仍依赖 CMDB 质量、方向配置和参数选择

传播算法并不是为了否定告警本身。告警仍然是整次计算的起点,只是不再被当作根因结论,而是被组织成事件级先验,再沿 CMDB 关系扩散。

(二)从 PageRank 到故障传播

(三)经典 PageRank 与 Personalized PageRank

经典 PageRank 将网页建模为有向图。一个节点的重要性不仅取决于有多少节点指向它,也取决于这些来源节点本身的重要性。原始工作对 PageRank 及阻尼思想的说明,可参见 The Anatomy of a Large-Scale Hypertextual Web Search Engine 和 1999 年技术报告 The PageRank Citation Ranking: Bringing Order to the Web。下式是对这一迭代关系的等价向量表达:

$$ \mathbf{r}^{(t+1)} = (1-d)\mathbf{u} + dP^\mathsf{T}\mathbf{r}^{(t)} $$

其中:

  • $\mathbf{r}^{(t)}$ 是第 $t$ 轮的节点得分向量;
  • $d$ 是阻尼系数;
  • $P$ 是转移矩阵;
  • $\mathbf{u}$ 是均匀分布,用于保留随机跳转的概率。

经典 PageRank 有向图排名示例

图 1:经典 PageRank 示例。节点大小反映其相对排名,箭头表示有向链接。图源:Wikimedia Commons - PageRanks-Example.svg,Public Domain。

Personalized PageRank 不再把随机跳转均匀分配给所有节点,而是使用偏好向量 $\mathbf{v}$。这种写法可由上述 1999 年技术报告中的个性化思想,以及 2003 年的 Personalized PageRank 工作 Scaling Personalized Web Search 支持:

$$ \mathbf{r}^{(t+1)} = (1-d)\mathbf{v} + dP^\mathsf{T}\mathbf{r}^{(t)} $$

偏好向量让计算结果围绕特定主题、用户或种子节点展开。放到告警根因分析中,这个思想很自然:一次告警事件本身就提供了一组非均匀的初始证据,算法没有必要从所有 CI 等概率出发。

不过,根因分析和网页排名的目标不同。网页排名关心相对稳定的全局重要性,告警分析关心特定事件下的局部嫌疑度。前者的链接长期存在,后者的告警证据、查询子图和传播方向会随事件变化。这里借用的是 Personalized PageRank 的个性化重启与图传播思路,具体计算还加入了业务先验、度数惩罚、有限轮迭代和 min-max 归一化。

(四)本项目的工程化改造

本项目保留了 Personalized PageRank 的两个核心观念:

  • 当前事件提供非均匀的节点先验;
  • 节点得分沿图关系反复传播,而不是只统计固定层数的邻居。

在此基础上,方案曾在“反向 PPR”和“自定义拓扑传播评分”之间权衡。最终实现吸收了前者沿图反向聚合证据的思路,也保留了后者按业务层级、CI 类型和告警属性定制评分的能力。算法具体做了几项面向故障场景的改造。

首先,偏好信息没有先归一化为概率和为 1 的向量,而是以 InitialScore 表示。这个分数同时包含节点层级、CI 类型、告警严重度和告警时间,强调的是当前事件中的证据强弱。

其次,实际实现根据 direction 配置以及 CMDB 边的 src、dst 选择传播邻居。同一份子图可以按 UP、DOWN 或 BIDIRECT 形成不同的邻接关系,分别服务于向上游追溯、向下游传播或双向计算。

再次,为了抑制高度节点吸收过多分数,传播分母从普通的出度分摊扩展为 $\operatorname{Degree}^{1+p}$。参数 $p$ 用来控制额外的度数惩罚。

最后,算法每轮把分数归一化到 $[0,1000]$,并在输出阶段通过 scorableLayers 限制可推荐层。前者统一单次事件内的展示尺度,后者把“参与传播”和“允许成为根因候选”拆成两个概念。

(五)传播算法的实现路径

1. 获取 CMDB 子图并建模

服务收到告警事件后,先根据告警关联的 CI 实例,在 Nebula 中查询一定深度的 CMDB 依赖子图。origin.rootcause.nebula.depth 控制查询深度,origin.rootcause.nebula.relationEdgeName 指定参与查询的关系边。

生产查询的结构可以抽象为一行骨架:FIND SHORTEST PATH WITH PROP FROM <起点> TO <终点> OVER <关系边> BIDIRECT UPTO <深度> STEPS YIELD path AS P。这只是结构示意,其中的占位符需要替换,并非一条可直接执行的完整 nGQL。

图中的顶点对应 CI 实例,边保留 CMDB 中的 src、dst 关系。后续算法需要为每个节点维护至少以下信息:

  • CI 实例 ID;
  • CI 类型和所在层级;
  • 与当前事件关联的告警集合;
  • 根据传播方向生成的入边和出边;
  • 当前轮分数与下一轮分数;
  • 用于出度分摊的 Degree。

这里需要区分查询方向和算法传播方向。Nebula nGQL 的 FIND PATH 文档中,BIDIRECT 只控制查询阶段沿边的两个方向寻找路径。算法阶段则根据 direction 配置以及边的 src、dst 选择传播邻居;查询阶段使用 BIDIRECT,不等于算法必然执行双向传播。

告警先验驱动的个性化 PageRank 故障传播流程图

图 2:告警先验驱动的 Personalized PageRank 式故障传播。多条告警先聚合为事件级 InitialScore,再沿 CMDB 子图传播,最后通过 scorableLayers 过滤并输出 Top N。图为本项目绘制的概念示意,不代表真实事件中的拓扑、分数或排序。

2. 计算事件级 InitialScore

每个节点的事件级初始分数为:

$$ \operatorname{InitialScore}(i) = \operatorname{LayerScore}(i) + \operatorname{CiTypeScore}(i) + \sum_{a \in A_i}\operatorname{SeverityWeight}(a.\operatorname{severity}) + \sum_{a \in A_i} \frac{\operatorname{TimeWeight}} {1+\Delta\operatorname{Minutes}(a)} $$

其中,$A_i$ 是当前事件中挂载到节点 $i$ 的告警集合,$\Delta\operatorname{Minutes}(a)$ 是告警与计算时点之间的分钟差。

这个定义解决了旧算法中“同实例多告警只算一次”的问题。同一节点上的多条告警不再只留下一个布尔标记,而是分别贡献严重度分和时间分。较新的告警获得更高的时间权重,随着时间差增加,其贡献按分母逐步下降。

层级分和 CI 类型分保留了领域知识。它们不是由图结构自动推导出来的,而是表达平台对不同资源层级和类型的先验判断。严重度与时间则提供事件本身的动态证据。两类信息相加后,构成这一轮故障分析的起始状态。

对于没有直接告警的节点,base-score-for-no-alarm 决定是否保留基础分:

  • 开启时,无告警节点仍可保留层级分和 CI 类型分;
  • 关闭时,无告警节点的 InitialScore 置为 0。

两种情况下,无告警节点都可以在后续迭代中接收其他节点传播过来的分数。因此,“初始分为 0”不等于“永远不能成为候选”。

3. 按方向迭代传播

传播公式为:

$$ \operatorname{Score}_i^{(t+1)} = (1-d)\operatorname{InitialScore}_i + d \sum_{j\rightarrow i} \frac{\operatorname{Score}_j^{(t)}} {\operatorname{Degree}_j^{1+p}} $$

其中:

  • $d$ 是阻尼系数;
  • $j\rightarrow i$ 表示按照当前 direction 及 src、dst 关系,节点 $j$ 可以向节点 $i$ 传递分数;
  • $\operatorname{Degree}_j$ 是来源节点 $j$ 在该传播方向上的出度;
  • $p$ 是额外的度数惩罚参数。

$(1-d)\operatorname{InitialScore}_i$ 保证每一轮都会重新注入当前事件的原始证据。如果没有这一项,迭代结果可能逐渐只剩拓扑结构影响,告警严重度和时间信息反而被稀释。

传播邻居按 direction 配置选取:

  • UP:结合 src、dst 关系选取上游邻居,追溯可能根因;
  • DOWN:结合 src、dst 关系选取下游邻居,向下游传递分数;
  • BIDIRECT:把两个方向的邻居都纳入传播计算。

这里的“上游”和“下游”必须与当前 CMDB 对 src、dst 的建模约定保持一致。若边语义理解相反,即使 direction 的配置值本身有效,实际选择出的传播邻居也会偏离预期。

算法按 max-iterations 执行固定轮数,没有以相邻两轮的收敛误差作为停止条件。它的工程目标是在有界轮数内完成当前子图的排序。

4. 归一化、层过滤与结果富化

每轮传播结束后,节点分数通过 min-max 方法归一化到 $[0,1000]$:

$$ \operatorname{NormalizedScore}(i) = 1000 \times \frac{\operatorname{Score}(i)-\operatorname{Score}_{\min}} {\operatorname{Score}_{\max}-\operatorname{Score}_{\min}} $$

归一化让一次事件内的候选分数更容易排序和展示,但 1000 只表示该次计算中的相对高点,不表示概率,更不能跨事件直接解释为同等强度。当所有节点的原始分数相同时,当前实现把节点统一记为 500 分;其他情况按 min-max 映射到 $[0,1000]$。

传播完成后,scorableLayers 会过滤掉不允许推荐为根因的层级,再从剩余节点中输出 Top N。被过滤的节点仍然可以参与前面的传播,只是不出现在最终候选列表中。这样可以避免为了控制输出而提前删除中间节点,导致原本连续的传播路径断开。

最终结果不仅包含实例 ID 和分数,还会回填:

  • CI 名称;
  • CI 类型;
  • 所属业务系统;
  • 影响业务系统数;
  • 代表性告警。

这些字段不改变传播分数,但决定了排序结果能否被值班人员理解。只有实例 ID 和一个归一化数字的结果,很难直接转化成排查动作。

相关配置集中在以下键中:

# 以下是本项目实际使用过的配置,不是所有环境的推荐默认值。
origin:
  rootcause:
    nebula:
      relationEdgeName: cmdb_object_relation
      depth: 4
    score:
      propagation:
        # UP 沿 CMDB 关系追溯上游;也可配置 DOWN 或 BIDIRECT。
        direction: UP
        # 阻尼越大,上一轮传播分数的影响越强。
        damping: 0.6
        max-iterations: 5
        severity-weight: '{"1":50,"2":30,"3":20,"4":10,"5":5}'
        base-score-for-no-alarm: true
        time-weight: 0.1
        # 在标准出度分摊之外增加来源端度数惩罚,p = 0.5。
        degree-penalty: 0.5
        # 只限制最终可推荐层,不会把这些节点从传播图中删除。
        scorable-layers: '["业务资源层","服务资源层","资源规划层","基础设施层"]'
    algorithm: propagation
  • algorithm 可选 original 或 propagation;未知值会回退到旧算法。
  • damping 决定传播项与每轮重新注入的 InitialScore 如何共同作用;max-iterations 限制计算轮数。
  • 查询阶段使用 BIDIRECT 不等于传播阶段双向传播;传播方向仍由 direction 决定。
  • base-score-for-no-alarm 开启时,无告警节点保留基础分;即使初始分为 0,无告警节点仍可接收传播。
  • scorable-layers 只约束最终候选范围,degree-penalty 则抑制高出度来源节点的传播富集。

(十)Hub 节点富集问题与修正

传播算法落地后,一个比“公式能不能迭代”更棘手的问题出现了:拓扑中的 Hub 节点可能因为汇聚了大量路径而获得异常高的分数。

用于推演这一问题的现场真实拓扑模拟为:

$$ 4\text{ 应用} \rightarrow 4\text{ 集群} \rightarrow 4\text{ 服务} \rightarrow 4\text{ 主机} \rightarrow 4\text{ 物理机} \rightarrow 1\text{ 机柜} \rightarrow 1\text{ 机房} $$

在这组模拟中,未告警的机柜归一化后得到 1000 分,而有告警的物理机只有 200 分。这个结果不是线上准确率,也不是生产效果统计,只说明在该拓扑和参数组合下,多个来源的传播分数会在单一汇聚节点上累积。

问题不在于机柜绝对不可能成为根因,而在于这次模拟暴露出:仅凭路径汇聚,高度节点就可能压过带有直接告警证据的节点。围绕这一反例,修正过程并不是一次完成的。

第一步尝试在目标端做衰减或按其子节点数量分摊,希望直接压低汇聚节点收到的分数。但传播量由来源节点发出,惩罚放在目标端会把接收方的连接特征混进来源方的传播责任,落错了位置。

第二步把惩罚移到来源端,却一度去掉了标准的 $/\operatorname{Degree}$ 出度分摊,每条边只剩下 $/\operatorname{Degree}^p$。此时,来源节点经过全部出边发出的总传播量不再是受控的:

$$ \operatorname{Degree} \times \frac{\operatorname{Score}} {\operatorname{Degree}^p} = \operatorname{Score} \times \operatorname{Degree}^{1-p} $$

当 $\operatorname{Degree}=4$、$p=0.5$ 时,总传播量为:

$$ \operatorname{Score} \times 4^{1-0.5} = 2\operatorname{Score} $$

也就是说,一个节点仅仅因为有 4 条出边,就能向外传播原分数 2 倍的总量;出边越多,反而越容易“凭空加分”。这条反例说明,额外幂次惩罚不能替代标准出度分摊。

第三步才收敛到最终形式:先保留标准出度分摊,再叠加来源端度数惩罚,使每条边除以 $\operatorname{Degree}^{1+p}$。下文中的 D2PR 是本项目调研过程使用的内部简称,不是已发表方法的标准名称,本文也不据此引申任何所谓的 D2PR 论文。

调研中还参考过 GRANO。GRANO(Wang et al., PVLDB 2019)使用 $\log(|V_c|+1)$ 作为聚合分的对数增益;它的方向是增强聚合规模带来的得分,与本项目抑制高度节点的目标相反,而且该论文并不是 PageRank 方法,因此没有采用,详见 GRANO: Interactive Graph-based Root Cause Analysis for Cloud-Native Distributed Data Platform。相关备选方案如下:

目标端子节点分摊

  • 做法: 根据目标节点关联的子节点数量分摊输入。
  • 能解决什么: 尝试削弱汇聚节点得分。
  • 为什么未采用或局限: 惩罚落在接收端,难以表达来源节点的传播责任。

仅层过滤

  • 做法: 直接禁止机柜、机房等层级进入结果。
  • 能解决什么: 快速避免特定层成为候选。
  • 为什么未采用或局限: 只能隐藏结果,不能修正传播过程中的分数富集。

GRANO $\log(|V_c|+1)$

  • 做法: 对聚合分施加随 $|V_c|$ 增长的对数增益。
  • 能解决什么: 增强聚合规模对得分的贡献。
  • 为什么未采用或局限: 方向与抑制高度节点相反,且 GRANO 不是 PageRank 方法。

按经典 PPR 迭代式重构

  • 做法: 使用随机转移矩阵和个性化重启向量重写迭代过程。
  • 能解决什么: 让传播过程满足经典 PPR 迭代式的概率解释。
  • 为什么未采用或局限: 改造范围较大,也不能单独解决业务层输出约束。

目标端 D2PR

  • 做法: 按目标节点度数增加惩罚。
  • 能解决什么: 压低部分高连接目标节点。
  • 为什么未采用或局限: 仍把主要惩罚放在接收方,方向语义不够稳定。

仅 $1/\operatorname{Degree}^p$

  • 做法: 每条边只按额外幂次衰减。
  • 能解决什么: 便于调节高度节点的影响。
  • 为什么未采用或局限: 没有先完成标准出度分摊,来源总传播量仍可能随出度增长。

来源端 D2PR + 出度分摊 + scorableLayers

  • 做法: 每条边除以 $\operatorname{Degree}^{1+p}$,输出时再限制候选层。
  • 能解决什么: 同时控制来源总传播量和最终推荐范围。
  • 为什么未采用或局限: 这是最终采用的组合;参数仍需结合拓扑验证,不存在已知的唯一最优值。D2PR 是项目内部简称。

最终采用的是来源端 D2PR、出度分摊和 scorableLayers 的组合。对来源节点 $j$ 来说,它向每条出边传播:

$$ \frac{\operatorname{Score}_j} {\operatorname{Degree}_j^{1+p}} $$

因为共有 $\operatorname{Degree}_j$ 条出边,其总传播量为:

$$ \operatorname{Degree}_j \times \frac{\operatorname{Score}_j} {\operatorname{Degree}_j^{1+p}} = \frac{\operatorname{Score}_j} {\operatorname{Degree}_j^p} $$

当 $p=0.5$ 时,总传播量为:

$$ \frac{\operatorname{Score}_j} {\sqrt{\operatorname{Degree}_j}} $$

下表中的百分比表示:保留标准出度分摊后,来源节点总传播量相对自身分数的解析比例。它不是实验结果,也不要与前文“去掉出度分摊”时出现的膨胀反例混淆。

出度p=0p=0.5p=1
1100%100%100%
2100%70.7%50%
4100%50%25%
10100%31.6%10%

degree-penalty: 0.5 表示:Degree=1 时不额外衰减,链式拓扑中的证据可以完整保留;Degree=4 时总传播量为 50%,能够压制 Hub,同时比 p=1 时的 25% 更温和。因此,0.5 是一个具有清晰平方根解释、衰减强度居中的工程折中,并在本文模拟拓扑中用于抑制 Hub;这不是通过网格搜索、线上准确率或参数扫描证明的理论最优值,仍需按实际拓扑验证。

并且当 $p\geq 0$、$\operatorname{Degree}_j\geq 1$ 时:

$$ \frac{\operatorname{Score}_j} {\operatorname{Degree}_j^p} \leq \operatorname{Score}_j $$

这意味着来源节点不会因为拥有更多出边而传播出超过自身分数的总量。$p=0$ 时退化为普通出度分摊;$p$ 增大时,高出度来源节点的总传播量会进一步降低。

scorableLayers 解决的是另一个层面的问题:某些 CI 可以作为传播路径的一部分,却不适合作为值班人员看到的根因候选。用层过滤控制最终推荐范围,比直接把这些节点从图中移除更稳妥。前者保留拓扑连通性,后者可能改变传播路径本身。

这套修正并不意味着 $p=0.5$ 已经是所有拓扑的最优参数。它只能说明该参数具有清晰的传播量解释,并能在上述模拟中用于抑制 Hub 富集。实际配置仍需要针对不同 CMDB 密度、边方向和层级结构做反例验证。

(十一)适用边界与未解决项

这类算法适合解决的是:在一个由告警触发、范围受控的 CMDB 子图内,对多个可能根因进行相对排序。它不能单独回答“这个节点是否就是唯一根因”,也不能替代指标、日志、链路和变更记录等后续证据。

首先,结果高度依赖 CMDB 质量。缺边会让证据无法到达真实相关节点,错边会把分数传向无关对象,src、dst 语义理解错误则可能让实际选择的传播邻居偏离预期。算法可以减少人工浏览拓扑的范围,但不能修复拓扑数据本身。

其次,InitialScore 中的层级分、CI 类型分、严重度权重和时间权重都带有领域假设。某类告警严重度高,不代表它在所有业务系统中都具有相同根因价值;某类 CI 层级重要,也不代表每次事件都应优先推荐。参数应通过可解释的反例集调整,而不是只观察少量结果后固定下来。

再次,每轮 min-max 归一化保留的是事件内相对顺序。它会受当前子图中的最大值和最小值影响,因此不能把不同事件中的 800 分直接解释为相同的故障可能性。分数也不是概率,不应在展示层附加未经校准的概率含义。

Hub 惩罚同样存在两面性。高度节点可能只是拓扑汇聚点,也可能确实是共享故障源。惩罚过弱会继续产生富集,惩罚过强则可能压低真正的共享基础设施故障。degree-penalty 需要与传播方向、子图深度和 CMDB 建模方式一起评估。

仍需继续验证的事项包括不同拓扑密度下的参数敏感性、孤立节点与悬挂节点的处理、归一化退化场景,以及代表性告警的选择规则。若要评价算法是否真正改善根因定位,还需要独立标注的历史事件和一致的评测口径;仅凭个别拓扑模拟不能推出准确率提升或定位时间缩短。

三、总结

PageRank 对告警根因分析最有价值的部分,不是“给每个节点算一个排名”,而是把节点自身证据与拓扑传递放进同一套迭代过程。告警负责提供事件先验,CMDB 负责限定传播路径,阻尼系数保留原始证据,度数惩罚抑制 Hub 富集,scorableLayers 则约束最终可以交付给值班人员的候选范围。

这套实现以 Personalized PageRank 的个性化重启和图传播思想为基础,又加入了业务先验、来源端度数惩罚、有限轮迭代、min-max 归一化与候选层过滤。它比固定邻居计数更适合表达当前子图内的跨层依赖,也带来了更明确的工程约束:CMDB 边必须可信,传播邻居必须按 direction 和 src、dst 语义正确选择,参数必须经过反例验证,归一化分数也只能在当前事件中作相对解释。算法给出的仍然是排查顺序,不是未经其他证据验证的根因判决。

四、文献引用

最后修改:2026 年 09 月 23 日
如果觉得我的文章对你有用,请随意赞赏