跳到内容

批处理

为了回答查询,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())会首先聚合一定数量的顶点,然后执行批量后端查询。这个聚合阶段和后端查询阶段会重复,直到所有顶点都被处理。与无限制批处理中一个批次对应查询中的一个步骤不同,这种方法可以为每个步骤构建多个批次。

这是 JanusGraph 自 1.0.0 版本以来的默认配置。

配置批大小

虽然不一定需要配置批大小,但它可以提供一个额外的调优参数来提高查询性能。默认情况下,TinkerPop 的 barrier 步骤的批大小将由 LazyBarrierStrategy 提供,目前为 2500。对于 LazyBarrierStrategy 不注入任何 barrier 步骤的可批处理情况,barrier 步骤将注入通过 query.batch.limited-size 配置的大小(默认为 2500,与 LazyBarrierStrategy 相同)。
每个顶点步骤的批大小可以通过前置 barrier(<size>) 步骤单独配置。例如,在下面的查询中,第一个 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,兼容批处理的步骤都将以批处理方式执行。每个存储后端可能以不同的方式执行此类批处理,但通常这意味着并行请求多个顶点的数据,这通常在查询访问许多顶点时提高查询性能。

批处理查询考虑到两种类型的步骤

  1. 批处理兼容步骤。这是将执行批处理请求的步骤。目前,此类步骤的列表如下:out()in()both()inE()outE()bothE()has()values()properties()valueMap()propertyMap()elementMap()label()drop()
  2. 父步骤。这是一个父步骤,它具有相同起点的局部遍历。此类父步骤也实现了接口 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"))
在上面的例子中,顶点 v1v2v3 将与 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")))
在上面的例子中,顶点 v1v2v3 将与 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 步骤的批处理

Repeat 步骤不遵循其他父步骤的规则,而是以不同的方式向子步骤注册顶点。目前,TinkerPop 的默认实现使用广度优先搜索而不是深度优先搜索(如其他步骤所用)。

JanusGraph 将 repeat 步骤顶点应用于局部 repeat 步骤的开头,如果 emit 步骤位于 repeat 步骤之前,则应用于局部 emit 步骤的开头,如果 until 步骤位于 repeat 步骤之前,则应用于局部 until 步骤的开头。此外,对于任何下一次迭代,JanusGraph 将局部 repeat 步骤(结束步骤)的结果应用于局部 repeat 步骤(起始步骤)的开头以及 emituntil 遍历的起始步骤。

按级别(loop)批量请求的用例
  1. 简单示例。

    g.V(v1, v2, v3).repeat(out("knows")).emit()
    
    在上述示例中,顶点 v1v2v3 将注册到 out("knows") 步骤,因为它是支持批处理的起始步骤。此外,out("knows") 的同一级别(loop)上所有迭代的结果将注册回 out("knows") 以进行下一级别(loop)的迭代,依此类推,直到 out("knows") 停止发出任何结果。

  2. repeat 之后带有自定义 emit 遍历的示例。

    g.V(v1, v2, v3).repeat(out("knows")).emit(out("follows"))
    
    上述示例的顶点注册流程与示例 1 相同,但不同之处在于 out("follows") 将以与 out("knows") 步骤本身从自身接收顶点注册相同的方式从 out("knows") 接收顶点进行注册。请注意,如果此处是 until 而不是 emit,则相同的逻辑也适用于 until 步骤。

  3. repeat 之前带有自定义 emit 遍历的示例。

    g.V(v1, v2, v3).emit(out("follows")).repeat(out("knows"))
    
    上述示例的顶点注册流程与示例 2 相同,但不同之处在于 out("follows") 将同时从 out("knows") 和起始顶点 v1v2v3 接收顶点进行注册。换句话说,在这种情况下,out("knows")out("follows") 的顶点注册源是相同的。请注意,如果此处是 until 而不是 emit,则相同的逻辑也适用于 until 步骤。

  4. repeat 之前带有自定义 emituntil 遍历的示例。

    g.V(v1, v2, v3).emit(out("follows")).until(out("feeds")).repeat(out("knows"))
    
    在上面的例子中,所有 3 个步骤 out("follows")out("feeds")out("knows") 具有相同的顶点注册流程,它们同时从查询开始(v1v2v3)和局部重复结束步骤(out("knows"))接收顶点。

  5. repeat 之后带有自定义 emituntil 遍历的示例。

    g.V(v1, v2, v3).repeat(out("knows")).emit(out("follows")).until(out("feeds"))
    
    上述示例的顶点注册流程与示例 4 相同,但不同之处在于 out("follows")out("feeds") 不会从查询开始(v1v2v3)接收顶点注册。

  6. repeat 之前带有自定义 until 遍历并在 repeat 之后带有 emit(true) 的示例。

    g.V(v1, v2, v3).until(out("feeds")).repeat(out("knows")).emit()
    
    上述示例的顶点注册流程与示例 4 相同,只是 emit 遍历没有任何支持批处理的起始步骤。因此,emit 遍历不接收批处理顶点注册。

按迭代批量请求的用例

在大多数情况下(如上述示例 1 - 6 和其他情况),TinkerPop 的默认 repeat 步骤实现会在 emituntil 执行之前执行整个级别(loop)的局部 repeat 遍历。换句话说,repeat 遍历在当前 loopemituntil 首次执行之前会执行多次(同一 loop 上的多次迭代)。这使得 JanusGraph 有可能发出包含来自多个 repeat 遍历迭代的顶点的更大批量请求,从而有效地执行批量请求。
话虽如此,在 3 种情况下,执行流程是不同的,TinkerPop 在每次 repeat 遍历迭代后执行 untilemit 遍历。在这种情况下,untilemit 步骤将仅对当前迭代中收集的顶点执行批量请求,而不是对同一级别(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"))

上面三个示例展示了当 untilemit 按迭代而不是按级别执行批处理的模式。如果任何 emit 步骤放置在 repeat 步骤之前,而 until 步骤放置在 repeat 步骤之后。如果 until 步骤放置在 repeat 步骤之前,而一个非 true 的 emit 步骤放置在 repeat 步骤之后。
在所有其他情况下,repeat 步骤将为整个 loop 执行,之后才会执行 emituntil

这些限制可能会在 JanusGraph 添加对 DFS repeat 步骤执行的支持后得到解决(参见 issue #3787)。

多嵌套 repeat 步骤模式

默认情况下,当批处理起始步骤有多个 repeat 父步骤时,批处理注册会考虑所有 repeat 父步骤。
然而,在事务缓存很小且 repeat 步骤遍历深度超过一层的情况下,可能会导致某些顶点被重新获取,或者由于早期循环结束而不需要获取的顶点可能会被获取到事务缓存中。这意味着在没有必要时会浪费操作。

因此,JanusGraph 提供了一个配置选项 query.batch.repeat-step-mode 来控制多 repeat 步骤行为

  • 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 步骤的性能可能会受到限制。这是一个临时限制,直到此功能实现(参见 issue #3788)。

属性的批处理

一些启用了优化的 Gremlin 步骤可能会批量预取顶点属性。目前,JanusGraph 使用切片查询来查询部分行数据。单一切片查询包含起始键和结束键,以定义 JanusGraph 感兴趣的数据切片。
由于 JanusGraph 目前不支持多范围切片查询,它要么在单个切片查询中获取单个属性,要么在单个切片查询中获取所有属性。因此,用户必须权衡不同的属性获取方法,并决定何时在单个切片查询中获取所有属性(通常更快,但可能会获取不必要的属性),或者在每个属性单独的切片查询中仅获取请求的属性(可能会稍慢,但只会获取请求的属性)。

参见 issue #3816,它将允许通过单个切片查询仅获取请求的属性。

参见配置选项 query.fast-property,当请求直接顶点属性时(例如 vertex.properties("foo")),它可用于在首次单个属性访问时预取所有属性。
参见配置选项 query.batch.has-step-mode,以控制 has 步骤的属性预取行为。
参见配置选项 query.batch.properties-mode,以控制 valuespropertiesvalueMappropertyMapelementMap 步骤的属性预取行为。
参见配置选项 query.batch.label-step-mode,以控制 label 步骤的标签预取行为。
参见配置选项 query.batch.drop-step-mode,以控制 drop 步骤的删除批处理行为。