分布式系统 Note
分布式文件系统 HDFS
- Hadoop 简介
- 分布式文件系统 HDFS
- 基本特征
- 大规模分布存储能力
- 高并发访问能力
- 强大的容错能力
- 顺序式文件访问
- 简单的一致性模型:一次写多次读
- 数据块存储模式:大力度
- 基本特征
- HDFS 上的数据存储
- NameNode 保存了一些元数据,主要对 DataNode 进行管理;DataNode 具体对数据块进行存储;
- 数据以块 block 的形式存储,默认大小为 64M,由于是这样的顺序访问方式提到读写的效率;
- Hadoop 安装,没啥好说的,看厦大林子雨实验室上的教程即可
- 常用操作命令
MapReduce
-
大数据处理:分而治之
-
构建抽象数据模型:Map 和 Reduce,借鉴了函数式设计语言 Lisp
- map: (k1; v1) ➞ [(k2; v2)]
- Reduce: (k2; [v2]) ➞ [(k3; v3)]
- 其中的 [...] 表示 list
- 一个例子:word count
-
上升到框架:自动并行化并隐藏底层细节
- MR 提供了一个统一的计算框架,实现了一些主要功能
- 任务调度
- 数据/代码互定位,代码向数据迁移
- 出错处理
- 分布式数据存储与文件管理
- Combiner 和 Partitioner,为了减少数据通信开销,combine 的目的是合并一些中间结果,而 partition 的目的是避免广播而使用一定的策略确保相关数据发送到同一个 reducer 节点
- MR 提供了一个统一的计算框架,实现了一些主要功能
-
MR 的主要涉及思想与特点
- 向「外」横向拓展,而非向「上」纵向拓展
- 失效被认为是常态
- 把处理向数据迁移
- 顺序处理数据、避免随机访问数据
- 对应用开发隐藏系统层细节
- 可拓展性
-
MR 基本工作原理
- 主从结构,用户和 Master 交互,Master 分配调度 Worker 用来作为 Map 和 Reduce 节点;注意 map 和 reduce 之间需要一个同步等待的时间
-
各执行阶段
- 分片split:HDFS 的存储单位的大小固定的 block,而 MR 的处理单位是 split,是一个逻辑概念,理想的大小是一个 block 块;
- Map 任务的数量等于分片的数量
- Shuffle 过程
- Map 端:有一个缓存默认 100M;溢写比例默认 0.8,在溢写的过程中需要分区、排序、合并 combine(👆提到的);map 结束之前会将之前溢写的文件进行归并 merge,整合成一个大的文件;
- 例如,对于两个键值对
<a, 1>, <a, 1>,combine 会得到<a,2>,而 merge 则只是单纯地把 value 变为 valuelist<a, <1,1>>。似乎 combine 主要是在内存中完成的,而 merge 则是针对多个溢写文件进行的操作。 - Reduce 端:Reduce 任务向 JobTracker 请求,领取从不同 Map 机器上的处理结果;先放进缓存,先 merge 后 combine;当然也会有溢写文件,多个溢写文件进行归并成一个大文件。
-
MR 程序执行过程
- Task 任务是 MR 框架的基本计算单位,是一个逻辑上的概念,存在于 JobTracker 和 TaskTracker 上
-
作业调度基本过程
- 每一个作业 Job 被划分成更小粒度的任务 Task,因此 Hadoop 作业调度在选择合适的作业之后还需要从中选择合适的任务,不同的调度器对作业有不同的组织结构,如单队列,多队列,作业池;
- 略
-
算法 #
- 搜索:可以是仅用到 Map(map-only job)
- 排序算法:仅需要用到 Partition 部分,保证 \(k_1<k_2\) ➞ \(h(k_1)<h(k_2)\)
- 单词同现矩阵
- 我们把一篇文档中的单词认为是「同现」的,要求计算所有单词的这样的一个相关关系的矩阵,显然没法直接存下来
- MR 的思路是,在 Map 阶段对于每一篇文档,每一组词输出 [(w1, w2), 1] 的形式,在 Reduce 阶段汇总即可;
- 优化:太多的小 key
(a,b):1,(a,c):2等,把这些 pairs 统合成 stripsa:{b:1, c:2}
- 文档倒排索引 Inversed Index
- 要求对于这些文档中的单词构建一个索引,key 是单词,value 是文档列表;
- 除了仅仅列出文档名,更为实用的是加入一些属性,有效负载 Payload,例如加入词频属性;这时的 map 输出是 word, [(docid, n)] 的形式;
- 为了对出现频次进行排序,变为 (word, n), docid 的形式,复合键;
- 同时也带来了新问题:直接用默认的 Partitioner 可能把同一个词分到不同的 Reducer,因此需要手动配置 Partitioner 把 key
(word, docid)中的 word 抽取出来作为准则
- K-Means 聚类
- 这里首次涉及到了迭代的算法,K-Means 的特点在于,对每一个点在每一轮只需要计算其与各个分类中心的距离——也就是说,我们需要的「全局信息」仅仅是各分类中心的位置
- 为此,维护一个全局信息,保存各分类中心的 id, center, #points (其中属于该类的数据点个数好像在实际迭代中不会用到?不过最终的结果可能是需要的)
- 在 Map 中计算点所属的类
- 在 Reduce 中汇总得到类中心
Hbase 数据库
- 从 BigTable 到 Hbase
- 概念
- 数据模型:表……
- 数据坐标,一个 cell 由四维坐标定位:
[行键, 列族, 列限定符, 时间戳] - 概念试图和物理视图
- 面向列的存储
- Hbase 实现原理
- 功能组件:库函数(链接到每个客户端)、Master 服务器、Region 服务器
- Region 的定位:一个列族最终拆分成很多个 Region 进行存储;而在定位的过程中,客户端不和 Master 打交道而直接和 Zookeeper 通信;这样降低了 Master 的负载
- Zookeeper 记录了
-ROOT-表的位置 -ROOT-表只能有一个 Region,记录了各个.META.表的位置信息.META.表保存了用户数据表中的 Region 位置信息,可以有多个 Region
- Zookeeper 记录了
- 这样就变成了「三级寻址」,不过思想也很简单,用
.META.表存储位置信息,但因为单个的表不够存储,所以另外引入了-ROOT-表,从而提高了可以索引的 Region 数量 - 另外,客户端有缓存机制,记录了已访问的 Region 地址
- Hbase 运行机制
- 这里讲了细节的 Region 服务器工作原理,每一个 Region 服务器包括多个 Region,然后每一个 Region 又有多个 Store 进行存储,每个 Store 包括了一个缓存 MemStore 和多个 StoreFile 文件;同时,一个服务器上的多个 Region 共享一个日志文件(预写式日志 Write Ahead Log WAL);更新的数据写入了日志之后才会写入 MenStore
- 缓存满了之后写成 StoreFile,并在 Hlog 中写入一个标记;StoreFile 文件过多的时候回进行合并,单个 StoreFile 过大又会分裂(一个父 Region 分裂成两个子 Region)
- 显然,Hlog 文件是用来恢复的,对于失效的服务器,取出 Hlog 文件进行分拆到另外的多个 Region 服务器上,并根据失效服务器上的 Region 文件进行重新的写入
- 这样听起来似乎很复杂(没有相关的背景知识),但实际上就是一个
Log Structured Merge Trees存储结构:用 MemStore 缓存加速写入,满了之后刷写成 Store,文件太多的时候进行 Merge,除此之外再加上 WAL 日志。
- HBase 应用方案
- 性能优化:利用行键是根据字典序存储这一特性涉及行键;其他的一些具体代码实现
- 性能监视
- 在 HBase 上构建 SQL 引擎
- Hive 整合 Hbase
- Phoenix
- 创建 HBase 二级索引
NoSQL
-
SQL,一些问题,新的使用场景,无法满足海量数据、并发访问、高拓展性、高可用等需求
-
NoSQL 理论
- (CAP 理论)一个系统的三种属性:consistency, availability, partition tolerance。只能得其二,而为了 scale out 必须进行 Partition,这意味着需要在可用性和一致性之中进行权衡。这里的 P 是 tolerance of network partition 分区容忍性,是指当网络出现了分区时系统仍可以运行。
- 一致性
- 强一致性:ACID - Atomic, Consistent, Isolated, Durable
- 弱一致性:BASE - Basically Available Soft-state Eventual consistency
- 基本可用,分布式系统一部分出现问题的时候,其余部分仍然可用,即允许网络分区
- 软状态(放松一致性),与硬状态对应,是指状态可以有一段时间不同步,具有一定的滞后性
- 最终一致性,即意味着在经历了较长的一段没有事务之后,网络中的节点之间是一致的(最常见的系统是 DNS 域名系统,有缓存)
- CA:放弃分区容忍性,传统的关系数据库,拓展性较差
- CP:放弃可用性,当出现网络分区的情况时,受影响的服务需要等待数据一致性
- AP:放弃一致性,允许系统返回不一致的数据
-
对于一个分布式系统来说,记 N 为数据复制的份数,W 为更新数据是需要保证写完成的节点数,R 为读取数据时需要读取的节点数;控制三者的关系,可以实现不同的一致性
- \(W+R>N\) 强一致性
- \(W+R\le N\) 弱一致性,如 \(N=2, W=R=1\) 这时无法保障
-
NoSQL 数据库与关系数据库的比较
- 从数据规模;数据库模式;查询效率;一致性;数据完整性;扩展性;可用性等角度
-
分类
- 键值数据库:如 Redis, Riak, SimpleDB
- 列族数据库:如 BigTable, Hbase, Cassandra HadoopDB
- 文档数据库:如 MongoDB, CouchDB
- 图数据库:如 Neo4J, OrientDB
-
后面介绍了一点 MongoDB
Hadoop 数据仓库
- SQL 概述
- 数据库、基本表、视图、索引
- 基本命令
- 数据仓库 Data Warehouse DW
- Subject Oriented, 面向主题的
- Integrated, 多种数据类型
- Non-Volatile, 不允许修改数据
- Time Variant, 记录同一个字段不同时间的 value
- OLAP Online analytical processing, 分析数据
- 层次化,不同粒度 Granularity 的数据
- Hive
- 系统架构
- 用户接口模块:CLI, HWI, JDBC, Trift Server
- 驱动模块 Driver:编译器、优化器、执行器
- 元数据存储模块 Metastore,是一个独立的关系数据库,Derby 或者 MySQL
- 底层依赖于 HDFS 和 MR;因此和传统 DB 不同的,例如仅支持批量更新;不支持数据更新;延时高
- Hive 工作原理
- 介绍了把 HiveQL 语言转化为 MR 作业:join, group by, 等
- 高可用 High Availability HA
- 由多个 Hive 实例组成,纳入到一个资源池中,由 HAProxy 提供一个统一的对外接口
- 总结一下优缺点
- 分布式数据仓库,支持大数据集查询;支持类似 SQL 的查询语言 HiveQL;有类似关系数据库的表结构和概念;多种格式数据的结构化管理;
- OLAP 应用;一些子查询和联合等 SQL 操作性能不佳;总体上采用 MR,适合批处理,反应速度不佳
- 系统架构
- Impala
- 并行数据库架构类型
- Shared memory
- shared disk
- shared nothing,每个节点由处理器、内存和磁盘组成,通过网络和其他 Node 的处理器通信
- hierachical
- 系统架构
- Impalad
- 负责协调客户端提交的查询的执行
- 与 HDFS 的 DataNode 运行在同一个节点上(每个 DataNode 上都有)
- 可以针对 HDFS 或者是 HBase 的数据进行操作
- State Store
- 创建一个 statestored 进程
- 负责收集分布在集群中各个 Impalad 进程的资源信息,用于查询调度
- CLI
- 给用户提供查询使用的命令行工具;提供 Hue, JDBC, ODBC 使用接口
- Impalad
- 具体的查询执行过程挺复杂的,略去
- 并行数据库架构类型
- Hive 和 Impala 比较
- 不同点
- Hive 适合长时间批处理查询,Impala 适合实时交互的 SQL 查询
- Hive 依赖于 MR 计算框架,而 Impala 把执行计划表现为一棵完整的执行计划树,直接分发执行计划到各个 Impalad 执行查询
- Hive 执行过程中若内存不够可以借助外存,而 Impala 则不会,因此有所限制
- 相同点
- 使用相同的存储数据池,都支持把数据存储与 HDFS 和 HBase 中
- 使用相同的元数据
- 对于 SQL 的解释处理比较相似,都是通过语法分析生成执行计划
- 不同点
- Hive 部署
Spark
- 简介
- Hadoop 的一些缺点:表达能力有限;磁盘 IO 开销大;延迟高
- 基于内存的 Spark 对于迭代运算的效率更高;基于 DAG 的任务调度执行机制要优于 MR 的迭代执行机制
- 实际应用中的三种需求:复杂的批量数据处理、基于历史数据的交互式查询、基于实时数据流的数据处理,对于时间跨度有不同的需求,原本需要 MR、Impala、Storm 结合处理,显然会带来不便;而 Spark 所提供的生态可以提供一站式的方案(Spark、Spark SQL、Spark Streaming)
- Spark 运行架构
- RDD 的思想
- 一个 Application 由一个 Driver 和多个 Job 组成;一个 Job 由多个 Stage 构成;一个 Stage 又由多个没有 Shuffle 关系的 Task 组成,Task 是运行在 Executor 的基本单元;
- 当执行一个 Application 的时候,Driver 向集群管理器申请资源启动 Executor,然后在 Executor 上执行 Task,结束后将结果返回给 Driver 或是直接写到 HDFS/数据库中。
- 运行基本流程
- 具体的流程中,由 Driver 构建一个 SparkContext,进行资源的申请、任务的分配和监控
- SC 和任务管理器交互,分配 Executor
- SC 根据 RDD 的依赖关系构建 DAG 图,由 DAGScheduler 解析成 Stage,把这些 Stage/Taskset 交给 TaskScheduler(底层的调度器);Executor 向 SC 申请 Task,TaskScheduler 将 Task 分配给 Executor 运行
- Executor 运行代码,将执行结果反馈给 TaskScheduler 和 DAGScheduler,执行结束后写入数据
- 听起来好像很复杂,涉及到的概念比较多,看了后面的 RDD 原理之后会更清楚,主要是由 Driver 构建了一个 SC 进行管理;因为用到的 RDD 和 DAG 任务管理机制,所以在 Driver 中有不同的组件来操作;Application 是总体的抽象包括了 Driver 和几个Job(相对独立/完整,可以理解成 RDD 中的 action)
- RDD 运行原理
- RDD 提供了「一种高度受限的共享内存模型」,即 RDD 是只读的记录分区的集合。
- RDD 提供了分类 action 和 transformation 的操作,两者的区别在于action 输入之后会立即执行而 transformation 遵循惰性机制;这一系列的操作构成 DAG, 称为血缘关系 Lineage,即 DAG 拓扑排序的结果。
- 在此基础上有一些优势,例如 RDD 本身就有高效的容错机制;存放在内存中避免 IO;存放的数据可以是 Java 对象从而避免了序列化和反序列化。
- 关于 RDD 中 Stage 的划分,整个流程我们可以转化为一个 DAG 图,通过这些 RDD 中的分区之间的依赖关系进行划分 Stage;具体来说,对 DAG 进行反向解析,遇到宽依赖就断开生成新的 Stage,遇到窄依赖则加入到这个 Stage 中,所谓的宽依赖,就是存在一个父 RDD 的一个分区对应一个子 RDD 的多个分区。Stage 可以看成一个 Taskset,分配到具体的 Executor 上去运行。
- RDD 操作
- Transformation
- Action
- Persistence
- 例子:基于 PySpark 实现的 word count、矩阵乘法
- 最后讲了一点 Hive on Spark 和 Spark SQL,以及两者之间的区别。后者相对不依赖 Hive 环境。
- 例子:K-Means 聚类、PageRank
- K-Means 的话,和上面讲 MR 时候的思路是一样的,我们关注的是 K 个中心的位置,而不变量是所有点的位置;在每一轮迭代中,用 map 找到最近的那个中心点,reduce 进行汇总,取平均以更新各中心点的位置即可;这里用了一个 Auxiliary 函数以计算一个点最近的那个中心;
- PageRank,首先需要设计数据结构,这里用了
links保留链接/网络结构,用ranks单独记录各 url 的 rank;主循环的时候将两者结合起来,flatMap 阶段分发某个 url 对其指向链接的权重,reduce 阶段加和汇总并引入随机浏览; - 这里使用 Spark SQL 来实现的,但感觉和 Spark 没啥区别?至少是代码结构上,可能
from pyspark.sql import SparkSession和之前直接从from pyspark import SparkContext是类似的吧,下面的代码也可以用 RDD 模型
- 对于关系数据,举了 Spark SQL 的例子
- 没有实际试过,不过和 SQL 很像,感觉主要的和👆的两个例子不同之处在于用了
sc.createDataFrame等相关的函数来生成结构化的数据
- 没有实际试过,不过和 SQL 很像,感觉主要的和👆的两个例子不同之处在于用了
- 最后提了 RDD, DataFeame, DataSet
Streaming Computing 流计算
-
流计算概述
- 静态数据和流数据
- 对应了批量计算和实时计算两种计算模式
-
流计算框架
- 商业级:IBM InfoSphere Streams
- 开源计算框架:Twitter Storm, Yahho! S4
- 公司自研发:Facebook Puma, Baidu Dstream
-
流程
- 数据实时采集
- 数据实时计算
- 数据查询服务(实时数据而非传统数据处理中的预先存储好的静态数据)
-
Storm
- Storm 设计思想
- Streams:将数据流描述成一个无限的 Tuple 序列,每个 tuple 都是一个键值对的 Map;由于各组件之间传递的 tuple 的字段名称都是事先定义的,因此实际上仅需要传 Value List 即可
- Spout:抽象的「水龙头」
- Bolt:用于 Stream 的状态转换,处理 tuples,创建新的 Streams
- Topology:个 Spout 和 Bolt 组件抽象出来的网络,Topology 中的每一个组件并行运行
- Stream Groupings:用于告知 Topology 如何在两个组件之间进行 Tuple 的传送
- Storm 框架设计
-
应用名称:总体上来看,Hadoop 运行的是 MR Job,而 Storm 上运行的是 Topology;
系统角色:JobTracker 和 TaskTracker 分别对应 Storm 中的 Nimbus 和 Superviser(运行在 Worker 上);
组件接口:Map/Reduce 对应 Spout/Bolt;
用 Zookeeper 作为分布式协调组件,负责 Nimbus 和 Supervisor 之间的协调工作。
-
在 Worker 节点上,运行着若干个 Worker 进程;
每个 Worker 进程内部有若干 Executor 线程(但默认下 Executor 和 Task 的数量一样,即每个 Executor 上运行一个 Task);
实际的数据处理由 Task 完成,每个 Task 对应了 Topology 中的 Spout/Bolt。
-
- Storm 设计思想
-
例子:用 Storm 实现 word count
-
Spark Streaming
- SS 的基本原理是对实时输入的数据流以时间片为单位进行划分,以类似批处理的方式进行处理;主要把数据流抽象成 DStream (Discreted Stream);当然这也导致了其无法实现真正毫秒级的流计算
图处理 Graph Processing
-
定义
- Vertex, Edge
- Eccentricity 一个顶点距离其他顶点的距离的最大值;在此基础上定义了 Radius 为图中任意顶点的 eccentricity 的最小值;Diameter 是图中任意顶点的 eccentricity 的最大值,即图中任意两点之间的距离的最大值。
-
Graph Queries
- 对于一张图 G,输入一个子图 Q 进行 query
- 分为 Subgraph isomorphism,需要节点/边之间的一一对应关系,双射,可理解成等价关系。NP-complete
- Graph simulation,定义是要求一个二元关系 S,满足对任意 \((u,v)\in S\),在 Q 上的边 \((u,u')\) 映射到 G 上的 \((v,v')\),使得 \((u', v')\in S\)。
- 子图查询是比较严谨的,而 simulation 则相对宽松一些,背景例如,需要从一个社交网络中找到犯罪团体,显然对于 pattern graph 不可能找到完全一致的子图同构,这是就需要 simulation。
-
回顾了一下关系数据库:schema,关系查询中的 Projection, Selection, Join, Union, Set difference, Group by and Aggregate
-
以及 XML 查询。SML 文档可以建模为一个 node-labeled ordered tree;XML 上的查询可以用 XPATH, XSLT, XQuary 等
-
提到的图查询的难点:1. 半结构化,没有 schema 或者 constraint,没有统一的查询语言;2. 图计算的很多操作复杂性高;2. 真实的图非常大,甚至 Polynomial/Linear 的算法都不可行
-
Graph systems
- Graph query engines: Giraph (Pregel, Google), GraphLab, Neo4j, GraphX
- Key-value stores
- RDF triple stores
-
Reachability queries
- 仅需要查询两点是否可达,采用 BFS,复杂度是 \(O(|V|+|E|)\) 的时间和空间
- 另一种方案是 2-hop cover,即对于每一个 v 维护 \(2hop(v)=(L_{in}(v), L_{out}(v))\) ,前者是所有能到 v 的节点,后者是所有 v 能到达的顶点,s 可到达 t 等价于 \(L_{out}(s)\cap L_{in}(t)\ne \varnothing\) ;测试来看时间复杂度要比 BFS 好些
-
Distance queries
- 对于一个点 s,找到其与 G 中其他任意点的距离,如用在运输网络中
- 经典的算法是 Dijkstra,就是一个 BFS,每次用最短的那个顶点(距离已经确定了)去更新其余的顶点
- MR 实现:来看对于一个顶点需要哪些信息,显然是其入度邻居对它的更新;另外需要注意的是 MR 过程中需要维护图的结构。因此,在 Map 阶段,首先原样输出 (nid m, node M) 其中包含了邻居列表(结构信息),然后利用自己的距离去更新其邻居节点;在 Reduce 阶段,对于每一个 nid m,接收顶点 node M,并从其入度邻居的中找到最小的那一个看是否能更新,重新整理成节点 (nid m, node M) 的形式输出。Termination 的话,可以设置一个 flag 检测是否发生了更新,这需要一个 non-MapReduce driver 来控制。
- 效率的话,相较于 Dijkstra 显然很低:1. 每一轮迭代中对于每一个节点都进行了操作,分发给所有的出度邻居;2. 不同计算节点之间消息传输的成本
-
MatchIng by subgraph isomorphism
- VF2,算法很直观:是 recursion, refinment 的,每次迭代维护一个集合 P 记录了可能的同构,在迭代中把点加入进来形成 \(S(P)\),这与这些新的可能的同构进行 feasibility check
- 关键在于可行性检测
- their predecessors are already mapped and included in P
- their successors can possibly be mapped
- Certain conditions on cardinalities of predecessors and successors to ensure correctness and expandability
- MR 版本,一个极为蠢的算法,对每一个顶点挖出其 d-hop 的子图,d 为 Q 的半径,在每一个这样的子图上进行查询;仅仅是概念上的,效率太低了。
-
MR 算法的局限性,其本身不支持迭代式的算法
- 中间结果的传递是 all to all 的;Reduce 需要等待所有的 Map 结束之后才能开始;IO 消耗……
-
BSP Bulk Sychronous Parallel Model
- 由一系列的超步 superstep 组成;每一个超步中,计算单元实现内部计算;超步之间实现消息传递 MP 和同步栅 sychronization barriar
- 具体到图计算上,这里的计算单元就是图中的每一个顶点,在一个超步中:1. 接受上一个超步其他顶点的 message;2. 本地计算;3. 传递 message 给下一步的其他顶点。超步中所有的顶点 parallel 计算。
-
Pregel
- 算法如何结束?类似于一个 flag,不过这里的表述的每个顶点 vote to halt,当所有顶点都 inactive 的时候结束
- 总而言之,是 vertix centric computation
- 例子:maximum value,这里向每个邻居节点传输该顶点的大小;每个顶点选最大的最后新的值;若出现了更新则调整状态为 active。
- 例子:PageRank,更加直观了,这里伪代码直接迭代了 30 此就结束了
- 例子:Dijkstra,也是直观的
-
其他的一些性质
- Combiner:可以对于一个超步中的传递给一个顶点的所有消息进行 combine,例如 Dijkstra 中取最小值
- Aggregation:每个顶点可以传递信息给 aggregator,其他的所有顶点在下一个超步可以读取;相当于引入了一个全局变量,restricted global communication
- Topology mutation:支持图的结构变化
-
Pregel 框架
- 还是主从结构
- 最好的分配顶点的方式,自然是要求 Worker 之间的信息交互最少,也即 Sparsest Cut Problem 问题(最小化切割经过的边数),不过是 NP-harp 的。
-
然后,从根本上来说,Pregel 的超步等策略,其实还是类似于 MR;只不过它对于每一个顶点建立计算单元,根据图的网络结构进行消息传递,是专门针对的图计算的一种计算框架;也就是说,Pregel 能做的 MR 也基本可以实现,只不过效率稍微低一点,而 MR 不适合做的 Pregel 也不适合做,例如之前提到的子图同构问题。

浙公网安备 33010602011771号