技术探讨 |YMatrix 高性能时序数据库引擎的技术实践

2022-11-10 · 王勇
#技术探讨#性能测试

导读:YMatrix 最近推出了 5.0 版本,同等条件下 SSB 性能比 ClickHouse 官方数据提升了 24%。本文将介绍新版本使用的性能优化技术,及最终的优化效果。

内容要点:

  • 高性能时序数据引擎的技术栈
  • 多核并行
  • 时序存储引擎 MARS
  • 向量化执行器
  • 高效编码链
  • 观测能力

一、时序分析场景的前景和挑战

时序数据是对实体对象持续观测得到的,带有时间戳的数据序列。例如数据中心的 CPU 利用率,证券交易所记录的股票价格,风电企业采集的风机转速等。

时序数据分析可以提供重要的商业价值。例如,金融时序分析可以及时发现欺诈、洗钱行为,阻止非法交易,挽回巨额损失。电商公司通过分析用户购买偏好和人群画像,实现实时、个性化推荐,大大增加交易额和客户满意度。在工业互联网领域,时序分析可以通过跟踪设备状态,结合订单情况进行产能匹配,优化供应链,提升设备利用率。物联网领域可以实时监测设备状况,及时阻止意外,还可以通过轨迹分析,实现实体追踪和异常行为检测。

如图 1 所示,随着工业互联网的稳步推进及物联网的兴起,联网设备呈指数增长,且远超传统的 IT 和通信设备。

时序数据分析在给业务发展带来更大潜能的同时,也给数据平台提出了巨大挑战。时序数据总量可以用:联网设备数量 x 采集频次 x 指标数量 来估算。以新能源车企为例:

  1. 数据总量巨大。按 100 万辆车、采集周期 1s 、250 个指标进行计算,一天的数据量达 172 TB,一个月达 5.1 PB。数据存储成本、分级管理及数据库系统运维就成为重要问题。

  2. 快速写入。按上述计算,每秒的写入速度要求达到 2 GB/s。另外还需要处理乱序到达的数据、重复的数据写入,以及后续进行查询时增加的复杂性。

  3. 高速查询。时序分析一方面要面向众多车主提供高吞吐、低时延的明细查询,另一方面也要提供针对批量数据的异常检测和统计分析。同时还要向数据科学家提供更为复杂的多维数据分析,甚至机器学习的能力。

YMatrix

YMatrix 是主打时序场景的超融合数据库。它融合了 OLTP、OLAP、时空和数据湖的能力,简化了过去用户需要使用维护多套系统的复杂度,增强了数据的流动和整合。YMatrix 源于 Greenplum,而 Greenplum 又是基于 PostgreSQL 的 MPP 型数据库。YMatrix 继承了 Greenplum 及 PostgreSQL 优秀的设计、稳定的基因和丰富的生态,增强了时序、分析方面的能力。但 Greenplum 和 PostgreSQL 基础架构是在较早期 IO、Memory 和 CPU 资源都相对紧张的情况下做出的,YMatrix 着力优化了现代硬件条件下的性能,同时完整地保留它们的功能丰富性和企业级品质。

YMatrix 最新推出的 5.0,针对时序分析场景实现了一系列优化,并和 Clickhouse 这一业界的性能标杆针对 SSB 进行了对比测试。下面将给出结果并解读相关优化技术以及带来的性能提升。

二、SSB 性能结果

SSB(Star Schema Benchmark)是由麻省州立大学基于真实商业应用的数据模型定义的性能评测基准,学术界和工业界普遍用它来评价数据库在分析场景的基础性能。SSB 在 TPC 组织发布的 TPC-H 基准之上,将雪花模型改为更为实用的星型模型,将复杂的 Ad-Hoc 查询简化为更固定的 OLAP 查询。随着 ClickHouse 在 SSB 上展示了亮眼的单表“硬核”计算速度,很多产品也竞相采用 SSB 来验证自身的性能水平。受限于初始的应用场景和技术实现,CK 采用了将 SSB 的多张表扩展成单一宽表的模式(可参考官方文档)。虽然 YMatrix 有优秀的多表 Join 能力,但为了更有比较意义,这里采取和 CK 同样的单表方式来测试。测试采用了和 ClickHouse 相同的 AWS m5.8xlarge 云主机,测试结果为单台主机上的性能。

图2 给出的是 100 倍数据集的性能结果,它的原始数据量约为 200 GB。可以看出 YMatrix 在全部 13 条查询中,有 10 条时间快于 CK,整体快 24%。测试中先进行一次查询预热数据,然后取连续 3 次的平均结果。具体测试过程可以参见:SSB 性能优化结果解读。在 1000 倍的数据集上,YMatrix 有更多的性能提升,由于 Clickhouse 没有披露官方的数据,因此这里不详细展开。

三、高性能时序数据引擎的技术栈

要达到全系统性能的大幅提升,需要全链路、多个模块互相配合才能实现。图 3 从查询的视角给出了实现高性能数据库引擎所需要的技术栈。

高效的存储引擎。数据库是重数据处理的系统,存储引擎要尽可能精准地准备数据,减少需要处理的数据总量。这主要是靠列存技术和索引技术,分别从列和行的角度减少数据总量。即便在 SSD 大行其道的今天,IO 的代价仍然占很大比重。存储引擎还需要高效地使用 IO 资源,减少 IO 次数和对于存储带宽的占用。对于 HDD 介质,尤其要利用好顺序读吞吐远高于随机读的 IO 特点。精心设计的存储格式和索引策略,再配合 Cache 机制可以使得随机访问大大减少,充分发挥出 HDD 吞吐的能力。另一方面,针对列存,特别是时序场景,可以充分利用数据同质和随时间渐变的规律性,大大减少实际数据的存储量和 IO,同时可以提高缓存命中率,从而提升查询性能。

高速的计算引擎。计算引擎,也就是执行器的性能潜力首先取决于架构,这包括:线程/进程模型,pull/push 模型,算子调度流水线,通信模型,单机并行及资源控制的粒度等。对于分布式查询,架构还包括节点间数据分发和同步模型。对于较复杂的查询,配合精心调整的代价模型,cascade 优化器能选出更优的执行计划。在实际执行阶段,分析查询一般会处理较大的数据量,而向量化成为当下标准的优化技术。它一次处理一批数据,通过减少调用函数次数、提升 cache 效率,生成高质量的 SIMD 指令来提升性能。另外,对于某些特定类型、某些关键算子都可以采用更高效的方式来专门优化,当然需要付出某些维护代价。存储还需要和计算引擎配合,提供更高效的数据处理格式,比如列存。进一步,还可以充分利用硬件特性,如 NVMe SSD 和分布式内存来优化性能,甚至直接使用硬件加速卡。

高性能的基础设施对性能也起到关键作用,尤其是在数据处理已经很快的情况下。这些技术包括内存池化技术、高效的网络库、轻量级的同步机制、全异步、无锁的任务模型、高效的编码库,以及字符串匹配、基本运算、排序等基础库。

全方位的观测能力。例如,能全面呈现查询执行过程各个算子、关键步骤、节点间任务情况的查询 Trace;分析数据集和查询特征,辅助设计表结构和编码方案的工具;以及跟踪 CPU、内存、IO 使用热点及执行序列的工具。这些观测能力可以帮助理解系统与业务,驱动各种资源的饱和使用,同时消除不合理的资源使用,从而实现性能的迭代优化。

释放 PostgreSQL/Greenplum 的性能潜力

YMatrix 继承了 PostgreSQL 和 Greenplum 这两个产品的基础架构。PostgreSQL 是最强大的开源单机数据库,有着完善的功能和 SQL 支持,开放的模式吸引了丰富的生态。它还提供了很多鲁棒、高效的基础组件和扩展钩子,方便进一步的优化和扩展。Greenplum 则是基于 PostgreSQL 的 MPP 分析型数据库。据 Gartner 2019 年报告,Greenplum 在传统领域数仓排名世界第三,仅次于 Teradata 和 Oracle Exadata,在实时数仓领域也并列排名世界第四。Greenplum 有着广泛的全球部署规模,丰富的企业级特性,以及久经考验的稳定性。

近期出现了不少从 NewSQL 发展起来的分析型数据库引擎,经过对比,结果表明:Greenplum 的 MPP 框架、Orca 优化器和 PostgreSQL 的执行流水线设计都非常优秀,但它们没有针对分析场景,充分利用相对富裕的内存和 CPU, 进行比较彻底的优化,因此依然存在很大的性能优化空间。

Heap 是 PostgreSQL 唯一内置的存储引擎,它以行为存储单位,对点查/更新友好。对应的,执行器采用的是火山模型,即每一行数据都要经过漫长的调用栈逐层在算子间传递与处理, 执行效率很低。长期以来,PostgreSQL 缺少一个高效的列式存储来支撑分析场景,因此执行器的火山模型也一直未有变化。另外,PostgreSQL 的执行器采取的是解释执行,对于复杂的表达式要递归地调用表达式树求解,每个操作都被翻译成一个函数,虚函数调用的开销很大。

Greenplum 引入了 AOCS (aka AOCO) 列式存储格式, 支持 external hash 的 hashjoin、hashagg 等算子,以优化海量数据的处理。AOCS 将每一列的数据独立存储在不同的文件中,因此有着很好的顺序 IO 效率。但 Greenplum 受限于执行器仍是火山模型,每次向下层算子仍只能读取一行数据进行处理。另外,AOCS 自身不具备块级数据筛选能力, 需要单独建立和维护 brin 索引。因此 Greenplum 的效率也不够优化。

图 4 展示了 YMatrix 通过多核并行、自研存储引擎、向量化、编码这些技术大幅提升了原有 Greenplum 的性能。可以看出:采用了多核并行后(并行度为 4),由于成倍增加了算力,性能提升了近 3倍。而采用自研的 MARS 存储引擎后,有效减少了访问的数据量,性能提升了近 8 倍。通过向量化计算引擎优化了计算的效率,性能进一步提升了近 4倍。相对于 LZ4,精细化的编码设置将压缩速度提升了 3 倍,整体性能进一步提升了 15%。总体上性能比未调优的 Greenplum 提升了将近百倍。值得说明的是,本文的重点并非要给出和 Greenplum 严谨的性能对比,而是说明 YMatrix 的优化路径。实际上,利用 PostgreSQL 的 并行查询机制,添加合适的 brin index,让执行计划走到 bitmap scan,这些技术也可以在 Greenplum 得到不错的优化效果。接下来将给出上述优化的技术细节。

四、多核并行

随着多核技术不断深化,当代服务器配置一般都有比较多的 CPU 核数。常见的 2 路服务器,即使配置核数较少的 Intel Xeon 处理器,也很容易达到 32 核 64 线程。Greenplum 虽然可以在单机上启动多个 segment 来提升 CPU 利用率,但一个 segment server 相当于一个独立的 PostgreSQL 数据库节点,因此 segment 数一般较 CPU 总核数要少得多。要想让单个查询充分利用 CPU 能力,还需要使用多核并行技术。PostgreSQL 原生提供了 parallel worker 的特性,它可以在单个执行节点上启动多个 worker,分别扫描不同部分的数据实现加速。由于上层节点可能是非并行的,因此它会额外放一个 gather 节点来收集 parallel worker 的结果。增加 Gather 节点一方面增加了数据传输的开销,另一方面,其串行化输出结果也不利于整体并行度的提高。

YMatrix 提供了一种更为彻底的并行化,它从 motion 开始的所有节点均并行执行,即 QE 上整个计划都是并行。并行 worker 和 Greenplum 原生的 QE 一样,相当于增加了更多的 segment,提升了整个查询的并行度。配合细粒度任务分发,同步资源的优化,以及网络层的优化,大大缩短了worker 间长尾延时。

当并行度为 4 时,SSB 性能整体提升了 2.5 倍。

五、时序存储引擎

MARS 是 YMatrix 自研的列式存储引擎。它通过排序键定义数据的存储顺序,针对大数据量的分析和 HDD 做了大量优化。如图 5 所示,它采取类似 LSM-Tree 的多层存储模式。run 是一个最核心的概念,它是指逻辑上有序的一组数据集合。它将数据集合按一定的尺寸分别存到多个文件里,每个文件大小在 GB 级。在每个文件里,同一列的数据顺序存储。同一列的数据按条数则进一步分解成若干个 range,每一个 range 可以独立地存取和解压。MARS 支持对每一列数据设定稀疏索引,索引里记录该列的最大最小值。索引按类似 btree方式组织,即上层节点记录所有子节点的 minmax 值。

MARS 只保证数据的局部有序,即 run 内有序,而采取 merge-on-read 的策略实现数据去重。刚进入系统的数据放在热层,这时候每个 run 都比较小。随着时间的推移,更多较小的 run 会合并成更大的 run。查询可以基于索引访问顺序地命中数据块,从而实现 IO 的友好。

5.1 高速写入、合并

搭配 YMatrix 自研的高性能数据加载工具 MatrixGate,MARS 存储引擎实现数据的高速写入。MatrixGate 仅和 master 沟通全局事务和控制信息,数据则直接发送到 segment 节点。在每个 segment 上,每接收完一批数据,MARS 都按排序键进行排序,形成一个 run。run 的数据被直接追加到文件末尾,索引也追加到对应索引文件的末尾。通过接入 MVCC 机制,多个 run 的写入可以并发执行,充分利用了单机的资源。

当 run 的数量或数据总量达到一定阈值后,MARS 自动触发 merge,将多个 run 的数据通过排序合并成一个更大的 run。然后重新生成新的 run Meta 和索引信息并存入 meta 文件。写入、索引和 Merge 过程均消除了随机 IO,磁盘带宽的利用率达到了 80%。Merge 操作和前台写入独立进行,不阻塞前台写入。

Merge 过程需要读出原始数据并写入新的排序数据,如何控制放大比以提升系统的吞吐能力就成为一个关键问题。全局有序可以让查询访问更集中的数据区段,减少 IO 次数,但维护真正的全局有序代价太高。因此 MARS 采取了一种折中的做法,通过控制每个 run 的尺寸和run 的总数来达到局部有序。其平衡策略见高效存内检索一节的描述。

写入过程还采取如下几种策略来缩短关键路径,以取得更大的写入吞吐。首先,和一般的 btree 索引不同,run 只维护自身的块索引,因此不需要全局操作,减少了并发控制的开销。另外,更大顺序的维护,包括去重都在后台完成,且和写入相互独立。

5.2 高效存内检索

作为一个 PostgreSQL 框架下的存储引擎,MARS 提供了执行器所需要获取一行数据的接口。为了提高分析场景下效率,MARS 还提供了和向量化执行器深度集成的批量接口,直接按投影列返回一批数据。每列数据按类型在内存以数组形式紧密排列。

MARS 的 IO 的优化目标可以用公式 1 来描述。iosize 指一次查询实际读取的数据总量,blocksize 指每次访问的块大小,blockfilter 指块过滤的能力。高效的存储引擎需要1)尽可能精确地读取数据块;2)自带过滤能力,尽可能少向执行器返回数据;3)通过顺序访问大块数据以提高 IO 效率;4)充分利用并行扫描能力。MARS 也是从上述四个方面进行优化,使得所有查询的总的 IO 次数最少。

  • 精确读取数据块。MARS 指定的排序键也是索引键,对于使用索引的查询,MARS 会从 5.1 描述的索引树中直接定位符合要求的数据块。主键索引也仅索引到数据块级别,索引数据的总量很小,因此可以最大程度地 cache 在内存中。另外,通过控制单个数据块的元数据总量,可以进一步减少无效元数据的读取和内存占用。

  • 块筛选。MARS 还支持 minmax 索引,即将每个索引指定列的最大最小值记录在 range 的元信息里。MARS 会理解执行器下推的扫描条件,对于建有 minmax 索引的列,自动判别是否命中某一个数据块,如果没有则直接忽略。进一步,MARS 还实现了 dataskip 技术以减少数据的返回。如果根据某一列的 minmax 索引能全命中或不命中,则无须再检查该列。如果这些列仅用作过滤,则无须返回该列的值,且执行器也可以直接忽略这个条件。这一优化对于 SSB 1000倍时的 Q1.2 和 Q1.3 有明显的优化效果。

  • IO 效率。MARS 通过高效编码和压缩,减少了实际读取的数据块大小,降低了初次读取的代价。刚进入系统的数据分散在不同的 run 里,后台 Merge 操作及时将它们合并成少数更大的 run。及时减少 run 数不仅减少了查询切换文件的开销,也提升了 IO 的顺序性。对于超过某一时限的冷数据,MARS 会合并成 GB 级的文件,从而保证在一个很大的范围内的顺序性能。从 strace 的结果来看,交叉访问多个顺序的 IO 流,也就是 stride 读的模式,可以被操作系统准确识别并 prefetch,IO 的代价被进一步分摊。

  • 并行扫描。配合下文要提到的多核并行能力,MARS 实现了 range 这一粒度的并行。range 中包含数据量的多少可以根据不同的表来配置,其大小设置需要同时考虑解压缩的开销和对筛选率的影响。

MARS 以元数据的形式记录了某些列上的聚集信息,如 count, min, max 值。MARS 实现了自定义扫描方法,可以直接基于这些预聚集数据算出聚集查询的结果,避免了访问实际数据的代价。相对于 AOCS 结合 bitmap 的做法, MARS 不仅实现了内置的过滤能力,而且针对数据块一次性处理完多个条件,无须先建立 bitmap,再完成过滤。

MARS 提供的内置索引要远优于一般意义上的延迟物化技术(如下图所示)。它将数据的筛选最大程度地限制在了存储引擎之内,无须执行器感知。

六、向量执行器

向量化是现代分析引擎常用的一种优化技术,它实际上是优化 CPU 计算能力的一组技术的集合。它解决的核心问题是如何利用现代 CPU 指令级并行(ILP, Instructionn Level Parallelism)的能力,在相同的时间内完成尽可能多的数据计算。

6.1 现代 CPU 架构

以 Intel 至强为例,现代 CPU 一般都采用如下技术来提高单时钟周期完成的指令数,即 IPC (Instructions Per Cycle)。

  • 超流水线。一条指令的完成一般会细分为取指、译码(CISC处理器会有多级)、执行、返回等多个阶段。现代 CPU 一般为 10~20 级,每个时钟周期只能完成其中几个阶段。多条指令可以重叠执行,即流水执行来提高速度。理想情况下,每个阶段都有指令执行,使得 IPC 接近于 1。数据相关和跳转是流水线的大敌,前者因为只能处理完当前指令才能进行下一条;后者则只有执行后才知道下一个周期执行的是哪一条指令。这两者都会导致流水线起泡,降低 IPC。

  • 超标量。CPU 有若干执行部件,具体同时工作的条件。调度器会在一个时钟周期内发射多条指令到不同的流水线里,使得 IPC > 1。它同样需要指令具有良好的模式,适合并行执行。

  • SIMD 指令。最初是为了多媒体处理的需要,CPU 引入了单指令多数据(SIMD,single instruction, multiple data)的指令。和普通指令不同,它使用更宽的寄存器和运算单元,一次可以执行多个数据元素的运算,从而大大提升了IPC。一般会采用 256 粒度的指令集,如 Intel CPU 的 AVX2 指令集。

  • 深存储层级。访问一次内存大概需要近百纳秒的时间,这相对于 CPU 处理而言过慢。一般 CPU 配置三级高速 cache,而 L1 cache 延时则只需要 3 个时钟周期。因此一次至少读取并处理 64 个 (cache line)8 位数据,可以利用内存的 burst 模式,最优化延时和数据吞吐。

结合前述的执行器模型,向量化技术一般包含如下方面:

  • 批量处理,一次处理若干条数据。从执行器方面,这可以极大减少函数调用次数。反复的函数调用会增加大量的上下文切换开销。另外,远程跳转(long jump)会导致指令 cache 失效,触发内存访问,拖慢执行。进一步,这些同质数据的处理彼此无关,一次性 load,循环处理即可充分利用 CPU 的处理能力,提升 IPC。

  • 数据类型特化。数据类型多样化是数据库需要支持的基本特性,PostgreSQL 会根据不同的数据类型选择需要的函数,比如数据类型的转换、函数计算、算术/逻辑运算等。利用代码生成机制,比如 C++ 模版,针对一批数据,只做一次类型选择。这会大大减少虚函数开销,也更有利于指令缓存。

  • SIMD 指令的使用。现代编译器都有能力生成更高质量的代码,例如将循环展开,自动使用向量化指令。但对于某些较为复杂的场景,也需要手动实现,比如调用 instrinsic 指令。

向量化需要全链路的实现以发挥最大作用,下面具体展开 YMatrix 在向量化方面的实现。

6.2 向量化框架

PostgreSQL 提供了一系列的钩子函数,方便在某些环节增加定制的逻辑。YMatrix 采取是将原有执行计划里的标量化节点换成向量化节点,并在节点内部采用批量化的处理模式并在节点间进行批量化的数据传递。和直接在 ExecutorStart,Executorrun,ExecutorEnd 开口子的方式比,这种方式的侵入更小。通过这些钩子函数,向量化 path 被加到原有的候选路径中并以合适的代价被选中;在执行前被翻译成 customscan 节点;在执行时通过 scan 节点生成上下文件并被反复调用,一次处理一批数据。对于基表的 scan 节点,则将索引信息和过滤条件下推下存储引擎。执行器通过反复调用 MARS 的批量接口获取数据并执行自己的向量化算法。对于像函数、表达式、类型等都要进行对等的翻译,实现对应的原生类型和向量化版本的计算。具体形式如图 7 所示,底层的 scan 和上层的 agg 已经换成了向量化版本的 MxVScan 和 MxVHashAgg。

6.3 数据结构

在 YMatrix 里,Array 用于表示内存中一列数据,它是数据处理的基本单元。Array 中的数据有两种,一种是紧密排列的,如各种定长的类型,另一种是指针形式的,如变长类型。变长类型的数据集合经过预处理后,比如对齐或增加自解释的头部后,如果空间不再变化,也可以转成更为高效的连续存储形式。这种紧凑的格式也适合转存储到持久存储上。由多个有序的 Array 组成的数据集称为一个 tablet。它可以被视作为一个表的分片。Tablet 是数据交换的基本单元,每个节点都针对上层节点提供的数据集合进行处理,然后将结果交给下一个节点。

数据类型描述了自身的信息,如底层的类型、和 PostgreSQL 类型的对应关系、存储大小,以及类型转换,比较等算子等。而 Array,函数,运算符等操作都根据特定类型实例化,针对一批数据生成具体的运算结构。各种类型的组合会非常多,例如类型之间的转换会形成 N * N 的效果。这会使得代码膨胀严重,影响编译速度和执行效率。需要利用 C++ 的特性进行剪枝,只进行必要组合的代码展开。

6.4 工程实现

YMatrix 实现了全面向量化,并对几乎全部的算子进行了向量化重写。它可以自动识别不能处理的特殊场景,并转成 PostgreSQL 原生的处理逻辑。这在保留功能完备的情况下,实现了向量化能力的最大化。

  • 全面向量化。除了部分复杂类型,如 numeric 和一些组合类型外,YMatrix 实现大部分数据类型的向量化。在此基础上,它完全支持各种复杂的表达式。同时,它还以向量化的方式支持了各种算子、不同个数参数的函数,以及各类合法的类型转换。

  • 全链路向量化。PostgreSQL 的算子是一个庞大的体系,而对于像 join,agg 等节点还有多种实现。Greenplum 又为分布式处理增加了节点间数据分发和汇聚模式,全面向量化所需的工作量极为巨大。YMatrix 不仅实现底层算子,如 scan,projection,sort,join 等基础算子的向量化,还支持各种 motion 的向量化。另外,针对 agg 和 sort 能根据不同的数据特征,自适应地选择合适的多种算法。像窗口函数等复杂一些模式,YMatrix 也有一定程度的支持。

  • 批量处理和微流水线。以 Array 为单位的批处理过程中,单个运算要充分利用 CPU 多发射能力,需要保持流水线充满。而对于连续的多个操作,如多个过滤条件,要充分利用 cache 能力。YMatrix 将 Array 的批次大小控制在一定范围,当数据依次流过多条过滤条件(微流水线)时,无须再触碰内存。

  • 延迟物化。通常所说的延迟物化是指延迟加载列,如图 6 所示,YMatrix 把这一能力下放到了存储层。随着数据的反复处理,同一 Tablet 的有效数据会越变越少,但重组内存的开销很大,而且会打断流水线的执行。当计算开销的比重下降后,这些额外的惩罚会很大。直到整个 tablet 中的比例降到一定程度,YMatrix 才会通过一个物化动作进行数据的重新整理,实现性能的优化。

  • 特化处理。简单的循环结构容易借助编译器生成 CPU 友好的代码,但数值处理出于正确性往往需要加入一些不友好的结构,比如空值处理、长度不固定的数据、处理溢出,还有一些复杂函数。这些结构会引入分支,从而破坏了流水线执行。解决方法包括:引入 branchless 的算法,以及选用针对性的指令或函数。

  • fallback 机制。PostgreSQL 有着丰富的、经优化的类型以及全面的函数支持,有些处理过于复杂,不适合向量化的模式。实现全面的向量化支持,既无必要也未必有收益,YMatrix 为此引入了运行时 fallback 的机制。在改写计划的阶段,原来的执行节点也被保留下来。执行器会动态判断该节点是否可以向量化执行,否则就使用原有的标量化节点。

  • 存储引擎深度协同。PostgreSQL 原生的 AccessMethod 方法将执行流程和访问数据严格分开,index 和 heap 是相互独立的访问方法。这使得 heap 不具备原生过滤的能力,和执行器的信息交互非常有限。YMatrix 则打通了执行器和引擎的数据通道,实现零拷贝。然后在批量接口的基础上,实现过滤条件、排序信息的下推和信息回传,从源头上缩减了数据集。

6.5 生成高效代码

  • 向量化思路和实现方式已经相对成熟,但要得到最优化的结果,工程实践也需要深度打磨。随着优化的逐步深入,每一个性能瓶颈的消除都会带来显著的性能收益。这些技巧包括:

  • 选用高版本 C++,并开启足够的优化级别,指定体系结构。工具链是系统开发的基础,高版本的 C++ 提供了更多高性能基础设施的封装,更友好的模版选择能力。编译器版本、优化级别,以及参数的指定都会影响向量化指令的生成。另外,SIMD 指令和体系结构关系密切,需要注意 CPU 类别,型号等。

  • 使用模版避免虚拟数开销,减少不必要的组合以避免代码膨胀。

  • 内存优化。当优化工作到了一定程度,内存延时,甚至内存带宽都会成为瓶颈。C++ 的容器提供了多种选项,需要选用合适的数据结构并避免不必要的拷贝。当元素数量不确定时,容器扩展的开销巨大,尤其是使用 PostgreSQL 的 memory context 的情况下。PostgreSQL 的统计信息提供了较好的估计,根据这些信息适当预留就会减少这部分开销。Array 的数据量和批次大小直接相关,可以自行维护固定尺寸的内存池来优化。

  • 用位操作和逻辑操作减少条件分支。

  • 用 builtin 函数或 C++ 封装提高低层操作的效率。

  • 使用高效的第三方库。有些三方库已经做过深度的指令级优化,可以直接用来优化相关操作的效率。YMatrix 用得不多,目前只用了 libpdqsort,libxxhash,libdivide 等。

  • 循环次数确定化,方便编译器生成自动 SIMD 指令代码。

综合上述优化,向量化整体贡献了将近 4 倍的性能优化。

七、高效编码链

时序数据字段主要分为三类:时间戳,属性信息和指标数值。而指标数据又分为即时值(gauge)和累计值(count),后者还会衍生出区间值。指标数据都是按时间生成,因此有很强的规律性,在列存模式下有很好的压缩性质。除了 LZ4 和 zstd 这些通用的压缩算法外,YMatrix 还提供了simple8b,doubledelta,deltazigzag 等整型编码方案,针对浮点型提供了gorilla、floatint 等编码方法。其中, floatint 是一种有损编码,它通过还原数据在十进制形式下的相似性而取得更好的压缩比。YMatrix 还实现了组合多种编码和压缩方式,即编码链的功能来最大化压缩效果。

在追求解压速度的场景下,比如 SSB,通常会选用 LZ4 压缩算法。它的压缩率要低于 zstd,但解压速度要快数倍。LZ4 对于容易压缩的数据,解压速度很快。但对于它不擅长的压缩数据也要在一个较大范围内寻找数据和拷贝,解压速度并不快。YMatrix 指定了 7 个排序键。这些列的数据前后重合度很高,无须专门考虑压缩。而在数值列上,采用 deltazigzag 和 simple8b 编码会得到较好的压缩率,实测有 3 倍于 LZ4 的解压速度。

虽然经过排序后的数据并不有利于发挥 YMatrix 的编解码能力,还是可以得到 15% 的性能优化。

八、观测能力

分布式查询优化和执行是非常复杂的过程,CPU、memory、network 和 IO 都可能成为瓶颈。数据库引擎整体的优化更是一个系统工程,涵盖了从系统特征、操作系统、网络、引擎架构、算法到编程实现诸多方面。整体的性能优化需要深刻地了解系统和查询特征,基于合理的期望进行系统设计;在代码优化的过程中需要准确抓住当时的瓶颈点。具体有以下实践。

  • 通过压测工具了解系统特征。实测发现:即使将物理机资源限缩到和 AWS m5.8x large 类似情况,AWS 上的 SSB 性能比物理机仍差了近 10%。使用像 stream 这样的单项压测工具测试,结果表明云主机虽然标称的主频更高,但整数性能要差 10%,内存带宽更是低了 30%。经过专门的内存优化后,性能得到比物理机更明显的提升。

  • 数据特征分析,优化排序键、分区键和编码方案。这些都可以利用 PostgreSQL 的统计信息,以及自研的特征分析工具及编码工具得以快速优化和验证。

  • sar 等系统级监控工具来揭示资源饱和度。早期 MARS 存储引擎的架构设计导致 IO 随机化较多,iops 是瓶颈,且存储带宽利用率不高。虽然在 NVMe 介质上并不明显,但大数据领域仍是 HDD 占主导。在新的 MARS 设计中,通过不断优化 IO 模型,实现了基本打满磁盘带宽的效果。另外,进程级的监控工具也有利于发现不同角色的进程的具体特征。

  • strace 展示 IO 序列特征。IO 的细节和执行次序、并发模式紧密相关,strace 可以给出某一类系统调用的序列、参数和时间,对于优化 IO 顺序性有重要价值。

  • perf 定位热点代码。perf 是和 Linux 内核配套的工具,在代码优化期间可以用于发现 CPU 热点,定位到具体的代码行。它也可以用于跟踪内存访问模式,优化内存分配和操作。

  • 查询 trace 工具显示具体的耗时。explain analyze 提供了常用的耗时信息,但不足以揭示在整个过程中某些内部改动对于查询整体性能的影响。YMatrix 增加了通用的节点级 trace 收集和 MPP 汇总机制。存储引擎的 IO 代价、过滤效果可以直接呈现在 analzye 结果中。像 sort、join 等复杂算子的运行信息也可以呈现出来,作为优化的依据。

九、总结展望

回顾 YMatrix 5.0 性能改进的过程,有如下经验教训可以分享。

  • PostgreSQL 和 Greenplum 有良好的架构设计,在其多进程模型下,也可以得到良好的性能结果。而它们的企业级特性、稳定性则是一些新型引擎无法比拟的。在此基础上,通过工程上的精细打磨也可以实现性能的巨大提升。

  • 设计上遵循第一性原理,切忌过度假设。时序场景查询一般都带有时间段,类似于时间分区,早期的 MARS 设计引入了分组键的概念,即将一定时间范围(比如 1 小时)的数据聚集在一起。事实证明,这几乎没有带来任何独特的收益。真正需要的是有效的过滤,时间作为一个过滤条件,并没有任何特殊之处。

  • 性能瓶颈此起彼伏,需要迭代优化。性能优化很容易陷入“拿着锤子找钉子”的误区:十八般武器轮番上阵,发现没有效果就偃旗息鼓。优化一定要抓住瓶颈点,否则就会被真正的瓶颈掩盖而发挥不出效果。因此过程一定是迭代的,前期没有效果的手段后期可能派上用场。

未来的工作将从如下方面展开。

  • 向量化深入。这包括算子的进一步打磨,内存管理的进一步优化,采用 codegen 优化复杂计算等。

  • 复杂场景的性能优化。以 TPC-DS 为例,复杂查询的优化涉及到优化器、功能支持、资源优化,范围和困难比单纯向量化复杂度高很多。

  • 面向云原生的性能优化。云原生架构一般会采取存算分离的基础设施,动态资源供给,微服务模式,多租户支持。这些架构上的改变需要重新考虑系统的存储结构和执行模型,远程数据访问也会成为优化重点。

附录:SSB 查询

---Q1.1
select sum(lo_extendedprice * lo_discount) as revenue
  from :tname
 where lo_orderdate >= '1993-01-01'
   and lo_orderdate = '1994-01-01'
   and lo_orderdate = '1994-01-31'
   and lo_orderdate = 'MFGR#2221'
   and p_brand = '1992-01-01'
   and lo_orderdate = '1992-01-01'
   and lo_orderdate = '1992-01-01'
   and lo_orderdate = '1997-12-01'
   and lo_orderdate = '1997-01-01'
   and lo_orderdate = '1997-01-01'
   and lo_orderdate