将磁盘溢出添加到矢量执行引擎中的方法指南

110 阅读8分钟

去年年底,我们推出了矢量执行引擎的v1版。该引擎支持基于列的查询执行,并加快了复杂的连接和聚合,提高了CockroachDB的分析能力(首先是针对OLTP工作负载的优化)。从CockroachDB v20.1版本开始,这些查询可以回落到磁盘(也被称为 "溢出 "到磁盘)。

在这篇文章中,我们自上而下地解释了我们是如何将磁盘溢出添加到矢量执行引擎中的,首先是对不同类型查询的磁盘算法的描述,最后是对所有这些算法使用的单一构建块的描述。请注意,磁盘溢出是默认的行时执行引擎的一个现有功能。这篇文章特别涵盖了最近在列即时间矢量执行引擎中增加的磁盘溢出功能。

要了解更多关于我们为什么以及如何创建矢量化引擎的信息,请参阅我们在2019年10月发表的《我们如何构建矢量化执行引擎》博文。

磁盘溢出操作符

排序

让我们先来了解一下排序和它们的内存使用情况。当一个带有关键字的查询发出时,就会计划一个排序操作。ORDER BY关键字的查询时,就会计划一个排序操作。作为输入,该操作者需要一组图元,以任何顺序,以及一列索引来排序。然后,它输出按列索引列表排序的图元。

请注意,运算器在发出元组之前必须对整个输入进行缓冲,因为首先排序的元组可能在输入的末端。因此,必须缓冲的输入的大小可能超过一个运算符允许使用的内存量。这种内存,也被称为 "工作 "内存,在CockroachDB中默认限制为64MB。当工作内存达到极限时,排序必须能够溢出到磁盘,以便对输入进行完全排序。

为了解决这个问题,我们采取了分而治之的方法,采用了一种外部合并排序算法,当输入无法在内存中完全缓冲时,就使用磁盘内存。该算法被分解成两个阶段:排序和合并。

在排序阶段,操作者在内存中缓冲尽可能多的输入数据(前面提到的 "工作 "内存),在内存中进行逐列排序,然后将排序后的分区写入磁盘。这个阶段不断重复,直到没有更多的图元需要处理。

一旦排序阶段结束,磁盘上将有N个排序的分区。然后,这些分区被合并以产生排序后的输出。

GRACE哈希连接

散列连接是一种连接算法,它根据一组相等的列来连接两个输入流。它使用一个哈希表来存储较小的数据流,然后用较大的数据流来探测该表。

例如,假设一个用户要发布SELECT * FROM customers, orders WHERE orders.cust_id = customers.id ,得到一个结果,其中每一行都包含客户数据和他们发出的一个订单。在这个查询的执行过程中,哈希连接操作者在内存中建立了一个customers 表的哈希表(它是较小的那个),其中的键是客户ID。然后,它使用订单表进行查询,并将结果发布出来。

这个例子中的内存使用量随着customers 表的增长而增长,因为整个表需要存储在内存中。为了尊重工作内存的64MB限制,哈希连接在溢出到磁盘时也使用了分而治之的方法。这种类型的哈希连接被称为GRACE哈希连接。

在GRACE散列连接中,orderscustomers 表中的所有图元都可以通过基于客户ID的散列,被分配到N个磁盘分区中的一个。正因为如此,所有具有相同客户ID的订单和客户图元最终都会出现在同一个分区里。然后,可以从磁盘上读取分区,并使用原来的内存算法将其连接起来,产生相同的输出。这就把原始问题分成了N个子问题。

请注意,GRACE散列连接只有在单个分区的大小不超过运算器的工作内存时才有效,因为分区必须被完全读入内存。为了绕过这个限制,算法可以简单地对大分区应用同样的分而治之的方法,如果它变得太大的话(即重新分区)。在边缘情况下,有可能存在具有相同连接列的图元,使得一个分区的大小不可能减少,无论重新分区的尝试次数如何。在这种情况下,对分区进行排序并使用合并连接

合并连接

合并连接的输出结果与哈希连接相同,但只在输入已经按平等列排序的情况下使用。正如在散列连接的例子中customers ,合并连接避免了用输入的一边构建散列表的需要,使运算符更有效率。合并连接操作符可以简单地推进两个输入流,直到图元在等价列上匹配,输出连接这些图元的结果,然后转到在等价列上匹配的下一组图元。

合并连接运算符通常被认为是一种流算法,因为在合并连接过程中不需要缓冲太多的状态。然而,在两个输入流都有许多具有相同平等列值的图元的情况下,操作者需要至少在一边缓冲所有这些图元,因为结果将是两组图元的交叉乘积。在这种情况下,溢出到磁盘是非常简单的,因为唯一需要的是一个将被多次重放的只附加的日志。

构建块

到目前为止所涉及的所有算法都有一个共同的磁盘使用访问模式:它们将数据追加到磁盘队列中(也被称为只追加的日志),并按顺序从该队列中读取(可能不止一次)。

在高层次上,调用者可以对图元的列式批次进行排队和取消排队。它还可以重置队列,以便从队列的前部返回到脱队。在引擎盖下,批次被序列化、压缩,并附加到一个文件中。如果文件超过了一定的大小,队列就会滚动到一个新的文件。当调用者从这些文件中读取时,一个内存中的游标被维护并递增。这个设计,包括替代方案,将在本RFC中更深入地介绍

批量文件使用Apache Arrow IPC文件格式写入磁盘,这是一个关于如何序列化列数据的规范。尽管我们不使用Arrow批处理来直接表示矢量执行引擎中的物理数据,但我们使用一种非常类似的表示方法,可以轻松有效地转换为Arrow批处理并以此进行序列化。

例如,假设我们有一批字符串,使用平面字节表示,由三个缓冲区组成。

  • 一个空位图,代表任何空位。
  • 一个单字节缓冲区,代表所有的字符串。
  • 一个附带的偏移量缓冲区,代表字节缓冲区中各个字符串的开始和结束索引。

这三个缓冲区被转换为字节,被视为Arrow缓冲区,然后使用相同的flatbuffer规范进行序列化,这通常包括一些指向这些缓冲区的元数据和缓冲区本身。

使用这种物理表示法可以通过使用*O(1)*转换到字节来避免复制,因为数据在内存中已经是连续的了。如果字符串被表示为一个二维数组,则需要通过分配一个新的缓冲区,然后迭代并复制每个元素到其中来准备序列化。

总结

在这篇文章中,我们介绍了如何在CockroachDB v20.1的矢量执行引擎中使用单一的构建块来实现各种磁盘算法以前可以使用无限制的内存的查询,现在最多可以使用恒定的工作内存,如果这个数量不够,就会溢出到磁盘。

随着磁盘溢出的增加,我们将experimental_on 矢量化模式改名为on,因为我们现在认为矢量化执行引擎已经可以用于生产,尽管它在默认情况下还没有完全启用。作为提醒,在v20.1中,只有使用流式(非缓冲)操作符的查询和可能读取的行数超过vectorize_row_count_threshold 的设置(默认为1,000)的查询才会默认运行。通过在会话中运行SET vectorize=onSET CLUSTER SETTING sql.defaults.vectorize=on ,所有支持的查询,包括溢出到磁盘的查询都将通过矢量执行引擎运行。

我希望你喜欢了解我们是如何将磁盘溢出添加到矢量执行引擎中的,并且我敦促你尝试启用它来加速任何复杂的连接或聚合。