☰
Dgraph 查询基准测试指南:SubGraph gob 快照、ToJSON/ToProto 输出对比与遍历策略选择
2026/10/1 9:44:59 网站建设 项目流程
  • 数据库
  • 图数据库
  • 分布式数据库
  • 后端

【免费下载链接】dgraph

high-performance graph database for real-time use cases

项目地址:https://gitcode.com/gh_mirrors/dg/dgraph
点击查看免费下载

本文是 Dgraph 仓库query/benchmark目录的完整解读,围绕其 README.md 展开:它记录了如何借助dgraph -dumpsg将真实查询执行后的中间结果(序列化后的 SubGraph 对象)落盘为 gob 文件,并在脱离数据库的情况下反复回放、度量ToJSON与ToProtocolBuffer两条输出链路的耗时与内存分配。读完本文,你将掌握这套基准测试的完整操作流程(交互式采集与run.sh一键重生成两种方式)、gob 快照的数据语义、源码中对应的SubGraph结构与基准用例写法,以及仓库历史上对"前序遍历 vs 后序遍历"取舍的实验结论。

一、这套基准测试要解决什么问题

Dgraph 执行一条查询时,会先把 DQL 解析为查询计划,逐层拉取 posting list、过滤与分页,最终形成一个树状的中间结果结构——SubGraph。查询的最后一公里则是把SubGraph转换成用户可见的输出格式:

  • JSON:通过 ToJson 序列化输出(对应outputnode.go中的encoder、preTraverse等实现);
  • Protocol Buffer:通过ToProtocolBuffer转成graph.Response并进一步编组。

query/benchmark目录的出发点很朴素:这两条输出链路的开销是查询总时延的重要组成部分,需要反复度量与优化;但每次都启动完整 Dgraph、灌入数据、跑完整查询来测,既慢又不稳定。于是仓库的做法是——把"查询执行完毕、尚未输出"的 SubGraph 对象用 gob 序列化保存为文件,基准测试只做 gob 反序列化 + 输出编码两步,从而在完全可复现的数据上对比不同输出路径、不同实现版本之间的性能差异。

这也解释了目录中actor.*.gob、director.*.gob这些二进制文件的来历:它们不是随机数据,而是两条真实电影图谱查询在特定演员/导演实体上执行后的 SubGraph 快照。

二、benchmark 目录的资产清单

文件作用
README.md方法论文档:如何用-dumpsg采集 gob、如何运行run.sh重生成
README.txt补充说明:gob 文件语义、两轮查询的定义、2016 年历次基准结果
run.sh一键重生成 gob 文件的完整脚本
actor.0.gob / actor.1.gob / actor.2.gob演员查询的 SubGraph 快照
director.0.gob / director.1.gob / director.2.gob导演查询的 SubGraph 快照
synthetic_results.txt合成 SubGraph 上 Pre/Post 遍历策略的实验数据

关于文件命名,README.txt 明确说明:文件名结尾的数字(10、100、1000)代表查询结果中的实体数量,而不是数据规模级别。以actor.0.gob为例,它对应"结果含 10 个实体"的演员查询快照。

三、方法一:交互式采集 gob 快照(-dumpsg)

README 给出的第一种方式是启动一个带调试转储功能的 Dgraph 实例:

dgraph -dumpsg dumpsg -port 8912

该命令会在当前目录下生成dumpsg/目录,每次查询执行后把处理过的 SubGraph 以 gob 文件的形式写入其中。需要说明的是,-dumpsg属于早期调试功能,在当前仓库源码中已检索不到该 flag(全仓库*.go内无匹配),因此下述命令按 README 原文还原历史方法论,用于理解快照的采集原理;在当前版本上实际操作时,以run.sh所在历史上下文为准。

3.1 采集演员(Actors)查询

rm -Rf dumpsg NUMS="10 56 300" for NUM in $NUMS; do curl localhost:8912/query -XPOST -d "{ me(_xid_:m.08624h) { type.object.name.en film.actor.film(first: $NUM) { film.performance.film { type.object.name.en } } } }" 2>/dev/null | python -m json.tool | wc -l done n=0 for S in dumpsg/*.gob; do echo $S cp -f $S /tmp/actor.${n}.gob n=$(($n+1)) done

这段脚本的要点:

  • 固定查询实体m.08624h(一位演员),通过first: $NUM逐步提高返回的影片数量上限;
  • 注释明确写道:"We increase the number of actors until we hit just a little below the max"——即把结果数量调到一个"略低于上限"的水平,以考察接近极限规模的输出开销;
  • python -m json.tool | wc -l的作用是把 JSON 响应格式化后数行数,作为结果规模的直观度量;
  • 每次查询产生的 gob 按顺序复制为/tmp/actor.0.gob、/tmp/actor.1.gob、/tmp/actor.2.gob。

3.2 采集导演(Directors)查询

rm -Rf dumpsg NUMS="10 31 100" for NUM in $NUMS; do curl localhost:8912/query -XPOST -d "{ me(_xid_:m.05dxl_) { type.object.name.en film.director.film(first: $NUM) { film.film.genre { type.object.name.en } } } }" 2>/dev/null | python -m json.tool | wc -l done n=0 for S in dumpsg/*.gob; do echo $S cp -f $S /tmp/director.${n}.gob n=$(($n+1)) done

导演查询的根实体是m.05dxl_,图模式为导演 -> film.director.film -> 影片 -> film.film.genre -> 类型,比演员查询多一层跳转。

3.3 拷贝到 benchmark 目录

cp -vf /tmp/*.gob ./

把/tmp下的 6 个 gob 文件复制到query/benchmark/目录,供benchmark_test.go读取。

四、方法二:用 run.sh 一键重生成 gob 文件

README 原文指出:"Just runrun.shto regenerate these gob files."——这些 gob 文件就是序列化后的 SubGraph 对象("These gob files are just serialized SubGraph objects"),完全可以脚本化重生成。运行前只有两点注意事项:

  1. 在该目录内运行,这样 gob 文件才会生成在当前目录;
  2. 必须指定DATADIR,它是执行dgraph-live-loader灌入数据后、启动 dgraph 时所在的数据目录(posting list 数据所在位置)。

run.sh 的完整逻辑如下:

set -e # Where you store posting list and other data. It's where you start dgraph in. DATADIR=${HOME}/dgraph THISDIR=$(pwd) # These actors have 10, 1000, 1007 results respectively. ACTORS="m.03c7p9t m.0148x0 m.08624h" # These directors have 10, 100, 992 results respectively. DIRECTORS="m.0bysn41 m.03k5gd m.05dxl_" pushd "${DATADIR}" &>/dev/null rm -Rf dumpsg dgraph -dumpsg dumpsg -port 8912 & sleep 2 for ACTOR in ${ACTORS}; do curl localhost:8912/query -XPOST -d " { me(_xid_:${ACTOR}) { type.object.name.en film.actor.film { film.performance.film { type.object.name.en } } } }" 2>/dev/null >/dev/null done n=0 for S in dumpsg/*.gob; do echo "${S}" cp -vf "${S}" "${THISDIR}"/actor."${n}".gob n=$((n + 1)) done rm -f dumpsg/* for DIRECTOR in ${DIRECTORS}; do curl localhost:8912/query -XPOST -d " { me(_xid_:${DIRECTOR}) { type.object.name.en film.director.film { film.film.genre { type.object.name.en } } } }" 2>/dev/null >/dev/null done n=0 for S in dumpsg/*.gob; do echo "${S}" cp -vf "${S}" "${THISDIR}"/director."${n}".gob n=$((n + 1)) done rm -Rf dumpsg killall dgraph popd &>/dev/null

几个值得注意的实现细节:

  • 脚本在DATADIR下启动 dgraph(pushd "${DATADIR}"),保证 dgraph 能读到自己写出的 posting list 数据,dumpsg/也在此目录生成;
  • 脚本用固定的三个实体替换了 README 交互式流程中的first: $NUM递增做法,并在注释中标注每个实体对应的结果规模:三位演员分别有 10、1000、1007 条结果,三位导演分别有 10、100、992 条结果——同样覆盖"小、中、接近最大"三档规模;
  • 输出重定向到/dev/null,避免响应内容刷屏;随后按生成顺序把 gob 复制为actor.0/1/2.gob、director.0/1/2.gob;
  • 两轮采集之间用rm -f dumpsg/*清空上一批快照;全部结束后killall dgraph清理进程;
  • set -e保证任一步失败即中止,避免产出残缺快照。

五、gob 文件到底是什么:SubGraph 结构速览

README 明确交代了文件内容,而源码把"被序列化的对象长什么样"讲得更清楚。SubGraph 定义 位于query/query.go,核心字段包括:

  • Attr:当前节点的谓词名;SrcUIDs/DestUIDs:源 UID 列表与目标 UID 列表;
  • valueMatrix:标量谓词的值列表(每个源 UID 对应一个pb.ValueList,支持 list 类型谓词);
  • uidMatrix:出边列表——图语义下每个源 UID 对应一条出边切片;
  • facetsMatrix:边上的 facet 值;Children []*SubGraph:子查询节点,叶子节点为空;
  • FilterOp/Filters、MathExp、Params等:记录过滤、数学表达式与分页参数。

因此actor.0.gob中保存的是一个"查询已经执行完、结果矩阵已经填充"的完整 SubGraph 树,反序列化后可以直接喂给输出编码器。

benchmark_test.go 中的benchmarkHelper展示了读取方式(该测试当前以注释形式保留,标注 "TODO: Fix this test"):

sg := new(SubGraph) data, err := os.ReadFile(filename) // 读取 benchmark/actor.N.gob buf := bytes.NewBuffer(data) dec := gob.NewDecoder(buf) err = dec.Decode(sg) // gob 解码为 SubGraph f(b, sg) // 执行 ToJSON 或 ToProto 基准函数

六、基准用例的设计:真实快照回放 + 合成图验证

benchmark_test.go中设计了四类基准:

1.BenchmarkToJSON(真实快照回放)

func BenchmarkToJSON(b *testing.B) { benchmarkHelper(b, func(b *testing.B, sg *SubGraph) { var l Latency b.ResetTimer() for i := 0; i < b.N; i++ { if _, err := sg.ToJSON(&l); err != nil { b.Fatal(err) } } }) }

2.BenchmarkToProto(真实快照回放)——注意它不只是调用ToProtocolBuffer,还额外走了Codec.Marshal:

pb, err := sg.ToProtocolBuffer(&l) r := new(graph.Response) r.N = pb var c Codec if _, err = c.Marshal(r); err != nil { b.Fatal(err) }

这个细节与 README.txt 中 8 月 5 日的记录完全对应:早期基准遗漏了 protobuf 的 marshalling 步骤,补齐后才构成 JSON 与 PB 的"真正对等比较"。

3/4.BenchmarkToProtoSynthetic/BenchmarkToJSONSynthetic(合成图)——通过sampleSubGraph(numUnique)构造一棵"唯一后代数量从 1 到 5000 递增"的树(源码见 benchmark_test.go),用于研究输出结果重叠度对遍历开销的影响,这正是synthetic_results.txt数据对应的实验。

七、历史基准结果解读

README.txt 保留了 2016 年的三轮关键对照实验,可直接复现当年的优化脉络:

7.1 第一轮(2016-05-14):JSON vs Protocol Buffer 的原始差距

基准次数ns/opB/opallocs/op
ToJSON_10_Actor200009279722616319
ToJSON_10_Director200008724621111303
ToJSON_100_Actor20007747672078932670
ToJSON_100_Director20005794671428112103
ToJSON_1000_Actor2007903001190486324712
ToJSON_1000_Director300433537595772816115
ToPB_10_Actor10000019672317660
ToPB_10_Director10000017891309660
ToPB_100_Actor1000037228830728556
ToPB_100_Director500022150637272701
ToPB_1000_Actor50026127572964865383
ToPB_1000_Director30039806773956007376

结论:ToProtocolBuffer比ToJSON分配更少内存、耗时更短(例如 10 实体规模下 PB 约为 JSON 的 1/5 耗时、约 1/7 的内存分配)。

7.2 第二轮(2016-05-20):[]byte直传优化,平均提升超过 50%

背景:把x.DirectedEdge的 Value 类型和 NQuad 的ObjectValue改为[]byte,并直接从 flatbuffers 取字节切片而非解析成interface{}(对应 commit480b1337f)。代表性结果:

基准次数ns/opB/opallocs/op
ToJSON_10_Actor50000274977626113
ToJSON_100_Actor1000013785337333619
ToJSON_1000_Actor20007808582404193863
ToPB_10_Actor500000325266415
ToPB_100_Actor30000447718008145
ToPB_1000_Actor500036613456217958

README 的结论是"所有指标平均提升超过 50%",并建议用benchcmp精确计算百分比变化。

7.3 第三轮(2016-08-05):补齐 PB 编组 + 切换到 gogo protobuf

这一轮把之前缺失的 protocol buffer marshalling 步骤补进基准,并说明改用 gogo protobuf 替代原 go protobuf。代表性数据(-2表示 2 核):

基准次数ns/opB/opallocs/op
ToJSON_1000_Actor20008315432395923854
ToJSON_1000_Director500364699496433915642
ToPB_1000_Actor500038985372856959
ToPB_1000_Director200010366472075063081

7.4 第四轮(2016-08-09):sync.Pool 复用graph.Node,内存显著下降

在ToProtocolBuffer中用sync.Pool管理graph.Node,并用以下命令生成内存 profile 验证:

go test -run=xx -bench=BenchmarkToPB_ -memprofile=syncpool.mem go tool pprof --alloc_space query.test syncpool.mem

观察结果:总分配内存从改动前的 1.76GB 降至 1.28GB,大部分减少来自preTraverse函数——这直接指向了下一节的主题。

八、遍历策略实验:PreTraverse 与 PostTraverse 的取舍

synthetic_results.txt 记录了在"唯一后代数量从 1、1000、2000、3000、4000 递增到 5000"的合成图上,两种遍历策略的对照结果(机器为 64G 内存桌面):

ToProto 的 Pre/Post 对比(节选):

  • WithPre:随唯一后代数增加,耗时基本恒定(约 95~125ms/op,内存约 39.8MB/op);
  • WithPost:唯一后代少(重叠多)时更快,最快 26ms/op;但重叠少时几乎慢一倍,内存最高 95MB/op,分配数达 181 万次。

ToJSON 的 Pre/Post 对比(节选):

  • WithPre:约 300~335ms/op,内存约 159MB/op;
  • WithPost:随重叠度不同在 235ms 到 462ms 之间波动,内存 127MB 到 271MB。

数据解读与结论(原文明确给出):

  1. WithPost 在重叠度高(唯一后代少)时确实省掉大量工作;
  2. 但,无论 JSON 还是 proto,最终 marshal 时都必须遍历每个节点,所以 Post 的实际收益并不大;
  3. 综合"更简单、且约一半场景下更快"的考量,最终选择PreTraverse。

这一决策与当前源码一致:query/outputnode.go中现存的正是在编码阶段逐节点展开的 preTraverse,而 README 记录的"内存优化主要来自 preTraverse"也与 synthetic_results.txt 的结论互相印证。

九、如何在当前代码中对照验证

如果你希望把这篇历史方法论映射到当前仓库代码上,可以沿以下路径验证:

  1. SubGraph 结构:query/query.go——确认快照对象包含SrcUIDs、DestUIDs、valueMatrix、uidMatrix、facetsMatrix、Children等执行结果字段;
  2. JSON 输出链路:outputnode.go 的ToJson,以及其内部依赖的 encoder.encode 与 preTraverse;
  3. 基准读取逻辑:benchmark_test.go 中的benchmarkHelper、BenchmarkToJSON、BenchmarkToProto(当前以注释保留,标有 "TODO: Fix this test");
  4. 合成图实验构造:benchmark_test.go 的sampleSubGraph与BenchmarkToProtoSynthetic。

需要提醒的是:-dumpsg调试 flag 在当前源码中已不存在,run.sh中dgraph -dumpsg dumpsg -port 8912的用法属于早期版本;不过"先执行真实查询、把中间结果落盘为 gob、再离线回放做输出层基准"这一方法论本身仍然成立,且是理解 Dgraph 查询执行与输出编码性能边界的极佳入手点。配合 README.txt 中完整的基准表格与 synthetic_results.txt 的遍历实验数据,你可以自行复跑或扩展这些基准,验证 JSON/PB 输出链路在不同结果规模下的时延与内存分配表现。

  • 数据库
  • 图数据库
  • 分布式数据库
  • 后端

【免费下载链接】dgraph

high-performance graph database for real-time use cases

项目地址:https://gitcode.com/gh_mirrors/dg/dgraph
点击查看免费下载

相关推荐

上一篇:Claude Code UI的MCP服务器配置教程:扩展你的AI工具生态
下一篇:Hydro插件系统深度解析:5种架构模式实现高性能在线评测平台扩展

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询