gpu-vs-cpu-performance.jpg

作者:

编辑:汤姆政府高级官员,Caitin陈

TiDB是一个混合事务/分析处理(HTAP)数据库,可以有效地处理分析查询。但是,当涉及到大量数据时,CPU就会成为处理以下查询的瓶颈加入语句和/或聚合函数。

与此同时,GPU在科学计算、人工智能、数据处理等领域迅速普及。在这些领域,它的性能比CPU高出几个数量级。gpu加速的数据库也正在兴起,并在数据库市场上引起了广泛的关注。

我们认为有可能使用GPU加速技术来增强TiDB,以提高cpu密集型分析查询处理的性能。这个想法后来成了我们的2020年TiDB黑客马拉松项目中,我们取得了10倍~150倍的业绩提升,并获得了两枚奖牌(三等奖和云起合作伙伴最佳市场潜力奖)。

在这篇文章中,我们将分享我们项目的一些技术细节,并分析我们的基准测试结果。

基本原理

我们的目标是gpu加速查询处理中cpu最密集的部分,即处理内存中数据的关系运算符,如连接和聚合。因此,我们需要使用GPU编程技术重新实现这些操作符。虽然这个想法很简单,但一个主要的挑战是TiDB是用Golang实现的,而GPU通常是用C/ c++类语言编程的。这一挑战直接导致了以下设计决策:

  • 我们将使用计算统一设备架构(CUDA)作为我们的GPU编程语言。它对程序员很友好,并且有大量的文档。
  • GPU操作符将在c++ /CUDA而不是Golang中实现。
  • TiDB (Golang)和GPU操作符(c++ /CUDA)将使用进程内通信。这样,我们就不会引入远程过程调用(rpc)、序列化/反序列化和网络传输开销。因此,go是我们唯一的语言选择。
  • 我们需要一个轻量级协议来简化TiDB和GPU操作符之间的编程边界。这将减轻go带来的编程负担。

这些设计决策是我们架构背后的驱动力,下面将对其进行描述。

体系结构

下图显示了我们的体系结构。正如您所看到的,TiDB进程可以分为用Golang编写的组件(蓝色框)和用c++ /CUDA编写的组件(绿色框)。

gpu加速TiDB的架构
gpu加速TiDB的架构

所有TiDB遗留结构,包括解析器、规划器、优化器和执行器都在Golang世界中。我们还添加了另外两个组件:计划转换器和执行适配器。第一个组件将TiDB查询计划转换为CUDA关系代数或“CURA”计划,另一个组件使TiDB执行模型适应CURA计划的执行,并适当地提供实际数据。CURA是一个用c++ /CUDA实现的库,包含我们的GPU运算符,以及一些辅助组件来组织和执行GPU运算符。

该体系结构还包括一个协议,该协议定义CURA与执行适配器之间的交互。它公开为几个CURA C api。执行适配器使用go来调用该协议。(见灰色箭头。)

你可以在这里找到我们的源代码:

查询处理

查询处理的解析、规划和优化步骤与前面基本相同。我们的工作重点是建筑图形中的橙色盒子。

计划翻译

如下图所示,给定一个优化的TiDB查询计划(具有最优的连接顺序和适当修剪的列),计划转换器将挑选除以下操作符之外的所有操作符表扫描s(灰色圆圈),并将它们打包到单个CURA计划(绿色圆圈)中。

计划翻译
查询计划翻译

随后将原始操作符树转换为新的操作符树,其中单个CURA计划节点作为根节点,所有原始操作符树都是根节点表扫描S是树叶。表扫描s主要是I/ o绑定的,因此保持不变。CURA计划指示GPU执行查询的哪一部分。将CURA计划与原始操作符树中的多个节点打包是很重要的。这样,我们就可以避免在主机和设备之间来回复制不必要的内存。

上图显示了以下查询的计划转换:

SELECT * FROM t0 JOIN t1 ON t0。Id = t1。id WHERE to 0。值= 42

转换后的CURA计划是子树的json格式表示,子树由左边的灰色虚线圈包围,带有一个占位符。InputSource,为每个表扫描节点。每一个InputSource是以后锚定的实际吗表扫描在CURA执行期间,这将在下一节中描述。下面的代码片段是这个CURA计划的实际JSON文本。如果您对CURA计划不感兴趣,可以跳到下一节。

{“rel”:[{“rel_op”:“在InputSource”,“source_id”:0,“模式”:[{“类型”:“INT32”、“可空”:假},{“类型”:“INT32”、“空”:假}]},{“rel_op”:“过滤器”,“条件”:{“binary_op”:“平等”,“操作数”:[{“col_ref”:1},{“类型”:“INT32”、“文字”:42}],“类型”:{“类型”:“BOOL8”,“空”:假}}},{“rel_op”:“在InputSource”,“source_id”:1、“模式”:[{“类型”:“INT32”、“空”:假}]},{“rel_op”:“HashJoin”、“类型”:“内心”,“build_side”:“左”,“条件”:{“binary_op”:“平等”,“操作数”:[{“col_ref”:0},{“col_ref”:2}],“类型”:{“类型”:“BOOL8”,“空”:假}}}]}

看台执行

看台执行
看台执行

在执行期间,执行适配器首先将CURA计划传递给CURA,将其编译为内部物理计划。上图显示了前面讨论过的CURA计划的物理计划和数据流。我们引入了“管道”,每个管道都包含从CURA计划重构的GPU操作符的子集,以组织GPU操作符并管理CPU并行性(多个CPU线程)。数据以“片段”的形式形成(图中显示为窗口图标),每个片段都是一个表的水平分割。

例如,由于哈希连接的固有属性,探针之后才能开始吗构建完成;因此,整个执行分为两个管道。过滤器没有这个限制,所以它可以被“流水线化”构建在同一管道中。从表扫描图(浅灰色窗口图标)流过过滤器然后进入构建之后。同时,在管道1(最后的管道)中,从表扫描t1(中间灰色窗口图标)流动通过探针并将CURA作为发送给客户机的最终结果(深灰色窗口图标)。每个管道接受任意CPU并行的片段。对同步的需求很少而且微不足道,GPU操作符在必要时就会处理它。

执行适配器和CURA通过协议(双向箭头)相互交互,并且:

  • 锚每个表扫描到相应的管道。
  • 按指定的顺序迭代所有管道。
  • 对于每条管道,从锚固处排出碎片表扫描s并以可配置的CPU并行性为管道提供它们。
  • 通过最终管道向客户端发出数据输出。

该协议还为我们带来了另一个好处:CURA成为了一个通用的GPU查询运行时,您可以通过适当地遵循协议来适应任何其他数据库。

GPU运算符

多亏了急流项目核心组成部分cuDF,我们可以使用它作为一个坚实的原语,并在其上构建我们自己的GPU操作符。cuDF提供运行在GPU上的成熟的数据帧api。它要求数据为列格式,并使用箭头布局。幸运的是,我们的片段匹配了两者,所以除了输入的主机到设备的内存副本和输出的设备到主机的内存副本(只有最后的管道)之外,我们不需要重新格式化任何cpu端数据。在这篇文章中,我们不会详细介绍cuDF。如果您想了解更多关于cuDF的信息,请参阅其GitHub回购

基准测试

我们选择TPC-H 50G作为基准数据集。我们没有使用所有22个查询,而是选择了四个cpu最密集的查询:Q5九方,的时候。这些查询在运行传统TiDB的强大服务器(使用CPU)和运行我们的GPU加速TiDB的商用PC(我们没有GPU服务器)上运行。硬件规格如下图所示:

CPU TiDB和GPU TiDB的硬件比较
CPU TiDB和GPU TiDB的硬件比较

为了避免I/O操作摊销整体性能改进,我们使用协处理器缓存的所有中间结果进行缓冲表扫描它有效地使TiDB成为一个“假设的”内存数据库。

这些TPC-H查询的最终结果如下所示。我们在Q5、Q9、Q17和Q18分别实现了12倍、32倍、27倍和28倍的性能提升。

GPU与CPU的TPC-H基准测试
tpc - h基准测试

TPC-H查询很复杂。我们还执行了几个手写的简单查询作为微基准。第一个查询是典型的“不同计数”。我们计算列的不同值l_returnflag从表lineitem

SELECT COUNT(DISTINCT l_returnflag) FROM lineitem

第二个查询是典型的“大表连接”。我们选择连接两个最大的桌子lineitem订单在列orderkey

SELECT COUNT(1) FROM lineitem JOIN orders ON l_orderkey = o_orderkey

这两个查询的最终结果如下所示。对于“不同计数”和“大表连接”,我们分别实现了67x和148x的性能改进。

CPU与GPU的简单查询基准
简单查询基准

下面几节将讨论我们从这些性能改进中获得的见解。

GPU上的哈希表

在许多数据库操作中,如散列连接和散列聚合,散列表是最关键的数据结构。它主导了大多数cpu密集型查询的执行。的并发哈希表在GPU上是一个高性能的。它将开放寻址与GPU的高度并行特性结合在一起。它的性能比遗留TiDB中哈希表的CPU实现高出数百倍。实际上,在我们的基准测试中,大多数性能改进都是GPU加速哈希表操作的结果。GPU不会像哈希表那样显著地加速其他数据库操作;因此,整体加速度提高了10到150倍。

内核/ memcpy效率

内核/内存效率是GPU执行计算和内存复制的时间之比。这是GPU如何适合加速此工作负载的主要指标。平均而言,我们看到查询的比例为1:1,这不是很高,并且随着查询产生更多的最终结果,该比例会下降。例如,当您连接两个大表而不进行聚合时,该比率会下降,因为它会带来更多的设备到主机的内存副本。

GPU内存分配器

cudaMalloccudaFree是管理GPU内存的标准CUDA api。然而,它们有副作用,迫使GPU通过耗尽所有待处理的作业来同步整个设备;即提交给GPU的异步内存副本和内核。这很容易成为我们管道模型中的瓶颈,因为:

  • CPU不能提交任何后续的GPU作业,直到GPU完成所需的设备同步cudaMalloccudaFree。我们的流水线模型往往有很长的GPU作业序列,因此,由于这种同步,CPU和GPU之间的潜在并行性降低了。
  • GPU有可能重叠多个作业,而频繁的同步消除了这些机会。
  • 如前所述,管道接受任意CPU并行性。同步的数量乘以CPU线程的数量。

幸运的是,cuDF提供了一个竞技场分配器,预先预留大量GPU内存,并在内部管理后续的分配/释放。它也是并发友好的。分配器有效地解决了我们的问题,并将整体性能提高了一倍。

CUDA流

CUDA流是一种通过重叠多个独立CPU线程提交的GPU作业来实现更高GPU利用率的方法。我们没有意识到这是个问题,直到我们使用nvvp来分析我们的查询,我们发现不同GPU作业之间没有重叠。所有GPU作业似乎都进入默认流。

通过启用每个线程默认流在编译时,我们看到每个CPU线程被分配一个单独的流。这在内核并发性和内存/内核重叠方面提高了10%~20%的GPU利用率。

GPU的利用率

在我们清除了内存管理开销和CUDA流的障碍后,我们终于能够直接面对GPU的利用率。老实说,这不是一个快乐的结局。经过努力,我们实现了:

  • 主机到设备内存拷贝吞吐量低于3gb /s,远低于PCIe 3.0 16gb /s的带宽。
  • 内核并发性和memcpy/内核重叠的比例小于10%。

第一个问题很可能是因为我们的数据片段不够大——它们通常是几十兆字节——而且它们没有完全占用PCIe带宽。

第二个问题的原因尚不清楚。考虑到我们克服了前面提到的“障碍”,我们假设我们的管道模型可以通过增加CPU并行性来提高GPU利用率。这是因为当更多的CPU线程并发地提交GPU作业时,GPU作业重叠的机会会更多。然而,峰值性能出现在2到3个CPU线程。在较高的CPU并行性下,内核并发性和内存/内核重叠都不会增加,而且整体性能甚至会下降。

是因为硬件单元耗尽了吗?或者其他一些未识别的CUDA api会导致意外的隐式开销cudaMalloccudaFree做什么?还是我们遇到了什么go线程问题?为了回答这些问题,我们必须进行更细粒度的分析。我们希望还有进一步改进的空间。

外卖

  • 通过在GPU等新一代硬件上运行,TiDB处理cpu密集型分析查询的速度可以提高10倍~150倍。
  • 我们构建了一个通用的GPU查询运行时看台在…之上cuDF。它适应于TiDB在这个项目中,它可以适应任何其他数据库(特别是内存数据库),以加速其查询引擎使用GPU。
  • 我们可以进一步提高GPU的利用率。但那得等到我们有更多的时间,我的朋友玩完《赛博朋克2077》后就会把我的RTX 2080还给我。

对这篇文章有问题或评论吗?参观TiDB论坛

订阅我们的网站!

欧宝娱乐最新在线平台TiDB云的标志是黑色的

欧宝娱乐最新在线平台

在完全托管的云服务中获得TiDB数据库的大规模和弹性

TiDB logo-black

TiDB

TiDB可以毫不费力地扩展、开放和信任,以满足数字企业的实时需求

Baidu
map