Hydra 是一个基于 Postgres 构建的完全托管的开源数据仓库。它易于使用,旨在扩展分析 (OLAP) 和混合事务工作负载。我们的团队很高兴地宣布 Hydra 是最快的 Postgres 分析数据库。了解我们如何启用列式存储、矢量化和查询并行化来提供比 Postgres 快 23 倍的性能!
立即注册即可获得云管理 Hydra 的 14 天免费试用 🐘🎄💨!
Postgres OLAP 就在这里
Hydra 通过以下方式衡量分析性能:
- 列式存储
- 并行查询执行
- 矢量化
基准测试
我们选择使用 ClickBench 基准测试是因为它们与分析工作负载的相关性和实用性。ClickBench方法如下:
该基准测试代表以下领域的典型工作负载:点击流和流量分析、网络分析、机器生成的数据、结构化日志和事件数据。它涵盖了临时分析和实时仪表板中的典型查询。该基准测试的数据集是从世界上最大的网络分析平台之一的实际流量记录中获得的。[1]
列式存储
列式存储是数据仓库的关键部分,但为什么会这样呢?让我们回顾一下什么是列式存储以及为什么它对于可扩展分析很重要。
默认情况下,Postgres 中的数据存储在堆表中。堆表逐行排列,类似于创建大型电子表格时排列数据的方式。列表是从行表横向组织的。行不是逐个添加的,而是批量插入到条带中。在每个条带内,每列的数据彼此相邻存储。想象一下表中的行包含:
该数据将以柱状形式存储,如下所示:
堆行表被组织成页。每页保存 8kb 数据。每页都保存指向数据中每行开头的指针。在列式中,您可以将每个条带视为一行元数据,最多可容纳 150,000 行数据。在每个条带内,数据被分为1000 行的块,然后在每个块内,存储其中的每一列数据都有一个“行”。此外,柱状存储每个块中每列的最小值、最大值和计数。
柱状的优点
列式针对表扫描进行了优化——事实上,它根本不使用索引。使用柱状,可以更快地获取特定列的所有数据。数据库不需要读取你目前不感兴趣的数据。它还使用有关块中值的元数据来消除读取数据。这是数据“自动索引”的一种形式。
此外,由于相似的数据彼此相邻存储,因此可以实现非常高的数据压缩。数据压缩是一个重要的好处,因为列式通常用于存储大量数据。通过压缩数据,您可以更快速地有效地从磁盘读取数据,这既减少了 I/O,又提高了有效读取速度。它具有更好地利用磁盘缓存的额外效果,因为数据以压缩形式缓存。最后,您可以大大降低存储成本。
我们的柱状方法
我们的方法是使用 Postgres 扩展。扩展使我们能够保持 Hydra 与社区 Postgres 版本和生态系统之间 100% 的兼容性。我们研究了现有的开源列式存储引擎并选择了 Citus 开发的列式访问方法[2]。测试 Citus Columnar 时,我们发现性能优于基于行的分析表,但可以通过矢量化和并行查询执行来改进。
并行化+矢量化
很明显,列式存储缺少对提高性能至关重要的功能。显而易见的选择是启用查询的并行化。我们从单进程列式执行开始,并启用了 SELECT 查询的并行执行。这提供了令人印象深刻的性能提升。
下一步是在处理查询时启用矢量化。我们首先在WHERE子句上启用矢量化执行。我们将继续在聚合函数的矢量化执行、使用显式 SIMD 执行、支持更多类型(但一般矢量化执行)方面投入资源,从而继续开展工作。
并行化
PostgreSQL 在 9.6 版本中引入了并行化功能。只有一个进程完成了之前的所有工作。PostgreSQL 中的并行性是作为多个功能的一部分实现的,其中包括顺序扫描、聚合和连接。
根据定义,查询并行化的工作原理是将查询工作划分为更小的任务,然后在多个内核或机器上同时执行。这使得数据库能够利用现代处理器,这些处理器旨在同时执行多个操作,并且可以显着提高涉及大量数据的查询的性能。
要启用并行化,自定义扫描需要实现特定的CustomExecMethods回调。
PostgreSQL并行化通信是通过共享内存完成的,并行化是通过多个进程而不是线程来实现的。
查询从称为Leader 的单个进程开始,该进程将通过EstimateDSMCustomScan提供自定义扫描共享内存的大小。之后,领导者需要使用InitializeDSMCustomScan初始化共享内存。自定义扫描共享内存结构在worker之间共享,每个worker需要在InitializeWorkerCustomScan回调中设置自己的内存自定义扫描结构。当请求重新初始化扫描时,将使用ReinitializeDSMCustomScan ,这意味着工作线程将关闭并重新创建 - 例如嵌套循环连接的行为。
在进行自定义扫描并行实现时需要回答一个问题 - 如何在工作人员之间划分数据以扫描和处理唯一的元组?幸运的是,内部柱状存储结构帮助我们解决了这个问题,并且解决方案很简单 - 每个工人将在不同的条带上工作。它为我们提供了逻辑和物理屏障,以满足并行处理数据的唯一性。
共享内存变量用于跟踪下一个条带的分配。变量被定义为原子性,因此获取和添加 CPU 指令将处理工作人员分配之间的原子性。
矢量化
向量化执行是一种通过同时执行多个操作来提高数据库查询性能的技术。这与传统执行相反,在传统执行中,每次执行一项操作。
向量化执行的工作原理是将数据划分为小块(称为向量),然后对每个向量并行执行多个操作。这使得数据库能够利用现代处理器,这些处理器被设计为同时执行多个操作,并且可以显着提高涉及大量数据的查询的性能。
Hydra 目前支持的矢量化目前仅限于 WHERE 子句。这些子句直接在自定义扫描节点中处理,因此我们可以完全控制如何执行代码。
仅当存在接受相同参数和运算符的向量化函数时,才会使用向量化执行。目前,我们支持基本类型以及它们之间的压缩运算符。支持的类型有 CHAR、SMALLINT、INT、BIGINT、DATE、TIME。
当前实现的另一个限制是仅适用于单个变量和常量值。
WHERE 子句存储在列表中,顶级子句将通过AND运算符按顺序求值。更复杂的子句将构造多个树状结构。
Hydra 向量化期望单个树中的所有子句都可以向量化,如果这不是真的,向量化执行将不会用于该分支。
在第一个示例中,使用了可以向量化的单个 WHERE 子句,因此我们可以触发该查询的向量化执行。将评估条款列表
- 且( b < 15 )
此示例表明,即使我们不支持所有 where 子句,仍然会使用向量化。这是可能的,因为顶级子句是独立的并且可以按顺序处理。此示例的子句计算为
- AND ( b < 15 , c <> '' )
在此示例中,将不使用矢量化。OR 运算符用于两个子句之间,其中一个子句可以向量化,而第二个子句则不能。子句被评估为
- 与(或( b < 15 , c <> '' ) )
由于并非 OR 分支的所有树叶都无法矢量化,因此我们不对此查询使用矢量化。
最后一个示例显示了如何将矢量化执行用于其中一个子句,但不用于第二个子句。
Where 子句的计算结果为:
- AND ( b < 15 , OR ( c <> '' , a < 40 ) )
可以使用columnar.enable_vectorization GUC 变量启用或禁用矢量化,该变量默认设置为 true。
矢量化可以加快执行速度,但如果非矢量化性能足够快,矢量化甚至会增加性能开销。我们期望在处理复杂查询和大数据的 OLAP 案例中,此功能可以提高性能。
优化 Postgres 设置
如果不进行调优,就无法充分发挥 PostgreSQL 的性能。Postgres 默认参数不会充分利用运行它的硬件,而不告诉 Postgres 它可以使用多少硬件。如果您在专用计算机上运行 Hydra,这些是我们为 Hydra 推荐的设置。您可能希望根据可用内存和并发负载减少work_mem和hash_mem_multiplier 。
ClickBench 报告
您可以亲自查看结果以检查查询、方法和数据大小。