批量处理
为了回答查询,JanusGraph必须对存储后端执行查询。 通常,有两种方式可以实现:
- 一旦需要来自后端的数据,执行一个后端查询并继续处理结果。
- 维护所需数据的列表。 一旦列表达到一定大小,执行批量 后端查询以一次性获取所有数据。
第一种选项通常响应更快且消耗更少内存,因为查询可以非常早地发出首批结果,而无需等待大批量查询完成。然而,对于遍历大量顶点的遍历操作,它会向存储后端发送许多小查询,从而导致性能较差。这就是为什么JanusGraph默认使用批处理的原因。这两种选项将在下面更详细地描述,包括有关配置批处理的信息。
注意
默认设置在1.0.0版本中进行了更改。 旧版本的JanusGraph默认不使用批处理(第一个选项)。
无批处理
在图遍历方面,查询的执行与深度优先搜索原则松散耦合。
在以下用例中使用此配置,例如...
- ... 每个查询仅访问图的少数顶点。
- ... 你的应用不需要完整的结果集立即返回,而是要求首批结果以低延迟到达。
可能的限制
- 遍历大型邻域可能导致查询变慢。
显式配置此选项的步骤:
- 确保
query.batch.enabled设置为false
无限制批处理
使用此配置时,从顶点开始遍历图的每个步骤(例如 in()、outE() 和 values(),但不包括 inV() 或 otherV(),也不包括 valueMap(),参见 #2444)会成为一个阻塞操作符,这意味着它不会产生任何结果,直到前一步的所有结果都已知。只有在那时,才会执行单个后端查询,并将结果传递给下一步。手动添加的 barrier() 步骤不会以任何有意义的方式影响此过程。这种执行方式可以被视为广度优先搜索。
在以下用例中使用此配置,例如...
- ... 你的查询可能需要在每一步中访问多个顶点。
- ... JanusGraph 与存储后端之间存在显著网络延迟。
可能的限制
- 内存消耗增加
- 如果限制步骤在查询的后期发生,限制步骤之前的步骤可能会产生不必要的开销。
- 执行非常大的后端查询可能会对存储后端造成压力。
显式配置此选项的步骤:
- 确保
query.batch.enabled设置为true - 确保
query.batch.limited设置为false
有限批处理
使用此配置,从顶点开始遍历图的每一步(例如 in()、outE() 和 values(),但不包括 inV() 或 otherV())首先会聚合多个顶点,然后执行批量后端查询。
此聚合阶段和后端查询阶段将重复进行,直到所有顶点都被处理完毕。
与无限制批处理(其中一批对应查询中的一个步骤)相比,此方法可以在每个步骤中构建多个批次。
这是自版本1.0.0以来JanusGraph的默认配置。
配置批量大小
虽然批处理大小不一定需要配置,
但它可以提供一个额外的调优参数来提升
查询性能。
默认情况下,TinkerPop的屏障步骤的批处理大小
将由LazyBarrierStrategy提供,当前为2500。
对于可批处理的情况,当LazyBarrierStrategy未注入任何barrier步骤时,
屏障步骤将通过query.batch.limited-size配置的大小注入
(默认为2500,与LazyBarrierStrategy相同)。
每个顶点步骤的批处理大小可以通过前置
barrier(步骤单独配置。
例如,在以下查询中,第一个out()步骤将
使用默认的批处理大小2500,而第二个out()步骤
将使用手动配置的批处理大小1234:
g.V(list_of_vertices).out().barrier(1234).out()
对于以顶点步骤开始的本地遍历,限制最好配置在本地遍历之外,如下所示:
g.V(list_of_vertices).out().barrier(1234).where(__.out())
barrier(1234)步骤将不被允许聚合多个遍历器。
一个特殊情况适用于repeat()步骤。
由于repeat()步骤的局部遍历有两个输入
(首先,repeat()步骤之前的步骤,其次,
重复遍历的最后一步,它将结果
反馈回开头),这里可以配置两个限制。
g.V(list_of_vertices).barrier(1234).repeat(__.barrier(2345).out()).times(5)
barrier(1234)步骤只能在遍历器
首次进入repeat步骤时聚合它们。对于每次迭代,内部的
barrier(2345)用于聚合来自
前一次迭代的遍历器。
在以下用例中使用此配置,例如...
- ... 你同时拥有遍历大量顶点的遍历和仅访问图中少量顶点的遍历。
可能的限制
- 内存消耗增加(与无批处理相比)
- 查询的性能取决于配置的批处理大小。 如果使用此配置,请确保查询的延迟和吞吐量满足您的要求,如果不满足,请相应地调整批处理大小。
显式配置此选项的步骤:
- 确保
query.batch.enabled设置为true - 确保
query.batch.limited设置为true
批量查询处理流程
每当query.batch.enabled设置为true时,兼容批处理的步骤将以批处理方式执行。每个存储后端可能以不同方式执行此类批处理,但通常意味着并行请求多个顶点的数据,这通常在查询访问许多顶点时提高查询性能。
批量查询处理考虑两种类型的步骤:
- 批量兼容步骤。这是将执行批量请求的步骤。目前,此类步骤的列表如下:
out(),in(),both(),inE(),outE(),bothE(),has(),values(),properties(),valueMap(),propertyMap(),elementMap(),label(),drop(). - 父步骤。这是一个父步骤,它包含具有相同起点的本地遍历。这类父步骤也实现了接口
TraversalParent。有许多这样的步骤,例如:and(...)、or(...)、not(...)、order().by(...)、project("valueA", "valueB", "valueC").by(...).by(...).by(...)、union(..., ..., ...)、choose(..., ..., ...)、coalesce(..., ...)、where(...)等。这些本地步骤的起点应相同,因此目前唯一的例外是步骤repeat()和match()(关于它们如何处理,请参见下文)。
父步骤将其顶点注册以便后续使用批处理兼容的起始步骤进行处理。例如,
g.V(v1, v2, v3).union(out("knows"), in("follows"))
v1、v2 和 v3 将被注册到 out("knows") 和 in("follows") 步骤进行批处理,因为它们的父步骤(union)会将任何输入注册到批处理兼容的子起始步骤。此外,父步骤即使与深度嵌套的批处理兼容起始步骤一起,也可以注册顶点进行批处理。例如,
g.V(v1, v2, v3).
and(
union(out("edge1"), in("edge2")),
or(
union(out("edge3"), in("edge4").optional(out("edge5"))),
optional(out("edge6")).in("edge7")))
v1、v2 和 v3 将被注册到 out("edge1")、in("edge2")、out("edge3")、in("edge4") 和 out("edge6") 步骤进行批处理,因为它们都可以被视为最根父步骤(and 步骤)的起始点。也就是说,这些顶点不会被注册到 out("edge5") 或 in("edge7") 步骤进行批处理,因为这些步骤要么不是起始步骤,要么是其他父步骤的起始步骤。因此,out("edge5") 将注册来自 in("edge4") 步骤返回的任何顶点,而 in("edge7") 将注册来自 optional(out("edge6")) 步骤返回的任何顶点。
针对repeat步骤的批处理
重复步骤不遵循其他父步骤的规则,并以不同方式向子步骤注册顶点。 目前,TinkerPop的默认实现使用广度优先搜索而非深度优先搜索(其他步骤使用的方式)。
JanusGraph 将重复步骤顶点应用于本地 repeat 步骤的开头、本地 emit 步骤的开头(如果它位于 repeat 步骤之前),以及本地 until 步骤的开头(如果它位于 repeat 步骤之前)。此外,对于任何下一次迭代,JanusGraph 将本地 repeat 步骤(结束步骤)的结果应用于本地 repeat 步骤(开始步骤)的开头,以及 emit 和 until 遍历的开始步骤。
每层级批量请求的使用场景(loop):
-
简单示例。
在上述示例中,顶点g.V(v1, v2, v3).repeat(out("knows")).emit()v1、v2和v3将被注册到out("knows")步骤,因为这是支持批处理的起始步骤。 此外,同一层级(loop)上所有out("knows")迭代的结果将被重新注册回out("knows")以进行下一层级(loop)的迭代, 依此类推,直到out("knows")不再产生任何结果。 -
使用自定义
emit遍历在repeat之后的示例。上述示例的顶点注册流程与示例g.V(v1, v2, v3).repeat(out("knows")).emit(out("follows"))1相同,但区别在于out("follows")将以与out("knows")步骤本身从自身接收顶点进行注册相同的方式,从out("knows")接收顶点进行注册。 注意,如果这里使用的是until而不是emit,同样的逻辑也适用于until步骤。 -
使用自定义
emit遍历在repeat之前的示例。上述示例的顶点注册流程与示例g.V(v1, v2, v3).emit(out("follows")).repeat(out("knows"))2相同,但区别在于out("follows")将从out("knows")和起始顶点v1、v2、v3接收要注册的顶点。换句话说, 在这种情况下,out("knows")和out("follows")的顶点注册来源是相同的。注意,同样的逻辑 也适用于until步骤,如果这里使用的是until而不是emit。 -
使用自定义
emit和until遍历在repeat之前的示例。在上面的示例中,所有3个步骤g.V(v1, v2, v3).emit(out("follows")).until(out("feeds")).repeat(out("knows"))out("follows")、out("feeds")和out("knows")都具有相同的顶点 注册流程,它们既从查询起始点(v1、v2、v3)接收顶点,也从本地重复结束 步骤(out("knows"))接收顶点。 -
使用自定义
emit和until遍历在repeat之后的示例。上述示例的顶点注册流程与示例g.V(v1, v2, v3).repeat(out("knows")).emit(out("follows")).until(out("feeds"))4相同,但区别在于out("follows")和out("feeds")不会从查询起点(v1、v2、v3)接收顶点注册。 -
使用自定义
until遍历在repeat之前和emit(true)在repeat之后的示例。上述示例的顶点注册流程与示例g.V(v1, v2, v3).until(out("feeds")).repeat(out("knows")).emit()4相同,除了emit遍历没有任何支持批处理的起始步骤。因此,emit遍历不会接收批处理的顶点注册。
每次迭代的批量请求用例:
在大多数情况下(如上述示例1 - 6及其他情况),TinkerPop的默认repeat步骤实现会在执行emit或until之前对整个层级(loop)执行本地repeat遍历。换句话说,repeat遍历会执行多次(在同一loop上进行多次迭代),然后才在当前loop上首次执行emit或until。这使JanusGraph能够发出包含来自多个repeat遍历迭代的顶点的大型批处理请求,从而高效执行批处理请求。
也就是说,存在3种用例,其执行流程不同,TinkerPop会在每次repeat遍历迭代后执行until或emit遍历。在这种情况下,until或emit步骤将仅对当前迭代收集的顶点执行批处理请求,而不是对来自同一层级(loop)所有迭代收集的顶点执行。
g.V(v1, v2, v3).emit().repeat(out("knows")).until(out("feeds"))
g.V(v1, v2, v3).emit(out("follows")).repeat(out("knows")).until(out("feeds"))
g.V(v1, v2, v3).until(out("feeds")).repeat(out("knows")).emit(out("follows"))
以上3个示例展示了当until或emit每次迭代执行批次而非每层级执行的模式。
若任何emit步骤置于repeat步骤之前,而until步骤置于repeat步骤之后。若
until步骤置于repeat步骤之前,而非真值emit步骤置于repeat步骤之后。
在所有其他情况下,repeat步骤将针对整个loop执行,仅在此之后才会执行emit或until。
这些限制可能在JanusGraph添加对DFS重复步骤执行的支持后得到解决 (see issue #3787)。
多重嵌套 repeat 步骤模式:
默认情况下,当批处理起始步骤有多个repeat步骤父级时,批处理注册会考虑所有repeat父级步骤。
然而,当事务缓存较小且重复步骤遍历深度超过一级时,可能导致某些顶点被重复获取,或者由于循环提前结束而不需要获取的顶点可能被获取到事务缓存中。这意味着在不必要的情况下浪费操作。
因此,JanusGraph 提供了一个配置选项 query.batch.repeat-step-mode 来控制多重复步骤行为:
closest_repeat_parent(默认选项) - 仅考虑最近的repeat步骤。在上面的示例中,g.V().repeat(and(repeat(out("knows")).emit())).emit()out("knows")将在第一次迭代时从and步骤输入接收用于批处理的顶点, 并在后续迭代中从out("knows")步骤输出接收顶点。all_repeat_parents- 考虑从每个repeat步骤父级的起始和结束处注册顶点。在上面的示例中,g.V().repeat(and(repeat(out("knows")).emit())).emit()out("knows")将从最外层的repeat步骤输入(用于第一次迭代)、最外层的repeat步骤输出(即and输出)(用于第一次迭代)、
and步骤输入(用于第一次迭代)以及out("knows")输出(用于后续迭代)接收顶点进行批处理。starts_only_of_all_repeat_parents- 考虑从每个repeat步骤父级的起始处注册顶点。在上面的示例中,g.V().repeat(and(repeat(out("knows")).emit())).emit()out("knows")将从最外层的repeat步骤输入(用于第一次迭代)、and步骤输入(用于第一次迭代)以及out("knows")输出(用于后续迭代)接收顶点进行批处理。
针对match步骤的批处理
目前,JanusGraph支持在match步骤的各个本地遍历内部进行顶点注册以进行批处理,但不支持在这些本地遍历之间进行。此外,JanusGraph不会将match步骤的起始点与match步骤的任何本地遍历进行注册。因此,match步骤的性能可能会受到限制。这是一个临时限制,直到该功能被实现(see issue #3788)。
批量处理属性
一些启用了优化的Gremlin步骤可能会批量预取顶点属性。
目前,JanusGraph使用切片查询来查询部分行数据。单个切片查询包含
起始键和结束键,以定义JanusGraph感兴趣的数据切片。
由于JanusGraph目前不支持多范围切片查询,它可以在单个切片查询中获取单个属性,
或者在单个切片查询中获取所有属性。因此,用户必须在不同的属性获取方法之间权衡取舍,
并决定何时在单个切片查询中获取所有属性(通常更快但可能会获取不必要的属性),
或者为每个属性在单独的切片查询中仅获取请求的属性(可能稍慢但仅获取请求的属性)。
See issue #3816 该问题将允许通过单一切片查询仅获取请求的属性。
请参阅配置选项 query.fast-property,该选项可用于在首次访问单一属性时预取所有属性,当请求直接顶点属性时(例如 vertex.properties("foo"))。
请参阅配置选项 query.batch.has-step-mode 以控制 has 步骤的属性预取行为。
请参阅配置选项 query.batch.properties-mode 以控制 values、properties、valueMap、propertyMap 和 elementMap 步骤的属性预取行为。
请参阅配置选项 query.batch.label-step-mode 以控制 label 步骤的标签预取行为。
请参阅配置选项 query.batch.drop-step-mode 以控制 drop 步骤的批量删除行为。