图工程·研究全集

图工程:深度研究全集

深入探索 22 个图工程里程碑项目——从图数据库、分布式处理框架,到知识图谱、GNN 系统、路由引擎与编译器 IR。每篇案例研究包含架构分析、带权衡考量的设计决策,以及模拟设计者的第一人称思考。

22
案例研究
1084
收集来源
9
子领域
10
架构节点图

图工程分类体系

本研究将图工程组织为以下子领域,每个领域代表一种独特的工程实践:

对比矩阵

项目领域规模核心创新语言
Neo4j图数据库数十亿节点无索引邻接Java/Scala
Google Pregel图处理框架数百亿边以顶点为中心的 BSPC++
Facebook Tao社交网络图数万亿边图感知双层缓存C++
LLVM编译器程序图任意程序规模SSA 图作为通用 IRC++
Apache TinkerPop / Gremlin图查询语言依赖后端遍历机 + 厂商中立 SPIJava
Amazon Neptune图数据库数十亿三元组云原生双模型存储C++
GraphX (Apache Spark)图处理框架数十亿边图即 RDD(顶点切分)Scala
LinkedIn 经济图谱社交网络图9 亿+ 会员联邦图平台Java/Scala
Uber H3 与路由引擎生产图算法5 亿+ 节点收缩层次 + H3 六边形C++/Go
Bazel (Google Blaze)依赖图构建系统数百万目标内容寻址动作图Java/C++
Deep Graph Library (DGL)图神经网络1 亿+ 节点消息传递作为通用 GNN 原语Python/CUDA
Wikidata知识图谱1 亿+ 条目协作开放知识图谱PHP/Java
TigerGraph图数据库1000 亿+ 边编译 GSQL + 累加器C++
阿里巴巴 GraphScope图处理框架数十亿边统一交互+分析+学习C++/Python
GraalVM / Truffle编译器程序图任意程序规模节点之海 + 部分求值Java
ArangoDB图数据库数十亿文档/边多模型(文档+图+KV)C++
GraphChi图处理框架数十亿边滑动窗口磁盘处理C++
PyTorch Geometric (PyG)图神经网络数百万节点MessagePassing 基类Python/CUDA
Microsoft Academic Graph (MAG)知识图谱2.6 亿论文批量实体解析Spark/Python
Google Maps 路由引擎生产图算法1 亿+ 节点CH + CRP 结构/权重分离C++
Apache JanusGraph图数据库1000 亿+ 边可插拔存储后端Java
Twitter 社交图 (Manhattan)社交网络图4.5 亿用户混合扇出(写/读)Scala/Java

案例研究

图数据库Neo4j

Neo4j:原生图存储引擎

节点记录 #42关系记录节点记录 #97first_rel 指针next_node 指针prev_node 指针无索引邻接:每跳 O(1) 物理指针跟随固定大小记录 · 直接文件偏移 · 无 B 树查找

概述

Neo4j 是开创性的原生图数据库,由 Neo Technology(现 Neo4j 公司)于 2007 年首次发布。与通过连接表模拟图的关系数据库、或嵌入关系数据的文档存储不同,Neo4j 将属性图模型一直贯彻到存储层。每个节点都存储指向其关系的直接物理指针,每个关系都存储指向其起止节点的指针——这种结构称为"无索引邻接"(index-free adjacency),使得图遍历成为 O(1) 的本地操作,与数据库总规模无关。

这个项目源于一个具体的挫败感:创始人 Emil Eifrem 在关系数据库中建模复杂网络结构时发现,关系查询中每增加一跳就需要多一个 JOIN,导致性能指数级退化。这个洞见在当时是激进的——如果你的数据本质上是图,你的存储引擎就应该是图,而不是用外键假装成边的表。

Neo4j 已从单节点嵌入式 Java 库成长为分布式图平台,服务 NASA、UBS、Walmart 等企业。其查询语言 Cypher 已成为属性图查询的事实标准,并成为 ISO GQL 标准的基础。该系统在生产部署中处理数十亿节点和关系,支撑欺诈检测、推荐引擎、网络管理和知识图谱。

架构

Neo4j 的架构围绕三个核心原则构建:原生图存储、原生图处理和 ACID 一致性。

存储层。 存储引擎使用一组固定大小的记录文件,而非单一的巨型存储。主要文件包括:

  • neostore.nodestore.db — 节点的固定大小记录(旧格式每个 15 字节)。每个节点记录包含:标志字节、第一个关系的 ID、第一个属性的 ID 和标签字段。
  • neostore.relationshipstore.db — 关系的固定大小记录(34 字节)。每条记录存储:第一个节点 ID、第二个节点 ID、关系类型、两个节点各自的前/后关系 ID、第一个属性 ID。
  • neostore.propertystore.db — 属性键值对,以链表形式挂在每个节点或关系下。
  • neostore.relationshipgroupstore.db — 针对稠密节点(关系类型很多),按类型分组关系以加速访问。

关键设计选择:这些是带物理指针的固定大小记录(记录 ID 直接映射到文件偏移量)。记录 ID 为 42 的节点位于字节偏移 42 × 记录大小处。从节点遍历到其关系只需一次磁盘寻址(或一次页缓存命中)——无需索引查找、无需哈希计算、无需 B 树遍历。

页缓存。 Neo4j 实现了自己的页缓存(MuninnPageCache),而不是依赖操作系统文件缓存。这带来了:可预测的淘汰策略(时钟扫描,而非 LRU)、对刷盘行为的直接控制以保证 ACID、无 mmap TLB 压力的类内存映射性能、基于校验和的逐页损坏检测。

事务层。 所有变更都经过带完整 ACID 语义的预写日志(WAL)。事务日志记录物理操作(而非逻辑操作),实现快速崩溃恢复。Neo4j 对每个数据库使用单写者模型(一次一个写事务),配合乐观读并发——读者永不阻塞写者,反之亦然。

查询处理。 Cypher 查询被编译为算子流水线:解析器 → AST → 语义分析 → 基于成本的计划选择(使用图统计信息)→ 流水线执行(企业版支持 morsel 驱动并行)。

分布式架构(Infinigraph / Fabric)。 现代 Neo4j(5.x+)引入:通过 Fabric 分片(跨多数据库的查询联邦)、因果集群(基于 Raft 共识的写复制)、将查询路由到适当分片的复合数据库。

设计决策

决策 1:固定大小记录 vs 变长存储。

团队选择了固定大小记录,尽管存在空间浪费(没有关系和属性的节点仍占 15 字节)。替代方案——变长记录(如 PostgreSQL 的堆元组)——需要一个间接层(槽目录或溢出页)来定位记录,每次遍历多加一次指针跳转。

权衡:2007 年磁盘寻址约 10ms。每多一次间接寻址,每次遍历就加 10ms。对 5 跳查询,变长记录意味着 50ms 纯寻址开销,而固定大小记录几乎为零。空间成本(约 2 倍存储开销)可以接受,因为内存相对延迟是便宜的。

被拒绝的替代方案:对节点 ID 建 B 树索引(如关系主键索引)。这会使每次遍历从 O(1) 变成 O(log N)。对十亿节点图,每跳多 30 次比较。

决策 2:无索引邻接 vs 全局关系索引。

Neo4j 不维护全局关系索引(那样可以高效"查找所有类型 X 的关系"),而是将关系存储为挂在每个节点下的双向链表。要找到节点的所有 FRIEND_OF 关系,需遍历该节点的关系链并按类型过滤。

权衡:这使"给定节点的所有关系"成为 O(度数) 而非 O(1) 查找 + O(k) 结果。但它使"从节点 A 遍历到其邻居"成为每跳 O(1)——图工作负载的主导操作。替代方案(全局索引)会优化罕见的"扫描所有边"查询,代价是常见的"遍历本地邻域"查询。

对稠密节点(拥有数百万关系的节点,如热门产品),Neo4j 增加了关系组——一种按类型划分节点关系的二级结构,避免全链遍历。

决策 3:单写者并发模型。

Neo4j 对每个数据库使用单一写锁(一次一个写事务),而非多写者 MVCC。理由:图变更本质上是相互关联的——创建关系会触及两个节点,对共享邻域的并发修改会产生复杂的冲突解决。单写者以写入吞吐为代价,消除所有写-写冲突。

考虑过的替代方案:带冲突检测的乐观并发(如 Datomic)。被拒绝,因为图工作负载冲突率高(许多事务触及共享枢纽节点),使乐观重试代价高昂。读路径完全并发——读者通过事务日志序列号看到一致快照。

决策 4:自定义页缓存 vs 操作系统缓存 / mmap。

早期 Neo4j(1.x)使用 Java NIO 内存映射文件。后来放弃,因为:mmap 无法控制淘汰——操作系统基于全局 LRU 而非图访问模式淘汰页;大映射导致 TLB 抖动;无法实现校验和或保证 ACID 的刷盘顺序。自定义 MuninnPageCache 使用时钟扫描淘汰、固定 8KB 页和事务提交时的显式刷盘屏障。

模拟设计者思考

我是 Emil,2006 年,我正盯着一个 4 跳社交网络查询的 MySQL 查询计划。EXPLAIN 输出显示 7 个嵌套循环连接。查询在 200 万行上耗时 12 秒。我需要它低于 100 毫秒。

根本问题是阻抗失配。我的数据是图——人连接人。但我的存储是表。每个关系是连接表中的一行,每次遍历是一个需要索引查找的 JOIN。B 树给我每跳 O(log N)。4 跳就是 4 × log(200万) ≈ 84 次索引比较,每次都可能是磁盘寻址。

如果我直接存储指针呢?每个节点记录有一个字段:"first_relationship_id"。每个关系记录有:"node_A 的下一个关系"和"node_B 的下一个关系"。这样遍历就是:读节点记录(一次寻址),跟指针到关系(一次寻址),跟指针到下一个节点(一次寻址)。每跳三次寻址,全部在小工作集内顺序进行。

但等等——固定大小记录。如果节点有可变数量的属性,就不能内联存储。我需要属性链:节点 → 第一个属性 → 下一个属性 → ……这增加了属性访问的寻址。但关键洞见是:在遍历中,通常直到最终结果集才需要属性。遍历本身只需要拓扑——节点 ID 和关系指针。属性是独立关注点。

空间开销让我困扰。没有关系和属性的节点仍占 15 字节。十亿节点就是 15GB 几乎为空的记录。但 2006 年内存是 50 美元/GB 且在下降。15GB 工作集可以放进商用服务器的内存。替代方案——带槽目录的变长记录——省空间但每次访问加一层间接。我用空间换延迟。

并发是难点。如果两个事务都想在节点 A 和 B 之间创建关系,都需要更新 A 的关系链和 B 的关系链。用单一全局写锁,这很简单——但写吞吐受损。用细粒度锁,需要以一致顺序锁四个记录以避免死锁。锁管理器本身成为瓶颈。

我决定先用单写者。论点:图工作负载读密集(社交网络、推荐引擎典型读写比 100:1)。单写者+并发读给我 100% 读吞吐和"足够好"的写吞吐。如果写成为瓶颈,以后可以按子图分片。但如果现在就构建多写者,我会花两年在冲突解决上,什么都发布不了。

页缓存的决策更清晰。Java 的 mmap 是个陷阱——看似简单但对淘汰零控制。当工作集超过物理内存,操作系统开始淘汰我 10 毫秒后还需要的页,p99 延迟从 5ms 飙到 500ms。我需要自己图感知淘汰的缓存。时钟扫描很便宜(无 LRU 的逐访问 CAS),足够好地近似最近性。我用 8KB 页——大到分摊寻址成本,小到不读无用数据。

决策已定:固定大小记录、物理指针、无索引邻接、单写者 ACID、自定义页缓存。2007 年,我将以嵌入式 Java 库发布。如果我对"图是一等数据模型"的判断正确,世界最终会需要它作为服务器。但首先,证明存储模型可行。

关键启示与行业影响

Neo4j 最持久的贡献不是存储引擎——而是"图值得一级存储"的概念证明。在 Neo4j 之前,行业共识是带递归 CTE 的关系数据库"足够"处理图工作负载。Neo4j 证明:对连接数据(社交网络、欺诈团伙、知识图谱),关系存储的阻抗失配对遍历查询造成 10-100 倍性能惩罚。这不是渐进改进——而是对新数据模型的范畴性论证。

无索引邻接原则影响了其后所有图数据库。即使不使用固定大小记录的系统(Neptune、ArangoDB)也优化 O(1) 邻居访问,承认遍历局部性是定义性工作负载。该原则可推广到数据库之外:任何主导操作是"跟指针到邻居"的系统都受益于将指针与记录共置。

Neo4j 的单写者并发模型被证明既是最大优势也是扩展限制。对读密集 OLTP(80% 场景),它是理想的——零读竞争、一致性推理简单。对写密集导入(加载十亿边),它是瓶颈。向 Infinigraph(分片)和 Fabric(查询联邦)的演进代表 Neo4j 承认单写者模型无法扩展到最大部署。简单性(单写者)与规模(分布式写)的张力在图数据库设计中仍未解决。

Cypher 查询语言(现为 ISO GQL 基础)验证了声明式模式匹配方法。替代方案(Gremlin 的命令式遍历)更具表达力但更难优化。Cypher 的模式语法 (a)-[:KNOWS]->(b) 非专家也可读,这推动了数据库团队之外(商业分析、数据科学)的采用。教训:查询语言的可读性是分发优势。

运维上,Neo4j 教会行业图数据库需要不同于关系数据库的运维专长。查询调优关乎遍历模式(最小化跳数、用索引确定起点),而非连接顺序。容量规划关乎工作集大小(热子图是否装进页缓存),而非表大小。监控关乎遍历深度分布,而非查询执行时间百分位。

图处理框架Google Pregel

Google Pregel:以顶点为中心的革命

V1V2V3V4msgmsgmsg全局同步屏障(超步边界)V1'V2'V3'V4'Pregel BSP:以顶点为中心 + 体同步并行Compute() → 发消息 → 屏障 → 下一超步

概述

Pregel 由 Google 于 2010 年发表(Malewicz 等,《Pregel:大规模图处理系统》,SIGMOD 2010),是定义分布式图计算范式的系统。在 Pregel 之前,处理数十亿边的图意味着要么编写自定义 MapReduce 作业(笨拙,迭代算法需多次传递),要么使用专用并行硬件。Pregel 引入了"以顶点为中心"(think like a vertex)的抽象:程序员编写一个运行在每个顶点上的单一函数,接收邻居的消息并向邻居发送消息,系统负责分布式、容错和同步。

该系统源于 Google 的一个实际危机:在整个网络图(数百亿边)上计算 PageRank 用 MapReduce 已不可行。PageRank 的每次迭代都需要一个完整的 MapReduce 作业——洗牌整个边列表、聚合、重复。40 次迭代就是 40 次对 PB 级数据的完整洗牌。Pregel 团队在 Jeff Dean 和 Sanjay Ghemawat 领导下由 Grzegorz Malewicz 带领,认识到图算法本质上是迭代且局部的——每个顶点只需要邻居的信息——并围绕这一洞见设计了系统。

Pregel 的影响远超 Google。它直接启发了 Apache Giraph(Hadoop 生态)、GraphX(Spark)、Flink Gelly 和数十个学术系统。它推广的体同步并行(BSP)模型成为分布式图处理十年的标准抽象。即使是超越 BSP 的系统(如 PowerGraph 和 GraphChi)也是以反对 Pregel 的设计选择来定义自己的。

架构

Pregel 的架构实现了为图计算改编的 BSP(体同步并行)模型:

计算模型。 计算按超步(superstep)序列进行。每个超步中:

  1. 每个活跃顶点接收上一超步发给它的消息
  2. 顶点的 `Compute()` 函数执行:读取消息、修改自身值、向邻居发送消息(通过边标识)、可选地投票停止
  3. 全局同步屏障分隔超步——顶点在当前超步内看不到消息,直到下一超步开始

顶点程序表达为:


void Compute(MessageIterator* msgs) {
  // 读消息,更新自身值
  // 通过 SendMessageTo() 向邻居发消息
  // 完成时 VoteToHalt()
}

分布式。 图被划分到多个工作节点(通常数千台商用机器)。每个工作节点拥有一组顶点及其出边。划分使用顶点 ID 的哈希——简单、无状态,配合随机 ID 分配可对幂律图负载均衡。

消息传递。 跨工作节点的消息在缓冲区聚合并通过 TCP 传输。工作节点内消息在内存中投递。系统对同一顶点的消息进行聚合(可交换聚合)以减少网络流量——对 PageRank,不是发送 1000 个独立的排名贡献,而是工作节点求和后发送一个聚合值。

容错。 工作节点按可配置间隔对顶点状态做检查点。工作节点故障时,主节点将故障节点的分区重新分配给存活节点,从最近检查点恢复并重放。检查点成本与顶点数成正比,与边数无关(边来自输入图)。

主从协调。 单一主进程协调超步:分配分区、检测故障、管理检查点、决定何时终止(所有顶点投票停止且无消息在途)。

输入/输出。 图以邻接表格式从分布式存储(GFS/Colossus)读取。输出写回为顶点-值对。系统支持计算中的图变更(增删边),但很少使用。

设计决策

决策 1:BSP 同步 vs 异步消息传递。

团队选择超步间的全局屏障,而非允许顶点在消息到达时立即处理(异步)。权衡:

  • BSP 优势:确定性。结果与消息投递顺序无关,调试可行。顶点在超步 k 的行为仅依赖超步 k-1 的消息——无竞争、无部分读取。
  • BSP 优势:容错简单。每个屏障做检查点;故障时从最近屏障重启。无需跟踪部分消息投递。
  • BSP 成本:落后者。一个工作节点慢,所有工作节点在屏障等待。对幂律图,枢纽顶点(数百万边)造成负载不均。
  • BSP 成本:不必要的等待。3 次迭代就收敛的顶点仍需等待最慢顶点完成 40 次迭代。

被拒绝的替代方案:完全异步(如 GraphLab)。团队判断确定性和简单容错比异步带来的 10-30% 吞吐增益更值,尤其考虑到 Google 内部网络工作节点速度方差低。

决策 2:以顶点为中心的抽象 vs 以边/子图为中心。

程序员以单一顶点的视角思考,而非整个图。这是刻意的 API 设计:

  • 优势:心智模型是局部的。"我是一个节点。我接收邻居的值。我计算新值。我发给邻居。"这直接映射教科书描述图算法的方式(Bellman-Ford、标签传播、连通分量)。
  • 成本:某些算法天然是以边为中心(三角形计数)或以子图为中心(社区发现)。以顶点表达需要笨拙的多阶段协议。
  • 成本:抽象隐藏了分区。一个有 100 个工作节点跨边的顶点每个超步产生 100 条网络消息。程序员无法共置相关顶点。

考虑过的替代方案:数据流 API(如现代 Flink/Gelly)。2009 年被拒绝,因为团队想要一个最小 API,能在 1000+ 台机器上高效实现且容错简单。那个时代的数据流引擎(Dryad、Flume)处理迭代计算不佳。

决策 3:哈希分区 vs 图感知分区。

顶点按 hash(vertex_id) % num_workers 分配到工作节点。这忽略图结构——相邻顶点可能落在不同工作节点,最大化跨节点消息。

权衡:图感知分区(METIS、谱聚类)可减少 5-10 倍跨节点边,但需要一个本身昂贵的预处理过程(O(E log V)),且图变化时需重跑。对 Google 的用例(每天重算网络图 PageRank),预处理成本不合理——哈希分区"足够好",因为网络图的幂律分布意味着大多数边来自低度顶点,聚合优化(对同一目的地的消息求和)分摊了跨节点成本。

被拒绝的替代方案:基于复制的分区(PowerGraph 方法,2012 年发表)。设计时不可用,且团队判断跨工作节点维护复制顶点状态的复杂性,对他们的工作负载而言超过了消息节省。

决策 4:基于检查点的容错 vs 复制。

Pregel 不跨多个工作节点复制顶点状态(昂贵:2-3 倍内存),而是定期做检查点到分布式存储。故障时,受影响的分区被重新分配并从检查点恢复。

权衡:检查点增加延迟(每 N 个超步将顶点值写入 GFS)和恢复时间(从最近检查点重放)。但它避免了复制的持续开销和变更期间跨副本维护一致性的复杂性。

团队观察到工作节点故障罕见(千机作业中每工作节点平均无故障时间为小时级),每 10 个超步做一次检查点将重放浪费限制在总计算量的 10% 以内。

模拟设计者思考

我是 Grzegorz,2008 年末,我和 Jeff、Sanjay 在会议室。白板上的问题:在 200 亿边上做 PageRank,MapReduce 每次迭代 6 小时。40 次迭代。10 天。我们需要总时间低于 6 小时。

MapReduce 表述就是问题。每次迭代:Map 为每条边发出 (destination_vertex, rank_contribution)。Shuffle 按目的地排序。Reduce 对每个顶点求和。这是每次迭代对 200 亿键值对的完整排序。网络是瓶颈——我们在洗牌 PB 级数据。

如果不洗牌呢?如果每个顶点只是……和邻居对话?网络图以邻接表格式存储。如果我按机器划分顶点并本地保留其出边,那么顶点可以通过接收入邻居的贡献来计算新排名。唯一的网络流量是排名值本身——每超步每条边一个 double,而非完整排序。

但同步。如果顶点 A 发排名给 B,B 发给 C,C 何时看到 B 的更新?如果立即(异步),就有竞争——C 可能在 B 写新值时读到 B 的旧值。结果依赖时序。对计算影响搜索质量的 PageRank 的生产系统,这不可接受。

所以:屏障。所有顶点同时计算,然后全局同步,然后所有顶点看到新消息。这是 BSP——Valiant 1990 年的模型。它是确定性的。无论机器速度或网络延迟,结果相同。容错变得简单:每个屏障做检查点,故障时从最近检查点重放。

API 问题:程序员写什么?我想要新工程师一小时能学会的东西。"你是一个顶点。你接收消息。你计算。你发送消息。完成时停止。"就这些。没有分区逻辑、没有网络代码、没有故障处理。系统做所有这些。

Jeff 问:"需要全局信息的算法怎么办?比如'最大排名变化低于 epsilon 时停止'?"我需要组合器——顶点可以投票停止,主节点检查是否全部投票。还有聚合器——每个超步计算并广播给所有顶点的全局归约(求和、最大、最小)。这些是逃生通道,但核心模型保持顶点局部。

Sanjay 追问容错:"一千台机器意味着每几分钟一次故障。不能从头重启。"检查点。每 N 个超步将顶点值写入 GFS。故障时重新分配分区,从检查点恢复,重放。成本:每检查点每顶点一次 GFS 写入。十亿顶点、8 字节值,每检查点 8GB。每 10 个超步一次检查点,可以接受。

分区问题困扰我。哈希分区意味着顶点的邻居分散在所有工作节点。对度数 1000 的顶点,每超步可能有 1000 条网络消息。但网络图是幂律的:大多数顶点度数 < 10。高度顶点(数百万边)很少,我们可以聚合同一目的地的消息。每超步预期跨节点消息是 O(E / W),W 是工作节点数——和边数除以并行度一样。不比 MapReduce 的洗牌差,但没有排序。

我信服了。BSP + 以顶点为中心 + 哈希分区 + 检查点容错。它不是每个图算法的最优——三角形计数会很笨拙,幂律枢纽会造成落后者。但对 80% 的场景(迭代顶点更新算法:PageRank、连通分量、最短路径、标签传播),它比 MapReduce 快 10 倍。API 简单到团队一个下午就能实现新图算法,而不是一周。

我们叫它 Pregel。像 Euler,但带 P。因为它并行处理图。论文将发表在 SIGMOD 2010。

关键启示与行业影响

Pregel 最重要的贡献是概念性的,而非技术性的:"像顶点一样思考"的抽象使分布式图编程变得可及。在 Pregel 之前,并行图算法需要消息传递接口(MPI)、分布式哈希表或自定义 MapReduce 管道的专业知识。Pregel 将其简化为单一函数:"每个顶点收到消息时做什么?"这个抽象催生了一代图处理系统(Giraph、GraphX、Flink Gelly),使图分析成为数据工程的标准工具。

BSP 同步模型被证明既是 Pregel 的优势也是局限。确定性(无论机器速度如何,相同输入总产生相同输出)使调试可行、结果可复现——对计算影响搜索质量的 PageRank 的生产系统至关重要。但全局屏障造成"落后者问题":一个慢工作节点拖累所有工作节点。对幂律图(枢纽顶点有数百万边),处理枢纽的工作节点成为落后者。这一局限推动了下一代系统:PowerGraph(枢纽的顶点复制)、GraphLab(异步更新)、GraphChi(避免分布式的单机磁盘处理)。

哈希分区决策——忽略图结构——对 Google 的工作负载是务实且基本正确的。网络图的幂律分布意味着大多数顶点度数低,聚合优化分摊了跨工作节点通信。但对有强社区结构的图(社交网络、生物网络),图感知分区可减少 5-10 倍通信。教训:分区策略应匹配图拓扑,没有单一策略普遍最优。

Pregel 对"图作为一等计算原语"运动的影响怎么强调都不为过。2010 年之前,图处理是数据库研究的小众领域。Pregel 之后,它成为主流系统话题:每个主要数据平台(Spark、Flink、Hadoop)都增加了图处理能力。以顶点为中心的模型成为分布式图计算的"Hello World"——每个学生学习的第一个抽象,即使研究已经超越了它。

运维教训:基于检查点的容错在故障罕见且重放便宜时"足够好"。Pregel 的检查点-重放模型(对比复制)用恢复时间(分钟级)换取恒定开销(检查点之间为零)。在 Google 的规模(千机作业),这是正确的权衡——故障不频繁,复制成本(2-3 倍内存)令人却步。教训:让容错机制匹配故障频率,而非最坏情况。

社交网络图Facebook Tao

Facebook Tao:行星级规模的社交图

客户端Follower 缓存Follower 缓存Leader(单写)MySQL未命中未命中Tao 双层缓存:读走 Follower,写走 Leader每秒 10 亿+ 读取 · 关联列表按时间排序

概述

Tao(The Associations and Objects,关联与对象)是 Facebook 用于社交图的分布式数据存储,发表于 USENIX ATC 2013(Bronson 等)。它服务于支撑 Facebook 所有产品的基础数据结构:谁和谁是朋友、谁点了哪个帖子的赞、谁关注了哪个主页。到 2013 年,Tao 在拥有数万亿边的图上每秒处理超过十亿次读取,使其成为有史以来部署的最大图存储系统之一。

Tao 并非设计为 Neo4j 意义上的"图数据库"。它不支持任意图遍历或 Cypher 这样的查询语言。相反,它是一个高度专门化的读优化存储,只做两种操作:"给我这个对象的关联(边)"和"给我这个对象(节点)的数据"。洞见在于:Facebook 的工作负载 99.9% 是本地邻域的读取——"显示我的朋友"、"显示这个帖子的点赞"——而非多跳遍历。通用图数据库会浪费大量资源支持 Facebook 从不使用的能力。

该系统从早期架构演进而来,早期社交图存储在带 memcached 层的 MySQL 中。那个系统遇到根本限制:MySQL 的复制延迟使图在跨数据中心间不一致,memcached 缺乏图感知意味着缓存失效是噩梦。Tao 用单一系统取代了两者,该系统原生理解图结构。

架构

Tao 的架构是由持久化存储支撑的两层分布式缓存:

数据模型。 两个原语:

  • 对象(Objects):带类型、一组类型化数据字段和版本的实体。例如:用户、帖子、照片、主页。存储为 (id, type, data_map, version)。
  • 关联(Associations):对象间带类型、有向的边,带可选数据和时间字段。例如:FRIEND(用户→用户)、LIKE(用户→帖子)、AUTHOR(用户→帖子)。存储为 (id1, atype, id2, time, data, version)。

关联存储在关联列表中:对给定的 (id1, atype),按时间(降序)排列的所有 id2 值列表。这是关键结构——"给我最近点赞这个帖子的 20 个人"就是关联列表上的范围扫描。

两层缓存。

  • Leader 层:每个区域少量 leader 服务器,各自负责 ID 空间的一个分区。Leader 是唯一写入后端存储(MySQL)的层,也是唯一使 follower 失效的层。
  • Follower 层:每个数据中心数千台 follower 服务器,处理所有读取。Follower 缓存对象和关联列表。缓存未命中时,follower 从其 leader 读取(不直接读 MySQL)。

写路径。 写入流向:客户端 → leader → MySQL → leader 缓存更新 → follower 失效(异步)。Leader 是每分区的唯一写者,完全消除写冲突。Follower 失效是异步的——写入后几秒内 follower 可能提供陈旧数据。

读路径。 读取流向:客户端 → follower(缓存命中?返回)→ leader(缓存命中?返回)→ MySQL。Follower 以 TTL 和最大容量淘汰缓存关联列表。热列表(名人的粉丝)缓存在每个 follower;冷列表(私密用户的好友)按需获取。

后端存储。 带自定义 schema 的 MySQL。对象是 objects 表中的行。关联是 associations 表中的行,带 (id1, atype, time) 复合索引。MySQL 层提供持久性,是缓存未命中的真相来源。

地理分布。 每个地理区域有自己的 leader + follower。跨区域一致性通过 MySQL 复制(异步,最终一致)实现。美国的写入通过 MySQL binlog 复制传播到欧盟 leader,然后使欧盟 follower 失效。典型跨区域陈旧度:1-2 秒。

设计决策

决策 1:专用存储 vs 通用图数据库。

团队明确拒绝使用图数据库(Neo4j、Titan 等)。理由:

  • Facebook 的访问模式绝大多数是单跳:"用户 X 的好友"、"帖子 Y 的点赞"。多跳遍历("朋友的朋友")由应用层计算,而非存储层。
  • 通用图数据库为遍历优化(无索引邻接、图查询规划)。Facebook 不遍历——它获取列表。这种优化被浪费。
  • 规模:每秒 10 亿+ 读取需要缓存优先架构。2012 年的图数据库是磁盘优先、可选缓存。把分布式缓存改造到 Neo4j 上比构建缓存原生存储更难。
  • 运维简单:MySQL 作为后端存储意味着团队复用现有 MySQL 专业知识、工具和运维流程。新存储引擎意味着新的故障模式分类。

接受的权衡:没有图查询语言、没有多跳遍历、数据库中没有图算法。应用层(PHP/Hack,后为 C++)通过发起多个单跳读取来组合多跳查询。

决策 2:Leader-Follower(每分区单写者)vs 多主。

每个分区恰好一个接受写入的 leader。这完全消除写冲突——无需 CRDT、向量时钟或冲突解决。

权衡:写吞吐受 leader 容量限制。但 Facebook 的工作负载 99.9% 是读取。写入(新友谊、点赞)相对读取(信息流渲染、主页浏览)很少。每分区单个 MySQL 实例轻松处理写负载。

被拒绝的替代方案:带冲突解决的多主(如 Cassandra)。解决冲突的关联列表变更(两个用户同时从不同区域成为朋友)的复杂性,被认为不值得边际写吞吐增益。

决策 3:按时间排序的关联列表 vs 无序集合。

关联按时间(最新优先)存储。这是刻意的反规范化:

  • 主导查询是"最近 K 个关联"(最近的点赞、评论、好友)。时间排序使其成为前缀扫描。
  • 替代方案(无序集合 + 时间的独立索引)需要每个"最近"查询做二级索引查找——多一次往返。
  • 成本:在列表中间插入关联(回填日期)很昂贵。但实际中回填日期很少。

决策 4:MySQL 后端存储 vs 自定义存储引擎。

尽管有局限(单节点写扩展、无原生图操作),团队保留 MySQL 作为真相来源。理由:

  • MySQL 免费提供 ACID、崩溃恢复、复制和运维工具。
  • 缓存层(Tao leader + follower)吸收 99.9% 的读取。MySQL 只见缓存未命中和写入——总流量的极小部分。
  • 构建自定义存储引擎意味着从零构建崩溃恢复、复制和运维工具。约 6 名工程师的团队在 MySQL 对残余负载"足够好"时无法证明其合理性。

考虑过的替代方案:RocksDB 作为后端存储(Facebook 自己的创造)。2012 年被拒绝,因为 RocksDB 尚不支持 MySQL 提供的复制和运维工具。后来的系统(如社交图的后继组件)确实迁移到了基于 RocksDB 的存储。

模拟设计者思考

我是 Nathan Bronson,2011 年,我正看着我们基础设施的图。社交图存储在 MySQL 中。前面是:memcached。数千个 memcached 实例。它正在崩溃。

问题是失效。当 Alice 和 Bob 成为朋友,我们需要失效:Alice 的好友列表(500 台服务器的 memcached 中)、Bob 的好友列表(另外 500 台)、以及包含他们中任何一人的每个"朋友的朋友"缓存。每次写入数千条失效消息。而 memcached 不理解图——它只有键。我们手动跟踪哪些缓存键依赖哪些图边。这是纸牌屋。

如果缓存理解图呢?如果不是任意键,缓存存储"关联列表"——顶点的边?那么失效就简单了:当边变化时,失效它所属的关联列表。一次写入 → 每个受影响列表一次失效。而不是数千次。

但我们不能只替换 memcached。我们需要持久性。如果缓存服务器宕机,不能从头重建——那会让 MySQL 遭遇惊群。我们需要后端存储。我们需要一致性——如果 Alice 的好友列表缓存在东京和纽约,她在纽约添加朋友,东京必须最终看到。

两层。Leader:少量,每区域,写 MySQL,维护权威缓存。Follower:大量,每数据中心,只读缓存,未命中时从 leader 获取。写入经过 leader。读取命中 follower。失效流向:leader → follower(异步)。这给我们:单写者(无冲突)、读扩展(加 follower)、有界陈旧度(失效延迟 ≈ 区域内 1 秒)。

MySQL 问题。要构建自定义存储吗?团队六个人。MySQL 给我们:ACID、崩溃恢复、binlog 复制(跨区域)、运维工具(备份、监控、故障转移)。是的,它不是图数据库。是的,它不能做多跳遍历。但我们不需要存储中的多跳——应用层通过发起多个单跳读取来做。缓存吸收 99.9% 的读取,所以 MySQL 只需处理约 100 万读/秒(缓存未命中)和约 10 万写/秒。分片 MySQL 集群可以处理。

数据模型。对象和关联。不是节点和边——那是图数据库的语言。对象有类型和数据。关联有类型、方向和时间。时间字段至关重要:"显示这个帖子最近的 20 个点赞"是第一大查询模式。如果关联按时间存储,这就是前缀读取。无二级索引。无排序。只读前 20 项。

一致性模型。区域内:写后读一致(leader 同时服务读写,所以写入通过该 leader 对后续读取立即可见)。跨区域:通过 MySQL 复制的最终一致(1-2 秒延迟)。这可以接受,因为:(1) 大多数读取是本地的(你看到自己区域的数据),(2) 跨区域读取罕见(查看另一个区域朋友的主页),(3) "Alice 和 Bob 是朋友吗"的 1-2 秒陈旧度对用户不可见。

规模检查。十亿用户,每人约 300 个好友。这是 3000 亿关联条目。每条约 100 字节,30TB。MySQL 一个实例装不下。按对象 ID 分片:每个 leader 拥有一段 ID 范围。1000 个 leader × 30GB = 30TB。Follower 缓存热集——也许 10% 的关联是"热"的(名人、热门帖子)。这是所有 follower 上 3TB 缓存。可管理。

我们不是在构建图数据库。我们在构建图感知缓存,带持久化后端存储。区别很重要:图数据库为遍历优化。我们为"给我这个顶点的边列表,按时间排序,限制 K 个"优化。这是不同的问题,也是服务十亿用户社交网络真正重要的问题。

关键启示与行业影响

Tao 最重要的教训是:"图数据库"并非总是图工作负载的正确抽象。Facebook 的社交图是图——但访问模式不是遍历。而是"给我这个顶点的边列表,按时间排序,限制 K 个"。这是列表获取,不是遍历。通用图数据库(Neo4j,带查询规划器、遍历引擎、无索引邻接)为 Facebook 从不执行的操作优化。Tao 为 Facebook 每秒执行数十亿次的操作优化:单跳、时间排序、限量读取。教训:为访问模式设计,而非为数据模型设计。

Leader-Follower 架构(每分区单写者、多只读 follower)成为读密集分布式系统的标准模式。Tao 证明对 999:1 的读写比,写瓶颈无关紧要——每分区单个 MySQL 实例轻松处理写负载。读规模来自添加 follower(无状态缓存)。这一模式影响了 Instagram 的社交图、Twitter 的时间线服务、LinkedIn 的连接图。教训:当读取主导时,优化读路径(缓存、复制),接受简单的写路径(单写者、无冲突解决)。

"MySQL 作为后端存储"的决策验证了在不显眼层使用成熟基础设施的原则。缓存(Tao)是创新。后端存储(MySQL)是商品。保留 MySQL,团队利用了十年的运维专业知识、工具和社区知识。替代方案(自定义存储引擎)会消耗团队全部带宽在崩溃恢复、复制和运维工具上——MySQL 已解决的问题。教训:在重要的层(缓存)创新,在不重要的层(存储)商品化。

Tao 的最终一致模型(跨区域 1-2 秒陈旧度)证明用户对社交图读取不需要强一致性。当 Alice 在纽约添加 Bob 为好友,Bob 在东京的好友列表 2 秒内更新。没有用户注意到。但如果"添加好友"按钮需要 500ms(同步跨区域写入),每个用户都会注意到。教训:延迟是可见的,一致性是不可见的(直到它出问题)。为可见指标优化。

关联列表数据模型(时间排序的边)预示了"信息流即图"范式。每个社交信息流(Facebook、Instagram、Twitter、LinkedIn)都是时间排序的边列表:"你关注的人的帖子",按时间排序,限制 K 个。Tao 的关联列表就是信息流的数据结构。图抽象(节点和边)直接映射到产品抽象(实体和活动)。教训:当你的产品是信息流时,你的存储应该是时间排序的邻接列表。

编译器程序图LLVM

LLVM:图结构化的编译器基础设施

Entry 块if 分支else 分支Merge (PHI)cond=truecond=false%v1%v2控制流图 + SSA 值图 · PHI 节点合并数据流

概述

LLVM(Low Level Virtual Machine,现官方名称就是"LLVM")是 Chris Lattner 于 2003 年在伊利诺伊大学发起的编译器基础设施项目。虽然它不是传统意义上的"图数据库"或"图处理系统",但它是计算史上最成功的图工程项目之一。其整个架构建立在图数据结构之上:控制流图(CFG)、数据依赖图、调用图、支配树和 SSA(静态单赋值)值图。LLVM 中的每个优化 pass 都是图算法——死代码消除是图剪枝、循环不变量代码外提是图分析、寄存器分配是图着色。

LLVM 的成功以其普及度衡量:它支撑 Clang(Apple 工具链中取代 GCC 的 C/C++/Objective-C 编译器)、Rust 编译器后端、Swift 编译器、Julia 的 JIT,以及 AMD、Intel、NVIDIA 的硬件综合器。该项目的图中心设计——将程序表示为结构化图而非线性指令序列——使其优化 pass 可组合、可重定向、可独立测试。

关键工程洞见:程序不是指令列表,而是图。基本块是节点,分支是边,值沿 SSA 边流动,函数通过调用图相互调用。通过在 IR 设计中使这种图结构显式且一等,LLVM 使新一代编译器优化成为可能,这些优化在线性表示中是不可能的。

架构

LLVM 的架构围绕三个层次组织,全部图结构化:

IR(中间表示)。 LLVM IR 是类型化的 SSA 形式汇编语言。其图结构在多个层次运作:

  • 模块层:模块包含全局变量列表和函数定义。函数通过调用图相互引用。
  • 函数层:函数是控制流图。基本块是节点;终结指令(br、switch、ret)是边。每个函数的 CFG 作为双向图显式维护——每个 BasicBlock 存储其前驱和后继。
  • 指令层:基本块内,指令形成数据依赖 DAG。每条指令的操作数是 SSA 值(寄存器),每个值有使用列表(消费它的所有指令的链表)。这是值图——定义和使用之间的二部图。

SSA 属性意味着每个值恰好定义一次。这消除了数据依赖图中的歧义:如果指令 X 使用值 %v,恰好有一条指令定义 %v。图边是无歧义的。CFG 汇合点的 PHI 节点合并来自不同前驱边的值,使图结构即使在控制流汇合处也显式。

Pass 基础设施。 LLVM 优化实现为遍历和变换 IR 图的 pass:

  • 分析 pass 计算图属性而不修改 IR:支配树(从 CFG 派生的树)、循环信息(通过 CFG 回边识别的自然循环)、别名分析(指针间可能别名关系的图)、标量演化(建模值在循环迭代中的变化)。
  • 变换 pass 修改 IR 图:GVN(全局值编号——识别值图中的冗余计算)、LICM(循环不变量代码外提——从循环子图中外提节点)、SROA(聚合体标量替换——将复合值拆分为独立 SSA 值)、内联(在调用点将一个函数的 CFG 拼接进另一个)。

Pass 在流水线(PassManager)中组合。顺序很重要:某些 pass 为其他 pass 创造机会(mem2reg 创建 SSA 形式,启用 GVN;内联暴露跨函数优化)。流水线本身是 pass 依赖的 DAG。

后端(代码生成)。 后端通过一系列图变换将 LLVM IR 降级为机器码:

  • SelectionDAG:指令被模式匹配到目标特定 DAG。这是图同构问题——将 IR 模式匹配到机器指令模板。
  • 机器 CFG:SelectionDAG 被线性化为机器级 CFG(带机器指令的 MachineBasicBlock)。
  • 寄存器分配:干扰图(两个值如果同时活跃则干扰)被着色以分配物理寄存器。这是图着色——一般情况 NP 难,用启发式(带溢出的贪心着色)求解。
  • 调度:指令在基本块内按数据依赖 DAG 和硬件约束(流水线冒险、功能单元可用性)重新排序。

设计决策

决策 1:SSA 形式作为通用 IR vs 栈式或三地址码。

LLVM IR 采用 SSA(静态单赋值)形式:每个虚拟寄存器恰好赋值一次。这是相对替代方案的刻意选择:

  • SSA 优势:数据依赖图成为 DAG(无重赋值产生的环)。优化 pass 可以推理值而无需跟踪"我在看 %x 的哪个版本?"——只有一个版本。
  • SSA 优势:使用-定义链是平凡的。每个值恰好一个定义,所以定义→使用图是简单的链式结构。无需数据流分析来查找定义。
  • SSA 成本:汇合点的 PHI 节点使 IR 冗长且令人类困惑。简单的 if/else 对同一变量赋值会生成 PHI 节点。
  • SSA 成本:某些变换在 SSA 中更难(例如,插入重赋值变量的代码需要创建新 SSA 名并更新 PHI 节点)。

被拒绝的替代方案:可变寄存器 IR(如早期 GCC RTL)。团队判断 SSA 的分析收益(平凡使用-定义链、值流无歧义)超过冗长成本,尤其因为 IR 主要由机器消费,而非人类编写。

决策 2:显式 CFG 作为一等数据结构 vs 隐式(顺序执行 + 标签)。

LLVM 将 CFG 维护为显式图:每个 BasicBlock 有 predecessors()successors() 列表,每次修改时更新。这比隐式表示(通过扫描终结指令派生 CFG)更昂贵,但提供:

  • O(1) 前驱/后继查询(对支配树构建、PHI 节点插入、活跃性分析至关重要)
  • 即时一致性:pass 添加分支时,CFG 原子更新。无"陈旧 CFG" bug。
  • 图算法访问:pass 可以直接在 CFG 上调用 depth_first_iteratorbreadth_first_iteratorinverse_graph

权衡:每次 CFG 修改(拆分块、插入分支)需要更新前驱/后继列表。每次修改 O(度数)。对重度重构 CFG 的 pass(循环展开、尾部复制),这种开销不可忽略但可接受。

决策 3:使用列表(值图)用链表 vs 向量。

每个 SSA Value 维护其 Use(引用它的指令)的链表。这是图邻接表——值图的边。选择链表而非向量:

  • 链表优势:O(1) 插入/删除。指令被删除时,其使用在 O(1) 内解链,无需移动向量。
  • 链表优势:稳定迭代器。遍历值的使用的 pass 可以安全删除当前使用而不失效迭代器。
  • 链表成本:缓存局部性差。使用分散在内存中(嵌入在所属指令中)。遍历值的使用是指针追逐。

考虑过的替代方案:带哈希映射的使用向量(O(1) 删除)。被拒绝,因为常见情况(遍历所有使用)用向量+映射比链表慢,删除情况(罕见)不值得开销。

决策 4:基于库的架构 vs 单体编译器。

LLVM 是库的集合(libLLVMCore、libLLVMTransforms、libLLVMCodeGen 等),而非单一编译器二进制。这是图工程决策:

  • 每个库操作定义良好的图结构(IR 模块、CFG、SelectionDAG)。
  • Pass 是向 PassManager 注册的插件。新优化无需修改现有代码即可添加。
  • 库边界强制图不变量:操作 CFG 的 pass 不会意外破坏 SelectionDAG,因为它无法访问。

这种模块化使 Clang、Rust、Swift、Julia 能复用 LLVM 的优化和代码生成基础设施。每个前端产生 LLVM IR(图),后端消费它。图是接口。

模拟设计者思考

我是 Chris Lattner,2000 年,我是 UIUC 的研究生。我和导师 Vikram Adve 很沮丧。编译器研究卡住了。每篇优化论文在不同的编译器、不同的 IR、不同的基础设施上实现其 pass。无法比较结果。无法复用代码。这个领域是碎片化的。

问题是 IR。GCC 的 RTL 是带隐式数据流的线性指令列表。要找到什么定义了寄存器,你向后扫描。要找到什么使用了寄存器,你向前扫描。每个分析 pass 从头重新推导数据流图。对 n 条指令的函数是 O(n²),且容易出错——漏掉一个定义,你的优化就不健全。

如果 IR 使图显式呢?如果每个值恰好一个定义(SSA),每个使用指回其定义?那么数据依赖图是一等结构。需要"值 X 的所有使用"的 pass 只需遍历 X 的使用列表。无扫描。无重新推导。O(度数) 而非 O(n)。

还有控制流。在 GCC 中,CFG 是隐式的——你解析分支目标和顺序执行。如果 pass 修改代码(插入块、重定向分支),CFG 在某人重建之前是陈旧的。陈旧 CFG 是编译器 bug 的第一大来源。如果 CFG 是显式的呢?每个基本块存储其前驱和后继。每次修改更新它们。图总是一致的。

SSA 决策是大事。SSA 使值图成为 DAG——无重赋值产生的环。但它引入 PHI 节点,这很丑。汇合点的 x = phi(x1, x2) 意味着"如果从左来 x 是 x1,从右来是 x2"。人类讨厌 PHI 节点。但机器不在乎。收益巨大:每个优化 pass 可以假设"这个值恰好意味着一件事"。没有"我在看 x 的哪个赋值?"的问题。

我要下注:IR 主要由机器生成和消费。人类为调试阅读它,但不编写它。所以我优化机器消费:显式图、SSA、类型化值、无歧义。冗长(PHI 节点、每个值的显式类型)是代价。值得。

库架构来自图设计。如果 IR 是图,那么 pass 是图变换。pass 接收图,返回图。接口是图结构本身。我可以定义干净的库边界:Core(图数据结构)、Analysis(只读图查询)、Transforms(图重写)、CodeGen(图降级为机器图)。每个库只依赖图接口,不依赖其他库的内部。

可重定向性问题。如果我想支持多种源语言(C、C++、Fortran 等)和多种目标(x86、ARM、PowerPC),IR 必须目标无关。无机器寄存器、无调用约定、无指令集细节。图是抽象的。后端将抽象图降级为具体机器图(SelectionDAG → MachineInstr → MCInst)。优化 pass 操作抽象图,在所有目标间复用。

这是使 LLVM 不同于 GCC 的关键洞见:图是接口。前端产生它。优化器变换它。后端消费它。任何人都可以编写新 pass、新前端或新后端,它们都通过图组合。这不是编译器。这是编译器基础设施。而基础设施是图。

我叫它 LLVM。Low Level Virtual Machine。虽然它并不真正低级(比汇编高),也不真正是虚拟机(不解释)。但名字留下了。图不在乎你怎么叫它。

关键启示与行业影响

LLVM 最具变革性的贡献是"图作为接口"架构。通过使 IR 成为定义良好的图结构(CFG + SSA 值图 + 调用图),LLVM 使单体编译器无法匹敌的生态系统成为可能。Clang、Rust、Swift、Julia 和数十种其他语言都产生 LLVM IR。AMD、Intel、NVIDIA、ARM 都消费它。图是契约。这种分离(前端产生图、后端消费图、优化器变换图)是使 LLVM 成为通用编译器基础设施的架构洞见。

SSA 决策——冗长、充满 PHI 节点、对人类不友好——证明了机器可消费性在 IR 中胜过人类可读性。没有人手写 LLVM IR。每天数百万行由编译器生成。IR 为机器优化:无歧义的值流、平凡的使用-定义链、无重赋值歧义。教训:当主要受众是机器(优化器、代码生成器)时,为机器的需求(显式性、不变量、图结构)设计,而非为人类的舒适。

基于库的架构(libLLVMCore、libLLVMTransforms、libLLVMCodeGen)证明图不变量使干净的模块边界成为可能。每个库操作定义良好的图结构并维护其不变量。变换 pass 不会意外破坏 CFG,因为 CFG 由 Core 库维护,pass 通过验证的 API 与之交互。教训:当数据结构是带不变量的图时,不变量定义模块边界。在库接口处强制它们。

LLVM 的 pass 流水线(图变换序列)成为所有编译器优化框架的模型。洞见:优化是可组合的图重写。死代码消除删除不可达节点。内联将一个图拼接进另一个。循环展开复制子图。每个变换维护图的不变量(SSA、CFG 连通性、类型正确性)。流水线是不变量维护的重写序列。教训:将计算建模为图上结构维护的变换序列,正确性就变得可组合。

可重定向性教训:基于图的 IR 是终极抽象边界。前端无需了解 x86 寄存器。后端无需了解 C++ 模板。图恰好携带两者需要的信息:值、依赖、控制流、类型。这种关注点分离使 LLVM 能用单一优化流水线支持 20+ 源语言和 10+ 目标架构。教训:正确的抽象(图)比任何优化都更有价值。

图查询语言Apache TinkerPop / Gremlin

Apache TinkerPop 与 Gremlin:图遍历机

概述

Apache TinkerPop 是一个图计算框架,提供厂商中立、语言无关的抽象,覆盖图数据库和图处理器。其查询语言 Gremlin 是命令式、函数式的遍历语言,将图计算表达为图上的一系列步骤。与 Cypher(声明式,"描述你想要的模式")或 SPARQL(声明式,用于 RDF 图)不同,Gremlin 是命令式的:"从这里开始,走到那里,过滤这个,聚合那个。"程序员显式编写遍历路径。

TinkerPop 由 Marko Rodriguez 于 2009 年创建,源于他对图遍历机的研究和一个哲学问题:"在图上计算任何东西所需的最小操作集是什么?"答案是 Gremlin 步骤词汇表——约 30 个核心步骤(out、in、both、values、filter、map、fold、unfold、repeat、until),可组合成任意图计算。这是最纯粹意义上的图工程项目:整个系统是一台虚拟机,其指令集就是图遍历。

该框架的天才之处在于其提供者模型。TinkerPop 定义遍历 API 和步骤语义。图厂商(Neo4j、JanusGraph、Amazon Neptune、CosmosDB、OrientDB)实现 TinkerPop SPI(服务提供者接口),将其存储暴露为 TinkerPop 兼容图。针对 TinkerPop 编写的 Gremlin 遍历可在任何合规后端上不变运行。这是"图数据库的 JDBC"——一次编写,处处遍历。

架构

TinkerPop 的架构是分层的:

图结构(TinkerGraph)。 参考实现是 TinkerGraph——内存属性图。其结构:

  • 顶点存储在 Map(ID → 顶点)
  • 边存储在 Map(ID → 边)
  • 每个顶点维护邻接表:按边标签键控的 Map>(outE、inE)
  • 属性存储为顶点和边上的 Map>

TinkerGraph 不用于生产——它是测试基准。其简单性(纯 Java map 和 set)使其成为正确遍历语义的参考。

遍历机。 Gremlin 遍历被编译为 Traversal 对象——Step 对象的线性序列。每个步骤是一个函数:(Traverser) → IteratorTraverser 携带:

  • 当前对象(顶点、边、值或路径)
  • 路径(访问过的对象序列)
  • sack(沿途携带的副作用数据)
  • bulk(OLAP 用:这个 traverser 代表多少逻辑 traverser)
  • 标签(用于策略中的反向引用)

遍历机以流水线方式执行步骤:每个 traverser 流经步骤序列。步骤可以是:

  • Map 步骤:变换当前对象(out()、values()、math())
  • Filter 步骤:通过或阻止 traverser(has()、where()、is())
  • 副作用步骤:累积而不变换(store()、aggregate()、tree())
  • 分支步骤:将 traverser 分裂为多条路径(choose()、union()、repeat())
  • FlatMap 步骤:一对多展开(outE().inV()、unfold())

策略系统。 执行前,遍历被 TraversalStrategy 对象链优化:

  • 装饰策略:添加步骤(例如,添加 limit 防止无界遍历)
  • 优化策略:重写步骤(例如,如果后端支持索引查找,out().has('name','x')has('name','x').out()
  • 提供者优化策略:后端特定重写(例如,Neptune 将某些模式转换为原生查询 API 调用)
  • 终结策略:锁定遍历以执行

这是图感知的查询优化器。它理解 out().out() 是两跳,如果后端原生支持多跳遍历则可以融合。

提供者 SPI。 图厂商实现:

  • Graph 接口:打开/关闭事务、创建顶点/边、提交遍历
  • GraphTraversalSource:构建遍历的入口点
  • GraphStepVertexStep 等:将计算推入存储引擎的后端特定步骤实现
  • TraversalStrategy:后端特定优化

通过 GraphComputer 的 OLAP。 对全图分析(PageRank、连通分量),TinkerPop 提供 GraphComputer 接口。遍历通过 Spark 或 Hadoop MapReduce 分布到集群。Traverser 获得 bulk 字段——一个 traverser 代表 N 个逻辑 traverser,实现 BSP 风格计算而无需物化 N 个对象。

设计决策

决策 1:命令式遍历 vs 声明式模式匹配。

Gremlin 是命令式的:g.V().has('name','Alice').out('knows').values('name')。你描述行走,而非模式。Cypher 是声明式的:MATCH (a)-[:knows]->(b) WHERE a.name='Alice' RETURN b.name。你描述形状,引擎去查找。

权衡:

  • 命令式优势:表达力。任何图计算都是遍历。图算法(最短路径、社区发现)是自然的 Gremlin 程序。在 Cypher 中,这些需要递归 CTE 或存储过程。
  • 命令式优势:可组合性。遍历像 Unix 管道一样组合。g.V().out().out() 是"两跳"。可以增量构建复杂遍历。
  • 命令式成本:优化更难。声明式查询给优化器一个可匹配索引的模式。命令式遍历给一系列步骤——优化器必须识别 V() 后的 has('name','x') 可以推入索引扫描。
  • 命令式成本:简单查询的可读性。对模式查询,MATCH (a)-[:knows]->(b)g.V().as('a').out('knows').as('b') 更可读。

团队选择命令式,因为 TinkerPop 的目标是普适性——不仅支持模式查询,还支持任意图计算(OLAP、路径查找、图生成)。声明式语言需要为每个算法扩展;命令式语言原生表达它们。

决策 2:基于步骤的流水线 vs 算子树。

Gremlin 遍历是线性步骤序列,而非算子树(如 SQL 的关系代数)。这是刻意的:

  • 线性优势:简单。遍历像管道一样从左到右阅读。无需树可视化。
  • 线性优势:流式。步骤以流水线方式执行——无中间结果物化。内存使用是每 traverser O(1),而非 O(中间结果大小)。
  • 线性成本:某些计算天然是树结构(子查询、相关 exists)。这些需要带嵌套遍历的 where(),打破线性模型。

策略系统补偿:它可以将线性步骤序列重写为更高效的形式(例如,将过滤步骤融合为索引查找),而不改变程序员的心智模型。

决策 3:厂商中立 SPI vs 单厂商优化。

TinkerPop 刻意牺牲单厂商性能以换取可移植性。Neo4j 上的 Gremlin 遍历无法使用 Neo4j 特定功能(APOC 过程、全文索引)而不破坏可移植性。

权衡:

  • 可移植优势:应用可以切换后端而不重写查询。初创公司可以在 TinkerGraph(内存、免费)上原型开发,部署到 Neptune(托管、可扩展)而无需代码更改。
  • 可移植成本:最低公共分母。一个后端独有的功能(Neo4j 的 APOC、Neptune 的全文搜索)通过标准 Gremlin 不可访问。
  • 可移植成本:优化限于所有后端支持的。有基于成本优化器的后端(Neo4j)无法通过通用 SPI 暴露其完整规划能力。

团队判断 2010-2015 年的图数据库市场足够碎片化,可移植性是用户第一需求。用户在 10+ 图数据库间选择,需要避免锁定的方法。

决策 4:基于 JVM vs 多语言原生。

TinkerPop 用 Java 实现,带语言驱动(Python、JavaScript、.NET、Go),通过 WebSocket(Gremlin Server)或嵌入式(仅 JVM 语言)通信。

权衡:

  • JVM 优势:遍历语义的单一实现。所有语言驱动获得相同行为,因为它们与同一 Java 引擎对话。
  • JVM 成本:非 JVM 语言支付网络延迟(每遍历一次 WebSocket 往返)。嵌入模式(零拷贝、进程内)仅限 JVM。
  • JVM 成本:JVM 的 GC 暂停影响延迟敏感的 OLTP 遍历。

考虑过的替代方案:每语言原生实现(如 PostgreSQL 的 libpq)。被拒绝,因为在 5 种语言间维护遍历语义将是正确性噩梦。一个实现,多个客户端。

模拟设计者思考

我是 Marko Rodriguez,2009 年,我刚完成关于图遍历机的博士论文。占据我论文的问题:"图的最小计算模型是什么?"不是带图扩展的 SQL。不是带属性图的 SPARQL。是最小的东西。

我不断回到同样的答案:遍历。穿过图的行走。从某些顶点开始,沿边前进,过滤、变换、聚合。就这些。我能想到的每个图算法——PageRank、最短路径、社区发现、模式匹配——都是带不同步骤组合的遍历。

设计问题:原始步骤是什么?我需要完整(可表达任何图计算)但最小(无冗余)的词汇表。经过数月迭代,我确定约 30 个步骤:out/in/both(遍历边)、has/where/is(过滤)、values/keys/properties(投影)、fold/unfold(聚合/展开)、repeat/until(迭代)、choose/union(分支)、path(记录历史)。

执行模型。每个步骤是一个函数:Traverser → Iterator。Traverser 是一个计算线程的"状态"——它在图中的位置、走过的路径、累积的副作用。遍历机将 traverser 流水线化通过步骤。无物化。无中间表。只有流经管道的 traverser 流。

但优化。朴素流水线很慢。g.V().has('name','Alice') 应该是索引查找,而非全扫描+过滤。我需要策略系统——执行前变换步骤序列的重写器链。策略可插拔:核心提供通用优化(步骤融合、死步骤消除),后端提供特定的(索引下推、原生 API 转换)。

厂商问题。我想让它在任何图数据库上运行。Neo4j、Titan、OrientDB,随便什么。所以我定义 SPI:实现 GraphVertexEdge 和几个步骤接口,你的数据库就 Gremlin 兼容。遍历机后端无关。它调用图上的 vertices()、顶点上的 edges(Direction.OUT, label),后端提供数据。

成本:最低公共分母。我无法通过标准 Gremlin 暴露 Neo4j 的 Lucene 全文索引。我无法使用 Titan 的 HBase 特定优化。但用户获得可移植性。他们在 TinkerGraph(内存、零配置)上原型开发,部署到生产后端而不改一行代码。这是价值主张。

OLAP 是难点。十亿顶点图上的 g.V().repeat(both()).times(10) 不能在进程内运行。我需要分布。GraphComputer 接口:遍历分区到集群。每个工作节点处理顶点子集。Traverser 获得 bulk 字段——一个 traverser 代表一百万逻辑 traverser。底层是 BSP(Pregel 风格),但程序员编写与 OLTP 相同的 Gremlin。系统处理分布。

哲学观点:图不是表。不是文档。是你遍历的结构。查询语言应该反映这一点。你不是从图中"选择"。你行走它。Gremlin 是行走的语言。TinkerPop 是使行走在任何图存储上可移植的机器。

我将以开源发布。Apache 许可。图社区是碎片化的——每个人都在构建自己的查询语言、自己的遍历引擎。如果我能给他们共同词汇、共同机器,也许这个领域能整合。也许"图数据库"不再意味着"带定制查询语言的数据库",而开始意味着"TinkerPop 提供者"。这是梦想。

关键启示与行业影响

TinkerPop 最持久的贡献是"图遍历即计算"范式。通过将所有图操作归约为遍历机上的一系列步骤,Gremlin 证明图查询语言不必是声明式的(描述模式)——可以是命令式的(描述行走)。这使图计算对声明式语言难以处理的算法开放:迭代算法(重复到收敛)、路径查找(带约束的最短路径)、图生成(遍历中创建新结构)。教训:正确的计算模型(遍历)比正确的语法更重要。

厂商中立 SPI 被证明既是 TinkerPop 的最大优势也是局限。可移植性(一次编写,任何后端运行)推动了图数据库碎片化时代(2010-2018:Neo4j、Titan、OrientDB、ArangoDB、CosmosDB)的采用。用户可以在 TinkerGraph(免费、内存)上原型开发,部署到生产后端而无需代码更改。但最低公共分母效应意味着后端特定优化(Neo4j 的 APOC、Neptune 的全文搜索)通过标准 Gremlin 不可访问。教训:抽象启用可移植性但限制性能。可移植性的价值取决于市场碎片化程度——随着市场整合(Neo4j 主导、云上 Neptune),可移植性溢价下降。

策略系统(可插拔遍历重写器)预示了现代查询引擎中标准的"查询优化即图变换"方法。每个策略是图到图的函数:接收遍历(步骤的线性图),返回优化的遍历。策略可组合(按序应用)、后端特定(提供者策略)、可独立测试。教训:优化不是单体阶段——而是小型、可组合、可测试的重写流水线。

TinkerPop 的 OLAP 故事(GraphComputer,通过 Spark/Hadoop 的 BSP)证明同一遍历 API 可同时服务 OLTP(单路径查询、毫秒延迟)和 OLAP(全图分析、小时级作业)。程序员编写相同的 Gremlin;系统处理分布。bulk 字段(一个 traverser 代表 N 个逻辑 traverser)是关键抽象:它将多跳遍历的组合爆炸压缩为单个加权 traverser。教训:正确的抽象(bulk)使规模对程序员透明。

运维教训:厂商中立框架需要治理模型。TinkerPop 的 Apache 软件基金会治理确保没有单一厂商控制规范。这防止了困扰早期数据库标准的"拥抱、扩展、消灭"动态。教训:开放标准需要开放治理。规范必须大于任何单一实现。

图数据库Amazon Neptune

Amazon Neptune:云原生图数据库

概述

Amazon Neptune 于 re:Invent 2017 发布、2018 年正式可用,是 AWS 的托管图数据库服务。它在同一底层存储引擎上同时支持属性图查询(通过 Gremlin 和 openCypher)和 RDF 图查询(通过 SPARQL)。这种双模型支持在架构上不同寻常——大多数图数据库要么承诺属性图模型(Neo4j、TigerGraph),要么承诺 RDF/语义网模型(Apache Jena、Virtuoso)。Neptune 的工程挑战是构建单一存储和查询引擎,服务两种范式而不牺牲任一性能。

Neptune 的诞生是为了解决特定的 AWS 客户问题:拥有高度连接数据(欺诈检测、社交网络、知识图谱、推荐引擎)的企业在关系数据库上运行图工作负载,忍受痛苦的 JOIN 性能,或者管理自己的图数据库集群承担运维开销。Neptune 提供完全托管、多可用区、自动扩展的图数据库,具备 AWS 运维模型(无补丁、无容量规划、自动备份)。

该系统的架构反映 AWS 的云原生哲学:存储与计算分离、日志结构存储、与 Aurora(AWS 云原生关系数据库)共享的分布式存储层。Neptune 的存储引擎是 Aurora 的分支,为图工作负载改编。

架构

Neptune 的架构遵循 Aurora 模式分离计算与存储:

存储层。 分布式日志结构存储引擎:

  • 数据以有向图形式存储,采用为邻接遍历优化的自定义格式
  • 存储卷分布在 3 个可用区(AZ)的 6 个副本上
  • 写入基于仲裁:4/6 副本确认即成功(写仲裁 4/6,读仲裁 3/6)
  • 存储层仅追加(日志结构):变更写为重做日志记录,而非原地页更新
  • 后台进程异步将重做日志应用到存储页("存储服务器"模型)

计算层。 数据库实例(最多 15 个只读副本):

  • 一个写实例处理所有变更
  • 最多 15 个只读副本服务读流量,复制延迟 <10ms
  • 每个实例有自己的缓冲池(缓存)存放热图页
  • 实例相对存储无状态——故障转移在 <30 秒内将副本提升为写者

图存储模型。 内部上,Neptune 将图存储为:

  • 语句(RDF 用):主-谓-宾三元组,存储在带多种索引排序(SPO、POS、OSP)的三元组表中以高效模式匹配
  • 节点和边(属性图用):以类似三元组模型的邻接列表格式存储——节点的边存储为"语句",节点是主语,边标签是谓词,目标节点是宾语
  • 属性值存储为附加语句(节点 → 属性键 → 字面值)

这种统一是关键架构洞见:属性图内部存储为三元组。同一数据上的 Gremlin 查询和 SPARQL 查询命中相同的存储索引。查询引擎不同(Gremlin 遍历机 vs SPARQL 代数求值器),但存储访问路径共享。

查询处理。

  • Gremlin/openCypher:遍历被编译为对三元组存储发起索引查找的物理计划。优化器将过滤推入存储(谓词下推),基于基数估计选择索引排序。
  • SPARQL:查询解析为 SPARQL 代数,优化(连接顺序、过滤下推),对相同三元组索引执行。优化器使用基于直方图的基数估计。

索引。 多种索引结构:

  • 主索引:SPO、POS、OSP(三元组模式匹配用)
  • 二级索引:字面值上(属性范围查询用)
  • 全文索引:与 OpenSearch 集成,用于标签/属性的文本搜索

设计决策

决策 1:Aurora 派生存储 vs 专用图存储。

团队分支 Aurora 的存储引擎,而非构建图原生存储(如 Neo4j 的固定记录格式)。理由:

  • Aurora 的存储提供:分布式复制(6 路、3 AZ)、自动故障转移、时间点恢复、持续备份——这些都是需要数年从头构建的已解决问题。
  • Aurora 的日志结构设计(仅追加写、后台页应用)提供:低写延迟(无随机 I/O)、崩溃一致性(日志即真相)、读扩展(副本应用相同日志)。
  • 成本:Aurora 的存储是页导向(16KB 页),而非记录导向。图遍历(从节点跟指针到边再到节点)可能跨页边界,导致额外 I/O。专用图存储(Neo4j)使用带直接指针的固定大小记录——每跳一次 I/O。

接受的权衡:Neptune 的每跳延迟略高于 Neo4j(页导向 vs 记录导向),但运维收益(托管、多 AZ、自动扩展、无容量规划)对优先考虑可用性和运维简单性而非原始遍历速度的 AWS 客户超过了延迟成本。

决策 2:统一三元组存储 vs 分离的属性图和 RDF 引擎。

Neptune 不维护两个存储引擎(一个属性图、一个 RDF),而是将所有东西存储为三元组。属性图的节点/边/属性映射为三元组模式。

权衡:

  • 统一优势:一个存储引擎维护、优化、扩展。一套索引。一个复制机制。工程团队可以专注一个访问路径。
  • 统一优势:跨模型查询成为可能(用 SPARQL 查属性图数据,或用 Gremlin 查 RDF 数据)。
  • 统一成本:属性图操作(从节点遍历到邻居)被转换为三元组查找(主语 → 谓词 → 宾语),增加间接层。原生属性图存储会跟随直接指针。
  • 统一成本:RDF 特定优化(推理、本体推理)在无专用索引的通用三元组存储上更难实现。

团队判断一个存储引擎的工程效率超过每操作开销,尤其因为缓冲池缓存热三元组,网络 I/O(到存储服务器)主导间接成本。

决策 3:单写者+只读副本 vs 多写者。

Neptune 使用单写实例+最多 15 个只读副本,匹配 Aurora 模型。

权衡:

  • 单写者优势:无写冲突。图变更(在节点间创建边)本质上是多对象操作。单写者平凡地串行化它们。
  • 单写者成本:写吞吐限于一个实例。对写密集图工作负载(导入十亿边),这是瓶颈。
  • 单写者成本:写者故障转移需要 15-30 秒(提升副本)。故障转移期间写入不可用。

考虑过的替代方案:带冲突解决的多写者(如 DynamoDB)。被拒绝,因为图变更有复杂依赖(创建边需要两个端点存在;同一对节点间的并发边创建必须串行化)。冲突解决逻辑比键值存储更复杂。

决策 4:托管服务 vs 开源引擎。

Neptune 是专有 AWS 服务,而非开源数据库。存储引擎派生自 Aurora(也是专有的)。查询引擎支持开放标准(Gremlin、openCypher、SPARQL),但实现是封闭的。

权衡:

  • 专有优势:与 AWS 基础设施深度集成(IAM、VPC、CloudWatch、KMS 加密)。团队可以针对 AWS 硬件/网络栈优化,无社区兼容性约束。
  • 专有成本:厂商锁定。客户无法在本地或其他云上运行 Neptune。迁移需要导出为开放格式(CSV、RDF 转储)并导入其他图数据库。
  • 专有成本:无社区贡献。bug 修复和功能完全依赖 AWS 团队的优先级。

模拟设计者思考

我是 Neptune 团队的首席工程师,2015 年,我们在设计阶段。领导的指令:"构建一个 AWS 客户会用来替代在 EC2 上运行 Neo4j 的图数据库。"运维模型必须匹配 Aurora 和 RDS:托管、多 AZ、自动备份、只读副本。无客户可见的运维负担。

第一个问题:从头构建图存储引擎,还是改编 Aurora 的?从头构建意味着:分布式复制、仲裁写入、日志结构存储、崩溃恢复、时间点恢复、持续备份到 S3。在写第一个图查询之前是 3-4 年的工作。Aurora 已经有所有这些。经过实战检验。处理 PB 级。团队了解它。

但 Aurora 的存储是页导向。16KB 页。图遍历是指针追逐:节点 → 边 → 节点 → 边。每跳可能跨页边界。在 Neo4j 中,一跳是一次记录读取(15 字节,固定偏移)。在我们的页导向存储中,一跳可能是:读包含节点记录的页,提取边指针,读包含边记录的页,提取目标指针,读包含目标节点的页。每跳三次页读取,每次都可能是缓存未命中。

缓解:缓冲池。如果图的热集适合内存(对大多数 OLTP 图工作负载确实如此——80/20 法则适用于图访问),页读取是内存拷贝,而非磁盘 I/O。"在固定偏移读 15 字节"和"读 16KB 页并提取 15 字节"的延迟差异在两者都在 DRAM 中时可忽略。页开销只在缓存未命中时重要,我们用预取最小化(读节点时预取其邻接页)。

双模型问题。客户要属性图(Gremlin)和 RDF(SPARQL)。要构建两个引擎吗?那会使存储团队、查询团队、测试矩阵翻倍。如果把所有东西存储为三元组呢?属性图节点是三元组主语。边是三元组(源 → 标签 → 目标)。属性是三元组(实体 → 键 → 值)。那么一个存储引擎、一套索引(SPO、POS、OSP)、一个复制机制服务两种模型。

Gremlin 引擎将遍历转换为三元组查找。g.V(1).out('knows') 变成:"查找主语=1 且谓词='knows' 的三元组,返回宾语。"SPARQL 引擎直接求值三元组模式。两者命中相同索引。查询优化器分离(遍历优化 vs SPARQL 代数优化),但存储访问统一。

写模型。单写者,像 Aurora。图写入复杂:创建边触及源节点的邻接表、目标节点的邻接表和边记录本身。多写者需要分布式事务或并发边创建的冲突解决。不值得。我们客户的工作负载读密集(推荐引擎、欺诈检测 100:1 读写比)。单写者+15 只读副本处理读规模。写吞吐(一个实例)足以支持约 10 万边/秒的导入速率。

可用性模型。跨 3 个 AZ 的 6 个副本。写仲裁 4/6。读仲裁 3/6。这容忍一个 AZ 故障而不影响可用性。故障转移:提升只读副本为写者。存储层共享——新写者只是开始接受对同一分布式卷的写入。无数据拷贝。无脑裂(旧写者被存储层的纪元机制隔离)。

我们不是在构建最快的图数据库。Neo4j 在原始遍历延迟上会打败我们(记录导向 vs 页导向)。TigerGraph 在分析吞吐上会打败我们(大规模并行 vs 单写者)。我们在构建运维最简单的图数据库。客户不思考存储引擎或复制拓扑。他们编写 Gremlin,得到答案,AWS 处理其余。这是产品。

关键启示与行业影响

Neptune 架构的更广泛含义是它验证了"云原生图数据库"模型:存储与计算分离、日志结构写入、仲裁复制、弹性读扩展。在 Neptune 之前,图数据库主要是本地系统(Neo4j、TigerGraph),需要手动容量规划、打补丁和故障转移配置。Neptune 证明运维模型与查询模型同样重要:自动处理可用性、持久性和扩展的图数据库——即使牺牲一些原始遍历性能——服务的市场比需要专职 DBA 的更快系统更大。教训:对云原生图工作负载,运维体验就是产品。查询引擎是入场券。差异化在于"你永远不用思考存储、复制、备份或故障转移"。图服务应用;云服务图。

图处理框架GraphX (Apache Spark)

GraphX:统一图并行与数据并行计算

概述

GraphX 是构建在 Apache Spark 之上的图处理库,由 UC Berkeley AMPLab(2013-2014)的 Joseph Gonzalez、Reynold Xin、Matei Zaharia 等人开发。其核心贡献不是新图算法或新分布式系统——而是架构统一。在 GraphX 之前,图处理(Pregel/Giraph)和数据并行处理(MapReduce/Spark)是分离的系统,有分离的 API、分离的集群和分离的数据管道。GraphX 证明图并行计算可以表达为数据并行计算的特例,运行在处理 ETL、机器学习和 SQL 的同一引擎(Spark)上。

关键洞见:图就是两张表(顶点表和边表),带连接关系。图操作(遍历、聚合、迭代)是这些表上的关系操作(连接、分组、迭代)。通过将图表示为 Spark RDD(弹性分布式数据集)、图操作表示为 RDD 变换,GraphX 继承了 Spark 的容错、缓存和多语言支持,无需构建新的分布式运行时。

GraphX 的影响可见于每个位于数据并行引擎之上的现代图处理系统:Databricks 的 graphframes、Flink 的 Gelly,甚至 TigerGraph 的 Spark 连接器等商业系统。"图即表"的洞见成为将图分析集成到数据湖架构的标准方法。

架构

GraphX 的架构是 Spark RDD 抽象之上的薄层:

数据表示。 图是两个 RDD:

  • VertexRDD[VD]:(VertexId, VD) 对的 RDD,VertexId 是 Long,VD 是顶点属性类型。内部优化为带路由表的哈希分区 RDD。
  • EdgeRDD[ED]:Edge[ED] 对象(srcId、dstId、attr)的 RDD。内部按顶点切分方案分区(见下文)。

Graph[VD, ED] 类包装这两个 RDD 并提供图特定操作。

顶点切分分区。 与 Pregel/Giraph(使用边切分:每个顶点分配到一个分区,跨分区的边被复制)不同,GraphX 使用顶点切分分区:

  • 边被分区(而非顶点)。高度顶点的边被拆分到多个分区。
  • 每个分区存储其拥有的边以及这些边端点的顶点属性。
  • 在 K 个分区中有边的顶点有 K 个"副本"——每分区一个。
  • 路由表将每个顶点映射到其分区集合。

这是关键设计选择。对幂律图(社交网络、网络图),少数顶点有数百万边。边切分将枢纽的所有边分配到一个分区,造成严重负载不均。顶点切分将枢纽的边拆分到各分区,以顶点复制为代价平衡负载。

Pregel API。 GraphX 暴露类 Pregel API 用于迭代图算法:


graph.pregel(initialMsg)(
  vprog = (id, attr, msg) => ...,  // 顶点程序
  sendMsg = triplet => ...,          // 消息生成
  mergeMsg = (a, b) => ...          // 消息组合器
)

这实现为 Spark 循环:每次迭代是连接(边与顶点属性连接)、映射(计算消息)、归约(按顶点聚合消息)、更新(新顶点属性)。循环运行到收敛。

图算子。 除 Pregel 外,GraphX 提供:

  • joinVertices:顶点属性与外部 RDD 连接(如 SQL 连接)
  • aggregateMessages:核心原语——对每条边,从边三元组(src、dst、边属性)计算消息,在顶点聚合消息
  • subgraph:过滤顶点/边(如 SQL WHERE)
  • connectedComponentspageRanktriangleCount:通过 aggregateMessages 实现的内置算法

容错。 继承自 Spark:RDD 基于血缘。如果分区丢失(节点故障),Spark 从血缘(产生它的变换序列)重算。正确性无需检查点(但检查点减少长迭代算法的重算成本)。

设计决策

决策 1:构建在 Spark 上 vs 构建新图运行时。

团队选择将 GraphX 实现为 Spark 上的库,而非构建新的分布式图处理系统(如 Giraph 或 PowerGraph)。

权衡:

  • Spark 优势:免费容错。RDD 血缘意味着无检查点基础设施。故障节点触发从血缘重算丢失分区。
  • Spark 优势:集成。图计算与 ETL(从 HDFS/S3 加载数据)、ML(运行 GraphX PageRank)、输出(写结果到数据仓库)在同一作业中运行。系统间无数据移动。
  • Spark 优势:缓存。Spark 的内存缓存(persist/cache)使热图分区在迭代间保留在 RAM 中。迭代算法(PageRank,40 次迭代)复用缓存分区,无需从磁盘重读。
  • Spark 成本:开销。Spark 的任务调度、序列化和洗牌机制相比专用图系统(Giraph,最小化每迭代开销)增加每迭代延迟。
  • Spark 成本:控制有限。GraphX 无法实现自定义网络拓扑或消息传递协议——受限于 Spark 基于洗牌的通信。

团队测量了开销:GraphX 在纯图基准(PageRank、连通分量)上比 Giraph 慢 2-3 倍,但在端到端管道(加载 → 变换 → 图计算 → 输出)上快 10 倍,因为它消除了系统间数据移动。

决策 2:顶点切分 vs 边切分分区。

GraphX 使用顶点切分(分区边,复制顶点)而非边切分(分区顶点,复制边)。

权衡:

  • 顶点切分优势:幂律图上的负载均衡。有 1000 万边的顶点其边被拆分到所有分区。没有单一分区被枢纽瓶颈。
  • 顶点切分优势:通信减少。在 Pregel 风格计算中,消息沿边流动。顶点切分下,边的端点在同一分区(边与两个顶点共置)。分区内边的消息不跨网络。
  • 顶点切分成本:顶点复制。在 K 个分区中有边的枢纽顶点有 K 个属性副本。枢纽的更新需要同步 K 个副本。
  • 顶点切分成本:更新顶点状态的顶点中心算法(PageRank)必须每超步聚合来自 K 个副本的更新。

团队的分析:对真实世界图(Twitter 社交图、网络图),幂律指数约 2.1。边切分创建的分区中 90% 的工作在 1% 的顶点(枢纽)上。顶点切分均匀分布枢纽的工作。复制成本(顶点属性 2-3 倍内存)可接受,因为顶点属性相对边数据很小(PageRank 的 double)。

决策 3:基于 RDD vs 自定义分布式数据结构。

GraphX 将图表示为 Spark RDD,而非自定义分布式图结构。

权衡:

  • RDD 优势:所有 Spark 操作(filter、map、join、groupBy)可用于图数据。可以将图与非图数据集连接(例如,顶点属性与用户画像表连接)而不离开 Spark API。
  • RDD 优势:Spark 的优化器(Catalyst,后期版本)可以将图操作优化为关系操作。
  • RDD 成本:存储层无图特定优化。专用图存储(Neo4j)使用无索引邻接实现 O(1) 遍历。GraphX 的遍历是分布式连接——每跳 O(E/P),P 是并行度。
  • RDD 成本:迭代算法需要每迭代重执行完整 RDD 血缘(除非缓存)。专用图系统跨迭代在内存中维护状态,无需重执行。

团队接受这些成本,因为 GraphX 的目标工作负载是分析型(OLAP):在整个图上计算 PageRank、查找连通分量、计数三角形。这些是全图计算,分布式连接成本分摊到数十亿边。对 OLTP 遍历(从一个节点沿路径走),GraphX 是错误工具——用图数据库。

决策 4:不可变图 vs 可变图。

GraphX 图是不可变的。操作返回新图(新 RDD)。没有 graph.addEdge()graph.updateVertex()

权衡:

  • 不可变优势:容错平凡。RDD 血缘是变换的 DAG。无需跟踪变更或在故障时撤销。
  • 不可变优势:缓存安全。缓存的图分区永不改变。无失效。
  • 不可变成本:图变更算法(增量图更新、流式边插入)笨拙。每次变更创建新图(新 RDD),可能触发完全重算。

团队判断分析型图处理(目标工作负载)是批处理导向:加载图、计算、输出。变更罕见。不可变模型匹配工作负载。

模拟设计者思考

我是 Joey Gonzalez,2012 年,我在 Berkeley 与 Matei Zaharia(Spark 创建者)和 Ion Stoica 一起工作。我一直在给 Giraph(Hadoop 图系统)做基准测试,很沮丧。不是 Giraph 的性能——它的 PageRank 很快。我对管道感到沮丧。

真实的图分析作业是这样的:(1) 从 HDFS 加载原始日志。(2) 解析和清洗(MapReduce)。(3) 构建图(另一个 MapReduce)。(4) 运行 PageRank(Giraph)。(5) 将 PageRank 分数与用户元数据连接(又一个 MapReduce)。(6) 写结果到数据库。这是四个系统、三次数据移动和一周的胶水代码。如果第 4 步失败,你从第 3 步重启。

如果图存在于 Spark 中呢?如果"构建图"只是 val edges = rdd.map(parseEdge),"运行 PageRank"只是 graph.pageRank(),"与元数据连接"只是 graph.joinVertices(metadata)?一个系统。一个容错机制。一个缓存。无数据移动。

技术问题:Spark 的 RDD 抽象能高效表达图计算吗?图是两张表:顶点和边。遍历是连接(边与顶点在 src/dst ID 上连接)。聚合是 groupBy(按目标顶点分组消息)。迭代是这些操作上的循环。Spark 都能做。问题是是否足够快。

分区方案是关键。Pregel/Giraph 使用边切分:每个顶点分配到分区,复制跨界的边。对幂律图,这很糟糕。枢纽顶点(1000 万边)在一个分区。该分区每超步做 1000 万次消息发送,其他分区做 100 次。负载不均扼杀并行性。

顶点切分:分区边,而非顶点。拆分枢纽的 1000 万边到所有分区。每个分区获得约 1000万/P 条边。负载均衡。成本:枢纽的顶点属性复制 P 次。但属性是 8 字节(PageRank 的 double)。复制 8 字节 P 次与平衡 1000 万边计算相比微不足道。

实现:EdgeRDD 按 (srcId, dstId) 哈希分区。每个分区存储其边以及两个端点的顶点属性。当顶点属性改变(PageRank 更新),需要传播到持有其边的所有分区。路由表告诉我们哪些分区。这是 scatter-gather:将新属性散射到 K 个分区,从所有边收集消息。

Spark 上的 Pregel API:每个超步是 (1) 边与顶点属性连接(Spark join),(2) 计算消息(Spark map),(3) 按顶点聚合消息(Spark reduceByKey),(4) 更新顶点属性(Spark join)。每超步四个 Spark 操作。每超步的开销是 Spark 的调度+洗牌成本。对 PageRank 的 40 个超步,是 160 个 Spark 阶段。有缓存(将图 RDD 持久化到内存),洗牌是唯一重复成本。

基准结果:GraphX 在纯 PageRank 上比 Giraph 慢 2 倍。但在端到端管道(加载 → 清洗 → 构建 → 计算 → 连接 → 输出)上快 5 倍,因为消除了三次数据移动。代码是 10 行而非 500 行。这是论点。不是"比 Giraph 快",而是"比使用 Giraph 的管道快"。

哲学观点:图处理不是独立学科。它是数据并行计算的特例。图是带连接关系的数据集。图算法是迭代连接。通过构建在 Spark 上,我们免费获得容错、缓存、多语言支持和与数据栈其余部分的集成。每迭代 2 倍开销是统一的代价。值得。

关键启示与行业影响

GraphX"图即表"洞见的更广泛含义是它消解了图处理与数据工程的边界。在 GraphX 之前,数据管道可能用 Spark 做 ETL、Giraph 做图分析、单独系统做输出。每个系统有自己的数据格式、容错机制和运维开销。GraphX 证明图只是管道中的另一个数据集——无需特殊系统。这一洞见影响了下一代图-on-数据湖系统(Databricks GraphFrames、AWS Neptune Analytics 与 Spark 集成、Snowflake 的图函数),并确立了对分析工作负载,数据平台就是图平台。

社交网络图LinkedIn 经济图谱

LinkedIn 经济图谱:作为图平台的职业网络

概述

LinkedIn 的"经济图谱"(Economic Graph)是公司内部对其统一图表示的称呼,涵盖全球职业经济:9 亿+ 会员、6000 万+ 公司、1 亿+ 职位、1.3 亿+ 教育机构,以及它们之间的关系(雇佣、教育、技能认可、内容互动)。它不是单一数据库——而是跨越数十个存储系统的图平台,由共同的身份层和图查询 API 统一,服务 LinkedIn 的信息流、推荐、搜索和广告产品。

经济图谱概念由 LinkedIn 工程领导层在 2014-2015 年前后提出,代表从"由独立数据库支撑的功能集合"到"所有功能查询的单一图"的战略转变。工程挑战不是构建图数据库——而是在不推倒重来迁移的情况下,将图抽象改造到十年积累的基础设施(Espresso、Voldemort、Oracle、Hadoop)之上。

系统每天处理数十亿图查询:"谁看了我的主页?"(反向边遍历)、"你可能认识的人"(带评分的 2 跳遍历)、"为你推荐的工作"(会员 → 技能 → 职位要求匹配)、"信息流排序"(会员 → 连接 → 内容互动)。每个都是图操作,但在针对不同访问模式优化的不同存储后端上运行。

架构

经济图谱是联邦式架构,而非单体图数据库:

身份层(图的节点)。 经济图谱中的每个实体有全局唯一 URN(统一资源名称):urn:li:member:12345urn:li:company:67890。身份服务(构建在 Espresso,LinkedIn 的分布式文档存储上)将 URN 解析为实体档案。这是图的"节点存储"——但它不是图数据库。它是带富文档的键值存储。

关系层(图的边)。 关系根据访问模式存储在多个系统中:

  • 连接图(会员 ↔ 会员):存储在 Espresso 支持的自定义邻接服务中。为"给我会员 X 的所有连接"(扇出读取)和"A 和 B 是否连接?"(点查)优化。
  • 内容互动(会员 → 帖子/文章):存储在 Kafka 流 + Espresso 物化视图中。为"最近互动"(时间排序)和"参与度评分"(聚合)优化。
  • 雇佣/教育(会员 → 公司/学校):存储在会员档案(Espresso 文档)中。为档案渲染(全文档读取)优化。
  • 技能和认可(会员 → 技能,会员 → 会员的认可):存储在带邻接表的专用图服务中。

图查询层(Galene / LI Graph)。 抽象多个存储后端的统一查询 API:

  • 查询表达为图遍历:"从会员 X 开始,遍历 'employed_at' 边到公司,遍历 'posting' 边到职位"
  • 查询规划器将多跳遍历分解为每后端查找,在查询层连接结果
  • 缓存:热子图(会员的一度连接、其最近活动)缓存在分布式缓存(Venice,LinkedIn 的派生存储)中

流处理(图的变更)。 图变更流经 Kafka:

  • 新连接 → Kafka 主题 → 连接图服务更新邻接表 → 信息流排序服务更新会员的信息流模型 → 通知服务发送提醒
  • 这种事件驱动架构意味着图在跨后端是最终一致的。连接在连接服务中立即"真实",但可能需要几秒才出现在推荐中。

离线图处理(Hadoop/Spark)。 全图分析离线运行:

  • 连接图上的 PageRank(搜索排序用)
  • 社区发现("你可能喜欢的群组")
  • 技能图构建(从档案中共现推断技能关系)
  • 这些作为 Spark 作业在全图快照上运行(每晚从在线存储导出到 HDFS)

设计决策

决策 1:联邦图 vs 单体图数据库。

LinkedIn 选择在现有存储系统上构建图抽象,而非迁移到单一图数据库(Neo4j、Titan 等)。

权衡:

  • 联邦优势:无迁移。LinkedIn 有 10+ 年数据在 Espresso、Voldemort、Oracle、Hadoop 中。将 9 亿会员档案和数十亿关系迁移到新存储需要数年,有数据丢失风险。
  • 联邦优势:按模式优化。连接图(扇出读取、点查)与内容图(时间排序流、聚合)有不同的访问模式。一个存储引擎无法同时优化两者。
  • 联邦成本:查询复杂性。2 跳遍历("朋友的朋友")需要两次后端查找和查询层内存连接。单体图数据库在一个引擎中完成。
  • 联邦成本:一致性。跨后端查询看到最终一致数据。会员的新连接可能几秒内不出现在推荐中。

团队判断迁移风险和按模式优化收益超过查询复杂性成本。查询层吸收复杂性——应用开发者编写图遍历,而非后端特定查找。

决策 2:事件驱动变更(Kafka)vs 同步写入。

图变更通过 Kafka 异步传播,而非同步更新所有受影响后端。

权衡:

  • 异步优势:写延迟。"接受连接请求"在 <50ms 返回(一次写入连接服务)。同步传播到 5 个下游系统会增加 200-500ms。
  • 异步优势:故障隔离。如果推荐服务宕机,连接仍然工作。Kafka 消费者在服务恢复时重试。
  • 异步成本:最终一致性。图在变更后几秒内跨后端不一致。"你可能认识的人"可能不反映 2 秒前建立的连接。
  • 异步成本:顺序复杂性。如果 Alice 连接 Bob 然后立即断开,Kafka 流必须保序,使下游系统不会结束于陈旧的"已连接"状态。

团队选择最终一致性,因为 LinkedIn 的用例容忍秒级陈旧。没有用户注意到新连接 3 秒内不出现在推荐中。但每个用户都会注意到"接受连接"需要 500ms。

决策 3:基于 URN 的身份 vs 数据库特定 ID。

每个实体有全局唯一 URN,而非使用后端特定 ID(Espresso 文档 ID、Oracle 主键等)。

权衡:

  • URN 优势:跨后端连接。查询层可以用 URN 作为连接键,将会员档案(Espresso)与其连接(邻接服务)与其内容(Kafka 流)连接。没有 URN,每个后端有自己的 ID 空间,查询层需要转换表。
  • URN 优势:稳定性。后端迁移(Voldemort → Espresso)不改变 URN。外部引用(共享链接、API 响应)保持有效。
  • URN 成本:间接层。将 URN 解析为后端特定 ID 需要查找(或缓存映射)。每实体访问多一跳。

团队构建了带激进缓存(Venice)的 URN 解析服务。间接成本是每请求每实体一次缓存命中——与图遍历成本相比微不足道。

决策 4:离线图处理(Spark)vs 在线图算法。

全图分析(PageRank、社区发现)作为批处理作业离线运行,而非在线图遍历。

权衡:

  • 离线优势:规模。9 亿节点和 100 亿+ 边上的 PageRank 需要全图迭代。没有在线图数据库能在请求时间(<100ms)内完成。Spark 在数小时内处理全图。
  • 离线优势:资源隔离。批处理分析不与在线服务竞争 CPU/内存/网络。
  • 离线成本:陈旧。PageRank 分数每晚计算。新会员的 PageRank 在下次批处理前是 0。
  • 离线成本:数据管道。全图必须每晚从在线存储导出到 HDFS。这是 PB 级数据移动。

团队接受全图分析的每晚陈旧。结果(PageRank 分数、社区标签)写回在线存储,作为预计算属性服务。在线查询读取预计算值——它们在请求时不运行图算法。

模拟设计者思考

我是 LinkedIn 的高级技术工程师,2014 年,我和工程副总裁在一个房间。问题:"如何让经济图谱成为现实?"我们一直把它作为愿景谈论——一个图,所有实体,所有关系。但现实是 40 个团队、15 个存储系统和十年积累的架构。

朴素答案:"把所有东西迁移到图数据库。"Neo4j、Titan,随便什么。一个存储、一个查询语言、一个团队。我立即否决这个想法。我们有 9 亿会员档案在 Espresso。数十亿内容互动在 Kafka。PB 级离线数据在 Hadoop。迁移需要 3 年、100 工程师年成本,有 50% 的灾难性数据丢失事件概率。董事会会解雇我们。

真正答案:抽象。不移动数据。在其上构建图查询层。应用开发者编写:"从会员 X 遍历 'knows' 边到深度 2,按行业过滤,按共同连接排序。"查询层将其分解为:(1) 在邻接服务查找 X 的连接,(2) 在 Espresso 查找每个连接的档案,(3) 按行业过滤,(4) 通过邻接服务计算共同连接,(5) 排序。五次后端调用,一个 API。

身份问题。后端 A 称会员为 "12345"。后端 B 称其为 "urn:li:member:12345"。后端 C 使用 UUID。没有共同键,查询层无法跨后端连接。我们需要 URN。每个实体获得一个。URN 服务将 URN 映射到后端特定 ID。它是带缓存的分布式哈希表。每实体一次额外查找,但使跨后端连接成为可能。

变更。当 Alice 连接 Bob,发生什么?同步:写入连接服务。异步(Kafka):通知信息流排序服务、推荐服务、通知服务、搜索索引。每个消费者更新自己对图的视图。连接立即"真实"(连接服务是真相来源)。其他一切在几秒内赶上。

为什么不同步传播?因为"接受连接"必须在 <50ms 返回。如果我们等待 5 个下游服务确认,就是 300ms。如果一个宕机,连接失败。不可接受。Kafka 给我们:即发即忘写入、保证投递(Kafka 保留消息 7 天)、顺序(按会员 ID 分区)、故障隔离(宕机的消费者不阻塞生产者)。

离线问题。全图 PageRank。9 亿节点、100 亿边、40 次迭代。这是 Spark 作业。每晚在 500 节点集群运行。耗时 4 小时。结果(每会员一个分数)写入 Espresso,作为预计算属性服务。在线查询不运行 PageRank——它们读取分数。图算法离线;图查询在线。

一致性模型。后端内:强一致(Espresso 在分区内是 CP)。跨后端:最终一致(Kafka 传播延迟)。查询层必须处理这一点:如果会员档案说"在 Google 工作",但公司图尚未索引新公司页面,查询返回档案数据,边返回"公司未找到"。优雅降级,而非失败。

我们不是在构建图数据库。我们在构建图平台。区别:图数据库是单一系统,带单一存储引擎和单一查询语言。图平台是抽象层,使 15 个系统对应用开发者看起来像一个图。更乱。更难。但这是唯一不需要 3 年迁移的路径。它让每个后端为其访问模式优化。连接图为扇出优化。内容图为时间排序流优化。档案存储为文档读取优化。一刀切不适合所有人。图平台拥抱异构性。

关键启示与行业影响

LinkedIn 联邦图架构的更广泛含义是它为拥有积累基础设施的大型组织演示了"图作为抽象层"模式。大多数企业无法推倒重来替换存储系统以采用图数据库。LinkedIn 的方法——在现有存储上构建图查询层、用 URN 统一身份、通过事件传播变更——是任何想要图能力而不需要多年迁移的组织的务实路径。该模式可推广:识别你的实体(节点),识别你的关系(边),分配全局标识符(URN),构建将图遍历分解为每后端查找的查询层,异步传播变更。图不是你购买的数据库——而是你在已有系统之上构建的抽象。教训:图平台是集成层,而非存储层。存储是遗留。图是你在其上构建的未来。

生产图算法Uber H3 与路由引擎

Uber 路由图:从 H3 六边形到实时导航

概述

Uber 的路由基础设施是生产中最大的实时图工程系统之一。每次 Uber 行程都需要在拥有数亿节点和数十亿边的道路网络图上计算最优路线,并持续用实时交通数据更新。系统必须在 100 毫秒内回答"从 A 到 B 的最快路线是什么?",同时考虑当前交通速度、道路封闭、转弯限制和历史模式——针对数百万并发查询。

路由图不是单一系统,而是管道:OpenStreetMap 数据被摄入、清洗、转换为有向图;交通速度实时融合到边上;图被分区和索引以支持快速最短路径查询;图抽象的层次结构(本地街道 → 主干道 → 高速公路)通过收缩层次实现亚线性查询时间。

与路由图互补的是 H3(六边形分层空间索引),Uber 的开源地理空间索引系统。H3 以 16 种分辨率将地球表面划分为六边形单元,提供图友好的空间索引:相邻六边形形成图,空间操作(附近司机、浪涌定价区、需求预测)成为 H3 网格上的图操作。

架构

道路网络图。 核心路由图是有向加权图:

  • 节点:路口和路段端点(全球约 5 亿)
  • :带属性的路段(限速、道路等级、转弯限制、单行标志、收费标志)
  • 权重:边遍历成本 = f(长度、速度、转弯惩罚、交通乘数)

图从 OpenStreetMap(OSM)数据构建:

  1. OSM 道路(折线)在路口处拆分为边
  2. 转弯限制(禁止左转、禁止掉头)编码为边对惩罚
  3. 单行道成为有向边
  4. 多车道道路可能成为不同速度的平行边

交通速度层。 实时速度融合到边上:

  • Uber 司机的 GPS 轨迹(匿名、聚合)提供每路段速度样本
  • 流处理管道(Flink)将样本聚合为每边速度估计,每 2-4 分钟更新
  • 历史速度画像(按星期几、小时)填补实时数据稀疏处
  • 有效边权重 = 基础成本 × (自由流速度 / 当前速度)

收缩层次(CH)。 查询算法不是全图 Dijkstra(对 5 亿节点太慢)。相反,Uber 使用收缩层次:

  • 预处理:迭代"收缩"低重要性节点(本地街道),在其邻居间添加快捷边。结果是层次结构:本地 → 主干道 → 高速公路。
  • 查询:层次结构上的双向 Dijkstra。从源的前向搜索沿层次"向上";从目标的后向搜索"向上"。它们在高阶节点(通常是高速公路)相遇。查询时间:O(√N) 而非 O(N)。
  • 定制:当交通速度变化(每 2-4 分钟),只有边权重变化——层次结构(哪些节点被收缩、哪些快捷边存在)稳定。权重更新通过快捷边以 O(快捷边数) 传播,而非 O(节点数)。

这种分离(结构 vs 权重)是关键工程洞见:昂贵的预处理(收缩)每次 OSM 更新(每周)运行一次。便宜的定制(权重更新)每几分钟运行一次。

H3 空间索引。 H3 以分辨率 9(约 174m 边长)将地球划分为约 140 亿六边形单元。H3 网格本身是图:

  • 每个六边形有 6 个(或 7 个)邻居
  • 邻接查询("哪些六边形与这个相邻?")通过 H3 索引方案 O(1)
  • 分层:分辨率 R 的每个六边形包含分辨率 R+1 的 7 个子六边形

H3 用于:

  • 司机定位:将 GPS 坐标映射到 H3 单元,按单元索引司机以快速"附近司机"查询
  • 浪涌定价:计算每 H3 单元的需求/供给比
  • 需求预测:每 H3 单元的时间序列预测
  • 路线可视化:按 H3 单元聚合行程数据生成热图

设计决策

决策 1:收缩层次 vs A* vs ALT vs 时间依赖 Dijkstra。

Uber 选择收缩层次而非实时路由的替代方案:

  • CH 优势:查询时间与距离无关,O(√N)。50km 路线和 500km 路线查询时间相似(约 50ms)。Dijkstra 是 O(N)——500km 比 50km 慢 10 倍。
  • CH 优势:权重定制便宜。交通更新改变边权重,而非图结构。CH 的层次结构(收缩顺序、快捷边)稳定。只有快捷边权重需要重算——O(快捷边数) ≈ O(N),几秒内完成。
  • CH 成本:预处理昂贵。收缩 5 亿节点在集群上耗时数小时。添加新道路(OSM 更新)需要重收缩受影响区域。
  • CH 成本:内存。快捷边大约使边数翻倍。5 亿节点 × 2.5 边/节点 × 2(快捷边)= 25 亿边 × 16 字节 = 40GB。适合大型服务器 RAM,但不适合小型。

被拒绝的替代方案:带地标的 A*(ALT)。ALT 提供 O(√N) 查询时间,但需要选择良好覆盖图的地标。对全球图,地标选择脆弱——坏的地标集退化为 O(N)。CH 的保证是结构性的,而非启发式。

被拒绝的替代方案:带剪枝的时间依赖 Dijkstra。实现更简单,但最坏情况 O(N)。在 5 亿节点,即使激进剪枝,查询时间 200-500ms。对 Uber 的 <100ms SLA 太慢。

决策 2:六边形网格(H3)vs 方形网格(S2/Geohash)vs R 树。

Uber 选择六边形作为空间索引:

  • 六边形优势:均匀邻接。每个六边形恰好有 6 个等距邻居。正方形有 4 个边邻居(距离 d)和 4 个角邻居(距离 √2·d)。这种不均匀性使空间算法(环搜索、k 近邻)复杂化。
  • 六边形优势:更好地近似圆形。半径 R 的六边形环比方形环覆盖更接近圆形的区域。对"附近司机"查询(圆形半径),六边形需要更少的单元覆盖该区域。
  • 六边形成本:无原生坐标系。六边形不像正方形那样整齐地镶嵌以用于地图渲染。H3 必须在六边形 ID 和经纬度之间转换,增加计算。
  • 六边形成本:分层细化是 7:1(每个六边形有 7 个子六边形),而非 4:1(正方形)。这意味着细分辨率下更多单元,但也更细粒度的索引。

被拒绝的替代方案:Google 的 S2(球面上的方形单元)。S2 有更好的分层属性(4:1 细化、空间填充曲线排序),但邻接不均匀。对 Uber 的用例(空间连接、环搜索、需求聚合),均匀邻接比分层效率更重要。

决策 3:分离结构与权重 vs 统一图更新。

CH 预处理(结构)每周运行。权重定制(交通)每 2-4 分钟运行。

权衡:

  • 分离优势:交通更新快。只有边权重变化。收缩层次(哪些节点被移除、哪些快捷边存在)不变。重算快捷边权重是快捷边上的线性过程。
  • 分离成本:新道路在下次 OSM 导入+重收缩(每周)前不出现。新开通的高速公路最多 7 天内不可路由。
  • 分离成本:结构变化(道路封闭、新转弯限制)需要重收缩受影响区域。这比权重更新更昂贵。

团队判断每周结构更新可接受(新道路相对交通变化罕见),交通更新 100 倍加速(秒 vs 小时)证明分离合理。

决策 4:集中式路由服务 vs 设备端路由。

Uber 在服务器端计算路线,而非司机手机上:

  • 服务器优势:访问实时交通数据(每分钟数百万 GPS 轨迹)。设备端路由需要将完整交通层流式传输到每个手机——不切实际。
  • 服务器优势:图更新即时。道路封闭在一次交通更新周期(2-4 分钟)内反映到所有路线。设备端需要应用更新或数据下载。
  • 服务器成本:延迟。到路由服务器的往返增加 50-100ms。设备端路由即时。
  • 服务器成本:可用性。如果路由服务宕机,无路线。设备端路由离线工作。

团队选择服务器端,因为交通数据优势是决定性的。用陈旧交通计算的路线(设备端)通常比用当前交通+50ms 延迟计算的路线(服务器端)更差。

模拟设计者思考

我是 Uber 路由团队的技术负责人,2016 年,我们每天处理 500 万行程。路由服务是瓶颈。每次行程需要路线。每条路线需要当前交通。图是巨大的:5 亿节点、12 亿边、全球覆盖。

Dijkstra 已死。在 5 亿节点图上,Dijkstra 为跨城路线探索数百万节点。每节点 100ns(乐观),每查询 100ms。我们每天 500 万行程 × 每行程 3 次路线请求 = 每天 1500 万查询。每次 100ms,每天需要 17,000 CPU 秒仅用于路由。这还没算交通更新。

收缩层次。想法:预计算层次结构。移除不重要的节点(死胡同、本地街道),在邻居间添加快捷边。重复。结果:多层图,高速公路在顶部,本地街道在底部。查询从源"向上"(本地 → 主干道 → 高速公路),从目标"向上",在顶部相遇。探索 O(√N) 节点而非 O(N)。对 5 亿节点,约 22,000 节点被探索。每个 100ns:2.2ms。加开销:总共 20-50ms。在 SLA 内。

但交通。CH 的预处理假设静态权重。交通每 2 分钟变化。我们每 2 分钟重收缩吗?不——收缩耗时数小时。洞见:分离结构与权重。层次结构(哪些节点被收缩、哪些快捷边存在)依赖图拓扑,而非权重。交通改变权重,而非拓扑。所以:收缩一次(每周,OSM 更新时)。持续定制权重(每 2 分钟,快捷边上的线性过程)。

定制过程:对收缩节点 v 创建的每个快捷边 (u, w),快捷边权重 = min(直接权重(u,w), 权重(u,v) + 权重(v,w))。当交通改变权重(u,v),重算快捷边。这是 O(快捷边数) ≈ O(边数) ≈ O(N)。对 12 亿边:多核机器上几秒。可接受。

H3 是不同问题。我们需要回答"附近司机在哪里?"城市中 50,000 名司机,每个在移动,每 4 秒报告 GPS。空间索引必不可少。R 树是经典答案,但难以分布式化(树结构、再平衡)。Geohash(方形网格)可行,但有对角线问题:共享角的两个单元在网格中"相邻",但现实中相距 √2×d。对"附近"查询,这意味着检查 8 个邻居而非 6 个。

六边形。每个单元有 6 个等距邻居。半径 R 的环覆盖接近圆形的区域。对"2km 内司机",我计算乘客的 H3 单元,扩展到适当分辨率的环,查询索引在这些单元中的司机。每环 6 个邻居,而非 8 个。更少单元检查。更均匀覆盖。

层次结构:16 种分辨率,从 4,357,449 km²(分辨率 0,覆盖大陆的单个六边形)到 0.9 m²(分辨率 15,覆盖桌子的六边形)。对司机定位,分辨率 9(174m 边长,约 0.1 km² 面积)是最佳点。城市在分辨率 9 有约 10 万单元。按单元索引司机给出 O(1)"附近"查询。

系统设计:路由图(CH,服务器端,50ms 查询)+ H3 索引(空间,分布式,O(1) 邻接)+ 交通管道(Flink,2 分钟更新)。三个图系统,三种访问模式,一个产品:通过最快路线让司机到达乘客。图无处不在。道路网络是图。六边形网格是图。交通流是图。Uber 的工程就是图的工程。

关键启示与行业影响

Uber 路由架构的更广泛含义是它演示了"图作为活系统"范式。道路图不是构建一次、永远查询的静态工件——而是持续更新、多层次的实时系统,结构每周变化(新道路)、权重每 2 分钟变化(交通),查询必须同时反映两层。这种"活图"模式——将稳定结构与动态属性分离、按各自自然节奏更新、服务跨越两层的查询——已成为任何有实时数据图系统的标准架构:金融交易图(结构:账户关系,每日更新;权重:交易风险分数,每交易更新)、物联网传感器图(结构:设备拓扑,部署时更新;权重:传感器读数,每秒更新)、物流网络(结构:仓库-路线拓扑,每季度更新;权重:容量和延迟,每小时更新)。教训:生产图永远不是"完成"的。它是多个节奏更新的管道,查询引擎必须透明地组合它们。

依赖图构建系统Bazel (Google Blaze)

Bazel:作为构建系统的依赖图

源文件 a.c源文件 b.c编译 a.o编译 b.o链接 binBazel 增量构建:依赖 DAG + 内容寻址缓存红色 = 脏节点(输入已变)· 仅重算受影响子图

概述

Bazel(Google 于 2015 年开源,内部自 2006 年起称为 Blaze)是一个构建和测试工具,将整个软件构建建模为有向无环图(DAG)。每个源文件、每个编译动作、每个测试、每个二进制文件都是节点。每个"此输入产生该输出"的关系都是边。构建系统的工作是遍历这个 DAG,确定哪些节点是脏的(自上次构建后输入变化),只重算那些——增量地、正确地、并行地。

Blaze 诞生于 Google 的一个特定痛点:monorepo(单一代码仓库)。到 2006 年,Google 的代码库是单一仓库,有数十亿行代码、数万个 BUILD 文件、数百万构建目标。完整构建不可行(数周 CPU 时间)。带朴素依赖跟踪器(make)的增量构建不正确——make 基于时间戳的失效遗漏传递依赖、头文件变化和生成代码。工程师浪费数小时在"干净构建"上以绕过陈旧工件。

Blaze 的洞见:如果将构建建模为带显式声明依赖(而非从时间戳推断)的图,你可以为任何变化计算受影响目标的精确集合,只重建那些,并保证正确性。图是真相。时间戳是启发式。Blaze 选择了真相。

架构

Bazel 的架构是图处理管道:

加载阶段(图构建)。 Bazel 读取 BUILD 文件(声明式目标定义)并构建目标图:

  • 每个 BUILD 文件定义目标:cc_libraryjava_binarypy_test
  • 每个目标声明其输入(srcsdepsdata)——这些是图中的边
  • 加载阶段将所有标签(//package:target)解析为具体目标,产生完整 DAG

目标图是静态的——构建期间不变。它是"可以构建什么"的图。

分析阶段(动作图构建)。 对每个请求的目标,Bazel 的规则生成动作——要执行的具体命令:

  • 有 10 个源文件的 cc_library 生成 10 个编译动作 + 1 个链接动作
  • 每个动作有:输入(文件)、输出(文件)、命令行、环境
  • 动作图是 DAG:编译动作依赖源文件和头文件;链接动作依赖所有目标文件

动作图是"必须执行什么"的图。它比目标图大(一个目标 → 多个动作),并且是惰性构建的(只针对请求的目标及其传递依赖)。

执行阶段(图遍历)。 Bazel 遍历动作 DAG:

  • 对每个动作,检查动作缓存:这个精确动作(相同输入、相同命令)之前执行过吗?如果是,复用缓存输出。
  • 如果未缓存,执行动作(本地或远程)并缓存结果。
  • 无未满足依赖的动作并行执行(受可用 CPU 或远程槽位限制)。

缓存键是以下内容的哈希:所有输入文件内容 + 命令行 + 环境。这是内容可寻址的——如果任何输入变化一个字节,哈希变化,动作重执行。无时间戳启发式。

远程执行。 动作可以在远程集群(Remote Execution API)上执行:

  • 输入文件上传到内容可寻址存储(CAS)——按内容哈希键控的 blob 存储
  • 远程工作节点下载输入、执行动作、上传输出
  • 多个 Bazel 客户端共享同一 CAS——如果工程师 A 已经编译了文件,工程师 B 的构建复用缓存输出,无需重编译

依赖图属性。

  • 密封性:动作声明所有输入。无对系统头文件、环境变量或未声明文件的隐式依赖。这使缓存键完整——如果输入相同,输出相同,无论机器如何。
  • 确定性:给定相同输入,动作产生相同输出。输出中无时间戳、无随机顺序。这使缓存跨机器和时间安全。
  • 最小性:只有声明的依赖可用。不声明对 //foo:bar 依赖的 cc_library 无法包含 foo/bar.h。编译器会失败。这强制图是准确的。

设计决策

决策 1:声明依赖 vs 推断依赖(make/CMake)。

Bazel 要求 BUILD 文件中显式声明依赖。Make 从文件时间戳和包含扫描推断依赖。

权衡:

  • 声明优势:正确性。依赖图正是开发者声明的。无遗漏依赖("在我机器上能跑"构建失败的第一大原因)。无幻影依赖(依赖你实际不使用的文件,导致不必要的重建)。
  • 声明优势:可缓存性。有完整输入声明,缓存键精确。两个有相同声明输入的构建产生相同输出。基于时间戳的系统无法保证这一点(touch 在无内容变化时失效)。
  • 声明成本:开发者负担。每个依赖必须声明。忘记 deps 条目导致构建失败(而非静默的不正确构建)。这是特性(快速失败),但感觉像开销。
  • 声明成本:工具。BUILD 文件必须随代码演进维护。自动化工具(buildifier、gazelle)有帮助,但不消除负担。

团队判断正确性保证(永不过时构建、永不"干净构建"绕路)证明声明开销合理。在 Google 的规模(数百万目标),不正确构建的成本(工程师小时浪费在调试幻影失败上)远超维护 BUILD 文件的成本。

决策 2:内容可寻址缓存 vs 基于时间戳的失效。

Bazel 的动作缓存使用内容哈希,而非时间戳。

权衡:

  • 内容哈希优势:跨机器正确性。如果工程师 A 在笔记本上编译文件,工程师 B 在构建服务器上编译相同文件,缓存命中有效(相同内容 → 相同哈希 → 相同输出)。基于时间戳的缓存无法跨机器共享(不同文件系统时间戳)。
  • 内容哈希优势:免疫 touch。触碰文件(改变时间戳而无内容变化)不失效。只有内容变化触发重建。
  • 内容哈希成本:哈希开销。缓存查找前必须哈希每个输入文件。对有 10 万输入文件的构建,是 10 万次哈希计算。通过缓存文件哈希缓解(只在 mtime 变化时重哈希)。
  • 内容哈希成本:存储。CAS 存储所有历史 blob。在 Google 的规模,CAS 是 PB 级。垃圾回收是不平凡的运维问题。

决策 3:密封动作 vs 系统依赖构建。

Bazel 动作无法访问未声明的系统资源。编译器是声明的依赖(而非 /usr/bin/gcc)。系统头文件是声明的输入(而非隐式可用)。

权衡:

  • 密封优势:可复现性。Linux 上的构建产生与 macOS 上相同的结果(给定相同工具链)。无"在我机器上能跑",因为"我的机器"无关紧要——只有声明的输入重要。
  • 密封优势:远程执行。动作可以在任何工作节点执行,因为它携带所有输入。无需在构建集群上复制开发者环境。
  • 密封成本:设置复杂性。每个工具(编译器、链接器、代码生成器)必须打包为 Bazel 依赖。你不能只 apt-get install gcc 然后构建。工具链本身是图节点。
  • 密封成本:学习曲线。新工程师必须理解为什么 #include 需要对 C 标准库 Bazel 包的声明依赖。

决策 4:monorepo 规模图 vs 每项目构建。

Bazel 为 monorepo(一个仓库、所有代码、所有依赖)设计。依赖图跨越整个代码库。

权衡:

  • Monorepo 优势:原子变更。触及 100 个包的重构是一个提交、一次构建。无版本协调、无"发布库、更新消费者"的舞蹈。
  • Monorepo 优势:全局优化。构建系统看到完整图。它可以检测整个代码库的未使用依赖、循环依赖和冗余编译。
  • Monorepo 成本:图规模。Google 的构建图有数百万节点和数千万边。加载完整图耗时数分钟。Bazel 用惰性加载缓解(只加载从请求目标可达的子图)。
  • Monorepo 成本:工具。VCS 必须处理 monorepo(Google 使用 Piper/CitC,自定义 VCS)。Git 在 Google 规模的 monorepo 上挣扎(但 Bazel 与 Git 在较小 monorepo 上工作良好)。

模拟设计者思考

我是 Google 的构建工程师,2005 年,我看着同事做"干净构建"。他调试链接错误两小时了。修复方法:rm -rf 构建目录,从头重建。四小时编译。这每天发生。跨越 20,000 名工程师。浪费惊人。

根本原因:make。Make 的依赖模型是时间戳。如果 A.o 比 A.c 旧,重编译。但 make 不知道头文件。如果 A.c 包含 B.h,B.h 变化,make 不重编译 A.c(A.c 的时间戳没变)。构建过时。二进制错误。工程师调试一小时,然后做干净构建。

如果依赖是显式的呢?如果 A.c 的 BUILD 文件说:"我依赖 B.h"?那么当 B.h 变化,构建系统知道重编译 A.c。无时间戳启发式。无遗漏依赖。无过时构建。

但 Google 规模的显式依赖。数百万目标。数千万依赖边。加载完整图耗时数分钟。我们需要惰性加载:只加载从请求目标可达的子图。如果我构建 //foo:bar,我只需要 //foo:bar 的传递依赖。图的其余部分无关。

动作图。目标不是动作。有 10 个文件的 cc_library 是 10 个编译动作 + 1 个归档动作。目标图是"什么"(声明式)。动作图是"如何"(命令式)。分析阶段将目标展开为动作。这种分离让规则变聪明:cc_library 可以基于平台生成不同动作(Linux:.so,macOS:.dylib),而不改变目标图。

缓存。动作缓存是杀手级特性。键:hash(所有输入) + hash(命令)。值:输出文件。如果键匹配,跳过执行。这是正确的,因为密封性:动作的输出只依赖其声明的输入和命令。无隐藏状态。无环境泄漏。相同键 → 相同输出。总是。

远程执行来自缓存。如果缓存共享(内容可寻址存储服务),那么工程师 A 的编译被工程师 B 复用。在 Google,CAS 跨 20,000 名工程师共享。编译一次的文件只编译一次。远程构建集群(数万核心)并行执行未缓存动作。本地需要 4 小时的构建远程 5 分钟。

密封性是很难推销的。工程师想要 #include 而不声明对 OpenSSL Bazel 包的依赖。"它在我系统上!它就是能跑!"是的,在你的系统上。在构建集群上,它不在。在下一个 macOS 版本,它被移除。密封性强制你声明它。如果你不声明,构建失败。这痛苦 5 分钟,节省 5 小时的"在我机器上能跑"调试。

图是产品。Bazel 不是构建工具。它是图处理系统,其图恰好代表软件构建。加载阶段构建图。分析阶段展开它。执行阶段遍历它。缓存记忆遍历。远程执行并行化它。每个设计决策服务图:使它显式、使它正确、使它可缓存、使它并行。构建只是应用。

关键启示与行业影响

Bazel 图中心设计的更广泛含义是它将"构建系统"重新定义为"增量计算引擎"。洞见:构建不是特殊类型的计算——它是"给定依赖 DAG,只重算脏节点"的通用模式。这种模式适用于软件构建之外:数据管道(只重算输入变化的表)、CI/CD(只重测依赖变化的服务)、甚至机器学习(只重训训练数据变化的模型组件)。Bazel 的图模型——显式依赖、内容可寻址缓存、密封执行——是所有增量计算系统的模板。Pants、Buck2、Turborepo、Nx 等工具都实现这种模式的变体,"自调整计算"(Acar 等)的学术文献提供理论基础。教训:当您将计算建模为带显式依赖的图时,增量性自然产生。图不仅是构建的数据结构——它是构建的算法。

图神经网络Deep Graph Library (DGL)

Deep Graph Library (DGL):消息传递作为通用 GNN 原语

v 聚合u1u2u3u4msgmsgmsgmsgGNN 消息传递:message → aggregate → updateGCN / GAT / GraphSAGE 均为该范式的实例

概述

Deep Graph Library(DGL)由纽约大学、亚马逊和更广泛学术社区的团队于 2018 年发布,是一个在现有深度学习框架(PyTorch、TensorFlow、MXNet)之上构建图神经网络(GNN)的框架。其核心贡献是将所有 GNN 架构——GCN、GAT、GraphSAGE、GIN、MPNN 和数十种变体——归约为单一原语:消息传递。GNN 层由三个函数定义:节点如何向邻居发送消息、消息如何聚合、节点如何更新状态。其他一切都是优化。

在 DGL 之前,实现新的 GNN 架构意味着为稀疏矩阵操作编写自定义 CUDA 内核、处理图批处理、管理图结构与张量计算之间的接口。DGL 抽象了这一点:程序员用熟悉的张量操作编写消息函数,DGL 处理图特定机制(稀疏聚合、邻居采样、小批处理、多 GPU 分布)。

DGL 的设计反映了 GNN 研究社区的特定洞见:尽管 GNN 论文激增(到 2019 年每年数百篇),几乎所有架构都是消息传递神经网络(MPNN)框架(Gilmer 等,2017)的实例。变化在于消息函数、聚合函数和更新函数——而非计算结构。DGL 使这种结构显式且高效。

架构

DGL 的架构有三层:

图存储(DGLGraph)。 核心数据结构:

  • 节点和边存储为 ID 范围(0 到 N-1,0 到 M-1)
  • 邻接以 CSR(压缩稀疏行)格式存储,用于快速邻居访问
  • 节点/边特征以密集格式存储为张量(PyTorch/TF/MXNet)
  • 通过"规范边类型"方案支持异构图(多种节点类型、多种边类型):(src_type, edge_type, dst_type)
  • 支持批处理图(多个小图打包成一个大图,带块对角邻接)

CSR 选择至关重要:对有 N 个节点和 M 条边的图,CSR 以 O(N + M) 空间存储邻接,并提供 O(度数) 邻居迭代。这与稀疏线性代数库(cuSPARSE、MKL)使用的格式相同,使 DGL 能够为消息聚合利用优化的稀疏矩阵内核。

消息传递引擎。 GNN 层分三个阶段执行:

  1. 消息函数:对每条边 (u, v),从 u 的特征、v 的特征和边的特征计算消息。`m_uv = msg_fn(h_u, h_v, e_uv)`
  2. 归约(聚合)函数:对每个节点 v,聚合所有入邻居的消息。`agg_v = reduce_fn({m_uv : u ∈ N(v)})`。支持的归约:sum、mean、max、min 和自定义(通过 UDF)。
  3. 更新函数:对每个节点 v,从旧特征和聚合消息计算新特征。`h_v' = update_fn(h_v, agg_v)`

DGL 通过 update_all()(所有节点)或 send_and_recv()(节点子集)实现。引擎:

  • 在可能时将消息+归约融合为单一稀疏操作(例如,sum 聚合 = 稀疏矩阵向量乘法)
  • 对复杂消息函数回退到逐边计算+scatter-gather
  • 支持通过消息传递的反向模式自动微分(梯度通过消息回流)

采样和小批处理层。 对大图(数百万节点),全图 GNN 计算不适合 GPU 内存。DGL 提供:

  • 邻居采样:对每个目标节点小批,每层采样 K 个邻居(GraphSAGE 风格)。这创建只包含采样边的"子图"(DGL 的 Block)。
  • 逐层采样:每个 GNN 层独立采样,减少指数级扇出(L 层的 K^L 邻居)。
  • Cluster-GC / GraphSAINT:将图划分为簇,每个小批在一个簇上训练。
  • 数据加载dgl.dataloading.DataLoader 处理多进程图采样+特征获取+整理成批张量。

设计决策

决策 1:消息传递作为通用原语 vs 架构特定内核。

DGL 将所有 GNN 归约为消息传递,而非提供每架构的优化实现(GCN 内核、GAT 内核等)。

权衡:

  • 消息传递优势:通用性。新 GNN 论文(有数百篇)可以用 10 行消息/归约/更新函数实现。无自定义 CUDA。无框架特定代码。
  • 消息传递优势:可组合性。异构 GNN(每种边类型不同消息函数)是自然的——只需定义每类型消息函数。
  • 消息传递成本:性能上限。手写 GCN 内核(融合 SpMM + ReLU + dropout)比 DGL 的通用消息传递快 20-30%(有消息函数分派开销)。
  • 消息传递成本:抽象泄漏。某些架构不适合(例如,需要全局图属性如谱方法的 GNN)。这些需要逃生通道。

团队测量:对 90% 场景(邻域聚合 GNN),通用消息传递在手写内核的 10-20% 以内。开发速度收益(一小时实现新架构 vs 一周)对研究用途超过性能差距。对生产,DGL 为常见场景(GCN、GAT)提供优化内核作为 dgl.nn 模块。

决策 2:CSR 邻接 vs COO vs 邻接表。

DGL 以 CSR(压缩稀疏行)格式存储图结构。

权衡:

  • CSR 优势:快速行切片。"获取节点 v 的所有邻居"是 O(1) 查找 + O(度数) 读取。这是消息传递的主导操作(迭代节点的入邻居以聚合消息)。
  • CSR 优势:稀疏矩阵兼容。CSR 是稀疏线性代数(cuSPARSE、MKL)的标准格式。sum/mean 的消息聚合归约为 SpMM(稀疏矩阵 × 密集矩阵),利用厂商优化的内核。
  • CSR 成本:边插入昂贵。添加边需要移动 CSR 数组。DGL 图在构建后实际上不可变(添加边需构建新图)。
  • CSR 成本:列访问慢。"获取节点 v 的所有入边"(列切片)需要扫描完整 CSR。通过同时存储 CSR(行)和 CSC(列)格式缓解。

被拒绝的替代方案:COO(坐标列表——(src, dst) 对数组)。COO 更简单,但不支持快速邻居迭代(必须扫描所有边以找到节点的邻居)。对消息传递(内循环是"对 v 的每个邻居"),CSR 必不可少。

决策 3:后端无关(PyTorch/TF/MXNet)vs 单后端。

DGL 支持三个深度学习后端,而非承诺一个。

权衡:

  • 多后端优势:用户选择。PyTorch 用户留在 PyTorch。TensorFlow 用户留在 TF。无强制迁移。
  • 多后端优势:研究覆盖。使用 DGL 的论文可以针对任何后端,最大化跨实验室可复现性。
  • 多后端成本:抽象层。DGL 的张量操作通过后端适配器(torch.tensor、tf.Tensor、mx.nd.array 的薄包装)。这增加小开销,限制后端特定功能的使用(例如 PyTorch 的 compile/fuse)。
  • 多后端成本:测试矩阵。每个功能必须在 3 个后端 × 2 个平台(CPU/GPU)= 6 种配置上工作。CI 时间 3 倍。

团队判断 GNN 社区(2018-2020)跨后端分裂,强制选择会限制采用。到 2022 年,PyTorch 主导,DGL 的 TF/MXNet 支持不太关键——但抽象层为向后兼容保留。

决策 4:基于子图的小批处理(Block)vs 全图计算。

对大图,DGL 创建"Block"——只包含小批采样边的子图。

权衡:

  • Block 优势:内存效率。1 亿节点图不适合 GPU 内存。有 1024 个目标节点 × 10 个采样邻居 × 3 层的 Block = 约 10 万节点。轻松适合。
  • Block 优势:计算效率。消息传递只在 Block 的边上运行,而非全图。每小批 O(采样边数) 而非 O(总边数)。
  • Block 成本:近似。每节点采样 K 个邻居引入方差。聚合消息是真实全邻域聚合的估计。收敛需要更多 epoch。
  • Block 成本:复杂性。程序员必须推理"这个 Block 中有哪些节点?"和"如何将 Block 节点 ID 映射回全图 ID?"DGL 提供实用工具,但心智模型比全图计算更复杂。

模拟设计者思考

我是纽约大学的研究员,2018 年,我实现了第 15 个 GNN 架构。每次:写消息函数、写聚合、写稀疏聚合的 CUDA 内核、处理批处理、处理多 GPU。每架构花一周。然后一篇论文出来,带轻微变体(不同聚合、不同消息函数),我又花一周。

Gilmer 等(2017)的洞见:一切都是消息传递。GCN:消息 = h_u / sqrt(deg(u)deg(v)),聚合 = sum。GAT:消息 = attention_weight h_u,聚合 = sum。GraphSAGE:消息 = h_u,聚合 = mean 或 LSTM。结构相同。只有函数不同。

如果我写一个框架,你定义三个函数——消息、归约、更新——框架处理图机制?无 CUDA。无稀疏矩阵代码。无批处理逻辑。只有:"这是我在边上计算消息的方式,这是我在节点聚合的方式,这是我更新的方式。"框架做其余。

图存储。我需要快速邻居迭代(消息传递迭代邻居)和快速稀疏聚合(对邻居 sum/mean)。CSR 给我两者:邻居迭代是连续内存读取,sum 聚合是 SpMM(稀疏矩阵 × 密集特征矩阵)。cuSPARSE 以 GPU 峰值吞吐的 90% 做 SpMM。我用 CSR,依赖 cuSPARSE。

但不是所有聚合都是 SpMM。Max 聚合不是矩阵乘法。自定义聚合(对邻居 LSTM)也不是。对那些,我需要回退:逐边计算消息(对所有边的密集张量操作),然后 scatter-gather 在节点聚合。这更慢(O(E) 逐边计算),但通用。框架自动选择:如果聚合是 sum/mean/max,用快速路径(SpMM 或分段归约)。否则,回退到逐边+scatter。

小批处理。1 亿节点图不适合 GPU 内存。我需要采样。GraphSAGE 的方法:对每个目标节点,采样 K 个邻居。对 3 层 GNN,感受野是 K³ 个节点。K=10 时,每目标 1000 个节点。1024 个目标的小批 → 子图中约 100 万节点。适合 16GB GPU 内存。

Block 抽象:有"输入节点"(采样邻域)和"输出节点"(我们计算其特征的目标节点)的子图。消息传递在 Block 上运行。输出是输出节点的特征。堆叠 3 个 Block(每层一个),你有 3 层 GNN 小批。程序员编写相同的消息/归约/更新函数——它们只在 Block 上运行,而非全图。

后端无关性。GNN 社区使用 PyTorch、TensorFlow、MXNet。我不能强制选择。我写薄适配层:F.tensor()F.matmul()F.relu() 分派到活动后端。图机制(CSR、采样、消息传递)后端无关(只是索引操作)。只有特征计算(消息函数、更新函数)通过适配器。

赌注:GNN 研究将继续以高速率产生新架构。每个都是新的消息/归约/更新三元组。如果 DGL 使实现新架构成为 10 行练习而非一周的 CUDA 项目,研究者会采用它。性能差距(vs 手写内核 10-20%)对研究可接受。对生产,我们为常见架构提供优化的 dgl.nn 模块。框架是研究工具;模块是生产路径。

关键启示与行业影响

DGL 消息传递抽象的更广泛含义是它为 GNN 研究建立了"通用语言"。在 DGL 之前,复现 GNN 论文意味着逆向工程自定义 CUDA 代码。DGL 之后,论文的实现是任何研究者可以阅读、修改和扩展的 20 行 MessagePassing 子类。这种标准化加速了该领域:2019 到 2022 年间每年发表的 GNN 架构数量增长两倍,DGL 实现伴随大多数。该框架不仅服务研究社区——它塑造了研究本身,使消息传递变体成为默认探索空间,推动非消息传递方法(谱方法、图 Transformer)显式证明其偏离范式的合理性。

知识图谱Wikidata

Wikidata:网络规模的协作知识图谱

概述

Wikidata 由 Wikimedia Deutschland 于 2012 年 10 月推出,是一个免费、协作、多语言的知识图谱,作为 Wikipedia 和更广泛 Wikimedia 生态系统的结构化数据骨干。拥有超过 1 亿条目(截至 2024 年)、15 亿+ 语句(边)和 30 万+ 编辑者的贡献,它是现存最大的协作维护知识图谱。每个条目是一个节点(人、地点、概念、事件),每个语句是带限定符和引用的类型化边,整个图可通过 SPARQL 查询。

Wikidata 的工程挑战在知识图谱中独一无二:它必须支持实时协作编辑(来自不受信任贡献者的每分钟数百次编辑)、维护 300+ 种语言 Wikipedia 版本间的一致性、在 10 亿+ 三元组上提供亚秒级 SPARQL 查询,并保持完全开放(CC0 许可、可下载转储、开放 API)。没有其他知识图谱将这种规模与这种开放性和这种编辑速度结合。

该系统的设计是为了解决特定问题:Wikipedia 的信息框(文章右侧的结构化数据表)在每个语言版本中独立维护。关于"柏林"的文章在英语、德语和法语 Wikipedia 中有不同的人口数字。Wikidata 集中了事实——柏林一个条目、一个人口语句,被所有语言版本引用。

架构

Wikidata 的架构构建在带自定义扩展的 MediaWiki 平台上:

数据模型。 图结构:

  • 条目(Q-ID):节点。每个条目有 QID(Q42 = 道格拉斯·亚当斯)、300+ 种语言的标签、描述、别名和语句。
  • 属性(P-ID):边类型。每个属性定义关系类型(P31 = "是...的实例"、P279 = "是...的子类"、P625 = "坐标位置")。
  • 语句:边。语句通过属性连接条目和值:(Q42, P31, Q5) = "道格拉斯·亚当斯是人类的实例"。语句可以有:

- 限定符:边属性(雇佣语句上的 P580 = "开始时间")

- 引用:来源(支持该语句的 URL 或引文)

- 等级:首选/正常/弃用(针对冲突值)

内部上,语句作为 RDF 三元组存储在三元组存储中(Blazegraph,后迁移到自定义 WDQS)。条目/属性/语句模型是叠加在 RDF 之上的属性图。

存储层。

  • 主存储:MySQL/MariaDB(通过 MediaWiki 的存储层)。条目和语句作为序列化 blob 存储在 MediaWiki 的 pagerevision 表中。每次编辑创建新修订(保留完整历史)。
  • 查询存储:Wikidata 查询服务(WDQS)——Blazegraph SPARQL 端点。复制管道将编辑从主存储流式传输到 Blazegraph,维护 RDF 三元组索引。查询延迟:大多数 SPARQL 模式 <1 秒。
  • 搜索索引:Elasticsearch,用于标签、描述和别名的全文搜索。

编辑管道。

  1. 用户提交编辑(通过 Web UI、API 或机器人)
  2. MediaWiki 验证权限、检查冲突(通过修订 ID 的乐观锁)
  3. 编辑应用到主存储(创建新修订)
  4. 变更事件发出到消息队列(Kafka/EventBus)
  5. 消费者:WDQS 更新器(将三元组流式传输到 Blazegraph)、Wikipedia 站点链接更新器(更新 300+ wiki 的信息框)、搜索索引更新器、转储生成器

SPARQL 查询引擎(WDQS)。

  • Blazegraph(Java 三元组存储),带 Wikidata 特定功能的自定义扩展
  • 查询超时:60 秒(对失控查询硬终止)
  • 速率限制:公共端点每 IP 1 查询/秒
  • 结果缓存:重复查询的 LRU 缓存
  • 联邦:SPARQL SERVICE 子句可以查询外部端点(但出于性能不鼓励)

设计决策

决策 1:基于 MediaWiki 的存储 vs 专用图数据库。

Wikidata 运行在带自定义扩展的 MediaWiki(Wikipedia 软件)上,而非专用图存储。

权衡:

  • MediaWiki 优势:社区工具。30 万编辑者已经了解 MediaWiki 的编辑模型(修订、讨论页、监视列表、回滚)。新存储后端需要重新培训社区并重建所有编辑工具。
  • MediaWiki 优势:经过验证的基础设施。MediaWiki 处理 Wikipedia 的规模(数百万页面、数千并发编辑者)。Wikidata 的编辑量(每分钟数百次)在其容量之内。
  • MediaWiki 成本:非图原生。条目存储为序列化 blob,而非带邻接表的图节点。遍历查询("传递跟随 P279 '子类' 边")需要 SPARQL 层,而非主存储。
  • MediaWiki 成本:修订开销。每次编辑将完整条目(所有语句)存储为新修订。对有 500 个语句的条目,单语句编辑写入全部 500 个。存储随编辑次数 × 条目大小线性增长。

团队判断社区和工具收益超过存储低效。Wikidata 的主要约束是编辑性的(让人类添加正确数据),而非技术性的(查询延迟)。为编辑者体验优化(熟悉工具、轻松回滚、透明历史)比为查询引擎优化更重要。

决策 2:SPARQL 查询 vs 自定义图查询语言。

Wikidata 暴露 SPARQL(W3C 的 RDF 查询标准),而非自定义语言。

权衡:

  • SPARQL 优势:标准合规。SPARQL 是 W3C 的 RDF 标准。工具、库和专业知识存在。外部应用可以用标准 SPARQL 客户端查询 Wikidata。
  • SPARQL 优势:表达力。SPARQL 支持属性路径(传递闭包)、OPTIONAL(左连接)、FILTER(任意谓词)和子查询。复杂图模式可表达。
  • SPARQL 成本:性能。在 10 亿三元组上的 SPARQL 查询规划很难。Blazegraph 的优化器是启发式的,而非基于成本。复杂查询(带 OPTIONAL 的多跳)可能需要 30+ 秒或超时。
  • SPARQL 成本:学习曲线。SPARQL 对非专家冗长且不直观。Wikidata 用可视化查询构建器(SQID)和示例查询缓解,但底层语言复杂。

考虑过的替代方案:Cypher(属性图查询语言)。被拒绝,因为 Wikidata 的数据模型是 RDF(带限定符/引用具体化的三元组)。Cypher 的属性图模型不能自然表达语句级元数据(关于语句的语句)。SPARQL 的三元组模型原生处理这一点。

决策 3:完整修订历史 vs 增量存储。

每次编辑将完整条目(所有语句、所有标签)存储为新修订,而非只存储增量(变化内容)。

权衡:

  • 完整修订优势:简单。回滚平凡:恢复先前修订。无需计算逆增量。冲突解决简单:完整条目上最后写入获胜(带乐观锁)。
  • 完整修订优势:可审计性。条目的任何过去状态可直接读取。无需从初始状态重放增量链。
  • 完整修订成本:存储。有 500 个语句的条目编辑 1000 次,存储 50 万个语句序列化。每语句约 200 字节,每高频编辑条目 100MB。跨越 1 亿条目,存储是 PB 级。
  • 完整修订成本:写放大。在 500 语句条目上编辑一个语句写入 500 个语句。写放大是每编辑 O(条目大小)。

团队接受存储成本,因为:(1) 存储相对工程复杂性便宜,(2) 可审计性要求(Wikipedia 的透明精神)需要完整历史,(3) 增量存储会使回滚和冲突解决显著更复杂。

决策 4:开放数据(CC0)vs 受控访问。

所有 Wikidata 内容是 CC0(公共领域)。完整转储可下载。API 开放(有速率限制但不受限)。

权衡:

  • 开放优势:网络效应。数千外部应用使用 Wikidata(研究工具、数据新闻、AI 训练数据)。每个使用增加 Wikidata 的价值并吸引更多编辑者。
  • 开放优势:信任。CC0 意味着无许可歧义。任何人可以出于任何目的使用数据,无法律风险。
  • 开放成本:滥用。机器人抓取 SPARQL 端点,造成负载。恶意编辑(破坏)对任何人可见,需要积极巡逻。
  • 开放成本:无货币化。Wikimedia 无法出售 Wikidata 访问权。基础设施由捐赠资助。

模拟设计者思考

我是 Denny Vrandečić,2011 年,我向 Wikimedia 基金会推销 Wikidata。问题:Wikipedia 有 285 个语言版本。每个维护自己的信息框。英语的柏林文章说人口 370 万。德语文章说 360 万。法语文章说 350 万。它们都引用不同年份的不同来源。一团糟。

解决方案:一个真相来源。柏林一个条目。一个人口语句(带引用和时间点限定符)。所有 285 个 Wikipedia 从这个条目拉取。当人口更新,所有版本同时更新。

但这不仅是数据库。这是结构化数据的协作百科全书。任何人都可以编辑。这意味着:破坏检测、编辑冲突、讨论页、监视列表、回滚。Wikipedia 的所有社交机制,应用于结构化数据。

存储问题。要构建图数据库吗?Neo4j?Virtuoso?Wikimedia 社区了解 MediaWiki。他们了解修订、讨论页、监视列表。如果我构建在 MediaWiki 上,第一天就有 30 万编辑者。如果我构建在 Neo4j 上,我有零编辑者和两年的工具项目。

所以:MediaWiki。条目是页面。语句存储在页面内容中(序列化)。每次编辑是修订。保留完整历史。回滚是恢复修订。冲突解决是乐观锁(如果修订 ID 变化,编辑失败)。这不优雅。这不是图原生。但它工作,社区采用它。

查询问题。研究者和应用需要查询图。"所有是'人类'实例、职业为'小说家'、国籍为'英国'的条目。"这是三元组模式查询。SPARQL 是标准。我运行 Blazegraph 实例,通过流管道将编辑复制到它,暴露 SPARQL 端点。

复制延迟。编辑命中 MediaWiki(主存储)。变更事件到 Kafka。消费者读取事件,将条目转换为 RDF 三元组,更新 Blazegraph。延迟:1-5 秒。可接受。主存储是真相;SPARQL 端点是派生索引。如果它们不一致,主存储获胜。

规模。1 亿条目。15 亿语句。每个语句约 3 个三元组(主-谓-宾 + 限定符三元组 + 引用三元组)。所以 Blazegraph 中约 50 亿三元组。查询性能:简单模式(一个三元组)<10ms。复杂模式(带 OPTIONAL 的 3 跳)1-10 秒。60 秒超时终止病态情况。

开放数据决策。CC0。公共领域。任何人可以下载完整转储(每周,压缩后约 100GB)。任何人可以查询 API。无注册、无许可谈判。这对知识图谱是激进的。Google、Amazon、Microsoft 的知识图谱是专有的。Wikidata 是免费的。赌注:开放创造专有图无法匹敌的网络效应。每个使用 Wikidata 的研究者成为倡导者。每个嵌入 Wikidata 数据的应用成为依赖。图因为免费而增长。

我们不是在构建最快的知识图谱。我们不是在构建表达力最强的查询引擎。我们在构建最受信任、最开放、协作维护最好的知识图谱。工程服务社区,而非相反。MediaWiki 是正确的存储,因为它是社区的工具。SPARQL 是正确的查询语言,因为它是标准。CC0 是正确的许可,因为它是精神。图是产品,但社区是引擎。

关键启示与行业影响

Wikidata 的更广泛含义是它演示了"开放知识图谱作为公共基础设施"模式的可行性。与专有知识图谱(Google 的、Amazon 的、Microsoft 的)服务单一公司的产品不同,Wikidata 服务整个网络。它为 300+ 种语言的 Wikipedia 信息框提供动力、为学术研究提供结构化数据、为 AI 系统提供训练数据、使数据新闻成为可能。CC0 许可意味着没有人拥有它,每个人都可以使用它。这种"公地"模式——共享、协作维护、自由许可的知识图谱——在技术格局中独一无二。它证明不是所有图工程都服务商业产品。有些图工程服务文明。教训:最有影响力的知识图谱可能不是最快或最大的——可能是最开放的。开放创造专有系统无法复制的网络效应:每个用户成为贡献者,每个应用成为依赖,图因为属于每个人而增长。

图数据库TigerGraph

TigerGraph:大规模并行图分析

概述

TigerGraph 由 Yu Xu(曾任职 IBM 和 Oracle)于 2012 年创立,是一个分布式图数据库,专为大规模图(1000 亿+ 边)上的实时分析设计。其区别性的工程主张是"原生并行图处理"——与将图分区并半独立处理分区的系统(Pregel 风格)不同,TigerGraph 的查询引擎在所有分区上同时执行图操作,每步都有分区间通信。这使系统能够对数十亿顶点的图上的深链接查询(10+ 跳)做出实时响应。

TigerGraph 的查询语言 GSQL 是带图特定扩展(累加器、图模式匹配、控制流)的类 SQL 命令式语言。它将 GSQL 查询编译为在存储引擎上原生运行的 C++ 代码——无解释、无 JVM 开销。这种编译方法给 TigerGraph 性能优势:GSQL 查询以原生速度运行,而非虚拟机或解释器的速度。

该系统面向企业分析用例:欺诈检测(在 <1 秒内发现跨 10 个账户的循环资金转移)、供应链优化(通过 15 层供应商追踪组件故障)、推荐(通过 5 跳图遍历实时计算个性化推荐)。

架构

TigerGraph 的架构是无共享 MPP(大规模并行处理)系统:

存储引擎(GPE — 图处理引擎)。

  • 图数据按顶点 ID 跨节点分区(哈希分区)
  • 每个节点以自定义列式格式存储其分区的顶点、边和属性
  • 边存储为邻接表:每个顶点的出边在内存中连续(类 CSR)
  • 属性按列存储:所有本地顶点属性 X 的所有值连续(对分析缓存友好)
  • 压缩:低基数属性的字典编码、排序 ID 的增量编码

查询引擎(GSE — 图搜索引擎)。

  • GSQL 查询由查询编译器编译为 C++
  • 编译后的代码在所有分区上同时运行(SIMD 风格并行)
  • 分区间通信使用自定义消息传递层(非 MPI、非 gRPC——专有低延迟协议)
  • 查询执行是流水线化的:一个算子的结果流向下一个,无物化

累加器模型。 GSQL 并行图计算的关键抽象:

  • 每个顶点有一组累加器(类型化寄存器:SumAccum、MinAccum、SetAccum 等)
  • 查询迭代顶点集,沿边发送消息,在目标顶点累加结果
  • 累加器可交换和结合——可以跨分区以任意顺序更新,最后合并
  • 这是 Pregel"合并消息"的泛化——累加器跨查询步骤持久化,使单次查询中的多跳计算成为可能

分布式执行。

  • 图按顶点 ID 哈希分区到 N 个节点
  • 查询的顶点集分布:每个节点处理其本地顶点
  • 边遍历跨分区:节点 A 的顶点有到节点 B 顶点的边。消息通过节点间协议发送。
  • 累加器更新是本地的(每个节点更新自己顶点的累加器)。跨分区累加器合并发生在同步点。

索引。

  • 主索引:顶点 ID → 分区 + 偏移(O(1) 查找)
  • 二级索引:顶点/边属性上(范围查询的 B 树、低基数的位图)
  • 图特定:邻接索引(CSR),用于 O(1) 邻居访问

设计决策

决策 1:编译查询(GSQL → C++)vs 解释执行。

TigerGraph 将 GSQL 编译为原生 C++,而非在运行时解释查询。

权衡:

  • 编译优势:性能。原生 C++ 以硬件速度执行。无字节码解释、无 JIT 预热、无 GC 暂停。对 100 亿边上 10 跳遍历,差异是 100ms(编译)vs 2-5 秒(解释/JVM)。
  • 编译优势:优化。C++ 编译器(gcc/clang)应用数十年的优化研究:循环展开、向量化、内联、寄存器分配。手写解释器无法匹敌。
  • 编译成本:延迟。编译 GSQL 查询需要 5-30 秒(C++ 编译慢)。这对即席查询不可接受。通过缓存编译查询缓解(编译一次,运行多次)。
  • 编译成本:复杂性。查询编译器必须为任意 GSQL 程序生成正确的 C++。这是比编写解释器更难的工程问题(类型错误、内存管理、并行代码生成)。

团队判断 TigerGraph 的目标工作负载(生产分析查询,运行数百万次)证明编译成本合理。欺诈检测查询每秒运行 10,000 次——在数十亿次执行上摊销 10 秒编译微不足道。即席探索使用预编译的"解释"模式(较慢但即时)。

决策 2:累加器模型 vs Pregel BSP vs 数据流。

TigerGraph 使用累加器(持久的每顶点寄存器),而非 Pregel 的消息传递或数据流模型。

权衡:

  • 累加器优势:单次查询多跳。Pregel 计算每跳需要一个超步。10 跳查询是 10 个超步带 10 次全局同步。基于累加器的查询可以在单次传递中表达 10 跳,带中间累加——更少同步、更少网络流量。
  • 累加器优势:表达力。累加器可以保存复杂状态(集合、映射、top-K 堆)。Pregel 消息通常是标量。"找到最短 5 条路径"的查询需要路径的 SetAccum,而非标量消息。
  • 累加器成本:内存。每个有活跃累加器的顶点消耗内存。对触及 10 亿顶点带 SetAccum 的查询,是数十 GB 的累加器状态。Pregel 的消息是临时的(每超步消费并丢弃)。
  • 累加器成本:语义。累加器合并顺序对非交换操作很重要。TigerGraph 将累加器限制为可交换/结合类型,以保证无论分区执行顺序如何结果确定。

决策 3:哈希分区 vs 图感知分区。

顶点按 ID 哈希分区,忽略图结构。

权衡:

  • 哈希优势:简单。无预处理。无分区计算。添加顶点是 O(1)(哈希到分区)。图增长时无需再平衡(一致性哈希)。
  • 哈希优势:负载均衡。对均匀 ID 分布,哈希分区给出均匀分区大小。无热分区。
  • 哈希成本:跨分区边。对社交图(幂律),枢纽顶点的邻居在所有分区。从枢纽的遍历向每个分区发送消息。网络流量是每跳 O(度数 × 消息大小)。
  • 哈希成本:无局部性。相邻顶点(图中)不共置(同一节点)。每次边遍历都可能是网络跳转。

团队缓解跨分区成本:(1) 低延迟节点间协议(可用时 RDMA,否则自定义 TCP),(2) 消息批处理(累积到同一分区的消息,批量发送),(3) 累加器局部性(每个节点只更新自己顶点的累加器——计算期间无跨分区写入,只在同步点)。

决策 4:无共享 MPP vs 共享磁盘/共享内存。

TigerGraph 是无共享的:每个节点有自己的 CPU、内存和磁盘。无共享存储、无共享内存。

权衡:

  • 无共享优势:线性可扩展性。添加节点按比例增加 CPU + 内存 + 磁盘。无共享瓶颈(无 SAN、无内存总线限制)。
  • 无共享优势:故障隔离。节点故障只影响其分区。其他节点继续。恢复是分区级,而非系统级。
  • 无共享成本:网络是瓶颈。跨分区操作(连接、遍历、聚合)需要网络通信。网络带宽限制吞吐。
  • 无共享成本:数据倾斜。如果一个分区的边比其他多 10 倍(幂律图),它成为瓶颈。所有其他节点等待它。

团队选择无共享,因为 TigerGraph 面向 1000 亿+ 边的图,单机内存装不下。共享内存(单机)在这个规模不可行。共享磁盘(SAN)增加存储瓶颈。无共享是目标规模的唯一选择。

模拟设计者思考

我是 Yu Xu,2012 年,我在 IBM 和 Oracle 构建数据库引擎 15 年了。我见过市场上所有图数据库。它们都慢。不是"比最优稍慢"的慢。是"10 亿边图上 5 跳查询需要 30 秒"的慢。对欺诈检测,这太慢了。欺诈者在 3 秒内就跑了。

为什么慢?两个原因。第一:解释。Neo4j 在运行时解释 Cypher。每步是 Java 方法调用、虚拟分派、GC 跟踪的对象分配。对触及 1000 万顶点的遍历,是 1000 万次方法调用。每次 100ns(对 Java 乐观),仅分派开销 1 秒。第二:同步。Pregel 风格系统每跳后同步。10 跳 = 10 次全局屏障。每次屏障等待最慢分区。在 100 节点集群,屏障开销是 10 × 5ms = 50ms。不算可怕,但累积。

我的赌注:编译为 C++。无解释。无虚拟分派。查询编译器生成紧凑的 C++ 循环:对每个顶点、对每条边、累加。C++ 编译器向量化(SIMD)、内联、展开。Java 需要 1 秒的相同遍历,编译 C++ 50ms。这是 20 倍。不来自更聪明的算法——来自消除解释开销。

累加器模型。Pregel 的消息传递对分析太受限。"找到 5 跳内有可疑模式的所有账户"需要跨跳携带状态(到目前为止的路径、运行总和、访问节点集)。Pregel 消息是标量。我需要跨查询持久的每顶点状态。累加器:附加到每个顶点的类型化寄存器。总和用 SumAccum。访问集用 SetAccum。top-K 用 HeapAccum。它们在遍历期间原地更新,在同步点合并。

并行模型。无共享 MPP。每个节点拥有一个分区。查询在所有节点同时运行。跨分区边是消息。但我不每跳后同步。我只在累加器需要合并时同步(当顶点从多个分区接收更新)。对大多数边是分区内(本地)的 5 跳查询,我可能只同步两次。其余是原生速度的本地计算。

存储。属性列式(分析扫描是列导向:"对分区 P 中顶点的交易金额求和")。边行邻接(遍历需要顶点的所有边连续)。邻接 CSR(O(1) 邻居访问)。这种混合布局服务分析(扫描属性)和遍历(跟随边),不牺牲任何一方。

编译延迟问题。编译 C++ 需要 10 秒。运行即席查询的用户不能等 10 秒。解决方案:两种模式。"解释模式"用于探索(执行慢、启动即时)。"编译模式"用于生产(执行快、10 秒编译)。生产查询编译一次并缓存。10 秒成本摊销到数百万次执行。

目标:100 亿边金融图上的 5 跳欺诈检测查询,<100ms 返回结果。这是基准。如果达到,银行会买。如果达不到,这是研究项目。编译 C++ + 累加器模型 + 无共享 MPP + RDMA 网络。这是技术栈。构建它。

关键启示与行业影响

TigerGraph 的编译查询方法验证了"查询即程序"的范式:图查询语言不必解释执行——它可以编译为原生代码,获得与手写实现相当的性能。这一洞见影响了后续图数据库的查询引擎设计(Neptune 的查询编译、Neo4j 的 Cypher 编译模式)。累加器模型证明了 BSP 消息传递的泛化——通过允许跨步骤持久的每顶点状态,多跳计算可以在单次查询中表达,减少全局同步次数。教训:对延迟敏感的企业图分析,编译执行和累加器抽象的组合比解释执行和纯消息传递更有优势。

图处理框架阿里巴巴 GraphScope

阿里巴巴 GraphScope:统一交互式、分析与学习工作负载

概述

GraphScope 由阿里巴巴达摩院于 2021 年开源,是一个统一图计算系统,在单一引擎中处理三种工作负载类型:交互式图查询(OLTP 风格遍历)、分析型图算法(OLAP 风格全图计算如 PageRank)和图神经网络训练(GNN 学习)。在 GraphScope 之前,这三种工作负载需要三个独立系统:图数据库(Neo4j/JanusGraph)用于查询、图处理引擎(Giraph/GraphX)用于分析、GNN 框架(DGL/PyG)用于学习。GraphScope 的工程论点是这些系统之间的数据移动才是瓶颈——而非计算本身。

该系统源于阿里巴巴的内部需求:其电商平台运行欺诈检测(交互式:"通过 10 跳追踪这笔交易")、物流优化(分析型:"计算配送网络上的最短路径")和推荐(学习:"在用户-商品交互图上训练 GNN")。在独立系统上运行这些意味着三倍的数据导入、三倍的存储成本,以及阶段之间数小时的 ETL。

GraphScope 的架构构建在 Vineyard(用于零拷贝数据共享的内存数据管理器)和 Kubernetes(用于弹性资源分配)之上。图在 Vineyard 的共享内存中存储一次,所有三个引擎(交互式、分析型、学习型)访问它,无需序列化或网络传输。

架构

GraphScope 的架构有四层:

存储层(Vineyard)。 Vineyard 是内存不可变数据存储:

  • 图以列式数组存储在共享内存中(顶点 ID、边列表、属性)
  • 零拷贝访问:引擎直接映射 Vineyard 的内存(无序列化、无网络)
  • 不可变:一旦写入,图不变。更新创建新版本(写时复制)
  • 分布式:多个节点上的 Vineyard 实例通过分布式对象存储共享数据(后端为 OSS/S3 用于持久化)

Vineyard 中的图格式:

  • CSR 邻接(行指针 + 列索引)用于快速遍历
  • 列式属性(每属性一个连续数组)用于快速分析
  • 顶点/边 ID 映射用于跨引擎一致性

交互式引擎(GIE — 图交互引擎)。

  • 支持 Gremlin 和自定义类 Cypher 语言
  • 构建在图原生执行引擎上(非 TinkerPop 的 Java 遍历机)
  • 索引结构:属性上的 B 树、ID 上的哈希索引、邻接的 CSR
  • 查询编译:查询编译为物理计划(算子流水线)并原生执行
  • 延迟目标:十亿边图上 3 跳遍历 <100ms

分析引擎(GAE — 图分析引擎)。

  • 实现 Pregel 和 PIE(并行增量执行)模型
  • 构建在 GRAPE(并行图处理框架)之上:算法表达为顺序代码 + "部分求值"接口,GRAPE 自动并行化
  • 支持:PageRank、连通分量、SSSP、社区发现、三角形计数,以及通过 PIE 接口的自定义算法
  • 执行:BSP 风格超步,带自动分区和消息聚合

学习引擎(GLE — 图学习引擎)。

  • 与 DGL 和 PyTorch Geometric 集成用于 GNN 训练
  • 图采样(邻居采样、逐层采样)在存储层(Vineyard)运行,而非训练框架中
  • 特征获取是零拷贝:GNN 训练器直接将 Vineyard 的属性数组映射为 PyTorch 张量
  • 分布式训练:图分区 + 跨 GPU 的数据并行 GNN 训练

协调(Kubernetes Operator)。

  • GraphScope 作为 Kubernetes 部署运行
  • Operator 按引擎分配资源(交互式:CPU 密集、分析型:内存密集、学习型:GPU 密集)
  • 弹性扩展:为大型 PageRank 作业添加分析工作节点,完成后释放
  • 会话管理:"会话"绑定图 + 引擎 + 资源分配

设计决策

决策 1:统一引擎 vs 独立系统。

GraphScope 将交互式、分析和学习引擎组合在一个系统中,而非集成独立系统。

权衡:

  • 统一优势:零数据移动。图在 Vineyard 中存储一次。所有引擎在共享内存中访问它。无 ETL、无序列化、无"查询图"和"在图上训练"之间的网络传输。
  • 统一优势:一致性。所有引擎看到相同图版本。无"分析引擎处理昨天的数据,而查询引擎看到今天的"。
  • 统一成本:复杂性。三个有不同执行模型(查询规划、BSP、梯度下降)的引擎必须共存。工程团队必须在所有三个领域专业。
  • 统一成本:资源竞争。大型分析作业(100 亿边上的 PageRank)消耗交互式引擎低延迟查询所需的内存。隔离需要仔细的资源管理。

团队测量:在阿里巴巴的欺诈检测管道中,将图从查询系统移动到分析系统再到 GNN 训练系统,每天需要 4 小时 ETL。GraphScope 消除了这一点。4 小时变为 0。计算时间(查询 + 分析 + 训练)不变,但管道延迟从 6 小时降到 2 小时。

决策 2:Vineyard(共享内存)vs 分布式文件系统(HDFS/S3)。

GraphScope 将图存储在 Vineyard 的共享内存中,而非分布式文件系统上。

权衡:

  • 共享内存优势:零拷贝。引擎通过映射内存访问图数据,而非读取文件。无反序列化、无网络 I/O。PyTorch GNN 训练器可以直接使用 Vineyard 的属性数组作为张量,无需复制。
  • 共享内存优势:延迟。内存访问约 100ns。S3 读取约 50ms。对交互式查询(100ms SLA),差异是决定性的。
  • 共享内存成本:容量。内存昂贵。带属性的 100 亿边图约 500GB。这是集群上 500GB 的 RAM。HDFS/S3 存储每 GB 便宜 10 倍。
  • 共享内存成本:持久化。Vineyard 在内存中。集群重启丢失图(除非后端为 S3)。GraphScope 持久化到 S3,启动时重新加载,但重新加载需要数分钟。

团队判断对阿里巴巴的工作负载(适合集群内存的图、多个引擎持续访问),零拷贝收益超过容量成本。对不适合内存的图,GraphScope 回退到磁盘支持的存储(较慢但更大)。

决策 3:GRAPE(自动并行化)vs 显式 Pregel API。

GraphScope 的分析引擎使用 GRAPE(自动并行化顺序图算法),而非要求程序员编写 Pregel 风格的顶点程序。

权衡:

  • GRAPE 优势:程序员生产力。编写顺序算法(单线程 C++)。GRAPE 自动跨分区并行化。无消息传递样板、无超步逻辑。
  • GRAPE 优势:正确性。顺序算法易于验证(就是教科书算法)。并行化是机械的(分区 + 部分求值 + 合并)。比手写 Pregel 更少 bug。
  • GRAPE 成本:性能上限。自动并行化对通信敏感算法(消息聚合顺序重要的地方)无法匹敌手写 Pregel。部分求值接口的开销增加 10-20%。
  • GRAPE 成本:表达力。某些算法不能干净地分解为"顺序 + 部分求值"。有复杂分区间依赖的算法(例如带冲突解决的图着色)很笨拙。

团队选择 GRAPE,因为目标用户是数据科学家,而非系统工程师。数据科学家可以用 20 行顺序 C++ 编写 PageRank,免费获得分布式实现。要求他们编写 Pregel 顶点程序(带消息类型、超步逻辑、停止条件)会限制采用。

决策 4:Kubernetes 原生 vs 裸机/YARN。

GraphScope 运行在 Kubernetes 上,而非 YARN(Hadoop)或裸机。

权衡:

  • Kubernetes 优势:弹性资源。为批处理作业扩展分析工作节点,完成后缩减。GNN 训练的 GPU 节点、交互式查询的 CPU 节点。Kubernetes 调度它们。
  • Kubernetes 优势:多租户。多个团队共享集群。Kubernetes 命名空间隔离他们的 GraphScope 会话。
  • Kubernetes 成本:开销。Kubernetes 增加延迟(Pod 调度、服务发现、容器网络)。对交互式查询(100ms SLA),每毫秒 Kubernetes 开销都重要。
  • Kubernetes 成本:复杂性。运维 Kubernetes 集群不平凡。GraphScope 在运维面上增加 Kubernetes Operator、CRD 和 Helm chart。

模拟设计者思考

我是阿里巴巴达摩院 GraphScope 的技术负责人,2019 年,我看着我们的欺诈检测管道。它是三个系统。步骤 1:在 JanusGraph 中查询交易图("找到这个可疑账户 5 跳内的所有账户")。步骤 2:将子图导出到 HDFS,在 Giraph 中运行连通分量作业("哪些账户形成集群?")。步骤 3:将集群特征导出到训练集,在 DGL 中训练 GNN("将每个账户分类为欺诈或合法")。三个系统。三种数据格式。步骤之间 4 小时 ETL。欺诈分析师从"标记这个账户"到"这是裁决"要等 6 小时。

数据是同一个图。交易图。存储在 JanusGraph。导出到 HDFS 作为邻接列表。加载到 DGL 作为 PyTorch 张量。三份拷贝。三种格式。三条导入管道。而且它们总是不同步——JanusGraph 有今天的交易,HDFS 有昨天的快照,DGL 有上周的训练数据。

如果图存在于一个地方呢?一个存储。三个引擎。交互式引擎查询它(Gremlin)。分析引擎处理它(PageRank、连通分量)。学习引擎在它上面训练(GNN)。无导出。无 ETL。无格式转换。图在共享内存中,所有三个引擎直接映射它。

Vineyard 是推动者。带零拷贝访问的内存数据存储。图存储为 CSR + 列式属性。交互式引擎遍历 CSR。分析引擎扫描列。学习引擎将属性数组映射为 PyTorch 张量。无序列化。无网络。只有内存映射。

但一个系统中三个引擎是系统工程噩梦。交互式引擎需要低延迟(100ms 查询)。分析引擎需要高吞吐(数分钟处理 100 亿边)。学习引擎需要 GPU(数百万节点特征上的梯度下降)。它们有不同的资源画像、不同的故障模式、不同的扩展特性。

Kubernetes。每个引擎是一组 Pod。交互式引擎:带低延迟网络的 CPU Pod。分析引擎:内存密集的 Pod,为批处理作业扩展。学习引擎:GPU Pod,为训练扩展。Kubernetes 调度它们、隔离它们、扩展它们。GraphScope Operator 管理生命周期:创建会话、绑定图、分配引擎、运行工作负载、释放资源。

分析引擎。我不想让数据科学家写 Pregel。他们不是系统工程师。他们知道 PageRank 是公式,而非顶点程序。GRAPE:编写顺序算法。"对每个顶点,对邻居的排名求和,除以度数,衰减。"就这些。GRAPE 分区图,在每个分区上运行顺序代码,交换边界值,迭代到收敛。并行化是自动的。正确性是显然的(就是教科书算法)。性能是手写 Pregel 的 80%。足够好。

学习引擎。10 亿节点图上的 GNN 训练。全图训练不适合 GPU 内存。采样:对每个小批,每个目标节点采样 K 个邻居。采样在 Vineyard 中运行(内存中、快速)。采样的子图作为零拷贝张量传递给 DGL/PyG。无序列化。GPU 训练器看到的 PyTorch 张量实际上是 Vineyard 的内存。梯度更新写回 Vineyard 的属性存储。下一个 epoch,更新的特征已经在那里。

结果:欺诈检测管道从 6 小时到 30 分钟。ETL 消失了。图在一个会话中被查询、分析和训练。分析师标记账户,系统追踪子图(交互式)、聚类它(分析型)、分类它(学习),返回裁决。一个图。三个引擎。零数据移动。这就是 GraphScope。

关键启示与行业影响

GraphScope 的更广泛含义是它为生产 AI 管道验证了"一个图、多个引擎"架构。图存储(Vineyard)与图计算(交互式、分析型、学习)的分离允许每个引擎为其工作负载优化而不牺牲其他。这种模式——共享不可变存储与专用计算层——是图原生的数据工程中 lakehouse 架构的等价物,它指向图数据成为 AI/ML 管道一等公民的未来,而非从数据库导出的事后想法。

编译器程序图GraalVM / Truffle

GraalVM:节点之海图编译器

概述

GraalVM 是 Oracle Labs 的通用虚拟机和编译器基础设施,核心是 Graal 编译器。Graal 是一个 JIT(即时)编译器,将程序表示为"节点之海"(sea of nodes)——一个统一的图,其中控制流和数据流都是节点,边表示数据依赖和控制顺序。这种统一图表示使 GraalVM 最具特色的能力成为可能:部分求值(partial evaluation),即通过将解释器的控制流折叠进程序的数据流,为特定程序特化解释器,为任何语言产生优化的机器码。

节点之海概念由 Cliff Click 引入(1995 年),首次在 HotSpot C2 编译器中实现。Graal 将其从编译器 IR 扩展为通用程序表示:任何语言(Java、JavaScript、Python、Ruby、R、LLVM 位码)都被翻译为相同的图 IR,由相同的图变换优化,编译为相同的机器码。图是通用语言。

GraalVM 的工程成就不是原始峰值性能(HotSpot C2 对 Java 持平或超过它),而是多语言优化:调用 Java 库的 JavaScript 程序调用 Python 脚本,被编译为一个图,跨语言内联、逃逸分析和死代码消除跨越所有三种语言。没有其他系统做到这一点。

架构

GraalVM 的架构围绕图 IR 组织:

节点之海 IR。 编译器 IR 是有向图,带两种边类型:

  • 数据边:连接产生值的节点和消费值的节点(如 SSA 使用-定义边)。这些定义值流向何处。
  • 控制边:连接控制产生节点(分支、循环、合并)和控制消费节点(程序顺序中的下一个操作)。这些定义执行顺序。

"海"的隐喻:节点漂浮在海中。数据边是水流(值沿其流动)。控制边是锚(将节点固定到执行顺序)。无控制边的节点是"漂浮"的——可以调度到其数据依赖允许的任何地方。这种自由使激进优化成为可能:调度器可以重排漂浮节点以最小化寄存器压力或最大化 ILP。

节点类型包括:

  • 值节点:ConstantNode、ParameterNode、AddNode、MulNode、LoadFieldNode 等
  • 控制节点:StartNode、IfNode、MergeNode、LoopBeginNode、ReturnNode 等
  • 帧状态节点:解释器状态的快照(用于去优化)

编译管道。

  1. 字节码解析:Java 字节码(或 Truffle AST)被解析为初始节点之海图。每个字节码成为 1-5 个 IR 节点。
  2. 高级优化:内联(将被调用者的图拼接进调用者)、逃逸分析(用栈槽替换堆分配)、循环变换(小循环的展开、剥离、完全展开)。
  3. 中级优化:全局值编号(合并相同子图)、强度削减(2 的幂的 MulNode 替换为 ShiftNode)、条件消除(移除冗余 IfNode)。
  4. 低级优化:调度(为漂浮节点分配控制边)、寄存器分配(干扰图上的图着色)、指令选择(将 IR 模式匹配到机器指令)。
  5. 代码生成:调度后的图线性化为机器码(x86-64、AArch64、RISC-V)。

Truffle 框架(多语言)。 Truffle 是语言实现框架:

  • 语言实现者编写 AST 解释器(节点树,每个节点的 execute() 方法解释一个 AST 节点)
  • Truffle 检测解释器:在热路径上,它捕获 AST + 执行画像并发送给 Graal
  • Graal 部分求值解释器:它在 AST 上"运行"解释器符号化,将解释器控制流(if (node instanceof AddNode) 分派)折叠进程序的数据流。结果:就是程序本身的图,无解释器开销。
  • 编译后的图被优化并生成为机器码。解释器消失了。

部分求值洞见。 Truffle 解释器是一个循环:while (true) { node = stack.pop(); node.execute(); }node.execute() 按节点类型分派(虚拟调用或 switch)。部分求值为特定 AST 展开这个循环:它知道 AST 形状(来自画像),所以它内联每个 execute() 调用、消除分派,产生程序操作的直线图。解释器的开销(分派、栈管理、类型检查)被编译掉。

设计决策

决策 1:节点之海 vs CFG + SSA(LLVM 风格)。

Graal 使用统一的节点之海,而非 LLVM 的分离 CFG + SSA 值图。

权衡:

  • 节点之海优势:调度自由。在 LLVM 的 CFG 中,指令在基本块内排序。重排需要在块之间移动指令(复杂)。在海中,漂浮节点没有固定位置——调度器最优地放置它们。这使更好的寄存器分配和 ILP 利用成为可能。
  • 节点之海优势:统一优化。控制流优化(分支消除、循环展开)和数据流优化(CSE、强度削减)在同一个图上操作。无需在 CFG 表示和值表示之间同步。
  • 节点之海成本:复杂性。图更难调试(无线性指令顺序可打印)。验证更难(必须同时检查数据流和控制流不变量)。
  • 节点之海成本:调度是强制的。LLVM 可以直接从 CFG 生成代码(指令已排序)。Graal 必须在生成前调度海——一个可能引入 bug 的额外阶段。

团队选择节点之海,因为 Graal 的目标是激进优化(匹配或超过 C2 的性能)。调度自由对在现代乱序 CPU 上利用 ILP 至关重要。复杂性成本对有小型专家团队的研究编译器可接受。

决策 2:部分求值用于多语言 vs 每语言编译器。

GraalVM 通过 Truffle 部分求值编译所有语言,而非为每种语言构建独立的优化编译器。

权衡:

  • PE 优势:一个编译器。Graal 优化所有语言。新语言(R、Ruby、DSL)只需要 Truffle 解释器(约 5000 行),而非完整编译器(约 10 万行)。优化编译器被复用。
  • PE 优势:跨语言优化。调用 JavaScript 函数的 Java 方法跨语言边界内联。组合图被整体优化。无 FFI 开销、边界处无序列化。
  • PE 成本:性能上限。PE 无法匹敌手工调优的语言特定编译器(JavaScript 的 V8、CPython 的特化字节码)。部分求值器做通用决策;V8 做 JavaScript 特定决策(隐藏类、JS 习惯用法的内联缓存)。
  • PE 成本:预热。PE 需要画像(知道哪些 AST 路径是热的)。冷代码在解释器中运行(慢)。热代码被编译(快)。预热期(数千次迭代)比 V8 长(V8 使用带更快预热的分层编译)。

团队判断多语言价值主张(一个 VM、所有语言、跨语言优化)证明每语言性能差距合理。GraalVM 的目标用户运行多语言工作负载(一个应用中 Java + JS + Python),跨语言优化和运维简单性(一个 VM、一个 GC、一个分析器)超过 vs 专用运行时 10-20% 的每语言性能差距。

决策 3:全程图基础 IR vs 每阶段多个 IR。

Graal 从解析到代码生成使用相同的节点之海图,而非在 IR 之间转换(如 LLVM:IR → SelectionDAG → MachineInstr → MCInst)。

权衡:

  • 单 IR 优势:无翻译 bug。每次 IR 转换(LLVM 的 SelectionDAG 降级、MachineInstr 构建)都是误编译的潜在来源。一个 IR 消除这些。
  • 单 IR 优势:统一调试。bug 可以通过一个图表示追踪,而非跨四个。
  • 单 IR 成本:图必须服务所有阶段。高级优化想要抽象节点(Java 特定:NewArrayNode、CheckCastNode)。低级代码生成想要具体节点(x86 特定:LEANode、CMPXCHGNode)。图在降级时增长节点类型,成为抽象级别的混合。
  • 单 IR 成本:验证复杂性。图的不变量在降级时变化(调度前:允许漂浮节点;调度后:所有节点有控制边)。验证器必须感知阶段。

决策 4:研究优先 vs 生产优先工程。

GraalVM 源于 Oracle Labs(研究),后来产品化。IR 和编译器反映研究优先级(新颖优化、形式验证、学术发表),而非生产优先级(稳定性、向后兼容、运维简单)。

权衡:

  • 研究优势:创新。部分求值、经济调度(新颖的寄存器分配器)和 Truffle 框架是研究贡献,不会从为季度发布优化的产品团队中出现。
  • 研究成本:稳定性。GraalVM 的早期版本(2018-2020)有显著 bug 和性能回归。生产加固花了数年。
  • 研究成本:采用。企业对研究起源技术谨慎。GraalVM 的采用落后其技术能力 3-5 年。

模拟设计者思考

我是 Oracle Labs 的 Thomas Würthinger,2013 年,我在设计 Graal 的 IR。我在 HotSpot C2(现有 JIT 编译器)工作过。C2 的 IR 是节点之海,但它是 Java 特定的。节点是 Java 操作:NewObjectNode、ArrayCopyNode、MonitorEnterNode。我想要处理任何语言的编译器。IR 必须语言无关。

节点之海是正确结构。我从 C2 确信这一点。调度自由至关重要——现代 CPU 有 6 宽发射、200+ 指令重排缓冲区。在基本块内固定指令顺序的编译器浪费性能。海让调度器利用所有可用 ILP。

但如何使它多语言?Christian Wimmer 工作的洞见:部分求值。如果我有语言 X 的解释器(树遍历解释器,最简单的种类),我可以部分求值它。在特定程序上"运行"解释器符号化。解释器的分派(按节点类型 switch)成为静态分支——消除。解释器的栈管理成为死代码——消除。剩下的是程序实际操作的图。解释器被编译掉。

这意味着:我不需要每语言编译器。我需要每语言解释器(简单:5000 行)和一个编译器(Graal)。解释器是"前端"。Graal 是"后端"。节点之海是接口。解释器产生它(通过部分求值)。Graal 优化它。代码生成器线性化它。

跨语言问题。Java 调用 JavaScript。在传统系统中,这跨越 FFI 边界:序列化参数、调用 JS 引擎、反序列化结果。在 GraalVM 中,Java 方法的图和 JavaScript 函数的图在同一个 IR 中。内联将它们拼接在一起。组合图被优化:JS 函数的类型检查被消除(Java 调用者保证类型)、参数序列化被消除(值已在寄存器中)、返回值直接使用。语言边界消失。

调度问题。优化后,海有漂浮节点(无控制边)。我需要将它们分配到控制流中的位置。这是"指令调度"。经典方法:列表调度(贪心地将节点分配到最早有效位置)。但贪心调度为延迟优化,而非寄存器压力。早调度的节点可能将值保存在寄存器中许多周期,增加压力。

经济调度(我同事的贡献):将调度建模为经济问题。每个节点有"成本"(早调度的寄存器压力)和"收益"(早调度的延迟减少)。调度器最大化收益 - 成本。这产生平衡 ILP 和寄存器压力的调度。这是新颖贡献——没有其他编译器这样做。

验证挑战。节点之海有不变量:(1) 每个值节点恰好一个数据定义边(SSA),(2) 每个控制节点有从 Start 到 Return 的路径,(3) 数据边无环,(4) 循环节点有回边。这些必须在每个优化阶段后成立。我写验证器检查所有不变量。它在调试模式下每阶段后运行。在生产中禁用(太慢)。但开发期间,它捕获 90% 的误编译 bug。

赌注:一个图、所有语言、一个编译器。节点之海是通用表示。部分求值是通用前端。调度 + 寄存器分配 + 代码生成是通用后端。Java、JavaScript、Python、Ruby、R——它们都成为同一个图。图不在乎它来自什么语言。它优化计算,而非语言。这就是 GraalVM。

关键启示与行业影响

GraalVM 节点之海的更广泛含义是它演示了基于图的程序表示的普适性。每个程序、每种语言,最终都是图:值沿数据边流动、执行沿控制边进行、优化是图变换。通过使这种图显式且通用(所有语言一个 IR),GraalVM 使以前不可能的跨语言优化成为可能。调用 JavaScript 函数的 Java 方法调用 Python 脚本,被编译为一个图——语言边界在 IR 层被抹去。这种"多语言编译"能力是 GraalVM 独有的,代表编译器工程的真正进步:图不仅是一种语言中一个程序的表示——它是计算本身的表示,独立于表达它的语言。教训:当图是接口时,源语言成为实现细节。计算才是产品。

图数据库ArangoDB

ArangoDB:多模型图引擎

概述

ArangoDB 由 triAGENS GmbH(德国科隆)于 2012 年首次发布,是一个原生多模型数据库,在单一存储引擎和查询语言中将文档、图和键值数据视为平等公民。与在文档存储上附加图层(或反之)的系统不同,ArangoDB 的存储引擎(基于 RocksDB)和查询语言(AQL)从一开始就设计为原生处理所有三种模型。单个 AQL 查询可以遍历图、过滤文档、聚合键值对,无需切换引擎或数据格式。

多模型论点解决了一个实际问题:真实应用很少只使用一种数据模型。电商平台将产品存储为文档(带嵌套属性的 JSON)、关系存储为图(客户 → 购买 → 产品 → 属于 → 类别)、会话数据存储为键值对。传统上,这需要三个数据库(MongoDB + Neo4j + Redis)、三种查询语言、三个运维栈,以及保持它们同步的 ETL。ArangoDB 的主张:一个数据库、一种查询语言、一个运维负担。

ArangoDB 的图引擎不是独立层——它是文档存储之上的查询时抽象。顶点和边作为 JSON 文档存储在集合中。图遍历是跟随边文档 _from_to 字段的 AQL 操作。这种"图即文档"设计意味着每个图操作同时也是文档操作,反之亦然。

架构

ArangoDB 的架构围绕单一存储引擎统一:

存储引擎(RocksDB)。

  • 所有数据(文档、边、索引)存储在 RocksDB(嵌入式 LSM 树键值存储)中
  • 文档存储为 JSON(通过 VelocyPack 序列化,ArangoDB 的二进制 JSON 格式)
  • 每个集合是一个 RocksDB 列族
  • 边是带 _from_to 字段(顶点 ID)的文档,存储在边集合中
  • 索引:持久化(通过 RocksDB 的 B 树)、全文(倒排索引)、地理(R 树)、TTL(生存时间)

文档模型。

  • 每个实体是带唯一 _key(和派生的 _id = collection/key)的 JSON 文档
  • 文档无模式:同一集合中不同文档可以有不同字段
  • 修订:每个文档有 _rev 字段(MVCC)。更新创建新修订;读者看到一致快照。

图模型(叠加在文档上)。

  • "图"是顶点集合 + 边集合的命名集
  • 边文档:{ _from: "users/alice", _to: "users/bob", type: "friend", since: 2020 }
  • 遍历:AQL 的 FOR v, e, p IN 1..5 OUTBOUND "users/alice" GRAPH "social" 跟随 _from → _to 链接最多 5 跳
  • 遍历引擎从 RocksDB 读取边文档,跟随 _to 到下一个顶点文档,重复
  • 命名图提供模式强制:图 "social" 中的边集合必须连接声明的顶点集合中的顶点

AQL(ArangoDB 查询语言)。

  • 所有三种模型的统一声明式语言
  • 文档查询:FOR doc IN collection FILTER doc.age > 30 RETURN doc
  • 图查询:FOR v, e IN 1..3 OUTBOUND start GRAPH g RETURN v
  • 键值:RETURN DOCUMENT("collection/key")
  • 跨模型连接:单个查询可以遍历图、过滤文档、聚合值
  • 查询编译:AQL 被解析 → 优化(基于规则:过滤下推、索引使用、连接重排)→ 通过 Volcano 风格迭代器管道执行

分布式模式(集群)。

  • 分片:集合按 _key(哈希)跨 Coordinator 和 DBServer 分片
  • 协调:Coordinator 节点解析查询、将子查询分发到 DBServer、合并结果
  • 复制:每分片异步主从复制
  • 一致性:会话内写后读一致(粘性会话路由到同一 DBServer)

设计决策

决策 1:多模型(一个引擎)vs 多语言持久化(多个引擎)。

ArangoDB 将所有模型存储在一个引擎(RocksDB)中,而非集成独立引擎。

权衡:

  • 一个引擎优势:事务一致性。事务可以原子地更新文档、插入边、修改键值对。跨引擎事务(文档存储 + 图存储)需要分布式事务协议(2PC)——慢且复杂。
  • 一个引擎优势:运维简单。一次备份、一个监控栈、一条升级路径。而非三个。
  • 一个引擎成本:无模型特定优化。RocksDB(LSM 树)上的图遍历比原生图存储(Neo4j 的固定记录格式)慢。LSM 树的读放大(每键多个 SST 文件)增加每跳延迟。
  • 一个引擎成本:万金油风险。ArangoDB 的图性能无法匹敌 Neo4j。文档性能无法匹敌 MongoDB。键值性能无法匹敌 Redis。三者都"足够好",没有一个是最好。

团队判断运维和一致性收益对目标用户超过每模型性能差距:需要所有三种模型但没有团队运维三个数据库的中型应用。对大规模纯图工作负载,ArangoDB 承认 Neo4j/TigerGraph 更快。

决策 2:图即文档(边集合)vs 原生图存储。

边是 RocksDB 中的 JSON 文档,而非带物理指针的固定大小记录。

权衡:

  • 文档边优势:灵活性。边可以有任意属性(权重、时间戳、元数据),无需模式迁移。向边添加字段只是添加 JSON 键。
  • 文档边优势:统一查询。相同的 AQL 操作(FILTER、SORT、COLLECT)适用于边和顶点。无独立"边查询语言"。
  • 文档边成本:遍历延迟。跟随边需要:读边文档(RocksDB 查找)、提取 _to、读顶点文档(另一次 RocksDB 查找)。每跳两次键值查找。Neo4j:每跳一次指针跟随。
  • 文档边成本:无无索引邻接。顶点的边不物理共置。找到顶点的所有边需要 _from 上的索引查找(二级索引扫描),而非指针跟随。

团队缓解遍历延迟:(1) RocksDB 的块缓存(热边/顶点在内存中,查找是缓存命中),(2) _from 上的边索引(顶点边的 O(log N) 查找,在遍历上摊销),(3) 遍历缓存(频繁遍历的子图缓存在查询引擎中)。

决策 3:AQL(统一语言)vs 每模型 API。

所有模型一种查询语言,而非独立 API(图 API、文档 API、KV API)。

权衡:

  • 统一优势:可组合性。查询可以遍历图、然后过滤结果文档、然后聚合值——在一个语句中。无中间物化、无上下文切换。
  • 统一优势:可学习性。一种语言可学,而非三种。开发者不需要知道特定操作使用哪个模型。
  • 统一成本:表达力上限。AQL 的图遍历语法(FOR v, e IN 1..N OUTBOUND)不如 Cypher 表达力强(模式匹配、最短路径、带谓词的变长模式)。复杂图模式需要变通。
  • 统一成本:优化复杂性。查询优化器必须在一个计划中处理图操作、文档操作和聚合。搜索空间比单模型优化器大。

决策 4:RocksDB 作为存储引擎 vs 自定义存储。

ArangoDB 使用 RocksDB(Facebook 的 LSM 树),而非自定义存储引擎。

权衡:

  • RocksDB 优势:规模验证。RocksDB 以可预测性能处理 TB 级数据。ArangoDB 继承其崩溃恢复、压缩和预写日志,无需构建。
  • RocksDB 优势:写吞吐。LSM 树擅长写密集工作负载(WAL 顺序写、批量压缩)。对高插入/更新速率的应用,这是理想的。
  • RocksDB 成本:读放大。点查可能读取多个 SST 文件(每压缩层一个)。对图遍历(每跳多次点查),这增加延迟。通过块缓存和布隆过滤器缓解。
  • RocksDB 成本:压缩开销。后台压缩(合并 SST 文件)消耗 I/O 带宽和 CPU。重度压缩期间,查询延迟飙升。

模拟设计者思考

我是 Max Neunhöffer,2011 年,我在为一家初创公司构建数据库。应用有三种数据模式:用户画像(文档)、社交连接(图)、会话令牌(键值)。"正确"架构是三个数据库:画像用 MongoDB、连接用 Neo4j、会话用 Redis。但我们是三个工程师。我们运维不了三个数据库。我们无法在它们之间构建 ETL。我们无法处理三种故障模式。

如果一个数据库做所有三种呢?不是"带图插件的文档存储"(那是假装是图的文档存储)。不是"带文档存储的图数据库"(那是假装是文档存储的图数据库)。一个所有三种模型都原生的数据库。查询可以遍历图边、读取目标文档、更新键值计数器——在一个事务、一种语言、一个引擎中。

存储问题。我需要一个底层的键值存储。一切底层都是键值对:文档是 (key → JSON)、边是 (key → 带 _from/_to 的 JSON)、索引是 (key → 倒排列表)。我用 RocksDB。它经过实战检验、处理 TB 级、免费给我崩溃恢复和 WAL。我不想构建存储引擎。我想在存储引擎之上构建查询引擎。

图模型。边是文档。带 _from_to 的文档是边。这种文档的集合是边集合。图是顶点集合 + 边集合的命名集。遍历:从顶点开始,查找 _from = vertex_id 的边,跟随 _to 到下一个顶点,重复。这不是无索引邻接(Neo4j 的 O(1) 指针跟随)。这是每跳索引查找(_from 索引上的 O(log N))。较慢。但灵活性值得:边有任意属性、无模式迁移、相同查询语言适用于边和顶点。

AQL。一种语言。文档用 FOR doc IN collection FILTER ... RETURN ...。图用 FOR v, e IN 1..5 OUTBOUND start GRAPH g。键值用 RETURN DOCUMENT("key")。优化器将三者都视为集合上的操作。图遍历只是嵌套循环连接:对每个顶点,在 _from 上与边集合连接,然后在 _to 上与顶点集合连接。优化器可以将过滤推入连接、使用索引、重排操作。这是带图特定语法糖的关系代数。

性能问题。是的,图遍历比 Neo4j 慢。5 跳遍历做 10 次 RocksDB 查找(5 边 + 5 顶点),而 Neo4j 是 5 次指针跟随。但 RocksDB 的块缓存使热查找约 1μs。5 跳:约 10μs。加查询开销:总共约 1ms。对 OLTP 查询(找朋友的朋友),没问题。对 OLAP 查询(1 亿节点上的 PageRank),太慢——但 ArangoDB 不面向 OLAP 图分析。那是 TigerGraph 的领域。

多模型事务。用户注册:插入画像文档、创建到组织图的 "member_of" 边、设置会话令牌。一个事务。如果边插入失败(约束违规),画像和令牌回滚。在三数据库架构中,这是分布式事务(2PC)。在 ArangoDB 中,这是本地 RocksDB 事务。ACID。快速。简单。这是价值主张:不是"最快的图"或"最快的文档存储",而是"一个同时做两者的数据库,正确地,在一个事务中,由三个工程师运维"。

关键启示与行业影响

ArangoDB 的多模型方法验证了"足够好"策略:对于需要多种数据模式但资源有限的组织,一个运维简单、事务一致的多模型系统,比三个各自最优但运维复杂的专用系统更有价值。其"图即文档"设计表明图能力可以作为查询层抽象叠加在通用存储之上,而不需要专用图存储——这影响了后续多模型数据库(CosmosDB、OrientDB、SurrealDB)的设计。教训:数据模型的选择不仅是技术问题,也是组织和运维问题。最优的技术方案如果超出团队的运维能力,就不是最优方案。

图处理框架GraphChi

GraphChi:单机上的十亿边图处理

概述

GraphChi 由 Aapo Kyrola、Guy Blelloch 和 Carlos Guestrin 在卡内基梅隆大学发表(OSDI 2012),是一个仅使用磁盘存储在单台商用机器上运行大规模图分析(数十亿边)的图处理系统。其核心洞见——"滑动窗口"方法——证明了 Pregel/Giraph 昂贵的分布式基础设施(数百台机器、网络通信、容错)对许多图工作负载是不必要的。带快速 SSD 的单机可以在数分钟内处理 21 亿边的 Twitter 图,匹配或超过 100 台机器 Giraph 集群的性能。

GraphChi 挑战了 2010-2012 年的主流假设:"大图需要大集群"。学术界和工业界的共识是,数十亿边的图需要分布式系统(Pregel、Giraph、PowerGraph)。GraphChi 表明这是对磁盘 I/O 的想象力失败,而非根本要求。通过为顺序磁盘访问(而非随机访问)组织图数据,单机的磁盘带宽(2012 年 SSD 上 500 MB/s)足以每秒流式处理数十亿边。

该系统的影响与其简单性不成比例。它启发了 FlashGraph、X-Stream 和"单机图处理"研究方向。它也影响了生产系统:几家初创公司采用 GraphChi 风格的磁盘处理,以避免分布式集群的运维复杂性。

架构

GraphChi 的架构极其简单:

数据布局(分片)。 图的边按源顶点 ID 划分为 P 个区间(分片):

  • 分片 p 包含源顶点在范围 [p·V/P, (p+1)·V/P) 内的所有边
  • 分片内,边按目标顶点 ID 排序
  • 顶点数据(属性、当前值)存储在单独文件中,按顶点 ID 索引
  • 边数据(权重、属性)与分片文件中的边一起存储

分片大小选择为适合 RAM:如果机器有 M 字节内存,每个分片 ≤ M/3 字节(为顶点数据缓冲区和 I/O 缓冲区留出空间)。

滑动窗口算法。 图算法的每次迭代顺序处理分片:

  1. 将分片 p 的边加载到内存(从磁盘顺序读取)
  2. 加载分片 p 源顶点的顶点数据("源区间")——已从分片范围在内存中
  3. 加载分片 p 目标顶点的顶点数据(分散在整个顶点范围)——缓冲读取,通过按目标排序边来合并
  4. 对内存中的边 + 顶点执行算法的更新函数
  5. 将更新的顶点数据写回磁盘(源区间顺序写、目标顶点缓冲写)
  6. 滑动到分片 p+1

关键:分片内,所有操作都在内存数据上。磁盘 I/O 是顺序的(流式分片)+ 缓冲的(顶点数据)。无随机 I/O。无网络。无分布式协调。

GAS 分解。 GraphChi 使用 Gather-Apply-Scatter(GAS)模型(与 PowerGraph 相同):

  • Gather:对每条边,从源顶点、目标顶点和边数据计算"gather"值。在目标处累积 gather。
  • Apply:对每个顶点,应用累积的 gather 更新顶点的值。
  • Scatter:对每条边,从更新的源顶点计算"scatter"值(用于下次迭代的 gather)。

滑动窗口一次处理一个分片的 GAS 操作。目标顶点的 gather 累积可能跨多个分片(如果顶点有来自许多源区间的入边)。GraphChi 用磁盘上的"gather 累加器"文件处理:每个分片后写入部分 gather,最终 apply 读取所有部分 gather。

函数语义。 GraphChi 保证结果与内存执行相同(无磁盘处理的近似)。滑动窗口是调度优化,而非语义改变。算法看到相同的图、相同的顺序、相同的一致性保证,就像整个图在 RAM 中一样。

设计决策

决策 1:单机磁盘 vs 分布式内存。

GraphChi 用磁盘在单机上处理十亿边图,而非分布到集群。

权衡:

  • 单机优势:无分布式系统复杂性。无网络通信、无容错协议、无分区协调、无集群管理。代码是 5000 行 C++,而非 10 万行 Java(Giraph)。
  • 单机优势:成本。一台带 SSD 的机器(2012 年 2000 美元)vs 100 节点集群(20 万美元)。对研究实验室或初创公司,成本差异是决定性的。
  • 单机成本:吞吐上限。单 SSD 提供约 500 MB/s(2012 年)。100 节点集群提供约 50 GB/s 聚合。对需要多次迭代的算法(PageRank,40 次迭代),单机在原始带宽上慢 100 倍。
  • 单机成本:无容错。如果机器在计算中途崩溃,作业从头重启。无检查点、无复制。

团队的关键洞见:吞吐上限不如表面那么紧。图算法是计算密集,而非 I/O 密集。瓶颈是处理每条边的 CPU(gather、apply、scatter),而非从磁盘流式处理边。2012 年 CPU 每秒处理 10 亿边(简单操作)。500 MB/s SSD 每秒流式处理 1.25 亿边(每边 4 字节)。磁盘比 CPU 慢 8 倍。但通过预取和流水线(处理分片 p 时读取分片 p+1),磁盘延迟被隐藏。有效吞吐接近 CPU 的处理速率。

决策 2:滑动窗口(顺序 I/O)vs 随机 I/O(mmap)。

GraphChi 使用显式顺序读取(滑动窗口),而非内存映射图文件并依赖操作系统页缓存。

权衡:

  • 顺序优势:可预测 I/O。操作系统知道访问模式(顺序)并积极预取。关键路径上无页错误。
  • 顺序优势:内存控制。GraphChi 决定什么在内存中(当前分片 + 顶点缓冲区)。操作系统不基于全局 LRU 驱逐图页。
  • 顺序成本:编程复杂性。滑动窗口需要显式缓冲区管理、分片调度和 gather 累积。Mmap 是"免费"的(操作系统处理分页)。
  • 顺序成本:分片大小调优。太大:不适合内存。太小:太多分片、太多 I/O 开销(分片边界)。最优分片大小取决于图的度分布。

被拒绝的替代方案:mmap 整个图。对 21 亿边图(8GB 边数据),16GB 机器上的 mmap 可行。但当其他进程分配内存时,操作系统页缓存驱逐图页。后台 updatedb 或 cron 作业可能在计算中途导致页错误,每次错误增加毫秒级延迟。滑动窗口消除这一点:GraphChi 显式控制所有 I/O。

决策 3:GAS 模型 vs Pregel 以顶点为中心。

GraphChi 使用 GAS(Gather-Apply-Scatter,来自 PowerGraph),而非 Pregel 的消息传递。

权衡:

  • GAS 优势:关注点分离。Gather(计算边贡献)、Apply(更新顶点)、Scatter(传播到边)是独立函数。这种分解自然映射到滑动窗口:gather 是每边的(分片内处理)、apply 是每顶点的(所有分片后处理)、scatter 是每边的(分片内处理)。
  • GAS 优势:无消息存储。Pregel 需要存储消息(每超步每边一条)——可能数十亿条消息。GAS 原地累积 gather(每顶点一个累加器)——无消息缓冲区。
  • GAS 成本:不如消息传递表达力强。某些算法需要任意顶点间通信(而非仅邻居聚合)。GAS 将通信限制为 gather/scatter 模式。

决策 4:精确语义 vs 近似(采样/分区)。

GraphChi 产生精确结果(与内存执行相同),而非近似。

权衡:

  • 精确优势:正确性。无近似误差。结果可证明与单线程内存执行相同。无"在 epsilon 内收敛"的警告。
  • 精确优势:调试。如果结果错误,bug 在算法,而非系统。无"这是近似伪影还是真实 bug?"的歧义。
  • 精确成本:分片间无并行性。滑动窗口顺序处理分片(维护精确 gather 累积)。分布式系统并行处理分区(带近似合并)。
  • 精确成本:比近似替代方案慢。采样边或分区顶点的系统可以更快处理图(以准确性为代价)。

团队判断对目标用户(验证算法的研究者、需要精确结果的分析者),正确性不可协商。性能足够(十亿边图数分钟),无需牺牲精确性。

模拟设计者思考

我是 Aapo Kyrola,2011 年,我是 CMU 的博士生,与 Carlos Guestrin 合作。每个人都在构建分布式图系统。Pregel、Giraph、PowerGraph。假设:十亿边图需要集群。我持怀疑态度。我的笔记本有 512GB SSD。Twitter 图(14 亿边)以每边 4 字节是 5.6GB。它适合我的 SSD。为什么我需要 100 台机器?

反驳:随机 I/O。如果我通过迭代顶点并跟随边来处理图,访问模式是随机的(顶点的邻居分散在文件中)。SSD 上的随机 I/O:100K IOPS × 4KB = 400 MB/s。但图是 5.6GB。以 400 MB/s,一次传递 14 秒。PageRank 40 次迭代:560 秒。近 10 分钟。这……实际上没问题?100 节点 Giraph 集群 2 分钟完成。但我的笔记本 10 分钟,2000 美元 vs 20 万美元。

但随机 I/O 是错误模式。如果我按源顶点组织图(按源 ID 分片边),我可以顺序处理分片。读取顶点 0-100 万的所有边:顺序读取,500 MB/s。在内存中处理它们。写入更新的顶点值。滑动到下一个分片。唯一的非顺序 I/O 是读取目标顶点值(分散)。但如果我在分片内按目标排序边,目标读取几乎是顺序的(局部性)。

滑动窗口。处理分片 0:顺序读边、在目标处 gather(累积在缓冲区)、对源顶点 apply(内存中)、写更新值。然后分片 1。然后分片 2。磁盘看到:顺序读(分片)、缓冲读(目标顶点)、顺序写(源顶点)。无随机 I/O。SSD 以峰值带宽运行。

gather 累积问题。顶点的入边可能跨多个分片(如果其邻居有不同的源 ID)。处理分片 0 后,顶点 X 有部分 gather。分片 1 后,另一个部分。最终 apply 需要所有部分的总和。解决方案:每个分片后将部分 gather 写入磁盘。所有分片后,读取部分并 apply。这增加对顶点文件的一次额外传递。可接受。

基准。Twitter 图:14 亿边、4200 万顶点。PageRank,10 次迭代。我的笔记本(2011 年 MacBook Pro、512GB SSD、8GB RAM):6 分钟。100 节点 Giraph 集群(来自 Pregel 论文):4 分钟。我在 2000 美元笔记本上与 20 万美元集群相差 2 倍以内。论文自己就写好了。

哲学观点:"大数据需要大集群"的叙事是 I/O 工程的失败。如果你为顺序访问组织数据,单磁盘以 500 MB/s 流式处理。每边 4 字节的十亿边是 4GB。一次传递:8 秒。四十次传递:5 分钟。加 CPU 时间:总共 10 分钟。你不需要 Hadoop。你不需要集群。你需要排序文件和滑动窗口。分布式系统社区花了五年构建容错消息传递基础设施,来解决排序文件用 5000 行 C++ 解决的问题。

我不是说分布式系统无用。对不适合一个磁盘的图(1000 亿+ 边),或对实时查询(而非批处理分析),分布式是必要的。但对常见情况——研究者在网络图上运行 PageRank、初创公司在社交图上计算连通分量——带好 SSD 的单机就足够。GraphChi 是证明。图适合磁盘。算法适合内存。滑动窗口连接它们。无需集群。

关键启示与行业影响

GraphChi 的更广泛含义是它挑战了从 2004 年(MapReduce)到 2015 年(Spark、Flink)主导系统研究的"规模需要分布式"正统。通过证明带良好组织磁盘布局的单机可以在图分析上匹配 100 节点集群,GraphChi 打开了"单机图处理"研究方向(FlashGraph、X-Stream、Ligra)的大门,并影响了优先考虑运维简单性而非水平扩展的生产系统。教训不是"永不分布式"——1000 亿+ 边的图确实需要集群。教训是:在分布式之前,问问题实际上是计算密集,还是由于数据组织不善导致的 I/O 密集。排序文件和滑动窗口解决了许多集群只是蛮力解决的问题。图适合磁盘。算法适合内存。工程在于高效连接它们。分布式是最后手段,而非第一直觉。

图神经网络PyTorch Geometric (PyG)

PyTorch Geometric:作为消息传递的几何深度学习

v 聚合u1u2u3u4msgmsgmsgmsgGNN 消息传递:message → aggregate → updateGCN / GAT / GraphSAGE 均为该范式的实例

概述

PyTorch Geometric(PyG)由 Matthias Fey 和 Jan Eric Lenssen 在多特蒙德工业大学于 2019 年发布,是一个构建在 PyTorch 之上、用于不规则结构数据——图、点云、流形——深度学习的库。其设计哲学是"几何深度学习":非欧几里得领域(图、网格、群)的神经网络架构共享一种公共结构,可表达为局部邻域上的消息传递。PyG 使这一原则具体化:每个 GNN 层都是定义 message()aggregate()update() 方法的 MessagePassing 子类。

PyG 源于一个观察:GNN 研究社区(2017-2019)是碎片化的:每篇论文发布自己的 PyTorch 实现,带自定义稀疏操作、自定义批处理和自定义数据加载。复现论文意味着调试别人的 CUDA 代码。PyG 统一了这一点:公共的 MessagePassing 基类、公共的 Data 对象、带图批处理的公共 DataLoader,以及优化的稀疏内核(通过 torch-scattertorch-sparsetorch-cluster)。

到 2024 年,PyG 是学术研究中使用最广泛的 GNN 框架,有 200+ GNN 架构的实现,并与 PyTorch 生态系统(Lightning、Distributed、Compile)集成。其设计选择反映特定优先级:研究者易用性优于生产性能。新 GNN 架构应该可以用 20 行 Python 实现,即使这比手写内核损失 10-20% 吞吐。

架构

PyG 的架构分层在 PyTorch 的张量操作之上:

Data 对象。 图是 torch_geometric.data.Data 对象:

  • edge_index:(source, destination) 对的 [2, E] 张量(COO 格式)
  • x:节点特征的 [N, F] 张量
  • edge_attr:边特征的 [E, D] 张量(可选)
  • y:标签(节点级、边级或图级)
  • 附加属性:pos(几何图的节点位置)、batch(批处理图的图分配)

选择 COO(坐标列表)而非 CSR:COO 构建更简单(只需堆叠源和目标张量)、支持动态图(通过张量操作添加/删除边),并且是 PyTorch 稀疏操作期望的格式。CSR 在内部由稀疏内核用于性能,但对用户暴露为 COO。

MessagePassing 基类。 每个 GNN 层继承自 MessagePassing


class GCNConv(MessagePassing):
    def message(self, x_j, norm):  # x_j = 邻居特征
        return norm * x_j
    def aggregate(self, inputs, index):  # scatter-add
        return scatter(inputs, index, dim=0, reduce='sum')
    def update(self, aggr_out, x):  # 与自身组合
        return aggr_out + x

基类处理:

  • 传播:给定 edge_index,收集源特征(x_j)、调用 message()、调用 aggregate()、调用 update()
  • 索引操作:为高效 scatter 操作将 edge_index 转换为 CSR
  • 自动微分:所有操作都是 PyTorch 张量操作,所以梯度自动流动
  • 二部支持:源和目标可以是不同节点集(用于异构图)

稀疏内核(torch-scatter、torch-sparse)。 性能关键操作:

  • scatter(src, index, reduce):按索引聚合值(sum/mean/max)。实现为 CUDA 内核(每输出元素一个线程,sum 用原子加)。
  • spmm(adj, x):稀疏矩阵 × 密集矩阵。当消息函数是线性时使用(GCN 的归一化邻接 × 特征)。sum 聚合利用 cuSPARSE。
  • gather(x, index):按索引收集特征("消息"步骤)。简单的索引读取。

这些内核是独立包(torch-scattertorch-sparse),针对 PyTorch 的 C++ 扩展 API 编译。它们提供相对朴素 Python 循环 10-100 倍的加速。

批处理(整理)。 多个小图被批处理为一个大的不连通图:

  • 节点特征连接:[N1+F, N2+F, ...] → [(N1+N2+...), F]
  • 边索引偏移:图 k 的边移动 sum(N1..Nk-1)
  • batch 向量将每个节点分配给其图:[0,0,...,1,1,...,2,2,...]
  • 图级读出(对节点 sum/mean)使用带 batch 向量的 scatter

这种"不相交并集"批处理是图级任务(分子性质预测、图分类)的标准方法。它避免填充(浪费计算在不存在的节点上),支持可变大小图。

设计决策

决策 1:MessagePassing 基类 vs 架构特定实现。

PyG 为所有 GNN 架构提供一个基类,而非每架构独立实现。

权衡:

  • 基类优势:统一性。研究者阅读 PyG 代码时,GCN、GAT、GraphSAGE、GIN 等看到相同结构。变化在 3 个方法(message、aggregate、update),而非 500 行自定义代码。
  • 基类优势:可扩展性。新架构是新子类。无需修改框架。论文的代码就是子类。可复现性即时。
  • 基类成本:性能开销。基类每层分派到 Python 方法(message、aggregate、update)。这增加 Python 解释器开销(每次调用约 10μs)。对 100 万边的 10 层 GNN,是 100μs Python 开销——相对 CUDA 内核时间(10ms)可忽略。但对小图(100 边),开销主导。
  • 基类成本:抽象泄漏。某些架构不适合 message/aggregate/update 分解(例如,在图拉普拉斯特征值上操作的谱 GNN)。这些需要逃生通道(绕过 MessagePassing 的自定义 forward() 方法)。

团队测量:对 95% 场景(>1000 边图上的邻域聚合 GNN),Python 开销 < 总层时间的 1%。易用性收益(20 行实现)证明开销合理。

决策 2:COO 边格式(面向用户)vs CSR(内部)。

用户看到 edge_index 是 [2, E] COO 张量。内部上,内核转换为 CSR 用于性能。

权衡:

  • COO 优势:简单。构建图是 edge_index = torch.tensor([[0,1,2],[1,2,0]])。无压缩、无行指针。对研究者直观。
  • COO 优势:动态图。添加边是 torch.cat([edge_index, new_edge], dim=1)。无 CSR 重建。
  • COO 成本:内核开销。每次 MessagePassing.forward() 转换 COO → CSR(排序 + 压缩)。对 100 万边的图,约 1ms。在训练上摊销(同一图数千次前向传递),可忽略。但对每查询新图的推理,增加延迟。
  • COO 成本:内存。COO 存储 2E 个整数(源 + 目标)。CSR 存储 N+1 个行指针 + E 个列索引。对稀疏图(E >> N),COO 索引多用约 2 倍内存。

团队为用户面向 API 选择 COO,因为研究者从边列表构建图(对分子图、社交网络、引用网络自然)。CSR 转换是用户不可见的内部优化。

决策 3:PyTorch 原生 vs 自定义 CUDA 框架。

PyG 完全构建在 PyTorch 的张量操作和扩展 API 上,而非自定义 CUDA 框架。

权衡:

  • PyTorch 原生优势:免费自动微分。所有 GNN 操作都是 PyTorch 张量操作。梯度流经消息传递、聚合和更新,无需自定义反向传递。
  • PyTorch 原生优势:生态系统。PyG 与 PyTorch Lightning(训练循环)、PyTorch Distributed(多 GPU)、torch.compile(图优化)和 PyTorch 的分析器一起工作。无需自定义集成。
  • PyTorch 原生成本:性能上限。PyTorch 的稀疏操作是通用的。手写 GNN 内核(将 message + aggregate + update 融合为一个 CUDA 内核)快 20-30%。PyG 无法跨 Python 方法边界融合。
  • PyTorch 原生成本:GPU 利用率。对小图(<1 万节点),GPU 利用不足(内核启动开销主导)。PyG 不解决这个问题(这是 PyTorch 的限制)。

团队选择 PyTorch 原生,因为 GNN 研究社区生活在 PyTorch 中。强迫研究者学习自定义 CUDA 框架(或不同的 DL 库)会限制采用。vs 手写内核 20% 的性能差距对研究可接受(迭代速度比吞吐更重要)。

决策 4:独立稀疏包(torch-scatter、torch-sparse)vs 捆绑。

性能关键的稀疏内核是独立 pip 包,而非捆绑在 PyG 中。

权衡:

  • 独立优势:独立编译。CUDA 内核必须针对用户的 PyTorch 版本和 CUDA 工具包编译。分离它们允许 PyG 是纯 Python(即时安装),而内核在后台编译。
  • 独立优势:模块化。不需要稀疏操作的用户(例如,对小图使用密集邻接矩阵)不安装 CUDA 包。
  • 独立成本:安装摩擦。pip install torch-geometric 不给你工作的稀疏操作。你需要 pip install torch-scatter torch-sparse -f 。这是第一大用户抱怨。
  • 独立成本:版本兼容性。CUDA 包必须精确匹配 PyTorch 版本。PyTorch 升级可能破坏稀疏包,直到重新编译。

模拟设计者思考

我是 Matthias Fey,2018 年,我是多特蒙德工业大学的博士生。我为研究实现了 12 个 GNN 架构。每次:写消息函数、写 scatter-add、处理批处理、处理可变大小图、调试 CUDA 内核。每架构花三天。然后我读另一实验室的论文,他们的实现与我的不兼容——不同数据格式、不同批处理、不同稀疏操作。

问题是碎片化。每个 GNN 论文发布代码,但代码是一次性的。没有公共抽象。没有公共数据格式。没有公共批处理。复现论文意味着将他们的代码适配到我的基础设施。这是浪费。

几何深度学习的洞见(Bronstein、Bruna,2017):所有 GNN 都是消息传递。GCN:消息 = 归一化邻居特征,聚合 = sum。GAT:消息 = 注意力加权特征,聚合 = sum。GraphSAGE:消息 = 邻居特征,聚合 = mean/LSTM。结构相同。只有函数不同。

所以:基类。MessagePassing。你定义 message()aggregate()update()。基类处理传播(收集源特征、调用 message、scatter 到目标、调用 aggregate、调用 update)。新架构是新子类。20 行。无 CUDA。无批处理逻辑。无数据加载。只有数学。

数据格式。图是 edge_index(COO)+ x(特征)+ y(标签)。就这些。无邻接矩阵(对大图太密集)。无 CSR(对研究者构建太复杂)。COO:(src, dst) 对的 [2, E] 张量。直观。动态。PyTorch 原生。

批处理。图级任务(分类分子)需要批处理多个图。标准方法:不相交并集。连接特征、偏移边索引、添加 batch 向量。DataLoader 自动完成。研究者编写 for batch in loader:,得到批处理图。无手动整理。

稀疏内核。scatter(src, index, reduce='sum') 是核心操作。它是 CUDA 内核:每输出元素一个线程,归约用原子加。对 sum 聚合,这等价于 SpMM(稀疏矩阵 × 密集矩阵)。我可以为该情况调用 cuSPARSE(更快)。对 max 聚合或自定义归约,我需要通用 scatter 内核。

我将内核放在独立包(torch-scatter、torch-sparse)。它们需要 CUDA 编译。PyG 本身是纯 Python。即时安装 PyG,后台编译内核。是的,这是摩擦。但替代方案(在 PyG 中捆绑 CUDA 代码)意味着每次 pip install 触发 10 分钟编译。更糟。

赌注:如果新 GNN 架构是 20 行子类,研究者会采用 PyG 作为标准。论文会发布 PyG 实现。可复现性变得平凡:pip install torch-geometric,运行论文的代码。vs 手写内核 20% 的性能差距是标准化的代价。值得。社区需要公共语言。MessagePassing 就是那种语言。

关键启示与行业影响

PyG 的更广泛含义是它证明了"框架即标准"的力量:通过提供足够好的通用抽象(MessagePassing)而非追求极致性能,PyG 成为 GNN 研究的事实标准,其 API 设计反过来塑造了整个领域的研究范式。这种"易用性驱动采用、采用塑造范式"的模式在开源基础设施中反复出现(NumPy 之于科学计算、PyTorch 之于深度学习)。教训:对研究社区,标准化的价值超过性能优化——因为标准化加速的是整个领域的迭代速度,而非单个实现的运行速度。

知识图谱Microsoft Academic Graph (MAG)

微软学术图谱:行星规模的引用网络

概述

微软学术图谱(MAG)于 2015 年发布并维护至 2021 年退役,是一个异构学术知识图谱,包含 2.6 亿+ 出版物、16 亿引用边、2.5 亿作者、5000 万场馆和 65 万研究领域概念。它是最大的公开可用学术知识图谱,为 Microsoft Academic(学术搜索引擎)、Azure 认知服务以及科学计量学、推荐和 NLP 领域的数千个研究项目提供支持。

MAG 的工程挑战是规模化的实体解析:同一篇论文出现在多个数据库(DBLP、PubMed、arXiv、出版商网站)中,标题、作者姓名拼写和场馆格式各不相同。MAG 的管道将这些解析为统一实体,创建引用图,其中"Attention Is All You Need"是一个节点,无论它是通过 arXiv、NeurIPS 会议录还是 Semantic Scholar 访问。这个实体解析管道处理数十亿原始记录以产生干净的图。

图的结构是异构的:论文引用论文(引用边)、作者撰写论文(作者边)、论文属于场馆(场馆边)、论文标记研究领域(概念边)、概念形成层次结构(更宽/更窄边)。每种边类型有不同的基数、不同的更新频率和不同的查询模式——需要高效服务所有这些的存储设计。

架构

MAG 的架构是存储在分布式文件系统中的批处理图:

数据模型。 六种实体类型、七种关系类型:

  • 实体:论文、作者、场馆、机构、研究领域、期刊、会议
  • 关系:引用(论文→论文)、撰写(作者→论文)、隶属(作者→机构)、发表于(论文→场馆)、有概念(论文→研究领域)、更宽概念(领域→领域)、子场馆(场馆→场馆)

每个实体存储为制表符分隔记录:


PaperId  Rank  Doi  Title  Abstract  ...
2104587912  45  10.1145/...  Attention Is All You Need  We propose...

存储。 图存储为 Azure Blob 存储 / HDFS 中的平面文件:

  • 每种实体类型一个文件(papers.txt、authors.txt 等)
  • 每种关系类型一个文件(paper_references.txt、paper_author_affiliations.txt 等)
  • 总大小:压缩后约 350GB(2021 年快照)
  • 格式:制表符分隔值(TSV)——无数据库、无索引、无查询引擎

这种"图即平面文件"设计是刻意的:主要访问模式是批量下载 + 本地处理(研究者下载完整图,用 Spark/Python 处理)。数据库会增加查询能力但降低可移植性。

实体解析管道。 产生干净图的管道:

  1. 摄入:爬取出版商网站、arXiv、DBLP、PubMed、Crossref。各种格式的原始记录(BibTeX、JSON、XML、HTML)。
  2. 解析:从原始记录中提取结构化字段(标题、作者、场馆、年份、DOI、参考文献)。
  3. 去重:匹配代表同一论文的记录。键:DOI(精确匹配),或标题+年份+作者(通过 token 集合 Jaccard 相似度的模糊匹配)。
  4. 作者消歧:相同作者名("J. Smith")映射到多个人。通过合著图(共同发表的作者可能是同一人)和隶属机构(相同机构 + 相同领域 = 可能是同一人)消歧。
  5. 引用提取:解析参考文献列表,将引用的论文匹配到现有实体(通过标题匹配或 DOI 查找)。
  6. 概念标记:通过训练的分类器(文本特征 → 研究领域层次)将论文分类到研究领域。

管道作为 Hadoop/Spark 作业在完整语料库上运行。完整重建:每周。增量更新(新论文):每天。

查询层(Microsoft Academic)。 构建在 MAG 上的搜索引擎:

  • 索引:标题、摘要、作者名、场馆名的倒排索引(从平面文件构建)
  • 图查询:预计算(例如,"引用论文 X 的论文"是 paper_references.txt 中按 PaperId 索引的查找)
  • 排序:使用图特征(引用数、引用图上的 PageRank、作者 h 指数)的学习排序模型

设计决策

决策 1:平面文件(TSV)vs 图数据库。

MAG 作为平面 TSV 文件分发,而非可查询的图数据库。

权衡:

  • 平面文件优势:可移植性。任何有笔记本的研究者都可以下载 MAG(350GB),用 Python/Spark/R 处理。无数据库安装、无查询语言学习、无 API 密钥。
  • 平面文件优势:可复现性。特定快照(2021-05-10)是固定文件集。在该快照上计算的结果可复现。活数据库每天变化——"今天"计算的结果"明天"无法复现。
  • 平面文件成本:无查询。"找到所有引用 'Attention Is All You Need' 且 2020 年后发表的论文"需要加载 paper_references.txt(16 亿行)、过滤、与 papers.txt 连接。无索引。无查询规划器。每个查询 O(N)。
  • 平面文件成本:无更新。图是快照。新论文出现在下周转储中。无实时引用跟踪。

团队判断 MAG 的主要用户是做批量分析的研究者(科学计量学、趋势分析、推荐系统训练),而非交互式查询。对批量分析,平面文件 + Spark 比图数据库更高效(无查询开销、完全并行、无网络)。交互式查询由 Microsoft Academic(带索引的独立系统)服务,而非 MAG 直接。

决策 2:批量实体解析 vs 增量/流式。

MAG 的实体解析作为每周批处理作业运行,而非流式管道。

权衡:

  • 批量优势:全局一致性。作者消歧需要完整合著图(不看所有合著者无法消歧 "J. Smith")。批处理作业看到完整图。流式系统一次看到一篇论文。
  • 批量优势:简单。管道是 Spark 作业序列:解析 → 去重 → 消歧 → 分类。无状态管理、无窗口、无迟到处理。
  • 批量成本:延迟。周一发表的论文出现在下周重建中(最多 7 天后)。对引用跟踪服务,这太慢。
  • 批量成本:成本。每周处理 2.6 亿论文需要大型 Spark 集群(100+ 节点)。计算成本显著。

团队接受每周延迟,因为 MAG 的用户(研究者,而非新闻服务)容忍它。全局一致性收益(正确的作者消歧)超过延迟成本。增量更新(每天,仅新论文)部分缓解延迟,而不牺牲现有实体的一致性。

决策 3:概率实体匹配 vs 精确匹配(仅 DOI)。

MAG 使用模糊匹配(标题相似度、作者重叠)加上精确 DOI 匹配进行去重。

权衡:

  • 模糊优势:覆盖率。许多论文没有 DOI(预印本、研讨会论文、旧出版物)。仅 DOI 匹配会遗漏 30-40% 的语料库。模糊匹配恢复这些。
  • 模糊优势:跨源链接。arXiv 中的同一论文(无 DOI)和 NeurIPS 会议录(有 DOI)通过标题匹配链接。
  • 模糊成本:错误。标题相似度产生错误匹配(两篇标题相似的不同论文被合并)和错误不匹配(标题略有不同的同一论文不合并)。模糊匹配的错误率:约 2-5%。
  • 模糊成本:非确定性。匹配阈值(Jaccard > 0.8?)是调优参数。不同阈值产生不同图。可复现性需要固定阈值。

团队选择模糊匹配,因为对 MAG 的用例覆盖率比精确率更重要。遗漏 30% 论文(仅 DOI)的科学计量分析比包含 2-5% 错误合并(模糊)更糟。错误是随机的,在聚合统计中平均掉。

决策 4:异构图(多种边类型)vs 同构引用图。

MAG 存储所有关系类型(引用、撰写、发表于、有概念),而非仅引用图。

权衡:

  • 异构优势:更丰富的查询。"找到 2018 年后在 NeurIPS 发表的 NLP 有影响力作者"需要作者 + 场馆 + 概念边。仅引用图无法回答。
  • 异构优势:更好的特征。在 MAG 上训练的机器学习模型使用所有边类型作为特征(作者合作模式、场馆声望、概念共现)。仅引用边不够。
  • 异构成本:存储。七个关系文件 vs 一个。350GB vs 约 100GB(仅引用)。
  • 异构成本:管道复杂性。每种关系类型有自己的提取逻辑(引用解析 vs 作者名匹配 vs 场馆规范化)。管道复杂 7 倍。

模拟设计者思考

我是微软研究院的 Kuansan Wang,2014 年,我们在构建 Microsoft Academic 与 Google Scholar 竞争。基础是图:每篇论文、每个作者、每条引用、每个场馆。没有图,我们只是搜索引擎。有了图,我们可以回答"谁是 Transformer 研究的新星?"和"BERT 的学术谱系是什么?"

数据问题。论文无处不在。arXiv、DBLP、PubMed、出版商网站、机构知识库。同一篇论文以五种不同的元数据格式出现在五个地方。"Attention Is All You Need"在 arXiv 上(无 DOI,作者为 "Vaswani, A. et al.")、在 NeurIPS 会议录中(有 DOI,作者为 "Ashish Vaswani, Noam Shazeer, ...")、在 Semantic Scholar 上(又一种格式)。如果我不将这些解析为一个实体,我的引用图对一篇论文有三个节点。图是错的。

实体解析是核心工程挑战。DOI 是简单情况:精确匹配,完成。但 40% 的论文没有 DOI。对那些:标题匹配。分词标题,计算 Jaccard 相似度。"attention is all you need" vs "attention is all you need!"——Jaccard 0.83。阈值 0.8。匹配。但 "attention is all you need" vs "attention is all we need"——Jaccard 0.75。不匹配。正确(不同论文)。阈值是调优旋钮。太高:漏匹配。太低:错误合并。

作者消歧更难。"Wei Wang" 在数据库、医学、地理学发表。三个不同的人。如何分离?合著:数据库的 Wei Wang 与 Jiawei Han 合著。医学的 Wei Wang 与医学研究者合著。构建合著图,找连通分量,将每个分量分配给一个人。这是应用于作者图的图算法(社区发现)。图解析实体。

存储。要放到图数据库吗?Neo4j?图有 2.6 亿节点和 16 亿边。Neo4j 可以处理(企业版,128GB RAM)。但我的用户不想查询 Neo4j。他们想下载图并运行 Spark 作业。他们想要可以 grepawk 并加载到 pandas 的 TSV 文件。数据库是障碍。平面文件是邀请。

所以:TSV 文件。papers.txt(2.6 亿行)。paper_references.txt(16 亿行)。authors.txt(2.5 亿行)。总计:压缩后 350GB。从 Azure Blob 存储下载。用你想要的处理。Python、Spark、R、Julia。无 API 密钥。无查询语言。无速率限制。图是你的。

管道。每周完整重建。Spark 集群,100 节点。解析 → 去重 → 消歧 → 分类 → 输出 TSV。端到端 48 小时。每日增量:仅新论文(每天 5 万),与现有实体匹配,追加到文件。每周重建确保全局一致性(作者消歧需要完整图)。每日更新确保新鲜度(新论文 24 小时内出现)。

退役。2021 年,微软关闭 Microsoft Academic。图捐赠给社区(MAKG——微软学术知识图谱)。平面文件继续存在。研究者仍在使用它们。图比产品活得长。这是开放数据可移植格式的价值。数据库会随服务消亡。TSV 文件是不朽的。

关键启示与行业影响

MAG 的更广泛含义是它演示了"图作为开放科学基础设施"的模式。通过将整个学术图谱作为平面文件免费发布,MAG 催生了数千个研究项目、数十个衍生数据集(Semantic Scholar 的早期训练数据、OAG、AMiner)和整个学术图谱研究领域。其"实体解析优先"的架构表明,知识图谱的质量瓶颈不在存储或查询,而在上游的数据清洗和实体统一——这个教训被后续所有大规模知识图谱(OpenAlex、Lens、Crossref 的图谱)继承。教训:对科学基础设施,开放性和可移植性比查询能力更有价值——因为研究者需要的不是 API,而是可以完全控制的数据。

生产图算法Google Maps 路由引擎

Google Maps 路由:世界最大的路由图

本地集散主干高速主干收缩层次:双向搜索在高层相遇结构每周重建 · 权重每 5 分钟定制 · 查询 O(√N)

概述

Google Maps 的路由引擎每天为超过十亿用户计算驾车、步行、骑行和公交路线,覆盖估计 1 亿+ 节点和 2.5 亿+ 边(仅驾车图)的全球道路网络图。系统必须在 200 毫秒内回答"从 A 到 B 的最快路线是什么?",针对地球上任意一对点,同时考虑实时交通、道路封闭、转弯限制、收费和用户偏好(避开高速、避开轮渡)。它可以说是人类历史上使用最重的图算法部署。

路由问题是在加权有向图上的最短路径问题,但规模和延迟要求使朴素 Dijkstra 不可行(1 亿节点上 O(N log N) = 秒级,而非毫秒级)。Google 的解决方案结合多种图算法技术:收缩层次(CH)用于快速点到点查询、可定制路线规划(CRP)用于交通感知权重更新,以及利用道路网络地理局部性的多层次图分区方案。

工程挑战不仅是算法性的——更是运维性的。道路图每天变化(新道路、施工、封闭)。交通每 5 分钟变化。系统必须合并这些变化而不从头重建路由索引(需要数小时)。图结构(稳定,每周重建)与图权重(动态,每 5 分钟更新)的分离是关键架构决策。

架构

Google 的路由系统(从出版物和专利推断;未完全公开):

道路图。 有向加权图:

  • 节点:路口、路段端点和"形状点"(曲线近似)
  • 边:带属性的路段(限速、道路等级、车道数、转弯限制、收费标志、时间依赖速度画像)
  • 权重:遍历成本 = f(距离、速度、转弯惩罚、交通乘数、用户偏好)
  • 多图:驾车、步行、骑行、公交(各有不同边集和权重函数)

图构建。 从原始地理数据:

  1. 摄入道路数据(政府数据集、卫星图像、用户贡献、街景)
  2. 拓扑提取:识别路口、在路口处拆分道路、创建有向边
  3. 属性分配:限速(来自数据或从道路等级推断)、转弯限制(来自街景中的标志检测)、单行标志
  4. 验证:连通性检查(无不可达子图)、几何检查(边匹配道路几何)

收缩层次(CH)。 核心查询加速:

  • 预处理:按重要性迭代收缩节点(先本地街道、后高速),添加快捷边
  • 层次结构约 10 层:住宅 → 集散道 → 主干道 → 高速 → 汽车道
  • 查询:层次结构上的双向 Dijkstra,探索 O(√N) 节点
  • 预处理时间:集群上数小时(每周完整重建)
  • 查询时间:图遍历 1-5ms(交通定制前)

可定制路线规划(CRP)。 交通感知路由,无需重收缩:

  • 图被划分为单元(地理区域,每个约 1 万节点)
  • 每个单元内,预计算本地 CH(结构稳定)
  • 单元间,"边界图"连接单元边界
  • 交通更新改变单元内边权重 → 重算本地 CH 权重(每单元秒级,跨单元并行化)
  • 查询:源单元内本地 CH → 边界图 → 目标单元内本地 CH
  • 权重定制:每 5 分钟,新交通速度传播到单元级权重

实时交通。 权重更新管道:

  1. Android 设备的 GPS 探针(匿名、聚合)→ 每路段速度样本
  2. 流式聚合(Flux/MapReduce):计算每段当前速度,每 2-5 分钟更新
  3. 速度 → 权重:权重 = 自由流通行时间 × (自由流速度 / 当前速度)。拥堵路段(速度 = 自由流的 20%)权重 = 5× 正常。
  4. 权重传播:更新单元级 CH 权重。对 1 万边的单元,<1 秒。
  5. 查询时:路由引擎读取最新权重。无索引重建。

分布式服务。 路由图从分布式系统服务:

  • 图按地理划分为分片(每个分片覆盖一个区域)
  • 跨分片边界的查询被分解:源分片内路由 → 跨边界路由 → 目标分片内路由
  • 缓存:热门路线(机场 → 市中心)被缓存。缓存命中率:约 30%。
  • 复制:每个分片跨数据中心复制以保证可用性。

设计决策

决策 1:收缩层次 vs 带地标的 A* vs 枢纽标签。

Google 使用 CH(及其变体),而非替代加速技术:

  • CH 优势:可证明的查询时间。最坏情况 O(√N),与距离无关。1km 路线和 1000km 路线耗时相似。带地标的 A* 对长距离退化(地标边界变松)。
  • CH 优势:权重定制。交通改变权重,而非结构。CH 的层次结构稳定。只有快捷边权重需要更新(线性时间)。枢纽标签需要在权重变化时完全重算(数小时)。
  • CH 成本:预处理。1 亿节点的完整收缩耗时数小时。添加新道路需要重收缩受影响区域。通过 CRP 的基于单元方法缓解(只有受影响单元重收缩)。
  • CH 成本:内存。快捷边大约使边数翻倍。2.5 亿边 × 2 × 16 字节 = 8GB。适合 RAM,但增加压力。

被拒绝的替代方案:枢纽标签(HL)。HL 提供最快查询(O(1) 查找:路线预计算并存储为标签)。但 HL 标签被任何权重变化失效。交通每 5 分钟更新,HL 需要每 5 分钟重算标签——不可行(数小时计算)。CH 的权重定制属性对交通感知路由是决定性的。

决策 2:基于单元的分区(CRP)vs 全局 CH。

Google 将图划分为地理单元,而非维护一个全局 CH。

权衡:

  • 单元优势:增量更新。交通变化影响一个单元。只有该单元的本地 CH 权重重算(秒级)。全局 CH 需要更新受变化边影响的所有快捷边(可能数百万)。
  • 单元优势:并行性。单元独立。权重定制跨单元并行运行(数千单元、数千核心)。
  • 单元成本:查询复杂性。跨单元查询需要:源单元本地路由 + 边界图路由 + 目标单元本地路由。边界图增加 vs 单一全局 CH 的开销。
  • 单元成本:分区质量。坏分区(一条高速跨多个单元边界)增加边界图大小和查询时间。地理分区(网格或 METIS)必须与道路结构对齐。

团队选择单元,因为运维要求(5 分钟交通更新)与全局 CH(更新需数小时)不兼容。查询开销(边界图)增加 1-2ms——在 200ms SLA 内可接受。

决策 3:分离结构与权重 vs 统一索引。

图的结构(拓扑、层次、单元)每周重建。权重(交通速度、成本)每 5 分钟更新。

权衡:

  • 分离优势:更新速度。权重更新是 O(边)——线性过程。结构更新(收缩)是 O(N log N)——数小时。分离它们意味着常见情况(交通变化)快。
  • 分离优势:正确性。结构变化(新道路)罕见且需要验证(新道路连通吗?转弯限制正确吗?)。权重变化(交通)频繁且低风险(错误速度导致次优路线,而非崩溃)。不同更新节奏匹配不同风险画像。
  • 分离成本:陈旧。新开通的道路在下次每周结构重建前不可路由。通过重大基础设施变化的加急重建缓解。
  • 分离成本:不一致。结构重建期间(数小时),旧结构用新权重服务查询。施工中的道路可能在权重层有 weight=∞(封闭),但结构层仍存在。查询引擎必须处理(跳过 weight=∞ 的边)。

决策 4:服务器端计算 vs 设备端路由。

Google Maps 在服务器端计算路线(设备端缓存用于离线):

  • 服务器优势:实时交通。服务器有完整交通图景(数百万 GPS 探针)。设备端需要将交通数据流式传输到每个手机——对完整图不切实际。
  • 服务器优势:图新鲜度。道路变化、新施工和封闭在服务器端立即反映。设备端需要地图下载(每周/每月)。
  • 服务器成本:延迟。到服务器的往返增加 50-100ms。设备端即时。
  • 服务器成本:可用性。无信号 = 无路线(除非下载离线地图)。Google 用最近路线的设备端缓存和可下载的离线区域缓解。

团队选择服务器端,因为交通感知路由是 Google Maps 的核心差异化。无交通的路线(设备端)通常比交通感知路线(服务器端)慢 30-50%。50ms 延迟与避开拥堵节省的 15 分钟相比不可见。

模拟设计者思考

我是 Google 的路由工程师,2010 年,我们每天服务 5 亿方向请求。路由引擎是 Google Maps 中延迟最敏感的服务。用户期望 <200ms 得到答案。图是全球的:1 亿节点、2.5 亿边。Dijkstra 出局(每查询秒级)。A 出局(对长路线仍太慢)。我们需要亚线性查询时间。*

收缩层次。Robert Geisberger 2008 年论文的想法:预计算层次结构。移除不重要的节点(死胡同、住宅街道),添加快捷边。层次结构有层级:底部住宅、顶部汽车道。查询从源"向上"、从目标"向上",在高阶节点相遇。探索 O(√N) 节点。对 1 亿节点:约 10,000 节点被探索。每节点 100ns:1ms。加开销:总共 5ms。远在 SLA 内。

但交通。CH 假设静态权重。交通每 5 分钟变化。我们每 5 分钟重收缩吗?不——我们的图上收缩耗时 6 小时。微软研究院的洞见(Delling、Pajor、Werneck 2013):可定制路线规划。分离结构与权重。层次结构(哪些节点被收缩、哪些快捷边存在)依赖拓扑,而非权重。交通改变权重。所以:收缩一次(每周)。持续定制权重(每 5 分钟)。

定制:对收缩 v 创建的每个快捷边 (u,w),shortcut_weight = min(direct(u,w), weight(u,v) + weight(v,w))。当交通改变 weight(u,v),重算快捷边。这是每单元 O(快捷边数)。有基于单元的分区(1 万单元,每个约 1 万快捷边),定制是 1 万 × 1 万 = 1 亿操作。跨 1000 核心并行化:0.1 秒。完成。

单元分区。将地图划分为地理单元(大约 50km × 50km)。每个单元约 1 万节点。单元内,本地 CH。单元间,边界图(单元边界上的节点、跨边界的边)。跨国查询:源单元本地 CH → 边界图(边界节点上的小 CH)→ 目标单元本地 CH。边界图约 100 万节点(单元边界)。其 CH 全局预计算。查询时间:5ms(本地)+ 2ms(边界)+ 5ms(本地)= 12ms。

交通摄入。数百万 Android 手机每几秒报告 GPS 位置(匿名、聚合)。流式管道计算每路段速度:连续探针的距离 / 时间,在 5 分钟窗口聚合。速度图是 2.5 亿值的向量(每边一个)。每 5 分钟,新速度向量传播到权重定制层。路由引擎读取最新权重。无索引重建。无重启。你 8:05 得到的路线反映 8:04 的交通。

规模。每天 5 亿查询 = 平均每秒 5,800 查询。峰值(周五下午 5 点):每秒 20,000 查询。每查询:12ms 图计算 + 50ms I/O(读取图分片、网络)。从数千台机器的集群服务,每台在 RAM 中持有完整图的副本(驾车图 8GB)。地理负载均衡(东京的查询命中东京数据中心)。缓存命中率:30%(热门路线)。有效计算:14,000 查询/秒 × 12ms = 168 CPU 秒/秒。几百个核心。算法便宜。基础设施昂贵。

图是活的。每天:10,000 个新路段(来自施工、用户编辑、卫星检测)。每 5 分钟:2.5 亿权重更新(交通)。每小时:1,000 个道路封闭(事故、活动)。路由引擎必须吸收所有这些,无重启、无查询失败、无延迟飙升。结构(每周)与权重(5 分钟)的分离使这成为可能。图持续变化。索引持续适应。用户 200ms 看到路线。他们看不到图。他们不需要。他们只是开车。

关键启示与行业影响

Google Maps 路由的更广泛含义是它确立了"层次化预处理 + 实时定制"作为大规模路由系统的标准架构。结构与权量的分离、基于地理单元的分区、边界图的多级组合——这些技术被后续所有路由系统继承(Uber、滴滴、HERE、TomTom)。其运维教训同样深刻:一个服务十亿用户的图系统,其工程挑战的 90% 不在算法,而在数据管道(道路提取、交通融合、变更验证)和分布式服务(分片、复制、缓存)。教训:对生产规模的图系统,算法是入场券,数据管道和运维架构才是护城河。

图数据库Apache JanusGraph

Apache JanusGraph:可插拔的分布式图数据库

概述

JanusGraph 于 2017 年作为 Linux 基金会项目启动(从 Titan 分支而来,Titan 最初由 Aurelius 开发,2015 年被 DataStax 收购),是一个具有激进可插拔架构的分布式图数据库。其定义性的工程决策是将图查询引擎与存储后端分离:JanusGraph 的遍历和索引逻辑运行在任何分布式键值存储(Apache Cassandra、Apache HBase、BerkeleyDB、Google Cloud Bigtable、Amazon DynamoDB)之上。这种"自带存储"设计意味着已经为其他工作负载运行 Cassandra 的组织可以添加图能力,而无需引入新的存储系统。

JanusGraph 面向企业图分析市场:跨金融交易网络的欺诈检测、制药研究的知识图谱构建、网络和 IT 基础设施管理、推荐引擎。其最佳位置是拥有 1 亿到 1000 亿边、需要分布式存储(超出单机容量)但不需要内存图数据库亚毫秒延迟的图。

该系统的架构反映了特定的哲学承诺:图查询引擎是创新,而非存储引擎。存储是已解决的问题(Cassandra、HBase、Bigtable 都提供分布式、容错的键值存储)。难题是在分布式存储上高效图遍历——最小化每遍历跳的键值查找次数、批量邻接读取、缓存热子图。JanusGraph 将工程重点放在这一层。

架构

JanusGraph 的架构是分层的:

存储后端(可插拔)。 存储适配器接口:

  • StoreManager:打开/关闭连接、管理事务
  • KeyColumnValueStore:核心抽象——(key, column) → value 的有序映射
  • 图数据编码为此 KV 模型:

- 顶点邻接:key = vertex_id,column = edge_sort_key(编码方向、标签、排序键),value = edge_properties

- 顶点属性:key = vertex_id,column = property_key,value = property_value

- 索引条目:key = index_key(例如 "name:Alice"),column = vertex_id,value = 空

关键洞见:顶点的邻接表是一个键下排序的列范围。"获取顶点 42 的所有出向 KNOWS 边"是单次范围扫描:key=42,列在 [OUT_KNOWS_MIN, OUT_KNOWS_MAX] 内。这高效映射到 Cassandra 的分区模型(每顶点一个分区,列按边类型排序)。

查询引擎。 构建在 Apache TinkerPop(Gremlin)之上:

  • Gremlin 遍历被编译为 JanusGraphStep 操作序列
  • 每个步骤翻译为一个或多个 KV 存储操作(点查、范围扫描、索引查询)
  • 查询优化器应用策略:过滤下推(将 has() 谓词推入索引查找)、限制下推(K 个结果后停止扫描)、批量预取(一次范围扫描读取顶点的所有边,而非每边一次查找)

索引。 两种索引类型:

  • 复合索引:属性值的精确匹配。存储在 KV 存储中作为 (index_key → vertex_id) 条目。快速(O(1) 查找),但限于等值谓词。
  • 混合索引:全文、地理空间、范围查询。由外部索引引擎(Elasticsearch、Apache Solr、Apache Lucene)支持。索引引擎处理查询;JanusGraph 获取匹配的顶点 ID 并从 KV 存储加载其数据。

事务层。 通过存储后端的 ACID 事务:

  • Cassandra:单分区写入的轻量级事务(LWT),跨分区的最终一致性
  • HBase:行级原子性(单行写入原子,多行需要协处理器)
  • BerkeleyDB:完整 ACID(单机,用于开发/测试)

缓存。 多级:

  • 顶点缓存:最近访问的顶点及其邻接表(LRU,可配置大小)
  • 索引缓存:最近使用的索引查询结果
  • 模式缓存:图模式(顶点标签、边标签、属性键)——很少变化,激进缓存

设计决策

决策 1:可插拔存储 vs 专用存储引擎。

JanusGraph 运行在现有分布式 KV 存储上,而非构建自己的。

权衡:

  • 可插拔优势:运维复用。为时序数据运行 Cassandra 的组织可以添加 JanusGraph 而无需新存储集群。运维知识(监控、备份、扩展、故障处理)直接迁移。
  • 可插拔优势:存储选择匹配工作负载。Cassandra 用于写密集(物联网传感器图)、HBase 用于读密集(知识图谱)、Bigtable 用于 Google Cloud 原生。图引擎相同;存储适应。
  • 可插拔成本:性能上限。专用图存储(Neo4j)使用无索引邻接(每跳 O(1) 指针跟随)。JanusGraph 的 KV 模型每跳需要范围扫描(列索引上 O(log N))。对深遍历(10+ 跳),累积延迟高 5-10 倍。
  • 可插拔成本:阻抗失配。图操作(遍历、聚合、路径查找)必须编码为 KV 操作(get、scan、put)。某些操作笨拙:"计数所有类型 X 的边"需要扫描所有顶点的邻接表(默认无全局边索引)。

团队判断运维收益(复用现有基础设施)对目标用户超过性能成本:已有 Cassandra/HBase 部署、想添加图能力而不需要新存储系统的企业。对需要最大遍历速度的纯图工作负载,JanusGraph 承认 Neo4j/TigerGraph 更快。

决策 2:TinkerPop/Gremlin vs 自定义查询语言。

JanusGraph 使用 Apache TinkerPop 的 Gremlin 作为查询语言。

权衡:

  • TinkerPop 优势:生态系统。Gremlin 驱动存在于 Java、Python、JavaScript、.NET、Go。工具(Gremlin Console、Gremlin Python)由 TinkerPop 社区维护。JanusGraph 免费继承这个生态系统。
  • TinkerPop 优势:可移植性。针对 TinkerPop 编写的应用可以切换后端(JanusGraph → Neptune → CosmosDB)而无需代码更改。这减少厂商锁定。
  • TinkerPop 成本:优化限制。TinkerPop 的策略系统是通用的。JanusGraph 特定优化(存储感知查询规划、KV 特定批处理)必须表达为 TinkerPop 策略,这约束其表达力。
  • TinkerPop 成本:性能开销。TinkerPop 的遍历机(基于 Java、每 traverser 对象)增加 vs 原生查询引擎的开销。对高吞吐 OLTP(每秒 1 万+ 查询),这种开销可测量。

决策 3:以顶点为中心的邻接模型 vs 以边为中心的存储。

JanusGraph 将顶点的所有边存储为该顶点键下的列(以顶点为中心),而非将边存储为独立实体(以边为中心)。

权衡:

  • 以顶点为中心优势:遍历效率。"获取顶点 V 的所有邻居"是单次范围扫描(一次 KV 操作)。以边为中心的模型需要索引查找以按端点找边,然后为每条边获取——多次操作。
  • 以顶点为中心优势:局部性。顶点的整个邻接表在一个分区(Cassandra)或一个区域(HBase)中。读取它是一次网络往返。
  • 以顶点为中心成本:超级节点问题。有 1000 万边的顶点(社交图中的名人)在一个分区中有 1000 万列。Cassandra 分区有推荐大小限制(约 100MB)。超级节点超过这个,导致读延迟飙升和压缩压力。
  • 以顶点为中心成本:边查询困难。"找到时间戳 T 后创建的所有边"需要扫描所有顶点(无全局边索引)。以边为中心的存储有自然的时间排序索引。

团队用边 TTL(生存时间:旧边过期)和边分页(使用列范围边界一次读取 K 条边)缓解超级节点问题。对无超级节点的图(大多数企业工作负载:欺诈、知识图谱、基础设施),以顶点为中心的模型是最优的。

决策 4:最终一致性(Cassandra 默认)vs 强一致性。

JanusGraph 在 Cassandra 上默认最终一致性(QUORUM 读/写,而非 ALL)。

权衡:

  • 最终优势:可用性。Cassandra 节点故障不阻塞读/写(只要仲裁可达)。对服务推荐的图数据库,短暂陈旧读取(看到 2 秒前的友谊)可接受。
  • 最终优势:延迟。QUORUM 读/写比 ALL 快(不等待最慢副本)。
  • 最终成本:异常。对顶点 A 邻接表的写入在读取顶点 B 的邻接表时可能不可见(如果它们在不同分区且写入未传播)。遍历 A→B 可能遗漏最近添加的边。
  • 最终成本:应用复杂性。需要一致性的应用(金融交易)必须实现自己的验证(写后读、幂等键)。

团队选择最终一致性作为默认,因为 JanusGraph 的目标工作负载(分析、推荐、知识图谱)容忍秒级陈旧。需要强一致性的应用可以配置 SERIAL 一致性(Cassandra LWT),以延迟为代价。

模拟设计者思考

我是 JanusGraph 项目的首席工程师,2017 年,我们在 DataStax 收购后社区不确定时从 Titan 分支。问题:Titan 的核心价值是什么,我们如何在使项目可持续的同时保留它?

Titan 的洞见是存储抽象。图引擎(遍历、索引、事务)与存储后端(Cassandra、HBase、BerkeleyDB)分离。这意味着:如果你已经运行 Cassandra,你免费获得图数据库。无新存储集群。无新运维负担。图引擎是与现有存储对话的库。

KV 编码是关键。图不是键值存储。但图可以编码为一个。每个顶点是一个键。每条边是该键下的列。列名编码边的方向、标签和排序键。列值编码边的属性。遍历("跟随所有出向 KNOWS 边")是列上的范围扫描。这种编码不优雅。这不是无索引邻接。但它在任何分布式 KV 存储上工作。而"在任何分布式 KV 存储上工作"就是价值主张。

超级节点问题困扰我们。有 1000 万边的顶点(Twitter 名人、热门产品)在一个 Cassandra 分区中有 1000 万列。Cassandra 的分区大小限制约 100MB。1000 万边 × 50 字节 = 500MB。我们超限。这个分区的读取慢(Cassandra 必须扫描大分区)。压缩昂贵(合并 500MB 分区)。图引擎无法修复这个——这是存储限制。

缓解:边 TTL(过期旧边——社交图不需要 2010 年的边)、边分页(使用列边界一次读取 1000 条边)、顶点切分(将超级节点拆分为多个"虚拟顶点",每个有部分边)。没有一个是完美的。根本张力:以顶点为中心的存储对正常顶点(度数 < 1000)最优,对超级节点(度数 > 100 万)病态。生产系统必须处理两者。

TinkerPop 决策。我们用 Gremlin,因为社区维护它。我们没有工程带宽构建和维护查询语言、5 种语言的驱动、控制台和可视化工具。TinkerPop 给我们所有这些。成本:TinkerPop 的遍历机基于 Java、对象重、未针对我们的 KV 访问模式优化。原生引擎快 2-3 倍。但 2-3 倍快在 50ms 的查询上给你 20ms。这值得 3 工程师年的语言设计、驱动开发和社区建设吗?不。我们用 Gremlin 发布。

一致性模型。Cassandra 默认最终一致。对图数据库,这意味着:我添加边 A→B。我立即从 A 遍历。边可能还不在(写入未传播到服务我读取的副本)。对社交网络,这没问题(新友谊 2 秒延迟不可见)。对金融交易图,不行(欺诈检测查询必须看到最新交易)。我们默认最终(快速、可用),让应用按事务选择强一致(慢、一致)。图引擎不判断。应用决定。

JanusGraph 不是最快的图数据库。不是最可扩展的。对已经运行 Cassandra 或 HBase 的组织,它是运维最方便的。图是你已有基础设施之上的层。这是推销。对企业市场(银行、制药、电信),"无新基础设施"是比"10 倍快遍历"更有说服力的论点。图服务组织,而非基准。

关键启示与行业影响

JanusGraph 的可插拔存储架构验证了"图作为查询层"范式:图引擎是库,而非数据库。这影响了后续系统(Amazon Neptune 的多模型方法、Azure CosmosDB 的图 API),将图查询视为众多访问模式之一,由通用分布式存储服务。教训:对已有基础设施的企业,图引擎的价值在查询语义(遍历、模式匹配、路径查找),而非存储机制。

超级节点问题在以顶点为中心的图数据库中仍未解决。每个按顶点存储邻接表的系统(JanusGraph、Cassandra 支持的图、DynamoDB 图模式)都面临同一堵墙:有数百万边的顶点创建热分区。解决方案(TTL、分页、顶点切分)是缓解,而非修复。局部性(所有边在一个地方)与平衡(无分区过大)之间的根本张力是数据倾斜问题的图特定实例,没有通用解决方案。教训:选择存储模型前了解你的度分布。

JanusGraph 的社区治理(Linux 基金会、多厂商)证明开源图数据库需要中立的管理。Titan 的命运(被 DataStax 收购、开发降优先级)表明厂商控制的开源是脆弱的。Linux 基金会模式(多贡献者、共享治理)提供可持续性。教训:对基础设施项目,治理结构与代码质量同样重要。

TinkerPop 集成证明语言生态系统是分发渠道。JanusGraph 的采用不是由其存储性能驱动,而是由 Gremlin 在 Python、Java、JavaScript 中的可用性驱动。开发者选择 JanusGraph 是因为他们可以用自己的语言编写 Gremlin,而非因为他们评估了存储后端。教训:在面向开发者的基础设施中,API 和语言支持是产品。存储引擎是实现细节。

社交网络图Twitter 社交图 (Manhattan)

Twitter 社交图:大规模的实时扇出

推文粉丝时间线粉丝时间线粉丝时间线粉丝时间线写时扇出:一条推文 → N 个时间线缓存粉丝 >1 万改用读时扇出 · Kafka 异步传播变更

概述

Twitter 的社交图——4.5 亿+ 账户之间定向的"关注"关系——是其时间线、推荐和通知系统的基础。与 Facebook 的双向友谊图不同,Twitter 的图是不对称的:Alice 可以关注 Bob 而 Bob 不关注 Alice。这种不对称性创造了根本不同的工程挑战:当拥有 1 亿粉丝的用户发布推文时,系统必须将该推文投递到 1 亿个时间线。这种"扇出"问题——沿大规模有向图的边实时传播内容——是 Twitter 定义性的图工程挑战。

Twitter 的社交图基础设施经历了三代演进:MySQL(2006-2010)、自定义分布式图服务(2010-2014)、Manhattan(2014-至今),一个内部构建的分布式、实时、多租户数据库。Manhattan 存储社交图(谁关注谁)、推文图(谁发了什么)和互动图(谁点赞/转推了什么)——全部作为邻接表存储在针对时间线构建特定访问模式优化的分布式键值存储中。

工程挑战不在存储(图适合分布式 KV 存储),而在传播:当创建边(新关注)或发布内容(新推文)时,效果必须在几秒内传播通过图。时间线是图的物化视图:"我关注的人的推文,按时间排序。"在 4.5 亿用户间实时维护这个视图是持续执行的图遍历问题。

架构

Twitter 的图基础设施以 Manhattan 为中心:

Manhattan(图存储)。 分布式、多租户键值存储:

  • 数据模型:(dataset, key, column) → value。社交图存储为:dataset="follows",key=follower_id,column=followee_id,value=timestamp。
  • 存储:带可配置一致性的 LSM 树(类似 Cassandra)(写强一致、读最终一致)
  • 分区:按键(用户 ID)。每个用户的关注列表和粉丝列表在一个分区中。
  • 复制:跨数据中心 3 倍。写入是仲裁(2/3)。读取是本地的(最近副本)。
  • 吞吐:每秒数百万读、每秒数十万写。

社交图服务。 Manhattan 之上的服务层:

  • getFollowing(user_id):(dataset="follows", key=user_id) 上的范围扫描 → followee ID 列表
  • getFollowers(user_id):(dataset="followers", key=user_id) 上的范围扫描 → follower ID 列表
  • follow(alice, bob):写入 alice 的 "follows" 分区和 bob 的 "followers" 分区(两次写入,最终一致)
  • isFollowing(alice, bob):(dataset="follows", key=alice, column=bob) 上的点查

时间线构建(扇出问题)。 两种策略:

  • 写时扇出(正常用户):当 Alice 发推文,系统读取 Alice 的粉丝列表(从 Manhattan),将推文 ID 写入每个粉丝的"时间线"缓存(Redis)。粉丝的时间线是预物化的。读取时间线是缓存读取(O(1))。
  • 读时扇出(名人):当拥有 1 亿粉丝的用户发推文,写时扇出需要 1 亿次写入。相反,推文存储一次,粉丝的时间线在读取时构建,通过合并:(1) 来自正常关注的预物化推文,(2) 来自名人关注的实时获取。读取更昂贵(合并 K 个名人时间线),但写入是 O(1)。

阈值(写时扇出 vs 读时扇出)是粉丝数:粉丝 >1 万的用户使用读时扇出。这种混合方法平衡写成本(与粉丝数成正比)和读成本(与名人关注数成正比)。

图变更(关注/取消关注)。 当 Alice 关注 Bob:

  1. 写入 Manhattan:alice 的 "follows" 列表 += bob,bob 的 "followers" 列表 += alice
  2. 向 Kafka 发出事件:"alice 关注了 bob"
  3. 消费者:时间线服务(将 bob 的未来推文添加到 alice 的时间线)、推荐服务(更新"推荐关注"模型)、通知服务(通知 bob)、搜索索引(更新社交图索引)
  4. 时间线服务执行"回填":获取 bob 的最近推文并插入 alice 的时间线缓存(使 alice 立即看到 bob 的最近内容)

推荐图。 除关注图外,Twitter 维护派生图:

  • "推荐关注"图:离线计算(Spark),从关注图 + 互动图。使用用户-推文二部图上的协同过滤。
  • "网内"vs"网外"分类:关注图定义"网内"(关注者的推文)。其他一切都是"网外"(算法推荐)。边界是图查询:"推文作者是否在读者的关注集中?"

设计决策

决策 1:写时扇出 vs 读时扇出(混合)。

Twitter 使用两种策略,按粉丝数选择。

权衡:

  • 写时扇出优势:读延迟。时间线是预物化的。读取是缓存查找(O(1),<5ms)。对 99% 粉丝 <1 万的用户,这是最优的。
  • 写时扇出成本:写放大。拥有 1 万粉丝的用户每条推文产生 1 万次写入。每天 5 亿推文,是每天 5 万亿次写入。写基础设施必须处理这个。
  • 读时扇出优势:写效率。名人推文写一次。无扇出。写成本 O(1),与粉丝数无关。
  • 读时扇出成本:读延迟。读者的时间线必须在读取时合并名人时间线。如果读者关注 50 个名人,是 50 次获取 + 合并。延迟:20-50ms(vs 预物化的 5ms)。

混合(阈值 1 万粉丝)平衡总系统成本。纯写时扇出需要每名人推文 1 亿次写入(不可行)。纯读时扇出需要每个用户在读取时合并数百个时间线(太慢)。阈值最小化用户分布上的写成本 + 读成本总和。

决策 2:Manhattan(自定义存储)vs Cassandra / MySQL。

Twitter 构建 Manhattan,而非使用 Cassandra 或 MySQL 用于社交图。

权衡:

  • 自定义优势:多租户。Manhattan 服务社交图、推文索引、互动图和私信存储——全部在一个系统中,带每数据集配置(一致性、TTL、复制)。Cassandra 需要每工作负载独立集群。
  • 自定义优势:Twitter 特定优化。Manhattan 的 LSM 树针对 Twitter 的访问模式调优(小值、高读写比、时间排序列)。通用 Cassandra 携带 Twitter 不使用的功能开销(CQL、物化视图、UDF)。
  • 自定义成本:工程投资。Manhattan 需要专门团队(20+ 工程师)构建和维护。Cassandra 是"免费"的(开源、社区维护)。
  • 自定义成本:生态系统。无第三方工具、无社区知识、无 Stack Overflow 答案。运维知识仅内部。

团队判断在 Twitter 的规模(4.5 亿用户、每秒数百万操作),专用存储 20% 的效率增益证明工程投资合理。多租户收益(一个系统、多工作负载)减少运维复杂性,足以抵消开发成本。

决策 3:有向图(关注)vs 双向图(友谊)。

Twitter 的图是有向的:Alice 关注 Bob ≠ Bob 关注 Alice。这是有深刻工程含义的产品决策。

权衡:

  • 有向优势:不对称关系。记者关注 500 个消息源。名人被 1 亿粉丝关注。图自然捕获这一点。双向模型需要"待处理"状态或隐式互惠。
  • 有向成本:扇出不对称。名人推文扇出到 1 亿时间线。正常用户推文扇出到 500 个。系统必须处理 20 万倍的扇出因子范围。这是核心工程挑战。
  • 有向成本:图查询更难。"互相关注"(Alice 关注 Bob 且 Bob 关注 Alice)需要两次查找。"推荐关注"(朋友的朋友)需要从用户关注者遍历出边——在有 1 亿度超级节点的图上 2 跳遍历。

团队接受扇出挑战,因为有向模型就是产品。Twitter 就是不对称关注。工程存在以服务产品模型,而非简化图。

决策 4:事件驱动传播(Kafka)vs 同步更新。

图变更通过 Kafka 异步传播。

权衡:

  • 异步优势:写延迟。"关注"在 <50ms 返回(一次 Manhattan 写入)。同步传播到 5 个下游服务增加 200ms。
  • 异步优势:故障隔离。如果推荐服务宕机,关注仍然工作。Kafka 保留事件;消费者赶上。
  • 异步成本:最终一致性。Alice 关注 Bob。2-3 秒内,Alice 的时间线不显示 Bob 的推文(时间线服务尚未处理事件)。Alice 可能认为关注没生效。
  • 异步成本:顺序。如果 Alice 关注 Bob 然后立即取消关注,事件必须按序处理。Kafka 按用户 ID 分区保证每用户顺序。

模拟设计者思考

我是 Twitter 存储团队的技术工程师,2013 年,我们在设计 Manhattan。社交图在 MySQL 中。它正在崩溃。3 亿用户,每个平均 200 个关注。这是 600 亿边。MySQL 无法处理读量:每次时间线渲染读取用户的关注列表(200 个 ID),然后从每个获取推文。每天 5 亿时间线渲染,是每天 1000 亿次读取。MySQL CPU 80%,我们每季度增长 20%。

访问模式简单:"给我这个用户关注的 ID 列表。"这是范围扫描。"添加 ID 到这个用户的关注列表。"这是追加。"这个 ID 是否在这个用户的关注列表中?"这是点查。三个操作。分布式 KV 存储处理所有三个。我们不需要图数据库。我们需要快速、分布式、排序的 KV 存储,带强写入和快速读取。

Manhattan。LSM 树存储(写优化:追加便宜)。按用户 ID 分区(用户的所有数据在一个分区)。排序列(关注列表按时间排序:最新关注在前)。三副本(仲裁写、本地读)。这是 Cassandra 的架构,针对我们的工作负载调优。为什么不直接用 Cassandra?多租户。我们想要一个系统用于社交图、推文索引、互动图和私信。每个数据集有不同一致性要求、不同 TTL、不同复制因子。Cassandra 的多租户(键空间)笨拙。我们构建自己的。

扇出问题。当 @elonmusk 发推文,1.3 亿时间线需要更新。写时扇出:1.3 亿次写入 Redis(时间线缓存)。以每秒 100 万写入(Redis 集群容量),是 130 秒。两分钟。不可接受。推文必须在 5 秒内出现在时间线中。

解决方案:不扇出名人。推文存储一次。读取时,时间线服务合并:(1) 来自正常关注的预物化推文(从 Redis),(2) 来自名人关注的推文实时获取(从推文存储)。读者关注 200 个正常用户 + 50 个名人。时间线合并读取 1 个 Redis 键(预物化)+ 50 个推文存储键(名人时间线)。51 次读取。每次 1ms:51ms。可接受。

阈值。谁是"名人"?粉丝数 > 1 万。低于 1 万:写时扇出(足够便宜:每推文 1 万次写入,在推文速率上摊销)。高于 1 万:读时扇出(写太贵、读足够便宜)。阈值调优以最小化总系统成本:写基础设施成本 + 读延迟成本。在 1 万,边际写成本等于边际读成本。以下:写更便宜。以上:读更便宜。

图是产品。Twitter 的价值是不对称关注图。工程存在以使这个图快速:读取快(时间线)、写入快(关注/取消关注)、传播快(新推文 → 时间线)。每个架构决策服务图的访问模式。Manhattan 存储图。Kafka 传播变更。Redis 物化视图。图不是抽象。它是产品。产品必须在 50 毫秒内工作。

关键启示与行业影响

Twitter 的扇出架构证明大规模的图传播需要混合策略,而非单一方法。写时扇出 vs 读时扇出的阈值(1 万粉丝)是通用原则的特定实例:当图的度分布是幂律的(少数超级节点、多数正常节点),系统必须不同地处理两种机制。一刀切不适合所有。这一教训适用于任何有异质度分布的图系统:社交网络、引用图、电商推荐图。

Manhattan 架构验证了"为特定访问模式专用存储"的方法。团队不是强迫通用数据库(MySQL、Cassandra)服务 Twitter 的工作负载,而是构建针对所需确切操作(排序范围扫描、点查、追加)优化的存储。20% 的效率增益在 Twitter 的规模上证明工程投资合理。教训:在足够规模下,专用系统的运维节省超过其开发成本。低于该规模,使用商品系统。

事件驱动的变更传播(Kafka)成为实时系统中图变更的标准模式。洞见:图变更(关注、点赞、转推)是事件,而非状态变化。将它们建模为事件(追加日志、有序、可重放)给你:可审计性(重放日志重建任何过去状态)、容错(消费者在故障后赶上)、解耦(生产者不等待消费者)。教训:将图变更建模为事件,而非状态转换。事件日志是真相;物化视图是派生的。

Twitter 的有向图模型(不对称关注)证明产品的关系语义必须驱动图工程,而非相反。双向模型会简化扇出问题(无超级节点),但会错误表示产品(关注不是友谊)。工程适应产品,而非反之。教训:图模型是有工程后果的产品决策。选择真实表示产品的模型,然后为其后果做工程。

研究来源

共收集 1084 个唯一来源(以下列出前 200 个):

  1. Asking for help on the storage engine of Neo4j
    https://community.neo4j.com/t/asking-for-help-on-the-storage-engine-of-neo4j/59903
  2. Database internals and transactional behavior - Operations ...
    https://neo4j.com/docs/operations-manual/current/database-internals/
  3. What is the internal architecture of graph databases such ...
    https://www.quora.com/What-is-the-internal-architecture-of-graph-databases-such-as-Titan-or-Neo4j
  4. Neo4j storage internals - Gaurav Sarma - Medium
    https://gauravsarma1992.medium.com/neo4j-storage-internals-be8d150028db
  5. How Neo4j stores data internally?
    https://stackoverflow.com/questions/24366078/how-neo4j-stores-data-internally
  6. Neo4j graph database realizes efficient storage performance ...
    https://journals.plos.org/plosone/article?id
  7. Neo4j Performance Architecture Explained & 6 Tuning Tips
    https://graphable.ai/blog/neo4j-performance/
  8. Neo4j · Delft Students on Software Architecture - delftswa
    https://delftswa.gitbooks.io/desosa2016/content/neo4j/chapter.html
  9. What is Neo4j Architecture? Can anyone explain ...
    https://www.quora.com/What-is-Neo4j-Architecture-Can-anyone-explain-the-Neo4J-Architecture-with-a-diagram
  10. NODES 2024 - Block Format: The Next Generation Graph ...
    https://www.youtube.com/watch?v=KLhRJEe50V4
  11. Neo4j Internals
    https://maxdemarzi.com/2012/08/13/neo4j-internals/
  12. Neo4j System Properties
    https://db-engines.com/en/system/Neo4j
  13. Optimal way to design for neo4j from the start
    https://community.neo4j.com/t/optimal-way-to-design-for-neo4j-from-the-start/32225
  14. Unlocking the Power of Neo4j: A Comprehensive Guide to ...
    https://byteridge.com/industry-insights/unlocking-the-power-of-neo4j-a-comprehensive-guide-to-graph-databases/
  15. 6. Neo4j Internals - Neo4j High Performance [Book]
    https://www.oreilly.com/library/view/neo4j-high-performance/9781783555154/ch06.html
  16. Understanding Neo4j Database
    https://www.linkedin.com/pulse/understanding-neo4j-database-mahesh-prasad-jb80c
  17. An overview of Neo4j Internals | PDF
    https://www.slideshare.net/slideshow/an-overview-of-neo4j-internals/13009803
  18. Amazon Neptune - Managed Graph Database
    https://aws.amazon.com/neptune/
  19. What Is Amazon Neptune? - Amazon ...
    https://docs.aws.amazon.com/neptune/latest/userguide/intro.html
  20. Graph Database introduction, deep-dive and demo with ...
    https://www.youtube.com/watch?v=Y6jbFC8tvVw
  21. Amazon Neptune DB: A Beginner's Guide to Graph ...
    https://medium.com/@mohammedanes008/amazon-neptune-db-a-beginners-guide-to-graph-databases-in-the-cloud-dd34afb42c3b
  22. Build multi-tenant architectures on Amazon Neptune
    https://aws.amazon.com/blogs/database/build-multi-tenant-architectures-on-amazon-neptune/
  23. Architectural differences between Neptune and Neo4j
    https://docs.aws.amazon.com/neptune/latest/userguide/migration-architectural-differences.html
  24. Neptune Serverless: Scalable Graph Database
    https://aws.amazon.com/video/watch/7a4bc62b07f/
  25. AWS Neptune vs Neo4j: Which Graph DB is Better?
    https://www.puppygraph.com/blog/aws-neptune-vs-neo4j
  26. AWS Neptune - Overview, Features, Use Cases & Many More
    https://k21academy.com/aws-cloud/aws-neptune/
  27. Amazon Neptune: Leveraging Graph Databases for ...
    https://curatepartners.com/tech-skills-tools-platforms/amazon-neptune-leveraging-graph-databases-for-modern-business-solutions/
  28. Exploring AWS Neptune - Medium
    https://tam-iii.medium.com/exploring-aws-neptune-9925c175a945
  29. Deep dive on Amazon Neptune - awsstatic.com
    https://d1.awsstatic.com/events/reinvent/2019/Deep_dive_on_Amazon_Neptune_DAT361.pdf
  30. Graph Database introduction, deep-dive and demo with ...
    https://pages.awscloud.com/rs/112-TZM-766/images/2022_VW_s46e01-DAT_Slide-Deck.pdf
  31. Managed Graph Database – Amazon Neptune Features
    https://aws.amazon.com/neptune/features/
  32. AWS re:Invent 2024 - Deep dive into Amazon Neptune and its ...
    https://www.youtube.com/watch?v=hha8rzO0dA4
  33. Amazon Neptune: A Look into AWS's Graph Database
    https://www.datacamp.com/tutorial/amazon-neptune
  34. Knowledge Graph Semantic Layer Architecture on ...
    https://builder.aws.com/content/2y6e1lj1gW72aPu3TxcshdcIEgG/knowledge-graph-semantic-layer-architecture-on-aws-using-neptune-db
  35. Knowledge Graphs in Amazon Neptune - AWS
    https://aws.amazon.com/video/watch/6af2e3091e5/
  36. Amazon Neptune – Fast, reliable graph database built for ...
    https://news.ycombinator.com/item?id=15808379
  37. Internal Architecture :: TigerGraph DB
    https://www.tigergraph.com/docs/tigergraph-server/4.2/intro/internal-architecture
  38. TigerGraph: AI-Powered Graph Database for Real-Time ...
    https://www.tigergraph.com/
  39. TigerGraph vs Dgraph : Key Differences Explained
    https://www.puppygraph.com/blog/tigergraph-vs-dgraph
  40. TigerGraph: High-Speed Graph Analytics for Real-Time ...
    https://www.youtube.com/watch?v=7OSunyi7brg
  41. Native Graph Database Engine
    https://www.tigergraph.com/tigergraph-db/
  42. TigerGraph Overview
    https://medium.com/dataness-ai/tigergraph-overview-50c949272a5d
  43. Introducing TigerGraph, a Native Parallel Graph Database
    https://thenewstack.io/introducing-tigergraph-native-parallel-graph-database/
  44. Parallel Graph Processing
    https://www.tigergraph.com/glossary/parallel-graph-processing/
  45. TigerGraph: The parallel graph database explained
    https://www.infoworld.com/article/2268730/tigergraph-the-parallel-graph-database-explained.html
  46. Native Parallel Graphs
    https://www.tigergraph.com/wp-content/uploads/2018/09/Native-Parallel-Graphs-The-Next-Generation-of-Graph-Database-for-Real-Time-Deep-Link-Analytics.pdf
  47. TigerGraph Architecture Overview: Why It's 100x Faster Than ...
    https://www.youtube.com/watch?v=3JDqwRGb1K8
  48. TigerGraph: Massively Parallel Graph Database with Deep ...
    https://intellyx.com/2017/09/22/tigergraph-massively-parallel-graph-database-with-deep-link-analytics/
  49. TigerGraph Database
    https://www.intechsolutions.com.au/tigergraph-database/
  50. (PDF) TigerGraph: A Native MPP Graph Database
    https://www.researchgate.net/publication/330617565_TigerGraph_A_Native_MPP_Graph_Database
  51. TigerGraph vs JanusGraph: Key Differences & Comparison
    https://www.puppygraph.com/blog/tigergraph-vs-janusgraph
  52. Introducing TigerGraph
    https://cdn2.hubspot.net/hubfs/4114546/Collateral/TigerGraph%20Real%20Time%20Graph%20Analytics.pdf
  53. Real-Time Graph Analytics
    https://www.tigergraph.com/glossary/real-time-data-analytics/
  54. TigerGraph Creates Graph Database History By Securing ...
    http://www.dbtalks.com/news/tigergraph-creates-graph-database-history-by-securing-31-million-in-investment
  55. Graph-Powered Analytics and Machine Learning with ...
    https://www.oreilly.com/library/view/graph-powered-analytics-and/9781098106645/
  56. JanusGraph
    https://janusgraph.org/
  57. What is an example of a distributed graph database?
    https://milvus.io/ai-quick-reference/what-is-an-example-of-a-distributed-graph-database
  58. Unlock the power of distributed graph databases with ...
    https://devblogs.microsoft.com/cosmosdb/janusgraph-azure-cassandra-graph-databases/
  59. Designing Large-Scale Graph Data Platforms with ...
    https://medium.com/@firmanbrilian/designing-large-scale-graph-data-platforms-with-janusgraph-and-distributed-storage-backends-3eec3f22a29b
  60. JanusGraph vs Dgraph: Which Graph Database Fits Your ...
    https://www.puppygraph.com/blog/janusgraph-vs-dgraph
  61. The JanusGraph FoundationDB Storage Adapter - Ted ...
    https://www.youtube.com/watch?v=rQM_ZPZy8Ck
  62. JanusGraph: An Introduction to the Distributed Graph ...
    https://statusneo.com/janusgraph-an-introduction-to-the-distributed-graph-database/
  63. JanusGraph: an open-source, distributed graph database
    https://github.com/JanusGraph/janusgraph
  64. Things to Consider in a Multi-Node JanusGraph Cluster
    https://docs.janusgraph.org/v0.4/basics/multi-node/
  65. Experimental Evaluation of Graph Databases: JanusGraph ...
    https://www.mdpi.com/2076-3417/13/9/5770
  66. Database Deep Dives: JanusGraph
    https://www.ibm.com/think/insights/database-deep-dives-janusgraph
  67. JanusGraph IDE & Graph Visualization Tool
    https://gdotv.com/janusgraph-visualization-tool/
  68. Janusgraph vs Neo4j : Key Differences & Comparison
    https://www.puppygraph.com/blog/janusgraph-vs-neo4j
  69. JanusGraph Managed Services and Visualization Solutions
    https://www.xenonstack.com/managed-services/janusgraph/
  70. Community-Driven Graphs with JanusGraph | PDF
    https://www.slideshare.net/slideshow/communitydriven-graphs-with-janusgraph-82388397/82388397
  71. JanusGraph
    https://hackolade.com/help/JanusGraph.html
  72. Why choosing between Neo4j, Dgraph, and JanusGraph is ...
    https://www.linkedin.com/pulse/why-choosing-between-neo4j-dgraph-janusgraph-really-architecture-liu-6z2pe
  73. JanusGraph: an open-source, distributed graph database
    https://www.reddit.com/r/Database/comments/ofrt8a/janusgraph_an_opensource_distributed_graph/
  74. Distributed, open source, scalable graph database
    https://news.ycombinator.com/item?id=27762109
  75. Introducing Dgraph: A Go-based Graph Database with ...
    https://www.linkedin.com/posts/golangcafe_github-hypermodeincdgraph-high-performance-activity-7378380685520166913-uoF_
  76. Why you should build your next app with a graph database
    https://discuss.dgraph.io/t/why-you-should-build-your-next-app-with-a-graph-database-dgraph-blog/7616
  77. DGraph, a graph database written in Go : r/golang
    https://www.reddit.com/r/golang/comments/hbk8ep/dgraph_a_graph_database_written_in_go/
  78. Dgraph: The Graph Database written in Go
    https://www.youtube.com/watch?v=CjkKRbtwWXA
  79. Dgraph, what is a graph database anyway?
    https://medium.com/@JalalOkbi/dgraph-what-is-a-graph-database-anyway-8b6c22fb1eeb
  80. Dgraph vs Amazon Neptune: Key Differences & Comparison
    https://www.puppygraph.com/blog/dgraph-vs-neptune
  81. Building a Native GraphQL Database
    https://discuss.dgraph.io/t/building-a-native-graphql-database-challenges-learnings-and-future-dgraph-blog/5308
  82. Documentation feedback - Discuss Dgraph
    https://discuss.dgraph.io/t/documentation-feedback/2862
  83. DGraph
    https://dbdb.io/db/dgraph/revisions/13
  84. dgraph-io/dgraph: high-performance graph database for ...
    https://github.com/dgraph-io/dgraph
  85. Thanks for writing this up! I worked with the Knowledge ...
    https://news.ycombinator.com/item?id=19178260
  86. Dgraph: Native GraphQL Database with Manish Jain
    https://softwareengineeringdaily.com/2021/01/19/dgraph-native-graphql-database-with-manish-jain/
  87. Dgraph, Microservices, Time series, Scalability and CI/CD
    https://discuss.dgraph.io/t/dgraph-microservices-time-series-scalability-and-ci-cd/8993
  88. Graph Databases, GraphQL and Dgraph [Tutorial]
    https://hackernoon.com/graph-databases-graphql-and-dgraph-tutorial-uz1i3u49
  89. Dgraph: the Graph Database written in Go
    https://speakerdeck.com/campoy/dgraph-the-graph-database-written-in-go
  90. Top 10 Open Source Graph Databases in 2025
    https://www.geeksforgeeks.org/blogs/open-source-graph-databases/
  91. Graph database
    https://en.wikipedia.org/wiki/Graph_database
  92. Exploring Dgraph, GraphQL and Go | Workshop | Go Systems ...
    https://www.youtube.com/watch?v=RkcgZko3Ppc
  93. Dgraph Database Semantics
    https://www.ardanlabs.com/blog/2020/06/dgraph-database-semantics.html
  94. Pregel: a system for large-scale graph processing
    https://15799.courses.cs.cmu.edu/fall2013/static/papers/p135-malewicz.pdf
  95. Pregel: a system for large-scale graph processing
    https://research.google/pubs/pregel-a-system-for-large-scale-graph-processing/
  96. Pregel: a system for large-scale graph processing
    https://dl.acm.org/doi/10.1145/1807167.1807184
  97. The Evolution of Graph Processing: From Pregel to ...
    https://medium.com/@pur4v/the-evolution-of-graph-processing-from-pregel-to-langgraph-6e8c2063df98
  98. Pregel: A System for Large Scale Graph Processing
    https://jshun.csail.mit.edu/6827-s22/lectures/lecture11-1.pdf
  99. How does Google Pregel work? | nuric Blog
    https://www.doc.ic.ac.uk/~nuric/posts/sysadmin/how-does-google-pregel-work/
  100. Pregel: A System for Large-Scale Graph Processing
    https://stephenholiday.com/notes/pregel/
  101. Large-scale graphs with Google(TM) Pregel by MICHAEL ...
    https://www.youtube.com/watch?v=yX_zJyNbpac
  102. Pregel: A System for Large-Scale Graph Processing
    https://podcasts.apple.com/gb/podcast/pregel-a-system-for-large-scale-graph-processing/id1776500414?i=1000676278262
  103. Pregel: A system for large-scale graph processing
    https://www.researchgate.net/publication/221257383_Pregel_A_system_for_large-scale_graph_processing
  104. Pregel: A System for Large-Scale Graph Processing
    https://blog.acolyer.org/2015/05/26/pregel-a-system-for-large-scale-graph-processing/
  105. igrigorik/pregel: Single-node implementation of Google's ...
    https://github.com/igrigorik/pregel
  106. Pregel: A System For Large Scale Graph Processing | PDF
    https://www.slideshare.net/slideshow/pregel-35504069/35504069
  107. Pregel: A System for Large-Scale Graph Processing.
    https://cs.uwaterloo.ca/~kmsalem/courses/cs743/F14/slides/Pregel.pdf
  108. Pregel Algorithms for Graph Connectivity Problems with ...
    http://www.vldb.org/pvldb/vol7/p1821-yan.pdf
  109. A quick introduction to Google's Pregel graph processing ...
    https://medium.com/@AdityaChatterjee/googles-pregel-graph-processing-system-90341156848a
  110. Overview of Pregel for Graph Processing | PDF
    https://www.scribd.com/doc/136640646/PREGEL-pptx
  111. From Pregel to LangGraph — The Complete Story
    https://colinmcnamara.com/blog/langgraph-conceptual-study-guide/
  112. Pregel: A System for Large- Scale Graph Processing
    https://cse.hkust.edu.hk/~dimitris/6311/L13-Pregel-Ke.pdf
  113. What is Apache Giraph?
    https://www.dremio.com/wiki/apache-giraph/
  114. SPFC: An Effective Optimization for Vertex-Centric Graph ...
    https://www.computer.org/csdl/journal/su/2019/01/08168278/13rRUypp566
  115. An Empirical Analysis on Expressibility of Vertex Centric ...
    https://www.cs.bgsu.edu/arijitk/Papers/VCExpress.pdf
  116. From "Think Like a Vertex" to "Think Like a Graph"
    http://www.vldb.org/pvldb/vol7/p193-tian.pdf
  117. Tutorial: Run Giraph Applications on GraphScope
    https://graphscope.io/docs/analytical_engine/tutorial_run_giraph_apps
  118. Multi-Task Processing in Vertex-Centric Graph Systems
    https://openproceedings.org/2023/conf/edbt/paper-176.pdf
  119. Vertexica: your relational friend for graph analytics!
    https://dspace.mit.edu/bitstreams/6c0ef7d0-5f8e-44ee-9be4-64c06de5e217/download
  120. Vertex-centric graph processing
    https://www.waitingforcode.com/graphs/vertex-centric-graph-processing/read
  121. Apache Giraph
    http://www.cse.cuhk.edu.hk/~ericlo/teaching/bigdata/lab/9-Giraph/
  122. Practical Graph Analytics with Apache Giraph
    https://pdfs.semanticscholar.org/37ce/2c6182e270581607db07dfbfb7c9caaf7706.pdf
  123. GoFFish: A Sub-Graph Centric Framework for Large-Scale ...
    https://ui.adsabs.harvard.edu/abs/2013arXiv1311.5949S/abstract
  124. Apache Giraph
    http://disa.fi.muni.cz/wp-content/uploads/ApacheGiraph.pdf
  125. Graph Computing Frameworks Comparison (Giraph vs ...
    https://nebula-graph.io/posts/benchmarking-graph-computing-frameworks
  126. Scaling Apache Giraph to a trillion edges - Engineering at Meta
    https://engineering.fb.com/2013/08/14/core-infra/scaling-apache-giraph-to-a-trillion-edges/
  127. A Beginner's Guide to Apache Giraph: Processing Large-scale ...
    https://malliktalksjava.com/2023/09/16/a-beginners-guide-to-apache-giraph-processing-large-scale-graph-data/
  128. To Think Like a Vertex (or Not) for Distributed Training ...
    https://ieeexplore.ieee.org/document/10181148/
  129. L6:Distributed Graph Processing
    http://cds.iisc.ac.in/wp-content/uploads/DS256.2018.L7.Pregel.pdf
  130. Extending Giraph with a Graph-Centric Programming API
    https://issues.apache.org/jira/browse/GIRAPH-818
  131. Scalable Collaborative Filtering on top of Apache Giraph
    https://www.youtube.com/watch?v=bHVFOvNHH0I
  132. GraphX - Spark 4.2.0 Documentation
    https://spark.apache.org/docs/latest/graphx-programming-guide.html
  133. GraphX: A Resilient Distributed Graph System on Spark
    https://event.cwi.nl/grades2013/02-xin.pdf
  134. Simplifying Graph-Parallel Computation in Apache Spark ...
    https://ieeexplore.ieee.org/document/10800958/
  135. GraphX: Unifying Data-Parallel and Graph-Parallel Analytics
    https://www.istc-cc.cmu.edu/publications/papers/2014/graphx.pdf
  136. Apache Spark Graph Analytics: From GraphX to ...
    https://www.puppygraph.com/blog/spark-graph
  137. GraphX: Graph Processing in a Distributed Dataflow ...
    https://amplab.cs.berkeley.edu/wp-content/uploads/2014/09/graphx.pdf
  138. GraphX: a resilient distributed graph system on Spark
    https://dl.acm.org/doi/10.1145/2484425.2484427
  139. The GraphX Graph Processing System
    https://people.eecs.berkeley.edu/~kubitron/courses/cs262a-F13/projects/reports/project21_report_ver2.pdf
  140. GraphX: A Resilient Distributed Graph System on Spark
    https://www.istc-cc.cmu.edu/publications/papers/2013/grades-graphx_with_fonts.pdf
  141. Spark GraphX- World's Leading Graph Analytics Engine
    https://www.ksolves.com/blog/big-data/spark/spark-graphx-worlds-leading-graph-analytics-engine
  142. [HELP] Implementing Graph Algorithm Using GraphX
    https://www.reddit.com/r/apachespark/comments/4m9lmm/help_implementing_graph_algorithm_using_graphx/
  143. Practical Apache Spark GraphX in 10 minutes
    https://www.youtube.com/watch?v=ilLt5tSZSU8
  144. Advanced Graph Algorithms in Spark Using GraphX ... - Site Title
    https://ingaredblog.wordpress.com/2016/09/20/advanced-graph-algorithms-in-spark-using-graphx-aggregated-messages-and-collective-communication-techniques/
  145. Neo4j or GraphX / Giraph what to choose?
    https://stackoverflow.com/questions/28609125/neo4j-or-graphx-giraph-what-to-choose
  146. What is Spark GraphX? Everything You Need To Know
    https://www.simplilearn.com/spark-graphx-article
  147. Is there an API for implementing graphs in Spark
    https://www.edureka.co/community/34828/is-there-an-api-for-implementing-graphs-in-spark
  148. Apache Spark WTF??? — Living Without You
    https://medium.com/towards-data-engineering/apache-spark-wtf-living-without-you-7b09ec017fcc
  149. Pregel, GraphLab, and GraphX
    https://id2221kth.github.io/slides/2022/10_graph_processing.pdf
  150. GraphX - Apache Spark
    https://spark.apache.org/graphx/
  151. PowerGraph: Distributed Graph-Parallel Computation on ...
    https://www.usenix.org/system/files/conference/osdi12/osdi12-final-167.pdf
  152. PowerGraph: Distributed Graph-Parallel Computation on ...
    https://blog.acolyer.org/2015/05/29/powergraph-distributed-graph-parallel-computation-on-natural-graphs/
  153. Distributed Graph-Parallel Computation on Natural Graphs
    https://stephenholiday.com/notes/powergraph/
  154. PowerGraph: Distributed Graph-Parallel Computation on ...
    https://jshun.csail.mit.edu/6886-s21/lectures/lecture10-2.pdf
  155. PowerGraph: Distributed graph-parallel computation on ...
    https://www.researchgate.net/publication/262392280_PowerGraph_Distributed_graph-parallel_computation_on_natural_graphs
  156. 28. PowerGraph Distributed Graph-Parallel Computation ...
    https://github.com/Guangtong/ThreePapersPerWeek_OnBigData/blob/master/695-28.%20PowerGraph%20Distributed%20Graph-Parallel%20Computation%20on%20Natural%20Graphs.md
  157. PowerGraph: Distributed Graph-Parallel Computation on ...
    https://www.cl.cam.ac.uk/~ey204/teaching/ACS/R244_2022_2023/presentation/S3/POWERGRAPH_Zejian.pdf
  158. PowerGraph: Optimizing Graph Computation | PDF
    https://www.scribd.com/document/735832982/PowerGraph-Distributed-Graph-Parallel-Computation-on-Natural-Graphs
  159. PowerGraph: Distributed Graph-Parallel Computation on ...
    http://alex-ii.github.io/notes/2018/09/16/powergraph_graph_parallel_computation.html
  160. PowerGraph: Distributed Graph-Parallel Computation on ...
    https://www.usenix.org/conference/osdi12/technical-sessions/presentation/gonzalez
  161. Distributed large-scale graph processing on FPGAs - PMC
    https://pmc.ncbi.nlm.nih.gov/articles/PMC10239738/
  162. Fan Jun Bo
    https://mobitec.ie.cuhk.edu.hk/ierg4330Spring2017/ESTR4316/powergraph.pdf
  163. PowerGraph | PDF
    https://www.slideshare.net/slideshow/powergraph/49684138
  164. arXiv:1705.05595v5 [cs.DC] 7 Aug 2017
    https://arxiv.org/pdf/1705.05595
  165. PowerGraph: Distributed Graph-Parallel Computation on ...
    https://jshun.csail.mit.edu/6827-s22/lectures/lecture11-2.pdf
  166. PowerGraph
    https://cs.uwaterloo.ca/~ssalihog/courses/slides/Lingyun-PowerGraph.pdf
  167. S-PowerGraph: Streaming Graph Partitioning for Natural ...
    https://ui.adsabs.harvard.edu/abs/2015arXiv151102586X/abstract
  168. PowerGraph: Distributed Graph-Parallel Computation on ...
    https://www.cl.cam.ac.uk/~ey204/teaching/ACS/R244_2021_2022/presentation/S3/POWERGRAPH_Samuel.pdf
  169. PowerGraph: A framework for large-scale machine ...
    https://github.com/jegonzal/PowerGraph
  170. GraphScope: A One-Stop Large-Scale Graph Computing ...
    https://github.com/alibaba/GraphScope
  171. GraphScope: A Unified Engine For Big Graph Processing
    http://vldb.org/pvldb/vol14/p2879-qian.pdf
  172. GraphScope Flex: LEGO-like Graph Computing Stack
    https://arxiv.org/html/2312.12107v1
  173. GraphScope
    https://graphscope.io/
  174. [Feedback] Initial experience with GraphScope #1466
    https://github.com/alibaba/GraphScope/discussions/1466
  175. GraphScope: A One-Stop Large-Scale Graph Computing System
    https://www.reddit.com/r/Python/comments/laug08/graphscope_a_onestop_largescale_graph_computing/
  176. GraphScope: a one-stop large graph processing system
    https://dl.acm.org/doi/10.14778/3476311.3476324
  177. GraphScope: A One-Stop Large-Scale Graph Computing ...
    https://news.ycombinator.com/item?id=26000243
  178. GraphScope: A One-Stop Large-Scale Graph Computing ...
    https://graphscope.netlify.app/
  179. GraphScope's Journey
    https://graphscope.io/journey/
  180. Wenyuan Yu
    https://datasets.ldbcouncil.org/event/sixteenth-tuc-meeting/attachments/wenyuan-yu-graphscope-flex.pdf
  181. GraphScope: A One-Stop Large Graph Processing System
    https://www.researchgate.net/publication/355145046_GraphScope_A_One-Stop_Large_Graph_Processing_System
  182. alibaba/GraphScope - GitHub Repository
    https://pypilb.vercel.app/repo/alibaba/GraphScope
  183. GraphScope: A One-Stop Large Graph Processing System
    https://lai.me/publication/graphscope-2021-demo/
  184. GraphScope Flex: A Graph Computing Stack with LEGO- ...
    https://jingbo.me/publication/gsf-abs.pdf
  185. GraphScope — Graph Database
    https://gdb-engines.com/db/graphscope/
  186. GraphScope: a unified engine for big graph processing
    https://dl.acm.org/doi/10.14778/3476311.3476369
  187. Alibaba/GraphScope
    https://gitee.com/alibaba/GraphScope
  188. Knowledge Graph (Google)
    https://en.wikipedia.org/wiki/Knowledge_Graph_(Google)
  189. Knowledge Graph Search API
    https://developers.google.com/knowledge-graph
  190. Google's Knowledge Graph Explained: How It Influences ...
    https://ahrefs.com/blog/google-knowledge-graph/
  191. How to Get Your Business on the Google Knowledge Graph
    https://webfor.com/blog/google-knowledge-graph/
  192. Google's Knowledge Graph and Knowledge Panels
    https://blog.google/products-and-platforms/products/search/about-knowledge-graph-and-knowledge-panels/
  193. Google's Great Clarity Cleanup: 3 Billion Entities Deleted
    https://www.linkedin.com/posts/search-engine-land_billions-of-entities-recently-vanished-activity-7363265272201314305-sxto
  194. Understanding Knowledge Panel, Graph & Google Business ...
    https://www.hillwebcreations.com/knowledge-graph-vs-knowledge-panel-vs-google-business-listing/
  195. Enterprise Knowledge Graph overview
    https://docs.cloud.google.com/enterprise-knowledge-graph/docs/overview
  196. Local Memo: Google Removes 3 Billion Knowledge Panels ...
    https://www.soci.ai/blog/local-memo-google-removes-3-billion-knowledge-panels-new-attribute-confirmations-creating-a-why-choose-us-page-for-ai/
  197. Google's great clarity cleanup: 3 shifts redefining the ...
    https://searchengineland.com/google-great-clarity-cleanup-knowledge-graph-ai-future-460836
  198. Optimize Your Business in the Knowledge Graph | Neil ...
    https://www.linkedin.com/posts/neilkpatel_seo-aisearch-knowledgegraph-activity-7442178604354002944-EGSF
  199. Google's Knowledge Graph: What You Need to Know
    https://kalicube.com/learning-spaces/faq-list/knowledge-panels/the-knowledge-graph-in-google-search/
  200. Knowledge graph scalability at billion entity scale, why ...
    https://www.equitus.ai/post/knowledge-graph
  201. …… 以及另外 884 个来源