分布式数据库产品总结

  • Pivotal
  • Greenplum Database(GPDB)
  • 架构
  • 查询计划并执行
  • 查询优化
  • 索引
  • blink tree
  • 执行器
  • 数据shuffle
  • 分布式事务
  • 2PC
  • gp实现
  • gp优化
  • MVCC
  • 特点
  • HTAP
  • HAWQ
  • Snowflake Elastic Data Warehouse
  • 数据存储
  • 虚拟数据仓库实例(Virtual Warehouse)
  • AnalyticDB
  • 系统架构
  • 保证
  • 存储引擎
  • 读写过程
  • 数据合并
  • 行列混存
  • inverted index
  • 元数据
  • 索引管理
  • 索引构建
  • 优化器
  • 执行引擎
  • 总结
  • PolarDB/PolarFS
  • 读写流程
  • ParallelRaft
  • PolarFS
  • 事务的数据可见性问题
  • DDL问题
  • Change Buffer问题
  • Polar-X
  • ClickHouse
  • 设计目标
  • ClickHouse存储引擎
  • 计算引擎
  • ClickHouse总结
  • TiDB
  • TiFlash
  • C-Store(2005)/Vertica
  • Apache ORC
  • Dremel (2010) / Apache Parquet
  • Impala
  • 查询流程
  • Druid
  • Pinot
  • mongoDB
  • 参考链接



Pivotal

Greenplum Database(GPDB)

**Greenplum Database(GPDB)**是一款基于开源 PostgreSQL 扩展的 MPP(massively parallel processing),可支持大规模水平扩展的分布式数据库。 GPDB 采用的是 master-worker 模式,每个 worker process 运行在不同的机器上,拥有各自的存储和运算资源。**客户端通过 master 把查询语句分发到各个机器上,以达到并行计算来处理海量数据。**集群节点(无论是master还是segemnt)上的每个实例都是一个物理上独立的PostgrepSQL数据库。

Greenplum数据库是一种shared nothing的分析型MPP数据库。这种模型与高度规范化的/事务型的SMP数据库有显著区别。Greenplum数据库使用非规范化的模式设计会工作得最好,非规范化的模式适合于MPP分析型处理,例如带有大型事实表和较小维度表的星形模式或者雪花模式。

分布式 数据库 ES 技术架构 分布式数据库总结_分布式 数据库 ES 技术架构

Greenplum 在 PostgreSQL 之上还添加了大量其他功能,例如 Append-Optimized 表、列存表、外部表、多级分区表、细粒度资源管理器、ORCA 查询优化器、备份恢复、高可用、故障检测和故障恢复、集群数据迁移、扩容、MADlib 机器学习算法库、容器化执行 UDF、PostGIS 扩展、GPText 套件、监控管理、集成 Kubernetes 等。

架构
  • master: 保存元数据而不保存用户数据,有用户表信息,优化器使用这些信息进行查询优化和计划生成
  • segment:每个segment保存用户数据表的一部分。在 Greenplum 中,用户数据按照某种策略分散到不同节点的不同 segment 实例中。

使用标准的 INSERT SQL 语句可以将数据自动按照用户定义的策略分布到合适的节点,然而 INSERT 性能较低,仅适合插入少量数据。Greenplum 提供了专门的并行化数据加载工具以实现高效数据导入,详情可以参考 gpfdist 和 gpload 的官方文档。

在数据分布方面,Greenplum 在这方面不单单做到了基本的分布式数据存储,还提供了很多更高级灵活的特性,譬如多级分区、多态存储。Greenplum 6 进一步增强了这一领域,实现了一致性哈希和复制表,并允许用户根据应用干预数据分布方法。有这么多种手段,可见Greenplum用户肯定时遇到了很多数据倾斜的问题

Greenplum支持的分区方法有:

范围分区:根据某个列的时间范围或者数值范围对数据分区。譬如以下 SQL 将创建一个分区表,该表按天分区,从 2016-01-01 到 2017-01-01 把全部一年的数据按天分成了 366 个分区:

CREATE TABLE sales (id int, date date, amt decimal(10,2))
DISTRIBUTED BY (id)
PARTITION BY RANGE (date)
( START (date '2016-01-01') INCLUSIVE
END (date '2017-01-01') EXCLUSIVE
  EVERY (INTERVAL '1 day') );

列表分区:按照某个列的数据值列表,将数据分不到不同的分区。譬如以下 SQL 根据性别创建一个分区表,该表有三个分区:一个分区存储女士数据,一个分区存储男士数据,对于其他值譬如 NULL,则存储在单独 other 分区。

CREATE TABLE rank (id int, rank int, year int, gender char(1), count int ) 
DISTRIBUTED BY (id)
PARTITION BY LIST (gender)
( PARTITION girls VALUES ('F'), 
PARTITION boys VALUES ('M'), 
DEFAULT PARTITION other );

Greenplum 支持多态存储,即单张用户表,可以根据访问模式的不同使用不同的存储方式存储不同的分区。通常不同年龄的数据具有不同的访问模式,不同的访问模式有不同的优化方案。多态存储以用户透明的方式为不同数据选择最佳存储方式,提供最佳性能。Greenplum 提供以下存储方式:

  • 堆表(Heap Table):堆表是 Greenplum 的默认存储方式,也是 PostgreSQL 的存储方式。支持高效的更新和删除操作,访问多列时速度快,通常用于 OLTP 型查询。
  • Append-Optimized 表:为追加而专门优化的表存储模式,通常用于存储数据仓库中的事实表。不适合频繁的更新操作。
  • AOCO (Append-Optimized, Column Oriented) 表:AOCO 表为列表,具有较好的压缩比,支持不同的压缩算法,适合访问较少的列的查询场景。
  • 外部表:外部表的数据存储在外部(数据不被 Greenplum 管理),Greenplum 中只有外部表的元数据信息。Greenplum 支持很多外部数据源譬如 S3、HDFS、文件、Gemfire、各种关系数据库等和多种数据格式譬如 Text、CSV、Avro、Parquet 等。

存储方式和分区方式相组合,可以对一张表不同的数据区域有不同的存储方式。

数据分布是任何 MPP 数据库的基础也是 MPP 数据库是否高效的关键之一。通过把海量数据分散到多个节点上,一方面大大降低了单个节点处理的数据量,另一方面也为处理并行化奠定了基础,两者结合起来可以极大的提高整个系统的性能。譬如在一百个节点的集群上,每个节点仅保存总数据量的百分之一,一百个节点同时并行处理,性能会是单个配置更强节点的几十倍。如果数据分布不均匀出现数据倾斜,受短板效应制约整个系统的性能将会和最慢的节点相同。因而数据分布是否合理对 Greenplum 整体性能影响很大

Greenplum 6 提供了以下数据分布策略。

  1. 哈希分布:数据使用哈希分布,每个分布键可以包含多个字段,分布的时候对整个分布键下的tuple算哈希,然后放入对应的segment
  2. 随机分布:如果不能确定一张表的哈希分布键或者不存在合理的避免数据倾斜的分布键,则可以使用随机分布。随机分布会采用循环的方式将一次插入的数据存储到不同的节点上。随机性只在单个 SQL 中有效,不考虑跨 SQL 的情况。譬如如果每次插入一行数据到随机分布表中,最终的数据会全部保存在第一个节点上。
  3. 复制表(Replicated Table):整张表在每个节点上都有一个完整的拷贝
查询计划并执行

PostgreSQL 生成的查询计划只能在单节点上执行,Greenplum 需要将查询计划并行化以充分发挥集群的优势

Greenplum 引入 Motion 算子(操作符)实现查询计划的并行化。Motion 算子实现数据在不同节点间的传输,它为其他算子隐藏了 MPP 架构和单机的不同,使得其他大多数算子不用关心是在集群上执行还是在单机上执行。每个 Motion 算子都有发送方和接收方。此外 Greenplum 还对某些算子进行了分布式优化,譬如聚集。

  • motion:MPP架构下必须有motion算子,协调数据分布,这是执行计划中很重要的一步。
  • dispatcher:分配QE资源,配置Slice,分发任务(plan+slicetable --> 还可以分发一些纯文本的命令、两阶段提交、CdbDispatchUtilityStatementvectoring中的分发语法树(一些语法信息只有QE上有)),协调控制(控制、等待下发的任务的状态)
  • interconnect:QE之间数据传送的模块
  • 引入udp主要是为了解决OLAP查询在大集群中使用连接资源过多的问题
  • interconnect主要遇到的问题就是连接数不够用和稳定性这两方面的问题,所以考虑:
  • QUIC协议
  • Proxy协议(感觉有点服务治理的意思了)
查询优化
索引
  • greenplum中的索引都是二级索引(非聚集索引)
  • 物理上是存储在独立的文件中的(独立于表的数据文件)
  • 并且也是按分片存储在每个segment上,其索引内容对应segement上的数据分片
  • 不同于原生blink树,兄弟节点之间使用右向指针,greenplum采用双向指针
  • 物理结构上,每个页包含索引元组(和boltdb差不多),special中存储了页面级的元信息:
  • 兄弟指针
  • 页面类型
  • 等等
  • 叶子节点的填充率最高是90%,内部节点(非叶子节点)的填充率最高是70%,不填满是为了方式insert会造成频繁的页分裂。
blink tree

要点总结

  1. 构建blink tree
  2. 节点分裂与合并,分裂的过程有可能导致树层数的增加,(不知道合并会不会涉及树层数的减少)
  3. 朴素并发控制
  1. search操作逐层下降,加锁操作先获取当前节点锁,然后再释放上层锁
  2. insert/update也是由根节点触发逐层下降,写锁会在所有层都加锁,但是如果确定当前的节点是安全节点(在某个节点上插入一个新数据之后,不会触发它的分裂,那么这个节点就称为安全节点),那么其上层父亲的锁就可以被释放了
  3. 正确性证明:
  1. 读读操作:只涉及读锁,完全并发,–>正确
  2. 写写/读写操作:查询操作路径每次都是先获取锁再释放锁,最终只可能锁住叶子节点,更新操作由于全部加锁,因此不存在并发修改,–>正确
  3. 是否死锁:加锁都是逐层下降的,因此不会死锁
  1. 问题:由于每次都是从根节点下降的时候都需要加锁,因此靠近根节点的位置锁冲突概率比较高,另外再路径下降时加的这些锁大概率马上就会被释放掉,所以效率较低
  1. greenplum并发控制优化:
  1. blink树希望可以放松在insert过程中对Btree加的锁类型,可以只加读锁,不加写锁
  2. 由此带来的问题是,并发写有可能导致某个写操作索引到了错误的叶子节点
  3. 因此blink树通过引入high key和右兄弟指针,用于及时发现节点已经被分裂,如果分裂,所查询的键值一定在右兄弟节点上。
执行器
  • QD(Query Dispatcher、查询调度器):Master 节点上负责处理用户查询请求的进程称为 QD(PostgreSQL 中称之为 Backend 进程)。 QD 收到用户发来的 SQL 请求后,进行解析、重写和优化,将优化后的并行计划分发给每个 segment 上执行,并将最终结果返回给用户。此外还负责整个 SQL 语句涉及到的所有的 QE 进程间的通讯控制和协调,譬如某个 QE 执行时出现错误时,QD 负责收集错误详细信息,并取消所有其他 QEs;如果 LIMIT n 语句已经满足,则中止所有 QE 的执行等。QD 的入口是 exec_simple_query()。主要是火山模型
  • QE(Query Executor、查询执行器):Segment 上负责执行 QD 分发来的查询任务的进程称为 QE。Segment 实例运行的也是一个 PostgreSQL,所以对于 QE 而言,QD 是一个 PostgreSQL 的客户端,它们之间通过 PostgreSQL 标准的 libpq 协议进行通讯。对于 QD 而言,QE 是负责执行其查询请求的 PostgreSQL Backend 进程。通常 QE 执行整个查询的一部分(称为 Slice)。QE 的入口是 exec_mpp_query()。
  • Slice:为了提高查询执行并行度和效率,Greenplum 把一个完整的分布式查询计划从下到上分成多个 Slice,每个 Slice 负责计划的一部分。划分 slice 的边界为 Motion,每遇到 Motion 则一刀将 Motion 切成发送方和接收方,得到两颗子树。每个 slice 由一个 QE 进程处理。上面例子中一共有三个 slice。
  • Gang:在不同 segments 上执行同一个 slice 的所有 QEs 进程称为 Gang
数据shuffle

相邻 Gang 之间的数据传输称为数据洗牌(Data Shuffling)。数据洗牌和 Slice 的层次相吻合,从下到上一层一层通过网络进行数据传输,不能跨层传输数据。根据 Motion 类型的不同有不同的实现方式,譬如广播和重分布。

Greenplum 实现数据洗牌的技术称为 interconnect,它为 QEs 提供高速并行的数据传输服务,不需要磁盘 IO 操作,是 Greenplum 实现高性能查询执行的重要技术之一。interconnect 只用来传输数据(表单的元组),调度、控制和错误处理等信息通过 QD 和 QE 之间的 libpq 连接传输。

Interconnect 有 TCP 和 UDP 两种实现方式,TCP interconnect 在大规模集群中会占用大量端口资源,因而扩展性较低。Greenplum 默认使用 UDP 方式。UDP interconnect 支持流量控制、网络包重发和确认等特性。

分布式事务

Greenplum 使用两阶段提交(2PC)协议实现分布式事务。2PC 是数据库经典算法,此处不再赘述。本节概要介绍两个 Greenplum 分布式事务的实现细节:

(更正:应该不是2pc,应该就是MVCC)

  • 分布式事务快照:实现 master 和不同 segment 间一致性
  • 共享本地快照:实现 segment 内不同 QEs

在 QD 开始一个新的事务(StartTransaction)时,它会创建一个新的分布式事务 id、设置时间戳及相应的状态信息;在获取快照(GetSnapshotData)时,QD 创建分布式快照并保存在当前快照中。和单节点的快照类似,分布式快照记录了 xmin/xmax/xip 等信息。这些信息被用于确定元组的可见性(HeapTupleSatisfiesMVCC)。

和 PostgreSQL 的提交日志 clog 类似,Greenplum 需要保存全局事务的提交日志,以判断某个事务是否已经提交。这些信息保存在共享内存中并持久化存储在 distributedlog 目录下。

为了提高判断本地 xid 可见性的效率,避免每次访问全局事务提交日志,Greenplum 引入了本地事务-分布式事务提交缓存

2PC

greenplum基于PG,虽然PG没有分布式事务管理器,但是支持2阶段提交2pc

PREPARE TRANSACTION
COMMIT PREPARED
ROLLBACK PREPARED

prepare之后持有的行锁不会被释放,就算宕机,重启pg节点之后锁还是在的(数据库会将prepare写进事务日志,需要进行负责恢复),方便后续协调者cordinator继续对该事务进行操作

gp实现

gp在pg的基础上,实现了:

  1. 分布式事务管理
  1. 分布式事务的创建、状态迁移等
  2. QD向QE发起两阶段提交
  1. 分布式快照(全局的一致性快照)
  1. QE向QE发送全局快照信息
  2. Writer QEReader QE共享本地快照信息
  1. Distributed log:分布式事务提交日志
  1. 由于判断分布式事务是否提交,擢用和PG的commmit log(clog)类似,基于simple LRU实现。
  1. 分布式死锁检测
gp优化

理论上,如果参与者只有1个,2pc可以简化为1pc(Bernstein称为协调权的转移,因为只有一个参与者的话,就不需要协调者了)

  • 满足1pc的条件:
  • 有写操作,但是参与者只有1个
  • 只读事务,也不需要2pc
MVCC

主流数据库三大并发控制方法:

  1. MVCC
  2. 2PC
  3. 乐观锁

事务的本质就是将多个步骤捆绑为原子的步骤,要么都成功,要么都不成功,事务的中间状态不应该被其他事务看到(隔离isolation)。

greenplum只实现了read commited和repeatable read.

MVCC主要为了解决读写冲突。

  • heaptuple用于整整存储每行数据 |t_xmin|t_xmax|t_cid|t_ctid|t_infomaskt2|t_infomask|NULL_bitmap|userdata|
  • Xmin:创建tuple的事务ID
  • Xmax:删除tuple的事务ID,有时用于行锁(有事务在更新该行,通过配合infomask中的HEAP_XMAX_EXCL_LOCK完成)
  • cid:事物内的查询命令编号,用户跟踪事务内部的可见性,创建cursor(游标)和更改cursor中的内容,这两步需要保证看到的事务是一样的。
  • ctid:指向下一个版本tuple的指针,由两个成员blocknumberoffset组成
  • 这个和mysql不同,mysql存储的是增量信息
  • t_infomask用以加速可见性的查询,标记优化,比用每次都去看mvcc快照或者pg_clog
  • heappage用于存每个heaptuple的偏移量
  • greenplum使用mvcc快照,在pg的2pc之上提供隔离级别的保证
  • 快照理论上是一个正在运行的事务列表
  • greenplum使用快照判断一个事务是否已提交,其中包含如下信息:
  • Xmin:所有小于Xmin的事务都已经提交
  • Running:正在执行的事务列表,这里面保存的所有事务都是未提交的,不在这里面,并且处于水位之间的id就是已经提交的
  • Xmax:所有大于等于Xmax的事务都未提交
  • 该快照中只存储了事务commited或running状态,如果需要查询abort状态,需要查询pg_clog(事务日志)。(事务在abort的时候不需要更改行中的t_xmax
  • 在READ COMMITTED隔离级别下,每个查询开始时生成快照
  • 在REPEATABLE READ隔离界别下,在每个事务开始时生成快照
  • 在一众版本中,某个事务必然只能看到其中一个,所以事务需要判断快照的可见性
  • 在视频22:50讲解的很详细
  • 写写冲突情况下,后面的事务会阻塞直到前面一个事务完成

一个transaction共有三种状态, committed,running,abort

特点
  1. mvcc有清理需求
  1. 在更新tuple的时候会创建一个新的tuple,所以旧的tuple需要清理
  2. 在删除tuple时,只会标记xmax,不会立即删除
  3. 失败的事务abort掉之后,tuple也就失效了
  4. 三种情况下产生的垃圾tuple在一段时间之后就都不可见了
  1. 何时删除?
  1. 访问到某个page的时候,顺手清理 --> 单页面清理
  2. 通过vacuum命令清理整个表
  1. 如果更新频繁,tuple特别特别多,那么在当前的page就不新生成index了,而是每个tuple通过链表的形式进行组织。节约index空间

HTAP

2021 sigmod论文《Greenplum: A Hybrid Database for Transactional and Analytical Workloads

HAWQ

针对 Hadoop 存储的 SQL 执行引擎。HAWQ 通过数据接口可以直接读取 Hive 表里的数据(也支持原生存储格式),然后用 SQL 执行引擎来计算得到查询结果。与 HiveQL 通过把 SQL 解析成一连串的 MapReduce job 的执行模式相比,速度要快好几个量级。HAWQ 虽然在开发执行引擎过程中借鉴了很多 GPDB 的东西,但毕竟是一款不同的数据库引擎,Pivotal 因此希望有一款兼容的优化器能够服务于它。由此,研发了开源优化器 ORCA。

Snowflake Elastic Data Warehouse

除了使用了 vec-exec(毕竟,联合创始人 Marcin 的博士毕业论文就是关于 vec-exec 的),Snowflake 也是一款 100%计算和存储分离,面向云原生的数据仓库系统。本文内容主要参考他们发表于 SIGMOD-16 的 paper: The Snowflake Elastic Data Warehouse

Snowlake 是 2012 年成立的,2015 年正式推出商用版本。2012 年,正是云服务起步不久,大数据热火朝天的时候。当时,数据仓库的主流趋势是 SQL On Hadoop。Cloudera, Hontornworks, MapR, Greenplum HAWQ, Facebook 的 Presto,算是百花齐放。但主创团队认为,RDBMS 不会消失,用户们会因为上云的趋势,想要一款完全适配云端的数据仓库。

文章简单介绍了市面上通常的 on-prem 分布式数据仓库的一些缺点。首先就是计算和存储硬件是耦合的,即每个服务器同时负责存储数据,并且执行 SQL 语句得到结果。耦合的劣势在于,不能针对不同的 workloads 做优化。二就是服务器的 node membership 改变(无论是因为服务器损坏,或者是因为数据量提升需要扩容)对用户来说都不友善。一,就是要进行大量数据的 reshuffle。二是,为了做到高可用,可能会保留一部分 node 作为 stand-by replica,当主节点有问题时,马上接替主节点,这相当于变相提高了数据成本。总结来说,on-prem 的数据仓库要做到同时保持可伸缩性(elasticity)和高可用性(availability)并兼顾成本,是很难鱼与熊掌兼得的。三就是对服务进行升级比较麻烦。

由于云服务的出现,很多上述的问题,变得不再是问题了。一就是,云服务通常会提供多种类型的服务器来针对特定的 usecase;二,服务器的下线,上线,扩容在云服务上都属于基本操作;三是,云上有高可用,低成本的存储系统;四是,服务更新非常方便。基于这些原因,Snowflake 选择了完完全全的计算和存储分离的架构设计。整个架构分成三个大模块:

  1. 数据存储:完全交给 AWS 的 S3 来存储数据。
  2. Virtual Warehouse(VW) 虚拟数据仓库实例(下面简称 VW):由多个 Virtual Node(AWS 中的 EC2 instance)组成的一个 Virtual Cluster,负责执行各种 SQL 语句,因此称为 Virtual Warehouse。数据库的执行引擎是也是自己构建的分布式引擎。
  3. Cloud Services:整个 Snowflake 的大脑:负责管理数据存储和 VW,以及其他一系列的操作,比如安全,登陆,事物管理,用户隔离,等等。值得注意的是,你可以大致认为整个 AWS,所有的用户,共享这一个大脑实例(当然,这个实例本身是多中心复制,高可用加高备份的),但每个用户只能管理属于自己的数据和 VW。

数据存储

在设计存储系统的时候,Snowflake 有纠结过,是应该使用 AWS 的 S3,还是自行设计类似于 HDFS 的存储系统。最终,在经过了各种比较,利弊权衡后,决定使用 S3。虽然,S3 的性能并不是最快;并且,由于是网络接入,也不是最稳定。但是,胜在高可用性和高可靠性上。团队决定基于 S3 打造数据存储系统,同时,可以把精力放在优化 local caching 和数据倾斜(skew resilience)上。

相对于本地文件系统,S3 的 access latency 会更高,并且,由于是网络接入(尤其是用 https),CPU 使用率也更高。而且,S3 本身就是一个简单的 blob 存储,支持的主要创建,删除和读取文件,即,不能对现有文件进行更新,更新相当于重新创建一个更新过的文件。但是,S3 的读取有一大好处在于,可以读取部分文件。

S3 的这些属性,对于整个 Snowflake 的数据存储和并行控制设计有重大的影响。首先,表数据被水平(horizontally partitioned)地切分成多个不可变的 blob 文件;每个文件通过列存(column-store)的形式保存数据,Snowflake 具体使用的存储格式是 PAX 的 Hybrid-column store(挖个坑,可以单独讲一期这个)。每个数据文件包含数据头用来存储元数据。基于 S3 的下载部分文件的 API,对于运行的 SQL 语句,优化器会选择只下载必须用到的数据 block 即可。这也就意味着所有snowflake的事务都是基于快照隔离Snapshot Isolation(SI)

值得一提的是,Snowflake 不单单使用 S3 来存储表数据文件,也用 S3 来存储临时生成的 intermediate result(语句执行中,某个 operator 产生的临时结果集)。一旦这些结果集的大小超过了本地磁盘空间,spill 到磁盘上的文件就会以 S3 的形式存储。这样的好处在于,可以让 Snowflake 真正可以处理巨大的数据而不用担心内存或者本地磁盘空间吃紧。另一个好处在于,这些临时结果集也可能被利用作为 cache 使用。

最后文中还提到了数据库的其他元数据存储,包括有哪些 caching 文件,每个表存在了哪些 S3 文件中,等等,都是存储在一个 transactional 的 key-value store 中,并不在 S3 里。

虚拟数据仓库实例(Virtual Warehouse)

执行 SQL 语句:每个语句 instance 都只会运行在一个 VW 上;每个 VW 有多个 WN;每个 WN 只隶属于一个 VW,不会被共享。(这边有注解说,WN 变成共享的会是一个未来的工作,因为可以更好地提升使用率并且会进一步降低用户成本)。当一个语句被运行时,所有的 WN 在这个 VW 上,(或者也可能是一部分 WN,如果优化器认为这是一个非常轻量级的语句),都会起一个 worker process,这个进程的生命周期就是这句语句的执行周期。worker process ,在执行的过程中,不会对外部资源造成任何变化,换言之,no side effect,即使是 update 语句。为什么这么说呢,因为所有的表数据文件都是 immutable 的。这样带来的好处就是,如果 worker process 由于各种原因崩溃了, 通常只是需要 retry 即可,没有其他善后事宜要做。现在 VW 里还不支持 partial retry,这也在未来计划的工作中。

由于 VW 的可伸缩性(elasticity),通常情况下,可以通过起一个更大 size 的 VW 来提升语句的性能,但保持一样的使用成本。例如,**一个复杂的分析语句在一个 4 节点 VW 上需要运行 15 个小时,但在一个 32 节点 VW 上只需要 2 小时。**因为是云原生,用户只需要支付运行 VW 时的费用即可。因此,在价格不变的情况下,用户体验和查询速度却大幅度提升。这也是 Snowflake 云原生数据仓库的一大卖点。

  • 本地缓存: 每个 WN 都会用本地文件为表数据做本地缓存,即已经被从 S3 那读取的数据文件。这些文件是包含元数据信息和要用到的 column 的数据。这些缓存的数据文件可以被多个 worker process 共享(如果需要读取一样的数据),文中提到维护了一个简单的 LRU 的 cache replacement 策略,效果非常不错。为了进一步提升 hit rate,同一份数据文件被多个 WN 节点保存,优化器会用 consistent hashing 算法,来分配哪些节点保存哪些数据。同时,对于后续要读取对应数据的语句,优化器也会根据这个分配发送到对应节点。
  • 数据倾斜处理:一些节点可能相对于其他节点,运行更慢,比如硬件问题或者是单纯网络问题。Snowflake 的优化是,每个 WN 在读取了相应的数据文件后,当它发现其他 WN 还在读取,他会发送请求给其他 WN 要求分担更多的数据,而且这些数据直接从 S3 读取。从而来确保不要把过多的数据处理放在速度慢的 WN 上。
  • 执行引擎:虽说可以通过增加节点来提升性能,但是 Snowflake 依然希望每一个节点的单体性能都能做到极致。因此,Snowflake 构建了自己的,基于列存向量执行(vec-exec),并且是 push-based(推模式)的执行引擎。
  • Columnar: 没啥争议,对于 OLAP 语句来说,Columnar-store 无论从存储,读取效率和执行效率来说,都优于 row-store。
  • Vec-exec:也没有争议,Marcin 肯定把 Vec-Exec 这套运行优化放到执行器上。
  • push-based: 相对于 Volcano 的拉模式,是下方的 operator,当处理完数据后,把数据 push 到上方的 operator(从执行计划角度来看上下),类似于 code-gen,这样的好处是提高了 cache 的利用率,因为可以避免不必要的循环控制语句。
  • 另一点就是,一些其他传统数据库系统在执行语句时需要考虑的麻烦,对于 Snowflake 来说没有。比如,不用 transaction management,因为所有的语句都是没有 side effect 的。(原因是S3中的文件不可以更改)

AnalyticDB

数据库带来的新挑战:

  1. 在线化和高可用:离线和在线的边界越来越模糊,一切数据皆服务化、一切分析皆在线化;
  2. 高并发低延时:越来越多的数据系统直接服务终端客户,对系统的pp和处理延时提出了新的交互性挑战;
  3. 混合负载:一套实时分析系统既要支持数据加工处理,又要支持高并发低延时的交互式查询;
  4. 融合分析:随着对数据新的使用方式探索,需要解决结构化与非结构化数据融合场景下的数据检索和分析问题。

Oracle RAC --> Greenplum --> HBase --> AnalyticDB

ADB主要是OLAP系统,同时要顾及各种点查询、优化的速度。底层采用盘古,所以数据库主要的创新点在数据格式、优化器、执行器等等

系统架构

AnalyticDB主要分为以下几个部分:

  1. Coordinator(协调节点):协调节点负责接收JDBC/ODBC连接发过来的请求,并将请求分发给读节点或者写节点
  2. Write Node(写节点):只处理写请求(如INSERT、DELETE、UPDATE)的节点。
  1. 某个写节点会被选为主节点,其他写节点选为从节点,主节点和从节点之间通过ZooKeeper来进行通信。每个节点会独立负责某些一级分区的数据,主节点的任务就是决定每个节点负责哪些一级分区。协调节点会将写请求分发到对应的写节点上,写节点收到请求后,会将写SQL语句放到内存buffer中,这些buffer中的SQL语句称为log数据。
  2. 写节点会将buffer中的log数据刷到盘古上,当刷盘古成功后,写节点会返回一个版本号(即LSN)给协调节点,表示写完成了。每个一级分区在其对应的写节点上,都会独立地对应一个版本号,每次写节点将某个一级分区的log数据刷到盘古后,都会增大这个版本号,并将最新版本号返回给协调节点。
  3. 当盘古上的log数据达到一定规模时,AnalyticDB会在伏羲上启动MapReduce任务,以将log数据转换成真实存储数据+索引
  1. Read Node(读节点):只处理读请求(如SELECT)的节点。
  1. 每个读节点也独立负责某些一级分区的数据。在每个读节点初始化时,它会从盘古上读取最新版本数据(包括索引)。之后,基于这份数据,读节点会从写节点的内存buffer中将写请求log周期性地拉取过来,并在本地进行replay,replay之后的数据不会再存储到盘古中(而是存到本地ssd中?)。读节点根据replay之后的数据,服务到来的读请求。
  2. 由于读节点需要去从写节点上拉取写请求数据,因此读节点为用户提供了两种可见性级别:实时(real-time)可见和延时(bounded-staleness)可见。实时可见允许读节点立即读到写节点写入的数据,延时可见允许读节点在一段时间后才读到写节点上写入的数据。AnalyticDB默认使用的可见性级别为延时可见。(我猜延时可见就是用某种方式读polarFS,这个架构有点像Aurora的一写14读了)
  1. Pangu(盘古):高可靠分布式存储系统,是AnalyticDB依赖的基础模块。写节点会将写请求的数据刷到盘古上进行持久化。
  2. Fuxi(伏羲):资源管理与任务调度系统,是AnalyticDB依赖的基础模块。伏羲合理使用集群机器的空闲资源,以进行相关计算任务的异步调度执行。

为便于大规模分析处理,AnalyticDB对数据表进行分区。AnalyticDB数据表有两个分区级别:一级分区和二级分区。

选择具有较高基数(cardinality)的列作为一级分区键,以保证数据行能均匀地分布到每个一级分区,最大化并行。用户还可以根据需要定义二级分区,以便进行数据的自动管理。二级分区拥有最大分区数,当二级分区的实际数目超过了这个最大分区数后,最老的二级分区会被自动删除。通常,选择时间列(天、周或月)作为二级分区列,这样,包含相同时间序列的数据行,会被划分到同一个二级分区中。

传统OLAP系统在同一个链路上同时处理读写请求,因此,所有的并发读写请求都共享同一个资源池,也会互相影响。但是当读写并发同时非常大时,这种设计会由于过度的资源竞争而导致不好的性能。如图5所示,为了解决这个问题,同时确保读和写的高性能,AnalyticDB采用的架构为读写分离架构,即AnalyticDB有独立的读写节点各自处理读写请求,且写节点和读节点完全互相隔离。

保证

  1. 可靠性:写节点自己选主,并且负责负载均衡,用户可以指定每个读节点的副本个数。既保证了可靠性,又保证了读写带宽
  2. 扩展性:当有新写节点加入时,自动负责负载均衡
  3. 多租户:使用CGroup负责多租户的隔离(CPU/内存/网络带宽)(一个AnalyticDB实例会根据对应的资源创建上面提到的各种节点)

存储引擎

AnalyticDB存储层采用Lambda架构,读节点上的数据包括基线数据增量数据两部分。增量数据又分为Incremental Data和Deleted bitset,按照行列混存的架构存放在读节点的SSD上。真正读取是,basline数据要和增量数据做UNION和MINUS之后,才能输出有效数据。

对于每张表,每k行的数据组成一个Row Group。Row Group中的数据连续存放在磁盘中。整个Row Group中,又将数据按照列(聚集列)分别顺序存放。AnalyticDB会对每列构建一份元数据,用于维护列数据的统计信息(包括Cardinality、Sum和Min/Max等)、字典数据(采用字典编码)以及物理映射等。AnalyticDB默认会对每一列数据建立索引,索引中的Key是列的值,Value是值出现的所有行号集合,采用后台异步构建模式。由于增量数据部分没有索引,随着数据的不断实时写入,增量数据的查询性能会越来越慢。AnalyticDB采用后台任务来合并基线数据和增量数据形成一个新的基线数据,并基于新的基线数据构建全量索引。

读写过程

使用copy on write技术(OLAP读多写少)来支持MVCC,delete数据被转化在Deleted bitset上,而update操作则被分为Incremental Data和Deleted bitset分别存放。每个写操作都会分配独立的LSN,从而达到MVCC

由于建立了全列倒排索引,所以执行引擎处理返回结果的时候用到了多路归并

数据合并

由于没有全局索引,随着数据的不断实时写入,增量数据的查询性能会越来越慢。因此ADB会在后台通过伏羲启动一个MapReduce 任务来合并基线数据和增量数据(同时去掉标记为删除的数据)形成一个新的基线数据,并基于新的基线数据构建全量索引。

在合并任务开始时,一部分增量数据会标记为immutable,并执行合并,合并完成之后,之前的baseline data和immutable会被删除

行列混存

在海量数据分析场景下,数据分析业务主要有以下三类workload:

  1. OLAP场景下的大规模多维分析:海量数据的统计分析和多表关联,比较适合列存格式
  2. 高并发的点查:通常需要捞取出一整行的明细数据,比较适合行存。
  3. 高写入吞吐:每秒千万的高吞吐实时写入,比较适合行存。

在ADB的实现中,每K行数据实现了Row Group,每个row group中的每个列存放在自己的block中,Row group按照索引排列

inverted index

为了应对ad-hoc,ADB对每列建立了倒排索引,从而提高复杂数据的查询效率。(每列都建立索引,不就是倒排索引了)

元数据

为了加速查询,AnalyticDB对每列构建一份元数据,并保存在一个叫detail_meta的单独文件中。detail_meta文件通常较小(小于1MB),首次查询时被加载在内存中。如图8左边所示,元数据主要包括4部分:

  • Header。包括版本号,文件长度以及一些统计信息。
  • 列统计信息。包括行数,NULL值数,cardinality,SUM,MAX和MIN 值。优化器根据这些信息来生成最佳执行计划。
  • 字典。对于cardinality较少(小于1024)的列,AnalyticDB采用字典编码,数据文件里保存字典号码。字典保存在该字段中。
  • 块地址信息。保存块号到数据文件起始地址和长度的映射关系。(我猜测是每次合并的时候更新)

索引管理

AnalyticDB设计和实现了一个新的索引引擎,在不影响写入性能的情况下,支持结构化和非结构化数据类型索引。它将构建过程从写入链路中移除,采用后台异步构建模式,支持对所有列构建索引,从而解决了OLAP任意查询的性能问题

AnalyticDB默认对所有列构建索引,并保存在一个单独的文件中。与传统的数据库不同,AnalyticDB索引中的key是列的值,value是该值出现的所有行号集合,并支持所有的条件同时走索引查询。多个列的操作去做union或者intersect

AnalyticDB在索引引擎是实现上也做了大量的优化,包括:多路流式归并、索引选择CBO和索引结果缓存。

  • 多路流式归并:传统数据库大多采用2路归并策略,在条件数特别多的场景下,会导致大量中间结果,计算效率很低。AanlyticDB采用K路流式归并算法,可以支持多个集合并行归并,避免产生大量中间结果集合,提升了整个归并的速度。
  • 索引选择CBO:当where条件中包括多个条件,并不是所有的条件走索引扫描能取得最佳的性能。利用索引中的统计信息,提前估算出各个条件可能的选择率,对于选择率很高的条件走索引查询,其他条件直接在上层进行过滤操作。例如对于where id = 1 and 0 < x < 1000000的情况下,id = 1这个条件的选择率已经很高,则0<x<1000000条件不走索引查询效率会更高。
  • 索引结果缓存:在OLAP分析场景中,多个查询条件中,可能会出现部分条件固定不变或重复多次出现。针对这种场景AnalyticDB 实现了一个高效的无锁缓存,缓存的的key为等值或range条件,value为行号集合。这样在出现重复查询情况下,可以直接读取缓存,避免索引IO扫描开销。
索引构建

为了支持每秒千万的实时数据写入,避免同步构建索引影响实时写入的性能,AnalyticDB并没有采用同步构建索引的策略,而是采用异步后台进程构建索引的方式。索引引擎会根据时间或增量数据的大小来决定是否启动后台进程来构建索引。该后台进程读取Pangu上的历史全量数据和新写入的增量日志数据,完成数据合并形成新的全量数据,并对该全量数据重新构建索引。该过程通过伏羲的MapReduce任务执行,选择负载较低的机器执行,对用户完全透明。

优化器

创新性引入了两个关键功能:存储感知的优化和高效实时采样。因为ADB独特的索引结构和分布式的数据存储

执行引擎

在优化器之下,AnalyticDB在MPP架构基础上,采用流水线执行的DAG架构,构建了一个适用于低延迟和高吞吐量工作负载的执行器。AnalyticDB的列式执行引擎能够充分利用底层的行列混合存储。与行式执行引擎相比,当前的向量化执行引擎更加缓存友好,能避免将不必要的数据加载到内存中。

与许多 OLAP 系统一样,AnalyticDB在运行时利用代码生成器(CodeGen) 来提高 CPU 密集型计算的性能。AnalyticDB的CodeGen基于 ANTLR ASM来动态生成表达式的代码树。同时此 CodeGen 引擎还将运行时因素纳入考虑,让AnalyticDB能在Task级别利用异构新硬件的能力。例如,如果集群中CPU支持 AVX-512指令集,我们通过生成字节码使用SIMD来提高性能。在此之外,通过整合内部数据表示形式,在存储层和执行引擎之间,AnalyticDB是能够直接对序列化二进制数据进行操作,而不是Java 对象。这有助于消除序列化和去序列化的开销,这在大数据量shuffle时可能会节约20%以上的时间。

总结

得益于流水线处理、全列索引、行列混存、运行时索引路径选择、K路归并、向量化执行引擎、CodeGen等优化机制,AnalyticDB获得了最优的TCP-H测试运行时间,并比Greenplum快了近2倍。

PolarDB/PolarFS

使用共享存储解决MySQL主从结构遇到的一系列问题

系统结构:

  • libpfs是一个用户空间文件系统库,负责数据库的I/O接入。
  • PolarSwitch运行在计算节点上,用于转发数据库的I/O请求。每个请求包含了数据库实例所在的Volume ID、起始偏移和长度。PolarSwitch将其划分为对应的一到多个Chunk,并将请求发往Chunk所属的ChunkServer完成访问。
  • ChunkServer部署在存储节点上,用于处理I/O请求和节点内的存储资源分布。ChunkServer之间通过所谓的ParallelRaft同步数据
  • PolarCtrl是系统的控制平面,它包含了一组实现为微服务的管理者,相应地Agent代理被部署到所有的计算和存储节点上。主要职责:
  • 监控ChunkServer的健康状况,确定哪些ChunkServer有权属于PolarFS集群;
  • Volume创建及Chunk的布局管理(即Chunk分配到哪些ChunkServer);
  • Volume至Chunk的元数据信息维护;
  • 向PolarSwitch推送元信息缓存更新;
  • 监控Volume和Chunk的I/O性能;
  • 周期性地发起副本内和副本间的CRC数据校验。

存储资源管理单元:

  • Volume:是为每个数据库提供的独立逻辑存储空间,其上建立了具体文件系统供此数据库使用,其大小为10GB至100TB,可充分适用于典型云数据库实例的容量要求。
  • Chunk:每个Volume内部被划分为多个Chunk,Chunk是数据分布的最小粒度,每个Chunk只存放于存储节点的单个NVMe SSD盘上,其目的是利于数据高可靠和高可用的管理。典型的Chunk大小为10GB,这远大于其他类似的系统,例如GFS的64MB。虽然chunk很大,但是chunk可以通过在线迁移维持负载均衡(chunk存储在固态盘上、还要在线迁移,这个服务不可用时间有多长???)
  • Block:在ChunkServer内,Chunk会被进一步划分为多个Block,其典型大小为64KB。Blocks动态映射到Chunk 中来实现按需分配。Chunk至Block的映射信息由ChunkServer自行管理和保存,除数据Block之外,每个Chunk还包含一些额外Block用来实现Write Ahead Log(写到optane)。

读写流程

  1. POLARDB通过libpfs发送一个写请求,经由ring buffer发送到PolarSwitch。
  2. PolarSwitch根据本地缓存的元数据,将该请求发送至对应Chunk的主节点。
  3. 新写请求到达后,主节点上的RDMA NIC将写请求放到一个提前分好的buffer中,并将该请求项加到请求队列。一个I/O轮询线程不断轮询这个请求队列,一旦发现新请求到来,它就立即开始处理。
  4. 请求通过SPDK写到硬盘的日志block,并通过RDMA发向副本节点。这些操作都是异步调用,数据传输是并发进行的。
  5. 当副本请求到达副本节点,副本节点的RDMA NIC同样会将其放到预分buffer中并加入到复制队列。
  6. 副本节点上的I/O轮询线程被触发,请求通过SPDK异步地写入Chunk的日志。
  7. 当副本节点的写请求成功回调后,会通过RDMA向主节点发送一个应答响应。
  8. 主节点收到一个复制组中大多数节点的成功返回后,主节点通过SPDK将写请求应用到数据块上。就这一步比较重要,其他不用看
  9. 随后,主节点通过RDMA向PolarSwitch返回。
  10. PolarSwitch标记请求成功并通知上层的POLARDB。

ParallelRaft

ParallelRaft与Raft最根本的不同在于,当某个entry提交成功时,并不意味着之前的所有entry都已成功提交。因此我们需要保证:

  1. 在这种情况下,单个存储的状态不会违反存储语义的正确性;
  2. 所有已提交的entry在各种边界情况下均不会丢失;

有了这两点,结合数据库或其他应用普遍存在的对存储I/O乱序完成的默认容忍能力,就可以保证它们在PolarFS上的正常运转,并获得PolarFS提供的数据可靠性。

ParallelRaft的乱序执行遵循如下原则:

  1. 当写入的Log项彼此的存储范围没有交叠,那么就认为Log项无冲突可以乱序执行;
  2. 否则,冲突的Log项将按照写入次序依次完成。

容易知道,依照此原则完成的I/O不会违反传统存储语义的正确性。

后面说了一大堆,反正就是paxos,因为同一个raft上面,可能会有多个并行的事务,所以一定要乱序提交,乱序确认

PolarFS

PolarFS设计中采用了如下技术以充分发挥I/O性能:

  • PolarFS采用了绑定CPU的单线程有限状态机的方式处理I/O,避免了多线程I/O pipeline方式的上下文切换开销。
  • PolarFS优化了内存的分配,采用MemoryPool减少内存对象构造和析构的开销,采用巨页来降低分页和TLB更新的开销。
  • PolarFS通过中心加局部自治的结构,所有元数据均缓存在系统各部件的内存中,基本完全避免了额外的元数据I/O。
  • PolarFS采用了全用户空间I/O栈,包括RDMA和SPDK,避免了内核网络栈和存储栈的开销。

PolarFS是共享访问的分布式文件系统,每个文件系统实例都有相应的Journal文件和与之对应的Paxos文件。Journal文件记录了metadata的修改历史,是共享实例之间元数据同步的中心。Journal文件逻辑上是一个固定大小的循环buffer。PolarFS会根据水位来回收journal。Paxos文件基于Disk Paxos实现了分布式互斥锁(文件锁,文件系统里的悲观锁,性能如何?)。

由于journal对于PolarFS非常关键,它们的修改必需被Paxos互斥锁保护。如果一个节点希望在journal中追加项,其必需使用DiskPaxos算法来获取Paxos文件中的锁。通常,锁的使用者会在记录持久化后马上释放锁。但是一些故障情况下使用者不释放锁。为此在Paxos互斥锁上分配有一个租约lease。其他竞争者可以重启竞争过程。当PolarFS当节点开始同步其他节点修改的元数据时,它从上次扫描的位置扫描到journal末尾,将新entry更新到memory cache中。

PolarFS的上述共享机制非常适合POLARDB一写多读的典型应用扩展模式。一写多读模式下没有锁争用开销,只读实例可以通过原子I/O无锁获取Journal信息,从而使得POLARDB可以提供近线性的QPS性能扩展。

由于PolarFS支持了基本的多写一致性保障,当可写实例出现故障时,POLARDB能够方便地将只读实例升级为可写实例,而不必担心底层存储产生不一致问题,因而方便地提供了数据库实例Failover的功能。(DBFS,单机高可用)

感觉这个系统从db到libpfs、到后端存储chunkserver,都有WAL…所以最底层做快照,libpfs可以恢复,然后上层的PolarDB也可以恢复。

对底层盘做快照而不是对上层db做快照有一个问题,就是对盘做快照的时候,当时正在执行的IO,其是否真正落盘了是UB的。PolarDB管这种快照叫做disk outage consistency snapshot,在具体的实现上,如果做快照,PolarCtrl会通知PolarSwitch,在某个时间点的IO上打Tag,chunkserver收到对应的tag之后,说明这个tag时间的时间位点就是一个快照点。所以会先做快照,然后再处理打上tag的IO。这样,做快照的时间就和上层对应的某个事务的LSN联系起来了。

事务的数据可见性问题

一、MySQL/InnoDB通过Undo日志来实现事务的MVCC,由于只读节点跟读写节点属于不同的mysqld进程,读写节点在进行Undo日志Purge的时候并不会考虑此时在只读节点上是否还有事务要访问即将被删除的Undo Page,这就会导致记录旧版本被删除后,只读节点上事务读取到的数据是错误的。

针对该问题,PolarDB提供两种解决方式:

  • 所有ReadOnly定期向Primary汇报自己的最大能删除的Undo数据页,Primary节点统筹安排;
  • 当Primary节点删除Undo数据页时候,ReadOnly接收到日志后,判断即将被删除的Page是否还在被使用,如果在使用则等待,超过一个时间后还未有结束则直接给客户端报错。

二、还有个问题,由于InnoDB BP刷脏页有多种方式,其并不是严格按照oldest modification来的,这就会导致有些事务未提交的页已经写入共享存储,只读节点读到该页后需要通过Undo Page来重建可见的版本,但可能此时Undo Page还未刷盘,这就会出现只读上事务读取数据的另一种错误。

针对该问题,PolarDB解决方法是:

  • 限制读写节点刷脏页机制,如果脏页的redo还没有被只读节点回放,那么该页不能被刷回到存储上。这就确保只读节点读取到的数据,它之前的数据链是完整的,或者说只读节点已经知道其之前的所有redo日志。这样即使该数据的记录版本当前的事务不可见,也可以通过undo构造出来。即使undo对应的page是旧的,可以通过redo构造出所需的undo page。
  • replica需要缓存所有未刷盘的数据变更(即RedoLog),只有primary节点把脏页刷入盘后,replica缓存的日志才能被释放。这是因为,如果数据未刷盘,那么只读读到的数据就可能是旧的,需要通过redo来重建出来,参考第一点。另外,虽然buffer pool中可能已经缓存了未刷盘的page的数据,但该page可能会被LRU替换出去,当其再次载入所以只读节点必须缓存这些redo。

DDL问题

如果读写节点把一个表删了,反映到存储上就是把文件删了。对于mysqld进程来说,它会确保删除期间和删除后不再有事务访问该表。但是在只读节点上,可能此时还有事务在访问,PolarFS在完成文件系统元数据同步后,就会导致只读节点的事务访问存储出错。

PolarDB目前的解决办法是:如果主库对一个表进行了表结构变更操作(需要拷表),在操作返回成功前,必须通知到所有的ReadOnly节点(有一个最大的超时时间),告诉他们,这个表已经被删除了,后续的请求都失败。当然这种强同步操作会给性能带来极大的影响,有进一步的优化的空间。

Change Buffer问题

Change Buffer本质上是为了减少二级索引带来的IO开销而产生的一种特殊缓存机制。当对应的二级索引页没有被读入内存时,暂时缓存起来,当数据页后续被读进内存时,再进行应用,这个特性也带来的一些问题,该问题仅存在于StandBy中。例如Primary节点可能因为数据页还未读入内存,相应的操作还缓存在Change Buffer中,但是StandBy节点则因为不同的查询请求导致这个数据页已经读入内存,可以直接将二级索引修改合并到数据页上,无需经过Change Buffer了。但由于复制的是Primary节点的redo,且需要保证StandBy和Primary在存储层的一致性,所以StandBy节点还是会有Change Buffer的数据页和其对应的redo日志,如果该脏页回刷到存储上,就会导致数据不一致。

为了解决这个问题,PolarDB引入shadow page的概念,把未修改的数据页保存到其中,将Change Buffer记录合并到原来的数据页上,同时关闭该Mtr的redo,这样修改后的Page就不会放到Flush List上。也就是StandBy实例的存储层数据跟Primary节点保持一致。

Polar-X

ClickHouse

ClickHouse拥有多种表引擎类型,在这众多的表引擎中,MergeTree是比较有代表性的引擎之一,被广泛使用。

MergeTree采用列式存储,类似LSM Tree的架构组织数据。数据导入时被划分为多个Part,每个Part对应一个目录。Part中包含各个列的数据,每个列都有独立的文件。后台会调度合并任务,将多个小的Part合并成更大的Part,类似LSM Tree的合并过程。 Part中包含几类文件:

  • 数据文件(.bin),每一列的数据都分别存储在数据文件,一般以主键排序。数据文件中划分为若干个Block,Block是列存文件的压缩单元。每个Block又会包含若干个索引Granularity,用于索引定位。
  • 索引文件(.idx),索引文件又分为主键索引和二级索引:
  • MergeTree的主键索引与传统数据库的主键索引有所不同,MergeTree的主键索引只负责排序,但是不会去重。主键索引文件中,存储的是每一个Granularity中起始行的主键值,可以在扫描过程中过滤部分Granularity。
  • MergeTree的二级索引文件中可以存储Granularity的minmax、set、bloom_filter、ngrambf_v1等信息。
  • Mark文件(.mrk),由于索引文件是对Granularity进行索引,类似于逻辑索引。Mark文件记录Granularity在数据文件中的物理偏移,类似于将逻辑索引转换成物理索引。

MergeTree对于批量导入支持较好,对OLTP级事务更新仅有限支持。MergeTree存储引擎对数据实时可见要求非常高的场景是不太友好的。

TiFlash

AnalyticDB

ClickHouse

SqlServer

存储结构

Delta Tree,磁盘行列混存

增量 + 基线,磁盘行列混存

MergeTree,磁盘列存

Hekaton列存索引,内存行列混存

索引结构

主键索引

全列倒排索引

主键索引 + 二级索引

本身是行存的索引,可以利用行存的其他索引

数据更新方式

MVCC事务隔离,支持TP型事务和批量导入

MVCC事务隔离,支持TP型事务

批量导入友好,有限支持更新

与行存保持一致

数据压缩

通用压缩

字典压缩

通用压缩

RLE等专用压缩

clickhouse极致的列存、查询优化,但是并发查询性能不佳,不支持事务等等,相比其他竞品(hadoop、impala。。。)做到了极致的查询性能

设计目标

  • OLAP数据库,适用于大宽表,查询会扫描到大量行但是只用到了少数几列。使用列式存储,
  • 优化查询的吞吐(查询速度),要求海量数据能尽快处理完成。
  • 无需事务,数据一致性要求低(可以搭配一款事务型数据库,CH实时从事务库中同步数据)

ClickHouse存储引擎

  • 纯列式存储,然后压缩(有着十倍甚至更高的压缩比,节省存储空间,降低存储成本),
  • ClickHouse支持在建表时,指定将数据按照某些列进行sort by。排序后,保证了相同sort key的数据在磁盘上连续存储,且有序摆放。在进行等值、范围查询时,where条件命中的数据都紧密存储在一个或若干个连续的Block中,而不是分散的存储在任意多个Block, 大幅减少需要IO的block数量。另外,连续IO也能够充分利用操作系统page cache的预取能力,减少page fault。
  • ClickHouse支持主键索引,它将每列数据按照index granularity(默认8192行)进行划分,每个index granularity的开头第一行被称为一个mark行。主键索引存储该mark行对应的primary key的值。对于where条件中含有primary key的查询,通过对主键索引进行二分查找,能够直接定位到对应的index granularity,避免了全表扫描从而加速查询。但是值得注意的是:ClickHouse的主键索引与MySQL等数据库不同,它并不用于去重,即便primary key相同的行,也可以同时存在于数据库中。要想实现去重效果,需要结合具体的表引擎ReplacingMergeTree、CollapsingMergeTree、VersionedCollapsingMergeTree实现。
  • 稀疏索引,ClickHouse支持对任意列创建任意数量的稀疏索引。其中被索引的value可以是任意的合法SQL Expression,并不仅仅局限于对column value本身进行索引。之所以叫稀疏索引,是因为它本质上是对一个完整index granularity(默认8192行)的统计信息,并不会具体记录每一行在文件中的位置。目前支持的稀疏索引类型包括:
  • minmax: 以index granularity为单位,存储指定表达式计算后的min、max值;在等值和范围查询中能够帮助快速跳过不满足要求的块,减少IO。
  • set(max_rows):以index granularity为单位,存储指定表达式的distinct value集合,用于快速判断等值查询是否命中该块,减少IO。
  • ngrambf_v1(n, size_of_bloom_filter_in_bytes, number_of_hash_functions, random_seed):将string进行ngram分词后,构建bloom filter,能够优化等值、like、in等查询条件。
  • tokenbf_v1(size_of_bloom_filter_in_bytes, number_of_hash_functions, random_seed): 与ngrambf_v1类似,区别是不使用ngram进行分词,而是通过标点符号进行词语分割。
  • bloom_filter([false_positive]):对指定列构建bloom filter,用于加速等值、like、in等查询条件的执行。
  • ClickHouse支持单机模式,也支持分布式集群模式。在分布式模式下,ClickHouse会将数据分为多个分片,并且分布到不同节点上。不同的分片策略在应对不同的SQL Pattern时,各有优势。
  • 1)random随机分片:写入数据会被随机分发到分布式集群中的某个节点上。
  • 2)constant固定分片:写入数据会被分发到固定一个节点上。
  • 3)column value分片:按照某一列的值进行hash分片。
  • 4)自定义表达式分片:指定任意合法表达式,根据表达式被计算后的值进行hash分片。
  • 用户根据自身业务特点选择合适的数据分片策略,可以有优化数据倾斜、避免shuffle直接本地oin等优点
  • ClickHouse采用类LSM Tree的结构,数据写入后定期在后台Compaction。通过类LSM tree的结构,ClickHouse在数据导入时全部是顺序append写,写入后数据段不可更改,在后台compaction时也是多个段merge sort后顺序写回磁盘。顺序写的特性,充分利用了磁盘的吞吐能力,即便在HDD上也有着优异的写入性能。
  • 在分析场景中,删除、更新操作并不是核心需求。ClickHouse没有直接支持delete、update操作,而是变相支持了mutation操作,语法为alter table delete where filter_expr,alter table update col=val where filter_expr。目前主要限制为删除、更新操作为异步操作,需要后台compation之后才能生效
  • ClickHouse支持PARTITION BY子句,在建表时可以指定按照任意合法表达式进行数据分区操作,比如通过toYYYYMM()将数据按月进行分区、toMonday()将数据按照周几进行分区、对Enum类型的列直接每种取值作为一个分区等。

总结来说,极致压缩,稀疏索引、数据分片

计算引擎

ClickHouse在计算层做了非常细致的工作,竭尽所能榨干硬件能力,提升查询速度。它实现了单机多核并行、分布式计算、向量化执行与SIMD指令、代码生成等多种重要技术。

  1. 多核并行:ClickHouse将数据划分为多个partition,每个partition再进一步划分为多个index granularity,然后通过多个CPU核心分别处理其中的一部分来实现并行数据处理。在这种设计下,单条Query就能利用整机所有CPU。极致的并行处理能力,极大的降低了查询延时。
  2. 分布式计算:除了优秀的单机并行处理能力,ClickHouse还提供了可线性拓展的分布式计算能力。ClickHouse会自动将查询拆解为多个task下发到集群中,然后进行多机并行处理,最后把结果汇聚到一起。在存在多副本的情况下,ClickHouse提供了多种query下发策略:
  1. 随机下发:在多个replica中随机选择一个;
  2. 最近hostname原则:选择与当前下发机器最相近的hostname节点,进行query下发。在特定的网络拓扑下,可以降低网络延时。而且能够确保query下发到固定的replica机器,充分利用系统cache。
  3. in order:按照特定顺序逐个尝试下发,当前一个replica不可用时,顺延到下一个replica。
  4. first or random:在In Order模式下,当第一个replica不可用时,所有workload都会积压到第二个Replica,导致负载不均衡。first or random解决了这个问题:当第一个replica不可用时,随机选择一个其他replica,从而保证其余replica间负载均衡。另外在跨region复制场景下,通过设置第一个replica为本region内的副本,可以显著降低网络延时。
  1. 向量化执行与SIMD:ClickHouse不仅将数据按列存储,而且按列进行计算。传统OLTP数据库通常采用按行计算,原因是事务处理中以点查为主,SQL计算量小,实现这些技术的收益不够明显。但是在分析场景下,单个SQL所涉及计算量可能极大,将每行作为一个基本单元进行处理会带来严重的性能损耗:
  1. 对每一行数据都要调用相应的函数,函数调用开销占比高;
  2. 存储层按列存储数据,在内存中也按列组织,但是计算层按行处理,无法充分利用CPU cache的预读能力,造成CPU Cache miss严重;
  3. 按行处理,无法利用高效的SIMD指令;
  4. ClickHouse实现了向量执行引擎Vectorized execution engine),对内存中的列式数据,一个batch调用一次SIMD指令(而非每一行调用一次),不仅减少了函数调用次数、降低了cache miss,而且可以充分发挥SIMD指令的并行能力,大幅缩短了计算耗时。向量执行引擎,通常能够带来数倍的性能提升。
  1. 动态代码生成Runtime Codegen
  1. 在经典的数据库实现中,通常对表达式计算采用火山模型,也即将查询转换成一个个operator,比如HashJoin、Scan、IndexScan、Aggregation等。为了连接不同算子,operator之间采用统一的接口,比如open/next/close。在每个算子内部都实现了父类的这些虚函数,在分析场景中单条SQL要处理数据通常高达数亿行,虚函数的调用开销不再可以忽略不计。另外,在每个算子内部都要考虑多种变量,比如列类型、列的size、列的个数等,存在着大量的if-else分支判断导致CPU分支预测失效。
  2. ClickHouse实现了Expression级别的runtime codegen,动态地根据当前SQL直接生成代码,然后编译执行。如下图例子所示,对于Expression直接生成代码,不仅消除了大量的虚函数调用(即图中多个function pointer的调用),而且由于在运行时表达式的参数类型、个数等都是已知的,也消除了不必要的if-else分支判断。
  1. 近似计算:ClickHouse实现了多种近似计算功能:
  1. 近似估算distinct values、中位数,分位数等多种聚合函数;
  2. 建表DDL支持SAMPLE BY子句,支持对于数据进行抽样处理;

ClickHouse总结

近年来ClickHouse发展趋势迅猛,社区和大厂都纷纷跟进使用。本文尝试从OLAP场景的需求出发,介绍了ClickHouse存储层、计算层的主要设计。ClickHouse实现了大多数当前主流的数据分析技术,具有明显的技术优势:

  • 提供了极致的查询性能:开源公开benchmark显示比传统方法快1001000倍,提供50MB200MB/s的高吞吐实时导入能力)
  • 以极低的成本存储海量数据: 借助于精心设计的列存、高效的数据压缩算法,提供高达10倍的压缩比,大幅提升单机数据存储和计算能力,大幅降低使用成本,是构建海量数据仓库的绝佳方案。
  • 简单灵活又不失强大:提供完善SQL支持,上手十分简单;提供json、map、array等灵活数据类型适配业务快速变化;同时支持近似计算、概率数据结构等应对海量数据处理。

相比于开源社区的其他几项分析型技术,如Druid、Presto、Impala、Kylin、ElasticSearch等,ClickHouse更是一整套完善的解决方案,它自包含了存储和计算能力(无需额外依赖其他存储组件),完全自主实现了高可用,而且支持完整的SQL语法包括JOIN等,技术上有着明显优势。相比于hadoop体系,以数据库的方式来做大数据处理更加简单易用,学习成本低且灵活度高。当前社区仍旧在迅猛发展中,相信后续会有越来越多好用的功能出现。

TiDB

shared-nothing,raft,很多mysql实现的功能还没实现。底层KV存储,TiBD主要负责和client对接,然后做优化,很多执行计划会下推到TiKV

我在想TiDB还是有很多问题的,首先TiDB的底座不是云原生的基础组件(类比snowflake polarDB ADB),很多问题上云之后就没法解决了

TiDB目前有两种存储节点,分别是 TiKV 和 TiFlash。TiKV 采用了行式存储,更适合 TP 类型的业务;而 TiFlash 采用列式存储,擅长 AP 类型的业务。TiFlash 通过 raft 协议从 TiKV 节点实时同步数据,拥有毫秒级别的延迟,以及非常优秀的数据分析性能。它支持实时同步 TiKV 的数据更新,以及支持在线 DDL。我们把 TiFlash 作为 Raft Learner 融合进 TiDB 的 raft 体系,将两种节点整合在一个数据库集群中,上层统一通过 TiDB 节点查询,使得 TiDB 成为一款真正的 HTAP 数据库。

TiFlash

TiFlash的列式存储引擎Delta Tree参考了B+ TreeLSM Tree的设计思想。

  • Delta Tree将数据按照主键划分为Range分区,每个分区称为Segment。
  • Segment通过B+ Tree作为索引。也就是说,B+ Tree索引的叶子节点为Segment。
  • 在Segment内部采用类似LSM Tree的分层存储方式,不过采用固定两层的LSM Tree,分别为Delta层和Stable层。
  • Delta层保存增量数据部分,其中,新写入的数据写入Delta Cache中,与LSM Tree的MemTable类似。当Delta Cache写满后,其中的数据刷入Delta层的Pack中,类似LSM Tree的L0层。
  • Stable层类似于LSM Tree的L1层,其中的数据以主键和版本号排序。
  • Delta层的Pack和Stable层需要做全量合并,得到新的Stable层数据。
  • 当Segment中的数据量超过阈值,就会做类似B+ Tree叶子节点的分裂操作,分裂成两个Segment。同时,如果相邻的Segment中的数据量都比较小,也会将相邻的Segment合并成一个Segment。

C-Store(2005)/Vertica

大多数DBMS都是为写优化,C-store是第一个为读优化的OLTP数据库。C-Store 的主要贡献有以下几点:通过精心设计的 projection 同时实现列数据的多副本和多种索引方式;用读写分层的方式兼顾了(少量)写入的性能。此外,C-Store 可能是第一个现代的列式存储数据库实现,其的设计启发了无数后来的商业或开源数据库,就比如 Vertica。

在 C-Store 内部,逻辑表被纵向拆分成 projections每个 projection 可以包含一个或多个列,甚至可以包含来自其他逻辑表的列(构成索引)。当然,每个列至少会存在于一个 projections 上。

Projection 内是以列式存储的:里面的每个列分别用一个数据结构存放。为了避免列太长引起问题,也支持每个 projection 以 sort key 的值做横向切分。

Projection 是有冗余性的,常常 1 个列会出现在多个 projection 中,但是它们的顺序也就是 sort key 并不相同,因此 C-Store 在查询时可以选用最优的一组 projections,使得查询执行的代价最小。

Apache ORC

Apache ORC 最初是为支持 Hive 上的 OLAP 查询开发的一种文件格式,如今在 Hadoop 生态系统中有广泛的应用。ORC 支持各种格式的字段,包括常见的 int、string 等,也包括 struct、list、map 等组合字段;字段的 meta 信息就放在 ORC 文件的尾部(这被称为自描述的)。

ORC 里的 Stripe 就像传统数据库的页,它是 ORC 文件批量读写的基本单位。这是由于分布式储存系统的读写延迟较大,一次 IO 操作只有批量读取一定量的数据才划算。这和按页读写磁盘的思路也有共通之处。

Apache ORC 提供有限的 ACID 事务支持。受限于分布式文件系统的特点,文件不能随机写,那如何把修改保存下来呢?

类似于 LSM-Tree 中的 MVCC 那样,writer 并不是直接修改数据,而是为每个事务生成一个 delta 文件,文件中的修改被叠加在原始数据之上。当 delta 文件越来越多时,通过 minor compaction 把连续多个 delta 文件合成一个;当 delta 变得很大时,再执行 major compaction 将 delta 和原始数据合并。这种保持基线数据不变、分层叠加 delta 数据的优化方式在列式存储系统中十分常见,是一种通用的解决思路

Dremel (2010) / Apache Parquet

Dremel 是 Google 研发的用于大规模只读数据的查询系统,用于进行快速的 ad-hoc 查询,弥补 MapReduce 交互式查询能力的不足。为了避免对数据的二次拷贝,Dremel 的数据就放在原处,通常是 GFS 这样的分布式文件系统,为此需要设计一种通用的文件格式。

Dremel 的系统设计和大多 OLAP 的列式数据库并无太多创新点,但是其精巧的存储格式却变得流行起来,Apache Parquet 就是它的开源复刻版。注意 Parquet 和 ORC 一样都是一种存储格式,而非完整的系统。

Impala

Impala是Cloudera公司主导开发的新型查询系统,它提供SQL语义,能查询存储在Hadoop的HDFS和HBase中的PB级大数据。已有的Hive系统虽然也提供了SQL语义,但由于Hive底层执行使用的是MapReduce引擎,仍然是一个批处理过程,难以满足查询的交互性。相比之下,Impala的最大特点也是最大卖点就是它的快速。Impala完全抛弃了MapReduce这个不太适合做SQL查询的范式,而是像Dremel一样借鉴了MPP并行数据库的思想另起炉灶,因此可做更多的查询优化,从而省掉不必要的shuffle、sort等开销。

Impala与Hive类似不是数据库而是数据分析工具,集群有以下几类节点

  1. Impalad,Impala的核⼼组件,负责读写数据,执行查询任务,并将结果返回协调者
  1. Impalad服务由三个模块组成:Query Planner、Query Coordinator和Query Executor,前两个模块组成前端,负责接收SQL查询请求,解析SQL并转换成执⾏计划,交由后端执⾏。
  1. statestored,负责监控集群中Impalad的健康状况,并将集群健康信息同步给Impalad。
  2. catalogd,Impala执⾏的SQL语句引发元数据发⽣变化时,catalog服务负责把这些元数据的变化同步给其它Impalad进程

查询流程

  1. Client提交任务
  • Client发送⼀个SQL查询请求到任意⼀个Impalad节点,会返回⼀个queryId⽤于之后的客户端操作。
  1. 生成查询计划(单机计划、分布式执行计划)
  • SQL提交到Impalad节点之后,Analyser依次执⾏SQL的词法分析、语法分析、语义分析等操作;从MySQL元数据库中获取元数据,从HDFS的名称节点中获取数据地址,以得到存储这个查询相关数据的所有数据节点。
  • 单机执行计划:根据上⼀步对SQL语句的分析,由Planner先⽣成单机的执⾏计划,该执⾏计划是有PlanNode组成的⼀棵树,这个过程中也会执⾏⼀些SQL化,例如Join顺序改变、谓词下推等。
  • 分布式并⾏物理计划:将单机执⾏计划转换成分布式并⾏物理执⾏计划,物理执⾏计划由⼀个个的Fragment组成,Fragment之间有数据依赖关系,处理过程中要在原有的执⾏计划之上加⼊⼀些ExchangeNode和DataStreamSink信息等。
  • Fragment : sql⽣成的分布式执⾏计划的⼀个⼦任务;
  • DataStreamSink:传输当前的Fragment输出数据到不同的节点;
  1. 任务调度和分发
  • Coordinator将Fragment(⼦任务)根据数据分区信息发配到不同的Impalad节点上执⾏。Impalad节点接收到执⾏Fragment请求交由Executor执⾏。
  1. Fragment之间的数据依赖
  • 每⼀个Fragment的执⾏输出通过DataStreamSink发送到下⼀个Fragment,Fragment运⾏过程中不断向coordinator节点汇报当前运⾏状态。
  1. 结果汇总
  • 查询的SQL通常情况下需要有⼀个单独的Fragment⽤于结果的汇总,它只在Coordinator节点运⾏,将多个节点的最终执⾏结果汇总,转换成ResultSet信息。
  1. 获取结果
  • 客户端调⽤获取ResultSet的接⼝,读取查询结果。

Druid

Druid可以对多列数据构建倒排索引(bitmap-based inverted indexes)

Pinot

mongoDB

参考链接

  1. 数据库内核杂谈(九):开源优化器 ORCA
  2. Greenplum :基于 PostgreSQL 的分布式数据库内核揭秘 (上篇)
  3. Greenplum:基于 PostgreSQL 的分布式数据库内核揭秘 (下篇)
  4. Exploring Postgres with Bruce Momjian
  5. 时序数据库连载系列: 时序数据库一哥InfluxDB之存储机制解析
  6. boltdb 1.3.0实现分析(一)
  7. 处理海量数据:列式存储综述(系统篇)
  8. Greenplum数据库文档
  9. postgreSQL中文社区
  10. Postgres Internals Presentations
  11. postgresql optimizer/README
  12. Hadoop 怎么了,大数据路在何方
  13. 如何理解 大数据、数据仓库领域的ad-hoc这个词?即席查询
  14. 海量实时计算(Ad-Hoc Query) 概览
  15. 前沿 | VLDB论文解读:阿里云超大规模实时分析型数据库AnalyticDB
  16. 面向云数据库,超低延迟文件系统PolarFS诞生了
  17. 阿里云PolarDB及其存储PolarFS技术实现分析(上)
  18. 阿里云PolarDB及其存储PolarFS技术实现分析(下)
  19. POLARDB(DRDS)、AnalyticDB、OceanBase核心技术实现八股背诵
  20. 重识 SQLite,简约不简单
  21. impala 概述
  22. Impala架构和查询原理
  23. Druid概述超多文章
  24. Kylin、Druid、ClickHouse核心技术对比 | 知识转载
  25. DataBase · 引擎特性 · OLAP/HTAP列式存储引擎概述要看
  26. 数据库干货汇