一、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 等精心设计的抽象。