数据库原理

一条sql的执行流程

alt 如上图所示,当一个数据库接受到一个 SQL 查询时,会依次通过四大模块进行处理:查询优化器,查询调度器,查询执行器,存储层,依次解决四个问题:

将 SQL 文本转换成一个 “最佳的” 分布式物理执行计划
将执行计划调度到计算节点
计算节点执行具体的物理执行计划
计算节点从存储层读取数据

存储层

alt 如上图所示,整个查询执行要快,存储层返回数据必须要快。 存储快的核心就是读更少的数据,且读的更快。

读更少数据的优化包括:

分区分桶裁剪
列裁剪
按列存储
按列压缩
高效的压缩和编码
必要的索引
根据索引和元数据尽可能提前过滤无关数据
延迟物化

读的更快的手段包括:

Operations On Encoded Data
Perfetch
向量化处理
减少 IO 和 Seek 次数
多线程

架构

数据库会成为一个平台

开源领域,模块的复用能力

数据库在 用户接口(SQL 语言)层,分布式层,查询执行层,存储层 每一层都会更加统一,更加组件化。

底层硬件导致架构的变更

alt hardware alt

需求导致架构的持续迭代

OLTP,到 OLAP, 再到 OPAP

从结构化数据,到半结构化数据,再到非结构化数据

从 Data WareHouse 到 Data Lake, 再到 LakeHouse alt

架构设计之关键因素之间的权衡

HTAP 数据库关键技术

https://mp.weixin.qq.com/s/swEx8f9oAwBHbOcLmR7IUg

架构层面的常见优化

Partitioning & Sharding

读写分离

特定场景启动专门的 Service

1 Bitmap 用户行为分析 Service
2 Snowflake Search Optimization Service
https://docs.snowflake.com/en/user-guide/search-optimization-service

如何快速打造一个高性能数据库原型

开源数据库会越来越模块化,打造一个高性能的数据库原型会越来越简单,下图是一个利用 DPA 和 一些开源系统打造的数据库架构示意,可能只需要 1 个或者几个人月,就可以打造出这个原型,并且在 SSB,TPC-H,TPC-DS 等标准测试集上取得不错的性能。下面会对图中的一些系统进行介绍。 alt

查询优化 Apache Calcite

Apache Calcite 网上相关的资料已经很多了,这里就不过多介绍了。 Calcite 相比 StarRocks 的优化器扩展性会更好,但是性能不及 StarRocks 的优化器。 架构 alt Calcite 的核心是优化器,同时支持 RBO 和 CBO,包括 Catalog, SQL parser, SQL validator, Query Optimizer,JDBC Server 和 内置的执行器

可扩展性

Calcite 的目标是成为一个通用的查询优化器,可以被各种系统使用,所以在扩展性上做的比较好,开发者在使用 Calcite 时对下面的模块都可以进行扩展:

Relational operators
Planner rules
Cost model
Statistics
元数据:支持 行数,基数,选择度,唯一性等,很多元数据都可以定义

Built-in SQL implementation

Calcite 也内置了一套执行器,可以执行所有的算子和表达式。Calcite 会生成 java 代码,然后编译,并在 JVM 中执行。Calcite 利用了 Janino 库来将优化后的 Plan 编译成 JVM Bytecode 来执行,虽然性能比较低下,但是可以作为默认的保底和 fall-back 的执行方式。

流行度

Calcite 在工业界已经被大量采用,许多项目都使用 Apache Calcite 实现 SQL Parsing, 查询优化,数据联邦,物化视图重写,知名的项目有 Apache Hive, Apache Drill, Apache Flink, Apache Kylin, Apache Druid 和 Dremio 等。

查询执行 Velox

Velox 是由 Mate 开源的执行引擎,目标是想打造一个统一的高性能 C++ 执行引擎,整体实现和 StarRocks 的执行引擎很类型,亮点不多。 可以参考 Velox 的论文,也可以参考之前StarRocks 执行引擎的介绍:

https://blog.bcmeng.com/post/starrocks-source-code-1.html
https://blog.bcmeng.com/post/fastest_database.html
https://zhuanlan.zhihu.com/p/506063323
https://zhuanlan.zhihu.com/p/555302106

Velox 的执行引擎,和 StarRocks 主要包括下面几部分

Type
兼容 Arrow 的列式布局
向量化的表达式计算
标量和聚合函数
向量化算子
序列化
资源管理

流行度

Velox 开源不久,还没有完全成熟,目前主要和是 Presto 和 Spark 项目合作,一起在搞。

内存存储 Apache Arrow

Apache Arrow 项目的目标是成为一个跨平台,跨语言的列式内存格式和磁盘格式 (Apache Parquet),并以此基础,实现了基于内存的查询引擎,最终成为一个内存数据处理的标准。 alt 上图是 Arrow 的列式内存布局。

网络传输 Apache Arrow Flight

Apache Arrow Flight 基于 gRpc 和 Arrow 列式格式的网络传输框架, 下面两个图是个简单示意,具体可以参考:

https://www.dremio.com/subsurface/an-introduction-to-apache-arrow-flight-sql/

alt alt DataFusion 基于 Apache Arrow 实现的可扩展的查询引擎,包括查询优化和查询执行框架。

下图是 Arrow 和 DataFusion 逐步向 DataBase 演化的路径: alt 下图是 DataFusion 的框架,核心是可扩展: alt 流行度 Apache Arrow 已经被业界项目广泛采用,比如 Velox,Spark, StarRocks, Snowflake 等系统都使用了 Apache Arrow。

分布式框架 DPA 对分布式查询的相关功能进行了统一和抽象:数据复制,更新一致性,容错,查询负载均衡,弹性,持久化等。 可以参考之前的文章:

千行代码构建高性能 OLAP 数据库
https://blog.bcmeng.com/post/dpa.html

RocksDB

一个基于 LSM Tree 的 KV store,被大量用来打造分布式 KV 存储或者元数据存储。

序列化

常见有 Json,ProtoBuf,FlatBuffers 等框架,业内已经有大量的成熟框架可供选择。

打造一个成熟的数据库的难点有哪些

有人会问,打造一个数据库原型如此简单的话,那么数据库公司花费数十人年,数百人年,甚至数千人年都是在做什么。 下一篇文章我会回答这个问题,为什么打造一个成熟数据库如此困难,为什么数据库是一个如此复杂的工程。

查询优化器

查询优化器的核心任务是生成一个 “最佳的”(执行 Cost 最低)的分布式物理执行计划,查询的数据量越大,查询的 SQL 越复杂,查询优化器的意义越大,因为不同的物化执行计划,执行时间可能相差成千上万倍。 查询优化器就像军队的元帅,一旦决策错了,底下的将军和士兵战斗力再强也注定失败。 所以一个 OLAP 数据想要在复杂查询下实现高性能,必须拥有一个成熟,高效的优化器。 正式因为查询优化器如此重要,StarRocks 选择了从零自研一个 CBO 优化器,StarRocks 的优化器主要基于 Cascades 和 ORCA 论文实现,并结合 StarRocks 执行器和调度器进行了深度定制,优化和创新。完整支持了 TPC-DS 99 条SQL,实现了公共表达式复用,相关子查询重写,Lateral Join, CTE 复用,Join Rorder,Join 分布式执行策略选择,Global Runtime Filter 下推,低基数字典优化等重要功能和优化。 alt 从 SQL 文本到分布式物理执行计划, 在 StarRocks 中,一般经过以下几个步骤:

SQL Parse: 将SQL 文本转换成一个 AST(抽象语法树)
SQL Analyze:基于 AST 进行语法和语义分析
SQL Logical Plan: 将 AST 转换成逻辑计划
SQL Optimize:基于关系代数,统计信息,Cost 模型对逻辑计划进行重写,转换,选择出 Cost “最低” 的物理执行计划
生成 Plan Fragment:将 Optimizer 选择的物理执行计划 转换为 BE 可以直接执行的 Plan Fragment。

查询执行器

查询执行器单核想要实现极致的查询性能,需要满足两点:

1 处理的数据尽可能少且尽可能快;
2 网络传输的数据尽可能少且尽可能快。

查询执行器处理数据量的多和少完全由查询优化器决定,查询执行器本身几乎无法决定;查询执行器处理数据如何尽可能快,就是向量化执行器的执行,我们在下一小节单独讲。 网络传输时数据如何尽可能少,尽可能快,如下图所示: alt

按列 Shuffle: 向量化引擎中按列处理更加高效
按列 压缩
Join 分布式执行时尽可能避免网络传输:主要由查询优化器和查询调度器合作完成
Global Runtime Filter: StarRocks 实现的 Global Runtime Filter 在如何减少网络传输上做了很多优化,这个功能之后我司会安排专门的分享介绍
Operations On Encoded Data: 主要是针对有字典编码的低基数字符串进行的优化,StarRocks的创新之处在于,在支持数据自动 Balance 的分布式架构下,实现了全局字典的维护,并结合 CBO 优化器,对包含低基数字符串的查询进行了全面加速,这个大功能之后我司也会安排专门的分享介绍。 这里我简单介绍下,对低基数字符串查询全面加速的原理:

alt 如上图所示,对于SQL Group By City, Platform, 如果 City, Platform 都是低基数字符串,我们就可以将对两个字符串列的 Hash 聚合变为针对两个 Int 列的 Hash 聚合,这样在 Scan, Shuffle,Hash,Equal,Memcpy 等多个重要操作上都会变快很多,我们实测整体查询性能可以有 3 倍的提升。

最理想的执行模型

充分利用多核能力同时处理一个查询
每个线程执行查询编译后的 Plan
查询编译的 Plan 触发向量化操作

Push VS Pull

alt 如上图所示,在Push的执行方式中,数据流和控制流方向一致,在Pull的执行方式,数据流和控制流方向相反。

大家可以从下面这段伪代码中更直接地体会Push和Pull的区别:

// pull: 先从相邻前驱的 Operater(调用getChild)获得Chunk,
// 然后调用具体算子的process函数处理,最后返回算出的Chunk
class PhysicalOperator {
public:
  Chunk getChunk() {
    auto input_chunk = getChild().getChunk();
    auto result_chunk = process(input_chunk);
    return result_chunk;
  }
  virtual Chunk process(Chunk const& src) = 0;
}

// push:src_chunk由相邻前驱算子传入, 调用具体算子的process函数处理, 得到结果之后,
// 通过相邻后继算子(getNextOperator获得)的consume函数,交给下一级算子处理.
class PhysicalOperator {
public:
  void consume(Chunk const& src_chunk) {
    auto result_chunk = process(src_chunk);
    getNextOperator().consume(result_chunk);
  }
  virtual Chunk process(Chunk const& src) = 0;
}

总的来说,Push 模型能做到的事情,Pull 模型也能做到

Push 模型中,Producers 驱动整个流程;Pull 模型中,Consumers 驱动整个流程。

Push 模型的优点

数据流和控制流解耦,每个算子自身不用处理控制逻辑
高效的支持 DAG 的plan,不只是 Tree 的plan, 可以进行CTE 复用优化 和 Scan Share 优化
可以方便地进行 yield
对异步IO 更友好,处理 IO 任务时,将对应算子暂停,数据就绪是唤醒对应的算子
对 Code-gen 模型更友好

Push 模型的缺点

不好处理 Sort-Merge join
不好处理 Limit 短路
不好处理 Runtime Filter

向量化执行器

alt 向量化在实现上主要是算子和表达式的向量化,上图一是算子向量化的示例,上图二是表达式向量化的示例,算子和表达式向量化执行的核心是批量按列执行,批量执行,相比与单行执行,可以有更少的虚函数调用,更少的分支判断;按列执行,相比于按行执行,对 CPU Cache 更友好,更易于SIMD优化。 alt 如上图所示,向量化执行工程的挑战包括:

磁盘,内存,网络的数据都按照列式布局
所有的算子都必须向量化
所有的表达式都必须向量化
尽可能使用 SIMD 指令
尽可能优化 CPU Cache
重新设计内存管理机制
重新设计重要算子的算法和数据结构
整体性能要提升5倍,意味着所有算子和表达式性能都必须提升 5 倍,任何一个算子和表达式慢了,整体性能就无法提升5倍。

向量化执行的架构没有任何难点,难点是工程实现,难点是实现细节。 向量化执行的目的就是优化性能,所以整个向量化执行其实是一个巨大的性能优化工程。 alt 如上图所示,StarRocks 向量化执行性能优化的手段主要包括以上7种,其中 2 - 7 任何一个环节处理不好,都无法实现一个高性能的向量化执行引擎。

向量化为什么可以提升数据库性能?

本文所讨论的数据库都是基于 CPU 架构的,数据库向量化一般指的都是基于 CPU 的向量化,因此数据库性能优化的本质在于:一个基于 CPU 的程序如何进行性能优化。这引出了两个关键问题:

如何衡量 CPU 性能
哪些因素会影响 CPU 性能

第一个问题的答案可以用以下公式总结:

CPU Time = Instruction Number CPI Clock Cycle Time

Instruction Number 表示指令数。当你写一个 CPU 程序,最终执行时都会变成 CPU 指令,指令条数一般取决于程序复杂度。 CPI 是 (Cycle Per Instruction)的缩写,指执行一个指令需要的周期。 Clock Cycle Time 指一个 CPU 周期需要的时间,是和 CPU 硬件特性强关联的。

我们在软件层面可以改变的是前两项:Instruction Number 和 CPI。那么问题来了,具体到一个 CPU 程序,到底哪些因素会影响 Instruction Number 和 CPI 呢?

我们知道 CPU 的指令执行分为如下 5 步:

取指令
指令译码
执行指令
内存访问
结果写回寄存器

其中 CPU 的 Frontend 负责前两部分,Backend 负责后面三部分。基于此,Intel 提出了 《Top-down Microarchitecture Analysis Method》的 CPU 微架构性能分析方法,如下图所示: alt op-down Microarchitecture Analysis Method 的具体内容大家可以参考相关的论文,本文不做展开,为了便于大家理解,我们可以将上图简化为下图(不完全准确): alt 即影响一个 CPU 程序的性能瓶颈主要有4大点:Retiring、Bad Speculation、Frontend Bound 和 Backend Bound,4个瓶颈点导致的主要原因(不完全准确)依次是:缺乏 SIMD 指令优化,分支预测错误,指令 Cache Miss, 数据 Cache Miss。

再对应到之前的 CPU 时间计算公式,我们就可以得出如下结论: alt 而数据库向量化对以上 4 点都会有提升,后文会有具体解释,至此,本文从原理上解释了为什么向量化可以提升数据库性能。

算子和表达式向量化的关键点

数据库的向量化在工程上主要体现在算子和表达式的向量化,而算子和表达式的向量化的关键点就一句话:Batch Compute By Column, 如下图所示: alt 对应 Intel 的 Top-down 分析方法,Batch 优化了 分支预测错误和指令 Cache Miss,By Column 优化了 数据 Cache Miss,并更容易触发 SIMD 指令优化。

Batch 这一点其实比较好做到,难点是对一些重要算子,比如 Join、Aggregate、Sort、Shuffle 等,如何做到按列处理,更难的是在按列处理的同时,如何尽可能触发 SIMD 指令的优化。

数据库向量化如何进行性能优化 前面提到,数据库向量化是一个巨大的、系统的性能优化工程,两年来,我们实现了数百个大大小小的优化点。我将 StarRocks 向量化两年多的性能优化经验总结为 7 个方面 (注意,由于向量化执行是单线程执行策略,所以下面的性能优化经验不涉及并发相关):

高性能第三方库:在一些局部或者细节的地方,已经存在大量性能出色的开源库,这时候,我们可能没必要从头实现一些算法或者数据结构,使用高性能第三方库可以加速我们整个项目的进度。在 StarRcoks 中,我们使用了 Parallel Hashmap、Fmt、SIMD Json 和 Hyper Scan 等优秀的第三方库。

数据结构和算法:高效的数据结构和算法可以直接在数量级上减少 CPU 指令数。在 StarRocks 2.0 中,我们引入了低基数全局字典,可以通过全局字典将字符串的相关操作转变成整形的相关操作。如下图所示,StarRcoks 将之前基于两个字符串的 Group By 变成了基于一个整形的 Group By,这样 Scan、Hash 计算、Equal、Memcpy 等操作都会有数倍的性能提升,整个查询最终会有 3 倍的性能提升。
alt
自适应优化:很多时候,如果我们拥有更多的上下文或者更多的信息,我们就可以做出更多针对性的优化,但是这些上下文或者信息有时只能在查询执行时才可以获取,所以我们必须在查询执行时根据上下文信息动态调整执行策略,这就是所谓的自适应优化。下图展示了一个根据选择率动态选择 Join Runtime Filter 的例子,有 3 个关键点:
  a. 如果一个 Filter 几乎不能过滤数据,我们就不选择;
  b. 如果一个 Filter 几乎可以把数据过滤完,我们就只保留一个 Filter;
  c. 最多只保留 3 个有用的 Filter
alt
SIMD 优化:如下图所示,StarRcoks 在算子和表达式中大量使用了 SIMD 指令提升性能。
alt
C++ Low Level 优化:即使是相同的数据结构、相同的算法,C++ 的不同实现,性能也可能相差好几倍,比如 Move 变成了 Copy,Vector 是否 Reserve,是否 Inline, 循环相关的各种优化,编译时计算等等。

内存管理优化:当 Batch Size 越大、并发越高,内存申请和释放越频繁,内存管理对性能的影响越大。我们实现了一个 Column Pool,用来复用 Column 的内存,显著优化了整体的查询性能。下图是一个 HLL 聚合函数内存优化的代码示意,通过将 HLL 的内存分配变成按 Block 分配,并实现复用,将 HLL 的聚合性能直接提升了 5 倍。
alt
CPU Cache 优化:做性能优化的同学都应该对下图的数据了熟于心,清楚 CPU Cache Miss 对性能的影响是十分巨大的,尤其是我们启用了 SIMD 优化之后,程序的瓶颈就从 CPU Bound 变成了 Memory Bound。同时我们也应该清楚,随便程序的不断优化,程序的性能瓶颈会不断转移。

alt 下面的代码展示了我们利用 Prefetch 优化 Cache Miss 的示例,我们需要知道,Prefetch 应该是最后一项优化 CPU Cache 的尝试手段,因为 Prefetch 的时机和距离比较难把握,需要充分测试。 alt

Pipeline 多核执行

Yield
用户空间
Morsel-Driven
Task-Queue
Operator asynchronzation
Local Exchange

alt Operator 的状

Ready
Running
Blocked
alt

MPP 多机执行

MPP 是大规模并行计算的简称,核心做法是将查询 Plan 拆分成很多可在单个节点上执行的计算实例,然后多个节点并行执行。每个节点不共享 CPU、内存、磁盘资源。MPP 数据库的查询性能可以随着集群的水平扩展而不断提升。 alt 如上图 所示,StarRocks 会将一个查询在逻辑上切分为多个 Query Fragment(查询片段),每个 Query Fragment 可以有一个或者多个 Fragment 执行实例,每个 Fragment 执行实例会被调度到集群某个 BE 上执行。一个 Fragment 可以包括一个或者多个 Operator(执行算子),图中的 Fragment 包括了Scan、Filter、Aggregate。每个 Fragment 可以有不同的并行度。 alt 如上图 所示,多个 Fragment 之间会以 Pipeline 的方式在内存中并行执行,而不是像批处理引擎那样 Stage By Stage 执行。Shuffle (数据重分布)操作是 MPP 数据库查询性能可以随着集群的水平扩展而不断提升的关键,也是实现高基数聚合和大表 Join 的关键。

查询编译

优化点

先解释执行,然后异步编译查询,编译完成后,将解释执行切换到 编译好的 Native Code 执行 https://github.com/cmu-db/peloton-design/blob/master/bytecode_interpreter/bytecode_interpreter.md

Plan Cache:将相同 pattern 的SQL 编译后的代码模板 Cache 下来,避免重复编译,可以选择 Cache 在 memory 或者 disk 上。 https://github.com/cmu-db/peloton-design/blob/master/codegen_cache/codegen_cache.md

向量化 VS 查询编译

Data-centric is better for "calculation-heavy" queries with few cache misses
Vectorization is slightly better at hiding cache miss latencies

查询调度器

为了能充分利用多机的资源,除了 MPP 多机并行执行框架外,查询调度器也必须能均衡多机上的负载,尽可能避免热点的出现; 为了能充分利用多核的资源,Pipeline 并行执行框架,在多核调度时也需要考虑多核上负载的均衡。 alt 在生成查询的分布式 Plan 之后,FE 调度模块会负责 PlanFragment 的执行实例生成、PlanFragment 的调度、每个 BE 执行状态的管理、查询结果的接收。

有了分布式执行计划之后,我们需要解决下面的问题:

哪个 BE 执行哪个 PlanFragment

每个 Tablet 选择哪个副本去查询

多个 PlanFragment 如何调度

StarRocks 会首先确认 Scan Operator 所在的 Fragment 在哪些 BE 节点执行,每个 Scan Operator 有需要访问的 Tablet 列表。然后对于每个 Tablet,StarRocks 会先选择版本匹配的、健康的、所在的 BE 状态正常的副本进行查询。在最终决定每个 Tablet 选择哪个副本查询时,采用的是随机方式,不过 StarRocks 会尽可能保证每个 BE 的请求均衡。假如我们有 10 个 BE、10 个 Tablet,最终调度的结果理论上就是每个 BE 负责 1 个 Tablet 的 Scan。

当确定包含 Scan 的 PlanFragment 由哪些 BE 节点执行后,其他的 PlanFragment 实例也会在 Scan 的 BE 节点上执行 (也可以通过参数选择其他 BE 节点 ),不过具体选择哪个 BE 是随机选取的。

当 FE 确定每个 PlanFragment 由哪个 BE 执行,每个 Tablet 查询哪个副本后,FE 就会将 PlanFragment 执行相关的参数通过 Thrift 的方式发送给 BE。

目前 FE 对多个 PlanFragment 调度的方式是 All At Once 的方式,是按照自顶向下的方式遍历 PlanFragment 树,将每个 PlanFragment 的执行信息发送给对应的 BE。

多资源多任务的统一调度

alt

预处理

不同的优化时机

alt

Snowflake 自动物化部分 Fragment 的查询结果

alt

物化视图

索引

预聚合

#物化列

alt alt

本文由 mdnice 多平台发布

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值