使用 Gremlin 制作高效的 upserts - Amazon Neptune
Amazon Web Services 文档中描述的 Amazon Web Services 服务或功能可能因区域而异。要查看适用于中国区域的差异,请参阅中国的 Amazon Web Services 服务入门

本文属于机器翻译版本。若本译文内容与英语原文存在差异,则一律以英文原文为准。

使用 Gremlin 制作高效的 upserts

高效的 upserts 可以对 Gremlin 查询的性能产生重大影响。

upsert(或有条件插入)会重复使用顶点或边缘(如果已存在),但如果不存在,则创建它。

Upserts 允许你编写幂等的插入操作:无论你运行多少次这样的操作,总体结果都是相同的。这在高度并发的写入场景中非常有用,在这种情况下,对图表的同一部分进行并发修改可能会强制使用ConcurrentModifcationException, 因此需要重试.

例如,以下查询首先在数据集中查找指定的顶点,然后将结果折叠到列表中,从而使顶点变得更新。在提供给coalesce()步骤,然后查询将展开此列表。如果展开的列表不为空,则结果将从coalesce(). 但是,如果unfold()返回一个空集合,因为顶点当前不存在,coalesce()继续评估提供了它的第二次遍历,然后在第二次遍历中,查询将创建缺失的顶点。

g.V('v-1').fold() .coalesce( unfold(), addV('Person').property(id, 'v-1') .property('email', 'person-1@example.org') )

使用优化的形式coalesce()为了提示

Neptune 可以优化fold().coalesce(unfold(), ...)成语可以进行高吞吐量更新,但是这种优化只有在coalesce()返回顶点或边缘但没有其他任何东西。如果你试图从其中的任何部分返回不同的东西,例如财产coalesce(),不会发生 Neptune 优化。查询可能会成功,但它不会像优化版本一样执行,特别是对于大型数据集。

由于未优化的 upsert 查询会增加执行时间并降低吞吐量,因此值得使用 Gremlinexplain终端节点,以确定 upsert 查询是否已完全优化。在审核时explain计划,寻找以开头的线+ not converted into Neptune stepsWARNING: >>. 例如:

+ not converted into Neptune steps: [FoldStep, CoalesceStep([[UnfoldStep], [AddEdgeSte... WARNING: >> FoldStep << is not supported natively yet

这些警告可以帮助您识别查询中阻止其完全优化的部分。

有时不可能完全优化查询。在这些情况下,你应该尝试将无法优化的步骤放在查询结束时,从而允许引擎优化尽可能多的步骤。在一些批处理 upsert 示例中使用了此技术,其中,在将任何其他可能未优化的修改应用于同一顶点或边之前,都会执行一组顶点或边的所有优化 upsert。

批处理 upserts 以提高吞吐量

对于高吞吐量写入场景,您可以将 upsert 步骤链接在一起,以批量打乱顶点和边缘。批处理减少了打乱大量顶点和边缘的事务开销。然后,您可以通过使用多个客户端 parallel 处理批处理请求来进一步提高吞吐量。

作为一项经验法则,我们建议每个批处理请求刷新大约 200 条记录。记录是单个顶点或边标签或属性。例如,具有单个标签和 4 个属性的顶点会创建 5 条记录。带有标签和单个属性的边缘可创建 2 条记录。如果你想加快批次顶点(每个顶点都有一个标签和 4 个属性),你应该从批次大小为 40 开始,因为200 / (1 + 4) = 40.

您可以尝试批量大小。每批 200 条记录是一个很好的起点,但理想的批量大小可能会更高或更低,具体取决于您的工作负载。但是请注意,Neptune 可能会限制每个请求的 Gremlin 步骤的总数。这个限制没有记录在案,但是为了安全起见,请尽量确保您的请求包含不超过 1500 个 Gremlin 步骤。Neptune 可能会拒绝超过 1500 个步骤的大批量请求。

为了提高吞吐量,您可以使用多个客户端 parallel 加快批处理。客户端数应与 Neptune 写入器实例上的工作线程数相同,通常是服务器上 vCPUs 数的 2 倍。例如,r5.8xlarge实例有 32 个 vCPUs 和 64 个工作线程。对于高吞吐量写入场景,请使用r5.8xlarge,你将 parallel 时使用 64 个客户端向 Neptune 写批量不高兴。

每个客户都应提交批处理请求,然后等待请求完成,然后再提交另一个请求。尽管多个客户端并行运行,但每个客户端都以连续方式提交请求。这可以确保为服务器提供稳定的请求流,这些请求流占用所有工作线程,而不会淹没服务器端请求队列。

尽量避免生成多个遍历器的步骤

当 Gremlin 步骤执行时,它需要一个传入的遍历器,并发出一个或多个输出遍历器。一个步骤发出的遍历器的数量决定了下一步执行的次数。

通常,在执行批处理操作时,您希望每个操作(例如 upsert 顶点 A)执行一次,以便操作序列如下所示:upsert 顶点 A,然后是 upsert 顶点 B,然后是 upsert 顶点 C,依此类推。只要一个步骤只创建或修改一个元素,它就只发出一个遍历器,而表示下一个操作的步骤只执行一次。另一方面,如果操作创建或修改了多个元素,则会发出多个遍历器,这反过来导致后续步骤多次执行,每个发射的遍历器一次。这可能会导致数据库执行不必要的额外工作,在某些情况下可能会导致创建不必要的额外顶点、边缘或属性值。

请参阅混合 upserts 和插入了解如何处理可以发出多个遍历器的操作。

不高兴顶点

您可以使用顶点 ID 来确定是否存在相应的顶点。这是首选的方法,因为 Neptune 针对 ID 周围的高度并发使用案例优化 upserts。例如,以下查询会创建一个具有给定顶点 ID 的顶点(如果不存在),如果它存在,则会重复使用它:

g.V('v-1') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-1') .property('email', 'person-1@example.org')) .id()

请注意,此查询以id()步骤。虽然不是为了使顶点加息的目的绝对必要,但是添加id()步骤到 upsert 查询的末尾可确保服务器不会将所有顶点属性序列化回客户端,这有助于降低查询的锁定成本。

或者,您可以使用顶点属性来确定顶点是否存在:

g.V() .hasLabel('Person') .has('email', 'person-1@example.org') .fold() .coalesce(unfold(), addV('Person').property('email', 'person-1@example.org')) .id(

如果可能,请使用自己的用户提供的 ID 来创建顶点,然后使用这些 ID 来确定在 upsert 操作期间是否存在顶点。这可以让 Neptune 优化身份证周围的不高潮。在高度并发的修改场景中,基于 ID 的 upsert 可能比基于属性的 upsert 高效得多。

串联顶点不安

你可以将顶点 upserts 链接在一起以批量插入它们:

g.V('v-1') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-1') .property('email', 'person-1@example.org')) .V('v-2') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-2') .property('email', 'person-2@example.org')) .V('v-3') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-3') .property('email', 'person-3@example.org')) .id()

使边缘变得更加紧张

您可以像使用自定义顶点 ID 打开顶点一样,使用边缘 ID 来打开边缘。同样,这是首选的方法,因为它允许 Neptune 优化查询。例如,以下查询根据边缘 ID 创建边缘(如果尚不存在),或者如果它存在,则重复使用它。该查询还使用fromto如果需要创建新边缘,则会有顶点。

g.E('e-1') .fold() .coalesce(unfold(), addE('KNOWS').from(V('v-1')) .to(V('v-2')) .property(id, 'e-1')) .id()

许多应用程序使用自定义顶点 ID,但是留下 Neptune 来生成边缘 ID。如果您不知道边缘的 ID,但您确实知道fromto顶点 ID,你可以使用这个公式来设置边缘:

g.V('v-1') .outE('KNOWS') .where(inV().hasId('v-2')) .fold() .coalesce(unfold(), addE('KNOWS').from(V('v-1')) .to(V('v-2'))) .id()

请注意,中的顶点步骤where()子句应该是inV()(或者outV()如果你用过inE()找到边缘),而不是otherV(). 请勿使用otherV(),在这里,否则查询将无法优化并且性能将受到影响。例如,Neptune 不会优化以下查询:

// Unoptimized upsert, because of otherV() g.V('v-1') .outE('KNOWS') .where(otherV().hasId('v-2')) .fold() .coalesce(unfold(), addE('KNOWS').from(V('v-1')) .to(V('v-2'))) .id()

如果你不知道边缘或顶点 ID,你可以使用顶点属性 upsert:

g.V() .hasLabel('Person') .has('name', 'person-1') .outE('LIVES_IN') .where(inV().hasLabel('City').has('name', 'city-1')) .fold() .coalesce(unfold(), addE('LIVES_IN').from(V().hasLabel('Person') .has('name', 'person-1')) .to(V().hasLabel('City') .has('name', 'city-1'))) .id()

与顶点 upserts 一样,最好使用边缘 ID 或者使用基于 ID 的边缘 upsertsfromto顶点 ID,而不是基于属性的 upserts,以便 Neptune 可以完全优化 upsert。

检查fromto顶点存在

请注意创建新边缘的步骤的构建:addE().from().to(). 这种构造可确保查询检查是否存在fromto顶点。如果其中任何一个不存在,查询将返回如下所示的错误:

{ "detailedMessage": "Encountered a traverser that does not map to a value for child... "code": "IllegalArgumentException", "requestId": "..." }

如果可能的话from或者to顶点不存在,你应该尝试在突破它们之间的边缘之前打乱它们。请参阅 组合顶点和边缘 upsert

还有一种替代结构可以创建一个你不应该使用的边缘:V().addE().to(). 只有在from顶点存在。如果to顶点不存在,如前所述,查询会生成错误,但是如果from顶点不存在,它默默地无法插入边缘,而不会产生任何错误。例如,如果from顶点不存在:

// Will not insert edge if from vertex does not exist g.V('v-1') .outE('KNOWS') .where(inV().hasId('v-2')) .fold() .coalesce(unfold(), V('v-1').addE('KNOWS') .to(V('v-2'))) .id()

串联边缘

如果您想将边缘 upserts 链接在一起以创建批处理请求,则必须以顶点查找开始每个 upsert,即使您已经知道边缘 ID。

如果你已经知道你想打乱的边缘的 ID,以及fromto顶点,你可以使用这个配方:

g.V('v-1') .outE('KNOWS') .hasId('e-1') .fold() .coalesce(unfold(), V('v-1').addE('KNOWS') .to(V('v-2')) .property(id, 'e-1')) .V('v-3') .outE('KNOWS') .hasId('e-2').fold() .coalesce(unfold(), V('v-3').addE('KNOWS') .to(V('v-4')) .property(id, 'e-2')) .V('v-5') .outE('KNOWS') .hasId('e-3') .fold() .coalesce(unfold(), V('v-5').addE('KNOWS') .to(V('v-6')) .property(id, 'e-3')) .id()

也许最常见的批处理边缘 upsert 场景是你知道fromto顶点 ID,但不知道你想打乱的边缘的 ID。在这种情况下,请使用以下公式:

g.V('v-1') .outE('KNOWS') .where(inV().hasId('v-2')) .fold() .coalesce(unfold(), V('v-1').addE('KNOWS') .to(V('v-2'))) .V('v-3') .outE('KNOWS') .where(inV().hasId('v-4')) .fold() .coalesce(unfold(), V('v-3').addE('KNOWS') .to(V('v-4'))) .V('v-5') .outE('KNOWS') .where(inV().hasId('v-6')) .fold() .coalesce(unfold(), V('v-5').addE('KNOWS').to(V('v-6'))) .id()

如果你知道你想打乱的边缘的 ID,但不知道fromto顶点(这很不寻常),你可以使用这个配方:

g.V() .hasLabel('Person') .has('email', 'person-1@example.org') .outE('KNOWS') .hasId('e-1') .fold() .coalesce(unfold(), V().hasLabel('Person') .has('email', 'person-1@example.org') .addE('KNOWS') .to(V().hasLabel('Person') .has('email', 'person-2@example.org')) .property(id, 'e-1')) .V() .hasLabel('Person') .has('email', 'person-3@example.org') .outE('KNOWS') .hasId('e-2') .fold() .coalesce(unfold(), V().hasLabel('Person') .has('email', 'person-3@example.org') .addE('KNOWS') .to(V().hasLabel('Person') .has('email', 'person-4@example.org')) .property(id, 'e-2')) .V() .hasLabel('Person') .has('email', 'person-5@example.org') .outE('KNOWS') .hasId('e-1') .fold() .coalesce(unfold(), V().hasLabel('Person') .has('email', 'person-5@example.org') .addE('KNOWS') .to(V().hasLabel('Person') .has('email', 'person-6@example.org')) .property(id, 'e-3')) .id()

组合顶点和边缘 upsert

有时候你可能想要使顶点和连接它们的边缘都不高兴。你可以混合这里提供的批处理示例。下面的示例打开 3 个顶点和 2 条边:

g.V('p-1') .fold() .coalesce(unfold(), addV('Person').property(id, 'p-1') .property('email', 'person-1@example.org')) .V('p-2') .fold() .coalesce(unfold(), addV('Person').property(id, 'p-2') .property('name', 'person-2@example.org')) .V('c-1') .fold() .coalesce(unfold(), addV('City').property(id, 'c-1') .property('name', 'city-1')) .V('p-1') .outE('LIVES_IN') .where(inV().hasId('c-1')) .fold() .coalesce(unfold(), V('p-1').addE('LIVES_IN') .to(V('c-1'))) .V('p-2') .outE('LIVES_IN') .where(inV().hasId('c-1')) .fold() .coalesce(unfold(), V('p-2').addE('LIVES_IN') .to(V('c-1'))) .id()

混合 upserts 和插入

有时候你可能想要使顶点和连接它们的边缘都不高兴。你可以混合这里提供的批处理示例。下面的示例打开 3 个顶点和 2 条边:

Upserts 通常一次执行一个元素。如果你坚持这里显示的 upsert 模式,每个 upsert 操作都会发出一个遍历器,这会导致后续的操作只执行一次。

但是,有时候你可能想将不高兴与插入物混合起来。例如,如果您使用边缘来表示操作或事件的实例,则可能会出现这种情况。请求可能会使用 upserts 来确保所有必要的顶点都存在,然后使用插入来添加边。对于这种请求,请注意每个操作可能发出的遍历者数量。

考虑以下示例,它混合了 upserts 和插入内容,以在图中添加表示事件的边缘:

// Fully optimized, but inserts too many edges g.V('p-1') .fold() .coalesce(unfold(), addV('Person').property(id, 'p-1') .property('email', 'person-1@example.org')) .V('p-2') .fold() .coalesce(unfold(), addV('Person').property(id, 'p-2') .property('name', 'person-2@example.org')) .V('p-3') .fold() .coalesce(unfold(), addV('Person').property(id, 'p-3') .property('name', 'person-3@example.org')) .V('c-1') .fold() .coalesce(unfold(), addV('City').property(id, 'c-1') .property('name', 'city-1')) .V('p-1', 'p-2') .addE('FOLLOWED') .to(V('p-1')) .V('p-1', 'p-2', 'p-3') .addE('VISITED') .to(V('c-1')) .id()

查询应该插入 5 条边缘:2 个后面的边缘和 3 个访问的边缘。但是,写入的查询会插入 8 个边缘:接下来 2 次,6 次访问过。其原因是,插入 2 个 FOUNDER 边的操作会发出 2 个遍历器,导致后续插入 3 条边的插入操作执行两次。

解决办法是添加fold()在可能发出多个遍历器的每个操作之后一步:

g.V('p-1') .fold() .coalesce(unfold(), addV('Person').property(id, 'p-1') .property('email', 'person-1@example.org')) .V('p-2') .fold() .coalesce(unfold(), addV('Person').property(id, 'p-2'). .property('name', 'person-2@example.org')) .V('p-3') .fold() .coalesce(unfold(), addV('Person').property(id, 'p-3'). .property('name', 'person-3@example.org')) .V('c-1') .fold(). .coalesce(unfold(), addV('City').property(id, 'c-1'). .property('name', 'city-1')) .V('p-1', 'p-2') .addE('FOLLOWED') .to(V('p-1')) .fold() .V('p-1', 'p-2', 'p-3') .addE('VISITED') .to(V('c-1')). .id()

在这里我们插入了fold()在插入跟随边的操作之后一步。这会导致单个遍历器,然后导致后续操作只执行一次。

这种方法的缺点是查询现在没有完全优化,因为fold()未优化。接下来的插入操作fold()现在不会进行优化。

如果您需要使用fold()为了减少代表后续步骤的遍历数量,请尝试对操作进行排序,以便最便宜的操作占用查询中未优化的部分。

修改现有顶点和边缘的 Upserts

有时候你想创建一个顶点或边(如果不存在),然后向它添加或更新属性,无论它是新的还是现有的顶点或边缘。

要添加或修改属性,请使用property()步骤。在外面使用此步骤coalesce()步骤。如果您尝试修改现有顶点或边的属性coalesce()步骤中,Neptune 查询引擎可能无法对查询进行优化。

以下查询添加或更新每个升级顶点上的计数器属性。EAREproperty()step 具有单个基数来确保新值替换任何现有值,而不是添加到一组现有值中。

g.V('v-1') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-1') .property('email', 'person-1@example.org')) .property(single, 'counter', 1) .V('v-2') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-2') .property('email', 'person-2@example.org')) .property(single, 'counter', 2) .V('v-3') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-3') .property('email', 'person-3@example.org')) .property(single, 'counter', 3) .id()

如果你有属性值,例如lastUpdated时间戳值,适用于所有高潮元素,您可以在查询结束时添加或更新它:

g.V('v-1') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-1') .property('email', 'person-1@example.org')) .V('v-2'). .fold(). .coalesce(unfold(), addV('Person').property(id, 'v-2') .property('email', 'person-2@example.org')) .V('v-3') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-3') .property('email', 'person-3@example.org')) .V('v-1', 'v-2', 'v-3') .property(single, 'lastUpdated', datetime('2020-02-08')) .id()

如果还有其他条件决定是否应进一步修改顶点或边,则可以使用has()步骤来过滤要应用修改的元素。下面的示例使用了has()根据顶点的值筛选不满的顶点version财产。然后,查询将更新为 3version的任何顶点version小于 3:

g.V('v-1') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-1') .property('email', 'person-1@example.org') .property('version', 3)) .V('v-2') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-2') .property('email', 'person-2@example.org') .property('version', 3)) .V('v-3') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-3') .property('email', 'person-3@example.org') .property('version', 3)) .V('v-1', 'v-2', 'v-3') .has('version', lt(3)) .property(single, 'version', 3) .id()

使用sideEffect()在一个不高兴

如果要删除属性作为 upsert 的一部分,必须使用sideEffect()步骤,但是,目前 Neptune 查询引擎尚未对其进行优化。这意味着sideEffect()step,查询的所有后续部分都将使用 TinkerPop 而不是 Neptune 的步骤。

以下查询打乱了 3 个顶点,删除score使用每个顶点的属性sideEffect()直接跟随每个顶点 upsert:

// Optimizes only the first upsert g.V('v-1') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-1'). .property('email', 'person-1@example.org')) .sideEffect(properties('score').drop()) .V('v-2') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-2') .property('email', 'person-2@example.org')) .sideEffect(properties('score').drop()) .V('v-3') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-3') .property('email', 'person-3@example.org')) .sideEffect(properties('score').drop()) .id()

由于sideEffect()未优化,第一个之后的所有内容sideEffect()将使用未优化 TinkerPop 步骤。换句话说,只有第一个顶点 upsert 才会被优化。为了提高查询的性能,你可以移动所有sideEffect()到底的行为:

g.V('v-1') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-1') .property('email', 'person-1@example.org')) .V('v-2'). .fold(). .coalesce(unfold(), addV('Person').property(id, 'v-2') .property('email', 'person-2@example.org')) .V('v-3') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-3') .property('email', 'person-3@example.org')) .sideEffect(V('v-1', 'v-2', 'v-3').properties('score').drop()) .id()

现在所有 upsert 步骤都将进行优化。这些区域有:sideEffect()这里的步骤仍未优化,但它对查询性能的影响已最小化。