1 MergeTree存储
1.1 MergeTree思想
列存文件在按块做压缩时,排序键中的列值是连续或者重复的,使得列存块的数据压缩可以获得极致的压缩比。
存储有序性本身就是一种可以加速查询的索引结构,根据排序键中列的等值条件或者range条件我们可以快速找到目标行所在的近似位置区间(下文会展开详细介绍),而且这种索引结构是不会产生额外存储开销的。
合并树(MergeTree)是存储引擎的族,通过主键来支持索引. 主键可以是列或表达式的任意 tuple。在MergeTree表中的数据被存储在 “parts” 中. 每一部分按照主键顺序存储数据 (数据通过主键 tuple 来排序). 所有的表的列都在各自的column.bin文件中保存。此文件由压缩的数据块组成。每个数据块大小从64 KB 到 1 MB,依赖于平均值的大小。数据块由列值组成,按顺序连续放置。对于每一列,列值在同一个顺序上 (顺序通过主键来定义), 因此,对于对应的列,当你通过多列迭代以后来获得值。
主键自身是"稀疏的"。它不定位到每个行 ,但是仅是一些数据范围。对于每个N-th行, 一个单独的primary.idx 文件有主键的值, N 被称为 index_granularity(通常情况下, N = 8192). 对于每个列, 我们有column.mrk 文件 ,带有 “marks”标签,对于数据文件中的每个N-th行,它是一个偏移量 。每个标签都成成对儿出现的:文件中的偏移量到压缩数据块的起始端,解压缩数据块的偏移量到数据的起始端。通常情况下,压缩的数据块通过"marks"标签来对齐,解压缩的数据块的偏移量是0。对于primary.idx的数据通常主流在存储中,对于column.mrk文件的数据放在缓存中。
当我们从MergeTree引擎中读取数据时,我们看到了 primary.idx 数据和定位了可能包含请求数据的范围, 然后进一步看column.mrk 数据,和计算偏移量从哪开始读取这些范围。因为稀疏性, 超额的数据可能被读取。ClickHouse 并不适合高负载的点状查询,因为带有索引粒度行的整个范围必须被读取, 整个压缩数据块必须被解压缩。我们构建的结构是索引稀疏的,因为我们必须在单台服务器上维护数万亿条数据, 对于索引来说没有显著的内存消耗。因为主键是稀疏的,它并不是唯一的:在 INSERT时,它不能够检查键的存在。在一个表内,相同的键你可以有多个行。
当你插入大量数据进入MergeTree时,数据通过主键顺序来筛选,形成一个新的部分。为了保持数据块数是低位的,有一些背景线程周期性地查询这些数据块,将他们合并到一个排序好的数据块。
这就是为什么称为MergeTree。当然,合并意味着"写入净化"。所有的部分都是非修改的:他们仅创建和删除,但是不会更新。当SELECT运行时,它将获得一个表的快照。在合并之后,我们也保持旧的部分用于故障数据恢复,所以如果我们某些合并部分的文件损坏了,我们能够根据原来的部分进行替换。
MergeTree 不是一个LSM 树,因为它不包含 “memtable” 和 “log”: 插入的数据直接写入到文件系统。这个仅适合于批量的INSERT操作,并不是每行写入,同时不能过于频繁 – 每秒一次写入是 OK 的,每秒几千次写入是不可以的。我们使用这种方式是为了简化,因为在生产环境中,我们主要以批量插入数据为主。
1.3 MergeTree存储结构
为了方便大家理解表的存储结构,下面列举了测试表DDL,我们将从这个表入手来分析MergeTree存储的内核设计。从DDL的PARTITION BY申明中我们可以看出用户按每个区每小时粒度创建了数据分区,而每个数据分区内部的数据又是按照(action_id, scene_id, time_ts, level, uid)作为排序键进行有序存储。
CREATE TABLE user_action_log (`time` DateTime DEFAULT CAST('2020-05-01 08:00:00', 'DateTime') COMMENT '日志时间',`action_id` UInt16 DEFAULT CAST(0, 'UInt16') COMMENT '日志行为类型id',`action_name` String DEFAULT '' COMMENT '日志行为类型名',`region_name` String DEFAULT '' COMMENT '区服名称',`uid` UInt64 DEFAULT CAST(0, 'UInt64') COMMENT '用户id',`level` UInt32 DEFAULT CAST(0, 'UInt32') COMMENT '当前等级',`trans_no` String DEFAULT '' COMMENT '事务流水号',`ext_head` String DEFAULT '' COMMENT '扩展日志head',`avatar_id` UInt32 DEFAULT CAST(0, 'UInt32') COMMENT '角色id',`scene_id` UInt32 DEFAULT CAST(0, 'UInt32') COMMENT '场景id',`time_ts` UInt64 DEFAULT CAST(0, 'UInt64') COMMENT '秒单位时间戳',index avatar_id_minmax (avatar_id) type minmax granularity 3) ENGINE = MergeTree()PARTITION BY (toYYYYMMDD(time), toHour(time), region_name)ORDER BY (action_id, scene_id, time_ts, level, uid)PRIMARY KEY (action_id, scene_id, time_ts, level);
该表的MergeTree存储结构逻辑示意图如下:
MergeTree表的存储结构中,每个数据分区相互独立,逻辑上没有关联。单个数据分区内部存在着多个MergeTree Data Part。这些Data Part一旦生成就是Immutable的状态,Data Part的生成和销毁主要与写入和异步Merge有关。MergeTree表的写入链路是一个极端的batch load过程,Data Part不支持单条的append insert。每次batch insert都会生成一个新的MergeTree Data Part。如果用户单次insert一条记录,那就会为那一条记录生成一个独立的Data Part,这必然是无法接受的。一般我们使用MergeTree表引擎的时候,需要在客户端做聚合进行batch写入或者在MergeTree表的基础上创建Distributed表来代理MergeTree表的写入和查询,Distributed表默认会缓存用户的写入数据,超过一定时间或者数据量再异步转发给MergeTree表。MergeTree存储引擎对数据实时可见要求非常高的场景是不太友好的。

Granule是数据按行划分时用到的逻辑概念。关于多少行是一个Granule这个问题,在老版本中这是用参数index_granularity设定的一个常量,也就是每隔确定行就是一个Granule。在当前版本中有另一个参数index_granularity_bytes会影响Granule的行数,它的意义是让每个Granule中所有列的sum size尽量不要超过设定值。老版本中的定长Granule设定主要的问题是MergeTree中的数据是按Granule粒度进行索引的,这种粗糙的索引粒度在分析超级大宽表的场景中,从存储读取的data size会膨胀得非常厉害,需要用户非常谨慎得设定参数。 Block是列存文件中的压缩单元。每个列存文件的Block都会包含若干个Granule,具体多少个Granule是由参数min_compress_block_size控制,每次列的Block中写完一个Granule的数据时,它会检查当前Block Size有没有达到设定值,如果达到则会把当前Block进行压缩然后写磁盘。 从以上两点可以看出MergeTree的Block既不是定data size也不是定行数的,Granule也不是一个定长的逻辑概念。所以我们需要额外信息快速找到某一个Granule。这就是Mark标识文件的作用,它记录了每个Granule的行数,以及它所在的Block在列存压缩文件中的偏移,同时还有Granule在解压后的Block中的偏移位置。
2 MergeTree查询
2.1 索引检索
/// Whether the condition is feasible in the key range./// left_key and right_key must contain all fields in the sort_descr in the appropriate order./// data_types - the types of the key columns.bool mayBeTrueInRange(size_t used_key_size, const Field * left_key, const Field * right_key, const DataTypes & data_types) const;/// Whether the condition is feasible in the direct product of single column ranges specified by `parallelogram`.bool mayBeTrueInParallelogram(const std::vector<Range> & parallelogram, const DataTypes & data_types) const;/// Is the condition valid in a semi-infinite (not limited to the right) key range./// left_key must contain all the fields in the sort_descr in the appropriate order.bool mayBeTrueAfter(size_t used_key_size, const Field * left_key, const DataTypes & data_types) const;
2.2 数据Sampling
2.3 数据扫描
MergeTree的数据扫描部分提供了三种不同的模式:
Final模式:该模式对CollapsingMergeTree、SummingMergeTree等表引擎提供一个最终Merge后的数据视图。前文已经提到过MergeTree基础上的高级MergeTree表引擎都是对MergeTree Data Part采用了特定的Merge逻辑。它带来的问题是由于MergeTree Data Part是异步Merge的过程,在没有最终Merge成一个Data Part的情况下,用户无法看到最终的数据结果。所以ClickHouse在查询是提供了一个final模式,它会在各个Data Part的多条BlockInputStream基础上套上一些高级的Merge Stream,例如DistinctSortedBlockInputStream、SummingSortedBlockInputStream等,这部分逻辑和异步Merge时的逻辑保持一致,这样用户就可以提前看到“最终”的数据结果了。
Sorted模式:sort模式可以认为是一种order by下推存储的查询加速优化手段。因为每个MergeTree Data Part内部的数据是有序的,所以当用户查询中包括排序键order by条件时只需要在各个Data Part的BlockInputStream上套一个做数据有序归并的InputStream就可以实现全局有序的能力。
Normal模式:这是基础MergeTree表最常用的数据扫描模式,多个Data Part之间进行并行数据扫描,对于单查询可以达到非常高吞吐的数据读取。
接下来展开介绍下Normal模式中几个关键的性能优化点:
并行扫描:传统的计算引擎在数据扫描部分的并发度大多和存储文件数绑定在一起,所以MergeTree Data Part并行扫描是一个基础能力。但是MergeTree的存储结构要求数据不断mege,最终合并成一个Data Part,这样对索引和数据压缩才是最高效的。所以ClickHouse在MergeTree Data Part并行的基础上还增加了Mark Range并行。用户可以任意设定数据扫描过程中的并行度,每个扫描线程分配到的是Mark Range In Data Part粒度的任务,同时多个扫描线程之间还共享了Mark Range Task Pool,这样可以避免在存储扫描中的长尾问题。
数据Cache:MergeTree的查询链路中涉及到的数据有不同级别的缓存设计。主键索引和分区键索引在load Data Part的过程中被加载到内存,Mark文件和列存文件有对应的MarkCache和UncompressedCache,MarkCache直接缓存了Mark文件中的binary内容,而UncompressedCache中缓存的是解压后的Block数据。
SIMD反序列化:部分列类型的反序列化过程中采用了手写的sse指令加速,在数据命中UncompressedCache的情况下会有一些效果。
PreWhere过滤:ClickHouse的语法支持了额外的PreWhere过滤条件,它会先于Where条件进行判断。当用户在sql的filter条件中加上PreWhere过滤条件时,存储扫描会分两阶段进行,先读取PreWhere条件中依赖的列值,然后计算每一行是否符合条件。相当于在Mark Range的基础上进一步缩小扫描范围,PreWhere列扫描计算过后,ClickHouse会调整每个Mark对应的Granule中具体要扫描的行数,相当于可以丢弃Granule头尾的一部分行。
文章来源地址:
阿里云> 开发者社区> ClickHouse 技术>
https://developer.aliyun.com/article/762092?spm=a2c6h.13148508.0.0.32e54f0eW8HM3f
上一篇:ClickHouse (MATERIALIZED) VIEW
近期推荐:
更多精彩内容欢迎关注微信公众号




