
Nebula Graph:一个开源的分布式图数据库。欢迎来 GitHub 交流:https://github.com/vesoft-inc/nebula
能力说明:
了解变量作用域、Java类的结构,能够创建带main方法可执行的java应用,从命令行运行java程序;能够使用Java基本数据类型、运算符和控制结构、数组、循环结构书写和运行简单的Java程序。
暂时未有相关云产品技术能力~
阿里云技能认证
详细说明摘要:本文主要介绍 Query 层的整体结构,并通过一条 nGQL 语句来介绍其通过 Query 层的四个主要模块的流程。 一、概述 分布式图数据库 Nebula Graph 2.0 版本相比 1.0 有较大改动,最明显的变化便是,在 1.0 版本中 Query、Storage 和 Meta 模块代码不作区分放在同一个代码仓中,而 Nebula Graph 2.0 开始在架构上先解耦成三个代码仓:nebula-graph、nebula-common 和 nebula-storage,其中 nebula-common 中主要是表达式的定义、函数定义和一些公共接口、nebula-graph 主要负责 Query 模块、nebula-storage 主要负责 Storage 和 Meta 模块。 本文主要介绍 Query 层的整体结构,并通过一条 nGQL 语句来介绍其通过 Query 层的四个主要模块的流程,由于 Nebula Graph 2.0 仍处于开发中,版本变化比较频繁,本文主要针对 2.0 的 nebula-graph 仓中 master 分支的 aea5befd179585c510fb83452cb82276a7756529 版本。 二、框架 Query 层主要框架如下所示: 主要分为 4 个子模块 Parser:词法语法解析模块 Validator:语句校验模块 Planner:执行计划和优化器模块 Executor:执行算子模块 三、代码结构 下面讲下 nebula-graph 的代码层次结构,如下所示 |--src |--context // 校验期和执行期上下文 |--daemons |--executor // 执行算子 |--mock |--optimizer // 优化规则 |--parser // 词法语法分析 |--planner // 执行计划结构 |--scheduler // 调度器 |--service |--util // 基础组件 |--validator // 语句校验 |--vistor 四、一个案例聊 Query 自 Nebula Graph v2.0 起,nGQL 的语法规则已经支持起始点的类型为 string ,正在兼容 1.0 的 int 类型。举个例子: GO FROM "Tim" OVER like WHERE like.likeness > 8.0 YIELD like._dst 上面的一条 nGQL 语句在 Nebula Graph 的 Query 层的数据流如下所示: 主要流程如下: 第一阶段:生成 AST 第一阶段:首先经过 Flex 和 Bison 组成的词法语法解析器模块 Parser 生成对应的 AST, 结构如下: 在此阶段 Parser 会拦截掉不符合语法规则的语句。举个例子,GO "Tim" FROM OVER like YIELD like._dst 这种语法使用错误的语句会在语法解析阶段直接被拦截。 第二阶段:校验 第二阶段:Validator 在 AST 上进行一系列的校验工作,主要工作如下: 元数据信息的校验 在解析 OVER 、 WHERE 和 YIELD 语句时,会查找 Schema,校验 edge、tag 的信息是否存在。或者在 INSERT 数据时校验插入数据类型和 Schema 中的是否一致 上下文引用校验 遇到多语句时,例如:$var = GO FROM "Tim" OVER like YIELD like._dst AS ID; GO FROM $var.ID OVER serve YIELD serve._dst ,Validator 会校验 $var.ID` 首先检查变量 `var` 是否定义,其次再检查属性 `ID` 是否属于变量 `var`, 如果是将 `$var.ID 替换为 $var1.ID` 或者 `$var.IID, 则会校验失败。 类型推断校验 推断表达式的结果属于什么类型,并根据具体的子句,校验类型是否正确。比如 WHERE 子句要求结果是 bool,null 或者 empty 。 *'' 展开** 例如,若输入语句为 GO FROM "Tim" OVER * YIELD like._dst, like.likeness, serve._dst,则在校验 OVER 子句时需要查询 Schema 将 * 展开为所有的边,假如 Schema 中只有 like 和 serve 两条边时,该语句会展开为:GO FROM "Tim" OVER serve, like YIELD like._dst, like.likeness, serve._dst 输入输出校验 遇到 PIPE 语句时,例如:GO FROM "Tim" OVER like YIELD like._dst AS ID | GO FROM $-.ID OVER serve YIELD serve._dst`,Validator 会校验 `$-.ID 由于 ID 在上一条语句中已经定义,则该子句合法,如果是将$-.ID 换为 `$-.a` 而此时 a 未定义,因此该子句非法。 第三阶段:生成可执行计划 第三阶段:经过 Validator 之后会生成一个可执行计划,其中执行计划的数据结构在 src/planner 目录下,其逻辑结构如下: Query 执行流 执行流:该执行计划是一个有向无环图,其中节点间的依赖关系在 Validator 中每个模块的 toPlan() 函数中确定,在这个例子中 Project 依赖 Filter, Filter 依赖 GetNeighbor,依次类推直到 Start 节点为止。 在执行阶段执行器会对每个节点生成一个对应的算子,并且从根节点(这个例子中是 Project 节点)开始调度,此时发现此节点依赖其他节点,就先递归调用依赖的节点,一直找到没有任何依赖的节点(此时为 Start 节点),然后开始执行,执行此节点后,继续执行此节点被依赖的其他节点(此时为 GetNeighbor 节点),一直到根节点为止。 Query 数据流 数据流:每个节点的输入输出也是在 toPlan() 中确定的, 虽然执行的时候会按照执行计划的先后关系执行,但是每个节点的输入并不完全依赖上个节点,可以自行定义,因为所有节点的输入、输出其实是存储在一个哈希表中的,其中 key 是在建立每个节点的时候自己定义的名称,假如哈希表的名字为 ResultMap,在建立 Filter 这个节点时,定义该节点从 ResultMap["GN1"] 中取数据,然后将结果放入 ResultMap["Filter2"] 中,依次类推,将每个节点的输入输出都确定好,该哈希表定义在 nebula-graph 仓下 src/context/ExecutionContext.cpp 中,因为执行计划并不是真正地执行,所以对应哈希表中每个 key 的 value 值都为空(除了开始节点,此时会将起始数据放入该节点的输入变量中),其值会在 Excutor 阶段被计算并填充。 这个例子比较简单,最后会放一个复杂点的例子以便更好地理解执行计划。 第四阶段:执行计划优化 第四阶段:执行计划优化。如果 etc/nebula-graphd.conf 配置文件中 enable_optimizer 设置为 true ,则会对执行计划的优化,例如上边的例子,当开启优化时: 此时会将 Filter 节点融入到 GetNeighbor 节点中,在执行阶段当 GetNeighbor 算子调用 Storage 层的接口获取一个点的邻边的时候,Storage 层内部会直接将不符合条件的边过滤掉,这样就可以极大的减少数据量的传输,俗称过滤下推。 在执行计划中,每个节点直接依赖另外一个节点。为了探索等价的变换和重用计划中相同的部分,会将节点的这种直接依赖关系转换为 OptGroupNode 与 OptGroup 的依赖。每个 OptGroup 中可以包含等价的 OptGroupNode 的集合,每个 OptGroupNode 都包含执行计划中的一个节点,同时 OptGroupNode 依赖的不再是 OptGroupNode 而是 OptGroup,这样从该 OptGroupNode 出发可以根据其依赖 OptGroup 中的不同的 OptGroupNode 拓展出很多等价的执行计划。同时 OptGroup 还可以被不同的 OptGroupNode 共用,节省存储的空间。 目前我们实现的所有优化规则认为是 RBO(rule-based optimization),即认为应用规则后的计划一定比应用前的计划要优。CBO(cost-based optimization) 目前正在同步开发。整个优化的过程是一个"自底向上"的探索过程,即对于每个规则而言,都会由执行计划的根节点(此例中是 Project 节点)开始,一步步向下找到最底层的节点,然后由该节点开始一步步向上探索每个 OptGroup 中的 OptGroupNode 是否匹配该规则,直到整个 Plan 都不能再应用该规则为止,再执行下一个规则的探索。 本例中的优化如下图所示: 例如,当搜索到 Filter 节点时,发现 Filter 节点的子节点是 GetNeighbors,和规则中事先定义的模式匹配成功,启动转换,将 Filter 节点融入到 GetNeighbors 节点中,然后移除掉 Filter 节点,继续匹配下一个规则。 优化的代码在 nebula-graph 仓下 src/optimizer/ 目录下。 第五阶段:执行 第五阶段:最后 Scheduler 会根据执行计划生成对应的执行算子,从叶子节点开始执行,一直到根节点结束。其结构如下: 其中每一个执行计划节点都一一对应一个执行算子节点,其输入输出在执行计划期间已经确定,每个算子只需要拿到输入变量中的值然后进行计算,最后将计算结果放入对应的输出变量中即可,所以只需要从开始节点一步步执行,最后一个算子的结果会作为最终结果返回给用户。 五、实例 下面执行一个最短路径的实例看看执行计划的具体结构,打开 nebula-console, 输入下面语句FIND SHORTEST PATH FROM "YAO MING" TO "Tim Duncan" OVER like, serve UPTO 5 STEPS ,在这条语句前加 EXPLAIN 关键字就可以得到该语句生成的执行计划详细信息: 上图从左到右依次显示执行计划中每个节点的唯一 ID、节点的名称、该节点所依赖的节点 ID、profiling data(执行 profile 命令时的信息)、该节点的详细信息(包括输入输出变量名称,输出结果的列名,节点的参数信息)。 如果想要可视化一点可以在这条语句前加 EXPLAIN format="dot",这时候 nebula-console 会生成 dot 格式的数据,然后打开 Graphviz Online 这个网站将生成的 dot 数据粘贴上去,就可以看到如下结构,该结构对应着执行阶段各个算子的执行流程。 因为最短路径使用了双向广度搜索算法分别从"YAO MING" 和 "Tim Duncan" 两边同时扩展,所以中间的 GetNeighbors、BFSShortest、 Project、 Dedup 分别有两个算子,通过 PassThrough 算子连接输入,由 ConjunctPath 算子拼接路径。然后由 LOOP 算子控制向外扩展的步数,可以看到 DataCollect 算子的输入其实是从 ConjuctPath 算子的输出变量中取值的。 各个算子的信息在 nebula-graph 仓下的 src/executor 目录下。 作者有话说:Hi,我是明泉,是图数据 Nebula Graph 研发工程师,主要工作和数据库查询引擎相关,希望本次的经验分享能给大家带来帮助,如有不当之处也希望能帮忙纠正,谢谢~ 喜欢这篇文章?来来来,给我们的 GitHub 点个 star 表鼓励啦~~ ♂️♀️ [手动跪谢] 交流图数据库技术?交个朋友,Nebula Graph 官方小助手微信:NebulaGraphbot 拉你进交流群~~ 推荐阅读 Nebula 架构剖析系列(二)图数据库的查询引擎设计
本文首发于 Nebula Graph 官方博客:https://nebula-graph.com.cn/posts/nebula-graph-risk-control-boss-zhipin/ 摘要:在本文中,BOSS 直聘大数据开发工程师主要分享一些他们内部的技术指标和选型,以及很多小伙伴感兴趣的 Dgraph 对比使用经验。 业务背景 在 Boss 直聘的安全风控技术中,需要用到大规模图存储和挖掘计算,之前主要基于自建的高可用 Neo4j 集群来保障相关应用,而在实时行为分析方面,需要一个支持日增 10 亿关系的图数据库,Neo4j 无法满足应用需求。 针对这个场景,前期我们主要使用 Dgraph,踩过很多坑并和 Dgraph 团队连线会议,在使用 Dgraph 半年后最终还是选择了更贴合我们需求的 Nebula Graph。具体的对比 Benchmark 已经有很多团队在论坛分享了,这里就不再赘述,主要分享一些技术指标和选型,以及很多小伙伴感兴趣的 Dgraph 对比使用经验。 技术指标 硬件 配置如下: 处理器:Intel(R) Xeon(R) Gold 6230 CPU @ 2.10GHz 80(cores) 内存:DDR4,128G 存储:1.8T SSD 网络:万兆 Nebula Graph 部署 5 个节点,按官方建议 3 个 metad / 5 个 graphd / 5 个 storaged 软件 Nebula Graph 版本:V1.1.0 操作系统:CentOS Linux release 7.3.1611 (Core) 配置 主要调整的配置和 storage 相关 # 按照文档建议,配置内存的 3 分之 1 --rocksdb_block_cache=40960 # 参数配置减小内存使用 --enable_partitioned_index_filter=true --max_edge_returned_per_vertex=100000 指标 目前安全行为图保存 3 个月行为,近 500 亿边,10 分钟聚合写入一次,日均写入点 3,000 万,日均写入边 5.5 亿,插入延时 <=20 ms。 读延时 <= 100 ms,业务侧接口读延时 <= 200 ms,部分超大请求 < 1 s 当前磁盘空间占用 600G * 5 左右 CPU 耗用 500% 左右,内存使用稳定在 60 G 左右 Dgraph 使用对比 目前来说原生分布式图数据库国内选型主要比对 Dgraph和 Nebula Graph,前者我们使用半年,整体使用对比如下,这些都是我们踩过坑的地方。 就我们使用经验,Dgraph 设计理念很好,但是目前还不太满足我们业务需求,GraphQL 的原生支持还是有很大吸引力,但是存储结构决定容易 OOM(边存储也分组的话会优化很多,官方之前计划优化);另外,采用自己编写的 badger 和 ristretto,目前最大的问题是从官方释放的使用案例来看,未经大规模数据场景验证,在我们实际使用中,大数据量和高 QPS 写入场景下容易出现崩溃和 OOM,且如果采用 SSD 存储海量数据,Dgraph 的磁盘放大和内存占用也需要优化。 如果没有高 QPS 写入,目前 Dgraph 还是值得一试,对于很多快速原型的场景,作为 GraphQL 原生图数据库使其非常适合做基于图的数据中台,这是目前的一个大趋势,它也上线了自己的云服务,业内标杆 TigerGraph 也在做相关探索,另外事务的完善支持也是它的优势,这块暂时用不到,所以没做相关评测。实测 Dgraph 在线写入并发不高或只是离线导入数据使用的情况下还是很稳定的,如果想借助它的高可用和事务功能,可以尝试下。 对比来说,Nebula Graph 很优秀,特别是工程化方面,体现在很多细节,可以看出开发团队在实际使用和实现上做较了较好的平衡: 1.支持手动控制数据平衡时机,自动固然很好,但是容易导致很多问题 2.控制内存占用(enable_partitioned_index_filter 优化和设置单次最大返回边数目),都放在内存固然快,但有时候也需要考虑数据量和性能的平衡 3.多图物理隔离,多张图实在太有必要 4.nGQL 最大程度接近最常用 MySQL 语句,2 期兼容 Cypher 更加完美;对比 GraphQL 固然香,但写起复杂图查询真的让人想爆炸,可能还是更加适合做数据中台查询语言 5.和图计算框架的结合,最近释放的 Spark GraphX 结合算法非常有用,原先我们的图计算都是基于 GraphX 从 Neo4j 抽取后离线计算团伙,后续打算尝试 Nebula Graph 抽取 这里主要从实际经验对比分享,二者都在持续优化,都在快速迭代,建议使用前多看看最新版本 release说明。 建议 当前 Nebula Graph 做得很优秀,结合我们现在的需求也提一点点建议: 1.更多离线算法,包括:现有的图神经网络这块的支持,图在线查询多用在分析,真正线上应用目前很多还是图计算离线算完后入库供查询 2.Plato 框架的合并支持,Spark GraphX 相对计算效率还是低一些,如果能整合腾讯的 Plato 框架更好 3.借鉴 TigerGraph 和 Dgraph,支持固化 nGQL 查询语句直接生成服务 REST 端点,HTTP 传入参数即可查询,这样可快速生成数据查询接口,不用后台再单独连接数据库写 SQL 提供数据服务 目前 Boss 直聘将 Nebula Graph 图数据库应用在安全业务,相关应用已经线上稳定运行大半年,本文分享了一点经验,抛砖引玉,期望更多技术伙伴来挖掘Nebula这座宝库。 Dgraph 遇到的一些问题,供有需要小伙伴参考 给 Dgraph 一些 issues 给 Dgraph 提交的 PRs 参考文章 360 的 JanusGraph 到 Nebula Graph 数据迁移 本文系 Boss直聘·安全技术中心 文洲 撰写 推荐阅读 Nebula Graph 在微众银行数据治理业务的实践 图数据库选型 | 360 数科的图数据库迁移史
本文主要讲述如何利用 Spark Connector 进行 Nebula Graph 数据的读取。 Spark Connector 简介 Spark Connector 是一个 Spark 的数据连接器,可以通过该连接器进行外部数据系统的读写操作,Spark Connector 包含两部分,分别是 Reader 和 Writer,而本文侧重介绍 Spark Connector Reader,Writer 部分将在下篇和大家详聊。 Spark Connector Reader 原理 Spark Connector Reader 是将 Nebula Graph 作为 Spark 的扩展数据源,从 Nebula Graph 中将数据读成 DataFrame,再进行后续的 map、reduce 等操作。 Spark SQL 允许用户自定义数据源,支持对外部数据源进行扩展。通过 Spark SQL 读取的数据格式是以命名列方式组织的分布式数据集 DataFrame,Spark SQL 本身也提供了众多 API 方便用户对 DataFrame 进行计算和转换,能对多种数据源使用 DataFrame 接口。 Spark 调用外部数据源包的是 org.apache.spark.sql,首先了解下 Spark SQL 提供的的扩展数据源相关的接口。 Basic Interfaces BaseRelation:表示具有已知 Schema 的元组集合。所有继承 BaseRelation 的子类都必须生成 StructType 格式的 Schema。换句话说,BaseRelation 定义了从数据源中读取的数据在 Spark SQL 的 DataFrame 中存储的数据格式的。 RelationProvider:获取参数列表,根据给定的参数返回一个新的 BaseRelation。 DataSourceRegister:注册数据源的简写,在使用数据源时不用写数据源的全限定类名,而只需要写自定义的 shortName 即可。 Providers RelationProvider:从指定数据源中生成自定义的 relation。 createRelation() 会基于给定的 Params 参数生成新的 relation。 SchemaRelationProvider:可以基于给定的 Params 参数和给定的 Schema 信息生成新的 Relation。 RDD RDD[InternalRow]: 从数据源中 Scan 出来后需要构造成 RDD[Row] 要实现自定义 Spark 外部数据源,需要根据数据源自定义上述部分方法。 在 Nebula Graph 的 Spark Connector 中,我们实现了将 Nebula Graph 作为 Spark SQL 的外部数据源,通过 sparkSession.read 形式进行数据的读取。该功能实现的类图展示如下: 定义数据源 NebulaRelatioProvider,继承 RelationProvider 进行 relation 自定义,继承 DataSourceRegister 进行外部数据源的注册。 定义 NebulaRelation 定义 Nebula Graph 的数据 Schema 和数据转换方法。在 getSchema() 方法中连接 Nebula Graph 的 Meta 服务获取配置的返回字段对应的 Schema 信息。 定义 NebulaRDD 进行 Nebula Graph 数据的读取。 compute() 方法中定义如何读取 Nebula Graph 数据,主要涉及到进行 Nebula Graph 数据 Scan、将读到的 Nebula Graph Row 数据转换为 Spark 的 InternalRow 数据,以 InternalRow 组成 RDD 的一行,其中每一个 InternalRow 表示 Nebula Graph 中的一行数据,最终通过分区迭代的形式将 Nebula Graph 所有数据读出组装成最终的 DataFrame 结果数据。 Spark Connector Reader 实践 Spark Connector 的 Reader 功能提供了一个接口供用户编程进行数据读取。一次读取一个点/边类型的数据,读取结果为 DataFrame。 下面开始实践,拉取 GitHub 上 Spark Connector 代码: git clone -b v1.0 git@github.com:vesoft-inc/nebula-java.git cd nebula-java/tools/nebula-spark mvn clean compile package install -Dgpg.skip -Dmaven.javadoc.skip=true 将编译打成的包 copy 到本地 Maven 库。 应用示例如下: 在 mvn 项目的 pom 文件中加入 nebula-spark 依赖 <dependency> <groupId>com.vesoft</groupId> <artifactId>nebula-spark</artifactId> <version>1.1.0</version> </dependency> 在 Spark 程序中读取 Nebula Graph 数据: // 读取 Nebula Graph 点数据 val vertexDataset: Dataset[Row] = spark.read .nebula("127.0.0.1:45500", "spaceName", "100") .loadVerticesToDF("tag", "field1,field2") vertexDataset.show() // 读取 Nebula Graph 边数据 val edgeDataset: Dataset[Row] = spark.read .nebula("127.0.0.1:45500", "spaceName", "100") .loadEdgesToDF("edge", "*") edgeDataset.show() 配置说明: nebula(address: String, space: String, partitionNum: String) address:可以配置多个地址,以英文逗号分割,如“ip1:45500,ip2:45500” space: Nebula Graph 的 graphSpace partitionNum: 设定spark读取Nebula时的partition数,尽量使用创建 Space 时指定的 Nebula Graph 中的 partitionNum,可确保一个Spark的partition读取Nebula Graph一个part的数据。 loadVertices(tag: String, fields: String) tag:Nebula Graph 中点的 Tag fields:该 Tag 中的字段,,多字段名以英文逗号分隔。表示只读取 fields 中的字段,* 表示读取全部字段 loadEdges(edge: String, fields: String) edge:Nebula Graph 中边的 Edge fields:该 Edge 中的字段,多字段名以英文逗号分隔。表示只读取 fields 中的字段,* 表示读取全部字段 其他 Spark Connector Reader 的 GitHub 代码:https://github.com/vesoft-inc/nebula-java/tree/master/tools/nebula-spark 在此特别感谢半云科技所贡献的 Spark Connector 的 Java 版本 参考资料 [1] Extending Spark Datasource API: write a custom spark datasource[2] spark external datasource source code 喜欢这篇文章?来来来,给我们的 GitHub 点个 star 表鼓励啦~~ ♂️♀️ [手动跪谢] 交流图数据库技术?交个朋友,Nebula Graph 官方小助手微信:NebulaGraphbot 拉你进交流群~~
摘要:一个有意思的 Crash 探究过程,Clang 有 GCC 没有 本文首发于 Nebula Graph 官方博客:https://nebula-graph.com.cn/posts/troubleshooting-crash-clang-compiler-optimization/ 如果有人告诉你,下面的 C++ 函数会导致程序 crash,你会想到哪些原因呢? std::string b2s(bool b) { return b ? "true" : "false"; } 如果再多给一些描述,比如: Crash 以一定的概率复现 Crash 原因是段错误(SIGSEGV) 现场的 Backtrace 经常是不完整甚至完全丢失的。 只有优化级别在 -O2 以上才会(更容易)复现 仅在 Clang 下复现,GCC 复现不了 好了,一些老鸟可能已经有线索了,下面给出一个最小化的复现程序和步骤: // file crash.cpp #include <iostream> #include <string> std::string __attribute__((noinline)) b2s(bool b) { return b ? "true" : "false"; } union { unsigned char c; bool b; } volatile u; int main() { u.c = 0x80; std::cout << b2s(u.b) << std::endl; return 0; } $ clang++ -O2 crash.cpp $ ./a.out truefalse,d$x4DdzRx Segmentation fault (core dumped) $ gdb ./a.out core.3699 Core was generated by `./a.out'. Program terminated with signal SIGSEGV, Segmentation fault. #0 0x0000012cfffff0d4 in ?? () (gdb) bt #0 0x0000012cfffff0d4 in ?? () #1 0x00000064fffff0f4 in ?? () #2 0x00000078fffff124 in ?? () #3 0x000000b4fffff1e4 in ?? () #4 0x000000fcfffff234 in ?? () #5 0x00000144fffff2f4 in ?? () #6 0x0000018cfffff364 in ?? () #7 0x0000000000000014 in ?? () #8 0x0110780100527a01 in ?? () #9 0x0000019008070c1b in ?? () #10 0x0000001c00000010 in ?? () #11 0x0000002ffffff088 in ?? () #12 0xe2ab001010074400 in ?? () #13 0x0000000000000000 in ?? () 因为 backtrace 信息不完整,说明程序并不是在第一时间 crash 的。面对这种情况,为了快速找出第一现场,我们可以试试 AddressSanitizer(ASan): $ clang++ -g -O2 -fno-omit-frame-pointer -fsanitize=address crash.cpp $ ./a.out ================================================================= ==3699==ERROR: AddressSanitizer: global-buffer-overflow on address 0x000000552805 at pc 0x0000004ff83a bp 0x7ffd7610d240 sp 0x7ffd7610c9f0 READ of size 133 at 0x000000552805 thread T0 #0 0x4ff839 in __asan_memcpy (a.out+0x4ff839) #1 0x5390a7 in b2s[abi:cxx11](bool) crash.cpp:6 #2 0x5391be in main crash.cpp:16:18 #3 0x7faed604df42 in __libc_start_main (/usr/lib64/libc.so.6+0x23f42) #4 0x41c43d in _start (a.out+0x41c43d) 0x000000552805 is located 59 bytes to the left of global variable '<string literal>' defined in 'crash.cpp:6:25' (0x552840) of size 6 '<string literal>' is ascii string 'false' 0x000000552805 is located 0 bytes to the right of global variable '<string literal>' defined in 'crash.cpp:6:16' (0x552800) of size 5 '<string literal>' is ascii string 'true' SUMMARY: AddressSanitizer: global-buffer-overflow (/home/dutor.hou/Wdir/nebula-graph/build/bug/a.out+0x4ff839) in __asan_memcpy Shadow bytes around the buggy address: … ... 从 ASan 给出的信息,我们可以定位到是函数 b2s(bool) 在读取字符串常量 "true" 的时候,发生了“全局缓冲区溢出”。好了,我们再次以上帝视角审视一下问题函数和复现程序,“似乎”可以得出结论:因为 b2s 的布尔类型参数 b 没有初始化,所以 b 中存储的是一个 0 和 1 之外的值[1]。那么问题来了,为什么 b 的这种取值会导致“缓冲区溢出”呢?感兴趣的可以将 b 的类型由 bool 改成 char 或者 int,问题就可以得到修复。 想要解答这个问题,我们不得不看下 clang++ 为 b2s 生成了怎样的指令(之前我们提到 GCC 下没有出现 crash,所以问题可能和代码生成有关)。在此之前,我们应该了解: 样例程序中,b2s 的返回值是一个临时的 std::string 对象,是保存在栈上的 C++ 11 之后,GCC 的 std::string 默认实现使用了 SBO(Small Buffer Optimization),其定义大致为 std::string{ char *ptr; size_t size; union{ char buf[16]; size_t capacity}; }。对于长度小于 16 的字符串,不需要额外申请内存。 OK,那我们现在来看一下 b2s 的反汇编并给出关键注解: (gdb) disas b2s Dump of assembler code for function b2s[abi:cxx11](bool): 0x00401200 <+0>: push %r14 0x00401202 <+2>: push %rbx 0x00401203 <+3>: push %rax 0x00401204 <+4>: mov %rdi,%r14 # 将返回值(string)的起始地址保存到 r14 0x00401207 <+7>: mov $0x402010,%ecx # 将 "true" 的起始地址保存至 ecx 0x0040120c <+12>: mov $0x402015,%eax # 将 "false" 的起始地址保存至 eax 0x00401211 <+17>: test %esi,%esi # “测试” 参数 b 是否非零 0x00401213 <+19>: cmovne %rcx,%rax # 如果 b 非零,则将 "true" 地址保存至 rax 0x00401217 <+23>: lea 0x10(%rdi),%rdi # 将 string 中的 buf 起始地址保存至 rdi # (同时也是后面 memcpy 的第一个参数) 0x0040121b <+27>: mov %rdi,(%r14) # 将 rdi 保存至 string 的 ptr 字段,即 SBO 0x0040121e <+30>: mov %esi,%ebx # 将 b 的值保存至 ebx 0x00401220 <+32>: xor $0x5,%rbx # 将 0x5 异或到 rbx(也即 ebx) # 注意,如果 rbx 非 0 即 1,那么 rbx 保存的就是 4 或 5, # 即 "true" 或 "false" 的长度 0x00401224 <+36>: mov %rax,%rsi # 将字符串起始地址保存至 rsi,即 memcpy 的第二个参数 0x00401227 <+39>: mov %rbx,%rdx # 将字符串的长度保存至 rdx,即 memcpy 的第三个参数 0x0040122a <+42>: callq <memcpy@plt> # 调用 memcpy 0x0040122f <+47>: mov %rbx,0x8(%r14) # 将字符串长度保存到 string::size 0x00401233 <+51>: movb $0x0,0x10(%r14,%rbx,1) # 将 string 以 '\0' 结尾 0x00401239 <+57>: mov %r14,%rax # 将 string 地址保存至 rax,即返回值 0x0040123c <+60>: add $0x8,%rsp 0x00401240 <+64>: pop %rbx 0x00401241 <+65>: pop %r14 0x00401243 <+67>: retq End of assembler dump. 到这里,问题就无比清晰了: clang++ 假设了 bool 类型的值非 0 即 1 在编译期,”true” 和 ”false” 长度已知 使用异或指令( 0x5 ^ false == 5, 0x5 ^ true == 4)计算要拷贝的字符串的长度 当 bool 类型不符合假设时,长度计算错误 因为 memcpy 目标地址在栈上(仅对本例而言),因此栈上的缓冲区也可能溢出,从而导致程序跑飞,backtrace 缺失。 注: C++ 标准要求 bool 类型至少_能够_表示两个状态: true 和 false ,但并没有规定 sizeof(bool) 的大小。但在几乎所有的编译器实现上, bool 都占用一个寻址单位,即字节。因此,从存储角度,取值范围为 0x00-0xFF,即 256 个状态。 喜欢这篇文章?来来来,给我们的 GitHub 点个 star 表鼓励啦~~ ♂️♀️ [手动跪谢] 交流图数据库技术?交个朋友,Nebula Graph 官方小助手微信:NebulaGraphbot 拉你进交流群~~ 推荐阅读 一次 Segmentation Fault 和 GCC Illegal Instruction 编译问题排查
摘要:本文所介绍 Nebula Graph 连接器 Nebula Flink Connector,采用类似 Flink 提供的 Flink Connector 形式,支持 Flink 读写分布式图数据库 Nebula Graph。 文章首发 Nebula Graph 官网博客:https://nebula-graph.com.cn/posts/nebula-flink-connector/ 在关系网络分析、关系建模、实时推荐等场景中应用图数据库作为后台数据支撑已相对普及,且部分应用场景对图数据的实时性要求较高,如推荐系统、搜索引擎。为了提升数据的实时性,业界广泛应用流式计算对更新的数据进行增量实时处理。为了支持对图数据的流式计算,Nebula Graph 团队开发了 Nebula Flink Connector,支持利用 Flink 进行 Nebula Graph 图数据的流式处理和计算。 Flink 是新一代流批统一的计算引擎,它从不同的第三方存储引擎中读取数据,并进行处理,再写入另外的存储引擎中。Flink Connector 的作用就相当于一个连接器,连接 Flink 计算引擎跟外界存储系统。 与外界进行数据交换时,Flink 支持以下 4 种方式: Flink 源码内部预定义 Source 和 Sink 的 API; Flink 内部提供了 Bundled Connectors,如 JDBC Connector。 Apache Bahir 项目中提供连接器 Apache Bahir 最初是从 Apache Spark 中独立出来的项目,以提供不限于 Spark 相关的扩展/插件、连接器和其他可插入组件的实现。 通过异步 I/O 方式。 流计算中经常需要与外部存储系统交互,比如需要关联 MySQL 中的某个表。一般来说,如果用同步 I/O 的方式,会造成系统中出现大的等待时间,影响吞吐和延迟。异步 I/O 则可以并发处理多个请求,提高吞吐,减少延迟。 本文所介绍 Nebula Graph 连接器 Nebula Flink Connector,采用类似 Flink 提供的 Flink Connector 形式,支持 Flink 读写分布式图数据库 Nebula Graph。 一、Connector Source Flink 作为一款流式计算框架,它可处理有界数据,也可处理无界数据。所谓无界,即源源不断的数据,不会有终止,实时流处理所处理的数据便是无界数据;批处理的数据,即有界数据。而 Source 便是 Flink 处理数据的数据来源。 Nebula Flink Connector 中的 Source 便是图数据库 Nebula Graph。Flink 提供了丰富的 Connector 组件允许用户自定义数据源来连接外部数据存储系统。 1.1 Source 简介 Flink 的 Source 主要负责外部数据源的接入,Flink 的 Source 能力主要是通过 read 相关的 API 和 addSource 方法这 2 种方式来实现数据源的读取,使用 addSource 方法对接外部数据源时,可以使用 Flink Bundled Connector,也可以自定义 Source。 Flink Source 的几种使用方式如下: 本章主要介绍如何通过自定义 Source 方式实现 Nebula Graph Source。 1.2 自定义 Source 在 Flink 中可以使用 StreamExecutionEnvironment.addSource(sourceFunction) 和 ExecutionEnvironment.createInput(inputFormat) 两种方式来为你的程序添加数据来源。 Flink 已经提供多个内置的 source functions ,开发者可以通过继承 RichSourceFunction来自定义非并行的 source ,通过继承 RichParallelSourceFunction 来自定义并行的 Source 。RichSourceFunction 和 RichParallelSourceFunction 是 SourceFunction 和 RichFunction 特性的结合。 其中SourceFunction 负责数据的生成, RichFunction 负责资源的管理。当然,也可以只实现 SourceFunction 接口来定义最简单的只具备获取数据功能的 dataSource 。 通常自定义一个完善的 Source 节点是通过实现 RichSourceFunction 类来完成的,该类兼具 RichFunction 和 SourceFunction 的能力,因此自定义 Flink 的 Nebula Graph Source 功能我们需要实现 RichSourceFunction 中提供的方法。 1.3 自定义 Nebula Graph Source 实现原理 Nebula Flink Connector 中实现的自定义 Nebula Graph Source 数据源提供了两种使用方式,分别是 addSource 和 createInput 方式。 Nebula Graph Source 实现类图如下: (1)addSource 该方式是通过 NebulaSourceFunction 类实现的,该类继承自 RichSourceFunction 并实现了以下方法: open准备 Nebula Graph 连接信息,并获取 Nebula Graph Meta 服务和 Storage 服务的连接。 close数据读取完成,释放资源。关闭 Nebula Graph 服务的连接。 run开始读取数据,并将数据填充到 sourceContext。 cancel取消 Flink 作业时调用,关闭资源。 (2)createInput 该方式是通过 NebulaInputFormat 类实现的,该类继承自 RichInputFormat 并实现了以下方法: openInputFormat准备 inputFormat,获取连接。 closeInputFormat数据读取完成,释放资源,关闭 Nebula Graph 服务的连接。 getStatistics 获取数据源的基本统计信息。 createInputSplits 基于配置的 partition 参数创建 GenericInputSplit。 getInputSplitAssigner 返回输入的 split 分配器,按原始计算的顺序返回 Source 的所有 split。 open开始 inputFormat 的数据读取,将读取的数据转换 Flink 的数据格式,构造迭代器。 close数据读取完成,打印读取日志。 reachedEnd是否读取完成 nextRecord通过迭代器获取下一条数据 通过 addSource 读取 Source 数据得到的是 Flink 的 DataStreamSource,表示 DataStream 的起点。 通过 createInput 读取数据得到的是 Flink 的 DataSource,DataSource 是一个创建新数据集的 Operator,这个 Operator 可作为进一步转换的数据集。DataSource 可以通过 withParameters 封装配置参数进行其他的操作。 1.4 自定义 Nebula Graph Source 应用实践 使用 Flink 读取 Nebula Graph 图数据时,需要构造 NebulaSourceFunction 和 NebulaOutputFormat,并通过 Flink 的 addSource 或 createInput 方法注册数据源进行 Nebula Graph 数据读取。 构造 NebulaSourceFunction 和 NebulaOutputFormat 时需要进行客户端参数的配置和执行参数的配置,说明如下: 配置项说明: NebulaClientOptions 配置 address,NebulaSource 需要配置 Nebula Graph Metad 服务的地址。 配置 username 配置 password VertexExecutionOptions 配置 GraphSpace 配置要读取的 tag 配置要读取的字段集 配置是否读取所有字段,默认为 false, 若配置为 true 则字段集配置无效 配置每次读取的数据量 limit,默认 2000 EdgeExecutionOptions 配置 GraphSpace 配置要读取的 edge 配置要读取的字段集 配置是否读取所有字段,默认为 false, 若配置为 true 则字段集配置无效 配置每次读取的数据量 limit,默认 2000 // 构造 Nebula Graph 客户端连接需要的参数 NebulaClientOptions nebulaClientOptions = new NebulaClientOptions .NebulaClientOptionsBuilder() .setAddress("127.0.0.1:45500") .build(); // 创建 connectionProvider NebulaConnectionProvider metaConnectionProvider = new NebulaMetaConnectionProvider(nebulaClientOptions); // 构造 Nebula Graph 数据读取需要的参数 List<String> cols = Arrays.asList("name", "age"); VertexExecutionOptions sourceExecutionOptions = new VertexExecutionOptions.ExecutionOptionBuilder() .setGraphSpace("flinkSource") .setTag(tag) .setFields(cols) .setLimit(100) .builder(); // 构造 NebulaInputFormat NebulaInputFormat inputFormat = new NebulaInputFormat(metaConnectionProvider) .setExecutionOptions(sourceExecutionOptions); // 方式 1 使用 createInput 方式注册 Nebula Graph 数据源 DataSource<Row> dataSource1 = ExecutionEnvironment.getExecutionEnvironment() .createInput(inputFormat); // 方式 2 使用 addSource 方式注册 Nebula Graph 数据源 NebulaSourceFunction sourceFunction = new NebulaSourceFunction(metaConnectionProvider) .setExecutionOptions(sourceExecutionOptions); DataStreamSource<Row> dataSource2 = StreamExecutionEnvironment.getExecutionEnvironment() .addSource(sourceFunction); Nebula Source Demo 编写完成后可以打包提交到 Flink 集群执行。 示例程序读取 Nebula Graph 的点数据并打印,该作业以 Nebula Graph 作为 Source,以 print 作为 Sink,执行结果如下: Source sent 数据为 59,671,064 条,Sink received 数据为 59,671,064 条。 二、Connector Sink Nebula Flink Connector 中的 Sink 即 Nebula Graph 图数据库。Flink 提供了丰富的 Connector 组件允许用户自定义数据池来接收 Flink 所处理的数据流。 2.1 Sink 简介 Sink 是 Flink 处理完 Source 后数据的输出,主要负责实时计算结果的输出和持久化。比如:将数据流写入标准输出、写入文件、写入 Sockets、写入外部系统等。 Flink 的 Sink 能力主要是通过调用数据流的 write 相关 API 和 DataStream.addSink 两种方式来实现数据流的外部存储。 类似于 Flink Connector 的 Source,Sink 也允许用户自定义来支持丰富的外部数据系统作为 Flink 的数据池。 Flink Sink 的使用方式如下: 本章主要介绍如何通过自定义 Sink 的方式实现 Nebula Graph Sink。 2.2 自定义 Sink 在 Flink 中可以使用 DataStream.addSink 和 DataStream.writeUsingOutputFormat 的方式将 Flink 数据流写入外部自定义数据池。 Flink 已经提供了若干实现好了的 Sink Functions ,也可以通过实现 SinkFunction 以及继承 RichOutputFormat 来实现自定义的 Sink。 2.3 自定义 Nebula Graph Sink 实现原理 Nebula Flink Connector 中实现了自定义的 NebulaSinkFunction,开发者通过调用 DataSource.addSink 方法并将 NebulaSinkFunction 对象作为参数传入即可实现将 Flink 数据流写入 Nebula Graph。 Nebula Flink Connector 使用的是 Flink 的 1.11-SNAPSHOT 版本,该版本中已经废弃了使用 writeUsingOutputFormat 方法来定义输出端的接口。 源码如下,所以请注意在使用自定义 Nebula Graph Sink 时请采用 DataStream.addSink 的方式。 /** @deprecated */ @Deprecated @PublicEvolving public DataStreamSink<T> writeUsingOutputFormat(OutputFormat<T> format) { return this.addSink(new OutputFormatSinkFunction(format)); } Nebula Graph Sink 实现类图如下: 其中最重要的两个类是 NebulaSinkFunction 和 NebulaBatchOutputFormat。 NebulaSinkFunction 继承自 AbstractRichFunction 并实现了以下方法: open调用 NebulaBatchOutputFormat 的 open 方法,进行资源准备。 close调用 NebulaBatchOutputFormat 的 close 方法,进行资源释放。 invoke是 Sink 中的核心方法, 调用 NebulaBatchOutputFormat 中的 write 方法进行数据写入。 flush调用 NebulaBatchOutputFormat 的 flush 方法进行数据的提交。 NebulaBatchOutputFormat 继承自 AbstractNebulaOutPutFormat,AbstractNebulaOutPutFormat 继承自 RichOutputFormat,主要实现的方法有: open准备图数据库 Nebula Graph 的 Graphd 服务的连接,并初始化数据写入执行器 nebulaBatchExecutor close提交最后批次数据,等待最后提交的回调结果并关闭服务连接等资源。 writeRecord核心方法,将数据写入 nebulaBufferedRow 中,并在达到配置的批量写入 Nebula Graph 上限时提交写入。Nebula Graph Sink 的写入操作是异步的,所以需要执行回调来获取执行结果。 flush当 bufferRow 存在数据时,将数据提交到 Nebula Graph 中。 在 AbstractNebulaOutputFormat 中调用了 NebulaBatchExecutor 进行数据的批量管理和批量提交,并通过定义回调函数接收批量提交的结果,代码如下: /** * write one record to buffer */ @Override public final synchronized void writeRecord(T row) throws IOException { nebulaBatchExecutor.addToBatch(row); if (numPendingRow.incrementAndGet() >= executionOptions.getBatch()) { commit(); } } /** * put record into buffer * * @param record represent vertex or edge */ void addToBatch(T record) { boolean isVertex = executionOptions.getDataType().isVertex(); NebulaOutputFormatConverter converter; if (isVertex) { converter = new NebulaRowVertexOutputFormatConverter((VertexExecutionOptions) executionOptions); } else { converter = new NebulaRowEdgeOutputFormatConverter((EdgeExecutionOptions) executionOptions); } String value = converter.createValue(record, executionOptions.getPolicy()); if (value == null) { return; } nebulaBufferedRow.putRow(value); } /** * commit batch insert statements */ private synchronized void commit() throws IOException { graphClient.switchSpace(executionOptions.getGraphSpace()); future = nebulaBatchExecutor.executeBatch(graphClient); // clear waiting rows numPendingRow.compareAndSet(executionOptions.getBatch(),0); } /** * execute the insert statement * * @param client Asynchronous graph client */ ListenableFuture executeBatch(AsyncGraphClientImpl client) { String propNames = String.join(NebulaConstant.COMMA, executionOptions.getFields()); String values = String.join(NebulaConstant.COMMA, nebulaBufferedRow.getRows()); // construct insert statement String exec = String.format(NebulaConstant.BATCH_INSERT_TEMPLATE, executionOptions.getDataType(), executionOptions.getLabel(), propNames, values); // execute insert statement ListenableFuture<Optional<Integer>> execResult = client.execute(exec); // define callback function Futures.addCallback(execResult, new FutureCallback<Optional<Integer>>() { @Override public void onSuccess(Optional<Integer> integerOptional) { if (integerOptional.isPresent()) { if (integerOptional.get() == ErrorCode.SUCCEEDED) { LOG.info("batch insert Succeed"); } else { LOG.error(String.format("batch insert Error: %d", integerOptional.get())); } } else { LOG.error("batch insert Error"); } } @Override public void onFailure(Throwable throwable) { LOG.error("batch insert Error"); } }); nebulaBufferedRow.clean(); return execResult; } 由于 Nebula Graph Sink 的写入是批量、异步的,所以在最后业务结束 close 资源之前需要将缓存中的批量数据提交且等待写入操作的完成,以防在写入提交之前提前把 Nebula Graph Client 关闭,代码如下: /** * commit the batch write operator before release connection */ @Override public final synchronized void close() throws IOException { if(numPendingRow.get() > 0){ commit(); } while(!future.isDone()){ try { Thread.sleep(10); } catch (InterruptedException e) { LOG.error("sleep interrupted, ", e); } } super.close(); } 2.4 自定义 Nebula Graph Sink 应用实践 Flink 将处理完成的数据 Sink 到 Nebula Graph 时,需要将 Flink 数据流进行 map 转换成 Nebula Graph Sink 可接收的数据格式。自定义 Nebula Graph Sink 的使用方式是通过 addSink 形式,将 NebulaSinkFunction 作为参数传给 addSink 方法来实现 Flink 数据流的写入。 NebulaClientOptions 配置 address,NebulaSource 需要配置 Nebula Graph Graphd 服务的地址。 配置 username 配置 password VertexExecutionOptions 配置 GraphSpace 配置要写入的 tag 配置要写入的字段集 配置写入的点 ID 所在 Flink 数据流 Row 中的索引 配置批量写入 Nebula Graph 的数量,默认 2000 EdgeExecutionOptions 配置 GraphSpace 配置要写入的 edge 配置要写入的字段集 配置写入的边 src-id 所在 Flink 数据流 Row 中的索引 配置写入的边 dst-id 所在 Flink 数据流 Row 中的索引 配置写入的边 rank 所在 Flink 数据流 Row 中的索引,不配则无 rank 配置批量写入 Nebula Graph 的数量,默认 2000 /// 构造 Nebula Graphd 客户端连接需要的参数 NebulaClientOptions nebulaClientOptions = new NebulaClientOptions .NebulaClientOptionsBuilder() .setAddress("127.0.0.1:3699") .build(); NebulaConnectionProvider graphConnectionProvider = new NebulaGraphConnectionProvider(nebulaClientOptions); // 构造 Nebula Graph 写入操作参数 List<String> cols = Arrays.asList("name", "age") ExecutionOptions sinkExecutionOptions = new VertexExecutionOptions.ExecutionOptionBuilder() .setGraphSpace("flinkSink") .setTag(tag) .setFields(cols) .setIdIndex(0) .setBatch(20) .builder(); // 写入 Nebula Graph dataSource.addSink(nebulaSinkFunction); Nebula Graph Sink 的 Demo 程序以 Nebula Graph 的 space:flinkSource 作为 Source 读取数据,进行 map 类型转换后 Sink 入 Nebula Graph 的 space:flinkSink,对应的应用场景为将 Nebula Graph 中一个 space 的数据流入另一个 space 中。 三、 Catalog Flink 1.11.0 之前,用户如果依赖 Flink 的 Source/Sink 读写外部数据源时,必须要手动读取对应数据系统的 Schema。比如,要读写 Nebula Graph,则必须先保证明确地知晓在 Nebula Graph 中的 Schema 信息。但是这样会有一个问题,当 Nebula Graph 中的 Schema 发生变化时,也需要手动更新对应的 Flink 任务以保持类型匹配,任何不匹配都会造成运行时报错使作业失败。这个操作冗余且繁琐,体验极差。 1.11.0 版本后,用户使用 Flink Connector 时可以自动获取表的 Schema。可以在不了解外部系统数据 Schema 的情况下进行数据匹配。 目前 Nebula Flink Connector 中已支持数据的读写,要实现 Schema 的匹配则需要为 Flink Connector 实现 Catalog 的管理。但为了确保 Nebula Graph 中数据的安全性,Nebula Flink Connector 只支持 Catalog 的读操作,不允许进行 Catalog 的修改和写入。 访问 Nebula Graph 指定类型的数据时,完整路径应该是以下格式:<graphSpace>.<VERTEX.tag> 或者 <graphSpace>.<EDGE.edge> 具体使用方式如下: String catalogName = "testCatalog"; String defaultSpace = "flinkSink"; String username = "root"; String password = "nebula"; String address = "127.0.0.1:45500"; String table = "VERTEX.player" // define Nebula catalog Catalog catalog = NebulaCatalogUtils.createNebulaCatalog(catalogName,defaultSpace, address, username, password); // define Flink table environment StreamExecutionEnvironment bsEnv = StreamExecutionEnvironment.getExecutionEnvironment(); tEnv = StreamTableEnvironment.create(bsEnv); // register customed nebula catalog tEnv.registerCatalog(catalogName, catalog); // use customed nebula catalog tEnv.useCatalog(catalogName); // show graph spaces of Nebula Graph String[] spaces = tEnv.listDatabases(); // show tags and edges of Nebula Graph tEnv.useDatabase(defaultSpace); String[] tables = tEnv.listTables(); // check tag player exist in defaultSpace ObjectPath path = new ObjectPath(defaultSpace, table); assert catalog.tableExists(path) == true // get nebula tag schema CatalogBaseTable table = catalog.getTable(new ObjectPath(defaultSpace, table)); table.getSchema(); Nebula Flink Connector 支持的其他 Catalog 接口请查看 GitHub 代码 NebulaCatalog.java。 四、 Exactly-once Flink Connector 的 Exactly-once 是指 Flink 借助于 checkpoint 机制保证每个输入事件只对最终结果影响一次,在数据处理过程中即使出现故障,也不会存在数据重复和丢失的情况。 为了提供端到端的 Exactly-once 语义,Flink 的外部数据系统也必须提供提交或回滚的方法,然后通过 Flink 的 checkpoint 机制协调。Flink 提供了实现端到端的 Exactly-once 的抽象,即实现二阶段提交的抽象类 TwoPhaseCommitSinkFunction。 想为数据输出端实现 Exactly-once,则需要实现四个函数: beginTransaction在事务开始前,在目标文件系统的临时目录创建一个临时文件,随后可以在数据处理时将数据写入此文件。 preCommit在预提交阶段,关闭文件不再写入。为下一个 checkpoint 的任何后续文件写入启动一个新事务。 commit在提交阶段,将预提交阶段的文件原子地移动到真正的目标目录。二阶段提交过程会增加输出数据可见性的延迟。 abort在终止阶段,删除临时文件。 根据上述函数可看出,Flink 的二阶段提交对外部数据源有要求,即 Source 数据源必须具备重发功能,Sink 数据池必须支持事务提交和幂等写。 Nebula Graph v1.1.0 虽然不支持事务,但其写入操作是幂等的,即同一条数据的多次写入结果是一致的。因此可以通过 checkpoint 机制实现 Nebula Flink Connector 的 At-least-Once 机制,根据多次写入的幂等性可以间接实现 Sink 的 Exactly-once。 要使用 Nebula Graph Sink 的容错性,请确保在 Flink 的执行环境中开启了 checkpoint 配置: StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); env.enableCheckpointing(10000) // checkpoint every 10000 msecs .getCheckpointConfig() .setCheckpointingMode(CheckpointingMode.AT_LEAST_ONCE); Reference Nebula Source Demo [testNebulaSource]:https://github.com/vesoft-inc/nebula-java/blob/master/examples/src/main/java/org/apache/flink/FlinkDemo.java Nebula Sink Demo [testSourceSink]:https://github.com/vesoft-inc/nebula-java/blob/master/examples/src/main/java/org/apache/flink/FlinkDemo.java Apache Flink 源码:https://github.com/apache/flink ApacheFlink 零基础入门:https://www.infoq.cn/theme/28 Flink 文档:https://flink.apache.org/flink-architecture.html Flink 实践文档:https://ci.apache.org/projects/flink/flink-docs-release-1.12/ flink-connector-jdbc 源码:https://github.com/apache/flink/tree/master/flink-connectors/flink-connector-jdbc Flink JDBC Catalog 详解:https://cloud.tencent.com/developer/article/1697913 喜欢这篇文章?来来来,给我们的 GitHub 点个 star 表鼓励啦~~ ♂️♀️ [手动跪谢] 交流图数据库技术?交个朋友,Nebula Graph 官方小助手微信:NebulaGraphbot 拉你进交流群~~
摘要:Nebula Operator 是 Nebula Graph 在 Kubernetes 系统上的自动化部署运维插件。在本文,你将了解到 Nebula Operator 的特性及它的工作原理。 从 Nebula Graph 的架构谈起 Nebula Graph 是一个高性能的分布式开源图数据库,从架构上可以看出,一个完整的 Nebula Graph 集群由三类服务组成,即 Meta Service, Query Service(Computation Layer)和 Storage Service(Storage Layer)。 每类服务都是一个由多副本组件组成的集群,在 Nebula Operator 中,我们分别称这三类组件为: Metad / Graphd / Storaged。 Metad:主要负责提供和存储图数据库的元数据,并承担集群中调度器的角色,指挥存储扩容和数据迁移,leader 变更等运维操作。 Graphd:主要负责处理 Nebula 查询语言语句(nGQL),每个 Graphd 都运行着一个无状态的查询计算引擎,且彼此间无任何通信关系。计算引擎仅从 Metad 集群中读取元信息,并和 Storaged 集群进行交互。同时,它也负责不同客户端的接入和交互。 Storaged:主要负责 Graph 数据存储。图数据被切分成很多的分片 Partition,相同 ID 的 Partition 组成一个 Raft Group,实现多副本一致性。Nebula Graph 默认的存储引擎是 RocksDB 的 Key-Value 存储。 在了解了 Nebula Graph 核心组件的功能后,我们可以得出一些结论: Nebula Graph 在设计上采用了存储计算分离的架构,组件间分层清晰,职责明确,这意味着各个组件都可以根据自身的业务需求进行独立地弹性扩容、缩容,非常适合部署在 Kubernetes 这类容器编排系统上,充分发挥 Nebula Graph 集群的弹性扩缩能力。 Nebula Graph 是一个较为复杂的分布式系统,它的部署和运维操作需要比较深入的领域知识,这带来了颇高的学习成本和负担。即使是部署运行在 Kubernetes 系统之上,有状态应用的状态管理、异常处理等需求,原生的Kubernetes 控制器也不能很好的满足,导致 Nebula Graph 集群不能发挥出它最大的能力。 因此,为了充分发挥 Nebula Graph 原生具备的弹性扩缩、故障转移等能力,也为了降低对 Nebula Graph 集群的运维管理门槛,我们开发了 Nebula Operator。 Nebula Operator 是 Nebula Graph 在 Kubernetes 系统上的自动化部署运维插件,依托于 Kubernetes 自身优秀的扩展机制,我们把 Nebula Graph 运维领域的知识,以 CRD + Controller 的形式全面注入到 Kubernetes 系统中,让 Nebula Graph 成为真正的云原生图数据库。 为了能够更好的理解 Nebula Operator 的工作原理,让我们先回顾一下什么是 Operator 什么是 Nebula Operator Operator 并不是什么很新的概念,早在 2017 年,就有 CoreOS 公司推出了 Etcd Operator。Operator 的初衷是为了扩展 Kubernetes 功能,以更好的管理有状态应用,这得益于 Kubernetes 的两大核心概念:声明式 API 和控制循环(Control Loop)。 我们可以用一段伪代码来描述这一过程。 在集群中声明对象X的期望状态并创建X for { 实际状态 := 获取集群中对象 X 的实际状态 期望状态 := 获取集群中对象 X 的期望状态 if 实际状态 == 期望状态 { 什么都不做 } else { 执行事先规定好的编排动作,将实际状态调协为期望状态 } } 在 Kubernetes 系统内,每一种内置资源对象,都运行着一个特定的控制循环,将它的实际状态通过事先规定好的编排动作,逐步调整为最终的期望状态。 对于 Kubernetes 系统内不存在的资源类型,我们可以通过添加自定义 API 对象的方式注册。常见的方法是使用 CustomResourceDefinition(CRD)和 Aggregation ApiServer(AA)。Nebula Operator 就使用 CRD 注册了一个 "Nebula Cluster" 资源,和一个 "Advanced Statefulset" 资源。 在注册了上述自定义资源之后,我们就可以通过编写自定义控制器的方式来感知自定义资源的状态变化,并按照我们编写的策略和逻辑去自动地运维 Nebula Graph,让集群的实际状态朝着期望状态趋近。这也是 Nebula Operator 降低用户运维门槛的核心原理。 apiVersion: nebula.com/v1alpha1 kind: NebulaCluster metadata: name: nebulaclusters namespace: default spec: graphd: replicas: 1 baseImage: vesoft/nebula-graphd imageVersion: v2-preview-nightly service: type: NodePort externalTrafficPolicy: Cluster storageClaim: storageClassName: fast-disks metad: replicas: 3 baseImage: vesoft/nebula-metad imageVersion: v2-preview-nightly storageClaim: storageClassName: fast-disks storaged: replicas: 3 baseImage: vesoft/nebula-storaged imageVersion: v2-preview-nightly storageClaim: storageClassName: fast-disks schedulerName: nebula-scheduler imagePullPolicy: Always 我们在这里展示了一个简单的 Nebula Cluster 实例,如果你想要扩展 Storaged 的副本数量至 10,你只需要简单修改 .spec.storaged.replicas 参数为 10,剩下的运维操作则由 Nebula Operator 通过控制循环来完成。 至此,想必你已经对 Nebula Graph 和 Operator 有了一个初步的认知,接下来,让我们来列举目前 Nebula Operator 已经具备了哪些能力,让你能更加深刻的体会到使用 Nebula Operator 带来的一些实际好处。 部署、卸载:我们将一整个 Nebula Graph 集群描述成一个 CRD 注册进 ApiServer 中,用户只需提供对应的 CR 文件,Operator 就能快速拉起或者删除一个对应的 Nebula Graph 集群,简化了用户部署、卸载集群的过程。 扩容、缩容:通过在控制循环中调用 Nebula Graph 原生提供的扩缩容接口,我们为 Nebula Operator 封装实现了扩缩容的逻辑,可以通过 yaml 配置进行简单的扩容,缩容,且保证数据的稳定性。 原地升级:我们在 Kubernetes 原生提供的 StatefulSet 基础上为其扩展了镜像原地替换的能力,它节省了 Pod 调度的耗时,并且在升级时,Pod 的位置、资源都不发生变化,极大提高了升级时集群的稳定性和确定性。 故障迁移:Nebula Operator 会内部调用 Nebula Graph 集群提供的接口,动态的感知服务是否正常运行,一旦发现异常,会自动的去做故障迁移操作,并根据错误类型配有对应的容错机制。 WebHook:一个标准的 Nebula Graph 最少需要三个 Metad 副本,如果用户错误地修改了此参数,可能会导致集群不可用,我们会通过 WebHook 的准入控制来检查一些必要参数是否设置正确,并通过变更控制来强制修改一些错误的声明,使集群始终能够稳定运行。 参考资料 Nebula Graph:https://github.com/vesoft-inc/nebula 作者有话说:Hi,我是刘鑫超,图数据库 Nebula Graph 的研发工程师,如果你对此文有疑问,欢迎来我们的 Nebula Graph 论坛交流下心得~~
不同来源的异构数据间存在着千丝万缕的关联,这种数据之间隐藏的关联关系和网络结构特性对于数据分析至关重要,图计算就是以图作为数据模型来表达问题并予以解决的过程。 一、背景 随着网络信息技术的飞速发展,数据逐渐向多源异构化方向发展,且不同来源的异构数据之间也存在的千丝万缕的关联,这种数据之间隐藏的关联关系和网络结构特性对于数据分析至关重要。但传统关系型数据库在分析大规模数据关联特性时存在性能缺陷、表达有限等问题,因此有着更强大表达能力的图数据受到业界极大重视,图计算就是以图作为数据模型来表达问题并予以解决的过程。图可以融合多源多类型的数据,除了可以展示数据静态基础特性之外,还可通过图计算展示隐藏在数据之间的图结构特性和点对关联关系,成为社交网络、推荐系统、知识图谱、金融风控、网络安全、文本检索等领域重要的分析手段。 二、算法应用 为了支撑大规模图计算的业务需求,Nebula Graph 基于 GraphX 提供了 PageRank 和 Louvain 社区发现的图计算算法,允许用户通过提交 Spark 任务的形式执行算法应用。此外,用户也可以通过 Spark Connector 编写 Spark 程序调用 GraphX 自带的其他图算法,如 LabelPropagation、ConnectedComponent 等。 PageRank PageRank 是谷歌提出的用于解决链接分析中网页排名问题的算法,目的是为了对互联网中数以亿计的网页进行排名。 PageRank 简介 美国斯坦福大学的 Larry Page 和 Sergey Brin 在研究网页排序问题时采用学术界评判论文重要性的方法,即看论文的引用量以及引用该论文的论文质量,对应于网页的重要性有两个假设: 数量假设:如果一个网页 A 被很多其他网页链接到,则该网页比较重要; 质量假设:如果一个很重要的网页链接到网页 A,则该网页的重要性会被提高。 并基于这两个假设提出 PageRank 算法。 PageRank 应用场景 社交应用的相似度内容推荐 在对微博、微信等社交平台进行社交网络分析时,可以基于 PageRank 算法根据用户通常浏览的信息以及停留时间实现基于用户的相似度的内容推荐; 分析用户社交影响力 在社交网络分析时根据用户的 PageRank 值进行用户影响力分析; 文献重要性研究 根据文献的 PageRank 值评判该文献的质量,PageRank 算法就是基于评判文献质量的想法来实现设计。 此外 PageRank 在数据分析和挖掘中也有很多的应用。 算法思路 GraphX 的 PageRank 算法是基于 Pregel 计算模型的,该算法流程包括 3 步骤: 为图中每个节点(网页)设置一个同样的初始 PageRank 值; 第一次迭代:沿边发送消息,每个节点收到所有关联边上对点的信息,得到一个新的 PageRank 值; 第二次迭代:用这组新的 PageRank 按不同算法模式对应的公式形成节点自己新的 PageRank。 Louvain 社区发现 Louvain 是用来进行社会网络挖掘的社区发现算法,属于图的聚类算法。 Louvain 算法介绍 Louvain 是基于模块度(Modularity)的社区发现算法,通过模块度来衡量一个社区的紧密程度。如果一个节点加入到某一社区中会使得该社区的模块度相比其他社区有最大程度的增加,则该节点就应当属于该社区。如果加入其它社区后没有使其模块度增加,则留在自己当前社区中。 模块度 模块度公式 模块度 Q 的物理意义:社区内节点的连边数与随机情况下的边数之差,定义函数如下: 其中 :节点 i 和节点 j 之间边的权重:所有与节点 i 相连的边的权重之和:节点 i 所属的社区: 图中所有边的权重之和 模块度公式变形 在此公式中,只有节点 i 和节点 j 属于同一社区,公式才有意义,所以该公式是衡量的某一社区内的紧密度。对于该公式的简化变形如下: 表示: 社区 c 内的边的权重之和 表示: 所有与社区 c 内节点相连的边的权重之和(因为 i 属于社区 c)包括社区内节点与节点 i 的边和社区外节点与节点 i 的边。 表示: 所有与社区 c 内节点相连的边的权重之和(因为 j 属于社区 c)包括社区内节点与节点 j 的边和社区外节点与节点 j 的边。 代替 和 。(即社区 c 内边权重和 + 社区 c 与其他社区连边的权重和) 求解模块度变化 在 Louvain 算法中不需要求每个社区具体的模块度,只需要比较社区中加入某个节点之后的模块度变化,所以需要求解 △Q。 将节点 i 分配到某一社区中,社区的模块度变化为: 其中 : 社区内所有节点与节点 i 连边权重之和(对应新社区的实际内部权重和乘以 2,因为 对于社区内所有的顶点 i,每条边其实被计算了两次): 所有与节点 i 相连的边的权重之和故实现算法时只需求 即可。 Louvain 应用场景 金融风控 在金融风控场景中,可以根据用户行为特征进行团伙识别; 社交网络 可以基于网络关系中点对之间关联的广度和强度进行社交网络划分;对复杂网络分析、电话网络分析人群之间的联系密切度; 推荐系统 基于用户兴趣爱好的社区发现,可以根据社区并结合协同过滤等推荐算法进行更精确有效的个性化推荐。 Louvain 算法思路 Louvain 算法包括两个阶段,其流程就是这两个阶段的迭代过程。 阶段一:不断地遍历网络图中的节点,通过比较节点给每个邻居社区带来的模块度的变化,将单个节点加入到能够使 Modularity 模块度有最大增量的社区中。(比如节点 v 分别加入到社区 A、B、C 中,使得三个社区的模块度增量为-1, 1, 2, 则节点 v 最终应该加入到社区 C 中) 阶段二:对第一阶段进行处理,将属于同一社区的顶点合并为一个大的超点重新构造网络图,即一个社区作为图的一个新的节点。此时两个超点之间边的权重是两个超点内所有原始顶点之间相连的边权重之和,即两个社区之间的边权重之和。 下面是对第一二阶段的实例介绍。 第一阶段遍历图中节点加入到其所属社区中,得到中间的图,形成四个社区; 第二节点对社区内的节点进行合并成一个超级节点,社区节点有自连边,其权重为社区内部所有节点间相连的边的权重之和的 2 倍,社区之间的边为两个社区间顶点跨社区相连的边的权重之和,如红色社区和浅绿色社区之间通过(8,11)、(10,11)、(10,13)相连,所以两个社区之间边的权重为 3。 注:社区内的权重为所有内部结点之间边权重的两倍,因为 Kin 的概念是社区内所有节点与节点 i 的连边和,在计算某一社区的 Kin 时,实际上每条边都被其两端的顶点计算了一次,一共被计算了两次。 整个 Louvain 算法就是不断迭代第一阶段和第二阶段,直到算法稳定(图的模块度不再变化)或者到达最大迭代次数。 三、算法实践 演示环境 三台虚拟机,环境如下: Cpu name:Intel(R) Xeon(R) Platinum 8260M CPU @ 2.30GHz Processors:32 CPU Cores:16 Memory Size:128G 软件环境 Spark:spark-2.4.6-bin-hadoop2.7 三个节点集群 yarn V2.10.0:三个节点集群 Nebula Graph V1.1.0:分布式部署,默认配置 测试数据 创建图空间 CREATE SPACE algoTest(partition_num=100, replica_factor=1); 创建点边 Schema CREATE TAG PERSON() CREATE EDGE FRIEND(likeness double); 导入数据 利用 Exchange 工具将数据离线导入 Nebula Graph。 测试结果 Spark 任务的资源分配为 --driver-memory=20G --executor-memory=100G --executor-cores=3 PageRank 在一亿数据集上的执行时间为 21min(PageRank 算法执行时间) Louvain 在一亿数据集上的执行时间为 1.3h(Reader + Louvain 算法执行时间) 如何使用 Nebula Graph 的算法 下载 nebula-algorithm 项目并打成 jar 包 $ git clone git@github.com:vesoft-inc/nebula-java.git $ cd nebula-java/tools/nebula-algorithm $ mvn package -DskipTests 配置项目中的 src/main/resources/application.conf { # Spark relation config spark: { app: { # not required, default name is the algorithm that you are going to execute. name: PageRank # not required partitionNum: 12 } master: local # not required conf: { driver-memory: 8g executor-memory: 8g executor-cores: 1g cores-max:6 } } # Nebula Graph relation config nebula: { # metadata server address addresses: "127.0.0.1:45500" user: root pswd: nebula space: algoTest # partition specified while creating nebula space, if you didn't specified the partition, then it's 100. partitionNumber: 100 # nebula edge type labels: ["FRIEND"] hasWeight: true # if hasWeight is true,then weightCols is required, and weghtCols' order must be corresponding with labels. # Noted: the graph algorithm only supports isomorphic graphs, # so the data type of each col in weightCols must be consistent and all numeric types. weightCols: [“likeness”] } algorithm: { # the algorithm that you are going to execute,pick one from [pagerank, louvain] executeAlgo: louvain # algorithm result path path: /tmp # pagerank parameter pagerank: { maxIter: 20 resetProb: 0.15 # default 0.15 } # louvain parameter louvain: { maxIter: 20 internalIter: 10 tol: 0.5 } } } 确保用户环境已安装 Spark 并启动 Spark 服务 提交 nebula-algorithm 应用程序: spark-submit --master xxx --class com.vesoft.nebula.tools.algorithm.Main /your-jar-path/nebula-algorithm-1.0.1.jar -p /your-application.conf-path/application.conf 如果你对上述内容感兴趣,欢迎用 nebula-algorithm 试试^^ References Nebula Graph:https://github.com/vesoft-inc/nebula GraphX:https://github.com/apache/spark/tree/master/graphx Spark-connector:https://github.com/vesoft-inc/nebula-java/tree/master/tools/nebula-spark Exchange:https://github.com/vesoft-inc/nebula-java/blob/master/doc/tools/exchange/ex-ug-toc.md nebula-algorithm:https://github.com/vesoft-inc/nebula-java/tree/master/tools/nebula-algorithm 作者有话说:Hi,我是安祺,Nebula Graph 研发工程师,如果你对本文有任何疑问,欢迎来论坛和我交流:https://discuss.nebula-graph.com.cn/ 推荐阅读 用图机器学习探索 A 股个股相关性变化 用 NetworkX + Gephi + Nebula Graph 分析<权力的游戏>人物关系(上篇)
首发于官方博客:https://nebula-graph.com.cn/posts/debug-nebula-graph-processes-docker/ 摘要:本文以 Nebula Graph 进程为例,讲解如何不破坏原有容器的内容,也不用在其中安装任何的工具包前提下,像在本地一样来调试进程 需求 在开发或者测试过程中,我们经常会用到 vesoft-inc/nebula-docker-compose 这个 repo 下的部署方式,因为当初为了尽可能的压缩每个 Nebula Graph 服务的 docker 镜像的体积,所以开发过程中常用的一切工具都没有安装,甚至连编辑器 VIM 都没有。 这给我们在容器内部定位问题带来一定的难度,因为每次只能去 install 一些工具包,才能开展接下来的工作,甚是费事。其实调试容器内部的进程还有另外一种方式,不需要破坏原有容器的内容,也不用在其中安装任何的工具包就能像在本地一样来调试。 这种技术在 k8s 环境下其实已经挺常用,就是 sidecar 模式。原理也比较朴素就是再起一个容器然后让这个容器跟你要调试的容器共享相同的 pid/network 的 namespace。这样原容器中的进程和网络空间在调试容器中就能“一览无余”,而在调试容器中安装了你想要的一切顺手工具,接下来的舞台就是留于你发挥了。 演示 接下来我就演示一下如何操作: 我们先用上述的 docker-compose 方式在本地部署一套 Nebula Graph 集群,教程见 repo 中的 README。部署好后的结果如下: $ docker-compose up -d Creating network "nebula-docker-compose_nebula-net" with the default driver Creating nebula-docker-compose_metad1_1 ... done Creating nebula-docker-compose_metad2_1 ... done Creating nebula-docker-compose_metad0_1 ... done Creating nebula-docker-compose_storaged2_1 ... done Creating nebula-docker-compose_storaged1_1 ... done Creating nebula-docker-compose_storaged0_1 ... done Creating nebula-docker-compose_graphd_1 ... done $ docker-compose ps Name Command State Ports ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- nebula-docker-compose_graphd_1 ./bin/nebula-graphd --flag ... Up (health: starting) 0.0.0.0:32907->13000/tcp, 0.0.0.0:32906->13002/tcp, 0.0.0.0:3699->3699/tcp nebula-docker-compose_metad0_1 ./bin/nebula-metad --flagf ... Up (health: starting) 0.0.0.0:32898->11000/tcp, 0.0.0.0:32896->11002/tcp, 45500/tcp, 45501/tcp nebula-docker-compose_metad1_1 ./bin/nebula-metad --flagf ... Up (health: starting) 0.0.0.0:32895->11000/tcp, 0.0.0.0:32894->11002/tcp, 45500/tcp, 45501/tcp nebula-docker-compose_metad2_1 ./bin/nebula-metad --flagf ... Up (health: starting) 0.0.0.0:32899->11000/tcp, 0.0.0.0:32897->11002/tcp, 45500/tcp, 45501/tcp nebula-docker-compose_storaged0_1 ./bin/nebula-storaged --fl ... Up (health: starting) 0.0.0.0:32901->12000/tcp, 0.0.0.0:32900->12002/tcp, 44500/tcp, 44501/tcp nebula-docker-compose_storaged1_1 ./bin/nebula-storaged --fl ... Up (health: starting) 0.0.0.0:32903->12000/tcp, 0.0.0.0:32902->12002/tcp, 44500/tcp, 44501/tcp nebula-docker-compose_storaged2_1 ./bin/nebula-storaged --fl ... Up (health: starting) 0.0.0.0:32905->12000/tcp, 0.0.0.0:32904->12002/tcp, 44500/tcp, 44501/tcp 这时我们分两个场景来演示,一个是进程空间,一个是网络空间。首先我们要先有一个顺手的调试镜像,我们就不自己构建了,从 docker hub 中找个已经打包好的用作演示,后期觉得不够用,我们可以维护一份 nebula-debug 的镜像,安装我们想要的所有调试工具,此处先借用社区内的方案 nicolaka/netshoot。我们先把镜像拉取到本地: $ docker pull nicolaka/netshoot $ docker images REPOSITORY TAG IMAGE ID CREATED SIZE vesoft/nebula-graphd nightly c67fe54665b7 36 hours ago 282MB vesoft/nebula-storaged nightly 5c77dbcdc507 36 hours ago 288MB vesoft/nebula-console nightly f3256c99eda1 36 hours ago 249MB vesoft/nebula-metad nightly 5a78d3e3008f 36 hours ago 288MB nicolaka/netshoot latest 6d7e8891c980 2 months ago 352MB 我们先看看直接执行这个镜像会是什么样: $ docker run --rm -ti nicolaka/netshoot bash bash-5.0# ps PID USER TIME COMMAND 1 root 0:00 bash 8 root 0:00 ps bash-5.0# 上面显示这个容器看不到任何 Nebula Graph 服务进程的内容,那么我们给其加点参数再看看: $ docker run --rm -ti --pid container:nebula-docker-compose_metad0_1 --cap-add sys_admin nicolaka/netshoot bash bash-5.0# ps PID USER TIME COMMAND 1 root 0:03 ./bin/nebula-metad --flagfile=./etc/nebula-metad.conf --daemonize=false --meta_server_addrs=172.28.1.1:45500,172.28.1.2:45500,172.28.1.3:45500 --local_ip=172.28.1.1 --ws_ip=172.28.1.1 --port=45500 --data_path=/data/meta --log_dir=/logs --v=15 --minloglevel=0 452 root 0:00 bash 459 root 0:00 ps bash-5.0# ls -al /proc/1/net/ total 0 dr-xr-xr-x 6 root root 0 Sep 18 07:17 . dr-xr-xr-x 9 root root 0 Sep 18 06:55 .. -r--r--r-- 1 root root 0 Sep 18 07:18 anycast6 -r--r--r-- 1 root root 0 Sep 18 07:18 arp dr-xr-xr-x 2 root root 0 Sep 18 07:18 bonding -r--r--r-- 1 root root 0 Sep 18 07:18 dev ... -r--r--r-- 1 root root 0 Sep 18 07:18 sockstat -r--r--r-- 1 root root 0 Sep 18 07:18 sockstat6 -r--r--r-- 1 root root 0 Sep 18 07:18 softnet_stat dr-xr-xr-x 2 root root 0 Sep 18 07:18 stat -r--r--r-- 1 root root 0 Sep 18 07:18 tcp -r--r--r-- 1 root root 0 Sep 18 07:18 tcp6 -r--r--r-- 1 root root 0 Sep 18 07:18 udp -r--r--r-- 1 root root 0 Sep 18 07:18 udp6 -r--r--r-- 1 root root 0 Sep 18 07:18 udplite -r--r--r-- 1 root root 0 Sep 18 07:18 udplite6 -r--r--r-- 1 root root 0 Sep 18 07:18 unix -r--r--r-- 1 root root 0 Sep 18 07:18 xfrm_stat 这次有点不一样了,我们看到 metad0 的进程了,并且其 pid 还是 1。看到这个进程再想对其做点啥就好办了,比如能不能直接在 gdb 中 attach 它,由于手边没有带 nebula binary 的对应 image,就留给大家私下探索吧。 我们已经看到 pid 空间通过指定 --pid container:<container_name|id> 可以共享了,那么我们接下来看看网络的情况,毕竟有时候需要抓个包,执行如下的命令: bash-5.0# netstat -tulpn Active Internet connections (only servers) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name 啥也没有,跟预想的有点不一样,我们有 metad0 这个进程不可能一个连接都没有。要想看到这个容器内的网络空间还要再加点参数,像如下方式再启动调试容器: $ docker run --rm -ti --pid container:nebula-docker-compose_metad0_1 --network container:nebula-docker-compose_metad0_1 --cap-add sys_admin nicolaka/netshoot bash bash-5.0# netstat -tulpn Active Internet connections (only servers) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name tcp 0 0 172.28.1.1:11000 0.0.0.0:* LISTEN - tcp 0 0 172.28.1.1:11002 0.0.0.0:* LISTEN - tcp 0 0 0.0.0.0:45500 0.0.0.0:* LISTEN - tcp 0 0 0.0.0.0:45501 0.0.0.0:* LISTEN - tcp 0 0 127.0.0.11:33249 0.0.0.0:* LISTEN - udp 0 0 127.0.0.11:51929 0.0.0.0:* - 这回就跟上面的输出不一样了,加了 --network container:nebula-docker-compose_metad0_1 运行参数后,metad0 容器内的连接情况也能看到了,那么想抓包调试就都可以了。 总结 通过运行另外一个容器,并让其跟想要调试的容器共享 pid/network namespace 是我们能像本地调试的关键。社区里甚至还有人基于上述想法开发了一些小工具进一步方便使用: Docker-debug 推荐阅读 使用 Docker 构建 Nebula Graph 源码
本文由美团 NLP 团队高辰、赵登昌撰写首发于 Nebula Graph 官方论坛:https://discuss.nebula-graph.com.cn/t/topic/1377 1. 前言 近年来,深度学习和知识图谱技术发展迅速,相比于深度学习的“黑盒子”,知识图谱具有很强的可解释性,在搜索推荐、智能助理、金融风控等场景中有着广泛的应用。美团基于积累的海量业务数据,结合使用场景进行充分地挖掘关联,逐步建立起包括美食图谱、旅游图谱、商品图谱在内的近十个领域知识图谱,并在多业务场景落地,助力本地生活服务的智能化。 为了高效存储并检索图谱数据,相比传统关系型数据库,选择图数据库作为存储引擎,在多跳查询上具有明显的性能优势。当前业界知名的图数据库产品有数十款,选型一款能够满足美团实际业务需求的图数据库产品,是建设图存储和图学习平台的基础。我们结合业务现状,制定了选型的基本条件: 开源项目,对商业应用友好 拥有对源代码的控制力,才能保证数据安全和服务可用性。 支持集群模式,具备存储和计算的横向扩展能力 美团图谱业务数据量可以达到千亿以上点边总数,吞吐量可达到数万 qps,单节点部署无法满足存储需求。 能够服务 OLTP 场景,具备毫秒级多跳查询能力 美团搜索场景下,为确保用户搜索体验,各链路的超时时间具有严格限制,不能接受秒级以上的查询响应时间。 具备批量导入数据能力 图谱数据一般存储在 Hive 等数据仓库中。必须有快速将数据导入到图存储的手段,服务的时效性才能得到保证。 我们试用了 DB-Engines 网站上排名前 30 的图数据库产品,发现多数知名的图数据库开源版本只支持单节点,不能横向扩展存储,无法满足大规模图谱数据的存储需求,例如:Neo4j、ArangoDB、Virtuoso、TigerGraph、RedisGraph。经过调研比较,最终纳入评测范围的产品为:NebulaGraph(原阿里巴巴团队创业开发)、Dgraph(原 Google 团队创业开发)、HugeGraph(百度团队开发)。 2. 测试概要 2.1 硬件配置 数据库实例:运行在不同物理机上的 Docker 容器。 单实例资源:32 核心,64GB 内存,1TB SSD 存储。【Intel(R) Xeon(R) Gold 5218 CPU @ 2.30GHz】 实例数量:3 2.2 部署方案 Nebula v1.0.1 Metad 负责管理集群元数据,Graphd 负责执行查询,Storaged 负责数据分片存储。存储后端采用 RocksDB。 |实例 1 | 实例 2 | 实例 3 ||-|-|-||Metad | Metad | Metad||Graphd | Graphd | Graphd||Storaged[RocksDB] | Storaged[RocksDB] | Storaged[RocksDB]| Dgraph v20.07.0 Zero 负责管理集群元数据,Alpha 负责执行查询和存储。存储后端为 Dgraph 自有实现。 |实例 1 | 实例 2 | 实例 3 ||-|-|-||Zero | Zero | Zero||Alpha | Alpha | Alpha| HugeGraph v0.10.4 HugeServer 负责管理集群元数据和查询。HugeGraph 虽然支持 RocksDB 后端,但不支持 RocksDB 后端的集群部署,因此存储后端采用 HBase。 |实例1 | 实例2 | 实例3 ||-|-|-||HugeServer[HBase]|HugeServer[HBase]|HugeServer[HBase]||JournalNode | JournalNode | JournalNode||DataNode | DataNode | DataNode||NodeManager | NodeManager | NodeManager||RegionServer | RegionServer | RegionServer||ZooKeeper | ZooKeeper | ZooKeeper||NameNode | NameNode[Backup] | -|| -|ResourceManager | ResourceManager[Backup]||HBase Master | HBase Master[Backup] |-| 3. 评测数据集 社交图谱数据集:https://github.com/ldbc011 生成参数:branch=stable, version=0.3.3, scale=1000 实体情况:4 类实体,总数 26 亿 关系情况:19 类关系,总数 177 亿 数据格式:csv GZip 压缩后大小:194 G 4. 测试结果 4.1 批量数据导入 4.1.1 测试说明 批量导入的步骤为:Hive 仓库底层 csv 文件 -> 图数据库支持的中间文件 -> 图数据库。各图数据库具体导入方式如下: Nebula:执行 Spark 任务,从数仓生成 RocksDB 的底层存储 sst 文件,然后执行 sst Ingest 操作插入数据。 Dgraph:执行 Spark 任务,从数仓生成三元组 rdf 文件,然后执行 bulk load 操作直接生成各节点的持久化文件。 HugeGraph:支持直接从数仓的 csv 文件导入数据,因此不需要数仓-中间文件的步骤。通过 loader 批量插入数据。 4.1.2 测试结果 4.1.3 数据分析 Nebula:数据存储分布方式是主键哈希,各节点存储分布基本均衡。导入速度最快,存储放大比最优。 Dgraph:原始 194G 数据在内存 392G 的机器上执行导入命令,8.7h 后 OOM 退出,无法导入全量数据。数据存储分布方式是三元组谓词,同一种关系只能保存在一个数据节点上,导致存储和计算严重偏斜。 HugeGraph:原始 194G 的数据执行导入命令,写满了一个节点 1,000G 的磁盘,造成导入失败,无法导入全量数据。存储放大比最差,同时存在严重的数据偏斜。 4.2 实时数据写入 4.2.1 测试说明 向图数据库插入点和边,测试实时写入和并发能力。 响应时间:固定的 50,000 条数据,以固定 qps 发出写请求,全部发送完毕即结束。取客户端从发出请求到收到响应的 Avg、p99、p999 耗时。 最大吞吐量:固定的 1,000,000 条数据,以递增 qps 发出写请求,Query 循环使用。取 1 分钟内成功请求的峰值 qps 为最大吞吐量。 插入点 Nebula INSERT VERTEX t_rich_node (creation_date, first_name, last_name, gender, birthday, location_ip, browser_used) VALUES ${mid}:('2012-07-18T01:16:17.119+0000', 'Rodrigo', 'Silva', 'female', '1984-10-11', '84.194.222.86', 'Firefox') Dgraph { set { <${mid}> <creation_date> "2012-07-18T01:16:17.119+0000" . <${mid}> <first_name> "Rodrigo" . <${mid}> <last_name> "Silva" . <${mid}> <gender> "female" . <${mid}> <birthday> "1984-10-11" . <${mid}> <location_ip> "84.194.222.86" . <${mid}> <browser_used> "Firefox" . } } HugeGraph g.addVertex(T.label, "t_rich_node", T.id, ${mid}, "creation_date", "2012-07-18T01:16:17.119+0000", "first_name", "Rodrigo", "last_name", "Silva", "gender", "female", "birthday", "1984-10-11", "location_ip", "84.194.222.86", "browser_used", "Firefox") 插入边 Nebula INSERT EDGE t_edge () VALUES ${mid1}->${mid2}:(); Dgraph { set { <${mid1}> <link> <${mid2}> . } } HugeGraph g.V(${mid1}).as('src').V(${mid2}).addE('t_edge').from('src') 4.2.2 测试结果 实时写入 4.2.3 数据分析 Nebula:如 4.1.3 节分析所述,Nebula 的写入请求可以由多个存储节点分担,因此响应时间和吞吐量均大幅领先。 Dgraph:如 4.1.3 节分析所述,同一种关系只能保存在一个数据节点上,吞吐量较差。 HugeGraph:由于存储后端基于 HBase,实时并发读写能力低于 RocksDB(Nebula)和 BadgerDB(Dgraph),因此性能最差。 4.3 数据查询 4.3.1 测试说明 以常见的 N 跳查询返回 ID,N 跳查询返回属性,共同好友查询请求测试图数据库的读性能。 响应时间:固定的 50,000 条查询,以固定 qps 发出读请求,全部发送完毕即结束。取客户端从发出请求到收到响应的 Avg、p99、p999 耗时。 60s 内未返回结果为超时。 最大吞吐量:固定的 1,000,000 条查询,以递增 qps 发出读请求,Query 循环使用。取 1 分钟内成功请求的峰值 qps 为最大吞吐量。 缓存配置:参与测试的图数据库都具备读缓存机制,默认打开。每次测试前均重启服务清空缓存。 N 跳查询返回 ID Nebula GO ${n} STEPS FROM ${mid} OVER person_knows_person Dgraph { q(func:uid(${mid})) { uid person_knows_person { #${n}跳数 = 嵌套层数 uid } } } HugeGraph g.V(${mid}).out().id() #${n}跳数 = out()链长度 N 跳查询返回属性 Nebula GO ${n} STEPS FROM ${mid} OVER person_knows_person YIELDperson_knows_person.creation_date, $$.person.first_name, $$.person.last_name, $$.person.gender, $$.person.birthday, $$.person.location_ip, $$.person.browser_used Dgraph { q(func:uid(${mid})) { uid first_name last_name gender birthday location_ip browser_used person_knows_person { #${n}跳数 = 嵌套层数 uid first_name last_name gender birthday location_ip browser_used } } } HugeGraph g.V(${mid}).out() #${n}跳数 = out()链长度 共同好友查询语句 Nebula GO FROM ${mid1} OVER person_knows_person INTERSECT GO FROM ${mid2} OVER person_knows_person Dgraph { var(func: uid(${mid1})) { person_knows_person { M1 as uid } } var(func: uid(${mid2})) { person_knows_person { M2 as uid } } in_common(func: uid(M1)) @filter(uid(M2)){ uid } } HugeGraph g.V(${mid1}).out().id().aggregate('x').V(${mid2}).out().id().where(within('x')).dedup() 4.3.2 测试结果 N 跳查询返回 ID N 跳查询返回属性 单个返回节点的属性平均大小为 200 Bytes。 共同好友本项未测试最大吞吐量。 4.3.3 数据分析 在 1 跳查询返回 ID「响应时间」实验中,Nebula 和 DGraph 都只需要进行一次出边搜索。由于 DGraph 的存储特性,相同关系存储在单个节点,1 跳查询不需要网络通信。而 Nebula 的实体分布在多个节点中,因此在实验中 DGraph 响应时间表现略优于 Nebula。 在 1 跳查询返回 ID「最大吞吐量」实验中,DGraph 集群节点的 CPU 负载主要落在存储关系的单节点上,造成集群 CPU 利用率低下,因此最大吞吐量仅有 Nebula 的 11%。 在 2 跳查询返回 ID「响应时间」实验中,由于上述原因,DGraph 在 qps=100 时已经接近了集群负载能力上限,因此响应时间大幅变慢,是 Nebula 的 3.9 倍。 在 1 跳查询返回属性实验中,Nebula 由于将实体的所有属性作为一个数据结构存储在单节点上,因此只需要进行【出边总数 Y】次搜索。而 DGraph 将实体的所有属性也视为出边,并且分布在不同节点上,需要进行【属性数量 X * 出边总数 Y】次出边搜索,因此查询性能比 Nebula 差。多跳查询同理。 在共同好友实验中,由于此实验基本等价于 2 次 1 跳查询返回 ID,因此测试结果接近,不再详述。 由于 HugeGraph 存储后端基于 HBase,实时并发读写能力低于 RocksDB(Nebula)和 BadgerDB(Dgraph),因此在多项实验中性能表现均落后于 Nebula 和 DGraph。 5. 结论 参与测试的图数据库中,Nebula 的批量导入可用性、导入速度、实时数据写入性能、数据多跳查询性能均优于竞品,因此我们最终选择了 Nebula 作为图存储引擎。 6. 参考资料 NebulaGraph Benchmark:https://discuss.nebula-graph.com.cn/t/topic/782 NebulaGraph Benchmark 微信团队:https://discuss.nebula-graph.com.cn/t/topic/1013 DGraph Benchmark:https://dgraph.io/blog/tags/benchmark/ HugeGraph Benchmark:https://hugegraph.github.io/hugegraph-doc/performance/hugegraph-benchmark-0.5.6.html TigerGraph Benchmark:https://www.tigergraph.com/benchmark/ RedisGraph Benchmark:https://redislabs.com/blog/new-redisgraph-1-0-achieves-600x-faster-performance-graph-databases/ 本次性能测试系美团 NLP 团队高辰、赵登昌撰写,如果你对本文有任意疑问,欢迎来原贴和作者交流:https://discuss.nebula-graph.com.cn/t/topic/1377
本文作者系:视野金服工程师 | 吴海胜首发于 Nebula Graph 论坛:https://discuss.nebula-graph.com.cn/t/topic/1388 一、前言 本文介绍如何使用 Docker Swarm 来部署 Nebula Graph 集群,并部署客户端负载均衡和高可用。 二、nebula 集群搭建 2.1 环境准备 机器准备 | ip | 内存(Gb) | cpu(核数) || --- | --- | --- | | 192.168.1.166 | 16 | 4 | | 192.168.1.167 | 16 | 4 | | 192.168.1.168 | 16 | 4 | 在安装前确保所有机器已安装 Docker 2.2 初始化 swarm 集群 在 192.168.1.166 机器上执行 $ docker swarm init --advertise-addr 192.168.1.166 Swarm initialized: current node (dxn1zf6l61qsb1josjja83ngz) is now a manager. To add a worker to this swarm, run the following command: docker swarm join \ --token SWMTKN-1-49nj1cmql0jkz5s954yi3oex3nedyz0fb0xx14ie39trti4wxv-8vxv8rssmk743ojnwacrr2e7c \ 192.168.1.166:2377 To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions. 2.3 加入 worker 节点 根据 init 命令提示内容,加入 swarm worker 节点,在 192.168.1.167 192.168.1.168 分别执行 docker swarm join \ --token SWMTKN-1-49nj1cmql0jkz5s954yi3oex3nedyz0fb0xx14ie39trti4wxv-8vxv8rssmk743ojnwacrr2e7c \ 192.168.1.166:2377 2.4 验证集群 docker node ls ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION h0az2wzqetpwhl9ybu76yxaen * KF2-DATA-166 Ready Active Reachable 18.06.1-ce q6jripaolxsl7xqv3cmv5pxji KF2-DATA-167 Ready Active Leader 18.06.1-ce h1iql1uvm7123h3gon9so69dy KF2-DATA-168 Ready Active 18.06.1-ce 2.5 配置 docker stack vi docker-stack.yml 配置如下内容 version: '3.6' services: metad0: image: vesoft/nebula-metad:nightly env_file: - ./nebula.env command: - --meta_server_addrs=192.168.1.166:45500,192.168.1.167:45500,192.168.1.168:45500 - --local_ip=192.168.1.166 - --ws_ip=192.168.1.166 - --port=45500 - --data_path=/data/meta - --log_dir=/logs - --v=0 - --minloglevel=2 deploy: replicas: 1 restart_policy: condition: on-failure placement: constraints: - node.hostname == KF2-DATA-166 healthcheck: test: ["CMD", "curl", "-f", "http://192.168.1.166:11000/status"] interval: 30s timeout: 10s retries: 3 start_period: 20s ports: - target: 11000 published: 11000 protocol: tcp mode: host - target: 11002 published: 11002 protocol: tcp mode: host - target: 45500 published: 45500 protocol: tcp mode: host volumes: - data-metad0:/data/meta - logs-metad0:/logs networks: - nebula-net metad1: image: vesoft/nebula-metad:nightly env_file: - ./nebula.env command: - --meta_server_addrs=192.168.1.166:45500,192.168.1.167:45500,192.168.1.168:45500 - --local_ip=192.168.1.167 - --ws_ip=192.168.1.167 - --port=45500 - --data_path=/data/meta - --log_dir=/logs - --v=0 - --minloglevel=2 deploy: replicas: 1 restart_policy: condition: on-failure placement: constraints: - node.hostname == KF2-DATA-167 healthcheck: test: ["CMD", "curl", "-f", "http://192.168.1.167:11000/status"] interval: 30s timeout: 10s retries: 3 start_period: 20s ports: - target: 11000 published: 11000 protocol: tcp mode: host - target: 11002 published: 11002 protocol: tcp mode: host - target: 45500 published: 45500 protocol: tcp mode: host volumes: - data-metad1:/data/meta - logs-metad1:/logs networks: - nebula-net metad2: image: vesoft/nebula-metad:nightly env_file: - ./nebula.env command: - --meta_server_addrs=192.168.1.166:45500,192.168.1.167:45500,192.168.1.168:45500 - --local_ip=192.168.1.168 - --ws_ip=192.168.1.168 - --port=45500 - --data_path=/data/meta - --log_dir=/logs - --v=0 - --minloglevel=2 deploy: replicas: 1 restart_policy: condition: on-failure placement: constraints: - node.hostname == KF2-DATA-168 healthcheck: test: ["CMD", "curl", "-f", "http://192.168.1.168:11000/status"] interval: 30s timeout: 10s retries: 3 start_period: 20s ports: - target: 11000 published: 11000 protocol: tcp mode: host - target: 11002 published: 11002 protocol: tcp mode: host - target: 45500 published: 45500 protocol: tcp mode: host volumes: - data-metad2:/data/meta - logs-metad2:/logs networks: - nebula-net storaged0: image: vesoft/nebula-storaged:nightly env_file: - ./nebula.env command: - --meta_server_addrs=192.168.1.166:45500,192.168.1.167:45500,192.168.1.168:45500 - --local_ip=192.168.1.166 - --ws_ip=192.168.1.166 - --port=44500 - --data_path=/data/storage - --log_dir=/logs - --v=0 - --minloglevel=2 deploy: replicas: 1 restart_policy: condition: on-failure placement: constraints: - node.hostname == KF2-DATA-166 depends_on: - metad0 - metad1 - metad2 healthcheck: test: ["CMD", "curl", "-f", "http://192.168.1.166:12000/status"] interval: 30s timeout: 10s retries: 3 start_period: 20s ports: - target: 12000 published: 12000 protocol: tcp mode: host - target: 12002 published: 12002 protocol: tcp mode: host volumes: - data-storaged0:/data/storage - logs-storaged0:/logs networks: - nebula-net storaged1: image: vesoft/nebula-storaged:nightly env_file: - ./nebula.env command: - --meta_server_addrs=192.168.1.166:45500,192.168.1.167:45500,192.168.1.168:45500 - --local_ip=192.168.1.167 - --ws_ip=192.168.1.167 - --port=44500 - --data_path=/data/storage - --log_dir=/logs - --v=0 - --minloglevel=2 deploy: replicas: 1 restart_policy: condition: on-failure placement: constraints: - node.hostname == KF2-DATA-167 depends_on: - metad0 - metad1 - metad2 healthcheck: test: ["CMD", "curl", "-f", "http://192.168.1.167:12000/status"] interval: 30s timeout: 10s retries: 3 start_period: 20s ports: - target: 12000 published: 12000 protocol: tcp mode: host - target: 12002 published: 12004 protocol: tcp mode: host volumes: - data-storaged1:/data/storage - logs-storaged1:/logs networks: - nebula-net storaged2: image: vesoft/nebula-storaged:nightly env_file: - ./nebula.env command: - --meta_server_addrs=192.168.1.166:45500,192.168.1.167:45500,192.168.1.168:45500 - --local_ip=192.168.1.168 - --ws_ip=192.168.1.168 - --port=44500 - --data_path=/data/storage - --log_dir=/logs - --v=0 - --minloglevel=2 deploy: replicas: 1 restart_policy: condition: on-failure placement: constraints: - node.hostname == KF2-DATA-168 depends_on: - metad0 - metad1 - metad2 healthcheck: test: ["CMD", "curl", "-f", "http://192.168.1.168:12000/status"] interval: 30s timeout: 10s retries: 3 start_period: 20s ports: - target: 12000 published: 12000 protocol: tcp mode: host - target: 12002 published: 12006 protocol: tcp mode: host volumes: - data-storaged2:/data/storage - logs-storaged2:/logs networks: - nebula-net graphd1: image: vesoft/nebula-graphd:nightly env_file: - ./nebula.env command: - --meta_server_addrs=192.168.1.166:45500,192.168.1.167:45500,192.168.1.168:45500 - --port=3699 - --ws_ip=192.168.1.166 - --log_dir=/logs - --v=0 - --minloglevel=2 deploy: replicas: 1 restart_policy: condition: on-failure placement: constraints: - node.hostname == KF2-DATA-166 depends_on: - metad0 - metad1 - metad2 healthcheck: test: ["CMD", "curl", "-f", "http://192.168.1.166:13000/status"] interval: 30s timeout: 10s retries: 3 start_period: 20s ports: - target: 3699 published: 3699 protocol: tcp mode: host - target: 13000 published: 13000 protocol: tcp # mode: host - target: 13002 published: 13002 protocol: tcp mode: host volumes: - logs-graphd:/logs networks: - nebula-net graphd2: image: vesoft/nebula-graphd:nightly env_file: - ./nebula.env command: - --meta_server_addrs=192.168.1.166:45500,192.168.1.167:45500,192.168.1.168:45500 - --port=3699 - --ws_ip=192.168.1.167 - --log_dir=/logs - --v=2 - --minloglevel=2 deploy: replicas: 1 restart_policy: condition: on-failure placement: constraints: - node.hostname == KF2-DATA-167 depends_on: - metad0 - metad1 - metad2 healthcheck: test: ["CMD", "curl", "-f", "http://192.168.1.167:13001/status"] interval: 30s timeout: 10s retries: 3 start_period: 20s ports: - target: 3699 published: 3640 protocol: tcp mode: host - target: 13000 published: 13001 protocol: tcp mode: host - target: 13002 published: 13003 protocol: tcp # mode: host volumes: - logs-graphd2:/logs networks: - nebula-net graphd3: image: vesoft/nebula-graphd:nightly env_file: - ./nebula.env command: - --meta_server_addrs=192.168.1.166:45500,192.168.1.167:45500,192.168.1.168:45500 - --port=3699 - --ws_ip=192.168.1.168 - --log_dir=/logs - --v=0 - --minloglevel=2 deploy: replicas: 1 restart_policy: condition: on-failure placement: constraints: - node.hostname == KF2-DATA-168 depends_on: - metad0 - metad1 - metad2 healthcheck: test: ["CMD", "curl", "-f", "http://192.168.1.168:13002/status"] interval: 30s timeout: 10s retries: 3 start_period: 20s ports: - target: 3699 published: 3641 protocol: tcp mode: host - target: 13000 published: 13002 protocol: tcp # mode: host - target: 13002 published: 13004 protocol: tcp mode: host volumes: - logs-graphd3:/logs networks: - nebula-net networks: nebula-net: external: true attachable: true name: host volumes: data-metad0: logs-metad0: data-metad1: logs-metad1: data-metad2: logs-metad2: data-storaged0: logs-storaged0: data-storaged1: logs-storaged1: data-storaged2: logs-storaged2: logs-graphd: logs-graphd2: logs-graphd3: 编辑 nebula.env,加入如下内容 TZ=UTC USER=root 2.6 启动 nebula 集群 docker stack deploy nebula -c docker-stack.yml 三、集群负载均衡及高可用配置 Nebula Graph 的客户端目前(1.X)没有提供负载均衡的能力,只是随机选一个 graphd 去连接。所以生产使用的时候要自己做个负载均衡和高可用。 图 3.1 将整个部署架构分为三层,数据服务层,负载均衡层及高可用层。如图 3.1 所示 负载均衡层:对 client 请求做负载均衡,将请求分发至下方数据服务层 高可用层: 这里实现的是 haproxy 的高可用,保证负载均衡层的服务从而保证整个集群的正常服务 3.1 负载均衡配置 haproxy 使用 docker-compose 配置。分别编辑以下三个文件 Dockerfile 加入以下内容 FROM haproxy:1.7 COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg EXPOSE 3640 docker-compose.yml 加入以下内容 version: "3.2" services: haproxy: container_name: haproxy build: . volumes: - ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg ports: - 3640:3640 restart: always networks: - app_net networks: app_net: external: true haproxy.cfg 加入以下内容 global daemon maxconn 30000 log 127.0.0.1 local0 info log 127.0.0.1 local1 warning defaults log-format %hr\ %ST\ %B\ %Ts log global mode http option http-keep-alive timeout connect 5000ms timeout client 10000ms timeout server 50000ms timeout http-request 20000ms # custom your own frontends && backends && listen conf # CUSTOM listen graphd-cluster bind *:3640 mode tcp maxconn 300 balance roundrobin server server1 192.168.1.166:3699 maxconn 300 check server server2 192.168.1.167:3699 maxconn 300 check server server3 192.168.1.168:3699 maxconn 300 check listen stats bind *:1080 stats refresh 30s stats uri /stats 3.2 启动 haproxy docker-compose up -d 3.3 高可用配置 注:配置 keepalive 需预先准备好 vip(虚拟 ip),在以下配置中 192.168.1.99 便为虚拟 ip 在 192.168.1.166 、192.168.1.167、192.168.1.168上 均做以下配置 安装 keepalived apt-get update && apt-get upgrade && apt-get install keepalived -y 更改 keepalived配置文件 /etc/keepalived/keepalived.conf(三台机器中 做如下配置,priority 应设置不同值确定优先级) 192.168.1.166 机器配置 global_defs { router_id lb01 # 标识信息,一个名字而已; } vrrp_script chk_haproxy { script "killall -0 haproxy" interval 2 } vrrp_instance VI_1 { state MASTER interface ens160 virtual_router_id 52 priority 999 # 设定 MASTER 与 BACKUP 负载均衡器之间同步检查的时间间隔,单位是秒 advert_int 1 # 设置验证类型和密码 authentication { # 设置验证类型,主要有 PASS 和 AH 两种 auth_type PASS # 设置验证密码,在同一个 vrrp_instance 下,MASTER 与 BACKUP 必须使用相同的密码才能正常通信 auth_pass amber1 } virtual_ipaddress { # 虚拟 IP 为 192.168.1.99/24; 绑定接口为 ens160; 别名 ens169:1,主备相同 192.168.1.99/24 dev ens160 label ens160:1 } track_script { chk_haproxy } } 167 机器配置 global_defs { router_id lb01 # 标识信息,一个名字而已; } vrrp_script chk_haproxy { script "killall -0 haproxy" interval 2 } vrrp_instance VI_1 { state BACKUP interface ens160 virtual_router_id 52 priority 888 # 设定 MASTER 与 BACKUP 负载均衡器之间同步检查的时间间隔,单位是秒 advert_int 1 # 设置验证类型和密码 authentication { # 设置验证类型,主要有 PASS 和 AH 两种 auth_type PASS # 设置验证密码,在同一个 vrrp_instance 下,MASTER 与 BACKUP 必须使用相同的密码才能正常通信 auth_pass amber1 } virtual_ipaddress { # 虚拟 IP 为 192.168.1.99/24; 绑定接口为 ens160; 别名 ens160:1,主备相同 192.168.1.99/24 dev ens160 label ens160:1 } track_script { chk_haproxy } } 168 机器配置 global_defs { router_id lb01 # 标识信息,一个名字而已; } vrrp_script chk_haproxy { script "killall -0 haproxy" interval 2 } vrrp_instance VI_1 { state BACKUP interface ens160 virtual_router_id 52 priority 777 # 设定 MASTER 与 BACKUP 负载均衡器之间同步检查的时间间隔,单位是秒 advert_int 1 # 设置验证类型和密码 authentication { # 设置验证类型,主要有 PASS 和 AH 两种 auth_type PASS # 设置验证密码,在同一个 vrrp_instance 下,MASTER 与 BACKUP 必须使用相同的密码才能正常通信 auth_pass amber1 } virtual_ipaddress { # 虚拟 IP 为 192.168.1.99/24;绑定接口为 ens160; 别名 ens160:1,主备相同 192.168.1.99/24 dev ens160 label ens160:1 } track_script { chk_haproxy } } keepalived 相关命令 # 启动 keepalived systemctl start keepalived # 使 keepalived 开机自启 systemctl enable keeplived # 重启 keepalived systemctl restart keepalived 四、其他 离线怎么部署?把镜像更改为私有镜像库就成了,有问题欢迎来勾搭啊。 我的小鱼你醒了 还认识早晨吗 昨夜你曾经说 愿夜幕永不开启 如果你对本文有任何疑问,欢迎来论坛和原作者聊聊~~ 原帖地址:https://discuss.nebula-graph.com.cn/t/topic/1388
Nebula Graph DBaaS 作为一款 DBaaS(DataBase as s Service)的产品,Nebula Graph Cloud Service 极大地降低了研发人员使用 Nebula Graph 的成本,更专注于使用 Nebula Graph 挖掘、分析数据背后的关联价值。 Nebula Graph Cloud Service Trial 版本已于近期开始公测试用,本篇文章主要帮助感兴趣的朋友快速了解我们云服务 Trial 版本的主要功能及开放范围。 主要功能 一键创建 Nebula Graph 云服务实例 权限管理 - 可邀请其他 Nebula Graph Cloud Service 注册用户一起使用实例 日志记录 - 记录查看实例有关操作记录 提供在线 Nebula Graph Studio——图数据库可视化工具: 控制台 - 快速尝试 nebula 语句的基本功能 图探索 - 通过图可视化发掘数据之间的联系 导数据 - 通过可视化配置将数据导入 nebula 可视化构图 - 通过可视化操作,迅速完成 点/边 构图建模(近期发布) 服务监控 - 实时洞察机器运行的基本情况 团队管理 - 简单的团队创建及成员添加,方便实例所属权的转移和交接 公测范围 初次了解 Nebula Graph 图数据库,想要快速无障碍体验 Nebula Graph 产品服务 有图数据库使用需求的用户,诸如金融风控、实时推荐、知识图谱等应用场景 企业用户:最好使用公司邮箱注册,有限的试用资源能帮助真正需要尝试的朋友 Trial 版本限制 试用期间无法提供独立 IP 供业务客户端直连,一切侧重产品功能体验为主,商用版本会提供。 试用期提供的实例服务均为单副本的统一资源: 1G 内存 单核 40G 磁盘大小 其他:商用版本会通过资源配置选择。 数据导入时上传的数据集单个文件大小不能超过 100M,总文件大小限制 1G。 试用链接及官方联系方式 欢迎感兴趣的朋友前来申请试用:https://cloud.nebula-graph.com.cn/,有更多需求和问题咨询的朋友,也欢迎联系我们。 邮箱联系:cloud-support@vesoft.com 论坛提问:https://discuss.nebula-graph.com.cn/c/users/DBaas/36
在本系列的前文 [1,2]中,我们介绍了如何使用 Python 语言图分析库 NetworkX [3] + Nebula Graph [4] 来进行<权力的游戏>中人物关系图谱分析。 在本文中我们将介绍如何使用 Java 语言的图分析库 JGraphT [5] 并借助绘图库 mxgraph [6] ,可视化探索 A 股的行业个股的相关性随时间的变化情况。 数据集的处理 本文主要分析方法参考了[7,8],有两种数据集: 股票数据(点集) 从 A 股中按股票代码顺序选取了 160 只股票(排除摘牌或者 ST 的)。每一支股票都被建模成一个点,每个点的属性有股票代码,股票名称,以及证监会对该股票对应上市公司所属板块分类等三种属性; 表1:点集示例 顶点id 股票代码 股票名称 所属板块 1 SZ0001 平安银行 金融行业 2 600000 浦发银行 金融行业 3 600004 白云机场 交通运输 4 600006 东风汽车 汽车制造 5 600007 中国国贸 开发区 6 600008 首创股份 环保行业 7 600009 上海机场 交通运输 8 600010 包钢股份 钢铁行业 股票关系(边集) 边只有一个属性,即权重。边的权重代表边的源点和目标点所代表的两支股票所属上市公司业务上的的相似度——相似度的具体计算方法参考 [7,8]:取一段时间(2014 年 1 月 1 日 - 2020 年 1 月 1 日)内,个股的日收益率的时间序列相关性 $P_{ij}$ 再定义个股之间的距离为 (也即两点之间的边权重): $$l_{ij} = sqrt{2(1-P_{ij})}$$ 通过这样的处理,距离取值范围为 [0,2]。这意味着距离越远的个股,两个之间的收益率相关性越低。 表2: 边集示例 边的源点 ID 边的目标点 ID 边的权重 11 12 0.493257968 22 83 0.517027513 23 78 0.606206233 2 12 0.653692415 1 11 0.677631482 1 27 0.695705171 1 12 0.71124344 2 11 0.73581915 8 18 0.771556458 12 27 0.785046446 9 20 0.789606527 11 27 0.796009627 25 63 0.797218349 25 72 0.799230001 63 115 0.803534952 这样的点集和边集构成一个图网络,可以将这个网络存储在图数据库 Nebula Graph 中。 JGraphT JGraphT 是一个开放源代码的 Java 类库,它不仅为我们提供了各种高效且通用的图数据结构,还为解决最常见的图问题提供了许多有用的算法: 支持有向边、无向边、权重边、非权重边等; 支持简单图、多重图、伪图; 提供了用于图遍历的专用迭代器(DFS,BFS)等; 提供了大量常用的的图算法,如路径查找、同构检测、着色、公共祖先、游走、连通性、匹配、循环检测、分区、切割、流、中心性等算法; 可以方便地导入 / 导出 GraphViz [9]。导出的 GraphViz 可被导入可视化工具 Gephi[10] 进行分析与展示; 可以方便地使用其他绘图组件,如:JGraphX,mxGraph,Guava Graphs Generators 等工具绘制出图网络。 下面,我们来实践一把,先在 JGraphT 中创建一个有向图: import org.jgrapht.*; import org.jgrapht.graph.*; import org.jgrapht.nio.*; import org.jgrapht.nio.dot.*; import org.jgrapht.traverse.*; import java.io.*; import java.net.*; import java.util.*; Graph<URI, DefaultEdge> g = new DefaultDirectedGraph<>(DefaultEdge.class); 添加顶点: URI google = new URI("http://www.google.com"); URI wikipedia = new URI("http://www.wikipedia.org"); URI jgrapht = new URI("http://www.jgrapht.org"); // add the vertices g.addVertex(google); g.addVertex(wikipedia); g.addVertex(jgrapht); 添加边: // add edges to create linking structure g.addEdge(jgrapht, wikipedia); g.addEdge(google, jgrapht); g.addEdge(google, wikipedia); g.addEdge(wikipedia, google); 图数据库 Nebula Graph Database JGraphT 通常使用本地文件作为数据源,这在静态网络研究的时候没什么问题,但如果图网络经常会发生变化——例如,股票数据每日都在变化——每次生成全新的静态文件再加载分析就有些麻烦,最好整个变化过程可以持久化地写入一个数据库中,并且可以实时地直接从数据库中加载子图或者全图做分析。本文选用 Nebula Graph 作为存储图数据的图数据库。 Nebula Graph 的 Java 客户端 Nebula-Java [11] 提供了两种访问 Nebula Graph 方式:一种是通过图查询语言 nGQL [12] 与查询引擎层 [13] 交互,这通常适用于有复杂语义的子图访问类型; 另一种是通过 API 与底层的存储层(storaged)[14] 直接交互,用于获取全量的点和边。除了可以访问 Nebula Graph 本身外,Nebula-Java 还提供了与 Neo4j [15]、JanusGraph [16]、Spark [17] 等交互的示例。 在本文中,我们选择直接访问存储层(storaged)来获取全部的点和边。下面两个接口可以用来读取所有的点、边数据: // space 为待扫描的图空间名称,returnCols 为需要读取的点/边及其属性列, // returnCols 参数格式:{tag1Name: prop1, prop2, tag2Name: prop3, prop4, prop5} Iterator<ScanVertexResponse> scanVertex( String space, Map<String, List<String>> returnCols); Iterator<ScanEdgeResponse> scanEdge( String space, Map<String, List<String>> returnCols); 第一步:初始化一个客户端,和一个 ScanVertexProcessor。ScanVertexProcessor 用来对读出来的顶点数据进行解码: MetaClientImpl metaClientImpl = new MetaClientImpl(metaHost, metaPort); metaClientImpl.connect(); StorageClient storageClient = new StorageClientImpl(metaClientImpl); Processor processor = new ScanVertexProcessor(metaClientImpl); 第二步:调用 scanVertex 接口,该接口会返回一个 scanVertexResponse 对象的迭代器: Iterator<ScanVertexResponse> iterator = storageClient.scanVertex(spaceName, returnCols); 第三步:不断读取该迭代器所指向的 scanVertexResponse 对象中的数据,直到读取完所有数据。读取出来的顶点数据先保存起来,后面会将其添加到到 JGraphT 的图结构中: while (iterator.hasNext()) { ScanVertexResponse response = iterator.next(); if (response == null) { log.error("Error occurs while scan vertex"); break; } Result result = processor.process(spaceName, response); results.addAll(result.getRows(TAGNAME)); } 读取边数据的方法和上面的流程类似。 在 JGraphT 中进行图分析 第一步:在 JGraphT 中创建一个无向加权图 graph: Graph<String, MyEdge> graph = GraphTypeBuilder .undirected() .weighted(true) .allowingMultipleEdges(true) .allowingSelfLoops(false) .vertexSupplier(SupplierUtil.createStringSupplier()) .edgeSupplier(SupplierUtil.createSupplier(MyEdge.class)) .buildGraph(); 第二步:将上一步从 Nebula Graph 图空间中读出来的点、边数据添加到 graph 中: for (VertexDomain vertex : vertexDomainList){ graph.addVertex(vertex.getVid().toString()); stockIdToName.put(vertex.getVid().toString(), vertex); } for (EdgeDomain edgeDomain : edgeDomainList){ graph.addEdge(edgeDomain.getSrcid().toString(), edgeDomain.getDstid().toString()); MyEdge newEdge = graph.getEdge(edgeDomain.getSrcid().toString(), edgeDomain.getDstid().toString()); graph.setEdgeWeight(newEdge, edgeDomain.getWeight()); } 第三步:参考 [7,8] 中的分析法,对刚才的图 graph 使用 Prim 最小生成树算法(minimun-spanning-tree),并调用封装好的 drawGraph 接口画图: 普里姆算法(Prim's algorithm),图论中的一种算法,可在加权连通图里搜索最小生成树。即,由此算法搜索到的边子集所构成的树中,不但包括了连通图里的所有顶点,且其所有边的权值之和亦为最小。 SpanningTreeAlgorithm.SpanningTree pMST = new PrimMinimumSpanningTree(graph).getSpanningTree(); Legend.drawGraph(pMST.getEdges(), filename, stockIdToName); 第四步:drawGraph 方法封装了画图的布局等各项参数设置。这个方法将同一板块的股票渲染为同一颜色,将距离接近的股票排列聚集在一起。 public class Legend { ... public static void drawGraph(Set<MyEdge> edges, String filename, Map<String, VertexDomain> idVertexMap) throws IOException { // Creates graph with model mxGraph graph = new mxGraph(); Object parent = graph.getDefaultParent(); // set style graph.getModel().beginUpdate(); mxStylesheet myStylesheet = graph.getStylesheet(); graph.setStylesheet(setMsStylesheet(myStylesheet)); Map<String, Object> idMap = new HashMap<>(); Map<String, String> industryColor = new HashMap<>(); int colorIndex = 0; for (MyEdge edge : edges) { Object src, dst; if (!idMap.containsKey(edge.getSrc())) { VertexDomain srcNode = idVertexMap.get(edge.getSrc()); String nodeColor; if (industryColor.containsKey(srcNode.getIndustry())){ nodeColor = industryColor.get(srcNode.getIndustry()); }else { nodeColor = COLOR_LIST[colorIndex++]; industryColor.put(srcNode.getIndustry(), nodeColor); } src = graph.insertVertex(parent, null, srcNode.getName(), 0, 0, 105, 50, "fillColor=" + nodeColor); idMap.put(edge.getSrc(), src); } else { src = idMap.get(edge.getSrc()); } if (!idMap.containsKey(edge.getDst())) { VertexDomain dstNode = idVertexMap.get(edge.getDst()); String nodeColor; if (industryColor.containsKey(dstNode.getIndustry())){ nodeColor = industryColor.get(dstNode.getIndustry()); }else { nodeColor = COLOR_LIST[colorIndex++]; industryColor.put(dstNode.getIndustry(), nodeColor); } dst = graph.insertVertex(parent, null, dstNode.getName(), 0, 0, 105, 50, "fillColor=" + nodeColor); idMap.put(edge.getDst(), dst); } else { dst = idMap.get(edge.getDst()); } graph.insertEdge(parent, null, "", src, dst); } log.info("vertice " + idMap.size()); log.info("colorsize " + industryColor.size()); mxFastOrganicLayout layout = new mxFastOrganicLayout(graph); layout.setMaxIterations(2000); //layout.setMinDistanceLimit(10D); layout.execute(parent); graph.getModel().endUpdate(); // Creates an image than can be saved using ImageIO BufferedImage image = createBufferedImage(graph, null, 1, Color.WHITE, true, null); // For the sake of this example we display the image in a window // Save as JPEG File file = new File(filename); ImageIO.write(image, "JPEG", file); } ... } 第五步:生成可视化: 图1中每个顶点的颜色代表证监会对该股票所属上市公司归类的板块。 可以看到,实际业务近似度较高的股票已经聚拢成簇状(例如:高速板块、银行版本、机场航空板块),但也会有部分关联性不明显的个股被聚类在一起,具体原因需要单独进行个股研究。 图1: 基于 2015-01-01 至 2020-01-01 的股票数据计算出的聚集性 第六步:基于不同时间窗口的一些其他动态探索 上节中,结论主要基于 2015-01-01 到 2020-01-01 的个股聚集性。这一节我们还做了一些其他的尝试:以 2 年为一个时间滑动窗口,分析方法不变,定性探索聚集群是否随着时间变化会发生改变。 图2:基于 2014-01-01 至 2016-01-01 的股票数据计算出的聚集性 图3:基于 2015-01-01 至 2017-01-01 的股票数据计算出的聚集性 图4:基于 2016-01-01 至 2018-01-01 的股票数据计算出的聚集性 图5:基于 2017-01-01 至 2019-01-01 的股票数据计算出的聚集性 图6:基于 2018-01-01 至 2020-01-01 的股票数据计算出的聚集性 粗略分析看,随着时间窗口变化,有些板块(高速、银行、机场航空、房产、能源)的板块内部个股聚集性一直保持比较好——这意味着随着时间变化,这个版块内各种一直保持比较高的相关性;但有些板块(制造)的聚集性会持续变化——意味着相关性一直在发生变化。 Disclaim 本文不构成任何投资建议,且作者不持有本文中任一股票。 受限于停牌、熔断、涨跌停、送转、并购、主营业务变更等情况,数据处理可能有错误,未做一一检查。 受时间所限,本文只选用了 160 个个股样本过去 6 年的数据,只采用了最小扩张树一种办法来做聚类分类。未来可以使用更大的数据集(例如美股、衍生品、数字货币),尝试更多种图机器学习的办法。 本文代码可见[18] Reference [1] 用 NetworkX + Gephi + Nebula Graph 分析<权力的游戏>人物关系(上篇)https://nebula-graph.com.cn/posts/game-of-thrones-relationship-networkx-gephi-nebula-graph/ [2] 用 NetworkX + Gephi + Nebula Graph 分析<权力的游戏>人物关系(下篇) https://nebula-graph.com.cn/posts/game-of-thrones-relationship-networkx-gephi-nebula-graph-part-two/ [3] NetworkX: a Python package for the creation, manipulation, and study of the structure, dynamics, and functions of complex networks. https://networkx.github.io/ [4] Nebula Graph: A powerfully distributed, scalable, lightning-fast graph database written in C++. https://nebula-graph.io/ [5] JGraphT: a Java library of graph theory data structures and algorithms. https://jgrapht.org/ [6] mxGraph: JavaScript diagramming library that enables interactive graph and charting applications. https://jgraph.github.io/mxgraph/ [7] Bonanno, Giovanni & Lillo, Fabrizio & Mantegna, Rosario. (2000). High-frequency Cross-correlation in a Set of Stocks. arXiv.org, Quantitative Finance Papers. 1. 10.1080/713665554. [8] Mantegna, R.N. Hierarchical structure in financial markets. Eur. Phys. J. B 11, 193–197 (1999). [9] https://graphviz.org/ [10] https://gephi.org/ [11] https://github.com/vesoft-inc/nebula-java [12] Nebula Graph Query Language (nGQL). https://docs.nebula-graph.io/manual-EN/1.overview/1.concepts/2.nGQL-overview/ [13] Nebula Graph Query Engine. https://github.com/vesoft-inc/nebula-graph [14] Nebula-storage: A distributed consistent graph storage. https://github.com/vesoft-inc/nebula-storage [15] Neo4j. www.neo4j.com [16] JanusGraph. janusgraph.org [17] Apache Spark. spark.apache.org. [18] https://github.com/Judy1992/nebula_scan
本文主要讲述如何使用数据导入工具 Nebula Graph Exchange 将数据从 Neo4j 导入到 Nebula Graph Database。在讲述如何实操数据导入之前,我们先来了解下 Nebula Graph 内部是如何实现这个导入功能的。 Nebula Graph Exchange 的数据处理原理 我们这个导入工具名字是 Nebula Graph Exchange,采用 Spark 作为导入平台,来支持海量数据的导入和保障性能。Spark 本身提供了不错的抽象——DataFrame,使得可以轻松支持多种数据源。在 DataFrame 的支持下,添加新的数据源只需提供配置文件读取的代码和返回 DataFrame 的 Reader 类,即可支持新的数据源。 DataFrame 可以视为一种分布式存表格。DataFrame 可以存储在多个节点的不同分区中,多个分区可以存储在不同的机器上,从而支持并行操作。Spark 还提供了一套简洁的 API 使用户轻松操作 DataFrame 如同操作本地数据集一般。现在大多数数据库提供直接将数据导出成 DataFrame 功能,即使某个数据库并未提供此功能也可以通过数据库 driver 手动构建 DataFrame。 Nebula Graph Exchange 将数据源的数据处理成 DataFrame 之后,会遍历它的每一行,根据配置文件中 fields 的映射关系,按列名获取对应的值。在遍历 batchSize 个行之后,Exchange 会将获取的数据一次性写入到 Nebula Graph 中。目前,Exchange 是通过生成 nGQL 语句再由 Nebula Client 异步写入数据,下一步会支持直接导出 Nebula Graph 底层存储的 sst 文件,以获取更好的性能。接下来介绍一下 Neo4j 数据源导入的具体实现。 Neo4j 数据导入具体实现 虽然 Neo4j 官方提供了可将数据直接导出为 DataFrame 的库,但使用它读取数据难以满足断点续传的需求,我们未直接使用这个库,而是使用 Neo4j 官方的 driver 实现数据读取。Exchange 通过在不同分区调取 Neo4j driver 执行不同 skip 和 limit 的 Cypher 语句,将数据分布在不同的分区,来获取更好的性能。这个分区数量由配置项 partition 指定。 Exchange 中的 Neo4jReader 类会先将用户配置中的 exec Cypher 语句,return 后边的语句替换成 count(*) 执行获取数据总量,再根据分区数计算每个分区的起始偏移量和大小。这里如果用户配置了 check_point_path 目录,会读取目录中的文件,如果处于续传的状态,Exchange 会计算出每个分区应该的偏移量和大小。然后每个分区在 Cypher 语句后边添加不同的 skip 和 limit,调用 driver 执行。最后将返回的数据处理成 DataFrame 就完成了 Neo4j 的数据导入。 过程如下图所示: Neo4j 数据导入实践 我们这里导入演示的系统环境如下: cpu name:Intel(R) Xeon(R) CPU E5-2697 v3 @ 2.60GHz cpu cores:14 memory size:251G 软件环境如下: Neo4j:3.5.20 社区版 Nebula graph:docker-compose 部署,默认配置 Spark:单机版,版本为 2.4.6 pre-build for hadoop2.7 由于 Nebula Graph 是强 schema 数据库,数据导入前需先进行创建 Space,建 Tag 和 Edge 的 schema,具体的语法可以参考这里。 这里建了名为 test 的 Space,副本数为 1。这里创建了两种 Tag 分别为 tagA 和 tagB,均含有 4 个属性的点类型,此外,还创建一种名为 edgeAB 的边类型,同样含有 4 个属性。具体的 nGQL 语句如下所示: # 创建图空间 CREATE SPACE test(replica_factor=1); # 选择图空间 test USE test; # 创建标签 tagA CREATE TAG tagA(idInt int, idString string, tboolean bool, tdouble double); # 创建标签 tagB CREATE TAG tagB(idInt int, idString string, tboolean bool, tdouble double); # 创建边类型 edgeAB CREATE EDGE edgeAB(idInt int, idString string, tboolean bool, tdouble double); 同时向 Neo4j 导入 Mock 数据——标签为 tagA 和 tagB 的点,数量总共为 100 万,并且导入了连接 tagA 和 tagB 类型点边类型为 edgeAB 的边,共 1000 万个。另外需要注意的是,从 Neo4j 导出的数据在 Nebula Graph 中必须存在属性,且数据对应的类型要同 Nebula Graph 一致。 最后为了提升向 Neo4j 导入 Mock 数据的效率和 Mock 数据在 Neo4j 中的读取效率,这里为 tagA 和 tagB 的 idInt 属性建了索引。关于索引需要注意 Exchange 并不会将 Neo4j 中的索引、约束等信息导入到 Nebula Graph 中,所以需要用户在执行数据写入在 Nebula Graph 之后,自行创建索引和 REBUILD 索引(为已有数据建立索引)。 接下来就可以将 Neo4j 数据导入到 Nebula Graph 中了,首先我们需要下载和编译打包项目,项目在 nebula-java 这个仓库下 tools/exchange 文件夹中。可执行如下命令: git clone https://github.com/vesoft-inc/nebula-java.git cd nebula-java/tools/exchange mvn package -DskipTests 然后就可以看到 target/exchange-1.0.1.jar 这个文件。 接下来编写配置文件,配置文件的格式为:HOCON(Human-Optimized Config Object Notation),可以基于 src/main/resources/server_application.conf 文件的基础上进行更改。首先对 nebula 配置项下的 address、user、pswd 和 space 进行配置,测试环境均为默认配置,所以这里不需要额外的修改。然后进行 tags 配置,需要 tagA 和 tagB 的配置,这里仅展示 tagA 配置,tagB 和 tagA 配置相同。 { # ======neo4j连接设置======= name: tagA # 必须和 Nebula Graph 的中 tag 名字一致,需要在 Nebula Graph 中事先建好 tag server: "bolt://127.0.0.1:7687" # neo4j 的地址配置 user: neo4j # neo4j 的用户名 password: neo4j # neo4j 的密码 encryption: false # (可选): 传输是否加密,默认值为 false database: graph.db # (可选): neo4j database 名称,社区版不支持 # ======导入设置============ type: { source: neo4j # 还支持 PARQUET、ORC、JSON、CSV、HIVE、MYSQL、PULSAR、KAFKA... sink: client # 写入 Nebula Graph 的方式,目前仅支持 client,未来会支持直接导出 Nebula Graph 底层数据库文件 } nebula.fields: [idInt, idString, tdouble, tboolean] fields : [idInt, idString, tdouble, tboolean] # 映射关系 fields,上方为 nebula 的属性名,下方为 neo4j 的属性名,一一对应 # 映射关系的配置是 List 而不是 Map,是为了保持 fields 的顺序,未来直接导出 nebula 底层存储文件时需要 vertex: idInt # 作为 nebula vid 的 neo4j field,类型需要是整数(long or int)。 partition: 10 # 分区数 batch: 2000 # 一次写入 nebula 多少数据 check_point_path: "file:///tmp/test" # (可选): 保存导入进度信息的目录,用于断点续传 exec: "match (n:tagA) return n.idInt as idInt, n.idString as idString, n.tdouble as tdouble, n.tboolean as tboolean order by n.idInt" } 边的设置大部分与点的设置无异,但由于边在 Nebula Graph 中有起点的 vid 和终点的 vid 标识,所以这里需要指定作为边起点 vid 的域和作为边终点 vid 的域。 下面给出边的特别配置。 source: { field: a.idInt # policy: "hash" } # 起点的 vid 设置 target: { field: b.idInt # policy: "uuid" } # 终点的 vid 设置 ranking: idInt # (可选): 作为 rank 的 field partition: 1 # 这里分区数设置为 1,原因在后边 exec: "match (a:tagA)-[r:edgeAB]->(b:tagB) return a.idInt, b.idInt, r.idInt as idInt, r.idString as idString, r.tdouble as tdouble, r.tboolean as tboolean order by id(r)" 点的 vertex 和边的 source、target 配置项下都可以设置 policy hash/uuid,它可以将类型为字符串的域作为点的 vid,通过 hash/uuid 函数将字符串映射成整数。 上面的例子由于作为点的 vid 为整数,所以并不需要 policy 的设置。hash/uuid的 区别请看这里。 Cypher 标准中如果没有 order by 约束的话就不能保证每次查询结果的排序一致,虽然看起来即便不加 order by Neo4j 返回的结果顺序也是不变的,但为了防止可能造成的导入时数据丢失,还是强烈建议在 Cypher 语句中加入 order by,虽然这会增加导入的时间。为了提升导入效率, order by 语句最好选取有索引的属性作为排序的属性。如果没有索引,也可观察默认的排序,选择合适的排序属性以提高效率。如果默认的排序找不到规律,可以使用点/关系的 ID 作为排序属性,并且将 partition 的值尽量设小,减少 Neo4j 的排序压力,本文中边 edgeAB 的 partition 就设置为 1。 另外 Nebula Graph 在创建点和边时会将 ID 作为唯一主键,如果主键已存在则会覆盖该主键中的数据。所以假如将某个 Neo4j 属性值作为 Nebula Graph 的 ID,而这个属性值在 Neo4j 中是有重复的,就会导致“重复 ID”对应的数据有且只有一条会存入 Nebula Graph 中,其它的则会被覆盖掉。由于数据导入过程是并发地往 Nebula Graph 中写数据,最终保存的数据并不能保证是 Neo4j 中最新的数据。 这里还要留意下断点续传功能,在断点和续传之间,数据库不应该改变状态,如添加数据或删除数据,且 partition 数量也不能更改,否则可能会有数据丢失。 最后由于 Exchange 需要在不同分区执行不同 skip 和 limit 的 Cypher 语句,所以用户提供的 Cypher 语句不能含有 skip 和 limit 语句。 接下来就可以运行 Exchange 程序导数据了,执行如下命令: $SPARK_HOME/bin/spark-submit --class com.vesoft.nebula.tools.importer.Exchange --master "local[10]" target/exchange-1.0.1.jar -c /path/to/conf/neo4j_application.conf 在上述这些配置下,导入 100 万个点用时 13s,导入 1000 万条边用时 213s,总用时是 226s。 附:Neo4j 3.5 Community 和 Nebula Graph 1.0.1的一些比较 Neo4j 和 Nebula Graph 在系统架构、数据模型和访问方式上都有一些差异,下表列举了常见的异同 作者有话说:Hi,我是李梦捷,图数据库 Nebula Graph 的研发工程师,如果你对此文有疑问,欢迎来我们的 Nebula Graph 论坛交流下心得~~ 喜欢这篇文章?来来来,给我们的 GitHub 点个 star 表鼓励啦~~ ♂️♀️ [手动跪谢] 交流图数据库技术?交个朋友,Nebula Graph 官方小助手微信:NebulaGraphbot 拉你进交流群~~ 推荐阅读 360 数科实践:JanusGraph 到 NebulaGraph 迁移
在上一篇[1]中,我们通过 NetworkX 和 Gephi 展示了<权力的游戏>中的人物关系。在本篇中,我们将展示如何通过 NetworkX 访问图数据库 Nebula Graph。 NetworkX NetworkX [2] 是一个用 Python 语言开发的图论与复杂网络建模工具,内置了大量常用的图与复杂网络分析算法,可以方便地进行复杂网络数据分析、仿真建模等工作,功能丰富,简单易用。 在 NetworkX 中,图是由顶点、边和可选的属性构成的数据结构。顶点表示数据,边是由两个顶点唯一确定的,表示两个顶点之间的关系。顶点和边也可以拥有更多的属性,以存储更多的信息。 NetworkX 支持 4 种类型的图: Graph:无向图 DiGraph: 有向图 MultiGraph: 多重无向图 MultiDiGraph: 多重有向图 在 NetworkX 中创建一个无向图: import networkx as nx G = nx.Graph() 添加顶点: G.add_node(1) G.add_nodes_from([2,3,4]) G.add_node(2,name='Tom',age=23) 添加边: G.add_edge(2,3) G.add_edges_from([(1,2),(1,3)]) g.add_edge(1, 2, start_year=1996, end_year=2019) 在上一篇文章(一)中,我们已经演示了 NetworkX 的 Girvan-Newman 社区发现算法。 图数据库 Nebula Graph NetworkX 通常使用本地文件作为数据源,这在静态网络研究的时候没什么问题,但如果图网络经常会发生变化——例如某些中心节点已经不存在(Fig.1)或者引入了重要的网络拓扑变化(Fig.2)——每次生成全新的静态文件再加载分析就有些麻烦,最好整个变化过程可以持久化在一个数据库中,并且可以实时地直接从数据库中加载子图或者全图做分析。本文选用 Nebula Graph [3]作为存储图数据的图数据库。 Fig. 1 Fig. 2 Nebula Graph 提供了两种方式来获取图结构: 编写一个查询语句,拉取一个子图; 全量扫描底层存储,获取一个完整的全图。 第一种方式适合在一个大规模的图网络中通过精细的过滤和剪枝条件来获取符合需求的若干个点和边。第二种方式更适合于全图的分析,这通常是在项目前期对全图进行一些启发式探索,当有进一步认知后再用第一种方式做精细的剪枝分析。 分析完 Nebula Graph 两种获取图结构方式后,下面来查看 Nebula Graph 的 Python 客户端代码,nebula-python/nebula/ngStorage/StorageClient.py 与 nebula-python/nebula/ngMeta/MetaClient.py 就是和底层存储交互的 API, 里面有扫描点、扫描边、读取一堆属性等等一系列丰富的接口。 下面两个接口可以用来读取所有的点、边数据: def scan_vertex(self, space, return_cols, all_cols, limit, start_time, end_time) def scan_edge(self, space, return_cols, all_cols, limit, start_time, end_time) 1) 初始化一个客户端,和一个 scan_edge_processor。scan_edge_processor 用来对读出来的边数据进行解码: meta_client = MetaClient([('192.168.8.16', 45500)]) meta_client.connect() storage_client = StorageClient(meta_client) scan_edge_processor = ScanEdgeProcessor(meta_client) 2) 初始化 scan_edge 接口的各项参数: space_name = 'nba' # 要读取的图空间名称 return_cols = {} # 要返回的边(或点)及其属性列 return_cols['serve'] = ['start_year', 'end_year'] return_cols['follow'] = ['degree'] allCols = False # 是否返回所有属性列,当该值为 False 时,仅返回在 returnCols 里指定的属性列,当为 True 时,返回所有属性列 limit = 100 # 最多返回的数据条数 start_time = 0 end_time = sys.maxsize 3) 调用 scan_part_edge 接口,该接口会返回一个 scan_edge_response 对象的迭代器: scan_edge_response_iterator = storage_client.scan_edge(space_name, return_cols, all_cols, limit, start_time, end_time) 4) 不断读取该迭代器所指向的 scan_edge_response 对象中的数据,直到读取完所有数据: while scan_edge_response_iterator.has_next(): scan_edge_response = scan_edge_response_iterator.next() if scan_edge_response is None: print("Error occurs while scaning edge") break process_edge(space, scan_edge_response) 其中,process_edge 是自定义的一个处理读出来边数据的函数,该函数可以先使用 scan_edge_processor 对 scan_edge_response 中的数据进行解码,解码后的数据可以直接打印出来,也可以做一些简单处理,另作他用,比如:将这些数据读入计算框架 NetworkX 里。 5) 处理数据。在这里我们将读出来的所有边都添加到 NetworkX 中的图G 里: def process_edge(space, scan_edge_response): result = scan_edge_processor.process(space, scan_edge_response) # Get the corresponding rows by edge_name for edge_name, edge_rows in result.rows.items(): for row in edge_rows: srcId = row.default_properties[0].get_value() dstId = row.default_properties[2].get_value() print('%d -> %d' % (srcId, dstId)) props = {} for prop in row.properties: prop_name = prop.get_name() prop_value = prop.get_value() props[prop_name] = prop_value G.add_edges_from([(srcId, dstId, props)]) # 添加边到 NetworkX 中的图G 读取顶点数据的方法和上面的流程类似。 此外,对于分布式的一些图计算框架[4]来说,Nebula Graph 还提供了根据分片 (partition) 并发地批量读取存储的功能,这会在之后的文章中演示。 在 NetworkX 中进行图分析 当我们把所有点和边数据都按照上述流程读入 NetworkX 后,我们还可以做一些基本的图分析和图计算: 1) 绘制图: nx.draw(G, with_labels=True, font_weight='bold') import matplotlib.pyplot as plt plt.show() plt.savefig('./test.png') 绘制出来的图: 2) 打印出图中的所有点和边: print('nodes: ', list(G.nodes)) print('edges: ', list(G.edges)) 输出的结果: nodes: [109, 119, 129, 139, 149, 209, 219, 229, 108, 118, 128, 138, 148, 208, 218, 228, 107, 117, 127, 137, 147, 207, 217, 227, 106, 116, 126, 136, 146, 206, 216, 226, 101, 111, 121, 131, 141, 201, 211, 221, 100, 110, 120, 130, 140, 150, 200, 210, 220, 102, 112, 122, 132, 142, 202, 212, 222, 103, 113, 123, 133, 143, 203, 213, 223, 104, 114, 124, 134, 144, 204, 214, 224, 105, 115, 125, 135, 145, 205, 215, 225] edges: [(109, 100), (109, 125), (109, 204), (109, 219), (109, 222), (119, 200), (119, 205), (119, 113), (129, 116), (129, 121), (129, 128), (129, 216), (129, 221), (129, 229), (129, 137), (139, 138), (139, 212), (139, 218), (149, 130), (149, 219), (209, 123), (219, 130), (219, 112), (219, 104), (229, 147), (229, 116), (229, 141), (229, 144), (108, 100), (108, 101), (108, 204), (108, 206), (108, 214), (108, 215), (108, 222), (118, 120), (118, 131), (118, 205), (118, 113), (128, 116), (128, 121), (128, 201), (128, 202), (128, 205), (128, 223), (138, 115), (138, 204), (138, 210), (138, 212), (138, 221), (138, 225), (148, 127), (148, 136), (148, 137), (148, 214), (148, 223), (148, 227), (148, 213), (208, 127), (208, 103), (208, 104), (208, 124), (218, 127), (218, 110), (218, 103), (218, 104), (218, 114), (218, 105), (228, 146), (228, 145), (107, 100), (107, 204), (107, 217), (107, 224), (117, 200), (117, 136), (117, 142), (127, 114), (127, 212), (127, 213), (127, 214), (127, 222), (127, 226), (127, 227), (137, 136), (137, 213), (137, 150), (147, 136), (147, 214), (147, 223), (207, 121), (207, 140), (207, 122), (207, 134), (217, 126), (217, 141), (217, 124), (217, 144), (106, 204), (106, 212), (106, 113), (116, 141), (116, 126), (116, 210), (116, 216), (116, 121), (116, 113), (116, 105), (126, 216), (136, 210), (136, 213), (136, 214), (146, 202), (146, 210), (146, 215), (146, 222), (146, 226), (206, 123), (216, 144), (216, 105), (226, 140), (226, 112), (226, 114), (226, 144), (101, 100), (101, 102), (101, 125), (101, 204), (101, 215), (101, 113), (101, 104), (111, 200), (111, 204), (111, 215), (111, 220), (121, 202), (121, 215), (121, 113), (121, 134), (131, 205), (131, 220), (141, 124), (141, 205), (141, 225), (201, 145), (211, 124), (221, 104), (221, 124), (100, 125), (100, 204), (100, 102), (100, 113), (100, 104), (100, 144), (100, 105), (110, 204), (110, 220), (120, 150), (120, 202), (120, 205), (120, 113), (140, 114), (140, 214), (140, 224), (150, 143), (150, 213), (200, 142), (200, 104), (200, 145), (210, 124), (210, 144), (210, 115), (210, 145), (102, 203), (102, 204), (102, 103), (102, 135), (112, 204), (122, 213), (122, 223), (132, 225), (202, 133), (202, 114), (212, 103), (222, 104), (103, 204), (103, 114), (113, 104), (113, 105), (113, 125), (113, 204), (133, 114), (133, 144), (143, 213), (143, 223), (203, 135), (213, 124), (213, 145), (104, 105), (104, 204), (104, 215), (114, 115), (114, 204), (134, 224), (144, 145), (144, 214), (204, 105), (204, 125)] 3) 常见的,可以计算两个点之间的最短路径: p1 = nx.shortest_path(G, source=114, target=211) print('顶点 114 到顶点 211 的最短路径: ', p1) 输出的结果: 顶点 114 到顶点 211 的最短路径: [114, 127, 208, 124, 211] 4) 也计算图中每个点的 PageRank 值,来看各自的影响力: print(nx.pagerank(G)) 输出的结果: {109: 0.011507076520104863, 119: 0.007835838669313514, 129: 0.015304593799331218, 139: 0.007772926737873626, 149: 0.0073896601012629825, 209: 0.0065558926178649985, 219: 0.014100908598251508, 229: 0.011454115940170253, 108: 0.01645334474680034, 118: 0.01010598371500564, 128: 0.01594717876199238, 138: 0.01671097227127263, 148: 0.015898676579503977, 208: 0.009437234075904938, 218: 0.0153795416919104, 228: 0.005900393773635255, 107: 0.009745182763645681, 117: 0.008716335675518244, 127: 0.021565565312365507, 137: 0.011642680498867146, 147: 0.009721031073465738, 207: 0.01040504770909835, 217: 0.012054472529765329, 227: 0.005615576255373405, 106: 0.007371191843767635, 116: 0.020955704443679106, 126: 0.007589432032220849, 136: 0.015987209357117116, 146: 0.013922108926721374, 206: 0.008554794629575304, 216: 0.011219193251536395, 226: 0.013613173390725904, 101: 0.016680863106330837, 111: 0.010121524312495604, 121: 0.017545503989576015, 131: 0.008531567756846938, 141: 0.014598319866130227, 201: 0.0058643663430632525, 211: 0.003936285336338021, 221: 0.009587911774927793, 100: 0.02243017302167168, 110: 0.007928429795381916, 120: 0.011875669801396205, 130: 0.0073896601012629825, 140: 0.01205992633948699, 150: 0.010045605782606326, 200: 0.015289870550944322, 210: 0.017716629501785937, 220: 0.008666577509181518, 102: 0.014865431161046641, 112: 0.007931095811770324, 122: 0.008087439927630492, 132: 0.004659566123187912, 142: 0.006487446038191551, 202: 0.013579313206377282, 212: 0.01190888044566142, 222: 0.011376739416933006, 103: 0.013438110749144392, 113: 0.02458154500563397, 123: 0.01104978432213578, 133: 0.00743370900670294, 143: 0.008011123394996112, 203: 0.006883198710237787, 213: 0.020392557117890422, 223: 0.012345866520333572, 104: 0.024902235588979776, 114: 0.019369722463816744, 124: 0.017165705442951484, 134: 0.008284361176173354, 144: 0.019363506469972095, 204: 0.03507634139024834, 214: 0.015500649025348538, 224: 0.008320315540621754, 105: 0.01439975542831122, 115: 0.007592722237637133, 125: 0.010808523955754608, 135: 0.006883198710237788, 145: 0.014654713389044883, 205: 0.014660118545887803, 215: 0.01337467974572934, 225: 0.009909720748343093} 此外,也可以和上一篇中一样,接入Gephi [5]来得到更好的图可视化效果。 本文的代码可以参见[6]. Reference [1] https://nebula-graph.com.cn/posts/game-of-thrones-relationship-networkx-gephi-nebula-graph/ [2] https://networkx.github.io/ [3] https://github.com/vesoft-inc/nebula [4] https://spark.apache.org/graphx/ [5] https://gephi.org/ [6] https://github.com/vesoft-inc/nebula-python/pull/31 喜欢这篇文章?来来来,给我们的 GitHub 点个 star 表鼓励啦~~ ♂️♀️ [手动跪谢] 交流图数据库技术?交个朋友,Nebula Graph 官方小助手微信:NebulaGraphbot 拉你进交流群~~ 作者有话说:Hi,我是王杰,是图数据 Nebula Graph 研发工程师,希望本次的经验分享能给大家带来帮助,如有不当之处也希望能帮忙纠正,谢谢~
我们都知道《权利的游戏》在全世界都很多忠实的粉丝,除去你永远不知道剧情下一秒谁会挂这种意外“惊喜”,当中复杂交错的人物关系也是它火爆的原因之一,而本文介绍如何通过 NetworkX 访问开源的分布式图数据库 Nebula Graph,并借助可视化工具—— Gephi 来可视化分析《权力的游戏》中的复杂的人物图谱关系。 数据集 本文的数据集来源:冰与火之歌第一卷(至第五卷)[1] 人物集 (点集):书中每个角色建模为一个点,点只有一个属性:姓名 关系集(边集):如果两个角色在书中发生过直接或间接的交互,则有一条边;边只有一个属性:权重,权重的大小代表交互的强弱。 这样的点集和边集构成一个图网络,这个网络存储在图数据库 Nebula Graph [2]中。 社区划分——Girvan-Newman 算法 我们使用 NetworkX [3] 内置的社区发现算法 Girvan-Newman 来为我们的图网络划分社区。 以下为「社区发现算法 Girvan-Newman」解释: 网络图中,连接较为紧密的部分可以被看成一个社区。每个社区内部节点之间有较为紧密的连接,而在两个社区间连接则较为稀疏。社区发现就是找到给定网络图所包含的一个个社区的过程。 Girvan-Newman 算法即是一种基于介数的社区发现算法,其基本思想是根据边介数中心性(edge betweenness)从大到小的顺序不断地将边从网络中移除直到整个网络分解为各个社区。因此,Girvan-Newman 算法实际上是一种分裂方法。 Girvan-Newman 算法的基本流程如下:(1)计算网络中所有边的边介数;(2)找到边介数最高的边并将它从网络中移除;(3)重复步骤 2,直到每个节点成为一个独立的社区为止,即网络中没有边存在。 概念解释完毕,下面来实操下。 使用 Girvan-Newman 算法划分社区。NetworkX 示例代码如下 comp = networkx.algorithms.community.girvan_newman(G) k = 7 limited = itertools.takewhile(lambda c: len(c) <= k, comp) communities = list(limited)[-1] 为图中每个点添加一个 community 属性,该属性值记录该点所在的社区编号 community_dict = {} community_num = 0 for community in communities: for character in community: community_dict[character] = community_num community_num += 1 nx.set_node_attributes(G, community_dict, 'community') 节点样式——Betweenness Centrality 算法 下面我们来调整下节点大小及节点上标注的角色姓名大小,我们使用 NetworkX 的 Betweenness Centrality 算法来决定节点大小及节点上标注的角色姓名的大小。 图中各个节点的重要性可以通过节点的中心性(Centrality)来衡量。在不同的网络中往往采用了不同的中心性定义来描述网络中节点的重要性。Betweenness Centrality 根据有多少最短路径经过该节点,来判断一个节点的重要性。 计算每个节点的介数中心性的值 betweenness_dict = nx.betweenness_centrality(G) # Run betweenness centrality 为图中每个点再添加一个 betweenness 属性 nx.set_node_attributes(G, betweenness_dict, 'betweenness') 边的粗细 边的粗细直接由边的权重属性来决定。 通过上面的处理,现在,我们的节点拥有 name、community、betweenness 三个属性,边只有一个权重 weight 属性。 下面显示一下: import matplotlib.pyplot as plt color = 0 color_map = ['red', 'blue', 'yellow', 'purple', 'black', 'green', 'pink'] for community in communities: nx.draw(G, pos = nx.spring_layout(G, iterations=200), nodelist = community, node_size = 100, node_color = color_map[color]) color += 1 plt.savefig('./game.png') emmm,有点丑… 虽然 NetworkX 本身有不少可视化功能,但 Gephi [4] 的交互和可视化效果更好。 接入可视化工具 Gephi 现在将上面的 NetworkX 数据导出为 game.gephi 文件,并导入 Gephi。 nx.write_gexf(G, 'game.gexf') Gephi 可视化效果展示 在 Gephi 中打开刚才导出的 game.gephi 文件,然后微调 Gephi 中的各项参数,就以得到一张满意的可视化: 将布局设置为 Force Atlas, 斥力强度改为为 500.0, 勾选上 由尺寸调整 选项可以尽量避免节点重叠: Force Atlas 为力引导布局,力引导布局方法能够产生相当优美的网络布局,并充分展现网络的整体结构及其自同构特征。力引导布局即模仿物理世界的引力和斥力,自动布局直到力平衡。 给划分好的各个社区网络画上不同的颜色: 在外观-节点-颜色-Partition 中选择 community(这里的 community 就是我们刚才为每个点添加的社区编号属性) 决定节点及节点上标注的角色姓名的大小: 在外观-节点-大小-Ranking 中选择 betweenness(这里的 betweenness 就是我们刚才为每个点添加的 betweenness 属性) 边的粗细由边的权重属性来决定: 在外观-边-大小-Ranking 中选择边的权重 导出图片再加个头像效果 大功告成,一张权力游戏的关系谱图上线 :) 每个节点可以看到对应的人物信息。 下一篇 本篇主要介绍如何使用 NetworkX,并通过 Gephi 做可视化展示。下一篇将介绍如何通过 NetworkX 访问图数据库 Nebula Graph 中的数据。 本文的代码可以访问[5]。 致谢:本文受工作 [6] 的启发 Reference [1] https://www.kaggle.com/mmmarchetti/game-of-thrones-dataset[2] https://github.com/vesoft-inc/nebula[3] https://networkx.github.io/[4] https://gephi.org/[5] https://github.com/jievince/nx2gephi[6] https://www.lyonwj.com/2016/06/26/graph-of-thrones-neo4j-social-network-analysis/ 作者有话说:Hi,我是王杰,是图数据 Nebula Graph 研发工程师,希望本次的经验分享能给大家带来帮助,如有不当之处也希望能帮忙纠正,谢谢~
由于 Nebula Graph 的底层存储使用了 RocksDB,出于运维管理需要,我们的社区用户 @chenxu14 在 pr#2243 为 Nebula Graph 贡献了 RocksDB 统计信息收集的功能 通过在 storage 服务配置文件中修改 --enable_rocksdb_statistics = true 即可开启收集 RocksDB 统计信息的功能。开启后,将会定期将统计信息转储到每个 DB 服务的日志文件中。 最近,chenxu14 为此功能带来了新的用法——支持通过 storage 服务自带的 Web 接口获取统计信息。此次 pr 提供了 3 种通过 Web 服务获取统计信息的方法: 获取全部统计信息; 获取指定条目的信息; 支持把结果以 json 格式返回。 下面让我们来体验一下这次的新功能吧~ 在 storage 的配置文件中修改:--enable_rocksdb_statistics = true 以开启收集 RocksDB 统计信息,修改后重启 storage 服务即可生效 访问 http://storage_ip:port/rocksdb_stats 获取 RocksDB 全部统计信息(部分截图展示) 访问 http://storage_ip:port/rocksdb_stats?stats=stats_name 获取部分 RocksDB 统计信息 在返回部分结果的查询地址基础上添加 & returnjson 获取部分 RocksDB 统计信息并以 json 格式返回 至此,本次特性讲解完毕,遇到问题?上 Nebula Graph 论坛:https://discuss.nebula-graph.com.cn/ 喜欢这篇文章?来啦,给我们的 GitHub 点个 star 表鼓励呗~~ ♂️♀️ [手动跪谢] 交流图数据库技术?交个朋友,Nebula Graph 官方小助手微信:NebulaGraphbot 拉你进交流群~~
摘要:在本文中,我们将通过数据流快速学习 Nebula Graph,以用户在客户端输入一条 nGQL 语句 SHOW SPACES 为例,使用 GDB 追踪语句输入时 Nebula Graph 是怎么调用和运行的。 首发于 Nebula Graph 博客:https://nebula-graph.com.cn/posts/how-to-read-nebula-graph-source-code/ 导读 对于一些刚开始接触 Nebula Graph 开源库的小伙伴来说,刚开始可能和我一样,想要提高自己,看看大神们的代码然后试着能够做点什么,或许能够修复一个看起来并不是那么困难的 Bug。但是面对如此多的代码,我裂开了,不知道如何下手。最后硬着头皮,再看了一遍又一遍代码,跑了一个又一个用例之后终于有点眉目了。 下面就分享下个人学习 Nebula Graph 开源代码的过程,也希望刚接触 Nebula Graph 的小伙伴能够少走弯路,快速入门。另外 Nebula Graph 本身也用到了一些开源库,详情可以见附录。 在本文中,我们将通过数据流快速学习 Nebula Graph,以用户在客户端输入一条 nGQL 语句 SHOW SPACES 为例,使用 GDB 追踪语句输入时 Nebula Graph 是怎么调用和运行的。 整体架构 一个完整的 Nebula Graph 包含三个服务,即 Query Service,Storage Service 和 Meta Service。每个服务都有其各自的可执行二进制文件。 Query Service 主要负责 客户端连接的管理 解析来自客户端的 nGQL 语句为抽象语法树 AST,并将抽象树 AST 解析成一系列执行动作。 对执行动作进行优化 执行优化后的执行计划 Storage Service 主要负责 数据的分布式存储 Meta Service 主要负责 图 schema 的增删查改 集群的管理 用户鉴权 这次,我们主要对 Query Service 进行分析 目录结构 刚开始,可以拿到一个 source 包,解压,可以先看看代码的层级关系,不同的包主要功能是干什么的 下面只列出 src 目录: |--src |--client // 客户端代码 |--common // 提供一些常用的基础组件 |--console |--daemons |--dataman |--graph // 包含了Query Service的大部分代码 |--interface // 主要是一些 meta、storage 和 graph 的通讯接口定义 |--jni |--kvstore |--meta // 元数据管理相关 |--parser // 主要负责词法和语法分析 |--storage // 存储层相关 |--tools |--webservice 代码跟踪 通过 scripts 目录下的脚本启动 metad 和 storaged 这两个服务: 启动后通过 nebula.service status all 查看当前的服务状态 然后 gdb 运行 bin 目录下的 nebula-graphd 二进制程序 gdb> set args --flagfile /home/mingquan.ji/1.0/nebula-install/etc/nebula-graphd.conf //设置函数入参 gdb> set follow-fork-mode child // 由于是守护进程,所以在 fork 子进程后 gdb 继续跟踪子进程 gdb> b main // 在 mian 入口打断点 在 gdb 中输入 run 开始运行 nebula-graphd 程序,然后通过 next 可以一步一步运行,直到遇到 gServer->serve(); // Blocking wait until shut down via gServer->stop(),此时 nebula-graphd 的所有线程阻塞,等待客户端连接,这时需要找到客户端发起请求后由哪个函数处理。 由于 Nebula Graph 使用 FBThrift 来定义生成不同服务的通讯代码,在 src/interface/graph.thrift 文件中可以看到 GraphService 接口的定义如下: service GraphService { AuthResponse authenticate(1: string username, 2: string password) oneway void signout(1: i64 sessionId) ExecutionResponse execute(1: i64 sessionId, 2: string stmt) } 在 gServer->serve() 之前有 auto interface = std::make_shared<GraphService>(); status = interface->init(ioThreadPool); gServer->setInterface(std::move(interface)); gServer->setAddress(localIP, FLAGS_port); 可以知道是由 GraphService 对象来处理客户端的连接和请求,因此可以在 GraphService.cpp:`future_execute` 处打断点,以便跟踪后续处理流程。 此时重新打开一个终端进入 nebula 安装目录,通过 ./nebule -u=root -p=nebula 来连接 nebula 服务,再在客户端输入 SHOW SPACES ,此时客户端没有反应,是因为服务端还在阻塞调试中,回到服务端输入 continue,如下所示: 经过 session 验证后,进入 executionEngine->execute() 中,step 进入函数内部 auto plan = new ExecutionPlan(std::move(ectx)); plan->execute(); 继续 step 进入ExecutionPlan 的 execute 函数内部,然后执行到 auto result = GQLParser().parse(rctx->query()); parse 这块主要使用 flex & bison,用于词法分析和语法解析构造对象到抽象语法树,其词法文件是 src/parser/scanner.lex,语法文件是 src/parser/parser.yy,其词法分析类似于正则表达式,语法分析举例如下: go_sentence : KW_GO step_clause from_clause over_clause where_clause yield_clause { auto go = new GoSentence(); go->setStepClause($2); go->setFromClause($3); go->setOverClause($4); go->setWhereClause($5); if ($6 == nullptr) { auto *cols = new YieldColumns(); for (auto e : $4->edges()) { if (e->isOverAll()) { continue; } auto *edge = new std::string(*e->edge()); auto *expr = new EdgeDstIdExpression(edge); auto *col = new YieldColumn(expr); cols->addColumn(col); } $6 = new YieldClause(cols); } go->setYieldClause($6); $$ = go; } 其在匹配到对应到 go 语句时,就构造对应的节点,然后由 bison 处理,最后生成一个抽象的语法树。 词法语法分析后开始执行模块,继续 gdb,进入 excute 函数,一直 step 直到进入ShowExecutor::execute 函数。 继续 next 直到 showSpaces(),step 进入此函数 auto future = ectx()->getMetaClient()->listSpaces(); auto *runner = ectx()->rctx()->runner(); ''' ''' std::move(future).via(runner).thenValue(cb).thenError(error); 此时 Query Service 通过 metaClient 和 Meta Service 通信拿到 spaces 数据,之后通过回调函数 cb 回传拿到的数据,至此 nGQL 语句 SHOW SPACES; 已经执行完毕,而其他复杂的语句也可以以此类推。 如果是正在运行的服务,可以先查出该服务的进程 ID,然后通过 gdb attach PID 来调试该进程; 如果不想启动服务端和客户端进行调试,在 src 目录下的每个文件夹下都有一个 test 目录,里面都是对对应模块或者功能进行的单元测试,可以直接编译对应的单元模块,然后跟踪运行。方法如下: 通过对应目录下的 CMakeLists.txt 文件找到对应的模块名 在 build 目录下 make 模块名,在 build/bin/test 目录下生成对应的二进制程序 gdb 跟踪调试该程序 附录 阅读 Nebula Graph 源码需要了解的一些库: flex & bison:词法分析和语法分析工具,将客户端输入的 nGQL 语句解析为抽象语法树 FBThrift:Facebook 开源的 RPC 框架,定义并生成了 Meta 层、Storage 层和 Graph 层的通讯过程代码 folly:Facebook 开源的 C++14 组件库,提供了类似 Boost 和 std 库的功能,在性能上更加优化 Gtest:Google 开源的 C++ 单元测试框架 其中数据库资料可以参考: 数据库基本介绍 SQL调优 Nebula 架构剖析系列(零)图数据库的整体架构设计 喜欢这篇文章?来来来,给我们的 GitHub 点个 star 表鼓励啦~~ ♂️♀️ [手动跪谢] 交流图数据库技术?交个朋友,Nebula Graph 官方小助手微信:NebulaGraphbot 拉你进交流群~~ 作者有话说:Hi,我是明泉,是图数据 Nebula Graph 研发工程师,主要工作和数据库查询引擎相关,希望本次的经验分享能给大家带来帮助,如有不当之处也希望能帮忙纠正,谢谢~
摘要:这篇文章将介绍图数据库 Nebula Graph 的查询语言 nGQL 和 SQL 的区别。 本文首发于 Nebula Graph 官方博客:https://nebula-graph.com.cn/posts/sql-vs-ngql-comparison/ 虽然本文主要介绍 nGQL 和 SQL 的区别,但是我们不会深入探讨这两种语言,而是将这两种语言做对比,以帮助你从 SQL 过渡到 nGQL。 SQL (Structured Query Language) 是具有数据操纵和数据定义等多种功能的数据库语言,这种语言是一种特定目的编程语言,用于管理关系数据库管理系统(RDBMS),或在关系流数据管理系统(RDSMS)中进行流处理。 nGQL 是一种类 SQL 的声明型的文本查询语言,相比于 SQL, nGQL 为可扩展、支持图遍历、模式匹配、分布式事务(开发中)的图数据库查询语言。 概念对比 对比项 SQL nGQL 点 \ 点 边 \ 边 点类型 \ tag 边类型 \ edge type 点 ID 主键 vid 边 ID 复合主键 起点、终点、rank 列 列 点或边的属性 行 行 点或边 语法对比 数据定义语言 (DDL) 数据定义语言(DDL)用于创建或修改数据库的结构,也就是 schema。 对比项 SQL nGQL 创建图空间(数据库) CREATE DATABASE <database_name> CREATE SPACE <space_name> 列出图空间(数据库) SHOW DATABASES SHOW SPACES 使用图空间(数据库) USE <database_name> USE <space_name> 删除图空间(数据库) DROP DATABASE <database_name> DROP SPACE <space_name> 修改图空间(数据库) ALTER DATABASE <database_name> alter_option \ 创建 tags/edges \ CREATE TAG | EDGE <tag_name> 创建表 CREATE TABLE <tbl_name> (create_definition,...) \ 列出表列名 SHOW COLUMNS FROM <tbl_name> \ 列出 tags/edges \ SHOW TAGS | EDGES Describe tags/edge \ DESCRIBE TAG | EDGE ` edge_name>` 修改 tags/edge \ ALTER TAG | EDGE ` edge_name>` 修改表 ALTER TABLE <tbl_name> \ 索引 对比项 SQL nGQL 创建索引 CREATE INDEX CREATE {TAG | EDGE} INDEX 删除索引 DROP INDEX DROP {TAG | EDGE} INDEX 列出索引 SHOW INDEX FROM SHOW {TAG | EDGE} INDEXES 重构索引 ANALYZE TABLE REBUILD {TAG | EDGE} INDEX <index_name> [OFFLINE] 数据操作语言(DML) 数据操作语言(DML)用于操作数据库中的数据。 对比项 SQL nGQL 插入数据 INSERT IGNORE INTO <tbl_name> [(col_name [, col_name] ...)] {VALUES | VALUE} [(value_list) [, (value_list)] INSERT VERTEX <tag_name> (prop_name_list[, prop_name_list]) {VALUES | VALUE} vid: (prop_value_list[, prop_value_list]) INSERT EDGE <edge_name> ( <prop_name_list> ) VALUES | VALUE <src_vid> -> <dst_vid>[@<rank>] : ( <prop_value_list> ) 查询数据 SELECT GO, FETCH 更新数据 UPDATE <tbl_name> SET field1=new-value1, field2=new-value2 [WHERE Clause] UPDATE VERTEX <vid> SET <update_columns> [WHEN <condition>] UPDATE EDGE <edge> SET <update_columns> [WHEN <condition>] 删除数据 DELETE FROM <tbl_name> [WHERE Clause] DELETE EDGE <edge_type> <vid> -> <vid>[@<rank>] [, <vid> -> <vid> ...] DELETE VERTEX <vid_list> 拼接数据 JOIN `\ ` 数据查询语言(DQL) 数据查询语言(DQL)语句用于执行数据查询。本节说明如何使用 SQL 语句和 nGQL 语句查询数据。 SELECT [DISTINCT] select_expr [, select_expr] ... [FROM table_references] [WHERE where_condition] [GROUP BY {col_name | expr | position}] [HAVING where_condition] [ORDER BY {col_name | expr | position} [ASC | DESC]] GO [[<M> TO] <N> STEPS ] FROM <node_list> OVER <edge_type_list> [REVERSELY] [BIDIRECT] [WHERE where_condition] [YIELD [DISTINCT] <return_list>] [| ORDER BY <expression> [ASC | DESC]] [| LIMIT [<offset_value>,] <number_rows>] [| GROUP BY {col_name | expr | position} YIELD <col_name>] <node_list> | <vid> [, <vid> ...] | $-.id <edge_type_list> edge_type [, edge_type ...] <return_list> <col_name> [AS <col_alias>] [, <col_name> [AS <col_alias>] ...] 数据控制语言(DCL) 数据控制语言(DCL)包含诸如 GRANT 和 REVOKE 之类的命令,这些命令主要用来处理数据库系统的权限、其他控件。 对比项 SQL nGQL 创建用户 CREATE USER CREATE USER 删除用户 DROP USER DROP USER 更改密码 SET PASSWORD CHANGE PASSWORD 授予权限 GRANT <priv_type> ON [object_type] TO <user> GRANT ROLE <role_type> ON <space> TO <user> 删除权限 REVOKE <priv_type> ON [object_type] TO <user> REVOKE ROLE <role_type> ON <space> FROM <user> 数据模型 查询语句基于以下数据模型: RDBMS 关系结构图 Nebula Graph 最小模型图 本文将使用 NBA 数据集。该数据集包含两种类型的点,也就是两个标签,即 player 和 team ;两种类型的边,分别是 serve 和 follow。 在关系型数据管理系统中(RDBMS)中,我们用表来表示点以及与点相关的边(连接表)。因此,我们创建了以下表格:player、team、serve 和 follow。在 Nebula Graph 中,基本数据单位是顶点和边。两者都可以拥有属性,相当于 RDBMS 中的属性。 在 Nebula Graph 中,点之间的关系由边表示。每条边都有一种类型,在 NBA 数据集中,我们使用边类型 serve 和 follow 来区分两种类型的边。 示例数据 在 RDBMS 插入数据 首先,让我们看看如何在 RDBMS 中插入数据。我们先创建一些表,然后为这些表插入数据。 CREATE TABLE player (id INT, name VARCHAR(100), age INT); CREATE TABLE team (id INT, name VARCHAR(100)); CREATE TABLE serve (player_id INT, team_id INT, start_year INT, end_year INT); CREATE TABLE follow (player_id1 INT, player_id2 INT, degree INT); 然后插入数据。 INSERT INTO player VALUES (100, 'Tim Duncan', 42), (101, 'Tony Parker', 36), (102, 'LaMarcus Aldridge', 33), (103, 'Rudy Gay',32), (104, 'Marco Belinelli', 32), (105, 'Danny Green', 31), (106, 'Kyle Anderson', 25), (107, 'Aron Baynes', 32), (108, 'Boris Diaw', 36), (109, 'Tiago Splitter', 34), (110, 'Cory Joseph', 27); INSERT INTO team VALUES (200, 'Warriors'), (201, 'Nuggets'), (202, 'Rockets'), (203, 'Trail'), (204, 'Spurs'), (205, 'Thunders'), (206, 'Jazz'), (207, 'Clippers'), (208, 'Kings'); INSERT INTO serve VALUES (100,200,1997,2016), (101,200,1999,2010), (102,200,2001,2005), (106,200,2000,2011), (107,200,2001,2009), (103,201,1999,2018), (104,201,2006,2015), (107,201,2007,2010), (108,201,2010,2016), (109,201,2011,2015), (105,202,2015,2019), (109,202,2017,2019), (110,202,2007,2009); INSERT INTO follow VALUES (100,101,95), (100,102,91), (100,106,90), (101,100,95), (101,102,91), (102,101,75), (103,102,70), (104,103,50), (104,105,60), (105,104,83), (105,110,87), (106,100,88), (106,107,81), (107,106,92), (107,108,97), (108,109,95), (109,110,78), (110,109,72), (110,105,85); 在 Nebula Graph 插入数据 在 Nebula Graph 中插入数据与上述类似。首先,我们需要定义好数据结构,也就是创建好 schema。然后可以选择手动或使用 Nebula Graph Studio (Nebula Graph 的可视化工具)导入数据。这里我们手动添加数据。 在下方的 INSERT 插入语句中,我们向图空间 NBA 插入了球员数据(这和在 MySQL 中插入数据类似)。 INSERT VERTEX player(name, age) VALUES 100: ('Tim Duncan', 42), 101: ('Tony Parker', 36), 102: ('LaMarcus Aldridge', 33), 103: ('Rudy Gay', 32), 104: ('Marco Belinelli', 32), 105: ('Danny Green', 31), 106: ('Kyle Anderson', 25), 107: ('Aron Baynes', 32), 108: ('Boris Diaw', 36), 109: ('Tiago Splitter', 34), 110: ('Cory Joseph', 27); 考虑到篇幅限制,此处我们将跳过插入球队和边的重复步骤。你可以点击此处下载示例数据亲自尝试。 增删改查(CRUD) 本节介绍如何使用 SQL 和 nGQL 语句创建(C)、读取(R)、更新(U)和删除(D)数据。 插入数据 mysql> INSERT INTO player VALUES (100, 'Tim Duncan', 42); nebula> INSERT VERTEX player(name, age) VALUES 100: ('Tim Duncan', 42); 查询数据 查找 ID 为 100 的球员并返回其 name 属性: mysql> SELECT player.name FROM player WHERE player.id = 100; nebula> FETCH PROP ON player 100 YIELD player.name; 更新数据 mysql> UPDATE player SET name = 'Tim'; nebula> UPDATE VERTEX 100 SET player.name = "Tim"; 删除数据 mysql> DELETE FROM player WHERE name = 'Tim'; nebula> DELETE VERTEX 121; nebula> DELETE EDGE follow 100 -> 200; 建立索引 返回年龄超过 36 岁的球员。 SELECT player.name FROM player WHERE player.age < 36; 使用 nGQL 查询有些不同,因为您必须在过滤属性之前创建索引。更多信息请参见 索引文档。 CREATE TAG INDEX player_age ON player(age); REBUILD TAG INDEX player_age OFFLINE; LOOKUP ON player WHERE player.age < 36; 示例查询 本节提供一些示例查询供您参考。 示例 1 在表 player 中查询 ID 为 100 的球员并返回其 name 属性。 SELECT player.name FROM player WHERE player.id = 100; 接下来使用 Nebula Graph 查找 ID 为 100 的球员并返回其 name 属性。 FETCH PROP ON player 100 YIELD player.name; Nebula Graph 使用 FETCH 关键字获取特定点或边的属性。本例中,属性即为点 100 的名称。nGQL 中的 YIELD 关键字相当于 SQL 中的 SELECT。 示例 2 查找球员 Tim Duncan 并返回他效力的所有球队。 SELECT a.id, a.name, c.name FROM player a JOIN serve b ON a.id=b.player_id JOIN team c ON c.id=b.team_id WHERE a.name = 'Tim Duncan' 使用如下 nGQL 语句完成相同操作: CREATE TAG INDEX player_name ON player(name); REBUILD TAG INDEX player_name OFFLINE; LOOKUP ON player WHERE player.name == 'Tim Duncan' YIELD player.name AS name | GO FROM $-.VertexID OVER serve YIELD $-.name, $$.team.name; 这里需要注意一下,在 nGQL 中的等于操作采用的是 C 语言风格的 ==,而不是SQL风格的 =。 示例 3 以下查询略复杂,现在我们来查询球员 Tim Duncan 的队友。 SELECT a.id, a.name, c.name FROM player a JOIN serve b ON a.id=b.player_id JOIN team c ON c.id=b.team_id WHERE c.name IN (SELECT c.name FROM player a JOIN serve b ON a.id=b.player_id JOIN team c ON c.id=b.team_id WHERE a.name = 'Tim Duncan') nGQL 则使用管道将前一个子句的结果作为下一个子句的输入。 GO FROM 100 OVER serve YIELD serve._dst AS Team | GO FROM $-.Team OVER serve REVERSELY YIELD $$.player.name; 您可能已经注意到了,我们仅在 SQL 中使用了 JOIN。这是因为 Nebula Graph 只是使用类似 Shell 的管道对子查询进行嵌套,这样更符合我们的阅读习惯也更简洁。 参考资料 我们建议您亲自尝试上述查询语句,这将帮您更好地理解 SQL 和 nGQL,并节省您上手 nGQL 的学习时间。以下是一些参考资料: Nebula Graph Studio 用户指南 Nebula Graph GitHub 仓库 Nebula Graph 快速入门文档 作者有话说:Hi,Hi ,大家好,我是 Amber,Nebula Graph 的文档工程师,希望上述内容可以给大家带来些许启发。限于水平,如有不当之处还请斧正,在此感谢^^ 喜欢这篇文章?来来来,给我们的 GitHub 点个 star 表鼓励啦~~ ♂️♀️ [手动跪谢] 交流图数据库技术?交个朋友,Nebula Graph 官方小助手微信:NebulaGraphbot 拉你进交流群~~
从 Hadoop 说起 近年来随着大数据的兴起,分布式计算引擎层出不穷。Hadoop 是 Apache 开源组织的一个分布式计算开源框架,在很多大型网站上都已经得到了应用。Hadoop 的设计核心思想来源于 Google MapReduce 论文,灵感来自于函数式语言中的 map 和 reduce 方法。在函数式语言中,map 表示针对列表中每个元素应用一个方法,reduce 表示针对列表中的元素做迭代计算。通过 MapReduce 算法,可以将数据根据某些特征进行分类规约,处理并得到最终的结果。 再谈 Apache Spark Apache Spark 是一个围绕速度、易用性构建的通用内存并行计算框架。在 2009 年由加州大学伯克利分校 AMP 实验室开发,并于 2010 年成为 Apache 基金会的开源项目。Spark 借鉴了 Hadoop 的设计思想,继承了其分布式并行计算的优点,提供了丰富的算子。 Spark 提供了一个全面、统一的框架用于管理各种有着不同类型数据源的大数据处理需求,支持批量数据处理与流式数据处理。Spark 支持内存计算,性能相比起 Hadoop 有着巨大提升。Spark 支持 Java,Scala 和 Python 三种语言进行编程,支持以操作本地集合的方式操作分布式数据集,并且支持交互查询。除了经典的 MapReduce 操作之外,Spark 还支持 SQL 查询、流式处理、机器学习和图计算。 弹性分布式数据集(RDD,Resilient Distributed Dataset)是 Spark 最基本的抽象,代表不可变的分区数据集。RDD 具有可容错和位置感知调度的特点。操作 RDD 就如同操作本地数据集合,而不必关心任务调度与容错等问题。RDD 允许用户在执行多个查询时,显示地将工作集合缓存在内存中,后续查询能够重用该数据集。RDD 通过一系列的转换就就形成了 DAG,根据 RDD 之间的依赖关系的不同将 DAG 划分成不同的 Stage。 与 RDD 相似,DataFrame 也是一个不可变分布式数据集合。区别于 RDD,DataFrame 中的数据被组织到有名字的列中,就如同关系型数据库中的表。设计 DataFrame 的目的就是要让对大型数据集的处理变得更简单,允许开发者为分布式数据集指定一个模式,便于进行更高层次的抽象。 DataSet 是一个支持强类型的特定领域对象,这种对象可以函数式或者关系操作并行地转换。DataSet 就是一些有明确类型定义的 JVM 对象的集合,可以通过 Scala 中定义的 Case Class 或者 Java 中的 Class 来指定。DataFrame 是 Row 类型的 Dataset,即 Dataset[Row]。DataSet 的 API 是强类型的;而且可以利用这些模式进行优化。 DataFrame 与 DataSet 只在执行行动操作时触发计算。本质上,数据集表示一个逻辑计划,该计划描述了产生数据所需的计算。当执行行动操作时,Spark 的查询优化程序优化逻辑计划,并生成一个高效的并行和分布式物理计划。 基于 Spark 的数据导入工具 Spark Writer 是 Nebula Graph 基于 Spark 的分布式数据导入工具,基于 DataFrame 实现,能够将多种数据源中的数据转化为图的点和边批量导入到图数据库中。 目前支持的数据源有:Hive 和HDFS。 Spark Writer 支持同时导入多个标签与边类型,不同标签与边类型可以配置不同的数据源。 Spark Writer 通过配置文件,从数据中生成一条插入语句,发送给查询服务,执行插入操作。Spark Writer 中插入操作使用异步执行,通过 Spark 中累加器统计成功与失败数量。 获取 Spark Writer 编译源码 git clone https://github.com/vesoft-inc/nebula.git cd nebula/src/tools/spark-sstfile-generator mvn compile package 标签数据文件格式 标签数据文件由一行一行的数据组成,文件中每一行表示一个点和它的属性。一般来说,第一列为点的 ID ——此列的名称将在后文的映射文件中指定,其他列为点的属性。例如Play标签数据文件格式: {"id":100,"name":"Tim Duncan","age":42} {"id":101,"name":"Tony Parker","age":36} {"id":102,"name":"LaMarcus Aldridge","age":33} 边类型数据文件格式 边类型数据文件由一行一行的数据组成,文件中每一行表示一条边和它的属性。一般来说,第一列为起点 ID,第二列为终点 ID,起点 ID 列及终点 ID 列会在映射文件中指定。其他列为边属性。下面以 JSON 格式为例进行说明。 以边类型 follow 数据为例: {"source":100,"target":101,"likeness":95} {"source":101,"target":100,"likeness":95} {"source":101,"target":102,"likeness":90} {"source":100,"target":101,"likeness":95,"ranking":2} {"source":101,"target":100,"likeness":95,"ranking":1} {"source":101,"target":102,"likeness":90,"ranking":3} 配置文件格式 Spark Writer 使用 HOCON 配置文件格式。HOCON(Human-Optimized Config Object Notation)是一个易于使用的配置文件格式,具有面向对象风格。配置文件由 Spark 配置段,Nebula 配置段,以及标签配置段和边配置段四部分组成。 Spark 信息配置了 Spark 运行的相关参数,Nebula 相关信息配置了连接 Nebula 的用户名和密码等信息。 tags 映射和 edges 映射分别对应多个 tag/edge 的输入源映射,描述每个 tag/edge 的数据源等基本信息,不同 tag/edge 可以来自不同数据源。 Nebula 配置段主要用于描述 nebula 查询服务地址、用户名和密码、图空间信息等信息。 nebula: { # 查询引擎 IP 列表 addresses: ["127.0.0.1:3699"] # 连接 Nebula Graph 服务的用户名和密码 user: user pswd: password # Nebula Graph 图空间名称 space: test # thrift 超时时长及重试次数,默认值分别为 3000 和 3 connection { timeout: 3000 retry: 3 } # nGQL 查询重试次数,默认值为 3 execution { retry: 3 } } Nebula 配置段 标签配置段用于描述导入标签信息,数组中每个元素为一个标签信息。标签导入主要分为两种:基于文件导入与基于 Hive 导入。 基于文件导入配置需指定文件类型 基于 Hive 导入配置需指定执行的查询语言。 # 处理标签 tags: [ # 从 HDFS 文件加载数据, 此处数据类型为 Parquet tag 名称为 ${TAG_NAME} # HDFS Parquet 文件的中的 field_0、field_1将写入 ${TAG_NAME} # 节点列为 ${KEY_FIELD} { name: ${TAG_NAME} type: parquet path: ${HDFS_PATH} fields: { field_0: nebula_field_0, field_1: nebula_field_1 } vertex: ${KEY_FIELD} batch : 16 } # 与上述类似 # 从 Hive 加载将执行命令 $ {EXEC} 作为数据集 { name: ${TAG_NAME} type: hive exec: ${EXEC} fields: { hive_field_0: nebula_field_0, hive_field_1: nebula_field_1 } vertex: ${KEY_FIELD} } ] 说明: name 字段用于表示标签名称 fields 字段用于配置 HDFS 或 Hive 字段与 Nebula 字段的映射关系 batch 参数意为一次批量导入数据的记录数,需要根据实际情况进行配置。 边类型配置段用于描述导入标签信息,数组中每个元素为一个边类型信息。边类型导入主要分为两种:基于文件导入与基于Hive导入。 基于文件导入配置需指定文件类型 基于Hive导入配置需指定执行的查询语言 # 处理边 edges: [ # 从 HDFS 加载数据,数据类型为 JSON # 边名称为 ${EDGE_NAME} # HDFS JSON 文件中的 field_0、field_1 将被写入${EDGE_NAME} # 起始字段为 source_field,终止字段为 target_field ,边权重字段为 ranking_field。 { name: ${EDGE_NAME} type: json path: ${HDFS_PATH} fields: { field_0: nebula_field_0, field_1: nebula_field_1 } source: source_field target: target_field ranking: ranking_field } # 从 Hive 加载将执行命令 ${EXEC} 作为数据集 # 边权重为可选 { name: ${EDGE_NAME} type: hive exec: ${EXEC} fields: { hive_field_0: nebula_field_0, hive_field_1: nebula_field_1 } source: source_id_field target: target_id_field } ] 说明: name 字段用于表示边类型名称 fields 字段用于配置 HDFS 或 Hive 字段与 Nebula 字段的映射关系 source 字段用于表示边的起点 target 字段用于表示边的终点 ranking 字段用于表示边的权重 batch 参数意为一次批量导入数据的记录数,需要根据实际情况进行配置。 导入数据命令 bin/spark-submit \ --class com.vesoft.nebula.tools.generator.v2.SparkClientGenerator \ --master ${MASTER-URL} \ ${SPARK_WRITER_JAR_PACKAGE} -c conf/test.conf -h -d 说明: -c:config 用于指定配置文件路径 -h:hive 用于指定是否支持 Hive -d:dry 用于测试配置文件是否正确,并不处理数据。 作者有话说:Hi ,大家好,我是 darion,Nebula Graph 的软件工程师,对分布式系统方面有些小心得,希望上述文章可以给大家带来些许启发。限于水平,如有不当之处还请斧正,在此感谢^^ 喜欢这篇文章?来来来,给我们的 GitHub 点个 star 表鼓励啦~~ ♂️♀️ [手动跪谢] 交流图数据库技术?交个朋友,Nebula Graph 官方小助手微信:NebulaGraphbot 拉你进交流群~~
摘要: 在本文中,我们将借助 D3.js 的灵活性这一优势,去新增一些 D3.js 本身并不支持但我们想要的一些常见的功能:Nebula Graph 图探索的删除节点和缩放功能。 文章首发于 Nebula Graph 官博:https://nebula-graph.com.cn/posts/d3-js-examples-for-advaned-uses/ 前言 在上篇文章中(D3.js 力导向图的显示优化),我们说过 D3.js 在自定义图形上相较于其他开源可视化库的优势,以及如何对文档对象模型(DOM)进行灵活操作。既然 D3.js 辣么灵活,那是不是实现很多我们想做的事情呢?在本文中,我们将借助 D3.js 的灵活性这一优势,去新增一些 D3.js 本身并不支持但我们想要的一些常见的功能。 构建 D3.js 力导向图 在这里我们就不再细说 d3-force 粒子物理运动模块原理,感兴趣同学可以看看我们的上篇的简单描述, 本次实践我们侧重于可视化操作的功能实现。 好的,进入我们的实践时间,我们还是以 D3.js 力导向图对图数据库的数据关系进行分析为目的,增加一些我们想要功能。 首先,我们用 d3-force 力导向图来构建一个简单的关联网 this.force = d3 .forceSimulation() // 为节点分配坐标 .nodes(data.vertexes) // 连接线 .force('link', linkForce) // 整个实例中心 .force('center', d3.forceCenter(width / 2, height / 2)) // 引力 .force('charge', d3.forceManyBody().strength(-20)) // 碰撞力 防止节点重叠 .force('collide',d3.forceCollide().radius(60).iterations(2)); 通过上述代码,我们可以得到下面这样一个可视化的节点和关系图。 这里我们简单介绍下上图,上图为图数据库 Nebula Graph 可视化工具 Studio 的图探索功能截图,在业务上,图探索支持用户任意选中某个点进行拓展,找寻、显示同它存在某种关系的点,例如上图点 100 和 点 200 存在单向 follow 关系。 上图数据量并不大,如果我们在拓展时返回的数据量较大或多步拓展出来的数据逐步累加显示,则会导致当前视图页节点和边极多,页面需呈现的数据信息量大,且也不好找到想要的某个节点。好的,一个新场景上线了:用户只想分析图中的部分节点数据,不想看到全部的节点信息。删除任意选中这个新功能就可以很好地应对上面场景,删除不需要的节点信息,只留下想探索的部分节点数据。 支持删除任意选中功能 在实现这个功能之前,我先开始介绍下 D3.js 自带 API。没错,还是上篇提及的 D3.js 的 enter() 及没提到的 exit() 摘自文档的描述: 数据绑定的时候可能出现 DOM 元素与数据元素个数不匹配的问题, enter 和 exit 就是用来处理这个问题的。enter 操作用来添加新的 DOM 元素,exit 操作用来移除多余的 DOM 元素。如果数据元素多于 DOM 个数时用 enter,如果数据元素少于 DOM元素,则用 exit。在数据绑定时候存在三种情形: 数据元素个数多于 DOM 元素个数 数据元素与 DOM 元素个数一样 数据元素个数少于 DOM 元素个数 根据文档描述,想实现删除任意选中功能还是很简单的,乐观的笔者想当然地认为直接在数据层面进行操作就行。于是笔者直接在 nodes 数据里删除选中的节点数据 node,然后根据官方用法 d3.select(this.nodeRef).exit().remove() 移除多余的元素,好的,我们现在来看看这样做会带来了什么? 不想选中的节点是删除了,但其他节点的显示也乱了,节点颜色和属性同当前 DOM 节点对不上,为什么会这样呢?笔者又仔仔细细地看了一遍上面的文档描述,灵光一闪,来,先打印下 exit().remove() 的节点,看看到底它 remove 哪些节点? 果然是它,D3.js enter().exit() 的触发其实是在监听元素的个数的变化,也就是说,如果总个数缺少了两个,它确实会触发 exit() 方法,但是它处理的数据不是真正需删除的数据,而是当前 nodes 数据最后两个节点。说白了 enter()、exit() 的触发原理,是 D3.js 监听当前数据的长度变化来触发的。然而 D3.js 在获取数据长度变化之后,以 exit() 为例,对单个数据的处理方法是根据长度的减量 N 截取数据数组位置中最后 N 位到最后一位区间的所有元素,enter() 则相反,会在数组位置中最后一个元素后面增加 N 个数据。 所以,如果选中删除的是之前拓展探索出来的节点(它不是当前数据数组位置的最后一个元素),进行删除操作时,虽然从我们的 nodes 数据里面删除了这个数据,但是在已经存在的视图中,d3.select(this.nodeRef).exit() 方法定位到的操作元素却是最后一个,这样显示就乱套了,那么,我们该如何处理这个问题呢? 这里就直接分享下我的方法,简单粗暴但有效——显然这个 exit() 并不能满足删除选中节点的业务需求,那我们单独地处理需删除的节点。我们定位到真实删除的节点 DOM 进行操作,为此我们需要在渲染时给每个节点绑定一个 ID,然后再进行遍历,根据已删除的节点数据找到这些需要删除的节点对应的 DOM,以下为我们的处理代码: componentDidUpdate(prevProps) { const { nodes } = this.props; if (nodes.length < prevProps.nodes.length) { const removeNodes = _.differenceBy( prevProps.nodes, nodes, (v: any) => v.name, ); removeNodes.forEach(removeNode => { d3.select('#name_' + removeNode.name).remove(); }); } else { this.labelRender(this.props.nodes); } } 其实在这里需要处理的不仅仅定位到当前真实删除节点的 DOM,还需要将它所关联的边、显示文案一并删除。因为没有起点/终点的边,是没有任何意义的,边、文案的处理方法同点删除的逻辑类似,这里不做赘述,如果你有任何疑问,欢迎前往我们的项目地址:https://github.com/vesoft-inc/nebula-web-docker 进行交流。 支持按钮缩放功能 说完删除选中点,在可视化视图中缩放操作也是比较常见的功能,D3.js 中的 d3.zoom() 就是用来实现缩放功能的,且该方法经过其他厂的业务考验相对来说成熟稳定,那我们还有什么理由要自己做呢?(要啥自行车 )。 其实缩放功能纯粹是交互改动层面上的一个功能。采用滚轮控制缩放的方案的话,不了解 Nebula Graph Studio 的用户很难发现这种隐藏操作,而且滚动控制缩放无法控制缩放的明确比例,举个例子,用户想缩放 30% / 50%,对于这种限定的比例,滚动控制缩放就无能为力了。除此之外,笔者在实施滚轮缩放的过程中发现滚动缩放会影响节点和边的位置偏移,这又是什么原因造成的呢? 通过查看 d3.zoom() 代码,我们发现 D3.js 本质是获取事件中 d3.event 的缩放值再针对整个画布修改 transform 属性值,但这样处理 svg 中的节点和边元素 x、y 坐标不发生变化,所以导致 d3.zoom() 实现缩放功能时,放大画布,视图会往坐左上方偏移(因为对画布来说,相较视图中的边元素 x、y 坐标,自己变小了),缩小画布,视图会往右下方偏移。 发现问题形成的原因是解决问题的第一步,下面来解决下问题,在进行缩放时添加一个节点和边相对画布大小偏移量的变化处理逻辑,好的,那开始操作吧。 我们先弄一个滑动条控件提供给用户进行手动控制缩放画布的比例,直接用 antd 的滑动条,根据它滑动的的值来控制整个画布缩放比例,下面直接贴代码了 <svg width={width} viewBox={`0 0 ${width * (1 + scale)} ${height * (1 + scale)}`} height={height} > {/*****/} </svg> 上面代码中的 scale 参数是我们根据控件滚动条中缩放值来生成的,我们需要记录这个值来放大画布(svg 元素),从来造成视图缩小的效果的。 此外,我们处理下上面提到的节点和边偏移问题时也需要 scale 值,因为我们需要给节点和边设置一个反偏移量。简单的说,画布放大 scale 倍,节点和边的 x、y 位置也要相对画布偏移当前的 scale 倍,这样就能保持在缩放过程中,节点和边位置相对画布大小变化而保持不变。下面就是处理节点缩放过程中偏移的关键代码 const { width, height } = this.props; const scale = (100 - zoomSize) / 100; const offsetX = width * (scale / 2); const offsetY = height * (scale / 2); // 操作节点边父元素 DOM <g/> 的偏移 d3.select(this.circleRef).attr( 'transform', `translate(${offsetX} ${offsetY})`, ); 结语 好了,以上便是笔者使用 D3.js 力导向图实现关系网的在自定义功能过程中思路和方法。不得不说,D3.js 的自由度真的高,我们可以尽情地开脑洞实现我们想要的功能。 在这次分享中,笔者分享了图数据库可视化业务中 2 个实用且用户高频使用的功能:任意选中删除节点、自定义缩放并优化视图偏移功能。说到可视化展示一个复杂的关系网,需要考虑的问题还很多,需要优化的交互和显示的地方也很多,我们会持续优化,后续我们会更新 D3.js 优化系列文,欢迎订阅 Nebula Graph 博客。 喜欢这篇文章?来来来,给我们的 GitHub 点个 star 表鼓励啦~~ ♂️♀️ [手动跪谢] 交流图数据库技术?交个朋友,Nebula Graph 官方小助手微信:NebulaGraphbot 拉你进交流群~~ 作者有话说:Hi,我是 Nico,是 Nebula Graph 的前端工程师,对数据可视化比较感兴趣,分享一些自己的实践心得,希望这次分享能给大家带来帮助,如有不当之处,欢迎帮忙纠正,谢谢~
本文作者系微信技术专家李本利 图数据在社交推荐、多跳实时计算、风控和安全等领域有可期待的前景。如何用图数据库高效存储和查询大规模异构图数据,是一个重大挑战。本文描述了开源分布式图数据库 Nebula Graph 实践中遇到的问题,并通过深度定制,实现:大数据集存储、小时级全量导入、多版本控制、秒级回滚、毫秒级访问等特性。 背景 为大众所熟知的图数据库大多在大数据集合上束手无策,如:Neo4j 的社区版本,采用 Cypher语言,由单机单副本提供服务,广泛应用于图谱领域。互联网公司只能在小数据集合下使用,还要解决 Neo4j 多副本一致性容灾的问题。JanusGraph 虽然通过外置元数据管理、kv 存储和索引的方式解决了大数据集合存储问题,但其存在广为诟病的性能问题。我们看到大部分图数据库在对比性能时都会提到和 JanusGraph 相比有几十倍以上的性能提升。 面临大数据量挑战的互联网公司,普遍走向了自研之路,为了贴合业务需求,仅支持有限的查询语义。国内主流互联网公司如何解决图数据库的挑战呢: 蚂蚁金服:GeaBase[1] 金融级图数据库,通过自定义类语言为业务方提供服务,全量计算下推,提供毫秒级延时。主要应用于以下场景: 金融风控场景:万亿级边资金网络,存储实时交易信息,实时欺诈检测。 推荐场景:股票证券推荐。 蚂蚁森林:万亿级的图存储能力,低延时强一致关系数据查询更新。 GNN:用于小时级 GNN 训练。尝试动态图 GNN 在线推理。[7] 阿里巴巴:iGraph[2] iGraph 是图索引及查询系统,存储用户的行为信息,是阿里数据中台四驾马车之一。通过 Gremlin 语言为业务方提供电商图谱实时查询。 今日头条:ByteGraph[3] ByteGraph 通过在 kv 上增加统一 cache 层,关系数据拆分为 B+ 树以应对高效的边访问和采样,类似 Facebook 的 TAO [6]。 ... 架构图 实践 从哪里开始呢? 我们选择从 Nebula Graph[4] 开始我们的图数据库之旅,其吸引我们的有以下几点: 数据集分片,每条边独立存储,超大规模数据集存储潜力。 定制强一致存储引擎,具有计算下推和 MMP 优化的潜力。 创始团队有丰富的图数据库经验,大数据集合下模型抽象思路经过验证。 实践中的问题 内存爆炸 本质上这是一个性能 VS 资源的问题,数据规模庞大的应用中,内存占用是一个不容忽视的问题。RocksDB 内存由三部分构成:block cache、index 和 bloom filter、iter pined block。 block cache 优化:采用全局 LRU cache,控制机器上所有 rocksdb 实例的 cache 占用。 bloom filter 优化:一条边被设计为一个 kv 存入到 rocksdb,如果全部 key 保存 bloom filter,每个 key 占用 10bit 空间,那么整个 filter 内存占用远超机器内存。观察到我们大部分的请求模式是获取某一个点的边列表,因此采用 prefix bloom filter;索引到点属性这一层实际上即可以对大多数请求进行加速。经过这个优化,单机 filter 所占用内存在 G 这个级别,大多数请求访问速度并未明显降低。 多版本控制 实践中,图数据需要进行快速回滚,定期全量导入,自动访问最新版本数据。我们把数据源大致可以分为两种类型: 周期性数据:比如,按天计算相似用户列表,导入后数据生效。 历史数据+实时数据:比如,历史数据按天刷新,和实时写入的数据进行合并成为全量数据。 如下是数据在 rocksdb 的存储模型: vertex 存储格式 edge 存储格式 其中实时写入的数据 version 记录为时间戳。离线导入的数据 version 需要自己指定。我们将该字段和离线导入模块联合使用,用三个配置项进行版本控制:reserve_versions(需要保留的版本列表)、active_version(用户请求访问到的版本号)、max_version(保留某个版本之后数据,把历史数据和实时写入数据进行合并)。这样可以高效管理离线数据和在线数据,不再使用的数据在下一次 compaction 中被清除出磁盘。 通过这样的方式,业务代码可以无感更新数据版本,并做到了秒级回滚。 举例: 保留 3 个版本,激活其中一个版本: alter edge friend reserve_versions = 1 2 3 active_version = 1 数据源为历史数据+实时导入数据。 alter edge friend max_version = 1592147484 快速批量导入 实践中导入大量数据是常规操作,如果不经任何优化,将需要导入的数据转为请求发给图数据库,不仅严重影响线上请求,而且大数据量导入耗时超过一天。对导入速度进行优化迫在眉睫。业界解决这个问题一般采用 SST Ingest 方式[5]。我们也是采用类似方式,通过例行调度 spark 任务,离线生成磁盘文件。然后数据节点拉取自己所需要的数据,并 ingest 到数据库中,之后进行版本切换控制请求访问最新版本数据。 整个过程导入速度快,约数个小时内完成全部过程。计算过程主要离线完成,对图数据库请求影响小。 shared nothing 这是近年来老生常谈的并发加速方式,然而要落地还是考验工程师的编程功底。meta cache 访问频繁,并用 shared_ptr 进行封装,也就成为了原子操作碰撞的高发地。为了能够实现真正的 shared nothing,我们将每一份 meta cache 拷贝为 thread local,具体解决方案请参考该 pull request [8] 小结 图数据库路阻且长,且行且珍惜。如果对于本文有什么疑问,可以在 GitHub[9] 上找找。 参考文献 Fu, Zhisong, Zhengwei Wu, Houyi Li, Yize Li, Min Wu, Xiaojie Chen, Xiaomeng Ye, Benquan Yu, and Xi Hu. "GeaBase: a high-performance distributed graph database for industry-scale applications." International Journal of High Performance Computing and Networking 15, no. 1-2 (2019): 12-21. https://mp.weixin.qq.com/s?__biz=MzU0OTE4MzYzMw==&mid=2247489027&idx=3&sn=c149ce488cfc5231d4273d6da9dc8679&chksm=fbb29ffdccc516ebb8313b9202cfd78ea199da211c55b0a456a9e632a33e7d5b838d8da8bc6a&mpshare=1&scene=1&srcid=0614MWpeEsBc1RaBrl4htn3D&sharer_sharetime=1592106638907&sharer_shareid=a2497c4756f8bac1bcbef9edf86a86ac&rd2werd=1#wechat_redirect https://zhuanlan.zhihu.com/p/109401046 https://github.com/vesoft-inc/nebula https://www.infoq.cn/article/SPYkxplsq7f36L1QZIY7 Bronson, Nathan, Zach Amsden, George Cabrera, Prasad Chakka, Peter Dimov, Hui Ding, Jack Ferris et al. "{TAO}: Facebook’s distributed data store for the social graph." In Presented as part of the 2013 {USENIX} Annual Technical Conference ({USENIX}{ATC} 13), pp. 49-60. 2013. http://blog.itpub.net/69904796/viewspace-2653498/ https://github.com/vesoft-inc/nebula/pull/2165 https://github.com/xuguruogu/nebula 腾讯高性能分布式图计算框架柏拉图 https://github.com/Tencent/plato 加入 Nebula Graph 交流群,请联系 Nebula Graph 官方小助手微信号:NebulaGraphbot
前言 建站工具,早已不是一个新颖的话题,抛开可视化建站单论开发层面,各类语言都有推出广受欢迎的建站框架,比如 Python 开发的 Pelican,JavaScript 开发的 Hexo,以及市场份额占比最大的 PHP 开发的 WordPress 等等,这些笔者在折腾个人博客时多少都有用过。但当需要快速搭建起我们的 Nebula Graph 官网 时,小小纠结对比之后,笔者选择了 Golang 语言的 Hugo 来作为我们的技术方案,下面就来分享下个人使用 Hugo 建站的一些个人思考和经验分享。 P.S: 客观来说,各类语言的博客类型框架并无太大差别,更多还是类比语言的个人喜好与审美不同,在此不做叙述。 实践介绍 我们的需求 博客系统,需要支持运营发布我们日常的技术文章资讯 Hugo 有灵活强大的内容管理系统,能随着需求,不断增加不同类型的资讯支持,诸如博客、Release Note、技术文档等,详细后面会介绍。 品宣介绍,常见就是站点首页、新闻介绍等 同样依赖内容管理系统,能很快支持到不同页面的实现,包括相同组件如导航、页脚等的共享,后面也会介绍。 SEO 需要 Hugo 本就是类似服务端模板语言的 Web 框架,天然的服务端渲染。 国际化支持,Nebula 注重国内外开发者的访问体验 Hugo 能渐进地拓展支持多国语言,只要你有对应的语料配置,就能迅速支撑需求并方便管理。 灵活易于管理,能让非技术的运营同学也能参与站点的内容管理 强大的模板系统,让技术人员专心开发完对应模板后,能将内容管理交给运营同学持续运营。 特点介绍 灵活强大的内容管理系统 ... ├── config // 模板需要的内容语料 | ├── default | | ├── config.toml | | └── config.cn.toml | | └── config.en.toml | | └── footer.cn.toml | | └── footer.en.toml | | └── ... ├── content // 内容部分,日常多由运营同学管理维护 | ├── en // 国际化支持 | | ├── posts // 内置默认的博客(post)类型资讯 | | | ├── post-01.md | | | ├── post-02.md | | └── release // 新增的 release 类型资讯 | | | ├── release-01.md | | | └── release-02.md | ├── cn | | ├── posts | | | ├── post-01.md | | | ├── post-02.md | | └── release | | | ├── release-01.md | | | └── release-02.md ... ├── themes // 站点的主题 | ├── nebula-theme // 主题名 | | ├── layout // 模板 | | | ├── _default // 默认的模板 | | | | ├── baseof.html // 渲染的种子页面定义 | | | | ├── list.html // 默认博客 post 类型资讯 - 列表页的使用模板页面 | | | | ├── single.html // 默认博客 post 类型资讯 - 详情页使用模板页面 | | | ├── partials // 复用的模板片段 | | | | ├── head.html | | | | ├── footer.html | | | | ├── menus.html | | | | ├── ... | | | ├── index.html // 首页('/') 默认会使用的模板 | | | ├── section | | | | ├── release.html // 新增资讯类型 release 渲染时使用的模板页面 - 发布历史页面 | | | | ├── news.html // 新增资讯类型 news 渲染时使用的模板页面 - 媒体新闻页面 ... 以上,便是 Hugo 用以支撑起灵活强大的模板系统所采用的项目结构,笔者感觉比较能直观反映出对于不同站点需求的支持,它甚至还可以是不断嵌以此结构不断嵌套,外层的配置覆盖内层的,更多信息可以参考官方的模板系统介绍。 内置丰富工具集 除了强大的内容管理系统外,Hugo 还有很多很好用的内置模板及工具函数,满足不同需求情况下提升搭建效率,抽象实现细节,更专注于站点的搭建,诸如: 资源类型列表的分页模板 - Pagination 这个针对只有列表页的需求,比如博客,发版历史,新闻类等,好用的分页模板,轻松的就帮你完成了。 资源 RSS 模板 - RSS 对于资讯型的站点必不可少,官方已内置了默认的 rss 模板,只需要添加一行代码,即可搞定 rss,当然还支持个性化定制。 各类内容及字符串处理工具函数 - Functions 这个不用多说,对应程序中的各类常见的字符串替换,Hugo 都有着良好的支持,同时它还支持类似 Pipe 管道的方式,将处理内容以 | 分隔层层传递下去,就像我们在 Linux 输入的命令一样。 好用的 CLI 工具 内置了 http server 方便本地开发,同时又能将整个站点打包成纯静态的资源,方便了对于部署的操作和维护成本,可以一键初始化并启动项目,开箱即用的感觉,上手容易。 好用的内容管理工具 迅速提取博客内容的目录导航 - TableOfContents 使用此工具函数,会根据你当前的文章内容,提取目录概要,节省了生成锚点内容的时间。 便捷获取文章的概览内容 - Summary 便捷获取文章的图片资源 - Image Processing 自定义 URL 的规则 - URL Management 以上便是我们在实践中,有接触过的一些 Hugo 比较好用的工具,当然它提供的远比这个更丰富,更多工具可查看参考官方文档。 社区资源丰富 生态很好,现成大量的主题可供选择 作为 Golang 语言最受欢迎的站点框架,随着越来越多人的使用,Hugo 官方鼓励大家开源自己的主题,约定了简易可行的规范,让贡献者的主题能在 Hugo 官网方便地被他人找到,易于复用。非技术的同学,也可以找到符合自己需求的主题,不用写一行 HTML 代码,也能让自己生成自己的站点。 答疑途径多样 作为一个 45k+ star 的项目,使用人群众多,知识沉淀很好,网上搜索能解决大部分问题。 有在线的论坛,维护者也相当活跃,只要提问得当,能及时得到回复,解决疑难杂症。 官方文档 的内容组织,层次也比较清晰,从笔者个人使用来看,体验还是很好的。 经验总结 除了上面偏向于 Hugo 本身提供的功能介绍外,下面结合笔者自身的实践经历,阐述一些小小的经验总结,方便后来的朋友: 使用现成的主题 基于 DIY 原则来说,结合自身需求,去主题市场找一个符合自己的主题来进行修改,应该是上手最快的方式了,甚至不需要开发就能拿来直接用,即使需要开发,使用他人已开发好的主题,由于 Hugo 框架本身具有很好的约定规范,你也能很快了解到一个 Hugo 项目的结构组成及运行机制,降低调研上手成本。 项目结构和内容关系 就像前面介绍的内容管理系统,从结构上了解内容 contents 与模板 layouts 之间的映射关系,适当结合官网文档的介绍,了解这层映射关系后,能方便在后续的开发过程中,让你的实践更符合 Hugo 期望的形式来进行,这样会让你不论是实现,还是在阅读 Hugo 文档的时候,事半功倍,易于理解。 个人定制 除了 Hugo 本身的框架、规范及工具能力外,因为网页的代码最终还是离不开 HTML/CSS/Javascript,自定义相关的内容,只要善用提供的规则(如各个模板的引用,组合),就能在各个模板入口引入你想自己控制的代码部分,为你自己的站点添砖加瓦。结合我们自身的实践,比如第三方站点插件的集成(埋点统计,Discourse、ShareThis等等),一些自定义弹窗等动态 js 的添加,所以只要熟悉网页的常规开发,除了 Hugo 的能力外,你可以做到你以往可做的任何事情。 纯静态站点 Hugo 打包构建后输出的是一个纯静态的资源包,这样地好处就是你可以将你的站点部署在任何地方,比如使用 GitHub 免费的 Pages,又或者是随便放在 oss 源中,没有维护服务器,数据库的烦恼。纯静态资源部署很便捷,以 Hugo 为例,他的路由适合文件目录相关的,我们的站点有中英文两个语言版本,开发时都放在一个项目中进行维护共享模板,在构建部署时,会根据语言打成不同的资源包,分别发到不同的国内外 Web 容器,以此优化访问体验。 最后 以上便是笔者使用 Hugo 框架搭建公司 Nebula Graph 官网 的一些实践心得,希望给有快速建站需求的朋友提供一些思路和参考,我们的站点是基于已有主题二次开发,更多细节感兴趣的朋友也可以看看我们放在 GitHub 的源站仓库。 也欢迎大家来了解我们的 Nebula Graph 图数据库产品 或者前往官方论坛:https://discuss.nebula-graph.com.cn/ 的 建议反馈 分类下提建议 ;加入 Nebula Graph 交流群,请联系 Nebula Graph 官方小助手微信号:NebulaGraphbot 作者有话说:Hi,我是 Jerry,是图数据 Nebula Graph 前端工程师,在前端平台工具开发及工程化方面有些小心得,希望写的经验分享能给大家带来帮助,如有不当之处也希望能帮忙纠正,谢谢~
摘要:用技术来解决 PM 枯燥的 approval pr 工作,本文将阐述如何自动化获取 GitHub Organization 下各个 repo 待 merge 的 pull requests 并通知相关人员,告别每日的手动操作。 在日常工作中,你是否遇到以下场景: Github 存在多个 repo,日常工作中需要一个个地手动筛选大量待 merge 的 pull requests 要找出多个 repo 中 ready to review 的 pull requests,要手动筛选,然后一遍又一遍地粘贴复制提交 dev 进行 review #倍感无聊 想自动推送 GitHub 待 merge 的 prs,GitHub Webhooks 却没有该 Event …… 用技术来解决 PM 枯燥的 approval pr 工作,本文将阐述如何自动化获取 GitHub Organization 下各个 repo 待 merge 的 pull requests 并通知相关人员,告别每日的手动操作。此文主要提供了解决自动发送 approval prs 的思路,并以钉钉群和 Slack 为例,给出了其 Python 的实现方式,如果你使用其他通讯工具,实现原理是相通的。 配置消息接收 配置钉钉群机器人 打开机器人管理页面。以 PC 端为例,打开 PC 端钉钉,点击“群设置” => “智能群助手” => “添加机器人”。 点击“添加机器人”,选择“自定义” 本例的“安全设置”使用自定义关键词的方式,之后给机器人所发送的消息中必须包含此处设置的关键词。 点击“完成”,获取 Webhook 详细的钉钉 bot 配置文档可参见官方文档:https://ding-doc.dingtalk.com/doc#/serverapi2/qf2nxq/26eaddd5 配置 Slack bot 创建一个 app(链接:https://api.slack.com/apps),设置 App Name,选择目标 Slack Workspace 在左侧栏中选择 “Basic Information” => “Add features and functionality” 选在 “Bots” 在左侧栏中选择 “OAuth & Permissions”,在 “Scopes” 中点击 “Add an OAuth Scope”,添加 chat:write.public 点击 “Install App to Workspace” 获取 OAuth Access Token 详细的 Slack bot 配置步骤参见官方英文文档:https://slack.com/intl/en-cn/help/articles/115005265703-Create-a-bot-for-your-workspace#add-a-bot-user 配置 Github 获取 Personal Access Tokens 生成 Token,赋予相应权限。在此例中,读取了 Organization 下所有 Public 和 Private Repos,需要勾选 repo。 详细 GitHub Token 配置步骤参见官方文档:https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line 代码说明 获取 Github 待 merge pr PyGithub 提供了访问 Github V3 API 的功能,可以让你用代码去实现 GitHub 上的操作,可通过 pip install pygithub 进行安装。 FILTER_TEMPLATE = "repo:{org}/{repo} is:pr is:open review:approved" class GithubPrList: @property def gh(self): return self._gh @property def org(self): return self._org FILTER_TEMPLATE = "repo:{org}/{repo} is:pr is:open review:approved" def __init__(self, org, repo, login_or_token, password=None, timeout=DEFAULT_CONNECT_TIMEOUT, retry=None, ): """ :param org: string :param repo: string :param login_or_token: string,token or username :param password: string :param timeout: integer :param retry: int or urllib3.util.retry.Retry object """ #实例化对 Github API v3 的访问 self._gh = Github(login_or_token=login_or_token, password=password, timeout=timeout, retry=retry) self._org = org self._repo = repo def getIssues(self, filter=None, sort=DEFAULT_PR_SORT, order=DEFAULT_ORDER, ): """ :param filter: string :param order: string ('asc', 'desc') :param sort: string('comments', 'created', 'updated') :rtype :class:`List` of :class:`PrList2.PrElement` """ if not filter: #生成查询的 filter,指定org/repo 下已经approved 的pr filter = self.FILTER_TEMPLATE.format(org=self._org, repo=self._repo) #查询 issues = self._gh.search_issues(filter, sort, order) prList = [] for issue in issues: prList.append(PrElement(issue.number, issue.title, issue.html_url)) return prList 函数说明: __init__ 支持使用 username/ password 或者 token 去实例化对 GitHub API V3的访问(英语是 instantiate to access the Github API v3)。 在 Github 中,pull requests 也是 issues,getIssues() 函数允许用户可使用默认条件(repo:{org}/{repo} is:pr is:open review:approved)查找指定 org/repo 下状态是 Approved 的 pull requests,也就是待 merge 的 prs。其中: Qualifier 说明 repo:org_/_repo 查找指定组织 repo 下的projects is:pr 查找 pull requests is:open 查找 open 的 issues review:approved 查找 review 状态是已经 approved,review status 可能取值 _none_、_required_、_approved_、_changes requested_ 用户也可指定 Github issues 的筛选条件,使用示例: filter = "repo:myOrg/myRepo is:pr is:open review:approved" GithubPrList(self.org, self.repo, self.token).getIssues(filter) 更多筛选条件,请参见官方文档:https://help.github.com/en/github/searching-for-information-on-github/searching-issues-and-pull-requests 发送消息 发送钉钉消息 DingtalkChatbot 对钉钉消息类型进行了封装。本文使用此工具发送待 merge 的 pr 到钉钉群,可通过 pip install DingtalkChatbot 安装 DingtalkChatbot。 from dingtalkchatbot.chatbot import DingtalkChatbot webhook = "https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxxxx" atPerson = ["123xxx456","123xxx678"] xiaoding = DingtalkChatbot(webhook) xiaoding.sendMsg({自定义关键词} + "上文中的 pr list", atPerson) 将消息发送到钉钉群,此处需要用到上文中的钉钉群机器人的 Webhook 和自定义的关键词。 发送 slack 消息 Python slackclient 是 Slack 开发的官方 API 库,能够从 Slack 频道中获取信息,也能将信息发送到Slack频道中,支持 Python 3.6 及以上版本。可通过 pip3 install slackclient 进行安装。 from slack import WebClient from slack.errors import SlackApiError client = WebClient(token={your_token}) try: response = client.chat_postMessage( channel='#{channel_name}', text="Hello world!") assert response["message"]["text"] == {pr_list} except SlackApiError as e: # You will get a SlackApiError if "ok" is False assert e.response["ok"] is False assert e.response["error"] # str like 'invalid_auth', 'channel_not_found' print(f"Got an error: {e.response['error']}") 用上文配置的 token 替换此处的 {your_token},替换 {channel_name},将 pr_list 发送给目标 channel。 至此,大功告成!来看看效果 本文中如有任何错误或疏漏,欢迎去 GitHub:https://github.com/vesoft-inc/nebula issue 区向我们提 issue 或者前往官方论坛:https://discuss.nebula-graph.com.cn/ 的 建议反馈 分类下提建议 ;加入 Nebula Graph 交流群,请联系 Nebula Graph 官方小助手微信号:NebulaGraphbot 作者有话说:Hi,我是 Jude,图数据 Nebula Graph 的 PM,欢迎大家提需求,虽然不一定都会实现,但是我们会认真评估^ ^
摘要:数据库权限管理对大家都很熟悉,然而怎么做好数据库权限管理呢?在本文中将详细介绍 Nebula Graph 的用户管理和权限管理。 本文首发 Nebula Graph 博客:https://nebula-graph.com.cn/posts/access-control-design-code-nebula-graph/ 数据库权限管理对大家来说都已经很熟悉了。Nebula Graph 本身是一个高性能的海量图数据库,数据库的安全问题更是数据库设计的重中之重。目前 Nebula Graph 已支持基于角色的权限控制功能。在这篇文章中将详细介绍 Nebula Graph 的用户管理和权限管理。 Nebula Graph 架构体系 由上图可知,Nebula Graph的主体架构分为三部分:Computation Layer、Storage Layer 和 Meta Service。Console 、API 和 Web Service 被统称为 Client API。 账户数据和权限数据将被存储在 Meta Engine中,当Query Engine 启动后,将会初始 Meta Client,Query Engine 将通过 Meta Client 与 Meta Service 进行通信。 当用户通过 Client API 连接 Query Engine 时,Query Engine 会通过 Meta Client 查询 Meta Engine 的用户数据,并判断连接账户是否存在,以及密码是否正确。当验证通过后,连接创建成功,用户可以通过这个连接执行数据操作。当用户通过 Client API 发送操作指令后,Query Engine 首先对此指令做语法解析,识别操作类型,通过操作类型、用户角色等信息进行权限判断,如果权限无效,则直接在 Query Engine 阻挡操作,并返回错误信息至 Client API。 在整个权限检查的过程中,Nebula Graph 对 Meta data 进行了缓存,将在以下章节中介绍。 功能描述 在介绍功能之前,需要先描述一下 Nebula Graph 的逻辑结构:Nebula Graph 是一个支持多图空间(Space) 的图数据库,Space 中独立管理 Schema 和 Data,Space 和 Space 之间相互独立。另外,Nebula Graph 还提供了一系列高级命令用于全局管理 Cluster,Cluster 的操作命令和 Space 的操作命令将在下文中详细描述。 因此 Nebula Graph 的权限管理将会基于图空间(Space)、角色(Role)、操作(Operation) 三个维度进行。详细描述请看下列子章节。 角色划分 Nebula Graph 提供了五种操作角色,分别是 GOD、ADMIN、DBA、USER、GUEST,这五种操作角色基本覆盖了所有的数据安全控制的场景。一个登陆账户(Account)可以在不同的 Space 中拥有不同角色,但一个 Account 在同一个 Space 中只能拥有一种角色。角色讲解: GOD:相当于 Linux 操作系统中的 root 用户,拥有最高的管理权限。Nebula Graph Cluster 在初始化时会默认创建一个 GOD 角色的 Account,名为 root。 ADMIN:基于 Space 的高级管理员,拥有此 Space 之内的所有管理权限,但对整个集群则没有管理权限。 DBA:数据库管理员,可以对权限内的 Space 进行管理,例如对 Schema / Data 进行修改和查询。和 ADMIN 的区别是 DBA 不能对某个 Account 进行授权操作,但 ADMIN 可以。 USER:普通的数据库使用角色。可读写 Data,可读 Schema 但没有写权限。 GUEST:访问者角色,对权限内 Space 的 Schema 和 Data 有只读权限。 详细权限列表如下图所示: OPERATION GOD ADMIN DBA USER GUEST Read Space Y Y Y Y Y Write Space Y Read Schema Y Y Y Y Y Write Schema Y Y Y Write User Y Write Role Y Y Read Data Y Y Y Y Y Write Data Y Y Y Y Special operation Y Y Y Y Y 注 : Special Operation 为特殊操作,例如 SHOW SPACE,每个角色都可以执行,但其执行结果只显示 Account 权限内的结果。 数据库操作权限细分 基于上边的角色列表,不同的角色拥有不同的操作许可,详细如下: OPERATION STATEMENTS Read Space 1.USE 2.DESCRIBE SPACE Write Space 1.CREATE SPACE 2.DROP SPACE 3.CREATE SNAPSHOT 4.DROP SNAPSHOT 5.BALANCE Read Schema 1.DESCRIBE TAG 2.DESCRIBE EDGE 3.DESCRIBE TAG INDEX 4.DESCRIBE EDGE INDEX Write Schema 1.CREATE TAG 2.ALTER TAG 3.CREATE EDGE 4.ALTER EDGE 5.DROP TAG 6.DROP EDGE 7.CREATE TAG INDEX 8.CREATE EDGE INDEX 9.DROP TAG INDEX 10.DROP EDGE INDEX Write User 1.CREATE USER 2.DROP USER 3.ALTER USER Write Role 1.GRANT 2.REVOKE Read Data 1.GO 2.PIPE 3.LOOKUP 4.YIELD 5.ORDER BY 6.FETCH VERTEX 7.FETCH EDGE 8.FIND PATH 9.LIMIT 10.GROUP BY 11.RETURN Write Data 1.REBUILD TAG INDEX 2.REBUILD EDGE INDEX 3.INSERT VERTEX 4.UPDATE VERTEX 5.INSERT EDGE 6.UPDATE DEGE 7.DELETE VERTEX 8.DELETE EDGE Special Operation 1. SHOW,eg: SHOW SPACE、SHOW ROLES 2.CHANGE PASSWORD 控制逻辑 Nebula Graph 的用户管理和权限管理和大多数数据库的控制相似,基于 meta server,对图空间(Space)、角色(Role)、操作(Operation)三个层面进行权限管理,当 Client 连接 Nebula Graph Server 的时候,Nebula Graph Server 首先会验证登陆账户(Account)是否存在,并验证密码是否有效。 登录成功后,Nebula Graph Server 会为此连接初始 Session ID,并将 Session ID、用户信息、权限信息和 Space 信息一起加载到 Session 结构中。后续的每次操作将基于 Session 结构中的信息进行权限判断。直到用户主动退出连接或 session timeout,Session 销毁。另外,Meta Client 对权限信息进行了缓存,并根据设置的时间频率进行缓存同步,有效降低了用户连接的过程的时间耗费。 控制逻辑代码片段 Permission Check bool PermissionCheck::permissionCheck(session::Session *session, Sentence* sentence) { auto kind = sentence->kind(); switch (kind) { case Sentence::Kind::kUnknown : { return false; } case Sentence::Kind::kUse : case Sentence::Kind::kDescribeSpace : { /** * Use space and Describe space are special operations. * Permission checking needs to be done in their executor. * skip the check at here. */ return true; } ... Permission Check Entry Status SequentialExecutor::prepare() { for (auto i = 0U; i < sentences_->sentences_.size(); i++) { auto *sentence = sentences_->sentences_[i].get(); auto executor = makeExecutor(sentence); if (FLAGS_enable_authorize) { auto *session = executor->ectx()->rctx()->session(); /** * Skip special operations check at here. they are : * kUse, kDescribeSpace, kRevoke and kGrant. */ if (!PermissionCheck::permissionCheck(session, sentence)) { return Status::PermissionError("Permission denied"); } } ... } 示例 查看现有用户角色 (root@127.0.0.1:6999) [(none)]> SHOW USERS; =========== | Account | =========== | root | ----------- Got 1 rows (Time spent: 426.351/433.756 ms) 创建用户 (root@127.0.0.1:6999) [(none)]> CREATE USER user1 WITH PASSWORD "pwd1" Execution succeeded (Time spent: 194.471/201.007 ms) (root@127.0.0.1:6999) [(none)]> CREATE USER user2 WITH PASSWORD "pwd2" Execution succeeded (Time spent: 33.627/40.084 ms) # 查看现有用户角色 (root@127.0.0.1:6999) [(none)]> SHOW USERS; =========== | Account | =========== | root | ----------- | user1 | ----------- | user2 | ----------- Got 3 rows (Time spent: 24.415/32.173 ms) 为 Space 中的不同 Account 指定角色 # 创建图空间 (root@127.0.0.1:6999) [(none)]> CREATE SPACE user_space(partition_num=1, replica_factor=1) Execution succeeded (Time spent: 218.846/225.075 ms) (root@127.0.0.1:6999) [(none)]> GRANT DBA ON user_space TO user1 Execution succeeded (Time spent: 203.922/210.957 ms) (root@127.0.0.1:6999) [(none)]> GRANT ADMIN ON user_space TO user2 Execution succeeded (Time spent: 36.384/49.296 ms) 查看特定 Space 的已有角色 (root@127.0.0.1:6999) [(none)]> SHOW ROLES IN user_space ======================= | Account | Role Type | ======================= | user1 | DBA | ----------------------- | user2 | ADMIN | ----------------------- Got 2 rows (Time spent: 18.637/29.91 ms) 取消特定 Space 的角色授权 (root@127.0.0.1:6999) [(none)]> REVOKE ROLE DBA ON user_space FROM user1 Execution succeeded (Time spent: 201.924/216.232 ms) # 查看取消之后,user_space 现有角色 (root@127.0.0.1:6999) [(none)]> SHOW ROLES IN user_space ======================= | Account | Role Type | ======================= | user2 | ADMIN | ----------------------- Got 1 rows (Time spent: 16.645/32.784 ms) 删除某个 Account 角色 (root@127.0.0.1:6999) [(none)]> DROP USER user2 Execution succeeded (Time spent: 203.396/216.346 ms) # 查看 user2 在 user_space 的角色 (root@127.0.0.1:6999) [(none)]> SHOW ROLES IN user_space Empty set (Time spent: 20.614/34.905 ms) # 查看数据库现有 account (root@127.0.0.1:6999) [(none)]> SHOW USERS; =========== | Account | =========== | root | ----------- | user1 | ----------- Got 2 rows (Time spent: 22.692/38.138 ms) 本文到此结束,如果你喜欢本文,可以给我们点个 star 哟 GitHub 传送门:https://github.com/vesoft-inc/nebula ; 想了解、交流图数据库技术的小伙伴,可以联系 Nebula Graph 官方小助手微信号:NebulaGraphbot 来群里和业内大牛聊聊哟~~ Hi,我是 bright-starry-sky,是图数据 Nebula Graph 研发工程师,对数据库存储有浓厚的兴趣,希望本次的经验分享能给大家带来帮助,如有不当之处希望你能在本文【评论】区留言,谢谢~
背景 CPack 是 CMake 2.4.2 之后的一个内置工具,用于创建软件的二进制包和源代码包。 CPack 在整个 CMake 工具链的位置。 CPack 支持打包的包格式有以下种类: 7Z (7-Zip file format) DEB (Debian packages) External (CPack External packages) IFW (Qt Installer Framework) NSIS (Null Soft Installer) NSIS64 (Null Soft Installer (64-bit)) NuGet (NuGet packages) RPM (RPM packages) STGZ (Self extracting Tar GZip compression TBZ2 (Tar GZip compression) TXZ (Tar XZ compression) TZ (Tar Compress compression) ZIP (ZIP file format) 为什么要用打包工具 软件程序想要在生产环境快速被使用,就需要一个一键安装的安装包,这样生产环境就可以很方便的部署和使用。 选择 CPack 的原因 C++ 工程大部分都是用 CMake 配置编译, 而 CPack 是 CMake 内置的工具,支持打包成多种格式的安装包。因为是 CMake 的内置工具,所以使用的方式也是通过在 CMakeLists.txt 配置参数,就能达到我们的需求。使用起来很方便,容易上手。 如何安装 CPack 安装 CMake 的时候会把 CPack 一起安装了,直接通过 yum 或者 apt-get 安装即可。 一个简单的例子 基础配置 这里介绍如何打包 rpm 包,deb 的打包是一样的,区别在于一些配置。Cpack 打包 rpm 用的是 CPack RPM 生成器,用到的配置变量是以 CPACK_RPM_XXX 为前缀。最终通过 rpm-build 这个工具去打包,所以需要安装 rpm-build 这个工具,可以通过 sudo yum install -y rpm-build 安装。下面配置是用 3.14.5 的 CMake 进行测试的。 现在有一个工程 example,其目录结构如下: example |-- CMakeLists.txt // example 的主 CMakeLists.txt 文件 |-- Readme.txt |-- License.txt |-- src | |-- CMakeLists.txt | |-- MainA.cpp // 可执行文件 Aprogram 的源码 | |__ MainB.cpp // 可执行文件 Bprogram 的源码 | |-- etc | |-- CMakeLists.txt | |-- A.conf // 可执行文件 Aprogram 的配置文件 | |__ B.conf // 可执行文件 Bprogram 的配置文件 | |__ scripts |-- preinst // 安装前执行的脚本 |-- postinst // 安装后执行的脚本 |-- prerm // 卸载前执行的脚本 |__ postrm // 卸载后执行的脚本 只需要在 example/CMakeLists.txt 文件里面添加如下配置 # 设置生成的安装包名字 set(CPACK_PACKAGE_NAME "example") # 设置支持指定安装目录的控制为 ON set(CPACK_SET_DESTDIR ON) # 设置安装到的目录路径 set(CPACK_INSTALL_PREFIX "/home/vesoft/install") # 这是生成的安装的版本号信息 set(CPACK_PACKAGE_VERSION "1.0.0") # 设置 group 名字 set(CPACK_RPM_PACKAGE_GROUP "vesoft") # 设置 vendor 名字 set(CPACK_PACKAGE_VENDOR "vesoft") # 设置 license 信息 set(CPACK_RPM_PACKAGE_LICENSE "Apache 2.0 + Common Clause 1.0") include(CPack) 执行 cmake 命令后, 你会发现当前目录下面多了两个文件 CPackConfig.cmake 和 CPackSourceConfig.cmake。 编译完成后,执行 cpack -G RPM 就可将文件打包成 rpm 包,当前目录下会生成一个 _CPack_Packages 目录和一个以 .rpm 为后缀名的文件 example-1.0.0-Linux.rpm,example-1.0.0-Linux.rpm 就是我们想要的安装包文件。 如果想要查看打包过程的详细输出,可以在命令后面添加 --verbose。CPack 是根据用户的配置生成_CPack_Packages/Linux/RPM/SPECS/example.spec 文件,然后让 rpm-build 用。 上面配置生成的安装包 example-1.0.0-Linux.rpm里面包含的文件如下: ⚠️注意:假如安装时出现 file /home from install of example-1.0.0-1.x86_64 conflicts with file from package filesystem-3.2-25.el7.x86_64,那么需要在配置文件里面添加以下配置,让生成的 rpm 文件不包含 /home 和 /home/vesoft 。 set(CPACK_RPM_EXCLUDE_FROM_AUTO_FILELIST_ADDITION "/home") list(APPEND CPACK_RPM_EXCLUDE_FROM_AUTO_FILELIST_ADDITION "/home/vesoft") 添加行为 我们要在安装前后、卸载前后做一些事情时,可以通过写相应的脚本文件: preinst:安装前脚本文件 postinst:安装后脚本文件 prerm:卸载前文件 postrm:卸载后文件 在上述的 CMakeLists.txt 文件里面添加如下配置: # 设置安装前执行的脚本文件 preinst set(CPACK_RPM_PRE_INSTALL_SCRIPT_FILE ${CMAKE_CURRENT_SOURCE_DIR}/scripts/preinst) # 设置卸载前执行的脚本文件 prerm set(CPACK_RPM_PRE_UNINSTALL_SCRIPT_FILE ${CMAKE_CURRENT_SOURCE_DIR}/scripts/prerm) # 设置安装后执行的脚本文件 postinst set(CPACK_RPM_POST_INSTALL_SCRIPT_FILE ${CMAKE_CURRENT_SOURCE_DIR}/scripts/postinst) # 设置卸载后执行的脚本文件 postrm set(CPACK_RPM_POST_UNINSTALL_SCRIPT_FILE ${CMAKE_CURRENT_SOURCE_DIR}/scripts/postrm) CPack 会将上面配置的脚本里面的内容写到生成的 SPEC 文件里面去。 ⚠️注意:上述的四个脚本文件需要的权限是所有用户和用户组均能执行,创建完脚本文件后,通过 chmod 755 scripts/* 修改 scripts 目录下面的脚本文件的权限。 执行 sudo rpm -ivh example-1.0.0-Linux.rpm 命令会有以下输出 执行 sudo rpm -e example-1.0.0 会有以下输出 可以看到图片里面绿色和红色字样,就是四个脚本文件的打印输出,分别对应安装前后和卸载前后执行打印。所以用户可以在这四个脚本里面实现自己想要的功能。 分装多个包 上述配置是将所有需要打包的文件打包成一个安装包,但一个项目往往会有多个不同服务,在实施部署时需安装到不同的机子上,这个时候如果把所有服务一起打包,会导致部署时包太大。这个时候就需要用上 CPack 的 Components 功能。 下面介绍在这个过程需要用到的三个函数:cpack_add_component 和 cpack_add_component_group,还有 install。 以下为添加 install 的函数定义 以下为添加 component 的函数定义 以下为添加 group 的函数定义 以上述为例,假如我们要将 program A 和它的配置文件 A.conf 打成一个 rpm 包,将 program B 和它的配置文件 B.conf 打成一个 rpm 包,则需要在 CMakeLists.txt 里添加以下内容,把上述配置的 include(CPack) 移到下面配置的位置: # 设置每个分组打包成一个 rpm 包 set(CPACK_COMPONENTS_GROUPING ONE_PER_GROUP) # 设置支持 COMPONENT set(CPACK_RPM_COMPONENT_INSTALL ON) include(CPack) # 添加一个名为 AComponent 的 component cpack_add_component(AComponent DISPLAY_NAME "A program" DESCRIPTION "The program for test" GROUP Aprogram) # 添加一个名为 BComponent 的 component cpack_add_component(BComponent DISPLAY_NAME "B program" DESCRIPTION "The program for test" GROUP Bprogram) # 添加一个名为 Aprogram 的 group, 这个名字会作为 rpm 包名字的一部分 cpack_add_component_group(Aprogram) # 添加一个名为 Bprogram 的 group cpack_add_component_group(Bprogram) 然后修改 src/CMakeLists.txt,看下图红框内容,将 program A 二进制文件配置为 AComponent,将 program B 二进制文件配置为 BComponent。 修改 etc/CMakeLists.txt,看下图红框内容,将配置文件 A.conf 配置为 AComponent, 将配置文件 B.conf 配置为 BComponent。 更新 CMakeLists.txt 的配置之后,重新执行下 cmake 命令生成新的 makefile 文件,并执行 cpack -G RPM,你可以在当前目录下面看到生成两个文件 example-1.0.0-Linux-Aprogram.rpm 和 example-1.0.0-Linux-Bprogram.rpm, 它们各自包含的文件如下: 其他常用参数 安装到指定目录:上述配置,生成的安装包只能安装到 /home/vesoft/install 目录,假如用户希望能够安装指定位置,这个时候需要在 include(CPack) 之前添加以下配置 # 将上述配置设置指定目录这个选项置为 OFF set(CPACK_SET_DESTDIR OFF) # 设置可重定目录的选择为 ON set(CPACK_RPM_PACKAGE_RELOCATABLE ON) # 设置默认重定的目录 set(CPACK_PACKAGING_INSTALL_PREFIX "/home/vesoft/install") 通过上述配置,重新生成的 rpm 包就可以支持安装到其他指定目录,下面是把它安装到 /home/test/install,使用如下: sudo rpm -ivh example-1.0.0-Linux-Aprogram.rpm --prefix=/home/test/install 用户可以通过 CPACK_RPM_SPEC_MORE_DEFINE 这个参数在生成的 SEPC 文件里面增加相应的宏,来应用 rpmbuild 的一些功能开关。 更多… CPack 有很多参数,不同版本参数有些差异,想要了解更多,可以去 CMake 官网查看,见 CPack。或直接通过 CPack --help 获取参数描述。 Nebula Graph 也是采用 CPack 进行打包成 rpm 和 deb 包,您可以通过 https://github.com/vesoft-inc/nebula/releases 获取到 Nebula Graph 每次 release 发布的包。 本文中如有任何错误或疏漏欢迎去 GitHub:https://github.com/vesoft-inc/nebula issue 区向我们提 issue 或者前往官方论坛:https://discuss.nebula-graph.com.cn/ 的 建议反馈 分类下提建议 ;加入 Nebula Graph 交流群,请联系 Nebula Graph 官方小助手微信号:NebulaGraphbot 作者有话说:Hi,我是 Laura,是图数据库 Nebula Graph 研发工程师,希望做的分享能给大家带来帮助,如有不当之处也希望能帮忙纠正,谢谢~
图数据库(英语:Graph Database)是一个使用图结构进行语义查询的数据库。该系统的关键概念是图,形式上是点 (Node 或者 Vertex) 和边 (Edge 或者 Relationship) 的集合。一个顶点代表一个实体,比如,某个人,边则表示两个实体间的关联关系,比如 “你关注 Nebula Graph”的关注关系。图广泛存在于现实世界中,从社交网络到风控场景、从知识图谱到智能推荐。 Nebula Graph 是什么 Nebula Graph 是一款开源的分布式图数据库,擅长处理千亿个顶点和万亿条边的超大规模数据集。提供高吞吐量、低延时的读写能力,内置 ACL 机制和用户鉴权,为用户提供安全的数据库访问方式。 作为一款高性能高可靠的图数据库,Nebula Graph 提供了线性扩容的能力,支持快照方式实现数据恢复功能。在查询语言方面,开发团队完全自研开发查询语言——nGQL,并且后续会兼容 OpenCypher 接口,让 Neo4j 的用户可无缝衔接使用 Nebula Graph。 Nebula Graph 特性 开源:Nebula Graph 代码开源,采用 Apache 2.0 License,用户可以从 GitHub 下载源码自己编译,部署。欢迎提交 pr,成为 Contributor。 可扩展性:存储计算相分离的架构,当存储空间或计算资源不足时,支持对两者独立进行扩容,避免了传统架构需要同时扩容导致的经济效率低问题。云计算场景下,能实现真正的弹性计算。提供线性扩展的能力。 高可用:全对称分布式集群,无单点故障。并且支持多种类型快照方式实现数据恢复,保证在局部失败的情况下服务的高可用性。 HTAP: 支持 OLTP 实时查询的同时提供了 OLAP 的接口,真正在同一份数据上提供实时在线更新的前提下,也提供复杂分析和挖掘的能力。 安全性:内置授权登录与 ACL 机制,提供用户安全的数据库访问方式,也可接入 LDAP 认证。 类 SQL 查询语言 nGQL:类 SQL 的风格减少了程序员迁移的成本,同时具有表达能力强的优点。 Nebula Graph 架构 Nebula Graph 1.0 功能 基础功能 多图空间:支持多图空间,不同的图空间的数据物理隔离,并且可设置不同的副本数,以应对不同的可用性要求。 顶点:支持基本增删改查操作,支持多种顶点类型,也支持同一顶点有多种类型。 边:支持基本增删改查操作,支持有向图,支持节点间存在同一种类型或者不同类型的多条边。 Schema:Tag / EdgeType 支持多种数据类型,支持对属性设置默认值。一个点可以设置多个 Tag。 聚合操作:聚合函数 GROUP BY、排序函数 ORDER BY、限定函数 LIMIT 自由组合返回所需数据。 组合查询:UNION,UNION DISTINCT,INTERSECT,MINUS 对数据集进行组合查询。 条件查询/更新:IF...RETURN 和 UPDATE ... WHEN 根据指定条件查询/更新数据。 Partition: 支持查看数据分片信息,以及 Partition 对应的 leader 信息。 顶点 ID 策略:支持用户自定义 int64 ID, 内置 hash() 和 uuid() 函数生成顶点 ID。 索引:支持索引、联合索引,对已建立索引的数据,按条件查找快速查找数据。 管道查询: 管道符前面查询语句的输出可作为管道符后面命令的输入。 用户定义变量:用户自定义变量可暂时将查询结果存储在自定义的变量中,并在随后查询语句中使用。 多种字符集、字符编码 高级功能 权限管理: 支持用户权限认证,支持用户角色访问控制。可轻松对接现有用户认证系统。 Nebula Graph 提供五种角色权限:GOD、ADMIN、DBA、USER 和 GUEST。 稠密点:对于超级顶点支持蓄水池采样, 在只遍历一遍数据(O(N))的情况下,随机的抽取k个元素。 集群快照:支持以集群维度创建快照,提供在线的数据备份功能,快速恢复。 TTL:支持设置数据的有效期,快速清理过期数据释放资源。 Job Manager:Job 管理调度工具,目前支持 Compaction / Flush 操作。 支持在线扩缩容以及负载均衡 图算法:支持全路径 / 最短路径算法。 提供 OLAP 接口,对接图计算平台。 监控接口:支持系统状态监控、API 访问时间监控、性能数据监控等操作。 客户端 Java 客户端:可自行编译或者从 mvn 仓库进行下载。 Python 客户端:可通过源码安装或者 pip 进行安装。 Go 客户端:可通过 go get -u -v github.com/vesoft-inc/nebula-go 安装使用。 周边工具 Nebula Graph Studio:基于 Web 的可视化环境,提供图操作界面、图数据展示与分析。见 Nebula Graph Studio 导入工具 Nebula Importer,提供高性能的 CSV 文件导入工具,支持导入本地和远程文件。见 Nebula-Importer Spark Writer 基于 Spark 的分布式数据导入工具。见 Spark Writer 导出工具 Dump Tool,单机离线数据导出工具,可以用于导出或统计指定条件的数据。 第三方支持 对接 Prometheus 系统以及 Grafana 可视化组件,支持 Ansible 和 Kubernetes 部署,可实时监控集群的状态。 Nebula Graph 一个开源的分布式图数据库,如果你在使用过程中遇到问题,你可以在论坛:https://discuss.nebula-graph.com.cn/ 和 GitHub:https://github.com/vesoft-inc/nebula 得到帮助 :)
缘起 Nebula Graph 最早的自动化测试是使用搭建在 Azure 上的 Jenkins,配合着 GitHub 的 Webhook 实现的,在用户提交 Pull Request 时,加个 ready-for-testing 的 label 再评论一句 Jenkins go 就可以自动的运行相应的 UT 测试,效果如下: 因为是租用的 Azure 的云主机,加上 nebula 的编译要求的机器配置较高,而且任务的触发主要集中在白天。所以上述的方案性价比较低,从去年团队就在考虑寻找替代的方案,准备下线 Azure 上的测试机,并且还要能提供多环境的测试方案。 调研了一圈现有的产品主要有: TravisCI CircleCI Azure Pipeline Jenkins on k8s(自建) 虽然上面的产品对开源项目有些限制,但整体都还算比较友好。 鉴于之前 GitLab CI 的使用经验,体会到如果能跟 GitHub 深度集成那当然是首选。所谓“深度”表示可以共享 GitHub 的整个开源的生态以及完美的 API 调用(后话)。恰巧 2019,GitHub Action 2.0 横空出世,Nebula Graph 便勇敢的入了坑。 这里简单概述一下我们在使用 GitHub Action 时体会到的优点: 免费。开源项目可以免费使用 Action 的所有功能,而且机器配置较高。 开源生态好。在整个 CI 的流程里,可以直接使用 GitHub 上的所有开源的 Action,哪怕就是没有满足需求的 Action,自己上手写也不是很麻烦,而且还支持 docker 定制,用 bash 就可以完成一个专属的 Action。 支持多种系统。Windows、macOS 和 Linux 都可以一键使用,跨平台简单方便。 可跟 GitHub 的 API 互动。通过 GITHUB_TOKEN 可以直接访问 GitHub API V3,想上传文件,检查 PR 状态,使用 curl 命令即可完成。 自托管。只要提供 workflow 的描述文件,将其放置到 .github/workflows/ 目录下,每次提交便会自动触发执行新的 action run。 Workflow 描述文件改为 YAML 格式。目前的描述方式要比 Action 1.0 中的 workflow 文件更加简洁易读。 下面在讲实践之前还是要先讲讲 Nebula Graph 的需求:首要目标比较明确就是自动化测试。 作为数据库产品,测试怎么强调也不为过。Nebula Graph 的测试主要分单元测试和集成测试。用 GitHub Action 其实主要瞄准的是单元测试,然后再给集成测试做些准备,比如 docker 镜像构建和安装程序打包。顺带再解决一下 PM 小姐姐的发布需求,就整个构建起来了第一版的 CI/CD 流程。 PR 测试 Nebula Graph 作为托管在 GitHub 上的开源项目,首先要解决的测试问题就是当贡献者提交了 PR 请求后,如何才能快速地进行变更验证?主要有以下几个方面。 符不符合编码规范; 能不能在不同系统上都编译通过; 单测有没有失败; 代码覆盖率有没有下降等。 只有上述的要求全部满足并且有至少两位 reviewer 的同意,变更才能进入主干分支。 借助于 cpplint 或者 clang-format 等开源工具可以比较简单地实现要求 1,如果此要求未通过验证,后面的步骤就自动跳过,不再继续执行。 对于要求 2,我们希望能同时在目前支持的几个系统上运行 Nebula 源码的编译验证。那么像之前在物理机上直接构建的方式就不再可取,毕竟一台物理机的价格已经高昂,何况一台还不足够。为了保证编译环境的一致性,还要尽可能的减少机器的性能损失,最终采用了 docker 的容器化构建方式。再借助 Action 的 matrix 运行策略和对 docker 的支持,还算顺利地将整个流程走通。 运行的大概流程如上图所示,在 vesoft-inc/nebula-dev-docker 项目中维护 nebula 编译环境的 docker 镜像,当编译器或者 thirdparty 依赖升级变更时,自动触发 docker hub 的 Build 任务(见下图)。当新的 Pull Request 提交以后,Action 便会被触发开始拉取最新的编译环境镜像,执行编译。 针对 PR 的 workflow 完整描述见文件 pull_request.yaml。同时,考虑到并不是每个人提交的 PR 都需要立即运行 CI 测试,且自建的机器资源有限,对 CI 的触发做了如下限制: 只有 lint 校验通过的 PR 才会将后续的 job 下发到自建的 runner,lint 的任务比较轻量,可以使用 GitHub Action 托管的机器来执行,无需占用线下的资源。 只有添加了 ready-for-testing label 的 PR 才会触发 action 的执行,而 label 的添加有权限的控制。进一步优化 runner 被随意触发的情况。对 label 的限制如下所示: jobs: lint: name: cpplint if: contains(join(toJson(github.event.pull_request.labels.*.name)), 'ready-for-testing') 在 PR 中执行完成后的效果如下所示: Code Coverage 的说明见博文:图数据库 Nebula Graph 的代码变更测试覆盖率实践。 Nightly 构建 在 Nebula Graph 的集成测试框架中,希望能够在每天晚上对 codebase 中的代码全量跑一遍所有的测试用例。同时有些新的特性,有时也希望能快速地打包交给用户体验使用。这就需要 CI 系统能在每天给出当日代码的 docker 镜像和 rpm/deb 安装包。 GitHub Action 被触发的事件类型除了 pull_request,还可以执行 schedule 类型。schedule 类型的事件可以像 crontab 一样,让用户指定任何重复任务的触发时间,比如每天凌晨两点执行任务如下所示: on: schedule: - cron: '0 18 * * *' 因为 GitHub 采用的是 UTC 时间,所以东八区的凌晨 2 点,就对应到 UTC 的前日 18 时。 docker 每日构建的 docker 镜像需要 push 到 docker hub 上,并打上 nightly 的标签,集成测试的 k8s 集群,将 image 的拉取策略设置为 Always,每日触发便能滚动升级到当日最新进行测试。因为当日的问题目前都会尽量当日解决,便没有再给 nightly 的镜像再额外打一个日期的 tag。对应的 action 部分如下所示: - name: Build image env: IMAGE_NAME: ${{ secrets.DOCKER_USERNAME }}/nebula-${{ matrix.service }}:nightly run: | docker build -t ${IMAGE_NAME} -f docker/Dockerfile.${{ matrix.service }} . docker push ${IMAGE_NAME} shell: bash package GitHub Action 提供了 artifacts 的功能,可以让用户持久化 workflow 运行过程中的数据,这些数据可以保留 90 天。对于 nightly 版本安装包的存储而言,已经绰绰有余。利用官方提供的 actions/upload-artifact@v1 action,可以方便的将指定目录下的文件上传到 artifacts。最后 nightly 版本的 nebula 的安装包如下图所示。 上述完整的 workflow 文件见 package.yaml 分支发布 为了更好地维护每个发布的版本和进行 bugfix,Nebula Graph 采用分支发布的方式。即每次发布之前进行 code freeze,并创建新的 release 分支,在 release 分支上只接受 bugfix,而不进行 feature 的开发。bugfix 还是会在开发分支上提交,最后 cherrypick 到 release 分支。 在每次 release 时,除了 source 外,我们希望能把安装包也追加到 assets 中方便用户直接下载。如果每次都手工上传,既容易出错,也非常耗时。这就比较适合 Action 来自动化这块的工作,而且,打包和上传都走 GitHub 内部网络,速度更快。 在安装包编译好后,通过 curl 命令直接调用 GitHub 的 API,就能上传到 assets 中,具体脚本内容如下所示: curl --silent \ --request POST \ --url "$upload_url?name=$filename" \ --header "authorization: Bearer $github_token" \ --header "content-type: $content_type" \ --data-binary @"$filepath" 同时,为了安全起见,在每次的安装包发布时,希望可以计算安装包的 checksum 值,并将其一同上传到 assets 中,以便用户下载后进行完整性校验。具体步骤如下所示: jobs: package: name: package and upload release assets runs-on: ubuntu-latest strategy: matrix: os: - ubuntu1604 - ubuntu1804 - centos6 - centos7 container: image: vesoft/nebula-dev:${{ matrix.os }} steps: - uses: actions/checkout@v1 - name: package run: ./package/package.sh - name: vars id: vars env: CPACK_OUTPUT_DIR: build/cpack_output SHA_EXT: sha256sum.txt run: | tag=$(echo ${{ github.ref }} | rev | cut -d/ -f1 | rev) cd $CPACK_OUTPUT_DIR filename=$(find . -type f \( -iname \*.deb -o -iname \*.rpm \) -exec basename {} \;) sha256sum $filename > $filename.$SHA_EXT echo "::set-output name=tag::$tag" echo "::set-output name=filepath::$CPACK_OUTPUT_DIR/$filename" echo "::set-output name=shafilepath::$CPACK_OUTPUT_DIR/$filename.$SHA_EXT" shell: bash - name: upload release asset run: | ./ci/scripts/upload-github-release-asset.sh github_token=${{ secrets.GITHUB_TOKEN }} repo=${{ github.repository }} tag=${{ steps.vars.outputs.tag }} filepath=${{ steps.vars.outputs.filepath }} ./ci/scripts/upload-github-release-asset.sh github_token=${{ secrets.GITHUB_TOKEN }} repo=${{ github.repository }} tag=${{ steps.vars.outputs.tag }} filepath=${{ steps.vars.outputs.shafilepath }} 上述完整的 workflow 文件见 release.yaml。 命令 GitHub Action 为 workflow 提供了一些命令方便在 shell 中进行调用,来更精细地控制和调试每个步骤的执行。常用的命令如下: set-output 有时在 job 的 steps 之间需要传递一些结果,这时就可以通过 echo "::set-output name=output_name::output_value" 的命令形式将想要输出的 output_value 值设置到 output_name 变量中。 在接下来的 step 中,可以通过 ${{ steps.step_id.outputs.output_name }} 的方式引用上述的输出值。 上节中上传 asset 的 job 中就使用了上述的方式来传递文件名称。一个步骤可以通过多次执行上述命令来设置多个输出。 set-env 同 set-output 一样,可以为后续的步骤设置环境变量。语法: echo "::set-env name={name}::{value}" 。 add-path 将某路径加入到 PATH 变量中,为后续步骤使用。语法: echo "::add-path::{path}" 。 Self-Hosted Runner 除了 GitHub 官方托管的 runner 之外,Action 还允许使用线下自己的机器作为 Runner 来跑 Action 的 job。在机器上安装好 Action Runner 之后,按照教程,将其注册到项目后,在 workflow 文件中通过配置 runs-on: self-hosted 即可使用。 self-hosted 的机器可以打上不同的 label,这样便可以通过不同的标签来将任务分发到特定的机器上。比如线下的机器安装有不同的操作系统,那么 job 就可以根据 runs-on 的 label 在特定的机器上运行。 self-hosted 也是一个特定的标签。 安全 GitHub 官方是不推荐开源项目使用 Self-Hosted 的 runner 的,原因是任何人都可以通过提交 PR 的方式,让 runner 的机器运行危险的代码对其所在的环境进行攻击。 但是 Nebula Graph 的编译需要的存储空间较大,且 GitHub 只能提供 2 核的环境来编译,不得已还是选择了自建 Runner。考虑到安全的因素,进行了如下方面的安全加固: 虚拟机部署 所有注册到 GitHub Action 的 runner 都是采用虚拟机部署,跟宿主机做好第一层的隔离,也更方便对每个虚拟机做资源分配。一台高配置的宿主机可以分配多个虚拟机让 runner 来并行地跑所有收到的任务。 如果虚拟机出了问题,可以方便地进行环境复原的操作。 网络隔离 将所有 runner 所在的虚拟机隔离在办公网络之外,使其无法直接访问公司内部资源。即便有人通过 PR 提交了恶意代码,也让其无法访问公司内部网络,造成进一步的攻击。 Action 选择 尽量选择大厂和官方发布的 action,如果是使用个人开发者的作品,最好能检视一下其具体实现代码,免得出现网上爆出来的泄漏隐私密钥等事情发生。 比如 GitHub 官方维护的 action 列表:https://github.com/actions。 私钥校验 GitHub Action 会自动校验 PR 中是否使用了一些私钥,除却 GITHUB_TOKEN 之外的其他私钥(通过 ${{ secrets.MY_TOKENS }} 形式引用)均是不可以在 PR 事件触发的相关任务中使用,以防用户通过 PR 的方式私自打印输出窃取密钥。 环境搭建与清理 对于自建的 runner,在不同任务(job)之间做文件共享是方便的,但是最后不要忘记每次在整个 action 执行结束后,清理产生的中间文件,不然这些文件有可能会影响接下来的任务执行和不断地占用磁盘空间。 - name: Cleanup if: always() run: rm -rf build 将 step 的运行条件设置为 always() 确保每次任务都要执行该步骤,即便中途出错。 基于 Docker 的 Matrix 并行构建 因为 Nebula Graph 需要在不同的系统上做编译验证,在构建方式上采用了容器的方案,原因是构建时不同环境的隔离简单方便,GitHub Action 可以原生支持基于 docker 的任务。 Action 支持 matrix 策略运行任务的方式,类似于 TravisCI 的 build matrix。通过配置不同系统和编译器的组合,我们可以方便地设置在每个系统下使用 gcc 和 clang 来同时编译 nebula 的源码,如下所示: jobs: build: name: build runs-on: ubuntu-latest strategy: fail-fast: false matrix: os: - centos6 - centos7 - ubuntu1604 - ubuntu1804 compiler: - gcc-9.2 - clang-9 exclude: - os: centos7 compiler: clang-9 上述的 strategy 会生成 8 个并行的任务(4 os x 2 compiler),每个任务都是(os, compiler)的一个组合。这种类似矩阵的表达方式,可以极大的减少不同纬度上的任务组合的定义。 如果想排除 matrix 中的某个组合,只要将组合的值配置到 exclude 选项下面即可。如果想在任务中访问 matrix 中的值,也只要通过类似 ${{ matrix.os }} 获取上下文变量值的方式拿到。这些方式让你定制自己的任务时都变得十分方便。 运行时容器 我们可以为每个任务指定运行时的一个容器环境,这样该任务下的所有步骤(steps)都会在容器的内部环境中执行。相较于在每个步骤中都套用 docker 命令要简洁明了。 container: image: vesoft/nebula-dev:${{ matrix.os }} env: CCACHE_DIR: /tmp/ccache/${{ matrix.os }}-${{ matrix.compiler }} 对容器的配置,像在 docker compose 中配置 service 一样,可以指定 image/env/ports/volumes/options 等等参数。在 self-hosted 的 runner 中,可以方便地将宿主机上的目录挂载到容器中做文件的共享。 正是基于 Action 上面的容器特性,才方便的在 docker 内做后续编译的缓存加速。 编译加速 Nebula Graph 的源码采用 C++ 编写,整个构建过程时间较长,如果每次 CI 都完全地重新开始,会浪费许多计算资源。因为每台 runner 跑的(容器)任务不定,需要对每个源文件及对应的编译过程进行精准判别才能确认该源文件是否真的被修改。目前使用最新版本的 ccache 来完成缓存的任务。 虽然 GitHub Action 本身提供 cache 的功能,由于 Nebula Graph 目前单元测试的用例采用静态链接,编译后体积较大,超出其可用的配额,遂使用本地缓存的策略。 ccache ccache 是个编译器的缓存工具,可以有效地加速编译的过程,同时支持 gcc/clang 等编译器。Nebula Graph 使用 C++ 14 标准,低版本的 ccache 在兼容性上有问题,所以在所有的 vesoft/nebula-dev 镜像中都采用手动编译的方式安装。 Nebula Graph 在 cmake 的配置中自动识别是否安装了 ccache,并决定是否对其打开启用。所以只要在容器环境中对 ccache 做些配置即可,比如在 ccache.conf 中配置其最大缓存容量为 1 G,超出后自动替换较旧缓存。 max_size = 1.0G ccache.conf 配置文件最好放置在缓存目录下,这样 ccache 可方便读取其中内容。 tmpfs tmpfs 是位于内存或者 swap 分区的临时文件系统,可以有效地缓解磁盘 IO 带来的延迟,因为 self-hosted 的主机内存足够,所以将 ccache 的目录挂载类型改为 tmpfs,来减少 ccache 读写时间。在 docker 中使用 tmpfs 的挂载类型可以参考相应文档。相应的配置参数如下: env: CCACHE_DIR: /tmp/ccache/${{ matrix.os }}-${{ matrix.compiler }} options: --mount type=tmpfs,destination=/tmp/ccache,tmpfs-size=1073741824 -v /tmp/ccache/${{ matrix.os }}-${{ matrix.compiler }}:/tmp/ccache/${{ matrix.os }}-${{ matrix.compiler }} 将所有 ccache 产生的缓存文件,放置到挂载为 tmpfs 类型的目录下。 并行编译 make 本身即支持多个源文件的并行编译,在编译时配置 -j $(nproc) 便可同时启动与核数相同的任务数。在 action 的 steps 中配置如下: - name: Make run: cmake --build build/ -j $(nproc) 坑 说了那么多的优点,那有没有不足呢?使用下来主要体会到如下几点: 只支持较新版本的系统。很多 Action 是基于较新的 Nodejs 版本开发,没法方便地在类似 CentOS 6 等老版本 docker 容器中直接使用。否则会报 Nodejs 依赖的库文件找不到,从而无法正常启动 action 的执行。因为 Nebula Graph 希望可以支持 CentOS 6,所以在该系统下的任务不得不需要特殊处理。 不能方便地进行本地验证。虽然社区有个开源项目 act,但使用下来还是有诸多限制,有时不得不通过在自己仓库中反复提交验证才能确保 action 的修改正确。 目前还缺少比较好的指导规范,当定制的任务较多时,总有种在 YAML 配置中写程序的感受。目前的做法主要有以下三种: 根据任务拆分配置文件。 定制专属 action,通过 GitHub 的 SDK 来实现想要的功能。 编写大的 shell 脚本来完成任务内容,在任务中调用该脚本。 目前针对尽量多使用小任务的组合还是使用大任务的方式,社区也没有定论。不过小任务组合的方式可以方便地定位任务失败位置以及确定每步的执行时间。 Action 的一些历史记录目前无法清理,如果中途更改了 workflows 的名字,那么老的 check runs 记录还是会一直保留在 Action 页面,影响使用体验。 目前还缺少像 GitLab CI 中手动触发 job/task 运行的功能。无法运行中间进行人工干预。 action 的开发也在不停的迭代中,有时需要维护一下新版的升级,比如:checkout@v2 不过总体来说,GitHub Action 是一个相对优秀的 CI/CD 系统,毕竟站在 GitLab CI/Travis CI 等前人肩膀上的产品,还是有很多经验可以借鉴使用。 后续 定制 Action 前段时间 docker 发布了自己的第一款 Action,简化用户与 docker 相关的任务。后续,针对 Nebula Graph 的一些 CI/CD 的复杂需求,我们亦会定制一些专属的 action 来给 nebula 的所有 repo 使用。通用的就会创建独立的 repo,发布到 action 市场里,比如追加 assets 到 release 功能。专属的就可以放置 repo 的 .github/actions 目录下。 这样就可以简化 workflows 中的 YAML 配置,只要 use 某个定制 action 即可。灵活性和拓展性都更优。 跟钉钉/slack 等 IM 集成 通过 GitHub 的 SDK 可以开发复杂的 action 应用,再结合钉钉/slack 等 bot 的定制,可以实现许多自动化的有意思的小应用。比如,当一个 PR 被 2 个以上的 reviewer approve 并且所有的 check runs 都通过,那么就可以向钉钉群里发消息并 @ 一些人让其去 merge 该 PR。免去了每次都去 PR list 里面 check 每个 PR 状态的辛苦。 当然围绕 GitHub 的周边通过一些 bot 还可以迸发许多有意思的玩法。 One More Thing... 图数据库 Nebula Graph 1.0 GA 快要发布啦。欢迎大家来围观。 本文中如有任何错误或疏漏欢迎去 GitHub:https://github.com/vesoft-inc/nebula issue 区向我们提 issue 或者前往官方论坛:https://discuss.nebula-graph.com.cn/ 的 建议反馈 分类下提建议 ;加入 Nebula Graph 交流群,请联系 Nebula Graph 官方小助手微信号:NebulaGraphbot 作者有话说:Hi,我是 Yee,是图数据 Nebula Graph 研发工程师,对数据库查询引擎有浓厚的兴趣,希望本次的经验分享能给大家带来帮助,如有不当之处也希望能帮忙纠正,谢谢~
D3.js 作为一个前端,说到可视化除了听过 D3.js 的大名,常见的可视化库还有 ECharts、Chart.js,这两个库功能也很强大,但是有一个共同特点是封装层次高,留给开发者可设计和控制的部分太少。和 EChart、Chart.js 等相比,D3.js 的相对来说自由度会高很多,得益于 D3.js 中的 SVG 画图对事件处理器的支持,D3.js 可将任意数据绑定到文档对象模型(DOM)上,也可以直接操作对象模型(DOM)完成 W3C DOM API 相关操作,对于想要展示自己设计图形的开发者,D3.js 绝对是一个不错的选择。 d3-force 力导向图 以实现一个关系网来说,d3-force 力导向图是不二的选择。d3-force 是 D3.js 实现以模拟粒子物理运动的 velocity Verlet 数值积分器的模块,可用来控制粒子和边秩序。在力导向图中,d3-force 中的每个节点都可以看成是一个放电粒子,粒子间存在某种斥力(库仑斥力)。同时,这些粒子间被它们之间的“边”所牵连,从而产生牵引力。 而 d3-force 中的粒子在斥力和牵引力的作用下,从随机无序的初态不断发生位移,逐渐趋于平衡有序。整个图只有点 / 边,图形实现样例较少且自定义样式居多。 下图就是最简单的关系网图,想要实现自己想要的关系网图,还是动手自己实现一个 D3.js 力导向图最佳。 构建 D3.js 力导向图 在这里实践过程中,我们用 D3.js 力导向图来对图数据库的数据关系进行分析,其节点和关系线直观地体现出图数据库的数据关系,并且还可以关联相对应的图数据库语句完成拓展查询。进阶来说,可通过对文档对象模型(DOM)的直接操作同步到数据库进而更新数据,当然操作这个比较复杂, 不在本文中详细讲述。 下面,我们来实现一个简单的力导向图,初窥 D3.js 对数据分析的作用和显示优化的一些思路。首先我们创建一个力导向图: this.force = d3 .forceSimulation() // 为节点分配坐标 .nodes(data.vertexes) // 连接线 .force('link', linkForce) // 整个实例中心 .force('center', d3.forceCenter(width / 2, height / 2)) // 引力 .force('charge', d3.forceManyBody().strength(-20)) // 碰撞力 防止节点重叠 .force('collide',d3.forceCollide().radius(60).iterations(2)); 通过上述代码,我们可以得到下面这样一个可视化的节点和关系图。 实现拓展查询显示优化 看到关系图(上图),我们会发现有一个新需求:选中节点继续往下拓展查询。为了实现拓展查询,在这里笔者要介绍下 D3.js 自带 API。 D3.js 的 enter() API 可对新增的节点作单独的逻辑处理,所以当拓展查询到新的节点 push 进节点数组时,不会去改变之前存在的节点信息(包括 x,y 坐标),而是按照 d3-force 实例分配的坐标进行渲染。从 API 上理解来说确实是这样,但是新增的节点对于 d3-force 这个已经存在的实例来说是一个不是简单的 push 就能处理的。因为 d3.forceSimulation() 这个模型给当前节点分配的位置坐标(x,y)是随机,目前看来没什么问题对不对? 但由于d3.forceSimulation().node() 的坐标随机分配导致了图形拓展出来位置的随机出现,加上之前 d3-force 实例中我们设定好的 collide(碰撞力)和 links (引力)参数,所以和新节点相关联的节点受到牵引力影响互相靠近。在靠近的过程中又会和其他节点发送碰撞力的作用,当力导图存在的节点的情况下,这些新增节点出现时会让整个力导向图在 collide 和 links 的作用下不停地碰撞,进行牵引,直到每个节点都找到自己合适的位置,即碰撞力和牵引力都满足要求时才停止移动,看看下图,像不像宇宙大爆炸 。 上述无序到有序熵减的过程,站在用户角度,每新增一个节点导致整个力导图都在一直在动,除了有一种抽搐的感,停止图形变化又需要长时间的等待,这是不能接受的。可 D3.js API enter() 又是这样定义规定好的,难道新增的节点和之前的节点的呈现处理需要开发者分开单独处理吗?如果是分开单独处理,每次节点渲染都要遍历判断是不是新增,在节点较多时反而更影响性能?那么如何优化这个新增节点呈现的问题呢? 网上解决新增节点呈现问题,大多采用减小 d3-force 实例 collide,增大 links 的 distance 参数值,这样会让节点很快地找到平衡点从而使整个力导图稳定下来,这确实是一个好办法。但是这样节点之间的连线长度相差明显,而且图形整体偏大,对于大数据量的 case 来说,这种显示方式并不太适合。 基于上述的方法,笔者有了另一种解决思路——保证新增节点是在该选中拓展的节点周围,也就是说直接把新增节点的坐标设置为对应选择拓展节点一样的 x,y 坐标而不用 D3 .forceSimulation().node() 分配,这样利用 d3-force 这个实例的节点碰撞力,保证新增节点的出现都不会覆盖,最终会在选中拓展节点周围出现。 这样处理虽然还是对新增节点小的范围内的节点有影响,但相对来说,不会大幅度地影响整个关系图形走势。关键代码如下: # 给新增的坐标设置为拓展起点中心或整个图中心 addVertexes.map(d => { d.x = _.meanBy(selectVertexes, 'x') || svg.style('width') / 2; d.y = _.meanBy(selectVertexes, 'y') || svg.style('heigth') / 2; }); 如果没有选中节点(既添加起点)则该起点坐标位置就在图中心位置,对已存在的节点来说,影响程度会小很多,这还是一个很不错的思路,这个解决办法可以推荐一下。 除了新增节点的呈现问题,整个图形的呈现还有另外一个问题:两点之间多边优化显示处理。 两点之间多边优化显示处理 当两个节点之间存在多条边关系时,默认连接线是直线的情况下肯定会出现多线覆盖。因此曲线连接便成了我们的另外需要解决的问题。曲线如何定义弯曲度保证两点之间的多条线不会交互覆盖呢?在多条线弯曲下,如何平均半圆弧弯曲避免全跑到某半圆弧上?定义曲线弧方向? 上述问题都是下一步需要解决的问题,其实问题的解决方法也不少。目前笔者采用了先统计下两点之间的线条数,再将这些连接线分配到一个 map 里,两个节点的 name 字段进行拼接做成 key,这样计算得到两点之间的连接线总数。 然后在遍历时同 map 的线根据方向分成正向、反向两组,正向组遍历给每条线追加设置一个 linknum 编号,同理,反向组遍历追加一个 -linknum 编号值。这个正向、反向判断方法很多,笔者是根据节点 source.name、target.name 进行比较,btw,这里其实是比较 ASCII 码。定义连接线的正反方向办法太多了,用两点之间的任意固定字段比较即可,在这里不做赘述。而我们设定的 linknum 值就是来确定该条弧线的弯曲度和弯曲方向的,这里搭配下面代码讲解比较好理解: const linkGroup = {}; // 两点之间的线根据两点的 name 属性设置为同一个 key,加入到 linkGroup 中,给两点之间的所有边分成一个组 edges.forEach((link: any) => { const key = link.source.name < link.target.name ? link.source.name + ':' + link.target.name : link.target.name + ':' + link.source.name; if (!linkGroup.hasOwnProperty(key)) { linkGroup[key] = []; } linkGroup[key].push(link); }); // 遍历给每组去调用 setLinkNumbers 来分配 linkum edges.forEach((link: any) => { const key = setLinkName(link); link.size = linkGroup[key].length; const group = linkGroup[key]; const keyPair = key.split(':'); let type = 'noself'; if (keyPair[0] === keyPair[1]) { type = 'self'; } setLinkNumbers(group, type); }); #根据不同方向分为 linkA,linkB 两个数组,分别分配两种 linknum,从而控制上下椭圆弧 export function setLinkNumbers(group) { const len = group.length; const linksA: any = []; const linksB: any = []; for (let i = 0; i < len; i++) { const link = group[i]; if (link.source.name < link.target.name) { linksA.push(link); } else { linksB.push(link); } } let startLinkANumber = 1; linksA.forEach(linkA=> { linkA.linknum = startLinkANumber++; } let startLinkBNumber = -1; linksB.forEach(linkB=> { linkB.linknum = startLinkBNumber--; } } 按照我们上面描述的思路,给每条连接线分配 linknum 值后,接着在实现监听连接线的的 tick 事件函数里面判断 linknum 正负数判断设置 path 路径的弯曲度和方向 就行了,最终效果如下图 结语 好了,以上便是笔者使用 D3.js 力导向图实现关系网的优化思路和方法。其实要构建一个复杂的关系网,需要考虑的问题很多,需要优化的地方也很多,今天给大家分享两个最容易遇到的新节点呈现、多边处理问题,后续我们会继续产出 D3.js 优化系列文,欢迎订阅 Nebula Graph 博客。 最后,你可以通过访问图数据库 Nebula Graph Studio:Nebula-Graph-Studio,体验下 D3.js 是如何呈现关系的。本文中如有任何错误或疏漏欢迎去 GitHub:https://github.com/vesoft-inc/nebula 我们用 D3.js 力导向图来对图数据库的数据关系进行分析,其节点和关系线直观地体现出图数据库的数据关系,并且还可以关联相对应的图数据库语句完成拓展查询。此外,本文还讲解了如何优化新增节点和多边关系的显示2github) issue 区向我们提 issue 或者前往官方论坛:https://discuss.nebula-graph.com.cn/ 的 建议反馈 分类下提建议 ;加入 Nebula Graph 交流群,请联系 Nebula Graph 官方小助手微信号:NebulaGraphbot 作者有话说:Hi,我是 Nico,是 Nebula Graph 的前端工程师,对数据可视化比较感兴趣,分享一些自己的实践心得,希望这次分享能给大家带来帮助,如有不当之处,欢迎帮忙纠正,谢谢~
对于一个持续开发的大型工程而言,足够的测试是保证软件行为符合预期的有效手段,而不是仅仅依靠 code review 或者开发者自己的技术素质。测试的编写理想情况下应该完全定义软件的行为,但是通常情况都是很难达到这样理想的程度。而测试覆盖率就是检验测试覆盖软件行为的情况,通过检查测试覆盖情况可以帮助开发人员发现没有被覆盖到的代码。 测试覆盖信息搜集 Nebula Graph 主要是由 C++ 语言开发的,支持大部分 Linux 环境以及 gcc/clang 编译器,所以通过工具链提供的支持,我们可以非常方便地统计Nebula Graph的测试覆盖率。 gcc/clang 都支持 gcov 式的测试覆盖率功能,使用起来也是非常简单的,主要有如下几个步骤: 添加编译选项 --coverage -O0 -g 添加链接选项 --coverage 运行测试 使用 lcov,整合报告,例如 lcov --capture --directory . --output-file coverage.info 去掉外部代码统计,例如 lcov --remove coverage.info '*/opt/vesoft/*' -o clean.info 到这里测试覆盖信息已经搜集完毕,接下可以通过 genhtml 这样的工具生成 html,然后通过浏览器查看测试覆盖率,如下图所示: 但是这样是非常不方便的,因为在持续的开发过程,如果每次都要手动进行这样一套操作,那必然带来极大的人力浪费,所以现在的常用做法是将测试覆盖率写入 CI 并且和第三方平台(比如 Codecov,Coveralls)集成,这样开发人员完全不必关心测试覆盖信息的收集整理和展示问题,只需要发布代码后直接到第三方平台上查看覆盖情况即可,而且现在的第三方平台也支持直接在 PR 上评论覆盖情况使得查看覆盖率的变更情况更加方便。 集成 CI Github Action 现在主流的 CI 平台非常多,比如 Travis,azure-pipelines 以及 GitHub Action 等。Nebula Graph 选用的是 GitHub Action,对于 Action 我们在之前的《使用 Github Action 进行前端自动化发布》这篇文章里已经做过介绍。 而 GitHub Action 相对于其他 CI 平台来说,有和 GitHub 集成更好,Action 生态强大简洁易用以及支持相当多的操作系统和 CPU 等优势。Nebula Graph 有关测试覆盖的 CI 脚本片段如下所示: - name: CMake with Coverage if: matrix.compiler == 'gcc-9.2' && matrix.os == 'centos7' run: | cmake -DENABLE_COVERAGE=ON -B build/ 可以看到这里我们将前文介绍的 coverage 相关的编译选项通过一个 cmake option 进行管理,这样可以非常方便地启用和禁止 coverage 信息的收集。比如在开发人员在正常的开发编译测试过程中通常不会开启这项功能以避免编译测试运行的额外开销。 - name: Testing Coverage Report working-directory: build if: success() && matrix.compiler == 'gcc-9.2' && matrix.os == 'centos7' run: | set -e /usr/local/bin/lcov --version /usr/local/bin/lcov --capture --gcov-tool $GCOV --directory . --output-file coverage.info /usr/local/bin/lcov --remove coverage.info '*/opt/vesoft/*' -o clean.info bash <(curl -s https://codecov.io/bash) -Z -f clean.info 这里主要是测试报告的收集、合并以及上传到第三方平台,这个在前文中已经比较详细地叙述过,CI 的运行情况如下图所示: 集成测试覆盖率平台 Codecov Nebula Graph 选择的测试覆盖平台是 Codecov——一个测试结果分析工具,对于 GitHub Action 而言,主要是在 CI 中执行上述的测试覆盖信息搜集脚本以及将最终的测试覆盖文件上传到 Codecov平台。 这里用户给自己的 repo 注册 Codecov 后可以获取一个访问的 token,通过这个 token 和 Codecov 的 API 可以将测试覆盖文件上传到 Codecov 这个平台上,具体的 API 可以参考 https://docs.codecov.io/reference#upload ,除了上传报告外还有列出 pr,commit 等 API 可以让用户开发自己的 bot 做一些自动化的工具,然后就可查看各种测试覆盖的信息,比如 Nebula Graph 的测试覆盖情况可以查看 https://codecov.io/gh/vesoft-inc/nebula 。 比如可以通过这个饼状图查看不同目录代码的覆盖情况: 也可以点开一个具体的文件,查看哪些行被覆盖那些行没有被覆盖: 当然我们一般不会直接使用 Codecov 的 API,而是使用他提供的一个 cli 工具,比如上传报告使用 bash <(curl -s https://codecov.io/bash) -Z -t <token> -f clean.info ,这里的 token 就是 Codecov 提供的认证 token,一般来说作为环境变量 CODECOV_TOKEN 使用,而不是输入明文。 通过上述操作呢就可以在 Codecov 平台上查看你的工程的测试覆盖情况,并且可以看到每次 pr 增加减少了多少覆盖率,方便逐渐提高测试覆盖率。最后的话还可以在你的 README 上贴上 Codecov 提供的测试覆盖率 badge,就像 Nebula Graph 一样:https://github.com/vesoft-inc/nebula。 本文中如有错误或疏漏欢迎去 GitHub:https://github.com/vesoft-inc/nebula issue 区向我们提 issue 或者前往官方论坛:https://discuss.nebula-graph.com.cn/ 的 建议反馈 分类下提建议 ;加入 Nebula Graph 交流群,请联系 Nebula Graph 官方小助手微信号:NebulaGraphbot 推荐阅读 应用 AddressSanitizer 发现程序内存错误 Github Statistics 一个基于 React 的 GitHub 数据统计工具 作者有话说:Hi,我是 shylock,是 Nebula Graph 的研发工程师,希望本文对你有所帮助,如果有错误或不足也请与我交流,不甚感激! 声明:本文采用 CC BY-NC-ND 4.0 协议进行授权 署名-非商业性使用-禁止演绎 4.0 国际
Nebula Graph 是一个高性能、高可用、强一致的分布式图数据库。由于 Nebula Graph 采用的是存储计算分离架构,在存储层实际只是暴露了简单的 kv 接口,采用 RocksDB 作为状态机,通过 Raft 一致性协议来保证多副本数据一致的问题。Raft 协议虽然比 Paxos 更加容易理解,但在工程实现上还是有很多需要注意和优化的地方。 另外,如何测试基于 Raft 的分布式系统也是困扰业界的问题,目前 Nebula 主要采用了 Jepsen 作为一致性验证工具。之前我的小伙伴已经在《Jepsen 测试框架在图数据库 Nebula Graph 中的实践》中做了详细的介绍,对 Jepsen 不太了解的同学可以先移步这篇文章。 在这篇文章中将着重介绍如何通过 Jepsen 来对 Nebula Graph 的分布式 kv 进行一致性验证。 强一致的定义 首先,我们需要什么了解叫强一致,它实际就是 Linearizability,也被称为线性一致性。引用《Designing Data-Intensive Applications》里一书里的定义: In a linearizable system, as soon as one client successfully completes a write, all clients reading from the database must be able to see the value just written. 也就是说,强一致的分布式系统虽然其内部可能有多个副本,但对外暴露的就好像只有一个副本一样,客户端的任何读请求获取到的都是最新写入的数据。 Jepsen 如何检查系统是否满足强一致 以一个 Jepsen 测试的 timeline 为例,采用的模型为 single-register,也就是整个系统只有一个寄存器(初始值为空),客户端只能对该寄存器进行 read 或者 write 操作(所有操作均为满足原子性,不存在中间状态)。同时有 4 个客户端对这个系统发出请求,图中每一个方框的上沿和下沿代表发出请求的时间和收到响应的时间。 从客户端的角度来看,对于任何一次请求,服务端处理这个请求可能发生在从客户端发出请求到接收到对应的结果这段时间的任何一个时间点。可以看到在时间上,客户端 1/3/4 的三个操作 write 1/write 4/read 1 在时间上实际上是存在 overlap 的,但我们可以通过不同客户端所收到的响应,确定系统真正的状态。 由于初始值为空,客户端 4 的读请求却获取到了 1,说明客户端 4 的 read 操作一定在客户端 1 的 write 1 之后,且 write 4 发生在 write 1 之前(否则会读出 4),则可以确认三个操作实际发生的顺序为 write 4 -> write 1 -> read 1。尽管从全局角度看,read 1 的请求最先发出,但实际却是最后被处理的。后面的几个操作在时间上是不存在 overlap,是依次发生的,最终客户端 2 最后读到了最后一次写入的 4,整个过程中没有违反强一致的定义,验证通过。 如果客户端 3 的那次 read 获取到的值是 4,那么整个系统就不是强一致的了,因为根据之前的分析,最后一次成功写入的值为 1,而客户端 3 却读到了 4,是一个过期的值,也就违背了线性一致性。事实上,Jepsen 也是通过类似的算法来验证分布式系统是否满足强一致的。 通过 Jepsen 的一致性验证找到对应问题 我们先简单介绍一下 Nebula Raft 里面处理一个请求的流程(以三副本为例),以便更好地理解后面的问题。读请求相对简单,由于客户端只会将请求发送给 leader,leader 节点只需要在确保自己是 leader 的前提下,直接从状态机获取对应结果返回给客户端即可。 写请求的流程则复杂一些,如 Raft Group 图所示: Leader(图中绿色圈) 收到 client 发送的 request,写入到自己的 wal(write ahead log)中。 Leader将 wal 中对应的 log entry 发送给 follower,并进入等待。 Follower 收到 log entry 后写入自己的 wal 中(不等待应用到状态机),并返回成功。 Leader 接收到至少一个 follower 返回成功后,应用到状态机,向 client 发送 response。 下面我将用示例来说明通过 Jepsen 测试在之前的Raft实现中发现的一致性问题: 如上图所示,ABC 组成一个三副本 raft group,圆圈为状态机(为了简化,假设其为一个 single-register),方框中则是保存的相应 log entry。 在初始状态,三个副本中的状态机中都为 1,Leader 为 A,term为 1 客户端发送了 write 2 的请求,Leader 根据上面的流程进行处理,在向 client 告知写入成功后被 kill。(step 4 完成后) 此后 C 被选为 term 2 的 leader,但由于 C 此时有可能还没有将之前 write 2 的 log entry 应用到状态机(此时状态机中仍为1)。如果此时 C 接受到客户端的读请求,那么 C 会直接返回 1。这违背了强一致的定义,之前已经成功写入 2,却读到了过期的结果。 这个问题是出在 C 被选为 term 2 的 leader 后,需要发送心跳来保证之前 term 的 log entry 被大多数节点接受,在这个心跳成功之前是不能对外提供读(否则可能会读到过期数据)。有兴趣的同学可以参考 raft parer 中的 Figure 8 以及 5.4.2 小节。 从上一个问题出发,通过 Jepsen 我们又发现了一个相关的问题:leader 如何确保自己还是 leader?这个问题经常出现在网络分区的时候,当 leader 因为网络问题无法和其他节点通信从而被隔离后,此时如果仍然允许处理读请求,有可能读到的就是过期的值。为此我们引入了 leader lease 的概念。 当某个节点被选为 leader 之后,该节点需要定期向其他节点发送心跳,如果心跳确认大多数节点已经收到,则获取一段时间的租约,并确保在这段时间内不会出现新的 leader,也就保证该节点的数据一定是最新的,从而在这段时间内可以正常处理读请求。 和 TiKV 的处理方法不同的是,我们没有采取心跳间隔乘以系数作为租约时间,主要是考虑到不同机器的时钟漂移不同的问题。而是保存了上一次成功的 heartbeat 或者 appendLog 所消耗的时间 cost,用心跳间隔减去 cost 即为租约时间长度。 当发生网络分区时, leader 尽管被隔离,但是在这个租约时间仍然可以处理读请求(对于写请求,由于被隔离,都会告知客户端写入失败), 超出租约时间后则会返回失败。当 follower 在至少一个心跳间隔时间以上没有收到 leader 的消息,会发起选举,选出新 leader 处理后续的客户端请求。 结语 对于一个分布式系统,很多问题需要长时间的压力测试和故障模拟才能发现,通过 Jepsen 能够在不同注入故障的情况下验证分布式系统。之后我们也会考虑采用其他混沌工程工具来验证 Nebula Graph,在保证数据高可靠的前提下不断提高性能。 本文中如有错误或疏漏欢迎去 GitHub:https://github.com/vesoft-inc/nebula issue 区向我们提 issue 或者前往官方论坛:https://discuss.nebula-graph.com.cn/ 的 建议反馈 分类下提建议 ;加入 Nebula Graph 交流群,请联系 Nebula Graph 官方小助手微信号:NebulaGraphbot 推荐阅读 Jepsen 测试框架在图数据库 Nebula Graph 中的实践 Nebula 架构剖析系列(一)图数据库的存储设计 作者有话说:Hi,我是 critical27,是 Nebula Graph 的研发工程师,目前主要从事存储相关的工作,希望能为图数据库领域带来一些自己的贡献。希望本文对你有所帮助,如果有错误或不足也请与我交流,不甚感激。
Discourse 介绍 Discourse 是一款由 Stack Overflow 的联合创始人——Jeff Atwood,基于 Ruby on Rails 开发的开源论坛。相较于传统论坛,Discourse 从他全面开放的开源态度、简介明了的页面风格到其特有的内容运作体系都在证明自己是一款为下一个 10 年的互联网而设计的产品。现在,诸如 Car Talk 等国外知名产品都采用 Discourse 为论坛方案。 作为一个开源的论坛项目,Discourse 相对其他的论坛有以下亮点: 高度可定制:从发帖等级要求权限到论坛帖子标题最少字数要求,Discourse 在论坛设置里罗列了 25 设置大项,300+ 个论坛小项,即使大家都使用 Discourse 搭建论坛但是每个用 Discourse 搭建的论坛都有自己的风格。 插件:Discourse 官方及 Discourse 开源社区用户开发了丰富的插件可供使用,比如:个性化导航、自定义论坛封面。 集成:可接入第三方产品,Google Analytics、 Slack、Wordpress 都在支持之列。 免费:虽然 Discourse 有 $100/Month 的托管服务,但是你可以完全自行部署免费使用 Discourse 服务。 其他:Discourse 还有其他许多的好处,举个例子,它提供了一个机器人 Discobot 是一个可自定义的 bot,交互式地教新用户使用平台的许多功能,例如为主题添加书签),单框链接(嵌入的预览),添加 emoji表情,非常简单的格式设置,添加图片回复,标记帖子以及如何使用搜索功能。 丰富的插件、可自定义论坛设置便是 Nebula Graph 选择 Discourse 最大的原因,而本文不在于介绍如何搭建 Discourse(搭建 Discourse 是一个简单的活,可自行搜索教程),本文旨在介绍图数据库 Nebula graph 如何利用 CDN 来部署 Discourse。 部署 Discourse 自托管的原因 尽管 Discourse 官方的托管服务,但由于国内的访问质量不稳定、不能自由的修改插件和自定义网络设置,因此我们决定自行托管这项服务。基于自托管服务,我们对网络、插件系统做了一些自定义修改,使得目前 Nebula Graph社区有着更好的访问速度和功能。 自托管论坛服务要求 经测试以下配置清单可以完全满足我们部署 Discourse 的要求: 2G 内存以上的 Linux 服务器,如果使用 1G 内存的主机,则需要开启 SWAP 分区。 具备完整控制权的域名,注册邮件服务和 CDN 服务时我们会用到它。 一个 Cloudflare 账号,这会对加速网站和提高安全性有帮助。 一个可用的 SMTP 邮件服务。 为 Linux 服务器部署 Docker 服务,国内用户可添加 Azure 中国、七牛云的镜像域名 部署实践 Cloudflare 介绍 Cloudflare 是一家覆盖全世界主要地区的 CDN 服务商,在提供基本的 CDN 服务同时,他们还提供高质量的 DNS 查询、DDOS 保护、缓存加速服务。相比其他的 CDN 服务商,他们产品理念更为先进,不仅有着良好的服务质量且拥有非常低廉的价格(通常情况下甚至是免费的),因此目前 Cloudflare 的用户规模非常庞大,是值得首选的CDN服务商。 设定 Cloudflare 的 DNS记录 先设定 DNS 记录可减少首次部署时无法通过 Let's encrypt 申请证书的概率。在 Cloudflare 的 DNS 配置中,添加类型为 A 的记录指向服务器的 IP 地址即可。 这里需提醒下,不要将 Proxy status 设置为“Proxied”,这会导致页面因重定向次数过多而无法访问。我们将在完成正确的配置后开启 Proxy status 设置。 配置 Cloudflare SSL/TLS Full 和 Flexible 是 Cloudflare 上最常用的两种 SSL 模式,在正确的启用 CDN 前,需要对其进行设置。首先来到 SSL/TLS 设置面板,选择 Full 模式,这种方式会确保 CDN 回源时也可以通过 HTTPS 来访问源站,有效地提高了内容安全性。 然后进入 Origin Server 标签,进行创建证书的操作,在私钥类型中选择 RSA,BTW,这是最具兼容性的证书类型,ECDSA 则具有更好的性能。 在被证书保护的域名列表中输入论坛的域名,例如 Nebula Graph 论坛地址为:discuss.nebula-graph.io,证书有效期选择 1 年即可。点击 Next 后将会获取到证书的公钥和私钥,分别保存为“ssl.crt”和“ssl.key”将其妥善保存,我们将在后续的步骤中用到他们。 配置和部署 Discourse Discourse 有完善的 Docker 镜像,因此在正确的安装 Docker 后可以直接运行它。 安装 Discourse 将 Discourse 官方 Docker 镜像拉取至 /var/discourse 目录下。 sudo -s git clone https://github.com/discourse/discourse_docker.git /var/discourse cd /var/discourse 在 /var/discourse 目录下执行 ./discourse-setup 可以看到如下交互式界面,在此界面依次填入域名、管理员邮箱、SMTP 邮件服务器信息以及 Let’s Encrypt 通知邮箱地址即可完成论坛的基础配置。 Hostname for your Discourse? [discourse.example.com]: [论坛的域名] Email address for admin account(s)? [me@example.com,you@example.com]:[管理员邮箱,此邮箱不会公开] SMTP server address? [smtp.example.com]: [SMTP邮件服务器地址] SMTP port? [587]: [SMTP邮件服务器端口] SMTP user name? [user@example.com]: [论坛自动发信邮箱账号] SMTP password? [pa$$word]: [论坛自动发信邮箱账号的密码] Let's Encrypt account email? (ENTER to skip) [me@example.com]: [自动更新证书的通知邮箱地址] SSL 注意事项 使用 SSL 需要注意的是,如果 DNS 记录还未传播至服务器所使用的 DNS 服务器,将无法使用 Let’s Encrypt 的 SSL 证书自动注册服务。由于我们将使用上文中已申请的 Cloudflare 证书,因此这里可以跳过 Let's Encrypt account email 这一项。 论坛启动 大约等待 10 分钟后,可通过之前设定的域名:discuss.nebula-graph.io 访问自己的 Discourse 论坛。如果首次访问时出现了 502 错误,这是由于服务还未完全初始化,通常情况下稍等片刻即可。 配置 Discourse Discourse 的配置文件位于 /var/discourse/containers/app.yml 邮件服务设定 邮件服务是整个部署过程中容易出现设定错误的部分之一。对于大多数邮件服务而言,正确的配置 SMTP 服务器地址、端口以及发信人的账户密码即可完成设定。SMTP 服务器地址和可用端口通常在邮件服务提供者的帮助页面上都可以查到,部分个人邮箱可能需要创建应用专用密码才能使用SMTP服务。 但对于 Office365 以及腾讯这种企业邮箱而言,则需要手动在 app.yml 中指定账户验证方式为 login。参考配置如下: DISCOURSE_SMTP_ADDRESS: smtp.office365.com DISCOURSE_SMTP_PORT: 587 DISCOURSE_SMTP_USER_NAME: example@office365.com DISCOURSE_SMTP_PASSWORD: ********** DISCOURSE_SMTP_ENABLE_START_TLS: true DISCOURSE_SMTP_AUTHENTICATION: login 这是因为 Discourse 默认的邮箱身份验证方式是 plain。如果不确定使用何种方式验证,可通过 swaks 这个邮件服务测试工具来进行监测。参考: swaks --to [收件邮箱] --from [发件邮箱] --server [SMTP服务器地址] --auth [login/plain] --auth-user [发件邮箱] -tls -p [端口] 在确保能够通过 Discourse 邮件发送测试的同时,还需注意两项功能设定才能够确保用户能够收到邮件。 位于论坛 Setting-Required 下的 notification email,这里需要配置为和 SMTP 登录账号相同的邮箱地址。 disable emails,通常在进行论坛迁移、备份还原后这一项通常会被设置为 non-staff,此时,设置为 no 后将恢复邮件发送。 SSL、CDN服务设定 在 app.yml 文件中,tempates 下引入 templates/cloudflare.template.yml 和 templates/web.ssl.template.yml 两个模板文件。再次登录 Cloudflare 账号,将 DNS 记录从 DNS only 改为 Proxied,等待记录生效。如果本地的网络质量不佳,也可引入 templates/web.china.template.yml 模版,它将从国内的 Ruby 镜像获取资源。 templates: - "templates/postgres.template.yml" - "templates/redis.template.yml" - "templates/web.template.yml" - "templates/web.ratelimited.template.yml" - "templates/cloudflare.template.yml" ## Uncomment these two lines if you wish to add Lets Encrypt (https) - "templates/web.ssl.template.yml" # - "templates/web.letsencrypt.ssl.template.yml" 在 /var/discourse/shared/standalone/`ssl` 目录下放入步骤2 中所创建的证书文件。 加入新插件(可选) 为了更好帮助海外用户阅读论坛上的中文内容,我们引入了翻译插件。Discourse 拥有丰富的插件,因此如果有需要,你可以安装任何你感兴趣的插件。 在 app.yml 文件的 hooks 字段下配置可被 git 获取的链接,当 Discourse 的 Docker container 重新创建时新插件既完成安装。 ## Plugins go here ## see https://meta.discourse.org/t/19157 for details hooks: after_code: - exec: cd: $home/plugins cmd: - git clone https://github.com/discourse/docker_manager.git - git clone https://github.com/discourse/discourse-translator.git 完成配置后 在完成以上配置后,在 /var/discourse 目录录下运行 ./launcher rebuild app,并再次等待 10 分钟,即可完成最终的构建。 对于个人站长而言,还需安装并配置 Fail2ban 来保护 ssh 服务安全。Discourse 每周会自动创建一个备份保存在本机的 /var/discourse/shared/standalone/backups 目录下,可设置 rsync 将它们备份到本地的服务器上。如果有可用的 Amazon S3 服务,还可在后台配置 S3 服务的相关信息,Discourse 会在完成备份后自动将备份上传至对应的 S3 实例。 现在,你拥有了一个具备全站 CDN 加速能力的 Discourse 论坛,得益于全站 CDN 和全链路 SSL,论坛可以在全球任何位置被安全的访问。从你的域名访问论坛,并根据需要填写的信息即可初始化论坛并创建管理员账号,通过邮件中的链接确认注册后即可开启论坛服务。 结语 以上是我们为 Nebula Graph 部署 Discourse 论坛服务的一点小小的心得,本文中如有错误或疏漏还请多指教。最后,欢迎大家前往 discuss.nebula-graph.io 参与图数据库及开源的讨论~ 作者有话说:Hi,我是 George,是 Nebula Graph 的实施工程师,在运维领域有一些心得体会,希望能为图数据库领域带来一些自己的贡献。希望本文对你有所帮助,如果有错误或不足也请与我交流,不甚感激! 以上为 Nebula Graph 部署 Discourse 论坛服务的一点小小的心得,本文中如有错误或疏漏欢迎前往 Nebula Graph 的 GitHub:https://github.com/vesoft-inc/nebula 提建议 ;加入 Nebula Graph 交流群,请联系 Nebula Graph 官方小助手微信号:NebulaGraphbot
本文主要讨论图数据库背后的设计思路、原理还有一些适用的场景,以及在生产环境中使用图数据库的具体案例。 从社交网络谈起 下面这张图是一个社交网络场景,每个用户可以发微博、分享微博或评论他人的微博。这些都是最基本的增删改查,也是大多数研发人员对数据库做的常见操作。而在研发人员的日常工作中除了要把用户的基本信息录入数据库外,还需找到与该用户相关联的信息,方便去对单个的用户进行下一步的分析,比如说:我们发现张三的账户里有很多关于 AI 和音乐的内容,那么我们可以据此推测出他可能是一名程序员,从而推送他可能感兴趣的内容。 这些数据分析每时每刻都会发生,但有时候,一个简单的数据工作流在实现的时候可能会变得相当复杂,此外数据库性能也会随着数据量的增加而锐减,比如说获取某管理者下属三级汇报关系的员工,这种统计查询在现在的数据分析中是一种常见的操作,而这种操作往往会因为数据库选型导致性能产生巨大差异。 传统数据库的解决思路 传统数据库的概念模型及查询的代码 传统解决上述问题最简单的方法就是建立一个关系模型,我们可以把每个员工的信息录入表中,存在诸如 MySQL 之类的关系数据库,下图是最基本的关系模型: 但是基于上述的关系模型,要实现我们的需求,就不可避免地涉及到很多关系数据库 JOIN 操作,同时实现出来的查询语句也会变得相当长(有时达到上百行): (SELECT T.directReportees AS directReportees, sum(T.count) AS count FROM ( SELECT manager.pid AS directReportees, 0 AS count FROM person_reportee manager WHERE manager.pid = (SELECT id FROM person WHERE name = "fName lName") UNION SELECT manager.pid AS directReportees, count(manager.directly_manages) AS count FROM person_reportee manager WHERE manager.pid = (SELECT id FROM person WHERE name = "fName lName") GROUP BY directReportees UNION SELECT manager.pid AS directReportees, count(reportee.directly_manages) AS count FROM person_reportee manager JOIN person_reportee reportee ON manager.directly_manages = reportee.pid WHERE manager.pid = (SELECT id FROM person WHERE name = "fName lName") GROUP BY directReportees UNION SELECT manager.pid AS directReportees, count(L2Reportees.directly_manages) AS count FROM person_reportee manager JOIN person_reportee L1Reportees ON manager.directly_manages = L1Reportees.pid JOIN person_reportee L2Reportees ON L1Reportees.directly_manages = L2Reportees.pid WHERE manager.pid = (SELECT id FROM person WHERE name = "fName lName") GROUP BY directReportees ) AS T GROUP BY directReportees) UNION (SELECT T.directReportees AS directReportees, sum(T.count) AS count FROM ( SELECT manager.directly_manages AS directReportees, 0 AS count FROM person_reportee manager WHERE manager.pid = (SELECT id FROM person WHERE name = "fName lName") UNION SELECT reportee.pid AS directReportees, count(reportee.directly_manages) AS count FROM person_reportee manager JOIN person_reportee reportee ON manager.directly_manages = reportee.pid WHERE manager.pid = (SELECT id FROM person WHERE name = "fName lName") GROUP BY directReportees UNION SELECT depth1Reportees.pid AS directReportees, count(depth2Reportees.directly_manages) AS count FROM person_reportee manager JOIN person_reportee L1Reportees ON manager.directly_manages = L1Reportees.pid JOIN person_reportee L2Reportees ON L1Reportees.directly_manages = L2Reportees.pid WHERE manager.pid = (SELECT id FROM person WHERE name = "fName lName") GROUP BY directReportees ) AS T GROUP BY directReportees) UNION (SELECT T.directReportees AS directReportees, sum(T.count) AS count FROM( SELECT reportee.directly_manages AS directReportees, 0 AS count FROM person_reportee manager JOIN person_reportee reportee ON manager.directly_manages = reportee.pid WHERE manager.pid = (SELECT id FROM person WHERE name = "fName lName") GROUP BY directReportees UNION SELECT L2Reportees.pid AS directReportees, count(L2Reportees.directly_manages) AS count FROM person_reportee manager JOIN person_reportee L1Reportees ON manager.directly_manages = L1Reportees.pid JOIN person_reportee L2Reportees ON L1Reportees.directly_manages = L2Reportees.pid WHERE manager.pid = (SELECT id FROM person WHERE name = "fName lName") GROUP BY directReportees ) AS T GROUP BY directReportees) UNION (SELECT L2Reportees.directly_manages AS directReportees, 0 AS count FROM person_reportee manager JOIN person_reportee L1Reportees ON manager.directly_manages = L1Reportees.pid JOIN person_reportee L2Reportees ON L1Reportees.directly_manages = L2Reportees.pid WHERE manager.pid = (SELECT id FROM person WHERE name = "fName lName") ) 这种 glue 代码对维护人员和开发者来说就是一场灾难,没有人想写或者去调试这种代码,此外,这类代码往往伴随着严重的性能问题,这个在之后会详细讨论。 传统关系数据库的性能问题 性能问题的本质在于数据分析面临的数据量,假如只查询几十个节点或者更少的内容,这种操作是完全不需要考虑数据库性能优化的,但当节点数据从几百个变成几百万个甚至几千万个后,数据库性能就成为了整个产品设计的过程中最需考虑的因素之一。 随着节点的增多,用户跟用户间的关系,用户和产品间的关系,或者产品和产品间的关系都会呈指数增长。 以下是一些公开的数据,可以反映数据、数据和数据间关系的一些实际情况: 推特:用户量为 5 亿,用户之间存在关注、点赞关系 亚马逊:用户量 1.2 亿,用户和产品间存在购买关系 AT&T(美国三大运营商之一): 1 亿个号码,电话号码间可建立通话关系 如下表所示,开源的图数据集往往有着上千万个节点和上亿的边的数据: Data set name nodes edges YahooWeb 1.4 Billion 6 Billion Symantec Machine-File Graph 1 Billion 37 Billion Twitter 104 Million 3.7 Billion Phone call network 30 Million 260 Million 在数据量这么大的场景中,使用传统 SQL 会产生很大的性能问题,原因主要有两个: 大量 JOIN 操作带来的开销:之前的查询语句使用了大量的 JOIN 操作来找到需要的结果。而大量的 JOIN 操作在数据量很大时会有巨大的性能损失,因为数据本身是被存放在指定的地方,查询本身只需要用到部分数据,但是 JOIN 操作本身会遍历整个数据库,这样就会导致查询效率低到让人无法接受。 反向查询带来的开销:查询单个经理的下属不需要多少开销,但是如果我们要去反向查询一个员工的老板,使用表结构,开销就会变得非常大。表结构设计得不合理,会对后续的分析、推荐系统产生性能上的影响。比如,当关系从_老板 -> 员工 _变成 _用户 -> 产品_,如果不支持反向查询,推荐系统的实时性就会大打折扣,进而带来经济损失。 下表列出的是一个非官方的性能测试(社交网络测试集,一百万用户,每个大概有 50 个好友),体现了在关系数据库里,随着好友查询深度的增加而产生的性能变化: levels RDBMS execution time(s) 2 0.016 3 30.267 4 1543.595 传统数据库的常规优化策略 策略一:索引 索引:SQL 引擎通过索引来找到对应的数据。 常见的索引包括 B- 树索引和哈希索引,建立表的索引是比较常规的优化 SQL 性能的操作。B- 树索引简单地来说就是给每个人一个可排序的独立 ID,B- 树本身是一个平衡多叉搜索树,这个树会将每个元素按照索引 ID 进行排序,从而支持范围查找,范围查找的复杂度是 O(logN) ,其中 N 是索引的文件数目。 但是索引并不能解决所有的问题,如果文件更新频繁或者有很多重复的元素,就会导致很大的空间损耗,此外索引的 IO 消耗也值得考虑,索引 IO 尤其是在机械硬盘上的 IO 读写性能上来说非常不理想,常规的 B- 树索引消耗四次 IO 随机读,当 JOIN 操作变得越来越多时,硬盘查找更可能发生上百次。 策略二:缓存 缓存:缓存主要是为了解决有具有空间或者时间局域性数据的频繁读取带来的性能优化问题。一个比较常见的使用缓存的架构是 lookaside cache architecture。下图是之前 Facebook 用 Memcached + MySQL 的实例(现已被 Facebook 自研的图数据库 TAO 替代): 在架构中,设计者假设用户创造的内容比用户读取的内容要少得多,Memcached 可以简单地理解成一个分布式的支持增删改查的哈希表,支持上亿量级的用户请求。基本的使用流程是当客户端需读数据时,先查看一下缓存,然后再去查询 SQL 数据库。而当用户需要写入数据时,客户端先删除缓存中的 key,让数据过期,再去更新数据库。但是这种架构有几个问题: 首先,键值缓存对于图结构数据并不是一个好的操作语句,每次查询一条边,需要从缓存里把节点对应的边全部拿出来;此外,当更新一条边,原来的所有依赖边要被删除,继而需要重新加载所有对应边的数据,这些都是并发的性能瓶颈,毕竟实际场景中一个点往往伴随着几千条边,这种操作带来的时间、内存消耗问题不可忽视。 其次,数据更新到数据读取有一个过程,在上面架构中这个过程需要主从数据库跨域通信。原始模型使用了一个外部标识来记录过期的键值对,并且异步地把这些读取的请求从只读的从节点传递到主节点,这个需要跨域通信,延迟相比直接从本地读大了很多。(类似从之前需要走几百米的距离而现在需要走从北京到深圳的距离) 使用图结构建模 上述关系型数据库建模失败的主要原因在于数据间缺乏内在的关联性,针对这类问题,更好的建模方式是使用图结构。假如数据本身就是表格的结构,关系数据库就可以解决问题,但如果你要展示的是数据与数据间的关系,关系数据库反而不能解决问题了,这主要是在查询的过程中不可避免的大量 JOIN 操作导致的,而每次 JOIN 操作却只用到部分数据,既然反复 JOIN 操作本身会导致大量的性能损失,如何建模才能更好的解决问题呢?答案在点和点之间的关系上。 点、关联关系和图数据模型 在我们之前的讨论中,传统数据库虽然运用 JOIN 操作把不同的表链接了起来,从而隐式地表达了数据之间的关系,但是当我们要通过 A 管理 B,B 管理 A 的方式查询结果时,表结构并不能直接告诉我们结果。如果我们想在做查询前就知道对应的查询结果,我们必须先定义节点和关系。 节点和关系先定义是图数据库和别的数据库的核心区别。打个比方,我们可以把经理、员工表示成不同的节点,并用一条边来代表他们之前存在的管理关系,或者把用户和商品看作节点,用购买关系建模等等。而当我们需要新的节点和关系时,只需进行几次更新就好,而不用去改变表的结构或者去迁移数据。 根据节点和关联关系,之前的数据可以根据下图所示建模: 通过图数据库 Nebula Graph 原生 nGQL 图查询语言进行建模,参考如下操作: -- Insert People INSERT VERTEX person(ID, name) VALUES 1:(2020031601, ‘Jeff’); INSERT VERTEX person(ID, name) VALUES 2:(2020031602, ‘A’); INSERT VERTEX person(ID, name) VALUES 3:(2020031603, ‘B’); INSERT VERTEX person(ID, name) VALUES 4:(2020031604, ‘C’); -- Insert edge INSERT EDGE manage (level_s, level_end) VALUES 1 -> 2: ('0', '1') INSERT EDGE manage (level_s, level_end) VALUES 1 -> 3: ('0', '1') INSERT EDGE manage (level_s, level_end) VALUES 1 -> 4: ('0', '1') 而之前超长的 query 语句也可以通过 Cypher / nGQL 缩减成短短的 3、4 行代码。 下面为 nGQL 语句 GO FROM 1 OVER manage YIELD manage.level_s as start_level, manage._dst AS personid | GO FROM $personid OVER manage where manage.level_s < start_level + 3 YIELD SUM($$.person.id) AS TOTAL, $$.person.name AS list 下面为 Cypher 版本 MATCH (boss)-[:MANAGES*0..3]->(sub), (sub)-[:MANAGES*1..3]->(personid) WHERE boss.name = “Jeff” RETURN sub.name AS list, count(personid) AS Total 从近百行代码变成 3、4 行代码可以明显地看出图数据库在数据表达能力上的优势。 图数据库性能优化 图数据库本身对高度连接、结构性不强的数据做了专门优化。不同的图数据库根据不同的场景也做了针对性优化,笔者在这里简单介绍以下几种图数据库,BTW,这些图数据库都支持原生图建模。 Neo4j Neo4j 是最知名的一种图数据库,在业界有微软、ebay 在用 Neo4j 来解决部分业务场景,Neo4j 的性能优化有两点,一个是原生图数据处理上的优化,一个是运用了 LRU-K 缓存来缓存数据。 原生图数据处理优化 我们说一个图数据库支持原生图数据处理就代表这个数据库有能力去支持 index-free adjacency。 index-free adjancency 就是每个节点会保留连接节点的引用,从而这个节点本身就是连接节点的一个索引,这种操作的性能比使用全局索引好很多,同时假如我们根据图来进行查询,这种查询是与整个图的大小无关的,只与查询节点关联边的数目有关,如果用 B 树索引进行查询的复杂度是 O(logN),使用这种结构查询的复杂度就是 O(1)。当我们要查询多层数据时,查询所需要的时间也不会随着数据集的变大而呈现指数增长,反而会是一个比较稳定的常数,毕竟每次查询只会根据对应的节点找到连接的边而不会去遍历所有的节点。 主存缓存优化 在 2.2 版本的 Neo4j 中使用了 LRU-K 缓存,这种缓存简而言之就是将使用频率最低的页面从缓存中弹出,青睐使用频率更高的页面,这种设计保证在统计意义上的缓存资源使用最优化。 JanusGraph JanusGraph 本身并没有关注于去实现存储和分析,而是实现了图数据库引擎与多种索引和存储引擎的接口,利用这些接口来实现数据和存储和索引。JanusGraph 主要目的是在原来框架的基础上支持图数据的建模同时优化图数据序列化、图数据建模、图数据执行相关的细节。JanusGraph 提供了模块化的数据持久化、数据索引和客户端的接口,从而更方便地将图数据模型运用到实际开发中。 此外,JanusGraph 支持用 Cassandra、HBase、BerkelyDB 作为存储引擎,支持使用 ElasticSearch、Solr 还有 Lucene 进行数据索引。在应用方面,可以用两种方式与 JanusGraph 进行交互: 将 JanusGraph 变成应用的一部分进行查询、缓存,并且这些数据交互都是在同一台 JVM 上执行,但数据的来源可能在本地或者在别的地方。 将 JanusGraph 作为一个服务,让客户端与服务端分离,同时客户端提交 Gremlin 查询语句到服务器上执行对应的数据处理操作。 Nebula Graph 下面简单地介绍了一下 Nebula Graph 的系统设计。 使用 KV 对来进行图数据处理 Nebula Graph 使用了 vertexID + TagID 作为键在不同的 partition 间存储 in-key 和 out-key 相关的数据,这种操作可以确保在大规模集群上的高可用,使用分布式的 partition 和 sharding 也增加了 Nebula Graph 的吞吐量和容错的能力。 Shared-noting 分布式存储层 Storage Service 采用 shared-nothing 的分布式架构设计,每个存储节点都有多个本地 KV 存储实例作为物理存储。Nebula 采用多数派协议 Raft 来保证这些 KV 存储之间的一致性(由于 Raft 比 Paxo 更简洁,我们选用了 Raft)。在 KVStore 之上是图语义层,用于将图操作转换为下层 KV 操作。图数据(点和边)通过 Hash 的方式存储在不同 partition 中。这里用的 Hash 函数实现很直接,即 vertex_id 取余 partition 数。在 Nebula Graph 中,partition 表示一个虚拟的数据集,这些 partition 分布在所有的存储节点,分布信息存储在 Meta Service 中(因此所有的存储节点和计算节点都能获取到这个分布信息)。 无状态计算层 每个计算节点都运行着一个无状态的查询计算引擎,而节点彼此间无任何通信关系。计算节点仅从 Meta Service 读取 meta 信息,以及和 Storage Service 进行交互。这样设计使得计算层集群更容易使用 K8s 管理或部署在云上。计算层的负载均衡有两种形式,最常见的方式是在计算层上加一个负载均衡(balance),第二种方法是将计算层所有节点的 IP 地址配置在客户端中,这样客户端可以随机选取计算节点进行连接。每个查询计算引擎都能接收客户端的请求,解析查询语句,生成抽象语法树(AST)并将 AST 传递给执行计划器和优化器,最后再交由执行器执行。 图数据库是当今的趋势 在当今,图数据库收到了更多分析师和咨询公司的关注 Graph analysis is possibly the single most effective competitive differentiator for organizations pursuing data-driven operations and decisions after the design of data capture. --------------Gartner “Graph analysis is the true killer app for Big Data.” --------------------Forrester 同时图数据库在 DB-Ranking 上的排名也呈现出上升最快的趋势,可见需求之迫切: 图数据库实践:不仅仅是社交网络 Netflix 云数据库的工程实践 Netflix 采用了JanusGraph + Cassandra + ElasticSearch 作为自身的图数据库架构,他们运用这种架构来做数字资产管理。节点表示数字产品比如电影、纪录片等,同时这些产品之间的关系就是节点间的边。当前的 Netflix 有大概 2 亿的节点,70 多种数字产品,每分钟都有上百条的 query 和数据更新。此外,Netflix 也把图数据库运用在了授权、分布式追踪、可视化工作流上。比如可视化 Git 的 commit,jenkins 部署这些工作。 Adobe 的技术迭代 一般而言,新技术往往在开始的时候大都不被大公司所青睐,图数据库并没有例外,大公司本身有很多的遗留项目,而这些项目本身的用户体量和使用需求又让这些公司不敢冒着风险来使用新技术去改变这些处于稳定的产品。Adobe 在这里做了一个迭代新技术的例子,用 Neo4j 图数据库替换了旧的 NoSQL Cassandra 数据库。 这个被大改的系统名字叫 Behance,是 Adobe 在 15 年发布的一个内容社交平台,有大概 1 千万的用户,在这里人们可以分享自己的创作给百万人看。 这样一个巨大的遗留系统本来是通过 Cassandra 和 MongoDB 搭建的,基于历史遗留问题,系统有不少的性能瓶颈不得不解决。MongoDB 和 Cassandra 的读取性能慢主要因为原先的系统设计采用了 fan-out 的设计模式——受关注多的用户发表的内容会单独分发给每个读者,这种设计模式也导致了网络架构的大延迟,此外 Cassandra 本身的运维也需要不小的技术团队,这也是一个很大的问题。 在这里为了搭建一个灵活、高效、稳定的系统来提供消息 feeding 并最小化数据存储的规模,Adobe 决定迁移原本的 Cassandra 数据库到 Neo4j 图数据库。在 Neo4j 图数据库中采用一种所谓的 Tiered relationships 来表示用户之间的关系,这个边的关系可以去定义不同的访问状态,比如:仅部分用户可见,仅关注者可见这些基本操作。数据模型如图所示 使用这种数据模型并使用 Leader-follower 架构来优化读写,这个平台获得了巨大的性能提升: 运维需求的时长在使用了 Neo4j 以后下降了 300%。 存储需求降低了 1000 倍, Neo4j 仅需 50G 存储数据, 而 Cassandra 需要 50TB。 仅仅需要 3 个服务实例就可以支持整个服务器的流畅运行,之前则需要 48 个。 图数据库本身就提供了更高的可扩展性。 结论 在当今的大数据时代,采用图数据库可以用小成本在原有架构上获得巨大的性能提升。图数据库不仅仅可以在 5G、AI、物联网领域发挥巨大的推动作用,同时也可以用来重构原本的遗留系统。虽然不同的图数据库可能有着截然不同的底层实现,但这些都完全支持用图的方式来构建数据模型从而让不同的组件之间相互联系,从我们之前的讨论来看,这一种数据模型层次的改变会极大地简化很多日常数据系统中所面临的问题,增大系统的吞吐量并且降低运维的需求。 图数据库的介绍就到此为止了,如果你对图数据库 Nebula Graph 有任何想法或其他要求,欢迎去 GitHub:https://github.com/vesoft-inc/nebula issue 区向我们提 issue 或者前往官方论坛:https://discuss.nebula-graph.io/ 的 Feedback 分类下提建议 ;加入 Nebula Graph 交流群,请联系 Nebula Graph 官方小助手微信号:NebulaGraphbot Reference [1] An Overview Of Neo4j And The Property Graph Model Berkeley, CS294, Nov 2015 https://people.eecs.berkeley.edu/~istoica/classes/cs294/15/notes/21-neo4j.pdf [2] several original data sources from talk made by Duen Horng (Polo) Chau ( Geogia tech ) www.selectscience.net www.phonedog.com、www.mediabistro.com www.practicalecommerce.com/ [3] Graphs / Networks Basics, how to build & store graphs, laws, etc. Centrality, and algorithms you should know Duen Horng (Polo) Chau(Georgia tech) [4] Graph databases, 2nd Edition: New Oppotunities for Connected Data [5] R. Nishtala, H. Fugal, S. Grimm, M. Kwiatkowski, H. Lee, H. C.Li, R. McElroy, M. Paleczny, D. Peek, P. Saab, D. Stafford, T. Tung, and V. Venkataramani. Scaling Memcache at Facebook.In Proceedings of the 10th USENIX conference on NetworkedSystems Design and Implementation, NSDI, 2013. [6] Nathan Bronson, Zach Amsden, George Cabrera, Prasad Chakka, Peter Dimov Hui Ding, Jack Ferris, Anthony Giardullo, Sachin Kulkarni, Harry Li, Mark Marchukov Dmitri Petrov, Lovro Puzar, Yee Jiun Song, Venkat Venkataramani TAO: Facebook's Distributed Data Store for the Social Graph USENIX 2013 [7] Janus Graph Architecture https://docs.janusgraph.org/getting-started/architecture/ [8] Nebula Graph Architecture — A Bird's View https://nebula-graph.io/en/posts/nebula-graph-architecture-overview/ [9] database engine trending https://db-engines.com/en/ranking_categories [10] Netflix Content Data Management talk https://www.slideshare.net/RoopaTangirala/polyglot-persistence-netflix-cde-meetup-90955706#86 [11] Harnessing the Power of Neo4j for Overhauling Legacy Systems at Adobe https://neo4j.com/graphconnect-2018/session/overhauling-legacy-systems-adobe 推荐阅读 聊聊图数据库和图数据库的小知识 Nebula Graph 技术总监陈恒:图数据库怎么和深度学习框架进行结合? 聊聊图数据库和图数据库的小知识 Vol.02 图数据库爱好者的聚会在谈论什么? 作者有话说:Hi,我是 Johhan。目前在 Nebula Graph 实习,研究和实现大型图数据库查询引擎和存储引擎组件。作为一个图数据库及开源爱好者,我在博客分享有关数据库、分布式系统和 AI 公开可用学习资源。
导读 身处在现在这个大数据时代,我们处理的数据量需以 TB、PB, 甚至 EB 来计算,怎么处理庞大的数据集是从事数据库领域人员的共同问题。解决这个问题的核心在于,数据库中存储的数据是否都是有效的、有用的数据,因此如何提高数据中有效数据的利用率、将无效的过期数据清洗掉,便成了数据库领域的一个热点话题。在本文中我们将着重讲述如何在数据库中处理过期数据这一问题。 在数据库中清洗过期数据的方式多种多样,比如存储过程、事件等等。在这里笔者举个例子来简要说明 DBA 经常使用的存储过程 + 事件来清理过期数据的过程。 存储过程 + 事件清洗数据 存储过程(procedure) 存储过程是由一条或多条 SQL 语句组成的集合,当对数据库进行一系列的读写操作时,存储过程可将这些复杂的操作封装成一个代码块以便重复使用,大大减少了数据库开发人员的工作量。通常存储过程编译一次,可以执行多次,因此也大大的提高了效率。 存储过程有以下优点: 简化操作,将重复性很高的一些操作,封装到一个存储过程中,简化了对这些 SQL 的调用 批量处理,SQL + 循环,减少流量,也就是“跑批” 统一接口,确保数据的安全 一次编译多次执行,提高了效率。 以 MySQL 为例,假如要删除数据的表结构如下: mysql> SHOW CREATE TABLE person; +--------+---------------------------------------------------------------------------------------------------------------------------------+ | Table | Create Table | +--------+---------------------------------------------------------------------------------------------------------------------------------+ | person | CREATE TABLE `person` ( `age` int(11) DEFAULT NULL, `inserttime` datetime DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8 | +--------+---------------------------------------------------------------------------------------------------------------------------------+ 1 row in set (0.00 sec) 创建一个名为 person 的表,其中 inserttime 字段为 datetime 类型,我们用 _inserttime_ 字段存储数据的生成时间。 创建一个删除指定表数据的存储过程,如下: mysql> delimiter // mysql> CREATE PROCEDURE del_data(IN `date_inter` int) -> BEGIN -> DELETE FROM person WHERE inserttime < date_sub(curdate(), interval date_inter day); -> END // mysql> delimiter ; 创建一个名为 _del_data_ 的存储过程,参数 _date_inter_ 指定要删除的数据距离当前时间的天数。当表 person 的 inserttime 字段值(datetime 类型)加上参数 date_inter 天小于当前时间,则认为数据过期,将过期的数据删除。 事件(event) 事件是在相应的时刻调用的过程式数据库对象。一个事件可调用一次,也可周期性的启动,它由一个特定的线程来管理,也就是所谓的“事件调度器”。事件和触发器类似,都是在某些事情发生的时候启动。当数据库上启动一条语句的时候,触发器就启动了,而事件是根据调度事件来启动的。由于它们彼此相似,所以事件也称为临时性触发器。事件调度器可以精确到每秒钟执行一个任务。 如下创建一个事件,周期性的在某个时刻调用存储过程,来进行清理数据。 mysql> CREATE EVENT del_event -> ON SCHEDULE -> EVERY 1 DAY -> STARTS '2020-03-20 12:00:00' -> ON COMPLETION PRESERVE ENABLE -> DO CALL del_data(1); 创建一个名为 del_event 的事件,该事件从 2020-03-20 开始,每天的 12:00:00 执行存储过程 del_data(1)。 然后执行: mysql> SET global event_scheduler = 1; 打开事件。这样事件 del_event 就会在指定的时间自动在后台执行。通过上述的存储过程 del_data 和事件 del_event,来达到定时自动删除过期数据的目的。 TTL(Time To Live) 清洗数据 通过上述存储过程和事件的组合可以定时清理数据库中的过期数据。图数据库 Nebula Graph 提供了更加简单高效的方式--使用 TTL 的方式来自动清洗过期数据。 使用 TTL 方式自动清洗过期数据的好处如下: 简单方便 通过数据库系统内部逻辑进行处理,安全可靠 数据库会根据自身的状态自动判断是否需要处理,如果需要处理,将在后台自动进行处理,无需人工干预。 TTL 简介 TTL,全称 Time To Live,用来指定数据的生命周期,数据时效到期后这条数据会被自动删除。在图数据库 Nebula Graph 中,我们实现 TTL 功能,用户设置好数据的存活时间后,在预定时间内系统会自动从数据库中删除过期的点或者边。 在 TTL 中,过期数据会在下次 compaction 时被删除,在下次 compaction 之前,query 会过滤掉过期的点和边。 图数据库 Nebula Graph 的 TTL 功能需 ttl_col 和 ttl_duration 两个字段一起使用,到期阈值是 ttl_col 指定的属性对应的值加上 ttl_duration 设置的秒数。其中 ttl_col 指定的字段的类型应为 integer 或 timestamp,ttl_duration 的计量单位为秒。 TTL 读过滤 针对 tag / edge,Nebula Graph 在 TTL 中将读数据过滤逻辑下推到 storage 层进行处理。在 storage 层,首先获取该 tag / edge 的 TTL 信息,然后依次遍历每个顶点或边,取出 ttl_col 字段值,根据 ttl_duration 的值加上 ttl_col 列字段值,跟当前时间的时间戳进行比较,判断数据是否过期,过期的数据将被忽略。 TTL compaction RocksDB 文件组织方式 图数据库 Nebula Graph 底层存储使用的是 RocksDB,RocksDB 在磁盘上的文件是分为多层的,默认是 7 层,如下图所示: SST文件在磁盘上的组织方式 Level 0 层包含的文件,是由内存中的 Memtable flush 到磁盘,生成的 SST 文件,单个文件内部按 key 有序排列,文件之间无序。其它 Level 上的多个文件之间都是按照 key 有序排列,并且文件内也有序,如下图所示: 非Level 0 层的文件数据划分 RocksDB compaction 原理 RocksDB 是基于 LSM 实现,但 LSM 并不是一个具体的数据结构,而是一种数据结构的概念和设计思想,具体细节参考LSM论文。而 LSM 中最重要部分就是 compaction,由于数据文件采用 Append only 方式写入,而对于过期的数据,重复的、已删除的数据,需要通过 compaction 进行逐步的清理。 RocksDB compaction 逻辑 我们采用的 RocksDB 的 compaction 策略为 Level compaction。当数据写到RocksDB 时,会先将数据写入到一个 Memtable 中,当一个 Memtable 写满之后,就会变成 Immutable 的 Memtable。RocksDB 在后台通过一个 flush 线程将这个 Memtable flush 到磁盘,生成一个 Sorted String Table (SST) 文件,放在 Level 0 层。当 Level 0 层的 SST 文件个数超过阈值之后,就会与Level 1 层进行 compaction。通常必须将 Level 0 的所有文件 compaction 到 Level 1 中,因为 Level 0 的文件的 key 是有交叠的。 Level 0 与 Level 1 的 compaction 如下: Level 0 与 Level 1 的 compaction 其他 Level 的 compaction 规则一样,以 Level 1与 Level 2 的 compaction 为例进行说明,如下所示: Level 1 与 Level 2 的 compaction 当 Level 0 compaction 完成后,Level 1 的文件总大小或者文件数量可能会超过阈值,触发 Level 1 与 Level 2 的 compaction。从 Level 1 层至少选择一个文件 compaction 到 Level 2 的 key 重叠的文件中。compaction 后可能会触发下一个 Level 的 compaction,以此类推。 如果没有 compaction,写入是非常快的,但这样会造成读性能降低,同样也会造成很严重的空间放大问题。为了平衡写入、读取、空间三者的关系,RocksDB 会在后台执行 compaction,将不同 Level 的 SST 进行合并。 TTL compaction 原理 除了上述默认的compaction操作外(sst文件合并),RocksDB 还提供了CompactionFilter 功能,可以让用户自定义个性化的compaction逻辑。Nebula Graph 使用了这个CompactionFilter来定制本文讨论的TTL功能。该功能是 RocksDB 在 compaction 过程中,每读取一条数据时,都会调用一个定制的Filter 函数。TTL compaction 的实现方法就是在 Filter 函数中实现 TTL 过期数据删除逻辑,具体如下: 首先获取 tag / edge 的 TTL 信息 然后遍历每个顶点或边数据,取出 ttl_col 列字段值 根据 ttl_duration 的值加上 ttl_col 列字段值,跟当前时间的时间戳进行比较,然后判断数据是否过期,过期的数据将被删除。 TTL 用法 在图数据库 Nebula Graph 中,edge 和 tag 实现逻辑一致,在这里仅以 tag 为例,来介绍 Nebula Graph 中 TTL 用法。 创建 TTL 属性 Nebula Graph 中使用 TTL 属性分为两种方式: create tag 时指定 ttl_duration 来表示数据的持续时间,单位为秒。ttl_col 指定哪一列作为 TTL 列。语法如下: nebula> CREATE TAG t (id int, ts timestamp ) ttl_duration=3600, ttl_col="ts"; 当某一条记录的 ttl_col 列字段值加上 ttl_duration 的值小于当前时间的时间戳,则该条记录过期,否则该记录不过期。 ttl_duration 的值为非正数时,则点的此 tag 属性不会过期 ttl_col 只能指定类型为 int 或者 timestamp 的列名。 或者 create tag 时没有指定 TTL 属性,后续想使用 TTL 功能,可以使用 alter tag 来设置 TTL 属性。语法如下: nebula> CREATE TAG t (id int, ts timestamp ); nebula> ALTER TAG t ttl_duration=3600, ttl_col="ts"; 查看 TTL 属性 创建完 tag 可以使用以下语句查看 tag 的 TTL 属性: nebula> SHOW CREATE TAG t; ===================================== | Tag | Create Tag | ===================================== | t | CREATE TAG t ( id int, ts timestamp ) ttl_duration = 3600, ttl_col = id | ------------------------------------- 修改 TTL 属性 可以使用 alter tag 语句修改 TTL 的属性: nebula> ALTER TAG t ttl_duration=100, ttl_col="id"; 删除 TTL 属性 当不想使用 TTL 属性时,可以删除 TTL 属性: 可以设置 ttl_col 字段为空,或删除配置的 ttl_col 字段,或者设置 ttl_duration 为 0 或者 -1。 nebula> ALTER TAG t1 ttl_col = ""; -- drop ttl attribute 删除配置的 ttl_col 字段: nebula> ALTER TAG t1 DROP (a); -- drop ttl_col 设置 ttl_duration 为 0 或者 -1: nebula> ALTER TAG t1 ttl_duration = 0; -- keep the ttl but the data never expires 举例 下面的例子说明,当使用 TTL 功能,并且数据过期后,查询该 tag 的数据时,过期的数据被忽略。 nebula> CREATE TAG t(id int) ttl_duration=100, ttl_col="id"; nebula> INSERT VERTEX t(id) values 102:(1584441231); nebula> FETCH prop on t 102; Execution succeeded (Time spent: 5.945/7.492 ms) 注意: 当某一列作为 ttl_col 值的时候,不允许 change 该列。 必须先移除 TTL 属性,再 change 该列。 对同一 tag,index 和 TTL 功能不能同时使用。即使 index 和 TTL 创建于不同列,也不可以同时使用。 edge 同 tag 的逻辑一样,这里就不在详述了。 TTL 的介绍就到此为止了,如果你对图数据库 Nebula Graph 的 TTL 有改进想法或其他要求,欢迎去 GitHub:https://github.com/vesoft-inc/nebula issue 区向我们提 issue 或者前往官方论坛:https://discuss.nebula-graph.io/ 的 Feedback 分类下提建议 作者有话说:Hi,我是 panda sheep,是图数据库 Nebula Graph 研发工程师,对数据库领域非常感兴趣,也有自己的一点点心得,希望写的经验分享能给大家带来帮助,如有不当之处也希望能帮忙纠正,谢谢~
云栖号:https://yqh.aliyun.com第一手的上云资讯,不同行业精选的上云企业案例库,基于众多成功案例萃取而成的最佳实践,助力您上云决策! 前言 说起自动化,无论是在公司还是我们个人的项目中,都会用到或者编写一些工具来帮助我们去处理琐碎重复的工作,以节约时间提升效率,尤其是我们做前端开发会涉及诸如构建、部署、单元测试等这些开发工作流中重复的事项,本篇文章就是介绍如何利用 GitHub 提供的 Actions 来完成我们前端的发布自动化。 Github Actions 什么是 Actions 笔者个人理解为在某种条件下可被触发的任务,利用一个个任务(Action)就可组建成我们的工作流,想要更详细的介绍定义的同学可以移步 官方Action定义,有助获取更多的信息,这里就不搬运啦~ 使用 Actions 的好处 前端自动化部署方案有多种,那么 GitHub 推出的 Actions 有什么魅力呢?在笔者看来,Action 在前端自动化发布有下面 3 点亮点: 免费,Action 可与 GitHub 中的 Repo 进行绑定(下图所示,具体操作见下文),开箱即用:这就意味着我们不需要提供跑任务的机器,也不用管怎么把任务流对接起来,只要简单地熟悉规则,就能将项目 run 起来。而我们大部分觉得某个工具麻烦,是因为使用步骤繁琐,若要实现功能 A,还需做 B/C/D 操作才行,这时候我们要么放弃要么转向操作更简单的工具,毕竟省时省事才是开发第一要务~ 任务插件化,持续丰富的插件开源市场:得益于 Github 定义了 Actions 规范,让我们使用的 Actions 时都是按某种已知规则开发,这使得 Actions 更易于装配复用,很多优秀的开发者在制作完成工作流后,将自己开发的 Actions 放到 GitHub 的 Actions 集市上去,这样尚未完成自己常规工作流的开发,不需要额外开发这些已有重复逻辑直接使用现成的他人 Actions 即可。在笔者的实践过程中,前端的构建部署工作流,就是用的各类现有的 Actions 组合实现的。 和 GitHub 集成好,可避免因为使用 Travis 等第三方工具引起额外的心智负担,在 GitHub 上可直接查看 CI/CD 的情况。 当然 Actions 还有许多其他好处,还待各位亲自尝试,至少使用过 Actions 的人都说好 Actions 在业务场景下的实践 分析来源 因为 Nebula Graph 是一家做开源的分布式图数据库(Nebula Graph:https://github.com/vesoft-inc/nebula),项目均依托 GitHub 来管理,所以很自然地使用由 GitHub 免费提供的 Actions 来完成我们日常的持续集成等工作流,在前端业务上自然也不例外。 举个例子,笔者开发了一个专门介绍图数据库 Nebula Graph 的官网,除了根据需求修改站点主题模板的开发人员,网站日常维护主要由运营同学来管理内置的博客内容,而内容更新这个动作比较高频,基本上每一天运营同学就会发布一篇技术性相关的博客文章。为了让内容更新这个动作完全不依赖于开发同学,站点实现实时部署更新,这就要求将内容发布过程自动化,这也是我们前端日常使用 Github Actions 的主要场景之一。 Actions 快速开始 要使用 Actions 是件容易的事情,前提只要你的 Repo 源同 GitHub 关联,关联之后根据以下操作就能实现你的前端部署自动化。 在 Repo 的根目录中,创建一个名为 .github/workflows/ 的文件夹,用来存放工作流的描述文件,一个项目可以有多个工作流,这里我们的工作流为前端的发布。 然后,在创建好的 .github/workflows/ 目录中,以 .yml 为扩展名创建对应该工作流的描述文件,命名可自定义,例如:publish.yml。 接下来,参考 GitHub 提供的工作流描述规则进行任务 Actions 配置,详细可以看官方文档,当然觉得文档冗长可在网上随便搜个简单的例子直接复制试试,亲测下很快就能摸清 Actions 配置套路。 下面是我们自己实现官网自动发布工作流的配置摘要,加了少量注释帮助大家理解: name: on: push: branches: - master jobs: build-and-deploy: runs-on: ubuntu-latest steps: # 此处每一个name对应着一个Action,具体执行逻辑已被提供者进行封装,暴露给用户的只是需要用户需要关心和配置的 # 从master上获取最新代码 - name: Checkout Github Action uses: actions/checkout@master # 我们的站点使用Hugo框架进行构建,此处是下载相关环境 - name: Setup Hugo uses: peaceiris/actions-hugo@v2 with: hugo-version: '0.65.3' # 为了将资源部署到云服务器,此处下载一个ssh传资料的工具 - name: Setup sshpass run: sudo apt-get install sshpass # 进行前端资源的构建 - name: Build run: hugo --minify -d nebula-website # 部署 - name: Deploy uses: garygrossgarten/github-action-scp@release with: local: nebula-website remote: /home/vesoft/nebula-website # 涉及偏安全隐私的信息,不要明文暴露在此文件中,因为repo很可能是公开的,会被所有人看见 # ${{ ... }} 会应用你在对应项目设置中,配置的对应serets的键值信息,从而保护私密信息不被看到 host: ${{ secrets.HOST }} username: vesoft password: ${{ secrets.PASSWORD }} concurrency: 20 最后,便是提交相应改动,将分支推到远端,只要符合工作流中我们预先定义好的触发规则,对应的操作即可被触发,比如,在笔者的实践中定义了官网仅限 master 代码变动。 完成以上步骤,就能使你的工作流 run 起来,更详细的介绍,可以看下 GitHub 提供的帮助文档,此处就不再赘述。 Actions 使用注意事项 私密信息保护 .yml 工作流配置文件中,不要出现私密信息,诸如:账号、密码、ip 等等,具体实操过程中你可将这类信息通通放到 Repo 的 secrets 设置中添加,并以 ${{ secrets.xxx }} 的变量访问形式在配置文件中使用。 寻找合适 Action 在配置我们的工作流中,基于我们的目的是为了快速高效地完成工作,因此不大可能亲自去开发每一个需要用到的 Action,一般操作是去现成的 Actions 市场 找寻已有的 Actions 直接使用。 对于一些处理敏感任务的 Actions,比如,上传服务器时若需将账号、密码传给此 Actions,使用前最好查看下这个 Actions 的具体实现,一来能预知其中是否存在的风险,二来也能满足好奇心了解相应的 Actions 规范和实现机制,帮助自己下次开发 Actions 做技术积累。 发挥想象力 根据实际的需要,我们的工作流搭配可能会有各类形形色色的需要,比如,笔者最开始使用 GitHub Actions 时,需要连接 VPN 才能访问开发服务器,刚开始没太理解如何连接怕麻烦弄不了,后面慢慢找到对应的 VPN 命令工具做实验并理解这个调用过程后,很快地实现了想要的效果。 这里想说的就是,只要需求合理,肯定不只你一个人会遇到,而此时就会有两种对应的解决办法,一是运气好地有现成的 Actions 等你使用,二是麻烦点自己用脚本来描述,总之要有想象力~ 考虑免费 runner 的性能 runner 就是执行配置工作流的环境,是由 GitHub 免费提供给用户使用,当然免费大概率意味着性能容量有限,对于一些大型项目的工作流来说,有时候免费的 runner 跑起来有些慢不满足需求,此时可考虑自己提供 runner 来集成,比如像我们的 Nebula 这样大的项目就自己提供了 runner 环境,这里不赘述,感兴趣的可查看 Self-hosted runners 官方指引。 Actions 实践小结 以上便是笔者在日常前端开发中使用 GitHub Action 的心得体会,Actions 还能完成更多不同类型的任务流程,比如持续集成,应该只有想不到没有做不到的道理。 通过项目下的一个个工作流,能从各个方面避免重复琐碎的工作,让我们更专注于实现逻辑本身,我想这是工程师最希望达到的状态。希望这里的简短介绍能给各位带来帮助,另外欢迎大家关注和使用我们的 Nebula开源图数据库,谢谢 作者有话说:Hi,我是 Jerry,是图数据 Nebula Graph 前端工程师,在前端平台工具开发及工程化方面有些小心得,希望写的经验分享能给大家带来帮助,如有不当之处也希望能帮忙纠正,谢谢~ 附录 Nebula Graph:一个开源的分布式图数据库 GitHub:https://github.com/vesoft-inc/nebula 官方论坛:https://discuss.nebula-graph.io 知乎:zhihu.com/org/nebulagraph/posts 微博:weibo.com/nebulagraph 云栖号在线课堂,每天都有产品技术专家分享立即加入圈子:https://c.tb.cn/F3.Z8gvnK与专家面对面,及时了解课程最新动态! 原文发布时间:2020-03-17本文作者:NebulaGraph本文来自:“阿里云云栖社区”,了解相关信息可以关注“阿里云云栖社区”
导读 索引是数据库系统中不可或缺的一个功能,数据库索引好比是书的目录,能加快数据库的查询速度,其实质是数据库管理系统中一个排序的数据结构。不同的数据库系统有不同的排序结构,目前常见的索引实现类型如 B-Tree index、B+-Tree index、B*-Tree index、Hash index、Bitmap index、Inverted index 等等,各种索引类型都有各自的排序算法。 虽然索引可以带来更高的查询性能,但是也存在一些缺点,例如: 创建索引和维护索引要耗费额外的时间,往往是随着数据量的增加而维护成本增大 索引需要占用物理空间 在对数据进行增删改的操作时需要耗费更多的时间,因为索引也要进行同步的维护 Nebula Graph 作为一个高性能的分布式图数据库,对于属性值的高性能查询,同样也实现了索引功能。本文将对 Nebula Graph的索引功能做一个详细介绍。 图数据库 Nebula Graph 术语 开始之前,这里罗列一些可能会使用到的图数据库和 Nebula Graph 专有术语: Tag:点的属性结构,一个 Vertex 可以附加多种 tag,以 TagID 标识。(如果类比 SQL,可以理解为一张点表) Edge:类似于 Tag,EdgeType 是边上的属性结构,以 EdgeType 标识。(如果类比 SQL,可以理解为一张边表) Property:tag / edge 上的属性值,其数据类型由 tag / edge 的结构确定。 Partition:Nebula Graph 的最小逻辑存储单元,一个 StorageEngine 可包含多个 Partition。Partition 分为 leader 和 follower 的角色,Raftex 保证了 leader 和 follower 之间的数据一致性。 Graph space:每个 Graph Space 是一个独立的业务 Graph 单元,每个 Graph Space 有其独立的 tag 和 edge 集合。一个 Nebula Graph 集群中可包含多个 Graph Space。 Index:本文中出现的 Index 指 nebula graph 中点和边上的属性索引。其数据类型依赖于 tag / edge。 TagIndex:基于 tag 创建的索引,一个 tag 可以创建多个索引。目前(2020.3)暂不支持跨 tag 的复合索引,因此一个索引只可以基于一个 tag。 EdgeIndex:基于 Edge 创建的索引。同样,一个 Edge 可以创建多个索引,但一个索引只可以基于一个 edge。 Scan Policy:Index 的扫描策略,往往一条查询语句可以有多种索引的扫描方式,但具体使用哪种扫描方式需要 Scan Policy 来决定。 Optimizer:对查询条件进行优化,例如对 where 子句的表达式树进行子表达式节点的排序、分裂、合并等。其目的是获取更高的查询效率。 索引需求分析 Nebula Graph 是一个图数据库系统,查询场景一般是由一个点出发,找出指定边类型的相关点的集合,以此类推进行(广度优先遍历)N 度查询。另一种查询场景是给定一个属性值,找出符合这个属性值的所有的点或边。在后面这种场景中,需要对属性值进行高性能的扫描,查出与此属性值对应的边或点,以及边或点上的其它属性。为了提高属性值的查询效率,在这里引入了索引的功能。对边或点的属性值进行排序,以便快速的定位到某个属性上。以此避免了全表扫描。 可以看到对图数据库 Nebula Graph 的索引要求: 支持 tag 和 edge 的属性索引 支持索引的扫描策略的分析和生成 支持索引的管理,如:新建索引、重建索引、删除索引、list | show 索引等。 系统架构概览 图数据库 Nebula Graph 存储架构 从架构图可以看到,每个Storage Server 中可以包含多个 Storage Engine, 每个 Storage Engine中可以包含多个Partition, 不同的Partition之间通过 Raft 协议进行一致性同步。每个 Partition 中既包含了 data,也包含了 index,同一个点或边的 data 和 index 将被存储到同一个 Partition 中。 业务具体分析 数据存储结构 为了更好的描述索引的存储结构,这里将图数据库 Nebula Graph 原始数据的存储结构一起拿出来分析下。 点的存储结构 点的 Data 结构 点的 Index 结构 Vertex 的索引结构如上表所示,下面来详细地讲述下字段: PartitionId:一个点的数据和索引在逻辑上是存放到同一个分区中的。之所以这么做的原因主要有两点: 当扫描索引时,根据索引的 key 能快速地获取到同一个分区中的点 data,这样就可以方便地获取这个点的任何一种属性值,即使这个属性列不属于本索引。 目前 edge 的存储是由起点的 ID Hash 分布,换句话说,一个点的出边存储在哪是由该点的 VertexId 决定的,这个点和它的出边如果被存储到同一个 partition 中,点的索引扫描能快速地定位该点的出边。 IndexId:index 的识别码,通过 indexId 可获取指定 index 的元数据信息,例如:index 所关联的 TagId,index 所在列的信息。 Index binary:index 的核心存储结构,是所有 index 相关列属性值的字节编码,详细结构将在本文的 #Index binary# 章节中讲解。 VertexId:点的识别码,在实际的 data 中,一个点可能会有不同 version 的多行数据。但是在 index 中,index 没有 Version 的概念,index 始终与最新 Version 的 Tag 所对应。 上面讲完字段,我们来简单地实践分析一波: 假设 _PartitionId_ 为 100,TagId 有 tag_1 _和 tag_2,_其中 _tag_1_ 包含三列 :col_t1_1、col_t1_2、col_t1_3,_tag_2_ 包含两列:col_t2_1、col_t2_2。 现在我们来创建索引: i1 = tag_1 (col_t1_1, col_t1_2) ,假设 i1 的 ID 为 1; i2 = tag_2(col_t2_1, col_t2_2), 假设 i2 的 ID 为 2; 可以看到虽然 tag_1 中有 col_t1_3 这列,但是建立索引的时候并没有使用到 col_t1_3,因为在图数据库 Nebula Graph 中索引可以基于 Tag 的一列或多列进行创建。 插入点 // VertexId = hash("v_t1_1"),假如为 50 INSERT VERTEX tag_1(col_t1_1, col_t1_2, col_t1_3), tag_2(col_t2_1, col_t2_2) \ VALUES hash("v_t1_1"):("v_t1_1", "v_t1_2", "v_t1_3", "v_t2_1", "v_t2_2"); 从上可以看到 VertexId 可由 ID 标识对应的数值经过 Hash 得到,如果标识对应的数值本身已经为 int64,则无需进行 Hash 或者其他转化数值为 int64 的运算。而此时数据存储如下: 此时点的 Data 结构 此时点的 Index 结构 说明:index 中 row 和 key 是一个概念,为索引的唯一标识; 边的存储结构 边的索引结构和点索引结构原理类似,这里不再赘述。但有一点需要说明,为了使索引 key 的唯一性成立,索引的 key 的生成借助了不少 data 中的元素,例如 VertexId、SrcVertexId、Rank 等,这也是为什么点索引中并没有 TagId 字段(边索引中也没有 EdgeType 字段),这是因为 IndexId 本身带有 VertexId 等信息可直接区分具体的 tagId 或 EdgeType。 边的 Data 结构 边的 Index 结构 Index binary 介绍 Index binary 是 index 的核心字段,在 index binary 中区分定长字段和不定长字段,int、double、bool 为定长字段,string 则为不定长字段。由于 index binary 是将所有 index column 的属性值编码连接存储,为了精确地定位不定长字段,Nebula Graph 在 index binary 末尾用 int32 记录了不定长字段的长度。 举个例子: 我们现在有一个 index binary 为 index1,是由 int 类型的索引列1 c1、string 类型的索引列 c2,string 类型的索引列 c3 组成: index1 (c1:int, c2:string, c3:string) 假如索引列 c1、c2、c3 某一行对应的 property 值分别为:23、"abc"、"here",则在 index1 中这些索引列将被存储为如下(在示例中为了便于理解,我们直接用原值,实际存储中是原值会经过编码再存储): length = sizeof("abc") = 3 length = sizeof("here") = 4 所以 index1 该 row 对应的 key 则为 23abchere34; 回到我们 Index binary 章节开篇说的 index binary 格式中存在 Variable-length field lenght 字段,那么这个字段的的具体作用是什么呢?我们来简单地举个例: 现在我们又有了一个 index binary,我们给它取名为 index2,它由 string 类型的索引列1 c1、string 类型的索引列 c2,string 类型的索引列 c3 组成: index2 (c1:string, c2:string, c3:string) 假设我们现在 c1、c2、c3 分别有两组如下的数值: row1 : ("ab", "ab", "ab") row2: ("aba", "ba", "b") 可以看到这两行的 prefix(上图红色部分)是相同,都是 "ababab",这时候怎么区分这两个 row 的 index binary 的 key 呢?别担心,我们有 Variable-length field lenght 。 若遇到 where c1 == "ab" 这样的条件查询语句,在 Variable-length field length 中可直接根据顺序读取出 c1 的长度,再根据这个长度取出 row1 和 row2 中 c1 的值,分别是 "ab" 和 "aba" ,这样我们就精准地判断出只有 row1 中的 "ab" 是符合查询条件的。 索引的处理逻辑 Index write 当 Tag / Edge中的一列或多列创建了索引后,一旦涉及到 Tag / Edge 相关的写操作时,对应的索引必须连同数据一起被修改。下面将对索引的write操作在storage层的处理逻辑进行简单介绍: INSERT——插入数据 当用户产生插入点/边操作时,insertProcessor 首先会判断所插入的数据是否有存在索引的 Tag 属性 / Edge 属性。如果没有关联的属性列索引,则按常规方式生成新 Version,并将数据 put 到 Storage Engine;如果有关联的属性列索引,则通过原子操作写入 Data 和 Index,并判断当前的 Vertex / Edge 是否有旧的属性值,如果有,则一并在原子操作中删除旧属性值。 DELETE——删除数据 当用户发生 Drop Vertex / Edge 操作时,deleteProcessor 会将 Data 和 Index(如果存在)一并删除,在删除的过程中同样需要使用原子操作。 UPDATE——更新数据 Vertex / Edge 的更新操作对于 Index 来说,则是 drop 和 insert 的操作:删除旧的索引,插入新的索引,为了保证数据的一致性,同样需要在原子操作中进行。但是对应普通的 Data 来说,仅仅是 insert 操作,使用最新 Version 的 Data 覆盖旧 Version 的 data 即可。 Index scan 在图数据库 Nebula Graph 中是用 LOOKUP 语句来处理 index scan 操作的,LOOKUP 语句可通过属性值作为判断条件,查出所有符合条件的点/边,同样 LOOKUP 语句支持 WHERE 和 YIELD 子句。 LOOKUP 使用技巧 正如根据本文#数据存储结构#章节所描述那样,index 中的索引列是按照创建 index 时的列顺序决定。 举个例子,我们现在有 tag (col1, col2),根据这个 tag 我们可以创建不同的索引,例如: index1 on tag(col1) index2 on tag(col2) index3 on tag(col1, col2) index4 on tag(col2, col1) 我们可以对 clo1、col2 建立多个索引,但在 scan index 时,上述四个 index 返回结果存在差异,甚至是完全不同,在实际业务中具体使用哪个 index,及 index 的最优执行策略,则是通过索引优化器决定。 下面我们再来根据刚才 4 个 index 的例子深入分析一波: lookup on tag where tag.col1 ==1 # 最优的 index 是 index1 lookup on tag where tag.col2 == 2 # 最优的 index 是index2 lookup on tag where tag.col1 > 1 and tag.col2 == 1 # index3 和 index4 都是有效的 index,而 index1 和 index2 则无效 在上述第三个例子中,index3 和 index4 都是有效 index,但最终必须要从两者中选出来一个作为 index,根据优化规则,因为 tag.col2 == 1 是一个等价查询,因此优先使用 tag.col2 会更高效,所以优化器应该选出 index4 为最优 index。 实操一下图数据库 Nebula Graph 索引 在这部分我们就不具体讲解某个语句的用途是什么了,如果你对语句不清楚的话可以去图数据库 Nebula Graph 的官方论坛进行提问:https://discuss.nebula-graph.io/ CREATE——索引的创建 (user@127.0.0.1:6999) [(none)]> CREATE SPACE my_space(partition_num=3, replica_factor=1); Execution succeeded (Time spent: 15.566/16.602 ms) Thu Feb 20 12:46:38 2020 (user@127.0.0.1:6999) [(none)]> USE my_space; Execution succeeded (Time spent: 7.681/8.303 ms) Thu Feb 20 12:46:51 2020 (user@127.0.0.1:6999) [my_space]> CREATE TAG lookup_tag_1(col1 string, col2 string, col3 string); Execution succeeded (Time spent: 12.228/12.931 ms) Thu Feb 20 12:47:05 2020 (user@127.0.0.1:6999) [my_space]> CREATE TAG INDEX t_index_1 ON lookup_tag_1(col1, col2, col3); Execution succeeded (Time spent: 1.639/2.271 ms) Thu Feb 20 12:47:22 2020 DROP——删除索引 (user@127.0.0.1:6999) [my_space]> DROP TAG INDEX t_index_1; Execution succeeded (Time spent: 4.147/5.192 ms) Sat Feb 22 11:30:35 2020 REBUILD——重建索引 如果你是从较老版本的 Nebula Graph 升级上来,或者用 Spark Writer 批量写入过程中(为了性能)没有打开索引,那么这些数据还没有建立过索引,这时可以使用 REBUILD INDEX 命令来重新全量建立一次索引。这个过程可能会耗时比较久,在 rebuild index 完成前,客户端的读写速度都会变慢。 REBUILD {TAG | EDGE} INDEX <index_name> [OFFLINE] LOOKUP——使用索引 需要说明一下,使用 LOOKUP 语句前,请确保已经建立过索引(CREATE INDEX 或 REBUILD INDEX)。 (user@127.0.0.1:6999) [my_space]> INSERT VERTEX lookup_tag_1(col1, col2, col3) VALUES 200:("col1_200", "col2_200", "col3_200"), 201:("col1_201", "col2_201", "col3_201"), 202:("col1_202", "col2_202", "col3_202"); Execution succeeded (Time spent: 18.185/19.267 ms) Thu Feb 20 12:49:44 2020 (user@127.0.0.1:6999) [my_space]> LOOKUP ON lookup_tag_1 WHERE lookup_tag_1.col1 == "col1_200"; ============ | VertexID | ============ | 200 | ------------ Got 1 rows (Time spent: 12.001/12.64 ms) Thu Feb 20 12:49:54 2020 (user@127.0.0.1:6999) [my_space]> LOOKUP ON lookup_tag_1 WHERE lookup_tag_1.col1 == "col1_200" YIELD lookup_tag_1.col1, lookup_tag_1.col2, lookup_tag_1.col3; ======================================================================== | VertexID | lookup_tag_1.col1 | lookup_tag_1.col2 | lookup_tag_1.col3 | ======================================================================== | 200 | col1_200 | col2_200 | col3_200 | ------------------------------------------------------------------------ Got 1 rows (Time spent: 3.679/4.657 ms) Thu Feb 20 12:50:36 2020 索引的介绍就到此为止了,如果你对图数据库 Nebula Graph 的索引有更多的功能要求或者建议反馈,欢迎去 GitHub:https://github.com/vesoft-inc/nebula issue 区向我们提 issue 或者前往官方论坛:https://discuss.nebula-graph.io/ 的 Feedback 分类下提建议 作者有话说:Hi,我是 bright-starry-sky,是图数据 Nebula Graph 研发工程师,对数据库存储有浓厚的兴趣,希望本次的经验分享能给大家带来帮助,如有不当之处也希望能帮忙纠正,谢谢~
Kubernetes 是什么 Kubernetes 是一个开源的,用于管理云平台中多个主机上的容器化的应用,Kubernetes 的目标是让部署容器化的应用简单并且高效,Kubernetes 提供了应用部署,规划,更新,维护的一种机制。Kubernetes 在设计结构上定义了一系列的构建模块,其目的是为了提供一个可以部署、维护和扩展应用程序的机制,组成 Kubernetes 的组件设计概念为松耦合和可扩展的,这样可以使之满足多种不同的工作负载。可扩展性在很大程度上由 KubernetesAPI 提供,此 API 主要被作为扩展的内部组件以及 Kubernetes 上运行的容器来使用。 Kubernetes 主要由以下几个核心组件组成: etcd 保存了整个集群的状态 apiserver 提供了资源操作的唯一入口,并提供认证、授权、访问控制、API注册和发现等机制 controller manager 负责维护集群的状态,比如故障检测、自动扩展、滚动更新等 scheduler 负责资源的调度,按照预定的调度策略将Pod调度到相应的机器上 kubelet 负责维护容器的生命周期,同时也负责 Volume和网络的管理 Container runtime 负责镜像管理以及 Pod 和容器的真正运行(CRI) kube-proxy 负责为 Service 提供 cluster 内部的服务发现和负载均衡 除了核心组件,还有一些推荐的 Add-ons: kube-dns 负责为整个集群提供 DNS 服务 Ingress Controller 为服务提供外网入口 Heapster 提供资源监控 Dashboard 提供 GUI Federation 提供跨可用区的集群 Fluentd-elasticsearch 提供集群日志采集、存储与查询 Kubernetes 和数据库 数据库容器化是最近的一大热点,那么 Kubernetes 能为数据库带来什么好处呢? 故障恢复: Kubernetes 提供故障恢复的功能,数据库应用如果宕掉,Kubernetes 可以将其自动重启,或者将数据库实例迁移到集群中其他节点上 存储管理: Kubernetes 提供了丰富的存储接入方案,数据库应用能透明地使用不同类型的存储系统 负载均衡: Kubernetes Service 提供负载均衡功能,能将外部访问平摊给不同的数据库实例副本上 水平拓展: Kubernetes 可以根据当前数据库集群的资源利用率情况,缩放副本数目,从而提升资源的利用率 目前很多数据库,如:MySQL,MongoDB 和 TiDB 在 Kubernetes 集群中都能运行很良好。 Nebula Graph在Kubernetes中的实践 Nebula Graph 是一个分布式的开源图数据库,主要组件有:Query Engine 的 graphd,数据存储的 storaged,和元数据的 meted。在 Kubernetes 实践过程中,它主要给图数据库 Nebula Graph 带来了以下的好处: Kubernetes 能分摊 nebula graphd,metad 和 storaged 不副本之间的负载。graphd,metad 和 storaged 可以通过 Kubernetes 的域名服务自动发现彼此。 通过 storageclass,pvc 和 pv 可以屏蔽底层存储细节,无论使用本地卷还是云盘,Kubernetes 均可以屏蔽这些细节。 通过 Kubernetes 可以在几秒内成功部署一套 Nebula 集群,Kubernetes 也可以无感知地实现 Nebula 集群的升级。 Nebula 集群通过 Kubernetes 可以做到自我恢复,单体副本 crash,Kubernetes 可以重新将其拉起,无需运维人员介入。 Kubernetes 可以根据当前 Nebula 集群的资源利用率情况水平伸缩 Nebula 集群,从而提供集群的性能。 下面来讲解下具体的实践内容。 集群部署 硬件和软件要求 这里主要罗列下本文部署涉及到的机器、操作系统参数 操作系统使用的 CentOS-7.6.1810 x86_64 虚拟机配置 4 CPU 8G 内存 50G 系统盘 50G 数据盘A 50G 数据盘B Kubernetes 集群版本 v1.16 Nebula 版本为 v1.0.0-rc3 使用本地 PV 作为数据存储 kubernetes 集群规划 以下为集群清单 服务器 IP nebula 实例 role 192.168.0.1 k8s-master 192.168.0.2 graphd, metad-0, storaged-0 k8s-slave 192.168.0.3 graphd, metad-1, storaged-1 k8s-slave 192.168.0.4 graphd, metad-2, storaged-2 k8s-slave Kubernetes 待部署组件 安装 Helm 准备本地磁盘,并安装本地卷插件 安装 nebula 集群 安装 ingress-controller 安装 Helm Helm 是 Kubernetes 集群上的包管理工具,类似 CentOS 上的 yum,Ubuntu 上的 apt-get。使用 Helm 可以极大地降低使用 Kubernetes 部署应用的门槛。由于本篇文章不做 Helm 详细介绍,有兴趣的小伙伴可自行阅读《Helm 入门指南》 下载安装Helm 使用下面命令在终端执行即可安装 Helm [root@nebula ~]# wget https://get.helm.sh/helm-v3.0.1-linux-amd64.tar.gz [root@nebula ~]# tar -zxvf helm/helm-v3.0.1-linux-amd64.tgz [root@nebula ~]# mv linux-amd64/helm /usr/bin/helm [root@nebula ~]# chmod +x /usr/bin/helm 查看 Helm 版本 执行 helm version 命令即可查看对应的 Helm 版本,以文本为例,以下为输出结果: version.BuildInfo{ Version:"v3.0.1", GitCommit:"7c22ef9ce89e0ebeb7125ba2ebf7d421f3e82ffa", GitTreeState:"clean", GoVersion:"go1.13.4" } 设置本地磁盘 在每台机器上做如下配置 创建 mount 目录 [root@nebula ~]# sudo mkdir -p /mnt/disks 格式化数据盘 [root@nebula ~]# sudo mkfs.ext4 /dev/diskA [root@nebula ~]# sudo mkfs.ext4 /dev/diskB 挂载数据盘 [root@nebula ~]# DISKA_UUID=$(blkid -s UUID -o value /dev/diskA) [root@nebula ~]# DISKB_UUID=$(blkid -s UUID -o value /dev/diskB) [root@nebula ~]# sudo mkdir /mnt/disks/$DISKA_UUID [root@nebula ~]# sudo mkdir /mnt/disks/$DISKB_UUID [root@nebula ~]# sudo mount -t ext4 /dev/diskA /mnt/disks/$DISKA_UUID [root@nebula ~]# sudo mount -t ext4 /dev/diskB /mnt/disks/$DISKB_UUID [root@nebula ~]# echo UUID=`sudo blkid -s UUID -o value /dev/diskA` /mnt/disks/$DISKA_UUID ext4 defaults 0 2 | sudo tee -a /etc/fstab [root@nebula ~]# echo UUID=`sudo blkid -s UUID -o value /dev/diskB` /mnt/disks/$DISKB_UUID ext4 defaults 0 2 | sudo tee -a /etc/fstab 部署本地卷插件 [root@nebula ~]# curl https://github.com/kubernetes-sigs/sig-storage-local-static-provisioner/archive/v2.3.3.zip [root@nebula ~]# unzip v2.3.3.zip 修改 v2.3.3/helm/provisioner/values.yaml # # Common options. # common: # # Defines whether to generate service account and role bindings. # rbac: true # # Defines the namespace where provisioner runs # namespace: default # # Defines whether to create provisioner namespace # createNamespace: false # # Beta PV.NodeAffinity field is used by default. If running against pre-1.10 # k8s version, the `useAlphaAPI` flag must be enabled in the configMap. # useAlphaAPI: false # # Indicates if PVs should be dependents of the owner Node. # setPVOwnerRef: false # # Provisioner clean volumes in process by default. If set to true, provisioner # will use Jobs to clean. # useJobForCleaning: false # # Provisioner name contains Node.UID by default. If set to true, the provisioner # name will only use Node.Name. # useNodeNameOnly: false # # Resync period in reflectors will be random between minResyncPeriod and # 2*minResyncPeriod. Default: 5m0s. # #minResyncPeriod: 5m0s # # Defines the name of configmap used by Provisioner # configMapName: "local-provisioner-config" # # Enables or disables Pod Security Policy creation and binding # podSecurityPolicy: false # # Configure storage classes. # classes: - name: fast-disks # Defines name of storage classe. # Path on the host where local volumes of this storage class are mounted # under. hostDir: /mnt/fast-disks # Optionally specify mount path of local volumes. By default, we use same # path as hostDir in container. # mountDir: /mnt/fast-disks # The volume mode of created PersistentVolume object. Default to Filesystem # if not specified. volumeMode: Filesystem # Filesystem type to mount. # It applies only when the source path is a block device, # and desire volume mode is Filesystem. # Must be a filesystem type supported by the host operating system. fsType: ext4 blockCleanerCommand: # Do a quick reset of the block device during its cleanup. # - "/scripts/quick_reset.sh" # or use dd to zero out block dev in two iterations by uncommenting these lines # - "/scripts/dd_zero.sh" # - "2" # or run shred utility for 2 iteration.s - "/scripts/shred.sh" - "2" # or blkdiscard utility by uncommenting the line below. # - "/scripts/blkdiscard.sh" # Uncomment to create storage class object with default configuration. # storageClass: true # Uncomment to create storage class object and configure it. # storageClass: # reclaimPolicy: Delete # Available reclaim policies: Delete/Retain, defaults: Delete. # isDefaultClass: true # set as default class # # Configure DaemonSet for provisioner. # daemonset: # # Defines the name of a Provisioner # name: "local-volume-provisioner" # # Defines Provisioner's image name including container registry. # image: quay.io/external_storage/local-volume-provisioner:v2.3.3 # # Defines Image download policy, see kubernetes documentation for available values. # #imagePullPolicy: Always # # Defines a name of the service account which Provisioner will use to communicate with API server. # serviceAccount: local-storage-admin # # Defines a name of the Pod Priority Class to use with the Provisioner DaemonSet # # Note that if you want to make it critical, specify "system-cluster-critical" # or "system-node-critical" and deploy in kube-system namespace. # Ref: https://k8s.io/docs/tasks/administer-cluster/guaranteed-scheduling-critical-addon-pods/#marking-pod-as-critical # #priorityClassName: system-node-critical # If configured, nodeSelector will add a nodeSelector field to the DaemonSet PodSpec. # # NodeSelector constraint for local-volume-provisioner scheduling to nodes. # Ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector nodeSelector: {} # # If configured KubeConfigEnv will (optionally) specify the location of kubeconfig file on the node. # kubeConfigEnv: KUBECONFIG # # List of node labels to be copied to the PVs created by the provisioner in a format: # # nodeLabels: # - failure-domain.beta.kubernetes.io/zone # - failure-domain.beta.kubernetes.io/region # # If configured, tolerations will add a toleration field to the DaemonSet PodSpec. # # Node tolerations for local-volume-provisioner scheduling to nodes with taints. # Ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ tolerations: [] # # If configured, resources will set the requests/limits field to the Daemonset PodSpec. # Ref: https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/ resources: {} # # Configure Prometheus monitoring # prometheus: operator: ## Are you using Prometheus Operator? enabled: false serviceMonitor: ## Interval at which Prometheus scrapes the provisioner interval: 10s # Namespace Prometheus is installed in namespace: monitoring ## Defaults to whats used if you follow CoreOS [Prometheus Install Instructions](https://github.com/coreos/prometheus-operator/tree/master/helm#tldr) ## [Prometheus Selector Label](https://github.com/coreos/prometheus-operator/blob/master/helm/prometheus/templates/prometheus.yaml#L65) ## [Kube Prometheus Selector Label](https://github.com/coreos/prometheus-operator/blob/master/helm/kube-prometheus/values.yaml#L298) selector: prometheus: kube-prometheus 将hostDir: /mnt/fast-disks 改成hostDir: /mnt/disks将# storageClass: true 改成 storageClass: true然后执行: #安装 [root@nebula ~]# helm install local-static-provisioner v2.3.3/helm/provisioner #查看local-static-provisioner部署情况 [root@nebula ~]# helm list 部署 nebula 集群 下载 nebula helm-chart 包 # 下载nebula [root@nebula ~]# wget https://github.com/vesoft-inc/nebula/archive/master.zip # 解压 [root@nebula ~]# unzip master.zip 设置 Kubernetes slave 节点 下面是 Kubernetes 节点列表,我们需要设置 slave 节点的调度标签。可以将 _192.168.0.2_,_192.168.0.3_,_192.168.0.4_ 打上 nebula: "yes" 的标签。 服务器 IP kubernetes roles nodeName 192.168.0.1 master 192.168.0.1 192.168.0.2 worker 192.168.0.2 192.168.0.3 worker 192.168.0.3 192.168.0.4 worker 192.168.0.4 具体操作如下: [root@nebula ~]# kubectl label node 192.168.0.2 nebula="yes" --overwrite [root@nebula ~]# kubectl label node 192.168.0.3 nebula="yes" --overwrite [root@nebula ~]# kubectl label node 192.168.0.4 nebula="yes" --overwrite 调整 nebula helm chart 默认的 values 值 nebula helm-chart 包目录如下: master/kubernetes/ └── helm ├── Chart.yaml ├── templates │ ├── configmap.yaml │ ├── deployment.yaml │ ├── _helpers.tpl │ ├── ingress-configmap.yaml\ │ ├── NOTES.txt │ ├── pdb.yaml │ ├── service.yaml │ └── statefulset.yaml └── values.yaml 2 directories, 10 files 我们需要调整 master/kubernetes/values.yaml 里面的 MetadHosts 的值,将这个 IP List 替换本环境的 3 个 k8s worker 的 ip。 MetadHosts: - 192.168.0.2:44500 - 192.168.0.3:44500 - 192.168.0.4:44500 通过 helm 安装 nebula # 安装 [root@nebula ~]# helm install nebula master/kubernetes/helm # 查看 [root@nebula ~]# helm status nebula # 查看k8s集群上nebula部署情况 [root@nebula ~]# kubectl get pod | grep nebula nebula-graphd-579d89c958-g2j2c 1/1 Running 0 1m nebula-graphd-579d89c958-p7829 1/1 Running 0 1m nebula-graphd-579d89c958-q74zx 1/1 Running 0 1m nebula-metad-0 1/1 Running 0 1m nebula-metad-1 1/1 Running 0 1m nebula-metad-2 1/1 Running 0 1m nebula-storaged-0 1/1 Running 0 1m nebula-storaged-1 1/1 Running 0 1m nebula-storaged-2 1/1 Running 0 1m 部署 Ingress-controller Ingress-controller 是 Kubernetes 的一个 Add-Ons。Kubernetes 通过 ingress-controller 将 Kubernetes 内部署的服务暴露给外部用户访问。Ingress-controller 还提供负载均衡的功能,可以将外部访问流量平摊给 k8s 中应用的不同的副本。 选择一个节点部署 Ingress-controller [root@nebula ~]# kubectl get node NAME STATUS ROLES AGE VERSION 192.168.0.1 Ready master 82d v1.16.1 192.168.0.2 Ready <none> 82d v1.16.1 192.168.0.3 Ready <none> 82d v1.16.1 192.168.0.4 Ready <none> 82d v1.16.1 [root@nebula ~]# kubectl label node 192.168.0.4 ingress=yes 编写 ingress-nginx.yaml 部署文件 apiVersion: v1 kind: Namespace metadata: name: ingress-nginx labels: app.kubernetes.io/name: ingress-nginx app.kubernetes.io/part-of: ingress-nginx --- kind: ConfigMap apiVersion: v1 metadata: name: nginx-configuration namespace: ingress-nginx labels: app.kubernetes.io/name: ingress-nginx app.kubernetes.io/part-of: ingress-nginx --- kind: ConfigMap apiVersion: v1 metadata: name: tcp-services namespace: ingress-nginx labels: app.kubernetes.io/name: ingress-nginx app.kubernetes.io/part-of: ingress-nginx --- kind: ConfigMap apiVersion: v1 metadata: name: udp-services namespace: ingress-nginx labels: app.kubernetes.io/name: ingress-nginx app.kubernetes.io/part-of: ingress-nginx --- apiVersion: v1 kind: ServiceAccount metadata: name: nginx-ingress-serviceaccount namespace: ingress-nginx labels: app.kubernetes.io/name: ingress-nginx app.kubernetes.io/part-of: ingress-nginx --- apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRole metadata: name: nginx-ingress-clusterrole labels: app.kubernetes.io/name: ingress-nginx app.kubernetes.io/part-of: ingress-nginx rules: - apiGroups: - "" resources: - configmaps - endpoints - nodes - pods - secrets verbs: - list - watch - apiGroups: - "" resources: - nodes verbs: - get - apiGroups: - "" resources: - services verbs: - get - list - watch - apiGroups: - "extensions" - "networking.k8s.io" resources: - ingresses verbs: - get - list - watch - apiGroups: - "" resources: - events verbs: - create - patch - apiGroups: - "extensions" - "networking.k8s.io" resources: - ingresses/status verbs: - update --- apiVersion: rbac.authorization.k8s.io/v1beta1 kind: Role metadata: name: nginx-ingress-role namespace: ingress-nginx labels: app.kubernetes.io/name: ingress-nginx app.kubernetes.io/part-of: ingress-nginx rules: - apiGroups: - "" resources: - configmaps - pods - secrets - namespaces verbs: - get - apiGroups: - "" resources: - configmaps resourceNames: # Defaults to "<election-id>-<ingress-class>" # Here: "<ingress-controller-leader>-<nginx>" # This has to be adapted if you change either parameter # when launching the nginx-ingress-controller. - "ingress-controller-leader-nginx" verbs: - get - update - apiGroups: - "" resources: - configmaps verbs: - create - apiGroups: - "" resources: - endpoints verbs: - get --- apiVersion: rbac.authorization.k8s.io/v1beta1 kind: RoleBinding metadata: name: nginx-ingress-role-nisa-binding namespace: ingress-nginx labels: app.kubernetes.io/name: ingress-nginx app.kubernetes.io/part-of: ingress-nginx roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: nginx-ingress-role subjects: - kind: ServiceAccount name: nginx-ingress-serviceaccount namespace: ingress-nginx --- apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRoleBinding metadata: name: nginx-ingress-clusterrole-nisa-binding labels: app.kubernetes.io/name: ingress-nginx app.kubernetes.io/part-of: ingress-nginx roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: nginx-ingress-clusterrole subjects: - kind: ServiceAccount name: nginx-ingress-serviceaccount namespace: ingress-nginx --- apiVersion: apps/v1 kind: DaemonSet metadata: name: nginx-ingress-controller namespace: ingress-nginx labels: app.kubernetes.io/name: ingress-nginx app.kubernetes.io/part-of: ingress-nginx spec: selector: matchLabels: app.kubernetes.io/name: ingress-nginx app.kubernetes.io/part-of: ingress-nginx template: metadata: labels: app.kubernetes.io/name: ingress-nginx app.kubernetes.io/part-of: ingress-nginx annotations: prometheus.io/port: "10254" prometheus.io/scrape: "true" spec: hostNetwork: true tolerations: - key: "node-role.kubernetes.io/master" operator: "Exists" effect: "NoSchedule" affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: app.kubernetes.io/name operator: In values: - ingress-nginx topologyKey: "ingress-nginx.kubernetes.io/master" nodeSelector: ingress: "yes" serviceAccountName: nginx-ingress-serviceaccount containers: - name: nginx-ingress-controller image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller-amd64:0.26.1 args: - /nginx-ingress-controller - --configmap=$(POD_NAMESPACE)/nginx-configuration - --tcp-services-configmap=default/graphd-services - --udp-services-configmap=$(POD_NAMESPACE)/udp-services - --publish-service=$(POD_NAMESPACE)/ingress-nginx - --annotations-prefix=nginx.ingress.kubernetes.io - --http-port=8000 securityContext: allowPrivilegeEscalation: true capabilities: drop: - ALL add: - NET_BIND_SERVICE # www-data -> 33 runAsUser: 33 env: - name: POD_NAME valueFrom: fieldRef: fieldPath: metadata.name - name: POD_NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace ports: - name: http containerPort: 80 - name: https containerPort: 443 livenessProbe: failureThreshold: 3 httpGet: path: /healthz port: 10254 scheme: HTTP initialDelaySeconds: 10 periodSeconds: 10 successThreshold: 1 timeoutSeconds: 10 readinessProbe: failureThreshold: 3 httpGet: path: /healthz port: 10254 scheme: HTTP periodSeconds: 10 successThreshold: 1 timeoutSeconds: 10 部署 ingress-nginx # 部署 [root@nebula ~]# kubectl create -f ingress-nginx.yaml # 查看部署情况 [root@nebula ~]# kubectl get pod -n ingress-nginx NAME READY STATUS RESTARTS AGE nginx-ingress-controller-mmms7 1/1 Running 2 1m 访问 nebula 集群 查看 ingress-nginx 所在的节点: [root@nebula ~]# kubectl get node -l ingress=yes -owide NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME 192.168.0.4 Ready <none> 1d v1.16.1 192.168.0.4 <none> CentOS Linux 7 (Core) 7.6.1810.el7.x86_64 docker://19.3.3 访问 nebula 集群: [root@nebula ~]# docker run --rm -ti --net=host vesoft/nebula-console:nightly --addr=192.168.0.4 --port=3699 FAQ 如何搭建一套 Kubernetes 集群? 搭建高可用的 Kubernetes 可以参考社区文档:https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/high-availability/你也可以通过 minikube 搭建本地的 Kubernetes 集群,参考文档:https://kubernetes.io/docs/setup/learning-environment/minikube/ 如何调整 nebula 集群的部署参数? 在使用 helm install 时,使用 --set 可以设置部署参数,从而覆盖掉 helm chart 中 values.yaml 中的变量。参考文档:https://helm.sh/docs/intro/using_helm/ 如何查看 nebula 集群状况? 使用kubectl get pod | grep nebula命令,或者直接在 Kubernetes dashboard 上查看 nebula 集群的运行状况。 如何使用其他类型的存储? 参考文档:https://kubernetes.io/zh/docs/concepts/storage/storage-classes/ 参考资料 Helm 入门指南 详解 k8s 组件 Ingress 边缘路由器并落地到微服务 附录 Nebula Graph:一个开源的分布式图数据库 GitHub:https://github.com/vesoft-inc/nebula 知乎:zhihu.com/org/nebulagraph/posts 微博:weibo.com/nebulagraph
应用 AddressSanitizer 发现程序内存错误 作为 C/ C++ 工程师,在开发过程中会遇到各类问题,最常见便是内存使用问题,比如,越界,泄漏。过去常用的工具是 Valgrind,但使用 Valgrind 最大问题是它会极大地降低程序运行的速度,初步估计会降低 10 倍运行速度。而 Google 开发的 AddressSanitizer 这个工具很好地解决了 Valgrind 带来性能损失问题,它非常快,只拖慢程序 2 倍速度。 AddressSanitizer 概述 AddressSanitizer 是一个基于编译器的测试工具,可在运行时检测 C/C++ 代码中的多种内存错误。严格上来说,AddressSanitizer 是一个编译器插件,它分为两个模块,一个是编译器的 instrumentation 模块,一个是用来替换 malloc/free 的动态库。 Instrumentation 主要是针对在 llvm 编译器级别对访问内存的操作(store,load,alloc等),将它们进行处理。动态库主要提供一些运行时的复杂的功能(比如 poison/unpoison shadow memory)以及将 malloc/free 等系统调用函数 hook 住。 AddressSanitizer 基本使用 根据 AddressSanitizer Wiki 可以检测下面这些内存错误 Use after free:访问堆上已经被释放的内存 Heap buffer overflow:堆上缓冲区访问溢出 Stack buffer overflow:栈上缓冲区访问溢出 Global buffer overflow:全局缓冲区访问溢出 Use after return:访问栈上已被释放的内存 Use after scope:栈对象使用超过定义范围 Initialization order bugs:初始化命令错误 Memory leaks:内存泄漏 这里我只简单地介绍下基本的使用,详细的使用文档可以看官方的编译器使用文档,比如 Clang 的文档:https://clang.llvm.org/docs/AddressSanitizer.html Use after free 实践例子 下面这段代码是一个很简单的 Use after free 的例子: //use_after_free.cpp #include <iostream> int main(int argc, char **argv) { int *array = new int[100]; delete [] array; std::cout << array[0] << std::endl; return 1; } 编译代码,并且运行,这里可以看到只需要在编译的时候带上 -fsanitize=address 选项就可以了。 clang++ -O -g -fsanitize=address ./use_after_free.cpp ./a.out 最终我们会看到如下的输出: ==10960==ERROR: AddressSanitizer: heap-use-after-free on address 0x614000000040 at pc 0x00010d471df0 bp 0x7ffee278e6b0 sp 0x7ffee278e6a8 READ of size 4 at 0x614000000040 thread T0 #0 0x10d471def in main use_after_free.cpp:6 #1 0x7fff732c17fc in start (libdyld.dylib:x86_64+0x1a7fc) 0x614000000040 is located 0 bytes inside of 400-byte region [0x614000000040,0x6140000001d0) freed by thread T0 here: #0 0x10d4ccced in wrap__ZdaPv (libclang_rt.asan_osx_dynamic.dylib:x86_64h+0x51ced) #1 0x10d471ca1 in main use_after_free.cpp:5 #2 0x7fff732c17fc in start (libdyld.dylib:x86_64+0x1a7fc) previously allocated by thread T0 here: #0 0x10d4cc8dd in wrap__Znam (libclang_rt.asan_osx_dynamic.dylib:x86_64h+0x518dd) #1 0x10d471c96 in main use_after_free.cpp:4 #2 0x7fff732c17fc in start (libdyld.dylib:x86_64+0x1a7fc) SUMMARY: AddressSanitizer: heap-use-after-free use_after_free.cpp:6 in main 可以看到一目了然,非常清楚的告诉了我们在哪一行内存被释放,而又在哪一行内存再次被使用。 还有一个是内存泄漏,比如下面的代码,显然 p 所指的内存没有被释放。 void *p; int main() { p = malloc(7); p = 0; // The memory is leaked here. return 0; } 编译然后运行 clang -fsanitize=address -g ./leak.c ./a.out 可以看到如下的结果 ================================================================= ==17756==ERROR: LeakSanitizer: detected memory leaks Direct leak of 7 byte(s) in 1 object(s) allocated from: #0 0x4ffc80 in malloc (/home/simon.liu/workspace/a.out+0x4ffc80) #1 0x534ab8 in main /home/simon.liu/workspace/./leak.c:4:8 #2 0x7f127c42af42 in __libc_start_main (/usr/lib64/libc.so.6+0x23f42) SUMMARY: AddressSanitizer: 7 byte(s) leaked in 1 allocation(s). 不过这里要注意内存泄漏的检测只会在程序最后退出之前进行检测,也就是说如果你在运行时如果不断地分配内存,然后在退出的时候对内存进行释放,AddressSanitizer 将不会检测到内存泄漏,这种时候可能你就需要另外的工具了 JeMalloc / TCMalloc。 AddressSanitizer 基本原理 这里简单介绍一下 AddressSanitizer 的实现,更详细的算法实现可以看《AddressSanitizer: a fast address sanity checker》:https://www.usenix.org/system/files/conference/atc12/atc12-final39.pdf AddressSanitizer 会替换你的所有 malloc 以及 free,然后已经被分配(malloc)的内存区域的前后会被标记为 poisoned (主要是为了处理 overflow 这种情况),而释放(free)的内存会被标记为 poisoned(主要是为了处理 Use after free)。你的代码中的每一次的内存存取都会被编译器做类似下面的翻译. before: *address = ...; // or: ... = *address; after: shadow_address = MemToShadow(address); if (ShadowIsPoisoned(shadow_address)) { ReportError(address, kAccessSize, kIsWrite); } *address = ...; // or: ... = *address; 这里可以看到首先会对内存地址有一个翻译(MemToShadow)的过程,然后再来判断当所访问的内存区域是否为 poisoned,如果是则直接报错并退出。 这里之所以会有这个翻译是因为 AddressSanitizer 将虚拟内存分为了两部分: Main application memory(Mem)也就是被当前程序自身使用的内存 Shadow memory 简单来说就是保存了主存元信息的一块内存,比如主存的那些区域被 posioned 都是在 Shadow memory 中保存的 AddressSanitizer 和其他内存检测工具对比 下图是 AddressSanitizer 与其他的一些内存检测工具的对比: AddressSanitizer Valgrind/Memcheck Dr. Memory Mudflap Guard Page gperftools technology CTI DBI DBI CTI Library Library ARCH x86, ARM, PPC x86, ARM, PPC, MIPS, S390X, TILEGX x86 all(?) all(?) all(?) OS Linux, OS X, Windows, FreeBSD, Android, iOS Simulator Linux, OS X, Solaris, Android Windows, Linux Linux, Mac(?) All (1) Linux, Windows Slowdown 2x 20x 10x 2x-40x ? ? Detects: Heap OOB yes yes yes yes some some Stack OOB yes no no some no no Global OOB yes no no ? no no UAF yes yes yes yes yes yes UAR yes (see AddressSanitizerUseAfterReturn) no no no no no UMR no (see MemorySanitizer) yes yes ? no no Leaks yes (see LeakSanitizer) yes yes ? no yes 参数说明: _DBI_: dynamic binary instrumentation(动态二进制插桩) _CTI_: compile-time instrumentation (编译时插桩) _UMR_: uninitialized memory reads (读取未初始化的内存) _UAF_: use-after-free (aka dangling pointer) (使用释放后的内存) _UAR_: use-after-return (使用返回后的值) _OOB_: out-of-bounds (溢出) _x86_: includes 32- and 64-bit. 可以看到相比于 Valgrind,AddressSanitizer 只会拖慢程序 2 倍运行速度。当前 AddressSanitizer 支持 GCC 以及 Clang,其中 GCC 是从 4.8 开始支持,而 Clang 的话是从 3.1 开始支持。 AddressSanitizer 的使用注意事项 AddressSanitizer 在发现内存访问违规时,应用程序并不会自动崩溃。这是由于在使用模糊测试工具时,它们通常都是通过检查返回码来检测这种错误。当然,我们也可以在模糊测试进行之前通过将环境变量 ASAN_OPTIONS 修改成如下形式来迫使软件崩溃: export ASAN_OPTIONS='abort_on_error=1'/ AddressSanitizer 需要相当大的虚拟内存(大约 20 TB),不用担心,这个只是虚拟内存,你仍可以使用你的应用程序。但像 american fuzzy lop 这样的模糊测试工具就会对模糊化的软件使用内存进行限制,不过你仍可以通过禁用内存限制来解决该问题。唯一需要注意的就是,这会带来一些风险:测试样本可能会导致应用程序分配大量的内存进而导致系统不稳定或者其他应用程序崩溃。因此在进行一些重要的模糊测试时,不要去尝试在同一个系统上禁用内存限制。 在 Nebula Graph 中开启 AddressSanitizer 我们在 Nebula Graph 中也使用了 AddressSanitizer,它帮助我们发现了非常多的问题。而在 Nebula Graph 中开启 AddressSanitizer 很简单,只需要在 Cmake 的时候带上打开 ENABLE_ASAN 这个 Option 就可以,比如: Cmake -DENABLE_ASAN=On 这里建议所有的开发者在开发完毕功能运行单元测试的时候都打开 AddressSanitizer 来运行单元测试,这样可以发现很多不容易发现的内存问题,节省很多调试的时间。 附录 Nebula Graph:一个开源的分布式图数据库 GitHub:https://github.com/vesoft-inc/nebula 知乎:zhihu.com/org/nebulagraph/posts 微博:weibo.com/nebulagraph
如果 2019 年技术圈有十大流行词,容器化肯定占有一席之地,随着 Docker 的风靡,前端领域应用到 Docker 的场景也越来越多,本文主要来讲述下开源的分布式图数据库 Nebula Graph 是如何将 Docker 应用到可视化界面中。 为什么要用 Docker 对于前端日常开发而言,有时也会用到 Docker,结合到 Nebula Graph Studio (分布式图数据库 Nebula Graph 的图形界面工具)使用 Docker 主要基于以下考虑: 统一运行环境:我们的工具背后有好几个服务组合在一起,诸如不同技术栈的现有服务,纯前端的静态资源。 用户使用成本低:目前云服务还在开发中,想让用户对服务组合无感,能直接在本地一键启动应用并使用。 快速部署:团队本就提供有 Nebula镜像版本 实践,给了我们前端一些参考和借鉴。 Docker 镜像的构建 既然要使用 Docker 来承载我们的应用,就得将项目进行镜像构建。与所有 build 镜像类似,需要配置一份命名为Dockerfile 的文件,文件是一些步骤的描述,简单来说就是把项目复制到镜像里,并设置好启动方式: # 选择基础镜像 FROM node:10 # 设置工作目录 WORKDIR /nebula-web-console # 把当前项目内容拷贝到镜像中的 /nebula-web-console 目录下 ADD . /nebula-web-console # 在镜像中下载前端依赖 RUN npm install # 执行构建 RUN npm run build EXPOSE 7001 # 镜像启动时执行的部署命令 CMD ["npm", "run", "docker-start"] Docker 镜像体积优化 如果按照上述的配置文件来构建 Docker 镜像,以我们的项目为例,将会生成一个体积约为 1.3GB 的镜像,这个看起来有点吓人,因为即使在网速快的用户电脑光下载镜像也需要等待不少时间,这是不能接受的。 在调研了相应的资料后,了解到可以从以下几个方面缩小 Docker 镜像体积进行优化: 基础镜像源的选择 所谓基础镜像源,就是我们在进行构建步骤时,选择的一个基础环境(如上 node:10 ),通过查看 Dockerhub 上有关 Node.js 的基础环境镜像时,我们会发现有多个版本,虽然都是 Node.js 相关基础镜像,但不同版本,他们除了 Node.js 版本不同外,在内部集成的环境也不一样,例如带有 alpine 的版本,相当于是一个比较精巧的 Linux 系统镜像,在此版本运行的容器中会发现不存在我们常规系统中所附带的工具,比如 bash、curl 等,由此来缩小体积。 根据项目实际需要,当我把基础镜像换为 alpine 版本后,再次进行构建,此时镜像体积已大幅度减小,从 1.3GB 直降为 500+MB,体积优化效果明显,所以当你发现自己构建的镜像体积过大时,可以考虑从更换基础镜像源的方式来着手,看看是否使用了过于臃肿的镜像源。 Multi-stage 构建镜像 所谓 multi-stage 即是 Docker 镜像构建的时候采取的策略,详细可点击链接提供的资料。 Docker 构建规则 简言之就是利用 Docker 构建提供的规则:Dockerfile 的操作都会增加一个所谓镜像的“层”,每一层都会增加镜像体积,通过采用多步骤策略,每一步骤包含具有相同意义的一系列操作(例如构建,部署),步骤与步骤之间通过产物镜像引用的方式,由此来缩减最终构建镜像所需要的层数,具体操作比如: # 设置第一步骤产生的镜像,并命名为builder FROM node:10-alpine as builder WORKDIR /nebula-web-console # 复制当前项目内容至镜像中 ADD . /nebula-web-console # 进行相应的构建 RUN npm install RUN npm run build .... # 进行第二步骤构建 FROM node:10-alpine WORKDIR /nebula-web-console # 复制第一步构建镜像的产物内容至当前镜像,只用到了一层镜像层从而节约了之前构建步骤的镜像层数 COPY --from=builder . /nebula-web-console CMD ["npm", "run", "docker-start"] .dockerignore 类似我们熟悉的 .gitignore ,就是当我们在进行 COPY 或 ADD 文件复制操作时,将不必要的文件忽略掉(诸如文档文件、git文件、node_modules以及一些非生成必要文件等),从而减小镜像体积,更详细内容可参考文档连接:.dockerignore。 操作合并 基于上述提到在 Dockerfile 构建镜像的过程做,每一个操作都会在前一步镜像基础上增加一“层”,可以利用 & 来合并多个操作,减少层数,比如: # 以下两个操作分别代表两层 RUN npm install RUN npm run build 改为: # 使用 & 后变了为一层 RUN npm install && npm run build 由此我们减少了层数的增加,即减少了镜像的体积。同时,在构建镜像的过程中,我们也可以通过在达到相同目的的前提下,尽量减少不必要的操作来减少“层数”的添加。 前端常规性体积优化 压缩丑化代码,移除源码 此操作可以放在构建步骤阶段,这样会进一步缩小镜像的文件体积。 node_modules 只下载生产环境需要的代码 此操作可以放在部署阶段,只下载生产环境所需要的第三方依赖代码: npm install --production 。 公共资源放在CDN 如果镜像被期待运行在联网环境,可以考虑将一些体积相比较大的公共文件(图片、第三方库等)放在CDN服务 器上,将部分资源剥离出去,也会进一步缩小体积。 ... 以上只作为一个线索参考,更多前端常规的优化步骤,都可以迁移至镜像中进行,毕竟和我们本地开发一样,镜像构建也是一个运行代码的环境嘛。 小结 以上便是我在此次使用 Docker 镜像来运行我们 Nebula Studio 所用到的一些优化镜像体积的方法,希望能给需要的人一些帮助和参考,可能还有一些认识不准确的地方,欢迎指出,同样欢迎你来试用 Nebula Graph Studio:https://github.com/vesoft-inc/nebula-web-docker
2010 年前后,对于社交媒体网络研究的兴起带动了图计算的大规模应用。2000 年前后热门的是 信息检索 和 分析 ,主要是 Google 的带动,以及 Amazon 的 e-commerce 所用的协同过滤推荐,当时 collaborative filtering也被认为是 information retrieval 的一个细分领域,包括 Google 的 PageRank 也是在信息检索领域研究较多。后来才是 Twitter,Facebook 的崛起带动了网络科学 network science的研究。图理论和图算法不是新科学,很早就有,只是最近 20 年大数据,网络零售和社交网络的发展, big data、social networks、e-commerce 、IoT让图计算有了新的用武之地,而且硬件计算力的提高和分布式计算日益成熟的支持也使图在高效处理海量关联数据成为可能。 上文摘录了#聊聊图数据库和图数据库小知识# Vol.01 的【图数据库兴起的契机】,在本次第二期#聊聊图数据库和图数据库小知识#我们将了解以下内容,如果有感兴趣的图数据库话题,欢迎添加 Nebula 小助手微信号:NebulaGraphbot 为好友进群来交流图数据库心得。 本文目录 图数据库和图数据库设计 传统数据库通过设计良好的数据结构是不是可以实现图数据库的功能 图数据库会出于什么考虑做存储计算分离 数据量小,业务量小的情况下,是否单机部署图数据库性能也不错。 图数据库 shared-storage 和 shared-nothing 的比较 图数据库顶点和边输出及超级顶点输出优化 如何处理图数据库中大数据量的点? Nebula Graph 实践细节 Nebula Graph 元数据(Meta Service)使用 etcd 吗? Nebula Graph Cache 位于那一层 Nebula Graph 集群中的 Partition 多大 如何理解 Nebula Graph Partition 图数据库和图数据库设计 在这个部分,我们会摘录一些图数据库设计通用的设计思路,或者已有图数据库的实践思考。 传统数据库通过设计良好的数据结构是不是可以实现图数据库的功能 图数据库相对传统数据库优化点在于,数据模型。传统数据库是一个关系型,是一种表结构做 Join,但存储结构表明了很难支持多度拓展,比如一度好友,两度好友,一度还支持,使用一个 Select 和 Join 就可完成,但是二度查询开销成本较大,更别提多度 Join 查询开销更大。图数据库的存储结构为面向图存储,更利于查询多度关系。特别的,有些图上特有的操作,用关系型数据库比较难实现和表达,比如最短路径、子图、匹配特定规则的路径这些。 图数据库会出于什么考虑做存储计算分离 存储与计算分离主要出于以下四方面的考虑: 存储和计算资源可以独立扩展,使资源利用更充分,达到缩减成本的目的。 更容易利用异构机型。 解耦计算节点,计算资源可以更大程度地做到线性扩展。基于之前的项目经历,存储计算不分离的分布式架构,计算能力的水平扩展会比较不方便。举个例子,在好友关系这种场景——基于好友关系查询再做一些排序和计算,在某个节点查询执行过程中需要去其他节点获取数据,或者将某个子计算交给其他节点,如果执行过程中需要的数据存储在本地,相较存储计算分离效率可能会高;但当涉及到和其他节点通信问题时,为了扩容计算资源而增加的机器会使得计算过程中的网络开销相应增加,抵消了相当一部分的计算能力。如果存储计算分离,计算和存储一对一,不存在节点越多网络通讯开销越大的问题。 Nebula Graph在存储层提供基于图的查询接口,但不具备运算能力,方便对接外部的批量计算,比如 Spark,可以将图存储层当作为图索引存储,直接批量扫描、遍历图自行计算,这样操作更灵活。存储层支持做一些简单的过滤计算,比如找寻 18 岁好友等过滤操作。 数据量小,业务量小的情况下,是否单机部署图数据库性能也不错。 单机按分布式架构部署,有一定网络开销因为经过网卡,所以性能还行。一定要分布式架构部署的原因在于强一致、多副本的业务需求,你也可以按照业务需求部署单副本。 图数据库 Shared-storage 和 Shared-nothing 的比较 【提问】对于图数据库来说,是不是 shared-storage 相比 shared-nothing 方式更好呢。因为图的节点间是高度关联的,shared-nothing 方式将这种联系拆掉了。对于多步遍历等操作来说,需要跨节点。而且由于第二步开始的不确定性,要不要跨节点好像没法提前通过执行计划进行优化。 【回复】交流群群友 W:errr,单个 storage 能不能放下那么多数据,特别数据量增加了会是个比较麻烦的问题。另外第二步虽然是随机的,但是取第二步数据的时候可以从多台机器并发 【回复】交流群群友 W:主要的云厂商,比如 AWS 的共享存储可以到 64 T,存储应该够,而且这种方式存储内部有多副本。顺便提一句:AWS 的 Neptune 的底层存储用的也是 Aurora 的那个存储。网络这块的优化,可参考阿里云的 Polarstore,基本上网络已经不是什么问题了。 此外,“第二步虽然是随机的,但是取第二步数据的时候可以从多台机器并发吧”这个倒是,Nebula 有个 storage server 可以做这个事情,但逻辑上似乎这个应该是 query engine 应该干的。 【回复】交流群群友 W:“网络这块的优化,可参考阿里云的 polarstore,基本上网络已经不是什么问题了。” 网络这问题,部署环境的网络不一定可控,毕竟机房质量参差不齐,当然云厂商自己的机房会好很多。 【回复】交流群群友 W:这个确实。所以开源的创业公司走 shared-nothing,云厂商走 shared-storage,也算是都利用了各自的优势 【回复】交流群群友 S:其实,shared-storage 可以被看成是一个存储空间极大的单机版,并不是一个分布式系统 【回复】交流群群友 W:嗯嗯。不过 Neptune 那种跟单机版的区别在于计算那部分是可扩展的,一写多读,甚至 Aurora 已经做到了多写。不过就像前面所说的,开源的东西基于那种需定制的高大上存储来做不现实。想了下拿 AWS 的存储做对比不大合适,存储内部跨网络访问的开销还是一个问题,拿 Polarstore 这种 RDMA 互联更能说明。可能 2 种方案都差不多,本质上都是能否减少跨网络访问的开销。 图数据库顶点和边输出及超级顶点输出优化 【提问】请教一个问题。Nebula 的顶点和出边是连续存放的。那么在查询语句进行 IO 读取时,是顶点和出边会一起读出来呢,还是根据查询语句的不同而不同? 【回复】交流群群友 W:会一个 block 一起读出来 【回复】交流群群友 W:恩恩。对于 supernode 这种情况,有没有做什么优化?Titian/Janusgraph 有一个节点所有边的局部索引,Neo4j 应该有在 object cache 中对一个节点的边按照类型组织 【回复】交流群群友 S:Nebula 也是用 index 来解决这个问题 【回复】交流群群友 W:Neo4j 的 relationship group 是落在存储上的,请教下,Nebula 的这种 index 和 Janusgraph 的 vertex centric 索引类似麽,还是指存储格式里面的 ranking 字段啊 【回复】交流群群友 S:类似于 Janusgraph 的索引,但是我们的设计更 general,支持 multi-column 的索引 【回复】交流群群友 W:ranking 字段其实给客户用的,一般可以拿来放时间戳,版本号之类的。 如何处理图数据库中大数据量的点? 【提问】:Nebula 的存储模型中属性和边信息一起存储在顶点上,针对大顶点问题有好的解决方案吗?属性和关系多情况下,针对这种实体的查询该怎么处理,比如:比如美国最有名的特产,中国最高的人,浙江大学年龄最大的校友 【回复】交流群群友 W:如果可以排序,那分数可以放在 key 上,这样其实也不用 scan 太多了,ranking 字段就可以放这个。要不然还有个办法是允许遍历的过程中截断或者采样,不然很容易爆炸的。 【回复】交流群群友 B:在做实时图数据库的数据模型设计时,尽量避免大出入度的节点。如果避免不了,那至少避免此类节点再往后的 traversal。如果还是避免不了,那别指望这样的查询会有好的性能 【回复】交流群群友 H:单纯的大点如果不从它开始 traversal,其实问题也不大。load 的 unbalance 都是有办法解决的。数据的 unbalance 因为分 part 存储,在分配 part 到 host 时可加入 part 数据量的权重,而 load 的 unbalance,对于读,可通过拓展只读副本 + cache 解决,写的话,可做 group commit,client 也可以做本地 cache。 【回复】交流群群友 B:图数据库的一个查询的性能最终取决于 physical block reads 的数量。不同方案会导致最后 block reads 不一样,性能会有差别。任何一种方案不可能对所有查询都是优化的,最终往往就是 tradeoff。主要看你大部分实际的 query pattern 和数据分布式如何,该数据库实现是否有优化。拆边和不拆边,各有其优缺点。 Nebula Graph 实践细节 在这个部分我们会摘录一些开源的分布式图数据库 Nebula Graph 在实践过程中遇到的问题,或者用户使用图数据库 Nebula Graph 中遇到的问题。 Nebula Graph 元数据(Meta Service)使用 etcd 吗? 不是。Meta Service的架构其实和 Storage Service 架构类似,是个独立服务。 Nebula Graph Cache 位于哪一层 A:KV 那层。目前只有针对顶点的 Cache,顶点的访问具有随机性,如果没有 Cache,性能较差。Query Plan 那层现在还没有。 如何理解 Nebula Graph Partition partition 是个逻辑概念,主要目的是为了一个 partition 内的数据可以一起迁移到另外一台机器。partition 数量是由创建图空间时指定的 partition_num 确立。而单副本 partition 的分布规则如下 通过算子:partID%engine_size,而多副本的情况,原理类似,follower 在另外两个机器上。 Nebula Graph 集群中的 Partition 多大 A:部署集群时需要设置 Partition 数,比如 1000 个 Partition。插入某个点时,会针对这个点的id做 Hash,找寻对应的 Partition 和对应 Leader。PartitionID 计算公式 = VertexID % num_Partition 单个 Partition 的大小,取决于总数据量和 Partition 个数;Partition 的个数的选择取决于集群中最大可能的机器节点数,Partition 数目越大,每个 Partition 数据量越小,机器间数据量不均匀发生的概率也就越小。 推荐阅读 #聊聊图数据库和图数据库小知识# Vol.01
在本篇文章中主要介绍图数据库 Nebula Graph 在 Jepsen 这块的实践。 Jepsen 简介 Jepsen 是一款用于系统测试的开源软件库,致力于提高分布式数据库、队列、共识系统等的安全性。作者 Kyle Kingsbury 使用函数式编程语言 Clojure 编写了这款测试框架,并对多个著名的分布式系统和数据库进行了一致性测试。目前 Jepsen 仍在 GitHub 保持活跃,能否通过 Jepsen 的测试已经成为各个分布式数据库对自身检验的一个标杆。 Jepsen 的测试流程 Jepsen 测试推荐使用 Docker 搭建集群。默认情况下由 6 个 container 组成,其中一个是控制节点(control node),另外 5 个是数据库的节点(默认为 n1-n5)。控制节点在测试程序开始后会启用多个 worker 进程,并发地通过 SSH 登入数据库节点进行读写操作。 测试开始后,控制节点会创建一组进程,进程包含了待测试分布式系统的客户端。另一个 Generator 进程产生每个客户端执行的操作,并将操作应用于待测试的分布式系统。每个操作的开始和结束以及操作结果记录在历史记录中。同时,一个特殊进程 Nemesis 将故障引入系统。 测试结束后,Checker 分析历史记录是否正确,是否符合一致性。用户可以使用 Jepsen 的 knossos 中提供的验证模型,也可以自己定义符合需求的模型对测试结果进行验证。同时,还可以在测试中注入错误对集群进行干扰测试。 最后根据本次测试所规定的验证模型对结果进行分析。 如何使用 Jepsen 使用 Jepsen 过程中可能会遇到一些问题,可以参考一下使用 Tips: 在 Jepsen 框架中,用户需要在 DB 接口中对自己的数据库定义下载,安装,启动与终止操作。在终止后,可以将 log 文件清除,同时也可以指定 log 的存储位置,Jepsen 会将其拷贝至 Jepsen 的 log 文件夹中,以便连同 Jepsen 自身的 log 进行分析。 用户还需要提供访问自己数据库的客户端,这个客户端可以是你用 Clojure 实现的,比如 etcd 的verschlimmbesserung,也可以是 JDBC,等等。然后需要定义 Client 接口,告诉 Jepsen 如何对你的数据库进行操作。 在 Checker 中,你可以选择需要的测试模型,比如,性能测试(checker/perf)将会生成 latency 和整个测试过程的图表,时间轴(timeline/html)会生成一个记录着所有操作时间轴的 html 页面。 另外一个不可或缺的组件就是在 nemesis 中注入想要测试的错误了。网络分区(nemesis/partition-random-halves)和杀掉数据节点(kill-node)是比较常见的注入错误。 在 Generator 中,用户可以告知 worker 进程需要生成哪些操作,每一次操作的时间间隔,每一次错误注入的时间间隔等等。 用 Jepsen 测试图数据库 Nebula Graph 分布式图数据库 Nebula Graph 主要由 3 部分组成,分别是 meta 层,graph 层和 storage 层。 我们在使用 Jepsen 对 kv 存储接口进行的测试中,搭建了一个由 8 个 container 组成的集群:一个 Jepsen 的控制节点,一个 meta 节点,一个 graph 节点,和 5 个 storage 节点,集群由 Docker-compose 启动。需要注意的是,要建立一个集群的 subnet 网络,使集群可以连通,另外要安装 ssh 服务,并为 control node 与 5 个 storage 节点配置免密登入。 测试中使用了 Java 编写的客户端程序,生成 jar 包并加入到 Clojure 程序依赖,来对 DB 进行 put,get 和 cas (compare-and-set) 操作。另外 Nebula Graph 的客户端有自动重试逻辑,当遇到错误导致操作失败时,客户端会启用适当的重试机制以尽力确保操作成功。 Nebula-Jepsen 的测试程序目前分为三种常见的测试模型和三种常见的错误注入。 Jepsen 测试模型 single-register 模拟一个寄存器,程序并发地对数据库进行读写操作,每次成功的写入操作都会使寄存器中存储的值发生变化,然后通过对比每次从数据库读出的值是否和寄存器中记录的值一致,来验证结果是否满足线性要求。由于寄存器是单一的,所以在此处我们生成唯一的 key,随机的 value 进行操作。 multi-register 一个可以存不同键的寄存器。和单一寄存器的效果一样,但此处我们可以使 key 也随机生成了。 4 :invoke :write [[:w 9 1]] 4 :ok :write [[:w 9 1]] 3 :invoke :read [[:r 5 nil]] 3 :ok :read [[:r 5 3]] 0 :invoke :read [[:r 7 nil]] 0 :ok :read [[:r 7 2]] 0 :invoke :write [[:w 7 1]] 0 :ok :write [[:w 7 1]] 1 :invoke :read [[:r 1 nil]] 1 :ok :read [[:r 1 4]] 0 :invoke :read [[:r 8 nil]] 0 :ok :read [[:r 8 3]] :nemesis :info :start nil :nemesis :info :start [:isolated {"n5" #{"n2" "n1" "n4" "n3"}, "n2" #{"n5"}, "n1" #{"n5"}, "n4" #{"n5"}, "n3" #{"n5"}}] 1 :invoke :write [[:w 4 2]] 1 :ok :write [[:w 4 2]] 2 :invoke :read [[:r 5 nil]] 3 :invoke :write [[:w 1 2]] 2 :ok :read [[:r 5 3]] 3 :ok :write [[:w 1 2]] 0 :invoke :read [[:r 4 nil]] 0 :ok :read [[:r 4 2]] 1 :invoke :write [[:w 6 4]] 1 :ok :write [[:w 6 4]] 以上片段是截取的测试中一小部分不同的读写操作示例, 其中最左边的数字是执行这次操作的 worker,也就是进程号。每发起一次操作,标志都是 invoke,接下来一列会指出是 write 还是 read操作,而之后一列的中括号内,则显示了具体的操作,比如 :invoke :read [[:r 1 nil]]就是读取 key 为 1 的值,因为是 invoke,操作刚刚开始,还不知道值是什么,所以后面是 nil。 :ok :read [[:r 1 4]] 中的 ok 则表示操作成功,可以看到读取到键 1 对应的值是 4。 在这个片段中,还可以看到一次 nemesis 被注入的时刻。 :nemesis :info :start nil 标志着 nemesis 的开始,后面的的内容 (:isolated ...) 表示了节点 n5 从整个集群中被隔离,无法与其他 DB 节点进行网络通信。 cas-register 这是一个验证 CAS 操作的寄存器。除了读写操作外,这次我们还加入了随机生成的 CAS 操作,cas-register 将会对结果进行线性分析。 0 :invoke :read nil 0 :ok :read 0 1 :invoke :cas [0 2] 1 :ok :cas [0 2] 4 :invoke :read nil 4 :ok :read 2 0 :invoke :read nil 0 :ok :read 2 2 :invoke :write 0 2 :ok :write 0 3 :invoke :cas [2 2] :nemesis :info :start nil 0 :invoke :read nil 0 :ok :read 0 1 :invoke :cas [1 3] :nemesis :info :start {"n1" ""} 3 :fail :cas [2 2] 1 :fail :cas [1 3] 4 :invoke :read nil 4 :ok :read 0 同样的,在这次测试中,我们采用唯一的键值,比如所有写入和读取操作都是对键 "f" 执行,在显示上省略了中括号中的键,只显示是什么值。 :invoke :read nil 表示开始一次读取 “f” 的值的操作,因为刚开始操作,所以结果是 nil(空)。 :ok :read 0 表示成功读取到了键 “f” 的值为 0。 :invoke :cas [1 2] 意思是进行 CAS 操作,当读到的值为 1 时,将值改为 2。 在第二行可以看到,当保存的 value 是 0 时,在第 4 行 cas[0 2] 会将 value 变为 2。在第 14 行当值为 0时,17 行的 cas[2 2] 就失败了。 第 16 行显示了 n1 节点被杀掉的操作,第 17、18 行会有两个 cas 失败(fail) Jepsen 错误注入 kill-node Jepsen 的控制节点会在整个测试过程中,多次随机 kill 某一节点中的数据库服务而使服务停止。此时集群中就少了一个节点。然后在一定时间后再将该节点的数据库服务启动,使之重新加入集群。 partition-random-node Jepsen 会在测试过程中,多次随机将某一节点与其他节点网络隔离,使该节点无法与其他节点通信,其他节点也无法和它通信。然后在一定时间后再恢复这一网络隔离,使集群恢复原状。 partition-random-halves 在这种常见的网络分区情景下,Jepsen 控制节点会将 5 个 DB 节点随机分成两部分,一部分为两个节点,另一部分为三个。一定时间后恢复通信。如下图所示。 测试结束后 Jepsen 会根据需求对测试结果进行分析,并得出本次测试的结果,可以看到控制台的输出,本次测试是通过的。 2020-01-08 03:24:51,742{GMT} INFO [jepsen test runner] jepsen.core: {:timeline {:valid? true}, :linear {:valid? true, :configs ({:model {:value 0}, :last-op {:process 0, :type :ok, :f :write, :value 0, :index 597, :time 60143184600}, :pending []}), :analyzer :linear, :final-paths ()}, :valid? true} Everything looks good! ヽ(‘ー`)ノ 自动生成的 timeline.html 文件 Jepsen 在测试执行过程中会自动生成一个名为 timeline.html 文件,以下为本次实践生成的 timeline.html 文件部分截图 上面的图片展示了测试中执行操作的时间轴片段,每个执行块有对应的执行信息,Jepsen 会将整个时间轴生成一个 HTML 文件。 Jepsen 就是这样按照顺序的历史操作记录进行 Linearizability 一致性验证,这也是 Jepsen 的核心。我们也可以通过这个 HTML 文件来帮助我们溯源错误。 Jepsen 生成的性能分析图 下面是一些 Jepsen 生成的性能分析图表,本次实践项目名为「basic-test」各位读者阅读时请自行脑补为你项目名。 可以看到,这一张图表展示了 Nebula Graph 的读写操作延时。其中上方灰色的区域是错误注入的时段,在本次测试我们注入了随机 kill node。 而在这一张图展示了读写操作的成功率,我们可以看出,最下方红色集中突出的地方为出现失败的地方,这是因为 control node 在杀死节点时终止了某个 partition 的 leader 中的 nebula 服务。集群此时需要重新选举,在选举出新的 leader 之后,读写操作也恢复到正常了。 通过观察测试程序运行结果和分析图表,可以看到 Nebula Graph 完成了本次在单寄存器模型中注入 kill-node 错误的测试,读写操作延时也均处于正常范围。 小结 Jepsen 本身也存在一些不足,比如测试无法长时间运行,因为大量数据在校验阶段会造成 Out of Memory。 但在实际场景中,许多 bug 需要长时间的压力测试、故障模拟才能发现,同时系统的稳定性也需要长时间的运行才能被验证。但与此同时,在使用 Jepsen 对 Nebula Graph 进行测试的过程中,我们也发现了一些之前没有遇到过的 Bug,甚至其中一些在使用中可能永远也不会出现。 目前,我们已经在日常开发过程中使用 Jepsen 对 Nebula Graph 进行测试。Nebula Graph 有代码更新后,每晚都将编译好的项目发布在 Docker Hub 中,Nebula-Jepsen 将自动下拉最新的镜像进行持续测试。 参考文献 Jepsen 主页:https://jepsen.io/ Jepsen GitHub:https://github.com/jepsen-io/jepsen Raft Understandable Distributed Consensus The Raft Consensus Algorithm Nebula Graph GitHub:https://github.com/vesoft-inc/nebula
1 概述 1.1 需求背景 图数据库 Nebula Graph 在生产环境中将拥有庞大的数据量和高频率的业务处理,在实际的运行中将不可避免的发生人为的、硬件或业务处理错误的问题,某些严重错误将导致集群无法正常运行或集群中的数据失效。当集群处于无法启动或数据失效的状态时,重新搭建集群并重新倒入数据都将是一个繁琐并耗时的工程。针对此问题,Nebula Graph 提供了集群 snapshot 的创建功能。 Snapshot 功能需要预先提供集群在某个时间点 snapshot 的创建功能,以备发生灾难性问题时用历史 snapshot 便捷地将集群恢复到一个可用状态。 1.2 术语 本文主要会用到以下术语: StorageEngine:Nebula Graph 的最小物理存储单元,目前支持 RocksDB 和 HBase,在本文中只针对 RocksDB。 Partition:Nebula Graph 的最小逻辑存储单元,一个 StorageEngine 可包含多个 Partition。Partition 分为 leader 和 follower 的角色,Raftex 保证了 leader 和 follower 之间的数据一致性。 GraphSpace:每个 GraphSpace 是一个独立的业务 Graph 单元,每个 GraphSpace 有其独立的 tag 和 edge 集合。一个 Nebula Graph 集群中可包含多个 GraphShpace。 checkpoint:针对 StorageEngine 的一个时间点上的快照,checkpoint 可以作为全量备份的一个 backup 使用。checkpoint files是 sst files 的一个硬连接。 snapshot:本文中的 snapshot 是指 Nebula Graph 集群的某个时间点的快照,即集群中所有 StorageEngine 的 checkpoint 的集合。通过 snapshot 可以将集群恢复到某个 snapshot 创建时的状态。 wal:Write-Ahead Logging ,用 raftex 保证 leader 和 follower 的一致性。 2 系统构架 2.1 系统整体架构 2.2 存储系统结构关系 2.3 存储系统物理文件结构 [bright2star@hp-server storage]$ tree . └── nebula └── 1 ├── checkpoints │ ├── SNAPSHOT_2019_12_04_10_54_42 │ │ ├── data │ │ │ ├── 000006.sst │ │ │ ├── 000008.sst │ │ │ ├── CURRENT │ │ │ ├── MANIFEST-000007 │ │ │ └── OPTIONS-000005 │ │ └── wal │ │ ├── 1 │ │ │ └── 0000000000000000233.wal │ │ ├── 2 │ │ │ └── 0000000000000000233.wal │ │ ├── 3 │ │ │ └── 0000000000000000233.wal │ │ ├── 4 │ │ │ └── 0000000000000000233.wal │ │ ├── 5 │ │ │ └── 0000000000000000233.wal │ │ ├── 6 │ │ │ └── 0000000000000000233.wal │ │ ├── 7 │ │ │ └── 0000000000000000233.wal │ │ ├── 8 │ │ │ └── 0000000000000000233.wal │ │ └── 9 │ │ └── 0000000000000000233.wal │ └── SNAPSHOT_2019_12_04_10_54_44 │ ├── data │ │ ├── 000006.sst │ │ ├── 000008.sst │ │ ├── 000009.sst │ │ ├── CURRENT │ │ ├── MANIFEST-000007 │ │ └── OPTIONS-000005 │ └── wal │ ├── 1 │ │ └── 0000000000000000236.wal │ ├── 2 │ │ └── 0000000000000000236.wal │ ├── 3 │ │ └── 0000000000000000236.wal │ ├── 4 │ │ └── 0000000000000000236.wal │ ├── 5 │ │ └── 0000000000000000236.wal │ ├── 6 │ │ └── 0000000000000000236.wal │ ├── 7 │ │ └── 0000000000000000236.wal │ ├── 8 │ │ └── 0000000000000000236.wal │ └── 9 │ └── 0000000000000000236.wal ├── data 3 处理逻辑分析 3.1 逻辑分析 Create snapshot 由 client api 或 console 触发, graph server 对 create snapshot 的 AST 进行解析,然后通过 meta client 将创建请求发送到 meta server 。 meta server 接到请求后,首先会获取所有的 active host ,并创建 adminClient 所需的 request 。通过 adminClient 将创建请求发送到每个 StorageEngine ,StorageEngine 收到 create 请求后,会遍历指定 space 的全部 StorageEngine,并创建 checkpoint ,随后对 StorageEngine 中的全部 partition 的 wal 做 hardlink。在创建 checkpoint 和 wal hardlink 时,因为已经提前向所有 leader partition 发送了 write blocking 请求,所以此时数据库是只读状态的。 因为 snapshot 的名称是由系统的 timestamp 自动生成,所以不必担心 snapshot 的重名问题。如果创建了不必要的 snapshot,可以通过 drop snapshot 命令删除已创建的 snapshot。 3.2 Create Snapshot 3.3 Create Checkpoint 4 关键代码实现 4.1 Create Snapshot folly::Future<Status> AdminClient::createSnapshot(GraphSpaceID spaceId, const std::string& name) { // 获取所有storage engine的host auto allHosts = ActiveHostsMan::getActiveHosts(kv_); storage::cpp2::CreateCPRequest req; // 指定spaceId,目前是对所有space做checkpoint,list spaces 工作已在调用函数中执行。 req.set_space_id(spaceId); // 指定 snapshot name,已有meta server根据时间戳产生。 // 例如:SNAPSHOT_2019_12_04_10_54_44 req.set_name(name); folly::Promise<Status> pro; auto f = pro.getFuture(); // 通过getResponse接口发送请求到所有的storage engine. getResponse(allHosts, 0, std::move(req), [](auto client, auto request) { return client->future_createCheckpoint(request); }, 0, std::move(pro), 1 /*The snapshot operation only needs to be retried twice*/); return f; } 4.2 Create Checkpoint ResultCode NebulaStore::createCheckpoint(GraphSpaceID spaceId, const std::string& name) { auto spaceRet = space(spaceId); if (!ok(spaceRet)) { return error(spaceRet); } auto space = nebula::value(spaceRet); // 遍历属于本space中的所有StorageEngine for (auto& engine : space->engines_) { // 首先对StorageEngine做checkpoint auto code = engine->createCheckpoint(name); if (code != ResultCode::SUCCEEDED) { return code; } // 然后对本StorageEngine中的所有partition的last wal做hardlink auto parts = engine->allParts(); for (auto& part : parts) { auto ret = this->part(spaceId, part); if (!ok(ret)) { LOG(ERROR) << "Part not found. space : " << spaceId << " Part : " << part; return error(ret); } auto walPath = folly::stringPrintf("%s/checkpoints/%s/wal/%d", engine->getDataRoot(), name.c_str(), part); auto p = nebula::value(ret); if (!p->linkCurrentWAL(walPath.data())) { return ResultCode::ERR_CHECKPOINT_ERROR; } } } return ResultCode::SUCCEEDED; } 5 用户使用帮助 5.1 CREATE SNAPSHOT CREATE SNAPSHOT 即对整个集群创建当前时间点的快照,snapshot 名称由 meta server 的 timestamp 组成。 在创建过程中可能会创建失败,当前版本不支持创建失败的垃圾回收的自动功能,后续将计划在 metaServer 中开发 cluster checker 的功能,将通过异步线程检查集群状态,并自动回收 snapshot 创建失败的垃圾文件。 当前版本如果 snapshot 创建失败,必须通过 DROP SNAPSHOT 命令清除无效的 snapshot。 当前版本不支持对指定的 space 做 snapshot,当执行 CREATE SNAPSHOT 后,将对集群中的所有 space 创建快照。CREATE SNAPSHOT 语法: CREATE SNAPSHOT 以下为笔者创建 3 个 snapshot 的例子: (user@127.0.0.1) [default_space]> create snapshot; Execution succeeded (Time spent: 28211/28838 us) (user@127.0.0.1) [default_space]> create snapshot; Execution succeeded (Time spent: 22892/23923 us) (user@127.0.0.1) [default_space]> create snapshot; Execution succeeded (Time spent: 18575/19168 us) 我们用 5.3 提及的 SHOW SNAPSHOTS 命令看下现在有的快照 (user@127.0.0.1) [default_space]> show snapshots; =========================================================== | Name | Status | Hosts | =========================================================== | SNAPSHOT_2019_12_04_10_54_36 | VALID | 127.0.0.1:77833 | ----------------------------------------------------------- | SNAPSHOT_2019_12_04_10_54_42 | VALID | 127.0.0.1:77833 | ----------------------------------------------------------- | SNAPSHOT_2019_12_04_10_54_44 | VALID | 127.0.0.1:77833 | ----------------------------------------------------------- Got 3 rows (Time spent: 907/1495 us) 从上 SNAPSHOT_2019_12_04_10_54_36 可见 snapshot 名同 timestamp 有关。 5.2 DROP SNAPSHOT DROP SNAPSHOT 即删除指定名称的 snapshot,可以通过 SHOW SNAPSHOTS 命令获取 snapshot 的名称,DROP SNAPSHOT 既可以删除有效的 snapshot,也可以删除创建失败的 snapshot。 语法: DROP SNAPSHOT name 笔者删除了 5.1 成功创建的 snapshot SNAPSHOT_2019_12_04_10_54_36 ,并用SHOW SNAPSHOTS 命令查看现有的 snapshot。 (user@127.0.0.1) [default_space]> drop snapshot SNAPSHOT_2019_12_04_10_54_36; Execution succeeded (Time spent: 6188/7348 us) (user@127.0.0.1) [default_space]> show snapshots; =========================================================== | Name | Status | Hosts | =========================================================== | SNAPSHOT_2019_12_04_10_54_42 | VALID | 127.0.0.1:77833 | ----------------------------------------------------------- | SNAPSHOT_2019_12_04_10_54_44 | VALID | 127.0.0.1:77833 | ----------------------------------------------------------- Got 2 rows (Time spent: 1097/1721 us) 5.3 SHOW SNAPSHOTS SHOW SNAPSHOTS 可查看集群中所有的 snapshot,可以通过 SHOW SNAPSHOTS 命令查看其状态(VALID 或 INVALID)、名称、和创建 snapshot 时所有 storage Server 的 ip 地址。语法: SHOW SNAPSHOTS 以下为一个小示例: (user@127.0.0.1) [default_space]> show snapshots; =========================================================== | Name | Status | Hosts | =========================================================== | SNAPSHOT_2019_12_04_10_54_36 | VALID | 127.0.0.1:77833 | ----------------------------------------------------------- | SNAPSHOT_2019_12_04_10_54_42 | VALID | 127.0.0.1:77833 | ----------------------------------------------------------- | SNAPSHOT_2019_12_04_10_54_44 | VALID | 127.0.0.1:77833 | ----------------------------------------------------------- Got 3 rows (Time spent: 907/1495 us) 6 注意事项 当系统结构发生变化后,最好立刻 create snapshot,例如 add host、drop host、create space、drop space、balance 等。 当前版本暂未提供用户指定 snapshot 路径的功能,snapshot 将默认创建在 data_path/nebula 目录下。 当前版本暂未提供 snapshot 的恢复功能,需要用户根据实际的生产环境编写 shell 脚本实现。实现逻辑也比较简单,拷贝各 engineServer 的 snapshot 到指定的文件夹下,并将此文件夹设置为 data_path,启动集群即可。 7 附录 最后,附上 Nebula Graph GitHub 地址:https://github.com/vesoft-inc/nebula 如果你在使用 Nebula Graph 过程中遇到任何问题,欢迎 GitHub 联系我们或者加入微信交流群,请联系微信号:NebulaGraphbot
前言 本文由 Nebula Graph 实习生@王杰贡献。 最近 @Yener 开源了史上最大规模的中文知识图谱——OwnThink(链接:https://github.com/ownthink/KnowledgeGraphData ),数据量为 1.4 亿条。 本文介绍如何将这份数据快速导入图数据库 Nebula Graph,全过程大约需要 30 分钟。 中文知识图谱 OwnThink 简介 思知(OwnThink) 知识图谱是由 Google 在 2012 年提出来的一个概念。主要是用来描述真实世界中存在的各种实体和概念,以及他们之间的关系。在搜索引擎、问答机器人、知识抽取等多个领域有着诸多应用。 最近 Yener 开源了史上最大规模的中文知识图谱—— OwnThink(链接:https://github.com/ownthink/KnowledgeGraphData),数据量为 1.4 亿条。数据以 (实体, 属性, 值) 和 (实体, 关系, 实体) 混合的三元组形式存储,数据格式为 csv。 可以点击这里下载:https://nebula-graph.oss-accelerate.aliyuncs.com/ownthink/kg_v2.tar.gz 查看原始文件 由于 ownthink_v2.csv 数据过多,摘录部分数据为例: 红色食品,描述,红色食品是指食品为红色、橙红色或棕红色的食品。 红色食品,是否含防腐剂,否 红色食品,主要食用功效,预防感冒,缓解疲劳 红色食品,用途,增强表皮细胞再生和防止皮肤衰老 大龙湫,描述,雁荡山景区分散,东起羊角洞,西至锯板岭;南起筋竹溪,北至六坪山。 大龙湫,中文名称,大龙湫 大龙湫,外文名称,big dragon autrum 大龙湫,门票价格,50元 大龙湫,著名景点,芙蓉峰 姚明[中国篮球协会主席、中职联公司董事长],妻子,叶莉 这里的 (红色食品,是否含防腐剂,否) 就是典型的 (实体, 属性, 值) 形式的三元组数据; 而 (姚明[中国篮球协会主席、中职联公司董事长],妻子,叶莉) 是典型的 (实体, 关系, 实体) 形式的三元组数据。 Step 1. 数据建模与清洗准备 建模 Nebula Graph 是一个开源的分布式图数据库(链接:https://github.com/vesoft-inc/nebula),相比 Neo4j 来说,它的主要特点是完全的分布式,因此图数据库 Nebula Graph 适合处理数据量超过单机的场景。 图数据库通常支持的数据模型为有向属性图(directed property graph)。图中的每个顶点(vertex)可以用标签(tag)来表示类型(Neo4j 叫做 Label),顶点和顶点之间的关系用边(edge)连接起来。每种 tag 和 edge 还可以带有属性。——然而,这些功能对于知识图谱的三元组数据没什么意义: 分析上图的三元组数据,发现无论是 (实体, 属性, 值) 形式的三元组数据,还是 (实体, 关系, 实体) 形式的三元组数据,每条三元组数据均可以建模成两个点和一条边的形式。前者三元组中的“实体”和“值”建模为两个点(起点、终点),“属性”建模为一条边,后者三元组中的两个“实体”也建模为两个点(起点、终点),“关系”建模为一条边. 而且,所有的点都是相同类型(取名叫entity ),只需要一个属性(叫 name ),所有的边也都是同一类型(取名叫 relation ),边上也只有一个属性(叫 name )。 比如 (大龙湫,著名景点,芙蓉峰) 可以表示成下图这个样子: 数据清洗和预处理 按照前一节的分析,原始的每条三元组数据,还需要清洗转换为两个点和一条边才能变成属性图的模型。 下载清洗工具 本文测试的时候,使用的操作系统是 CentOS 7.5,工具由 Golang 语言编写而成。 你可以在这里 (链接:https://github.com/jievince/rdf-converter) 下载这个简单的清洗工具源代码并编译使用。 该工具会把转换后的顶点的数据写入到 vertex.csv 文件、边数据写入到 edge.csv 文件。 说明:在测试过程中,发现有大量的重复点数据,所以工具里面也做了去重。完全去重后的点的数据大概是 4600 万条,完全去重后的边的数据大概是 1 亿 4000 万条。 清洗完的 vertex.csv 文件长这样: -2469395383949115281,过度包装 -5567206714840433083,Over Package 3836323934884101628,有的商品故意增加包装层数 1185893106173039861,很多采用实木、金属制品 3455734391170888430,非科学 9183164258636124946,教育 5258679239570815125,成熟市场 -8062106589304861485,"成熟市场是指低增长率,高占有率的市场。" 说明:每一行是一个顶点,第一列整型 -2469395383949115281 是顶点的 ID(叫做 VID),它是由第二列文字通过 hash 计算出来的,例如 -2469395383949115281 就是由 std::hash("过度包装") 计算出来的值。 清洗完的 edge.csv 文件: 3413383836870836248,-948987595135324087,含义 3413383836870836248,8037179844375033188,定义 3413383836870836248,-2559124418148243756,标签 3413383836870836248,8108596883039039864,标签 2587975790775251569,-4666568475926279810,描述 2587975790775251569,2587975790775251569,中文名称 2587975790775251569,3771551033890875715,外文名称 2587975790775251569,2900555761857775043,地理位置 2587975790775251569,-1913521037799946160,占地面积 2587975790775251569,-1374607753051283066,开放时间 说明:第一列是起点的 VID,第二列是终点的 VID,第三列是这条边的"属性"或者"描述"。 在本机完全去重的清洗程序运行时间大约是 6 分钟。 Step 2. Nebula Graph 启动准备 下载和安装 登陆 GitHub 后,在这里 (链接:https://github.com/vesoft-inc/nebula/actions) 找到 Nebula 的安装包。 找到你所用系统对应的下载链接: 笔者系统是 CentOS 7.5,下载 CentOS 7.5 最新的压缩包,解压后能找到 rpm 安装包 nebula-5ace754.el7-5.x86_64.rpm,注意 5ace754 是 git commit 号,使用时可能会有所不同。下载好后解压,输入下面命令进行安装,记得替换成新的 git commit: $ rpm -ivh nebula-5ace754.el7-5.x86_64.rpm 启动 Nebula Graph 服务 在 命令行 CLI 输入下面命令启动服务 $ /usr/local/nebula/scripts/nebula.service start all 命令执行结果如下: 可以执行以下命令检查服务是否成功启动 $ /usr/local/nebula/scripts/nebula.service status all 命令执行结果如下: 连接 Nebula Graph 服务 输入下面命令连接 Nebula Graph: $ /usr/local/nebula/bin/nebula -u user -p password 命令执行结果如下: 准备 schema 等元数据 Nebula Graph 的使用风格有点接近 MySQL,需要先准备各种元信息。 新建图空间 space create space 的概念接近 MySQL 里面 create database。在 nebula console 里面输入下面这个命令。 nebula> CREATE SPACE test; 进入 test space nebula> USE test; 创建点类型(entity) nebula> CREATE TAG entity(name string); 创建边类型 (relation) nebula> CREATE EDGE relation(name string); 最后简单确认下元数据是不是正确。 查看 entity 标签的属性: nebula> DESCRIBE TAG entity; 结果如下: 查看 relation 边类型的属性: nebula> DESCRIBE EDGE relation; 结果如下: Step 3. 使用 nebula-importer 导入数据 登陆 GitHub 进入 https://github.com/vesoft-inc/nebula-importer ,nebula-importer 这个工具也是 Golang 语言写的,在这里下载并编译源代码。 另外,准备一个 YAML 配置文件,告诉这个 importer 工具去哪里找 csv 文件。(可直接复制下面这段) version: v1rc1 description: example clientSettings: concurrency: 10 # number of graph clients channelBufferSize: 128 space: test connection: user: user password: password address: 127.0.0.1:3699 logPath: ./err/test.log files: - path: ./vertex.csv failDataPath: ./err/vertex.csv batchSize: 100 type: csv csv: withHeader: false withLabel: false schema: type: vertex vertex: tags: - name: entity props: - name: name type: string - path: ./edge.csv failDataPath: ./err/edge.csv batchSize: 100 type: csv csv: withHeader: false withLabel: false schema: type: edge edge: name: relation withRanking: false props: - name: name type: string 说明:测试时候发现 csv 数据文件中有大量转义字符 (\) 和换行字符 (\r),nebula-importer 也做了处理。 最后:开始导入数据 go run importer.go --config ./config.yaml 执行过程如下: 可以看到, 本次导入 QPS 大约在 40 w/s。全部导入总耗时大约 15 min。 Step 4. 随便读点什么试试 导入完毕后,我们可以使用 Nebula Graph 服务做一些简单的查询。回到 Nebula Graph 的命令行 CLI : $ /usr/local/nebula/bin/nebula -u user -p password 进入刚才导入的三元组数据的 test 空间: nebula> USE test; 现在,我们可以做一些简单查询 例 1:与姚明有直接关联的边的类型和点的属性 (user@127.0.0.1) [test]> GO FROM hash("姚明[中国篮球协会主席、中职联公司董事长]") OVER relation YIELD relation.name AS Name, $$.entity.name AS Value; 执行结果如下: 可以看到:本次查询返回 51 条数据,耗时 3 ms 左右; 例2:查询姚明和其妻子叶莉在三跳之内的所有路径 (user@127.0.0.1) [test]> FIND ALL PATH FROM hash("姚明[中国篮球协会主席、中职联公司董事长]") TO hash("叶莉") OVER relation UPTO 3 STEPS; 执行结果如下: 当数据量较大时,查找全路径/最短经之类的操作会比较耗时。可以看到:本次查询返回 8 条数据,说明姚明和其妻子叶莉在三跳之内共有 8 条直接或间接的关系。 总结 本篇文章涉及到的一些概念和链接: OwnThink 的中文知识图谱数据:https://github.com/ownthink/KnowledgeGraphData。它的数据以三元组形式保存为 csv Nebula Graph 是一个开源的图数据库,GitHub 地址:https://github.com/vesoft-inc/nebula,和 Neo4j 相比,它是分布式的 数据清洗工具,GitHub 地址:https://github.com/jievince/rdf-converter。因为原始的图谱 ownthink_v2.csv 数据以三元组形式保存,并和一般图数据库的属性图模型略微有些不同,所以写了一个 Go 语言工具将原始 ownthink_v2.csv 变成 vertex.csv 和 edge.csv 数据导入工具,GitHub 地址:https://github.com/vesoft-inc/nebula-importer。将清洗完的 vertex.csv 和 edge.csv 批量写入到 Nebula Graph。 后面的工作 调整 Nebula 的参数。似乎默认的日志级别和内存都不是很好,可以用下面这个命令关闭日志,这样导入性能可以好很多。 curl "http://127.0.0.1:12000/set_flags?flag=minloglevel&value=4" 写个对应的 Python 版本示例 附录 Nebula Graph GitHub 地址:https://github.com/vesoft-inc/nebula ,加入 Nebula Graph 交流群,请联系 Nebula Graph 官方小助手微信号:NebulaGraphbot
摘要 上文(存储篇)说到数据库重要的两部分为存储和计算,本篇内容为你解读图数据库 Nebula 在查询引擎 Query Engine 方面的设计实践。 在 Nebula 中,Query Engine 是用来处理 Nebula 查询语言语句(nGQL)。本篇文章将带你了解 Nebula Query Engine 的架构。 上图为查询引擎的架构图,如果你对 SQL 的执行引擎比较熟悉,那么对上图一定不会陌生。Nebula 的 Query Engine 架构图和现代 SQL 的执行引擎类似,只是在查询语言解析器和具体的执行计划有所区别。 Session Manager Nebula 权限管理采用基于角色的权限控制(Role Based Access Control)。客户端第一次连接到 Query Engine 时需作认证,当认证成功之后 Query Engine 会创建一个新 session,并将该 session ID 返回给客户端。所有的 session 统一由 Session Manger 管理。session 会记录当前 graph space 信息及对该 space 的权限。此外,session 还会记录一些会话相关的配置信息,并临时保存同一 session 内的跨多个请求的一些信息。 客户端连接结束之后 session 会关闭,或者如果长时间没通信会切为空闲状态。这个空闲时长是可以配置的。客户端的每个请求都必须带上此 session ID,否则 Query Engine 会拒绝此请求。 Storage Engine 不管理 session,Query Engine 在访问存储引擎时,会带上 session 信息。 Parser Query Engine 解析来自客户端的 nGQL 语句,分析器(parser)主要基于著名的 flex / bison 工具集。字典文件(lexicon)和语法规则(grammar)在 Nebula 源代码的 src/parser 目录下。设计上,nGQL 的语法非常接近 SQL,目的是降低学习成本。 图数据库目前没有统一的查询语言国际标准,一旦 ISO/IEC 的图查询语言(GQL)委员会发布 GQL 国际标准,nGQL 会尽快去实现兼容。Parser 构建产出的抽象语法树(Abstrac Syntax Tree,简称 AST)会交给下一模块:Execution Planner。 Execution Planner 执行计划器(Execution Planner)负责将抽象树 AST 解析成一系列执行动作 action(可执行计划)。action 为最小可执行单元。例如,典型的 action 可以是获取某个节点的所有邻节点,或者获得某条边的属性,或基于特定过滤条件筛选节点或边。当抽象树 AST 被转换成执行计划时,所有 ID 信息会被抽取出来以便执行计划的复用。这些 ID 信息会放置在当前请求 context 中,context 也会保存变量和中间结果。 Optimization 经由 Execution Planner 产生的执行计划会交给执行优化框架 Optimization,优化框架中注册有多个 Optimizer。Optimizer 会依次被调用对执行计划进行优化,这样每个 Optimizer都有机会修改(优化)执行计划。最后,优化过的执行计划可能和原始执行计划完全不一样,但是优化后的执行结果必须和原始执行计划的结果一样的。 Execution Query Engine 最后一步是去执行优化后的执行计划,这步是执行框架(Execution Framework)完成的。执行层的每个执行器一次只处理一个执行计划,计划中的 action 会挨个一一执行。执行器也会一些有针对性的局部优化,比如:决定是否并发执行。针对不同的 action所需数据和信息,执行器需要经由 meta service 与storage engine的客户端与他们通信。 最后,如果你想尝试编译一下 Nebula 源代码可参考如下方式: 有问题请在 GitHub(GitHub 地址:https://github.com/vesoft-inc/nebula) 或者微信公众号上留言,也可以添加 Nebula 小助手微信号:NebulaGraphbot 为好友反馈问题~ 推荐阅读 Nebula 架构剖析系列(零)图数据库的整体架构设计 Nebula 架构剖析系列(一)图数据库的存储设计
摘要 笔者最近在重新整理和编译 Nebula Graph 的第三方依赖,选出两个比较有意思的问题给大家分享一下。 Flex Segmentation Fault——Segmentation fault (core dumped) 在编译 Flex 过程中,遇到了 Segmentation fault: make[2]: Entering directory '/home/dutor/flex-2.6.4/src' ./stage1flex -o stage1scan.c ./scan.l make[2]: *** [Makefile:1696: stage1scan.c] Segmentation fault (core dumped) 使用 gdb 查看 coredump: Core was generated by `./stage1flex -o stage1scan.c ./scan.l'. Program terminated with signal SIGSEGV, Segmentation fault. #0 flexinit (argc=4, argv=0x7ffd25bea718) at main.c:976 976 action_array[0] = '\0'; (gdb) disas Dump of assembler code for function flexinit: 0x0000556c1b1ae040 <+0>: push %r15 0x0000556c1b1ae042 <+2>: lea 0x140fd(%rip),%rax # 0x556c1b1c2146 ... 0x0000556c1b1ae20f <+463>: callq 0x556c1b1af460 <allocate_array> #这里申请了buffer ... => 0x0000556c1b1ae24f <+527>: movb $0x0,(%rax) # 这里向buffer[0]写入一个字节,地址非法,挂掉了 ... (gdb) disas allocate_array Dump of assembler code for function allocate_array: 0x0000556c1b1af460 <+0>: sub $0x8,%rsp 0x0000556c1b1af464 <+4>: mov %rsi,%rdx 0x0000556c1b1af467 <+7>: xor %eax,%eax 0x0000556c1b1af469 <+9>: movslq %edi,%rsi 0x0000556c1b1af46c <+12>: xor %edi,%edi 0x0000556c1b1af46e <+14>: callq 0x556c1b19a100 <reallocarray@plt> # 调用库函数申请内存 0x0000556c1b1af473 <+19>: test %eax,%eax # 判断是否为 NULL 0x0000556c1b1af475 <+21>: je 0x556c1b1af47e <allocate_array+30># 跳转至NULL错误处理 0x0000556c1b1af477 <+23>: cltq # 将 eax 符号扩展至 rax,造成截断 0x0000556c1b1af479 <+25>: add $0x8,%rsp 0x0000556c1b1af47d <+29>: retq ... End of assembler dump. 可以看到,问题出在了 allocate_array 函数。因为 reallocarray 返回指针,返回值应该使用 64 bit 寄存器rax,但 allocate_array 调用 reallocarray 之后,检查的却是 32 bit 的 eax,同时使用 cltq 指令将 eax 符号扩展 到 rax。原因只有一个:allocate_array 看到的 reallocarray 的原型,与 reallocarry 的实际定义不符。翻看编译日志,确实找到了 implicit declaration of function 'reallocarray' 相关的警告。configure 阶段添加 CFLAGS=-D_GNU_SOURCE 即可解决此问题。 注:此问题不是必现,但编译/链接选项 -pie 和 内核参数 kernel.randomize_va_space 有助于复现。 总结: 隐式声明的函数在 C 中,返回值被认为是 int。 关注编译器告警,-Wall -Wextra 要打开,开发模式下最好打开 -Werror。 GCC Illegal Instruction——internal compiler error: Illegal instruction 前阵子,接到用户反馈,在编译 Nebula Graph 过程中遭遇了编译器非法指令的错误,详见(#978)[https://github.com/vesoft-inc/nebula/issues/978] 错误信息大概是这样的: Scanning dependencies of target base_obj_gch [ 0%] Generating Base.h.gch In file included from /opt/nebula/gcc/include/c++/8.2.0/chrono:40, from /opt/nebula/gcc/include/c++/8.2.0/thread:38, from /home/zkzy/nebula/nebula/src/common/base/Base.h:15: /opt/nebula/gcc/include/c++/8.2.0/limits:1599:7: internal compiler error: Illegal instruction min() _GLIBCXX_USE_NOEXCEPT { return FLT_MIN; } ^~~ 0xb48c5f crash_signal ../.././gcc/toplev.c:325 Please submit a full bug report, with preprocessed source if appropriate. 既然是 _internal compiler error_,想必是 g++ 本身使用了非法指令。为了定位具体的非法指令集及其所属模块,我们需要复现这个问题。幸运的是,下面的代码片段就能触发: #include <thread> int main() { return 0; } 非法指令一定会触发 SIGILL,又因为 g++ 只是编译器的入口,真正干活的是 cc1plus。我们可以使用 gdb 来运行编译命令,抓住子进程使用非法指令的第一现场: $ gdb --args /opt/nebula/gcc/bin/g++ test.cpp gdb> set follow-fork-mode child gdb> run Starting program: /opt/nebula/gcc/bin/g++ test.cpp [New process 31172] process 31172 is executing new program: /opt/nebula/gcc/libexec/gcc/x86_64-pc-linux-gnu/8.2.0/cc1plus Thread 2.1 "cc1plus" received signal SIGILL, Illegal instruction. [Switching to process 31172] 0x00000000013aa0fb in __gmpn_mul_1 () gdb> disas ... 0x00000000013aa086 <+38>: mulx (%rsi),%r10,%r8 ... Bingo!mulx 属于 BMI2 指令集,报错机器 CPU 不支持该指令集。仔细调查,引入该指令集的是 GCC 的依赖之一,GMP。默认情况下,GMP 会在 configure 阶段探测当前机器的 CPU 具体类型,以期最大化利用 CPU 的扩展指令集,提升性能,但却牺牲了二进制的可移植性。解决方法是,在 configure 之前,使用代码目录中的 configfsf.guess configfsf.sub 替换或者覆盖默认的 config.guess 和 config.sub 总结: 某些依赖可能因为性能或者配置的原因,造成二进制的不兼容。 缺省参数下,GCC 为了兼容性,不会使用较新的指令集。 为了平衡兼容性和性能,你需要做一些额外的工作,比如像 glibc 那样在运行时选择和绑定某个具体实现。 最后,如果你想尝试编译一下 Nebula 源代码可参考以下方式: bash> git clone https://github.com/vesoft-inc/nebula.git bash> cd nebula && ./build_dep.sh N 有问题请在 GitHub 或者微信公众号上留言。 附录 Nebula Graph:一个开源的分布式图数据库 GitHub:https://github.com/vesoft-inc/nebula 官方博客:https://nebula-graph.io/cn/posts/ 微博:weibo.com/nebulagraph
11 月 2 号 - 11 月 3 号,以“大爱无疆,开源无界”为主题的 2019 中国开源年会(COSCon'19)正式启动,大会以开源治理、国际接轨、社区发展和开源项目为切入点同全球开源爱好者们共同交流开源。 作为图数据库技术的代表,Nebula Graph 总监——吴敏在本次大会上将会讲述了大规模分布式图数据库设计思考和实践。在信息爆发式增长和内容平台遍地开花的信息时代,图数据库在当中扮演了什么样的角色?同传统数据库相比,图数据库又有什么优势?图数据库开发需要哪些新技术?就此,开源社特访吴敏来分享下图数据库主题内容,从图数据 Nebula 的研发开始,就传统数据库面临的挑战,开源模式的优势,Nebula 的社区开展和产品规划等问题进行深入解析。 About Nebula 总监--吴敏 开源社:Hi,吴敏,先和大家介绍下自己。 大家好,我是吴敏,VEsoft 总监,博士毕业于浙江大学。 曾就职于阿里云、蚂蚁金服,从事分布式图数据库以及云存储相关工作。 开源社:谈谈您在 COSCon'19 上的分享话题。 随着抖音、小红书等社交内容平台的爆红诞生了一种基于社交关系网路的推荐需求,而以垂直领域作为切入点的知识图谱过去两年的“爆火”,传统数据库在处理社交推荐、风控、知识图谱方面的性能缺陷,图数据库的研发应运而生。 本演讲开篇将陈述图数据库行业现状,让你对图数据库存储的数据及对场景有所了解,再从开源的分布式图数据库 Nebula Graph 切入深度讲解大规模分布式图数据库应该如何设计存储、计算及架构,最后讲述开源对图数据库开发的影响。 内容大纲 图数据库概述及应用 Nebula Graph 设计介绍 技术细节 开源社区及服务 开源社:哪些人可以应该了解这个内容? 对图数据库有兴趣,或是有推荐、风控、知识图谱等业务场景需求的人。 Nebula 研发之旅 开源社:为什么给图数据库取名 Nebula ? Nebula 是星云的意思,很大嘛,也是漫威宇宙里面漂亮的星云小姐姐。对了,Nebula的发音是:[ˈnɛbjələ] 开源社:现在数据库领域百花齐放,国产的 OceanBase 和 TiDB 都发展得不错,为什么还要研发 Nebula 这样的图数据库? OceanBase、TiDB 这类 NewSQL 最近发展势头很强劲,他们的出现更多的是对传统单机的关系型数据库在可用性的补充。Nebula 聚焦在图数据库这一领域,也是近年来在数据库各分支中增长最为快速的领域。图数据库使用图(或者网)的方式很直接、自然的表达现实世界的关系: 用节点来表示实体,边来表示关联关系,everything is connected。能高效的提供图检索,提供专业的分析算法、工具,比如 ShortestPath、PageRank、标签传播等等。 开源社:图数据库应用场景有哪些? 典型的应用场景有社交网络,金融风控,推荐引擎,知识图谱等。 社交网络,比如,推荐一条最短路径让我结识迪纳热巴,还可以加上筛选条件,路径中的每个人都是单身女性。 金融风控场景,比如,去查一个信用卡反套现的网络。很典型的一个场景,A 转账到 B,B 转账到 C,C 又转回给 A 即是一个典型的闭环。对于这样的闭环,这类查询在图数据库大规模应用之前,大部分都是采用离线计算的方式去查找,但是离线场景很难去控制当前发生的这笔交易。一个信用卡交易或者在线贷款,整个作业流程很长,而在反套现这块的审核时间又限制在毫秒级,这就是图数据库非常大的一个应用场景。 在推荐算法中,为某个人推荐他的好友。现在的方案是去找好友的好友,判断好友的好友有没有可能成为某人的新好友,这当中涉及好友关系的亲密度,抵达好友的好友的最短路径等。业务方可能会用 MySQL 等传统数据库或是 HBase 来存各类好友关系,然后通过多个串行的 Key-Value 来做查询,但这在线上场景是很难满足性能要求的。 知识图谱这些年非常火,知识图谱结合自然语言的形式在金融,医疗,互联网等众多领域被广泛使用,常见的有语音助手、聊天机器人、智能问答等应用场景。而图数据库存储的数据结构完全适配知识图谱数据,图谱中的实体对应图数据库的点,实体与实体的关系对应图数据库的边,拿 Nebula 为例,Nebula Graph Schema 采用属性图,点边上的属性对应图谱实体和关系中的属性,边的方向表示了关系的方向,边上的标记表示了关系的类型。 再说到最近国内非常火的区块链场景,由于区块链上的所有行为都是公开被记录的又是不可篡改的,因此所有的交易行为,不管是历史数据,还是大概每几分钟产生的新 block,都可以对 DAT 文件解析后导入到图数据库和 GNN 中做分析。例如我们都听说在一些数字货币场景下,洗钱、盗窃、团伙、操纵市场的各类事情很多,通过图的手段包括可以帮助我们挖掘里面的非法行为。 开源社:作为图数据库,有参考借鉴了哪些数据库吗?哪些方面是 Nebula 有特点的设计? Nebula 是完全自主研发的数据库,它主要有以下的技术特点 存储计算分离 对于 Nebula Graph 来讲,有这么几个技术特点:第一个就是采用了存储计算分离的架构,主要好处就是为了上云或者说弹性,方便单独扩容。业务水位总是很难预测的,一段时间存储不够了,有些时候计算不够了。在云上或者使用容器技术,计算存储分离的架构运维起来会比较方便,成本也更好控制。大家使用 HBase 那么久,这方面的感触肯定很多。 查询语言 nGQL Nebula Graph 的第二个技术特点是它的查询语言,我们称为 nGQL,比较接近 SQL。唯一大一点的语法差异就是 不用嵌套 (embedding)。大家都知道嵌套的 SQL,读起来是非常痛苦的,要从里向外读。另外,由于图这块目前并没有统一的国际标准,这对整个行业的发展并不是好事,用户的学习成本很高。目前有个 ISO / IEC 组织在准备图语言的国际标准,我们也在积极兼容标准。 支持多种后端存储 第三个特点就是 Nebula Graph 支持多种后端存储,除了原生的引擎外,也支持 HBase。因为很多用户,对 HBase 已经相当熟悉了,并不希望多一套存储架构。从架构上来说,Nebula Graph 是完全对等的分布式系统。 计算下推 和 HBase 的 CoProcessor 一样,Nebula Graph 支持数据计算下推。数据过滤,包括一些简单的聚合运算,能够在存储层就做掉,这样对于性能来讲能提升会非常大。 多租户 多租户,Nebula Graph是通过多 Space 来实现的。Space 是物理隔离。 索引 除了图查询外,还有很常见的一种场景是全局的属性查询。这个和 MySQL 一样,要提升性能的主要办法是为属性建立索引 ,这个也是 Nebula Graph 原生支持的功能。 图算法 最后的技术特点就是关于图算法方面。这里的算法和全图计算不太一样,更多是一个子图的计算,比如最短路径。大家知道数据库通常有 OLTP 和 OLAP 两种差异很大的场景,当然现在有很多 HTAP 方面的努力。那对于图数据库来说也是类似,我们在设计 Nebula Graph 的时候,做了一些权衡。我们认为全图的计算,比如 Page Rank,LPA,它的技术挑战和 OLTP 的挑战和对应的设计相差很大。所以 Nebula 的查询引擎主要针对 OLTP 类的场景。那么,对于 OLAP 类的计算需求,我们的考虑是通过支持和 Spark 的相互访问,来支持 Spark 上图计算,比如 graphX。这块工作正在开发中,应该在最近一两个月会发布。 开源社:为什么会考虑存储计算分离的架构呢? 存储计算分离是个很热的话题。我们将存储模块和 Query Engine 层分开主要有以下考虑。 成本的原因。存储和计算对计算机资源要求不一样,存储依赖 I/O,计算对 CPU 和内存的要求更高,业务在不同的应用或者发展时期,需要不同的存储空间和计算能力配比,存储和计算的耦合会使得机器的选型会比较复杂,存储计算分离的架构,使得 storage 的 scale out/in 更容易。 存储层抽象出来可以给计算带来新的选择,比如对接 Pregel, Spark GraphX 这些计算引擎。通常来说,图计算对于存储的要求是吞吐量优先的,而在线查询是时延优先的。通过把存储层分离出来,不管是开发的时候(做 QoS )还是运维的时候(单独集群部署),都会更容易一些。 在云计算场景下,能实现真正的弹性计算。 开源社:作为一个分布式数据库,是如何保障数据一致性的? 我们使用 Raft 协议,Raft 一致性协议使得 share-nothing 的 kv 有一致性保障。为什么选择 Raft?相对于 Paxos,Raft 更加有利于工程化实现。Nebula 存储层 Raft 使用 Multi-Raft 的模型,多个 replica 上的同一个 partition 组成一个 Raft 组,同一个集群内存在互相独立 Raft 组,在一致性保障的同时,提高了系统的并发能力。 开源社:在数据库的优化方面,Nebula 做了哪些? Nebula 在数据优化方面主要做了以下工作: 异步和并发执行:由于 IO 和网络均为长时延操作,Nebula Graph 采用异步及并发操作。此外,为避免一些大query 的长尾影响,为每个 query 设置单独的资源池以保证服务质量 QoS。 计算下沉:为避免存储层将过多数据回传到计算层,占用宝贵带宽,条件过滤等算子会随查询条件一同下发到存储层节点。 数据库系统的优化与数据的物理存储方式以及数据的分布息息相关。而且随着业务的发展,数据分布是会发生变化的,一开始设计的索引和数据存储或者分区会慢慢变得不是最优的,这就需要系统能够做一些动态的调整。我们 storage 支持 scale out/in, load balance。系统的调整会带来 overhead,这是需要权衡考虑的问题。 开源社:现在市面上已有一些图数据库,Nebula 考虑兼容部分数据库让已有的用户无缝切到 Nebula 吗? Nebula 有 CSV、HDFS 批量 数据导入工具。用户可以将数仓的数据导入到 Nebula。也提供 C++,Java,Golang,Python 的客户端。另外对于市面上已有的一些产品,现在也正在开发将它的数据格式直接解析为 Nebula 的数据格式,这样就可以非常方便的迁移,包括查询语言层面的兼容。 开源社:水平伸缩能够支持多大的规模? 存储层 share-nothing 的架构,理论上支持无限加机器。 开源社:Nebula 最新的版本 RC1 支持最短路径和全路径算法,可以具体讲下这块的实现,及以后的研发规划吗? 目前实现较为简单,基于双向搜索,返回点边组合的路径。未来规划是计划在执行计划与优化器都完成后,完善对路径的支持,包括实现 match,支持双向 bfs、双向 dijkstra、allpair(全路径),kshortest 等。当然我们欢迎社区的同学们都参与完善 Nebula 的路径算法。 开源社:使用 Nebula 之前,用户应该做哪些准备工作? 对于刚开始使用图数据库的用户,我们提供了详细的文档; 对于已经在使用其他图数据库,想要试试 Nebula 的用户,我们提供了数据导入等工具,有疑问或者任何问题,欢迎在 GitHub 上给我们提 issue,我们的工程师会在第一时间为您解答。 Nebula 和开源 开源社:作为一个企业级产品,为什么 Nebula 一开始就选择了走开源路线? 如果没 Linux,现在互联网的格局也不会是今天这样。我们想要建立图数据库的社区,做出更好的图数据库产品,也希望更多对 Nebula,对图数据库感兴趣的同学成为社区的贡献者,一起努力,共同建立一个互助互利的社区。 开源社:在开源的过程中,有遇到什么困难吗? 很多人都想为开源做一份力,但会被开源项目的门槛“劝退”,尤其是 Nebula 是一个即使耕耘在数据库领域多年的数据库专家,如果对图数据库的不够了解的话,都会感叹“高大上”的一个项目。但技术是为业务服务的,所以 Nebula 力求自己的文档让你即使你对图数据库一无所知,通过 Nebula 的文档也能够了解到图数据库及其应用场景。 开源社:在开源社区搭建这块,有什么可以和开源社小伙伴们分享的吗? 开源项目最重要的是生态的搭建,Nebula Graph 刚开源半年在社区搭建这块只能说略有心得,仅供大家参考 :) 开源社区运营主要从下面几个方面展开 简洁明了的文档:一个好的文档能让使用者快速同产品拉近距离,Nebula 的文档从“让非技术人做技术事”的出发,力求即使你是一个不懂技术的人也可以按照文档部署 Nebula,玩起来——用 Nebula 完成简单的 CRUD,如果开源社的小伙伴阅读过 Nebula 文档觉得哪里有更改意见,欢迎联系我们; 实时的反馈回复:用户的反馈,我们会第一时间进行回复,在 GitHub 的 issue 及用户交流群里进行回复; 同用户直接对话:在线上,Nebula 在各大技术平台同图数据库和 Nebula 爱好者们进行交流,包括 Nebula 架构设计、用户使用实操等系列文章;在线下,我们也开展了主题 Meetup 同各地爱好者交流图数据库技术及 Nebula 的开发心得; 社区用户体系:在 Nebula 的 GitHub 上,现阶段你可以看到 3 种用户,User、Contributor、Committer,User 通过向 Nebula 提 issue / pr 或者投稿等方式成为 Contributor,Contributor 再进阶成为 Committer。配合 Nebula 开展的各类社区活动,eg:捉虫活动,帮助社区用户完成角色“升级”; 最后,打个小广告:欢迎大家来参与到 Nebula 的建设中,为开源贡献一份力 :) 程序员寄语 开源社: 作为资深数据库从业人员,怎样让自己的眼界更加开阔,怎么获取这个领域的最前沿信息? 多看看论文,看看开源分布式系统的设计以及源代码;多关注数据库的的会议,比如,SIGMOD, VLDB,关注学术界的最新成果;多关注业界相关公司的发展和动态,比如 OsceanBase,TiDB。 Nebula 有话说 以上为开源社对图数据库 Nebula 总监——吴敏的采访,欢迎你关注 Nebula GitHub:github.com/vesoft-inc/nebula 了解 Nebula 最新动态或添加 Nebula 小助手为好友进图数据库技术交流群交流,小助手微信号:NebulaGraphbot 推荐阅读 图数据库 Nebula Graph 的数据模型和系统架构设计 Nebula Graph 在 HBaseCon Asia2019 的分享实录 Vol.03 nMeetup | 图数据库综述与 Nebula 在图数据库设计的实践
Nebula Graph:一个开源的分布式图数据库。作为唯一能够存储万亿个带属性的节点和边的在线图数据库,Nebula Graph 不仅能够在高并发场景下满足毫秒级的低时延查询要求,还能够实现服务高可用且保障数据安全性。 图数据库 Nebula RC1 主要更新 本次 RC1 主要增强了 nGQL,新增 LIMIT , GROUP BY 等语句;算法方面增加了最短路径,全路径搜索。 Storage 层新增 PUT/GET 接口,支持 scale out/in,以及新增了 Golang 客户端以及多线程 Golang 数据导入工具。 nGQL 新增 LIMIT 指定返回的记录数。(#750) 管道操作中支持 YIELD 指定返回类型。(#745) 新增 ORDER BY 对结果集进行排序 (#537) 新增 udf_is_in 来查询特定集合的数据。(#1096) 新增 DELETE VERTEX 删除指定的 vertex 和相关联的出入边。 (#868) 新增 UUID() 函数生成唯一值. (#958, #961, #1031) 支持逻辑运算符 XOR、OR、AND 和 NOT. (#858) 支持 TIMESTAMP 数据类型. (#843) 针对 STRING 数据类型,支持更多函数操作,比如 upper(), trim(), lower(), substr() 等. (#841) 逻辑运算中支持类型转换 (#964) 新增 SHOW CONFIGS 获取指定服务 [meta/storage/graph] 的配置项, GET CONFIGS 获取指定配置项值 and UPDATE CONFIGS 修改配置项值. (#504) SHOW HOSTS 新增 Leader 信息。(#918) 支持 FIND PATH 最短路径、全路径搜索 (#847) GO 支持多个 edge types 的图查询。(#699) 优化了源代码的编译流程。 (#1047, #948, #1083) Storage 新增 PUT/GET 接口. (#977) 支持 Leader balance. (#731, #881) 支持 HTTP API 获取性能指标。(比如 QPS, Latency AVG/ P99/ P999 等) (#872, #1136) 支持 Scaling out/in。 (#421, #444, #795, #881, #998) Meta client 支持重试, 默认是 3 次. (#814) Tools 新增 Golang Importer 工具,支持多线程从 CSV 导入数据. Change 更改了 storaged 的配置模板 Others 将 metad , storaged 和 graphd 拆到不同 Image 中。增加 Dockerfiles 创建镜像。 (#923) 新增 Golang 客户端,将客户端移到各自的 Repositories 下(vesoft-inc/nebula-go 和 vesoft-inc/nebula-java)。 Coming Soon 支持数据 Snapshot, 数据回滚功能 增强从 Hive 数据导入功能 Storage 层支持集群缩容 引入 CI/CD 附录 最后是 Nebula 的 GitHub 地址,欢迎大家试用,有什么问题可以向我们提 issue。GitHub 地址:https://github.com/vesoft-inc/nebula;加入 Nebula Graph 交流群,请联系 Nebula Graph 官方小助手微信号:NebulaGraphbot
摘要 在讨论某个数据库时,存储 ( Storage ) 和计算 ( Query Engine ) 通常是讨论的热点,也是爱好者们了解某个数据库不可或缺的部分。每个数据库都有其独有的存储、计算方式,今天就和图图来学习下图数据库 Nebula Graph 的存储部分。 Nebula 的 Storage 包含两个部分, 一是 meta 相关的存储, 我们称之为 Meta Service ,另一个是 data 相关的存储, 我们称之为 Storage Service。 这两个服务是两个独立的进程,数据也完全隔离,当然部署也是分别部署, 不过两者整体架构相差不大,本文最后会提到这点。 如果没有特殊说明,本文中 Storage Service 代指 data 的存储服务。接下来,大家就随我一起看一下 Storage Service 的整个架构。 Let's go~ Architecture 图一 storage service 架构图 如图1 所示,Storage Service 共有三层,最底层是 Store Engine,它是一个单机版 local store engine,提供了对本地数据的 get / put / scan / delete 操作,相关的接口放在 KVStore / KVEngine.h 文件里面,用户完全可以根据自己的需求定制开发相关 local store plugin,目前 Nebula 提供了基于 RocksDB 实现的 Store Engine。 在 local store engine 之上,便是我们的 Consensus 层,实现了 Multi Group Raft,每一个 Partition 都对应了一组 Raft Group,这里的 Partition 便是我们的数据分片。目前 Nebula 的分片策略采用了 静态 Hash 的方式,具体按照什么方式进行 Hash,在下一个章节 schema 里会提及。用户在创建 SPACE 时需指定 Partition 数,Partition 数量一旦设置便不可更改,一般来讲,Partition 数目要能满足业务将来的扩容需求。 在 Consensus 层上面也就是 Storage Service 的最上层,便是我们的 Storage interfaces,这一层定义了一系列和图相关的 API。 这些 API 请求会在这一层被翻译成一组针对相应 Partition 的 kv 操作。正是这一层的存在,使得我们的存储服务变成了真正的图存储,否则,Storage Service 只是一个 kv 存储罢了。而 Nebula 没把 kv 作为一个服务单独提出,其最主要的原因便是图查询过程中会涉及到大量计算,这些计算往往需要使用图的 schema,而 kv 层是没有数据 schema 概念,这样设计会比较容易实现计算下推。 Schema & Partition 图存储的主要数据是点和边,但 Nebula 存储的数据是一张属性图,也就是说除了点和边以外,Nebula 还存储了它们对应的属性,以便更高效地使用属性过滤。 对于点来说,我们使用不同的 Tag 表示不同类型的点,同一个 VertexID 可以关联多个 Tag,而每一个 Tag 都有自己对应的属性。对应到 kv 存储里面,我们使用 vertexID + TagID 来表示 key, 我们把相关的属性编码后放在 value 里面,具体 key 的 format 如图2 所示: 图二 Vertex Key Format Type : 1 个字节,用来表示 key 类型,当前的类型有 data, index, system 等 Part ID : 3 个字节,用来表示数据分片 Partition,此字段主要用于 Partition 重新分布(balance) 时方便根据前缀扫描整个 Partition 数据 Vertex ID : 4 个字节, 用来表示点的 ID Tag ID : 4 个字节, 用来表示关联的某个 tag Timestamp : 8 个字节,对用户不可见,未来实现分布式事务 ( MVCC ) 时使用 在一个图中,每一条逻辑意义上的边,在 Nebula Graph 中会建模成两个独立的 key-value,分别称为 out-key 和in-key。out-key 与这条边所对应的起点存储在同一个 partition 上,in-key 与这条边所对应的终点存储在同一个partition 上。通常来说,out-key 和 in-key 会分布在两个不同的 Partition 中。 两个点之间可能存在多种类型的边,Nebula 用 Edge Type 来表示边类型。而同一类型的边可能存在多条,比如,定义一个 edge type "转账",用户 A 可能多次转账给 B, 所以 Nebula 又增加了一个 Rank 字段来做区分,表示 A 到 B 之间多次转账记录。 Edge key 的 format 如图3 所示: 图三 Edge Key Format Type : 1 个字节,用来表示 key 的类型,当前的类型有 data, index, system 等。 Part ID : 3 个字节,用来表示数据分片 Partition,此字段主要用于 Partition 重新分布(balance) 时方便根据前缀扫描整个 Partition 数据 Vertex ID : 4 个字节, 出边里面用来表示源点的 ID, 入边里面表示目标点的 ID。 Edge Type : 4 个字节, 用来表示这条边的类型,如果大于 0 表示出边,小于 0 表示入边。 Rank : 8 个字节,用来处理同一种类型的边存在多条的情况。用户可以根据自己的需求进行设置,这个字段可_存放交易时间_、_交易流水号_、或_某个排序权重_ Vertex ID : 4 个字节, 出边里面用来表示目标点的 ID, 入边里面表示源点的 ID。 Timestamp : 8 个字节,对用户不可见,未来实现分布式做事务的时候使用。 针对 Edge Type 的值,若如果大于 0 表示出边,则对应的 edge key format 如图4 所示;若 Edge Type 的值小于 0,则对应的 edge key format 如图5 所示 图4 出边的 Key Format 图5 入边的 Key Format 对于点或边的属性信息,有对应的一组 kv pairs,Nebula 将它们编码后存在对应的 value 里。由于 Nebula 使用强类型 schema,所以在解码之前,需要先去 Meta Service 中取具体的 schema 信息。另外,为了支持在线变更 schema,在编码属性时,会加入对应的 schema 版本信息,具体的编解码细节在这里不作展开,后续会有专门的文章讲解这块内容。 OK,到这里我们基本上了解了 Nebula 是如何存储数据的,那数据是如何进行分片呢?很简单,对 Vertex ID 取模 即可。通过对 Vertex ID 取模,同一个点的所有_出边_,_入边_以及这个点上所有关联的 _Tag 信息_都会被分到同一个 Partition,这种方式大大地提升了查询效率。对于在线图查询来讲,最常见的操作便是从一个点开始向外 BFS(广度优先)拓展,于是拿一个点的出边或者入边是最基本的操作,而这个操作的性能也决定了整个遍历的性能。BFS 中可能会出现按照某些属性进行剪枝的情况,Nebula 通过将属性与点边存在一起,来保证整个操作的高效。当前许多的图数据库通过 Graph 500 或者 Twitter 的数据集试来验证自己的高效性,这并没有代表性,因为这些数据集没有属性,而实际的场景中大部分情况都是属性图,并且实际中的 BFS 也需要进行大量的剪枝操作。 KVStore 为什么要自己做 KVStore,这是我们无数次被问起的问题。理由很简单,当前开源的 KVStore 都很难满足我们的要求: 性能,性能,性能:Nebula 的需求很直接:高性能 pure kv; 以 library 的形式提供:对于强 schema 的 Nebula 来讲,计算下推需要 schema 信息,而计算下推实现的好坏,是 Nebula 是否高效的关键; 数据强一致:这是分布式系统决定的; 使用 C++实现:这由团队的技术特点决定; 基于上述要求,Nebula 实现了自己的 KVStore。当然,对于性能完全不敏感且不太希望搬迁数据的用户来说,Nebula 也提供了整个KVStore 层的 plugin,直接将 Storage Service 搭建在第三方的 KVStore 上面,目前官方提供的是 HBase 的 plugin。 Nebula KVStore 主要采用 RocksDB 作为本地的存储引擎,对于多硬盘机器,为了充分利用多硬盘的并发能力,Nebula 支持自己管理多块盘,用户只需配置多个不同的数据目录即可。分布式 KVStore 的管理由 Meta Service 来统一调度,它记录了所有 Partition 的分布情况,以及当前机器的状态,当用户增减机器时,只需要通过 console 输入相应的指令,Meta Service 便能够生成整个 balance plan 并执行。(之所以没有采用完全自动 balance 的方式,主要是为了减少数据搬迁对于线上服务的影响,balance 的时机由用户自己控制。) 为了方便对于 WAL 进行定制,Nebula KVStore 实现了自己的 WAL 模块,每个 partition 都有自己的 WAL,这样在追数据时,不需要进行 wal split 操作, 更加高效。 另外,为了实现一些特殊的操作,专门定义了 Command Log 这个类别,这些 log 只为了使用 Raft 来通知所有 replica 执行某一个特定操作,并没有真正的数据。除了 Command Log 外,Nebula 还提供了一类日志来实现针对某个 Partition 的 atomic operation,例如 CAS,read-modify-write, 它充分利用了Raft 串行的特性。 关于多图空间(space)的支持:一个 Nebula KVStore 集群可以支持多个 space,每个 space 可设置自己的 partition 数和 replica 数。不同 space 在物理上是完全隔离的,而且在同一个集群上的不同 space 可支持不同的 store engine 及分片策略。 Raft 作为一个分布式系统,KVStore 的 replication,scale out 等功能需 Raft 的支持。当前,市面上讲 Raft 的文章非常多,具体原理性的内容,这里不再赘述,本文主要说一些 Nebula Raft 的一些特点以及工程实现。 Multi Raft Group 由于 Raft 的日志不允许空洞,几乎所有的实现都会采用 Multi Raft Group 来缓解这个问题,因此 partition 的数目几乎决定了整个 Raft Group 的性能。但这也并不是说 Partition 的数目越多越好:每一个 Raft Group 内部都要存储一系列的状态信息,并且每一个 Raft Group 有自己的 WAL 文件,因此 Partition 数目太多会增加开销。此外,当 Partition 太多时, 如果负载没有足够高,batch 操作是没有意义的。比如,一个有 1w tps 的线上系统单机,它的单机 partition 的数目超过 1w,可能每个 Partition 每秒的 tps 只有 1,这样 batch 操作就失去了意义,还增加了 CPU 开销。实现 Multi Raft Group 的最关键之处有两点, 第一是共享 Transport 层,因为每一个 Raft Group 内部都需要向对应的 peer 发送消息,如果不能共享 Transport 层,连接的开销巨大;第二是线程模型,Mutli Raft Group 一定要共享一组线程池,否则会造成系统的线程数目过多,导致大量的 context switch 开销。 Batch 对于每个 Partition来说,由于串行写 WAL,为了提高吞吐,做 batch 是十分必要的。一般来讲,batch 并没有什么特别的地方,但是 Nebula 利用每个 part 串行的特点,做了一些特殊类型的 WAL,带来了一些工程上的挑战。 举个例子,Nebula 利用 WAL 实现了无锁的 CAS 操作,而每个 CAS 操作需要之前的 WAL 全部 commit 之后才能执行,所以对于一个 batch,如果中间夹杂了几条 CAS 类型的 WAL, 我们还需要把这个 batch 分成粒度更小的几个 group,group 之间保证串行。还有,command 类型的 WAL 需要它后面的 WAL 在其 commit 之后才能执行,所以整个 batch 划分 group 的操作工程实现上比较有特色。 Learner Learner 这个角色的存在主要是为了 应对扩容 时,新机器需要"追"相当长一段时间的数据,而这段时间有可能会发生意外。如果直接以 follower 的身份开始追数据,就会使得整个集群的 HA 能力下降。 Nebula 里面 learner 的实现就是采用了上面提到的 command wal,leader 在写 wal 时如果碰到 add learner 的 command, 就会将 learner 加入自己的 peers,并把它标记为 learner,这样在统计多数派的时候,就不会算上 learner,但是日志还是会照常发送给它们。当然 learner 也不会主动发起选举。 Transfer Leadership Transfer leadership 这个操作对于 balance 来讲至关重要,当我们把某个 Paritition 从一台机器挪到另一台机器时,首先便会检查 source 是不是 leader,如果是的话,需要先把他挪到另外的 peer 上面;在搬迁数据完毕之后,通常还要把 leader 进行一次 balance,这样每台机器承担的负载也能保证均衡。 实现 transfer leadership, 需要注意的是 leader 放弃自己的 leadership,和 follower 开始进行 leader election 的时机。对于 leader 来讲,当 transfer leadership command 在 commit 的时候,它放弃 leadership;而对于 follower 来讲,当收到此 command 的时候就要开始进行 leader election, 这套实现要和 Raft 本身的 leader election 走一套路径,否则很容易出现一些难以处理的 corner case。 Membership change 为了避免脑裂,当一个 Raft Group 的成员发生变化时,需要有一个中间状态, 这个状态下 old group 的多数派与 new group 的多数派总是有 overlap,这样就防止了 old group 或者新 group 单方面做出决定,这就是论文中提到的 joint consensus 。为了更加简化,Diego Ongaro 在自己的博士论文中提出每次增减一个 peer 的方式,以保证 old group 的多数派总是与 new group 的多数派有 overlap。 Nebula 的实现也采用了这个方式,只不过 add member 与 remove member 的实现有所区别,具体实现方式本文不作讨论,有兴趣的同学可以参考 Raft Part class 里面 addPeer / removePeer 的实现。 Snapshot Snapshot 如何与 Raft 流程结合起来,论文中并没有细讲,但是这一部分我认为是一个 Raft 实现里最容易出错的地方,因为这里会产生大量的 corner case。 举一个例子,当 leader 发送 snapshot 过程中,如果 leader 发生了变化,该怎么办? 这个时候,有可能 follower 只接到了一半的 snapshot 数据。 所以需要有一个 Partition 数据清理过程,由于多个 Partition 共享一份存储,因此如何清理数据又是一个很麻烦的问题。另外,snapshot 过程中,会产生大量的 IO,为了性能考虑,我们不希望这个过程与正常的 Raft 共用一个 IO threadPool,并且整个过程中,还需要使用大量的内存,如何优化内存的使用,对于性能十分关键。由于篇幅原因,我们并不会在本文对这些问题展开讲述,有兴趣的同学可以参考 SnapshotManager 的实现。 Storage Service 在 KVStore 的接口之上,Nebula 封装有图语义接口,主要的接口如下: getNeighbors : 查询一批点的出边或者入边,返回边以及对应的属性,并且需要支持条件过滤; Insert vertex/edge : 插入一条点或者边及其属性; getProps : 获取一个点或者一条边的属性; 这一层会将图语义的接口转化成 kv 操作。为了提高遍历的性能,还要做并发操作。 Meta Service 在 KVStore 的接口上,Nebula 也同时封装了一套 meta 相关的接口。Meta Service 不但提供了图 schema 的增删查改的功能,还提供了集群的管理功能以及用户鉴权相关的功能。Meta Service 支持单独部署,也支持使用多副本来保证数据的安全。 总结 这篇文章给大家大致介绍了 Nebula Storage 层的整体设计, 由于篇幅原因, 很多细节没有展开讲, 欢迎大家到我们的微信群里提问,加入 Nebula Graph 交流群,请联系 Nebula Graph 官方小助手微信号:NebulaGraphbot。 相关阅读 Nebula 架构剖析系列(零)图数据库的整体架构设计 附录 Nebula Graph:一个开源的分布式图数据库。 GitHub:https://github.com/vesoft-inc/nebula 官方博客:https://nebula-graph.io/cn/posts/ 微博:https://weibo.com/nebulagraph
Nebula Graph 是一个高性能的分布式开源图数据库,本文为大家介绍 Nebula Graph 的整体架构。 一个完整的 Nebula 部署集群包含三个服务,即 Query Service,Storage Service 和 Meta Service。每个服务都有其各自的可执行二进制文件,这些二进制文件既可以部署在同一组节点上,也可以部署在不同的节点上。 Meta Service 上图为 Nebula Graph 的架构图,其右侧为 Meta Service 集群,它采用 leader / follower 架构。Leader 由集群中所有的 Meta Service 节点选出,然后对外提供服务。Followers 处于待命状态并从 leader 复制更新的数据。一旦 leader 节点 down 掉,会再选举其中一个 follower 成为新的 leader。 Meta Service 不仅负责存储和提供图数据的 meta 信息,如 schema、partition 信息等,还同时负责指挥数据迁移及 leader 的变更等运维操作。 存储计算分离 在架构图中 Meta Service 的左侧,为 Nebula Graph 的主要服务,Nebula 采用存储与计算分离的架构,虚线以上为计算,以下为存储。 存储计算分离有诸多优势,最直接的优势就是,计算层和存储层可以根据各自的情况弹性扩容、缩容。 存储计算分离还带来的另一个优势:使水平扩展成为可能。 此外,存储计算分离使得 Storage Service 可以为多种类型的个计算层或者计算引擎提供服务。当前 Query Service 是一个高优先级的计算层,而各种迭代计算框架会是另外一个计算层。 无状态计算层 现在我们来看下计算层,每个计算节点都运行着一个无状态的查询计算引擎,而节点彼此间无任何通信关系。计算节点仅从 Meta Service 读取 meta 信息,以及和 Storage Service 进行交互。这样设计使得计算层集群更容易使用 K8s 管理或部署在云上。 计算层的负载均衡有两种形式,最常见的方式是在计算层上加一个负载均衡(balance),第二种方法是将计算层所有节点的 IP 地址配置在客户端中,这样客户端可以随机选取计算节点进行连接。 每个查询计算引擎都能接收客户端的请求,解析查询语句,生成抽象语法树(AST)并将 AST 传递给执行计划器和优化器,最后再交由执行器执行。 Shared-nothing 分布式存储层 Storage Service 采用 shared-nothing 的分布式架构设计,每个存储节点都有多个本地 KV 存储实例作为物理存储。Nebula 采用多数派协议 Raft 来保证这些 KV 存储之间的一致性(由于 Raft 比 Paxo 更简洁,我们选用了 Raft )。在 KVStore 之上是图语义层,用于将图操作转换为下层 KV 操作。 图数据(点和边)是通过 Hash 的方式存储在不同 Partition 中。这里用的 Hash 函数实现很直接,即 vertex_id 取余 Partition 数。在 Nebula Graph 中,Partition 表示一个虚拟的数据集,这些 Partition 分布在所有的存储节点,分布信息存储在 Meta Service 中(因此所有的存储节点和计算节点都能获取到这个分布信息)。 附录 Nebula Graph GitHub 地址:https://github.com/vesoft-inc/nebula ,加入 Nebula Graph 交流群,请联系 Nebula Graph 官方小助手微信号:NebulaGraphbot Nebula Graph:一个开源的分布式图数据库。 GitHub:https://github.com/vesoft-inc/nebula 官方博客:https://nebula-graph.io/cn/posts/ 微博:https://weibo.com/nebulagraph
摘要 本文翻译自 CMSWire 网站的《Open Source vs. Open Core: What's the Difference?》,主要介绍 Open Source 和 Open Core 的区别。Open Source 已广为人知,那么 Open Core 又是什么,在开源软件盛行的今天,二者会怎样影响这个市场呢? 开篇之前,我们先回到一个 2013 年 CMSWire 向行业内专家提出一个问题:专有软件( proprietary software )和开源软件 ( open source),哪个更好?当时就这个问题业内人士没有达成共识,而现在这个问题似乎已经失去其存在价值。 开源无处不在 Constellation Research 副总裁兼首席分析师 Holger Mueller 曾表示——“开源无处不在“,而专有软件供应商过去的经历也证实了这一点。开源代码促进了专有软件的发展,反观专有软件也是开源项目的重要贡献者。许多大厂,如:思科,谷歌,IBM,微软,Pivotal,SAP,SUSE 也都是 Cloud Foundry Foundation 的成员,此外, Red Hat 也被归类到开源公司行列, 拥有 4,550 名员工为开源项目贡献代码的微软也不例外。无独有偶,除微软外,亚马逊,IBM 和 SAP 也位列开源代码贡献榜单的前十名。 尽管 Open Source 盛行,大多数软件供应商并不会给自己贴上“Open Source”的标签。这是为什么呢?此外,还有些公司自称“Open Core” 或附加额外许可证以限制其开源代码的使用,比如,Confluent 使用 Confluent Community 许可证而 MongoDB 使用 SSPL 许可证,背后的原因又是什么呢? Open Source 和“免费开源软件”(FOSS)的开发者和爱好者对开源以及非开源的讨论充满热情,他们讨论关于“free”的不同含义,比如“免费软件”(free, as in beer)和“开源软件”(free as in libre)。但对于大多数开发者而言,尤其是面向 GitHub 编程的这类人,他们从 Github 上获取需要的代码为他们所用,却不会关注对应软件的许可证。正如 Mueller 所说,“PM 只在意代码运行结果和开销并不在意开发是如何实现的,因此造成开发者对许可证的不敏感”。 但开发者、PM,或正在阅读本文的你,真的应该去关注许可证吗?来,我们研究下企业在听到“开源”时,他们在想什么。 管理者如何看待 Open Source 作为 Host Analytics、Marklogic 的前首席执行官和 Nuxeo 的董事,软件主管戴夫·凯洛格(Dave Kellogg)说过,人们在面对开源时会混淆两件事:源代码和商业模式。在涉及到源代码时,凯洛格指出需要考虑以下方面: 代码访问:代码是否可见,可获取,可更改等? 代码作者:它是由谁编写的?是开源社区中的成千上万贡献者共同编写,还是来自软件供应商的工程师编写? 比如 Drupal 有来自社区的 114,702 个贡献者,而 MongoDB 99% 的代码是由其员工编写。 说到商业模式,大多数情况下开源软件是“免费的”,假设不是直接从 Apache Software Foundation 或 Eclipse Foundation 这样机构获取所使用的代码,Kellogg 建议我们直接研究开源项目的供应商是如何赚钱的。 开源软件有如下商业模式: 纯服务模式,比如,之前的 Hortonworks (现在的 HDP—— Hortonworks 发行版),用户只需为技术支持及咨询服务买单。 Open Core 模式,比如,大家熟悉的 Elastic,部分产品是免费,而高级版本或附加组件则使用商业许可证(参考:社区版和企业版)。正如凯洛格指出的那般,"开源软件供应商最大的竞争对手往往是他们自己的免费社区版"。 SaaS 模式,比如,Databricks,供应商将其开源软件作为服务托管在云上,通过收取每月/每年的托管和服务费获利。 Gartner 分析师 Nick Heudecker 是这样区分 Open Core 与 Open Source 的:"Open Core 是以 Open Source 为基础的商业产品。Open Source 既是一种开发形式,也是一种源代码的许可方式"。 Heudecker 在博客中提出: Open Source 供应商的核心价值在于不再受供应商的约束。毕竟,产品核心部分是开源的,且由全球社区开发。产品核心部分并不属于某个公司,多数情况是由 Apache Software Foundation(ASF)拥有。在最坏情况下,即使公司倒闭这种最坏的情况发生,核心代码依然安全存在(于) ASF,被 ASF 所支持。 这听起来不错。坦白来讲,这不是事实。这是迈出了第一步并在头一年迅速扩张的好方法。 在 24 或 48 个月的限期后期阶段,供应商需要增加收入,有时他们甚至会大幅提高软件价格。这会导致我和我的同事要接很多客户打来的咨询电话。他们会询问他们实际所使用及有价值的功能,如果不用开源组件外的其他功能,他们能正常使用产品吗?有些时候,这个问题的答案是肯定的 。此外,客户们还会这些疑问:市面上还有谁家是支持开源组件的?它们更便宜吗?他们和我之前合作过的软件供应商用的同一策略吗? 其他软件供应商见机,会咨询我们是否该提供对这些开源组件外的其他组件的简单支持。因为他们的客户也会与他们谈论其他的内部供应商的策略。 凯洛格对这种销售策略有其他的看法,他表示,“从供应商的角度来看,免费/社区版本既是潜在客户的主要来源,也是最大的竞争对手——如果企业版没有提供相较于社区版更棒的功能,那么人们就不会为之买单或再次购买。” 企业级购买者更倾向于 Open Source 还是 Open Core? 关于企业级购买者更倾向 Open Source 而不是 Open Core,还是 仅仅根据他们业务选择软件/服务这个问题,Ovum 分析师 Tony Baer 表示,这是一个复杂的问题。他说:“这是一个难题,答案是‘视情况而定’”。理论上,所有软件决策取决于商业利益,而商业利益又由一系列选项构成,包括:增加收入,提高盈利,员工保留策略(留用希望在简历上体现开源项目经历的开发者),现有 IT 环境的兼容性和并购中的机构。 我们咨询的大多数分析师一致认为,公司应该关注它们正在使用的开源技术,然而,这说起来容易做起来难。首先,开发者从 GitHub 或其他站点获取资源时通常不会征求许可。其次,正如凯洛格提及的那样,在开放源代码计划(OSI)批准的清单上有 82 种不同的许可证类型,公司需要了解哪些组件受哪些许可证约束以及使用这些许可证的后果。 OSI总经理兼董事 Patrick Masson 表示,这正是一些大厂,甚至是那些拥护开源的公司针对许可证采取行动的原因。比如,Google 已彻底禁止了至少七种类型的开源许可证。 有人会说因为产品背后的软件供应商会处理许可证兼容性之类的问题,并且内置了企业所需的管理和安全功能,因此 Open Core 软件可能是一种更安全,更轻松的方法。但是亚马逊,谷歌和微软等大型云提供商可能正在改变游戏规则。 附录 Nebula Graph GitHub 地址:https://github.com/vesoft-inc/nebula ,加入 Nebula Graph 交流群,请联系 Nebula Graph 官方小助手微信号:NebulaGraphbot Nebula Graph:一个开源的分布式图数据库。 GitHub:https://github.com/vesoft-inc/nebula 官方博客:https://nebula-graph.io/cn/posts/ 微博:https://weibo.com/nebulagraph
Nebula Graph:一个开源的分布式图数据库。作为唯一能够存储万亿个带属性的节点和边的在线图数据库,Nebula Graph 不仅能够在高并发场景下满足毫秒级的低时延查询要求,还能够实现服务高可用且保障数据安全性。 聚会概述 在上周六的聚会中,Nebula Graph Committer 吴敏给爱好者们介绍了整体架构和特性,并随后被各位大佬轮番蹂躏(划掉)。 image.png 本次分享主要介绍了 Nebula Graph 的特性,以及新上线的《使用 Docker 构建 Nebula Graph》功能。 下面是现场的 Topic ( 以下简称:T ) & Discussion ( 以下简称:D ) 速记: 讨论话题目录 算法和语言 图库的 builtin 只搞在线查询可以吗?有必要搞传播算法和最短路径吗?Nebula 怎么实现对图分析算法的支持? 为什么要新开发一种查询语言 nGQL?做了哪些优化? 对于超大点,有啥优化的办法吗,或者对于构图有什么建议嘛? 图库相比其它系统和数据库未来发展趋势,比如相比文档和关系型,它的核心价值是什么? 架构和工程 key 为什么选择用 hash 而不是 range? gRPC,bRPC,fbthrift 为什么这么选 rpc?有没有打算自己写一个? 图库在设计上趋同化和同质化,架构上还有哪些创新值得尝试? 关于生态 图的生态怎么打造?和周边其它系统怎么集成融合? 算法和语言 T: 图库的 builtin 只搞在线查询可以吗?有必要搞传播算法和最短路径吗?Nebula 怎么实现对图分析算法的支持? ️D:Nebula 目前阶段侧重 OLTP,现在支持的算法是 全路径 和 最短路径 。在图库 builtin LPA 有不少工作要做(当然其实市面上也有产品),Nebula 现阶段的考虑是采用 存储计算分离架构 ,用户可以将图结构或者子图抽取到 GraphX 这种图计算框架,在图计算框架中实现传播算法。如果 OLTP 这块工作完成比较多了,再考虑向 OLAP 这个方向走。 T:为什么要新开发一种查询语言 nGQL?做了哪些优化? ️ D:其实目前市场上没有统一的图查询语言,可能 Cypher 和 Gremlin 影响力要大一些,当然要说图语言类的其实更多,比如还有 GraphQL,SPARQL。nGQL 与 SQL 接近,比较容易上手,但不用 SQL 那样嵌套(embedding)。 我感觉描述性的语言,大家的总体风格还是挺类似的,上手学习成本其实真没有想象那么高,花个十几分钟看看大概也明白了。有点类似中国各地方言(温州话除外,划掉),或者欧洲的各语言,共通的部分挺多的,连蒙带猜基本也能用。当然特别复杂的逻辑还是得看看手册才行。 优化方面:为避免存储层将过多数据回传到计算层,占用宝贵带宽,Nebula 做了 计算下沉 ,条件过滤会随查询条件一同下发到存储层节点。如果不带这个过滤,传 100% 和 1% 的数据,性能是数量级的差异。 对图查询的执行计划优化也进行了一定的探索,包括 执行计划缓存 和上下文无关语句的 并发执行 。当然其实查询优化挺难做的,我感觉 更能有效提升速度的是如何构图 。因为图的自由度还是挺大的,同一个东西,其实既可以构图成点、边也可以做成属性,其实对大多数目前的使用者来说,构图对性能的影响应该会比 DB 优化更明显更快。当然构图其实是和 DB 怎么实现也挺有关系的,比如减少网络传输(比如过滤)、用好 SSD 和 cache(比如减少随机读)、增加各种并发(多线程、多机)。 还有不要构造一个超大点出来,不然热点太明显了。回到语言,我们也考虑是不是 nGQL 上面加一层 Driver 支持 Cypher 和 Gremlin,比如 80% 的常见功能。还有就是考虑在 webconsole 上增加一些流程图的功能模块,CRUD 操作用图形化支持,复杂的就写 query,对长尾用户上手也有帮助。 T:刚才聊到超大点,有啥优化的办法吗,或者对于构图有什么建议嘛? ️ D:对于超大点建议还是构图和查询时,想办法处理(分解)比较好,这个和 SQL 分库分表差不多。比如:遍历过程中 touch 到的交易对手很大(比如:美团),那最好能给这种大点打标,遍历时候过滤掉。当然打标可能要离线 count 一下才知道。 比方说,根据业务类型、时间片段,把一个超大点最好能拆成多个小点,这样操作点一般不会落在一个 partition 上,再把当中热点的 partition 迁移到不同的机器上。举例来说,遍历太深的话,通常性能都不会太好,所以可以把属性放在起点和终点上。像 (Subject1)->(Predicate1)->(Object1) 这样, (Subject1)、(Predicate1)、(Object1) 三个节点,两跳深度,可能要走一次网络,但改成 (Subject1)-[Predicate1]->(Object1) 这样 -[Predicate1]-> 改成一种类型的边,那就不走网络,特别当查询深度更深时,这种构图对性能优化很明显。类似的,还有属性值处理,如:起点的 Name(string),不要作为边属性,不然同一个点出去的所有边上都冗余了这个 Name(string),更新的时候也巨麻烦。 T:图库相比其它系统和数据库未来发展趋势,比如相比文档和关系型,它的核心价值是什么? ️ D: Everything is connected. 图数据库天生适合表达 connection,或者说多对多的关系。 图数据库可以很高效的查询几度关系,而传统关系型数据库不擅长,一般都需要做表连接,表连接是一个很昂贵的操作,涉及到大量的 IO 操作及内存消耗。 但我觉得其实文档、关系和图相互还是借鉴非常多的,我记得《DesigningData-Intensive Applications》里面有章就是做它们之间的比较。 架构和工程 T:key 为什么选择用 hash 而不是 range? ️ D:其实并不是一定要 hash,只是要求 vid 是定长的 64 bit。定长主要是出于对齐性能考虑,还可以用上 prefix bloomfilter。那么变长 id 一般 hash 成 64 bit 最简单,当然用户自己指定 vid 也是支持的,一般这个时候,需要把原始 id 放到点的属性里。 T:gRPC,bRPC,fbthrift 为什么这么选 rpc?有没有打算自己写一个? ️ D:从使用体验上看,fbthrift 易读性不错。gRPC 之前用过也挺多。当然写个好的 rpc 还是挺不容易的,这个轮子暂时不是很急迫。 T:图库在设计上趋同化和同质化,架构上还有哪些创新值得尝试? ️ D:其实图产品有很多,我觉得这些产品不能说都是趋同,毕竟从几个知名竞品的架构看,彼此之间相差还是蛮大的 :)。因为功能集和架构出发点主要还是针对业务目标,Nebula 设计目标是实现 万亿级别关联关系 和 大并发 低时延 ,所以选择了存储计算分离,存储层采用 raft 一致协议,数据 partition 到不同机器上。这样设计主要考虑到存储和计算两者的业务特点和增长速度不一样,比如 learner 可以拿来给一些 throughput 优先的场景使用,原集群给 latency 优先的场景使用。 说到大的架构创新,主要看长期的硬件更新速度。当然 DB 可做的优化的事情已经很多的,刚才 PPT 里面有提及。 T:在测试方面,Nebula 做了哪些工作? ️ D:一个是集成测试框架,包括 混沌工程 、 错误注入 这些,等完善之后也会开放出来。还有是关于测试集和数据集,对于 DB 来说,这部分的价值是最大的,不过图领域可参考的数据集较少,都是大家自己积累的。 关于生态 T:图的生态怎么打造?和周边其它系统怎么集成融合? ️ D:在查询语言方面,增加对 Gremlin、Cypher 的支持。 在工具方面,提供数据批量导入和导出的工具,比如 GraphX,Yarn,Spark 等。还有,就是对机器学习的需求支持,存储计算相分离的架构使得 Nebula 非常容易集成图计算框架。因为 Nebula 是开源产品,这些工具欢迎大家一起参与:) Nebula Graph:一个开源的分布式图数据库。 GitHub:https://github.com/vesoft-inc/nebula 官方博客:https://nebula-graph.io/cn/posts/ 微博:https://weibo.com/nebulagraph
2021年01月
2020年12月
2020年11月
2020年10月
2020年09月
2020年08月
2020年07月
2020年06月