MemraphDB:为什么以及如何实现Bolt协议v4

2020-10-30 02:39:06

今天,我们自豪地宣布发布Memgraph 1.2,它显著提高了Memgraph与更广泛的图形生态系统的兼容性。这使得开发人员和数据科学家更容易使用他们喜欢的工具来使用Memgraph。

此版本中最大的变化之一是增加了Boltv4和v4.1支持。

在这篇文章中,我们将探索Bolt协议到底是什么,它给我们带来了什么,以及我们是如何在Memgraph中实现它的。

如果您正在考虑在您的应用程序中使用Memgraph,其中一个要求是可以尽可能少地直接从您的应用程序中查询Memgraph。您可以通过用您想要支持的语言编写Memgraph服务器的驱动程序来实现这一点。驱动程序是遵循预定义规则(也称为协议)的特定库,用于在应用程序和服务器之间进行通信。

Memgraph没有定义自己的规则,而是决定使用Neo4j的称为Bolt的协议。做出这一决定有三个重要原因:

Neo4j也使用Cypher(这并不意味着Bolt协议不能用于其他查询语言!)。

换句话说,通过使我们的服务器与Bolt协议兼容,您只需使用Neo4j的库就可以在上面列出的任何语言和框架中使用Memgraph。

首先,我们需要知道如何交换信息。Bolt在客户端和服务器之间使用请求-响应模式交换消息。每条请求消息后面可以跟零个或记录消息,然后这两个消息后面跟一个摘要消息。记录消息的不同可能性取决于请求消息的类型。

此外,我们还需要知道如何序列化我们的数据。Bolt使用自己的PackStream,它为序列化一系列不同类型的数据提供了规范。它与Cypher支持的类型完全兼容。我们不会深入讨论细节,但是每种类型都是用它的标记、大小和数据来定义的。

其中一种类型是结构。结构的大小定义了包含多少字段,字段可以是任何其他类型。但我们遗漏了一条重要信息。我们怎么知道这个结构代表什么呢?结构承载附加数据,其标记字节。这个标签告诉我们这个结构代表什么。我们现在已经理解了如何定义请求和响应消息。

我们不能指望我们的数据总是足够小,可以一次发送所有数据。为了解决这个问题,Bolt定义了消息的分块方式。每个块都以两字节头开始,它告诉我们块数据的大小(以字节为单位),然后是块数据本身。现在,我们有了另一个问题。我们怎么知道我们是否收到了最后一段消息呢?我们只要加个记号笔就行了!在我们的情况下,武器一直到最后一块00 00。

现在,我们已经拥有了定义我们的信息所需的一切。我们可以将每种类型的请求/响应消息定义为唯一的结构,具有唯一的字段集。我们可以使用前面定义的分块方法发送定义的结构。我们设置好了!我们现在可以序列化和反序列化消息了!

一个好的协议规范应该包含尽可能多的信息。没有足够的信息,我们只能猜测我们的服务器或客户端在某些情况下应该如何运行,这给每个试图实现该协议的开发人员带来了很多令人头疼的问题。

因此,作为一个很好的协议,Bolt定义了如何解析不同类型的请求消息并发送正确的响应消息。它定义了每个请求消息的外观,以及在每个可能的情况下作为响应消息发送的内容。此外,它还定义了每个请求消息及其结果之后的服务器状态。

如果想要更深入地研究Bolt协议规范,您可以在这里找到所有内容。

实现规则并不难,但要有效地实现它,需要进行大量仔细的规划,并对协议的工作原理有很好的理解。

与任何软件一样,协议很容易更改。Bolt使用主要版本和次要版本来定义它的版本。在每个连接开始时,客户端需要与服务器握手。

是的,Memgraph确实支持Bolt协议。但到目前为止,它只支持Bolt v1,而目前的版本是4.1。通过查看握手过程,我们可以得出结论,客户端最多只能支持四个版本。合乎逻辑的想法是,客户端将始终支持最新的4个版本。在写这篇文章的时候,最新的版本是v4.1,它把v1赶出了支持列表,这让我们和其他所有想要使用Neo4j的驱动程序来尝试Memgraph的人感到非常难过。

需要强调的是,在1.0版之后,没有记录更新的版本,这使得跟上更新的版本变得非常困难。但是,在4.1版之后,Neo4j决定很好地记录每个版本,让我们的生活变得容易得多。感谢Neo4j!

因为Memgraph只与Bolt协议的第一个版本兼容,所以我们有三个主要的和一个小的版本更改要跟上,大部分只是对现有消息的一些基本添加,但也有一些更大的更改。例如,我们决定保留对Bolt v1的支持。这是非常具有挑战性的,因为编程中最困难的事情之一是在不破坏旧行为的情况下对现有代码进行更大的更改。

处理每个版本行为不同的代码可能很困难。在我们决定了特定连接的版本之后,我们需要注意该版本允许哪些消息、每条消息应该生成哪个响应、允许哪些参数,等等。要做到这一点,同时尽可能多地重用代码,并保持可读性,这可能是一个挑战。在这里,我能给您的唯一真正的建议是编写尽可能多的测试,这些测试将覆盖尽可能多的内容,因为最小的细节可能会使您的服务器在实现对协议的支持时行为不正常。

在Boltv3中,添加了处理事务的新请求消息。这些消息用于通过提交或回滚更改来启动显式事务和结束事务。因为我们已经支持事务,并且您已经可以通过运行由BEGIN、COMMIT和ROLLBACK命令组成的查询来做同样的事情,所以我们唯一需要做的就是添加在收到相应请求时直接运行这些序列的函数。

对Bolt协议最大的更改是对拉和丢弃消息的更改。在我们深入研究之前,让我们先解释一下这些消息。当您要使用Bolt消息在服务器上运行查询时,首先需要发送包含我们要执行的查询的Run消息。为了获得查询的结果,我们发送一条Pull消息,如果我们想丢弃结果,只需发送Discard消息即可。处理这种情况的自然方法是在我们收到Run消息时准备查询,并在我们收到Pull消息时执行它。此外,为了避免浪费内存,我们不保存结果,只需将其转发给编码器,然后直接发送给客户端。

在Boltv1中,有Pull_ALL和DIRECAD_ALL消息。顾名思义,你唯一的选择就是要么全有要么什么都没有。考虑到这一点,我们开发了一个解决方案,在客户端收到PULL_ALL消息后,它将简单地将所有结果流式传输到客户端。但是,从V4.0开始,事情变得稍微复杂一些。Pull_All消息被重命名为Pull。此外,Pull消息可以带有一些额外的参数。

现在,您可以提取任意数量的结果。这一小小的更改意味着对现有代码的大量更改。最简单的解决方案是在第一次拉入时执行查询,并将所有结果保存在内存中。在那之后,对于每一次拉取,我们只发送下一个n个结果。尽管这是最容易实现的解决方案,但在内存方面效率太低。考虑到这一点,我们有一个严格的要求,既要保留旧的、懒惰的行为,又不能在内存中保留任何结果。

有不同类型的查询,每个查询需要不同的方法来实现此行为。具有恒定结果大小的查询(如概要分析和解释查询)可以有一个简单的结果矢量,从该矢量中可以相对地提取结果。对于大多数结果大小可变的查询,我们为执行准备所有必要的资源,只在需要的时候才请求下一个结果,然后将结果立即流式传输到客户端。资源在返回上一个结果的Pull请求之后被清理。这是可能的,因为Memgraph懒惰地处理执行。

令人惊讶的是,最难实现的查询是转储查询,就其本身而言,实现该查询非常简单。您分析数据库的不同部分,结果是发送定义该部分的查询。例如,我们迭代数据库中的每个顶点,然后发回创建该顶点的查询。如前所述,创建顶点只是转储查询结果的一部分。我们需要为许多不同的部分做同样的事情,比如定义索引和约束,使用新的Bolt协议,我们需要懒惰地做每件事。这意味着转储查询的执行可以在任何地方、任何时间停止。我们最终采用的一个解决方案是将每个部分定义为一个独立的块。每一块都需要跟踪它的状态,以便从它停止的地方继续,并知道它什么时候结束。我们还需要定义将迭代这些块的对象,仅当前一个块完成时才继续到下一个块。这样,我们在定义每个块时就不需要考虑其他块了。我们可以通过实现特定的接口轻松地添加新的块,最重要的是,没有结果保存在内存中。

在显式事务中,每个运行消息返回一个唯一定义该执行的qid。使用该qid,我们可以随时从显式事务内的每个未完成的执行中拉出。随着返回qid的API的小变化,我们需要保存关于每个未完成的执行的信息。当然,这里和那里都有一些内存问题,但在设计解决方案时,最重要的是如何找到由该qid表示的执行。我们决定使用qid作为执行列表中每个执行的索引。您唯一需要注意的是删除已完成的查询,这样您的qid和索引就不会不同步。

以上所有内容都适用于重命名为DiscardAll的DISCARD_ALL消息。

在Memgraph的以前版本中,DIRECAD_ALL消息不会产生正确的行为,当我们实现该协议时,我们得到的唯一信息是DIRECAD_ALL消息会丢弃所有结果。我们的结论是,这意味着我们可以安全地忽略准备好的执行。我们很久以后才发现,在社区成员的帮助下,该消息应该已经执行了查询,并简单地丢弃了所有结果,即结果不应该被流式传输。通过执行有副作用的查询并发送DISCARD_ALL消息,这个错误非常明显。我们在最新版本的服务器中修复了这个问题,但这是一个很好的例子,说明应该如何总是尽可能详细地定义协议。

我们计划尽可能跟上最新版本的Bolt协议。4个版本的池给了我们一些回旋余地,但我们的工作仍然是跟上最新版本的Neo4j驱动程序,这样你就可以从许多不同的语言查询Memgraph。

虽然Memgraph支持Neo4j驱动程序,但我们也在使用Bolt协议开发我们自己的驱动程序,以提供更好的性能和开发人员体验。到目前为止,我们已经实现了以下驱动程序:

在这篇博客文章中,我们探讨了支持Neo4j驱动程序的意义,以及为什么跟上最新版本并不总是那么容易。

既然Memgraph支持最新版本的Bolt协议,我们鼓励您尝试一下,并让我们知道您的想法。

如果您想不出一个示例来试用Memgraph,请不要担心,我们在“如何以编程方式查询Memgraph?”一书中为每个支持的驱动程序提供了示例。

此外,错误和错误总是可能的,所以请随时在我们的论坛上报告任何奇怪的行为。