如何用台式栏基准备忘录[或neo4j]?
#database #neo4j #memgraph #benchmark

准备,运行和评估基准是一个乏味且耗时的过程。在整个基准测试过程中,最难的一步是建立类似生产的情况。生产环境中有很多数据库,其中一堆具有不同的目的,运行不同的工作负载,并且以不同的方式操作。这意味着不可能模拟您的特定工作量以及数据库的运行方式,因此进行基准测试是一个重要因素。最重要的是,每个用例都有一些用于评估系统性能的不同度量。

由于我们在CI/CD环境中运行台式图,因此我们决定进行小型调整以减轻孟加尔(Memgraph)(和neo4jð)在您的硬件,工作量和下方运行基准测试的过程对您重要的条件。目前,重点是执行纯码头查询。

要能够运行这些基准测试,您至少需要安装和运行Docker,以及对Python的非常基本的知识。

在开始工作负载之前,您需要进行download benchgraph zip或拉动Memgraph Github repository。如果您使用的是MEMGRAPH存储库,则只需将自己放置在/tests/mgbench中,如果您使用的ZIP只是将其解压缩并打开您喜欢的IDE或代码编辑器中的文件夹。

添加您的工作量

在项目结构中,您将看到各种不同的脚本,请查看Benchgraph architecture,以更详细地概述这些脚本所做的内容。但是,重要的文件夹是Workloads文件夹,您将在其中添加工作量。您可以首先创建一个简单的空Python脚本,并给它一个名称,在我们的情况下是demo.py

为了指定您可以在memgraph和neo4j上运行的工作负载,您需要在脚本中实现一些详细信息。这是您需要执行的5个步骤:

  1. 继承工作负载类
  2. 定义工作量名称
  3. 实现数据集生成器方法
  4. 实施索引生成器方法
  5. 定义要基准的查询

这五个步骤将导致类似于demo.py示例的简化版本:

import random
from workloads.base import Workload

class Demo(Workload):

    NAME = "demo"

    def dataset_generator(self):

        queries = []
        for i in range(0, 10000):
            queries.append(("CREATE (:NodeA {id: $id});", {"id": i}))
            queries.append(("CREATE (:NodeB {id: $id});", {"id": i}))
        for i in range(0, 50000):
            a = random.randint(0, 9999)
            b = random.randint(0, 9999)
            queries.append(
                (("MATCH(a:NodeA {id: $A_id}),(b:NodeB{id: $B_id}) CREATE (a)-[:EDGE]->(b)"), {"A_id": a, "B_id": b})
            )

        return queries

    def indexes_generator(self):
        indexes = [
                    ("CREATE INDEX ON :NodeA(id);", {}),
                    ("CREATE INDEX ON :NodeB(id);", {}),
                ]
        return indexes

    def benchmark__test__get_nodes(self):
        return ("MATCH (n) RETURN n;", {})

    def benchmark__test__get_node_by_id(self):
        return ("MATCH (n:NodeA{id: $id}) RETURN n;", {"id": random.randint(0, 9999)})

让S这个demo.py脚本分成较小的零件,并解释您需要执行的步骤,以便更容易理解正在发生的事情。

1.继承workload

演示类具有父级工作负载。每个自定义工作负载应从基本工作负载类继承。这意味着您需要在脚本的顶部添加导入语句,并在Python中指定继承。

from workloads.base import Workload

class Demo(Workload):

2.定义工作负载名称

类应指定NAME属性。这用于描述您要执行的工作负载类。调用benchmark.py(运行基准过程的脚本)时,此属性将用于区分不同的工作负载。

NAME = "demo"

3.实施数据集生成器方法

类应实现dataset_generator()方法。该方法生成一个返回元组列表的数据集。每个元组都包含一个cypher查询和字典的字符串,其中包含可选参数,因此结构如下所示。让我们看一下示例列表的样子:

queries = [
    ("CREATE (:NodeA {id: 23});", {}),
    ("CREATE (:NodeB {id: $id, foo: $property});", {"id" : 123, "property": "foo" }),
    ...
]

如您所见,您可以将Cypher查询作为纯字符串传递,而字典中没有任何值。

("CREATE (:NodeA {id: 23});", {}),

或者您可以在字典中指定参数。查询字符串中$符号旁边的变量将被字典的键背后的适当值替换。在这种情况下,$id被123取代,而$property被FOO取代。字典键名和可变名称需要匹配。

("CREATE (:NodeB {id: $id, foo: $property});", {"id" : 123, "property": "foo" })

回到我们的demo.py示例,在dataset_generator()方法中,您可以在这里指定用于生成数据集的查询。这意味着数据集生成的所有查询均基于此指定的查询列表。请记住,由于数据库在Docker中运行,因此您无法直接导入数据集文件(例如CSV,JSON等),您需要将数据集文件转换为纯Cypher查询。但这在Python中不应该太难了。

demo.py中,第一个循环正在准备使用标签nodea创建10000个节点的查询,并带有标签nodeb的10000个节点。我们正在使用random类来生成一个随机的整数ID序列。每个节点的ID在0到9999之间。在第二个循环中,随机连接节点的查询是随机生成的。总共有50000个边缘,每个边缘连接到随机节点和节点。

def dataset_generator(self):

    for i in range(0, 10000):
        queries.append(("CREATE (:NodeA {id: $id});", {"id" : i}))
        queries.append(("CREATE (:NodeB {id: $id});", {"id" : i}))
    for i in range(0, 50000):
        a = random.randint(0, 9999)
        b = random.randint(0, 9999)
        queries.append((("MATCH(a:NodeA {id: $A_id}),(b:NodeB{id: $B_id}) CREATE (a)-[:EDGE]->(b)"), {"A_id": a, "B_id" : b}))

    return queries

4.实施索引生成器方法

类还应实现indexes_generator()方法。这是与dataset_generator()方法相同的方法,而不是对数据集的查询,indexes_generator()应返回将要使用的索引列表。当然,您可以在工作量中包含约束和其他查询。来自indexes_generator()的查询列表将在数据集生成器的查询之前执行。返回结构再次是包含查询字符串和参数字典的单元列表。这是一个示例:

def indexes_generator(self):
    indexes = [
                ("CREATE INDEX ON :NodeA(id);", {}),
                ("CREATE INDEX ON :NodeB(id);", {}),
            ]
    return indexes

5.定义要基准的查询

现在,您的数据库已具有索引,并且导入数据集,您可以指定要在给定数据集上进行基准测试的查询。这是demo.py工作负载定义的两个查询。它们被写为python方法,它像数据生成器方法一样返回带有查询和词典的单个元组。

def benchmark__test__get_nodes(self):
    return ("MATCH (n) RETURN n;", {})

def benchmark__test__get_node_by_id(self):
    return ("MATCH (n:NodeA{id: $id}) RETURN n;", {"id": random.randint(0, 9999)})

这里的必要详细信息是,您希望在基准测试中使用的每种方法都需要以名称为benchmark__开始,否则将被忽略。完整的方法名称具有以下结构benchmark__group__name。该组可用于执行特定的测试,但稍后进行更多。

从工作负载设置中,这就是您需要做的全部。下一步是运行您的工作量。

在工作量上运行基准测试

让我们从上面的示例运行demo.py工作量的最直接方法开始。管理基准执行的主要脚本是benchmark.py,它接受了各种不同的参数,但我们将到达那里。打开您选择的终端,然后将自己放置在下载的台式文件夹中。

要启动基准测试,您需要运行以下命令:

python3 benchmark.py vendor-docker --vendor-name ( memgraph-docker || neo4j-docker ) benchmarks demo/*/*/* --export-results result.json --no-authorization

例如,要在 memgraph 上运行此操作,该命令看起来像这样:

python3 benchmark.py vendor-docker --vendor-name memgraph-docker benchmarks “demo/*/*/*” --export-results results.json --no-authorization

几秒钟或几分钟后,根据您的工作量,应执行基准。
在您的终端中,您应该看到与此类似的东西:

benchgraph terminal output

最后,您可以看到结果的摘要,随时探索和编写不同的查询,以查看您可以期望的表演类型。

要在neo4j上运行相同的工作量,只需将–vendor-name参数更改为neo4j-docker即可。如果您偶然发现了设置特定索引或查询的问题,请查看如何在不同供应商上运行相同的工作量。

如何比较结果

一旦两个供应商或具有不同配置的基准测试,结果就会保存在--export-results参数指定的文件中。您可以使用结果文件,并通过compare_results.py脚本将它们与其他供应商结果进行比较:

python3 compare_results.py --compare path_to/run_1.json path_to/run_2.json --output run_1_vs_run_2.html --different-vendors

输出是一个HTML文件,具有两个比较供应商之间性能差异的可视化表示。第一个传递的摘要JSON文件是参考点。随时在手头的任何浏览器中打开HTML文件。

benchmark output html file

如何配置基准运行

配置基准运行将使您能够在不同条件下查看事物的变化。上述运行中使用的一些论点是自我解释的。有关完整列表,请查看Benchmark.py脚本。现在,让我们分解最重要的:

  • NAME/VARIANT/GROUP/QUERY-参数demo/*/*/*说要执行名为demo的工作负载及其所有变体,组和查询。此标志用于直接控制您希望执行的工作负载。 NAME这是工作负载类中定义的工作负载的名称。 VARIANT是一种额外的工作负载配置,将稍后再解释。 GROUP在查询方法名称中定义,而QUERY是您希望执行的查询名称。如果您想从demo.py执行特定查询,则看起来像这样:demo/*/test/get_nodes。这将在所有variants上运行demo工作量,在test查询组和查询get_nodes中。

  • --single-threaded-runtime-sec-当前的问题是您希望作为数据库基准的示例执行的每个特定查询中有多少个。每个查询都可能需要不同的时间来执行,因此固定一个数字可以产生一些查询,以在1秒内完成一些查询,而另一些则可以在一分钟内运行。此标志定义了持续时间,该持续时间将用于近似您希望执行多少查询。默认值为10秒,这意味着benchmark.py将生成预定数量的查询数,以近似于10秒钟的单个单个运行时。增加这将产生更长的运行测试。每个特定查询将获得不同的计数,以指定将生成多少查询。测试后可以检查一下。例如,在10秒钟的单线程运行时,emo Workload get_node_by_id的查询获得了64230不同的查询,而get_nodes的查询由于查询的时间复杂性不同。

  • --num-workers-for-benchmark-标志定义了多少并发客户端将打开并查询数据库。使用此标志,您可以模拟连接到数据库并执行查询的不同数据库用户。每个客户端都是独立的,并尽可能快地执行查询。他们共享由--single-threaded-runtime-sec产生的总查询池。这意味着需要执行的查询总数在指定的工人数量之间共享。

  • --warm-up-热身标志可以采用三个不同的参数,coldhotvulcanic。冷是默认值。没有进行热身执行,hot将在基准测试之前执行一些预定义的查询,而vulcanic将在进行测量之前先运行整个工作负载。这是warm-up

  • 的实现

如何在不同供应商上运行相同的工作量

基本workload class具有benchmarking context信息,其中包含基准运行中使用的所有基准参数。上面提到了一些。这里的关键参数是--vendor-name,它定义了此基准中使用的数据库。

在创建工作量期间,您可以使用self.benchmark_context.vendor_name访问父级属性。例如,如果要为每个供应商指定特殊索引创建,则indexes_generator()看起来像这样:

 def indexes_generator(self):
        indexes = []
        if "neo4j" in self.benchmark_context.vendor_name:
            indexes.extend(
                [
                    ("CREATE INDEX FOR (n:NodeA) ON (n.id);", {}),
                    ("CREATE INDEX FOR (n:NodeB) ON (n.id);", {}),
                ]
            )
        else:
            indexes.extend(
                [
                    ("CREATE INDEX ON :NodeA(id);", {}),
                    ("CREATE INDEX ON :NodeB(id);", {}),
                ]
            )
        return indexes

相同的dataset_generator()也是如此。在生成数据集期间,您可以为不同的供应商使用特殊类型的查询,以模拟相同的方案。

愉快的基准测试

有一个有趣的基准测试memgraph和neo4j!我们很想听听您在Discord server上的结果。如果您有了解正在发生的事情的问题,请查看台面架构或伸出援手!

Read more about Memgraph internals on memgraph.com