Spark GraphX 网页排名(PageRank)算法

一、PageRank 数学原理

1.1 概括

PageRank 的核心思想可以用一句话:

一个网页的重要性,取决于链接到它的网页的数量和质量。

把你想象成一个"随机冲浪者":

  • 你在当前页面随机点击一个链接跳转到下一页(概率 ddd,通常 d=0.85d = 0.85d=0.85)
  • 偶尔你会觉得无聊,直接随机跳到任意一个页面(概率 1−d1-d1−d,称为"阻尼因子")

所有页面被访问的概率分布,就是 PageRank 值。

1.2 数学公式

经典定义:

PR(u)=1−dN+d×∑v∈BuPR(v)L(v)PR(u) = \frac{1-d}{N} + d \times \sum_{v \in B_u} \frac{PR(v)}{L(v)}PR(u)=N1−d+d×v∈Bu∑L(v)PR(v)

其中:

符号 含义
PR(u)PR(u)PR(u) 页面 uuu 的 PageRank 值
ddd 阻尼因子(damping factor),通常取 0.85
NNN 图中总页面数(顶点数)
BuB_uBu 链接到 uuu 的所有页面的集合(入边邻居)
L(v)L(v)L(v) 页面 vvv 的出链数量(出度)

1.3 矩阵形式

令 M\mathbf{M}M 为转移矩阵,r\mathbf{r}r 为 PR 向量:

r(t+1)=1−dN1+d⋅MTr(t)\mathbf{r}^{(t+1)} = \frac{1-d}{N}\mathbf{1} + d \cdot \mathbf{M}^T \mathbf{r}^{(t)}r(t+1)=N1−d1+d⋅MTr(t)

复制代码
         ┌──────────────────────┐
         │  M[i][j] = 1/L(j)    │  如果 j→i 有链接
         │  M[i][j] = 0         │  否则
         └──────────────────────┘

1.4 迭代求解过程

#mermaid-svg-eqAFsI7QlquAqgN5{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-eqAFsI7QlquAqgN5 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-eqAFsI7QlquAqgN5 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-eqAFsI7QlquAqgN5 .error-icon{fill:#552222;}#mermaid-svg-eqAFsI7QlquAqgN5 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-eqAFsI7QlquAqgN5 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-eqAFsI7QlquAqgN5 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-eqAFsI7QlquAqgN5 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-eqAFsI7QlquAqgN5 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-eqAFsI7QlquAqgN5 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-eqAFsI7QlquAqgN5 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-eqAFsI7QlquAqgN5 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-eqAFsI7QlquAqgN5 .marker.cross{stroke:#333333;}#mermaid-svg-eqAFsI7QlquAqgN5 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-eqAFsI7QlquAqgN5 p{margin:0;}#mermaid-svg-eqAFsI7QlquAqgN5 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-eqAFsI7QlquAqgN5 .cluster-label text{fill:#333;}#mermaid-svg-eqAFsI7QlquAqgN5 .cluster-label span{color:#333;}#mermaid-svg-eqAFsI7QlquAqgN5 .cluster-label span p{background-color:transparent;}#mermaid-svg-eqAFsI7QlquAqgN5 .label text,#mermaid-svg-eqAFsI7QlquAqgN5 span{fill:#333;color:#333;}#mermaid-svg-eqAFsI7QlquAqgN5 .node rect,#mermaid-svg-eqAFsI7QlquAqgN5 .node circle,#mermaid-svg-eqAFsI7QlquAqgN5 .node ellipse,#mermaid-svg-eqAFsI7QlquAqgN5 .node polygon,#mermaid-svg-eqAFsI7QlquAqgN5 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-eqAFsI7QlquAqgN5 .rough-node .label text,#mermaid-svg-eqAFsI7QlquAqgN5 .node .label text,#mermaid-svg-eqAFsI7QlquAqgN5 .image-shape .label,#mermaid-svg-eqAFsI7QlquAqgN5 .icon-shape .label{text-anchor:middle;}#mermaid-svg-eqAFsI7QlquAqgN5 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-eqAFsI7QlquAqgN5 .rough-node .label,#mermaid-svg-eqAFsI7QlquAqgN5 .node .label,#mermaid-svg-eqAFsI7QlquAqgN5 .image-shape .label,#mermaid-svg-eqAFsI7QlquAqgN5 .icon-shape .label{text-align:center;}#mermaid-svg-eqAFsI7QlquAqgN5 .node.clickable{cursor:pointer;}#mermaid-svg-eqAFsI7QlquAqgN5 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-eqAFsI7QlquAqgN5 .arrowheadPath{fill:#333333;}#mermaid-svg-eqAFsI7QlquAqgN5 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-eqAFsI7QlquAqgN5 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-eqAFsI7QlquAqgN5 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eqAFsI7QlquAqgN5 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-eqAFsI7QlquAqgN5 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eqAFsI7QlquAqgN5 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-eqAFsI7QlquAqgN5 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-eqAFsI7QlquAqgN5 .cluster text{fill:#333;}#mermaid-svg-eqAFsI7QlquAqgN5 .cluster span{color:#333;}#mermaid-svg-eqAFsI7QlquAqgN5 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-eqAFsI7QlquAqgN5 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-eqAFsI7QlquAqgN5 rect.text{fill:none;stroke-width:0;}#mermaid-svg-eqAFsI7QlquAqgN5 .icon-shape,#mermaid-svg-eqAFsI7QlquAqgN5 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eqAFsI7QlquAqgN5 .icon-shape p,#mermaid-svg-eqAFsI7QlquAqgN5 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-eqAFsI7QlquAqgN5 .icon-shape .label rect,#mermaid-svg-eqAFsI7QlquAqgN5 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eqAFsI7QlquAqgN5 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-eqAFsI7QlquAqgN5 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-eqAFsI7QlquAqgN5 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否

初始化: 所有顶点 PR = 1/N
迭代 t=0
每个顶点收集入边邻居的

PR/出度 之和
应用阻尼因子公式

得到新一轮 PR 值
收敛?

max|PR_new - PR_old| < tol
输出最终 PR 排名

1.5 收敛性保证

PageRank 迭代收敛的数学保证来自马尔可夫链稳态分布理论:

  • 阻尼因子 d<1d < 1d<1 保证了转移矩阵的遍历性(irreducible + aperiodic)
  • 随机跳转项 1−dN\frac{1-d}{N}N1−d 处理了悬挂节点(dead ends,出度为 0 的页面)
  • 迭代是幂迭代法(Power Iteration),线性收敛

二、GraphX 图计算框架

2.1 架构概览

GraphX 基于 Spark RDD,抽象出三个核心结构:
#mermaid-svg-MnmeB1meZor59opN{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-MnmeB1meZor59opN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-MnmeB1meZor59opN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-MnmeB1meZor59opN .error-icon{fill:#552222;}#mermaid-svg-MnmeB1meZor59opN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-MnmeB1meZor59opN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-MnmeB1meZor59opN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-MnmeB1meZor59opN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-MnmeB1meZor59opN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-MnmeB1meZor59opN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-MnmeB1meZor59opN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-MnmeB1meZor59opN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-MnmeB1meZor59opN .marker.cross{stroke:#333333;}#mermaid-svg-MnmeB1meZor59opN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-MnmeB1meZor59opN p{margin:0;}#mermaid-svg-MnmeB1meZor59opN .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-MnmeB1meZor59opN .cluster-label text{fill:#333;}#mermaid-svg-MnmeB1meZor59opN .cluster-label span{color:#333;}#mermaid-svg-MnmeB1meZor59opN .cluster-label span p{background-color:transparent;}#mermaid-svg-MnmeB1meZor59opN .label text,#mermaid-svg-MnmeB1meZor59opN span{fill:#333;color:#333;}#mermaid-svg-MnmeB1meZor59opN .node rect,#mermaid-svg-MnmeB1meZor59opN .node circle,#mermaid-svg-MnmeB1meZor59opN .node ellipse,#mermaid-svg-MnmeB1meZor59opN .node polygon,#mermaid-svg-MnmeB1meZor59opN .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-MnmeB1meZor59opN .rough-node .label text,#mermaid-svg-MnmeB1meZor59opN .node .label text,#mermaid-svg-MnmeB1meZor59opN .image-shape .label,#mermaid-svg-MnmeB1meZor59opN .icon-shape .label{text-anchor:middle;}#mermaid-svg-MnmeB1meZor59opN .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-MnmeB1meZor59opN .rough-node .label,#mermaid-svg-MnmeB1meZor59opN .node .label,#mermaid-svg-MnmeB1meZor59opN .image-shape .label,#mermaid-svg-MnmeB1meZor59opN .icon-shape .label{text-align:center;}#mermaid-svg-MnmeB1meZor59opN .node.clickable{cursor:pointer;}#mermaid-svg-MnmeB1meZor59opN .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-MnmeB1meZor59opN .arrowheadPath{fill:#333333;}#mermaid-svg-MnmeB1meZor59opN .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-MnmeB1meZor59opN .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-MnmeB1meZor59opN .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MnmeB1meZor59opN .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-MnmeB1meZor59opN .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MnmeB1meZor59opN .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-MnmeB1meZor59opN .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-MnmeB1meZor59opN .cluster text{fill:#333;}#mermaid-svg-MnmeB1meZor59opN .cluster span{color:#333;}#mermaid-svg-MnmeB1meZor59opN div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-MnmeB1meZor59opN .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-MnmeB1meZor59opN rect.text{fill:none;stroke-width:0;}#mermaid-svg-MnmeB1meZor59opN .icon-shape,#mermaid-svg-MnmeB1meZor59opN .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MnmeB1meZor59opN .icon-shape p,#mermaid-svg-MnmeB1meZor59opN .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-MnmeB1meZor59opN .icon-shape .label rect,#mermaid-svg-MnmeB1meZor59opN .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MnmeB1meZor59opN .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-MnmeB1meZor59opN .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-MnmeB1meZor59opN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 底层
GraphX 核心抽象
VertexRDD

顶点 RDD
EdgeRDD

边 RDD
VertexCut

顶点切割分区
RDD 弹性分布式数据集

2.2 核心 API

scala 复制代码
// Graph 定义
class Graph[VD, ED] {
    val vertices: VertexRDD[VD]    // 顶点集合  (VertexId, VD)
    val edges: EdgeRDD[ED]         // 边集合    Edge(srcId, dstId, attr)
    val triplets: RDD[EdgeTriplet[VD, ED]]  // 三元组视图
}

// 关键操作(PageRank 的核心)
def aggregateMessages[A](
    sendMsg: EdgeContext[VD, ED, A] => Unit,
    mergeMsg: (A, A) => A
): VertexRDD[A]

def outerJoinVertices[U](other: RDD[(VertexId, U)])(
    mapFunc: (VertexId, VD, Option[U]) => VD2
): Graph[VD2, ED]

2.3 顶点切割 vs 边切割

GraphX 采用**顶点切割(Vertex Cut)**策略:

复制代码
边切割(传统):              顶点切割(GraphX):
┌──────┬──────┐              ┌──────┬──────┐
│  A ──┤── B  │              │  A ──┤      │
│      │      │              │      │  B   │
│  C ──┴── D  │              │  C ──┤      │
└─────────────┘              └──────┴──────┘
  切在边上 → 通信少            切在顶点 → 副本多但负载均衡更好

对 PageRank 而言,顶点切割意味着每个顶点只需收集本地分区内的入边消息,然后通过 mergeMsg 跨分区合并。


三、静态 PageRank 实现

3.1 算法流程

静态 PageRank 每轮迭代使用上一轮的全局 PR 值(类似同步 BSP 模型)。

scala 复制代码
// 核心循环
var g = graph
var oldPR: VertexRDD[Double] = null

for (iter <- 1 to numIter) {
    // 1. 每个顶点将 PR/出度 发送给所有出边邻居
    val msgRDD = g.aggregateMessages[Double](
        ctx => ctx.sendToDst(ctx.srcAttr / ctx.srcAttr),  // ← 实际应发送 PR/出度
        _ + _
    )
    
    // 2. 应用 PageRank 公式
    val newPR = msgRDD.mapValues(msg =>
        (1 - resetProb) / numVertices + resetProb * msg
    )
    
    // 3. 更新图的顶点属性
    g = g.outerJoinVertices(newPR) {
        (_, old, newOpt) => newOpt.getOrElse(0.0)
    }
}

3.2 完整代码

scala 复制代码
import org.apache.spark.graphx._
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.SparkSession

object StaticPageRank {

    def main(args: Array[String]): Unit = {
        val spark = SparkSession.builder()
            .appName("GraphX Static PageRank")
            .master("local[*]")
            .getOrCreate()
        
        val sc = spark.sparkContext
        
        // ===== 1. 构建图 =====
        // 边: (srcId, dstId, weight)
        val edges: RDD[Edge[Double]] = sc.parallelize(Seq(
            Edge(1L, 2L, 1.0),  // 页面1 → 页面2
            Edge(1L, 3L, 1.0),  // 页面1 → 页面3
            Edge(2L, 3L, 1.0),  // 页面2 → 页面3
            Edge(2L, 4L, 1.0),  // 页面2 → 页面4
            Edge(3L, 1L, 1.0),  // 页面3 → 页面1
            Edge(4L, 1L, 1.0),  // 页面4 → 页面1
            Edge(4L, 3L, 1.0),  // 页面4 → 页面3
        ))
        
        // 顶点: 初始 PR = 1.0
        val vertices: RDD[(VertexId, Double)] = sc.parallelize(Seq(
            (1L, 1.0), (2L, 1.0), (3L, 1.0), (4L, 1.0)
        ))
        
        val graph = Graph(vertices, edges)
        val numVertices = graph.vertices.count()
        val resetProb = 0.15  // 随机跳转概率 = 1 - d
        
        println(s"顶点数: $numVertices")
        println("初始图:")
        graph.triplets.collect().foreach { t =>
            println(s"  ${t.srcId} --[${t.attr}]--> ${t.dstId}")
        }
        
        // ===== 2. 静态 PageRank 迭代 =====
        var g = graph
            .mapVertices((_, _) => 1.0 / numVertices)  // 初始化为 1/N
            .cache()
        
        val maxIter = 20
        val tolerance = 0.0001
        
        for (iter <- 1 to maxIter) {
            // Step A: 聚合入边消息
            // 每个顶点发送: PR / 出度
            val messages: VertexRDD[Double] = g.aggregateMessages[Double](
                ctx => {
                    // sendMsg: 向目标顶点发送 (srcPR / srcOutDegree)
                    val msg = ctx.srcAttr / ctx.srcAttr  // ← 简化示意
                    ctx.sendToDst(msg)
                },
                _ + _  // mergeMsg: 累加来自不同入边的贡献
            )
            
            // Step B: 应用阻尼公式
            val newPR: VertexRDD[Double] = messages.mapValues { rankSum =>
                resetProb / numVertices + (1.0 - resetProb) * rankSum
            }
            
            // Step C: 处理悬挂节点(无入边的顶点)
            // 这些顶点只保留随机跳转项
            val allVertices = g.outerJoinVertices(newPR) { (_, oldPR, newPROpt) =>
                newPROpt.getOrElse(resetProb / numVertices)
            }
            
            g = allVertices.cache()
            
            // Step D: 检查收敛
            val diff = g.vertices.join(newPR).map {
                case (_, (oldV, newV)) => math.abs(oldV - newV)
            }.max()
            
            println(f"迭代 $iter%2d: 最大差值 = $diff%.6f")
            
            if (diff < tolerance) {
                println(s"在第 $iter 轮收敛!")
            }
        }
        
        // ===== 3. 输出结果 =====
        println("\n======== 最终 PageRank 排名 ========")
        g.vertices.sortBy(-_._2).collect().foreach {
            case (id, pr) => println(f"  页面 $id: PR = $pr%.6f")
        }
        
        spark.stop()
    }
}

注意:上面代码中的 ctx.srcAttr / ctx.srcAttr 是占位示意。实际需要将 PR 值和出度分开存储,因为 srcAttr 同时承载了 PR 值,完整写法见下文"完整案例"。

3.3 静态算法的优缺点

优点 缺点
实现简单,逻辑清晰 每轮全量计算,收敛慢
每轮独立,易于调试 对悬挂节点处理需要额外步骤
确定性结果(可复现) 大量顶点已收敛时仍参与计算浪费资源

四、动态 PageRank 实现

4.1 核心思想

动态 PageRank 的核心优化:只传播有变化的 PR 值

复制代码
静态:  所有顶点每轮都发送 PR/出度
动态:  仅当 |PR_new - PR_old| > 阈值 时才向邻居发送增量 Δ = PR_new - PR_old

4.2 数学推导

设 Δi=PRi(t)−PRi(t−1)\Delta_i = PR_i^{(t)} - PR_i^{(t-1)}Δi=PRi(t)−PRi(t−1),则有:

PRu(t+1)=PRu(t)+dN∑v→uΔvL(v)PR_u^{(t+1)} = PR_u^{(t)} + \frac{d}{N} \sum_{v \to u} \frac{\Delta_v}{L(v)}PRu(t+1)=PRu(t)+Ndv→u∑L(v)Δv

即:新 PR = 旧 PR + 所有入边邻居的增量贡献之和。

4.3 GraphX 内置 API

scala 复制代码
import org.apache.spark.graphx.lib.PageRank

// 方式一: 固定迭代次数(静态)
val staticRanks = PageRank.run(graph, numIter = 20)

// 方式二: 收敛容差(静态,迭代至收敛)
val staticRanks2 = PageRank.runUntilConvergence(graph, tol = 0.0001)

// 方式三: 动态 PageRank(推荐)
val dynamicRanks = PageRank.runUntilConvergenceWithOptions(
    graph,
    tol = 0.0001,
    resetProb = 0.15
)

内置 API 参数说明:

scala 复制代码
def runUntilConvergenceWithOptions[VD, ED](
    graph: Graph[VD, ED],
    tol: Double,          // 收敛容差,所有顶点 |ΔPR| < tol 时停止
    resetProb: Double,    // 随机跳转概率 = 1 - 阻尼因子,默认 0.15
    srcPrefs: Option[GraphXUtils.PageRankOpts] = None  // 个性化 PR 参数
): Graph[Double, Double]

4.4 手写动态 PageRank

scala 复制代码
def dynamicPageRank(
    graph: Graph[Double, Double],
    resetProb: Double = 0.15,
    tol: Double = 0.0001,
    maxIter: Int = 100
): Graph[Double, Double] = {
    
    val numVertices = graph.vertices.count()
    
    // 初始化: 每个顶点存储 (PR, 出度, 上次发送的增量)
    var g = graph
        .outerJoinVertices(graph.outDegrees) { (_, pr, degOpt) =>
            (pr, degOpt.getOrElse(0), 0.0)  // (PR, outDeg, delta)
        }
        .mapVertices { (_, data) =>
            (1.0 / numVertices, data._2, 1.0 / numVertices)  // 首轮全量发送
        }
        .cache()
    
    var converged = false
    var iter = 0
    
    while (!converged && iter < maxIter) {
        // 只发送增量
        val messages: VertexRDD[Double] = g.aggregateMessages[Double](
            ctx => {
                // 仅当有有效增量且出度 > 0 时才发送
                if (ctx.srcAttr._3 > tol && ctx.srcAttr._2 > 0) {
                    val contribution = ctx.srcAttr._3 / ctx.srcAttr._2
                    ctx.sendToDst(contribution)
                }
            },
            _ + _
        )
        
        // 更新 PR 并计算新一轮增量
        var totalDiff = 0.0
        val newVertices = g.vertices.leftJoin(messages) { (id, oldData, msgOpt) =>
            val (oldPR, outDeg, oldDelta) = oldData
            val msgSum = msgOpt.getOrElse(0.0)
            
            // PageRank 公式
            val newPR = resetProb / numVertices + (1.0 - resetProb) * msgSum
            
            // 计算新增量(本轮变化量)
            val newDelta = math.abs(newPR - oldPR)
            
            totalDiff += newDelta
            
            (newPR, outDeg, newDelta)
        }
        
        g = Graph(newVertices, g.edges).cache()
        iter += 1
        
        println(f"动态迭代 $iter%3d: 累计变化 = $totalDiff%.6f")
        converged = totalDiff < tol
    }
    
    // 只返回 PR 值
    g.mapVertices((_, data) => data._1)
}

4.5 静态 vs 动态对比

维度 静态 PageRank 动态 PageRank
消息发送 所有顶点每轮都发 仅 PR 变化超过阈值的顶点
收敛速度 慢(幂迭代) 快(Gauss-Seidel 类增量法)
每轮计算量 O(E)O(E)O(E) 固定 逐渐递减 → 趋近 O(0)O(0)O(0)
内存 仅需存储当前 PR 需额外存储增量
确定性 确定 依赖阈值选择
适合场景 教学、小图 大型生产图

五、源码深度剖析

5.1 GraphX PageRank 源码结构

scala 复制代码
// spark/graphx/lib/PageRank.scala (简化版)
object PageRank extends Logging {
    
    // 入口: 固定迭代次数
    def run[VD, ED](graph: Graph[VD, ED], numIter: Int,
                    resetProb: Double = 0.15): Graph[Double, Double] = {
        runUntilConvergence(graph, Int.MaxValue, resetProb)
    }
    
    // 入口: 收敛模式(内部调动态实现)
    def runUntilConvergence[VD, ED](graph: Graph[VD, ED],
                                     tol: Double, resetProb: Double): Graph[Double, Double] = {
        runUntilConvergenceWithOptions(graph, tol, resetProb)
    }
    
    // 核心实现
    def runUntilConvergenceWithOptions[VD, ED](
        graph: Graph[VD, ED], tol: Double, resetProb: Double,
        srcPrefs: Option[PageRankOpts] = None
    ): Graph[Double, Double] = {
        
        // 1. 个性化权重(如果提供)
        val pRankGraph: Graph[Double, Double] = srcPrefs match {
            case Some(opts) => graph
                .outerJoinVertices(opts.personalization) { (_, _, srcPR) => srcPR.getOrElse(0.0) }
                .mapTriplets(_.attr, TripletFields.Src)  // 保留边权重
            case None => graph
                .mapVertices((_, _) => 0.0)  // 无个性化,初始化为 0
                .mapTriplets(_ => 1.0, TripletFields.None)  // 默认边权重=1
        }
        
        // 2. 核心迭代(动态)
        val dynamicImpl = new Pregel[Double, Double, Double](
            pRankGraph,
            initialMsg = resetProb)(
            // vertexProgram: 顶点更新函数
            (id, oldPR, msgSum) => {
                val newPR = resetProb / numVertices + (1.0 - resetProb) * msgSum
                if (math.abs(newPR - oldPR) < tol) oldPR else newPR
            },
            // sendMsg: 消息发送函数
            triplet => {
                if (triplet.srcAttr > tol) {
                    Iterator((triplet.dstId, triplet.srcAttr / triplet.attr))
                } else {
                    Iterator.empty
                }
            },
            // mergeMsg: 消息合并
            _ + _
        )
        
        dynamicImpl
    }
}

5.2 Pregel 模型解析

GraphX 的 PageRank 底层基于 Pregel(BSP 计算模型):
Worker 3 Worker 2 Worker 1 Master Worker 3 Worker 2 Worker 1 Master #mermaid-svg-kUHc71qdQHfkMEHt{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-kUHc71qdQHfkMEHt .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-kUHc71qdQHfkMEHt .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-kUHc71qdQHfkMEHt .error-icon{fill:#552222;}#mermaid-svg-kUHc71qdQHfkMEHt .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-kUHc71qdQHfkMEHt .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-kUHc71qdQHfkMEHt .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-kUHc71qdQHfkMEHt .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-kUHc71qdQHfkMEHt .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-kUHc71qdQHfkMEHt .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-kUHc71qdQHfkMEHt .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-kUHc71qdQHfkMEHt .marker{fill:#333333;stroke:#333333;}#mermaid-svg-kUHc71qdQHfkMEHt .marker.cross{stroke:#333333;}#mermaid-svg-kUHc71qdQHfkMEHt svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-kUHc71qdQHfkMEHt p{margin:0;}#mermaid-svg-kUHc71qdQHfkMEHt .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-kUHc71qdQHfkMEHt text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-kUHc71qdQHfkMEHt .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-kUHc71qdQHfkMEHt .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-kUHc71qdQHfkMEHt .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-kUHc71qdQHfkMEHt .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-kUHc71qdQHfkMEHt #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-kUHc71qdQHfkMEHt .sequenceNumber{fill:white;}#mermaid-svg-kUHc71qdQHfkMEHt #sequencenumber{fill:#333;}#mermaid-svg-kUHc71qdQHfkMEHt #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-kUHc71qdQHfkMEHt .messageText{fill:#333;stroke:none;}#mermaid-svg-kUHc71qdQHfkMEHt .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-kUHc71qdQHfkMEHt .labelText,#mermaid-svg-kUHc71qdQHfkMEHt .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-kUHc71qdQHfkMEHt .loopText,#mermaid-svg-kUHc71qdQHfkMEHt .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-kUHc71qdQHfkMEHt .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-kUHc71qdQHfkMEHt .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-kUHc71qdQHfkMEHt .noteText,#mermaid-svg-kUHc71qdQHfkMEHt .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-kUHc71qdQHfkMEHt .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-kUHc71qdQHfkMEHt .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-kUHc71qdQHfkMEHt .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-kUHc71qdQHfkMEHt .actorPopupMenu{position:absolute;}#mermaid-svg-kUHc71qdQHfkMEHt .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-kUHc71qdQHfkMEHt .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-kUHc71qdQHfkMEHt .actor-man circle,#mermaid-svg-kUHc71qdQHfkMEHt line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-kUHc71qdQHfkMEHt :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 本地计算 + 发送消息 同步屏障 (Barrier) 重复直到收敛 Superstep 0: 初始化Superstep 0: 初始化Superstep 0: 初始化消息交换 (Shuffle)消息交换 (Shuffle)消息交换 (Shuffle)Superstep 1Superstep 1Superstep 1

Pregel 三要素在 PageRank 中的对应:

Pregel 概念 PageRank 中的含义
vertexProgram 应用阻尼公式更新 PR 值
sendMsg 沿出边发送 PR / 出度
mergeMsg 对入边贡献求和

5.3 aggregateMessages 内部机制

scala 复制代码
// 这是 GraphX 中最核心的操作
def aggregateMessages[A: ClassTag](
    sendMsg: EdgeContext[VD, ED, A] => Unit,
    mergeMsg: (A, A) => A,
    tripletFields: TripletFields = TripletFields.All
): VertexRDD[A] = {
    
    // 内部执行步骤:
    // 1. mapPartitions: 每个分区遍历三元组,调用 sendMsg
    // 2. shuffle: 按 dstId 重新分区
    // 3. reduceByKey: 对同一 dstId 的消息执行 mergeMsg
}

执行示意图:

复制代码
分区1: Edge(1→2), Edge(1→3)     分区2: Edge(2→3), Edge(3→1)
  │ sendMsg                         │ sendMsg
  ▼                                 ▼
  (2, msgFrom1)  (3, msgFrom1)      (3, msgFrom2)  (1, msgFrom3)
                    │                                 │
                    └─────── shuffle ─────────────────┘
                                   │
                                   ▼
                         (1, [msgFrom3])
                         (2, [msgFrom1])
                         (3, [msgFrom1, msgFrom2])
                                   │
                                   ▼ reduceByKey (mergeMsg: _+_)
                         (1, msgFrom3)
                         (2, msgFrom1)
                         (3, msgFrom1 + msgFrom2)

六、性能调优

6.1 分区策略

scala 复制代码
// 方式一: 按边分区(默认 EdgePartition2D)
val partitionedGraph = graph.partitionBy(PartitionStrategy.EdgePartition2D)

// 方式二: 按顶点切割(减少 shuffle,适合 PageRank)
val partitionedGraph = graph.partitionBy(PartitionStrategy.CanonicalRandomVertexCut)

// 方式三: 自定义分区数
val repartitioned = graph.partitionBy(
    PartitionStrategy.EdgePartition2D,
    numPartitions = 200  // 推荐: 2-4 倍 executor 核心数
)

分区策略对比:

策略 原理 适合场景
EdgePartition2D 按边矩阵分块 通用、默认
CanonicalRandomVertexCut 随机顶点切割 高度倾斜的图
EdgePartition1D 按源顶点哈希 出边聚合多的场景

6.2 checkpoint 与缓存

scala 复制代码
import org.apache.spark.storage.StorageLevel

// 每 N 轮做一次 checkpoint(截断血缘、释放内存)
val checkpointInterval = 10

var g = initialGraph.cache()
for (iter <- 1 to maxIter) {
    val newG = computeNextPR(g)
    
    // 缓存 + checkpoint
    if (iter % checkpointInterval == 0) {
        newG.checkpoint()
    }
    
    g = newG.persist(StorageLevel.MEMORY_AND_DISK_SER)
    g.vertices.count()  // 触发物化
    
    // 释放前一轮缓存
    g.unpersist()
}

6.3 序列化优化

scala 复制代码
// 使用 Kryo 序列化(比 Java 序列化快 10x)
val conf = new SparkConf()
    .set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
    .set("spark.kryo.registrationRequired", "true")
    .registerKryoClasses(Array(
        classOf[Edge[Double]],
        classOf[(VertexId, Double)],
        classOf[Array[Edge[Double]]]
    ))

6.4 内存配置建议

bash 复制代码
# spark-submit 参数
--conf spark.executor.memory=8g
--conf spark.executor.memoryOverhead=2g        # 堆外内存(GraphX 大量使用)
--conf spark.storage.memoryFraction=0.4        # 降低缓存占比,给 shuffle 留空间
--conf spark.shuffle.memoryFraction=0.4        # 增加 shuffle 内存
--conf spark.default.parallelism=400           # 初始并行度

6.5 大规模图优化步骤

复制代码
1. 预处理: 过滤孤立顶点和自环边
2. 压缩: VertexId 用 Int 替代 Long(图 < 21 亿顶点时)
3. 增量: 大型图的动态更新 → 用动态 PageRank
4. 采样: 超大图先用随机游走采样估算,再全量计算
5. 分层: Web 图按域名分层,先域间 PR 再域内 PR

七、完整案例

7.1 维基百科链接图 PageRank

scala 复制代码
import org.apache.spark.graphx._
import org.apache.spark.graphx.lib.PageRank
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.SparkSession

object WikiPageRank {

    def main(args: Array[String]): Unit = {
        val spark = SparkSession.builder()
            .appName("Wiki PageRank")
            .config("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
            .getOrCreate()
        
        val sc = spark.sparkContext
        
        // ===== 数据格式: srcId \t dstId =====
        // 实际维基百科链接图(示例数据)
        val rawEdges: RDD[(Long, Long)] = sc.textFile("hdfs://path/to/wiki-links.txt")
            .map { line =>
                val parts = line.split("\t")
                (parts(0).toLong, parts(1).toLong)
            }
            .filter { case (src, dst) => src != dst }  // 去除自环
        
        // 构建图
        val edges: RDD[Edge[Double]] = rawEdges.map {
            case (src, dst) => Edge(src, dst, 1.0)
        }
        
        val graph = Graph.fromEdges(edges, defaultValue = 1.0)
        
        println(s"原始图: ${graph.vertices.count()} 个顶点, ${graph.edges.count()} 条边")
        
        // 过滤悬挂节点(可选:提升质量)
        val outDegrees = graph.outDegrees
        val validGraph = graph
            .outerJoinVertices(outDegrees) { (_, _, degOpt) =>
                degOpt.getOrElse(0)
            }
            .subgraph(
                epred = _ => true,
                vpred = (_, outDeg) => outDeg > 0  // 至少有 1 条出边
            )
        
        println(s"有效图: ${validGraph.vertices.count()} 个顶点, ${validGraph.edges.count()} 条边")
        
        // ===== 运行 PageRank =====
        val ranks = PageRank.runUntilConvergenceWithOptions(
            validGraph,
            tol = 0.0001,
            resetProb = 0.15
        )
        
        // ===== 输出 Top 20 =====
        println("\n========== PageRank Top 20 ==========")
        ranks.vertices
            .sortBy(-_._2)  // 降序
            .take(20)
            .zipWithIndex
            .foreach { case ((id, pr), idx) =>
                println(f"${idx + 1}%3d. 页面 $id%12d  PR = $pr%.8f")
            }
        
        // ===== 统计分析 =====
        val prValues = ranks.vertices.map(_._2)
        val stats = prValues.stats()
        
        println(s"\n========== 统计信息 ==========")
        println(s"最小值:  ${stats.min}")
        println(s"最大值:  ${stats.max}")
        println(s"均值:    ${stats.mean}")
        println(s"标准差:  ${stats.stdev}")
        println(s"总和:    ${stats.sum}")
        
        // ===== 分布直方图 =====
        println(s"\n========== PR 值分布 ==========")
        val buckets = prValues
            .map { pr =>
                if (pr < 1e-8) "0 (极低)"
                else if (pr < 1e-6) "1e-8 ~ 1e-6"
                else if (pr < 1e-4) "1e-6 ~ 1e-4"
                else if (pr < 1e-3) "1e-4 ~ 1e-3"
                else if (pr < 1e-2) "1e-3 ~ 1e-2"
                else "> 1e-2 (高影响力)"
            }
            .countByValue()
        
        buckets.toSeq.sortBy(_._1).foreach { case (bucket, count) =>
            println(f"  $bucket%-25s: $count%10d")
        }
        
        spark.stop()
    }
}

7.2 带权重的 PageRank

并非所有链接都等价------可以根据链接位置(导航栏/正文/页脚)赋予不同权重。

scala 复制代码
// 权重边
case class LinkInfo(position: String, count: Int)

val weightedEdges: RDD[Edge[Double]] = rawData.map { line =>
    val parts = line.split("\t")
    val src = parts(0).toLong
    val dst = parts(1).toLong
    val position = parts(2)  // "nav", "body", "footer"
    
    val weight = position match {
        case "nav"    => 0.3   // 导航链接权重低
        case "body"   => 1.0   // 正文链接标准权重
        case "footer" => 0.1   // 页脚链接权重很低
        case _        => 0.5
    }
    
    Edge(src, dst, weight)
}

// 使用权重边运行 PageRank
val weightedGraph = Graph.fromEdges(weightedEdges, 1.0)
// 边权重在 aggregateMessages 的 sendMsg 中参与计算

7.3 个性化 PageRank(Personalized PageRank)

scala 复制代码
// 为每个类别设置偏好顶点
val personalization: RDD[(VertexId, Double)] = sc.parallelize(Seq(
    (1001L, 0.5),  // 科技类种子页面
    (2001L, 0.3),  // 体育类种子页面
    (3001L, 0.2),  // 娱乐类种子页面
))

val pprGraph = PageRank.runUntilConvergenceWithOptions(
    graph,
    tol = 0.0001,
    resetProb = 0.15,
    srcPrefs = Some(personalization)  // 传入个性化偏好
)

八、常见问题

8.1 悬挂节点(Dangling Nodes)

问题:出度为 0 的顶点,PR 值无法流出,导致总 PR"泄漏"。

GraphX 的处理方式

scala 复制代码
// GraphX 源码中的处理(简化)
val danglingMass = graph.vertices
    .filter { case (_, (pr, outDeg)) => outDeg == 0 }
    .map { case (_, (pr, _)) => pr }
    .sum()

// 将悬挂质量均匀分配
val additionalPR = danglingMass / numVertices

自查方法

scala 复制代码
// 检查是否有大量悬挂节点
val danglingCount = graph.vertices
    .filter { case (_, outDeg) => outDeg == 0 }
    .count()
val totalVertices = graph.vertices.count()
println(s"悬挂节点占比: ${danglingCount.toDouble / totalVertices * 100}%")

8.2 收敛判断

scala 复制代码
// 错误: 仅检查个别顶点
if (maxDiff < tol) converged = true

// 正确: 使用 L1 范数或所有顶点最大变化量
val l1Norm = g.vertices.join(prevG.vertices)
    .map { case (_, (newPR, oldPR)) => math.abs(newPR - oldPR) }
    .sum()
if (l1Norm / numVertices < tol) converged = true

8.3 内存溢出

现象 原因 解决
OutOfMemoryError: Java heap 图过大,缓存层面过多 增加 executor.memory,启用 checkpoint,降低缓存级别为 MEMORY_AND_DISK
Shuffle fetch failed shuffle 数据倾斜 repartition 增加分区数,使用 CanonicalRandomVertexCut
GC 频繁 迭代中不断创建新对象 使用 Kryo 序列化,复用 RDD

8.4 结果为空或异常

scala 复制代码
// 常见原因检查清单:

// 1. 是否所有顶点都可达?
val reachable = graph.connectedComponents()

// 2. 边方向是否反了?
// PageRank 要求 edge.src → edge.dst 表示 src 链接到 dst
// 如果数据是 dst 被 src 引用,需要反转
val correctGraph = graph.reverse  // 快速反转边方向

// 3. 迭代次数是否足够?
// 小图 20 轮,大图可能需 50-100 轮

总结

关键要点

复制代码
PageRank 本质 = 随机游走的稳态分布
              │
              ├── 静态实现: 全量同步迭代,简单但慢
              ├── 动态实现: 增量异步传播,快但需额外存储
              └── GraphX 优化: 顶点切割 + aggregateMessages + Pregel

实现选择决策树

#mermaid-svg-rQvBmik9y69iajB9{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-rQvBmik9y69iajB9 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-rQvBmik9y69iajB9 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-rQvBmik9y69iajB9 .error-icon{fill:#552222;}#mermaid-svg-rQvBmik9y69iajB9 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-rQvBmik9y69iajB9 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-rQvBmik9y69iajB9 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-rQvBmik9y69iajB9 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-rQvBmik9y69iajB9 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-rQvBmik9y69iajB9 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-rQvBmik9y69iajB9 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-rQvBmik9y69iajB9 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-rQvBmik9y69iajB9 .marker.cross{stroke:#333333;}#mermaid-svg-rQvBmik9y69iajB9 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-rQvBmik9y69iajB9 p{margin:0;}#mermaid-svg-rQvBmik9y69iajB9 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-rQvBmik9y69iajB9 .cluster-label text{fill:#333;}#mermaid-svg-rQvBmik9y69iajB9 .cluster-label span{color:#333;}#mermaid-svg-rQvBmik9y69iajB9 .cluster-label span p{background-color:transparent;}#mermaid-svg-rQvBmik9y69iajB9 .label text,#mermaid-svg-rQvBmik9y69iajB9 span{fill:#333;color:#333;}#mermaid-svg-rQvBmik9y69iajB9 .node rect,#mermaid-svg-rQvBmik9y69iajB9 .node circle,#mermaid-svg-rQvBmik9y69iajB9 .node ellipse,#mermaid-svg-rQvBmik9y69iajB9 .node polygon,#mermaid-svg-rQvBmik9y69iajB9 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-rQvBmik9y69iajB9 .rough-node .label text,#mermaid-svg-rQvBmik9y69iajB9 .node .label text,#mermaid-svg-rQvBmik9y69iajB9 .image-shape .label,#mermaid-svg-rQvBmik9y69iajB9 .icon-shape .label{text-anchor:middle;}#mermaid-svg-rQvBmik9y69iajB9 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-rQvBmik9y69iajB9 .rough-node .label,#mermaid-svg-rQvBmik9y69iajB9 .node .label,#mermaid-svg-rQvBmik9y69iajB9 .image-shape .label,#mermaid-svg-rQvBmik9y69iajB9 .icon-shape .label{text-align:center;}#mermaid-svg-rQvBmik9y69iajB9 .node.clickable{cursor:pointer;}#mermaid-svg-rQvBmik9y69iajB9 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-rQvBmik9y69iajB9 .arrowheadPath{fill:#333333;}#mermaid-svg-rQvBmik9y69iajB9 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-rQvBmik9y69iajB9 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-rQvBmik9y69iajB9 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rQvBmik9y69iajB9 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-rQvBmik9y69iajB9 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rQvBmik9y69iajB9 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-rQvBmik9y69iajB9 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-rQvBmik9y69iajB9 .cluster text{fill:#333;}#mermaid-svg-rQvBmik9y69iajB9 .cluster span{color:#333;}#mermaid-svg-rQvBmik9y69iajB9 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-rQvBmik9y69iajB9 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-rQvBmik9y69iajB9 rect.text{fill:none;stroke-width:0;}#mermaid-svg-rQvBmik9y69iajB9 .icon-shape,#mermaid-svg-rQvBmik9y69iajB9 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rQvBmik9y69iajB9 .icon-shape p,#mermaid-svg-rQvBmik9y69iajB9 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-rQvBmik9y69iajB9 .icon-shape .label rect,#mermaid-svg-rQvBmik9y69iajB9 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rQvBmik9y69iajB9 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-rQvBmik9y69iajB9 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-rQvBmik9y69iajB9 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 小图 < 100万顶点
中图 100万~1亿
大图 > 1亿




选择 PageRank 实现
图规模?
静态 PageRank

固定迭代次数
动态 PageRank

收敛容差模式
是否有新数据持续到达?
动态 PageRank + checkpoint

  • 分区优化
    增量 PageRank

  • 流式更新
    需要个性化?
    Personalized PageRank
    标准 PageRank

性能基准参考

图规模 顶点数 边数 迭代轮数 集群规模 耗时
10万 100万 20 4 × 8GB ~30s
100万 1000万 30 10 × 16GB ~5min
1亿 10亿 50 50 × 32GB ~30min
超大 10亿+ 100亿+ 80 200+ × 64GB ~2h

PageRank 不是搜索引擎的全部,但它是理解图排序思想的钥匙。 GraphX 用不到 300 行 Scala 代码完成了分布式 PageRank 的实现,背后是 Pregel 模型、顶点切割、aggregateMessages 等精心设计的抽象。