跳到主要内容

5GB 跑完十亿条边:把“图计算必须分布式”这条默认判断拆了

深潜
深潜

· 阅读约 7 分钟

Semyon Sinchenko 发的那篇文章,标题本身就像个玩笑:Apache DataFusion,5GB 内存硬限制,跑 graph500-26 的 PageRank。十亿出头条边,15 轮完整迭代,大约 30 分钟,结果和 ground truth 在 0.0001 容差内 100% 吻合。这还没完。他又加了一组:10GB 限制、8GB pool 下跑 twitter_mpi 的弱连通分量,20 亿条边,预处理阶段对称化加去重峰值接近 40 亿条边,22 次前向迭代加对应的反向传播,全流程 10 分钟内跑完,结果和 Graphalytics 的 ground truth 对得上。

这两个数字拆出来的含义,不是“DataFusion 有个新功能”。被撬动的是一句图计算圈子里默许了十几年的判断:图规模一旦超过单机内存,就只能上分布式框架。Sinchenko 自己开头也承认,他原本也是这么想的,最初打算用 Spark 加 GraphFrames,后来发现笔记本就够了。顺序很重要——是结果倒逼观念转变,不是反过来。

先把机制拆开。

PageRank 在 Pregel 批量同步并行模型下,每一轮迭代就两件事:把每个节点当前的 rank 沿出边传播出去,然后按节点聚合收到的值。翻译成 DataFrame 语言,第一件是 join,第二件是 aggregate。WCC 的逻辑也类似,Bögeholz 那篇 “In-database connected component analysis” 的核心思路就是反复做 join 和聚合,直到组件编号收敛。Sinchenko 之前给 Spark GraphFrames 写过同一套算法,这次只是把同样的逻辑用 DataFusion 的 DataFrame 重新表达了一遍。核心代码他自己都说很轻——甚至没怎么用 LLM 写,边学 Rust 边写——因为重活全在引擎里:溢写管理、sort-merge join 的执行、聚合、查询规划。数据结构就是一个 GraphFrame,vertices 和 edges 两个 DataFrame,和 Spark GraphFrames 那个结构对齐。

这里有个容易看岔的地方。很多人一听“单机跑十亿条边”,脑子里第一反应是把图塞进内存。DataFusion 这条路根本不是这么走的。它靠的是溢写:内存不够就往磁盘上写,读写走批量扫描,尽量避免随机访问。这恰好是 Pregel 这类 BSP 算法天然适配的顺序访问模式——每一轮迭代要读写的数据是有规律的、可批量化的,不是图数据库那种随机游走式的边查找。Sinchenko 的 Map-Reduce 思路就是顺着这个特性设计的:算法层按可批量扫描的方式组织,剩下的交给 DataFusion 的查询规划器去安排执行。

而这个执行是有硬约束背书的。Sinchenko 通过 systemd-run 给进程设的是内核级 cgroup 硬限制,不是开个监控看看的软限制。5GB 跑 PageRank 时 DataFusion pool 只给 4GB,10GB 跑 WCC 时 pool 给 8GB。这个 pool 大小的意思是:引擎在内存里真正能周转的窗口就那么点,其余全得靠溢写。WCC 那个案例尤其狠——原始 CSV 大约 30GB,对称化之后中间结果膨胀到 3,228,212,374 条边,按 8GB pool 算,平均每条边能分到的内存小到可以忽略。但 10 分钟内跑完了。这账如果按“分布式才能处理”的旧逻辑算,根本对不上。

当然不是全部顺。Sinchenko 在文章里写了两件具体的事:FairSpillPool 在极端场景下会死锁,以及 sort-merge join 没有利用磁盘上已经排好序的数据。这俩都是 DataFusion 溢写路径上的真实边界。我在意这些细节不是因为它们降低了文章的说服力,恰恰相反——一个实验报告如果没写哪里裂开了,反而可疑。FairSpillPool 的事说明溢写机制本身在极端压力下还有未解决的竞态,sort-merge join 那个说明查询规划器在“用户数据已经有序”这个语义上不够聪明。这些是当前方案的边界标记,不是让它垮掉的东西。边界内的部分,已经被两个独立的真实数据集验证过了。

然后看利益格局。

这件事真正绕开的,是一整套分布式计算栈的隐性成本。Spark 集群不是免费的。运维、调参数、处理数据倾斜、管理 checkpoint、给 executor 分资源,每一层都有成本。GraphFrames 作为 Spark 上的图分析层,本质上做的也是 join 和 aggregate——Spark 内部就是在做分布式 join、分布式聚合、网络间 shuffle 数据。当数据规模落在 10 亿到 20 亿条边这个区间,而单机加溢写加查询引擎能用磁盘顺序 IO 把同样的 join 和 aggregate 解决掉,那分布式框架的“必要性”就从技术问题变成了成本核算问题。单机 DataFusion 绕开的不是 Spark 的技术能力,是 Spark 那套集群资源调度、shuffle 网络传输、分布式状态维护的开销。账算下来,在某个规模以下,分布式反而是贵的那个选项。

另一层被绕开的是专用图处理系统。NetworkX 和 iGraph 这类库的瓶颈很明确:大多数图算法要求图完全驻留在内存里。graph500-26 这个量级,节点 3200 万,边 10 亿,光原始表示就不是 NetworkX 能承载的。传统上这个规模以上的选择就只剩下分布式图系统或者外存图系统。Sinchenko 的实验等于在两者之间凭空多出了一条路:一个通用的列式查询引擎,加上溢写,再加上 Pregel 的批量同步并行逻辑。DataFusion 不是图引擎,它甚至对图的语义毫无理解——它只知道对 DataFrame 做 join 和 aggregate。但正因为图算法在这一层可以被完全还原成关系操作,一个通用查询引擎就足够扛起来了。

这件事真正的分量在这:它把“图分析”从专用系统的问题,降维成了“关系操作加溢写”的问题。这个降维不是理论上的——是有人在 5GB 硬限制下跑完 10 亿条边、10 分钟内跑完 20 亿条边之后,用可复现的结果把降维变成了既成事实。Sinchenko 之前对用 DataFusion 做图分析持否定态度,这次实验完全改变了他的看法。作者本人都被自己的结果说服了——这类叙事通常比任何宣传都有力,因为它展示的是证据先于结论。

我的判断摆在这儿:在 10 亿到 20 亿条边这个规模区间,“图计算必须上分布式”已经不再是一条成立的默认判断。Pregel 类的批量同步并行算法完全可以在单机上用查询引擎的溢写机制来跑,结果可验证,成本结构明显更简单。这个判断的适用边界很具体——它限于那些算法上可以表达为 join 加 aggregate 的批量图计算。随机游走、实时图查询、亚秒级延迟的图遍历、需要大量随机边访问的算法,不在这个范围内。这些工作负载仍然需要能把图放进内存的系统,也就是说 NetworkX 和 iGraph 的瓶颈虽然被绕开了,但绕开的路线不经过它们。

这个判断会在什么条件下被推翻?三件事。

第一,如果 DataFusion 的溢写机制在极限压力下的死锁和排序数据优化问题长期无法解决,那溢写路径的鲁棒性就撑不起更大规模的负载,这个方案的适用边界会被锁死在当前区间。第二,如果数据规模再上一个数量级——百亿条边以上——单机磁盘 IO 带宽可能成为新瓶颈,即便溢写机制的软件层没问题,硬件层的顺序扫描速度也可能不够,分布式粒度就仍有必要。第三,如果在同一规模区间出现了内存更便宜更快的单机大内存方案——比如单机 512GB 甚至 1TB 内存成为这类工作负载的默认配置——那“溢写”这条路本身就会被内存兜底方案替代,整个论证的前提会变。这三条里,第一条和第三条在现有趋势下都有可能发生,第二条取决于数据增长速度和单机磁盘带宽增长哪个跑得快。

在任何一个推翻条件实际发生之前,我对这件事的基本判断不变。Sinchenko 的实验不是“单机图计算的一个有趣尝试”,它是对“大数据必须分布式”这条行业默认路径的一次有效证伪。证伪的范围被严格限定了,但证伪本身是成立的。

深潜
深潜

把一个行业趋势拆到商业+技术+利益格局,最后给一句明确判断。

查看主页 →