写点什么

Clickhouse Projection 特性探索

发布于: 15 小时前
Clickhouse Projection 特性探索

年初的 clickhouse meetup 上快手团队分享了 clickhouse projection 在其公司内部的实践。分享包括了 projection 原理、使用、性能测试等内容。从性能测试的数据上看,projeciton 对查询性能有着百倍级别的提升,意味着之前分钟级的查询响应延迟,将会提升到秒级响应。秒级的查询响应延迟,将会提升到毫秒级的响应,对于使用者将会有更加完美的体验。


看完了快手同学的 clickhouse projeciton 的分享,在我脑中也产生了几个问题?

  1. 没有 projection 功能之前,clickhouse 还存在什么问题?

  2. clickhouse projection 如何解决的问题?

  3. clickhouse projection 适用于哪些场景?

  4. clickhouse proejction 有什么要注意的吗?

没有 projection 功能之前,clickhouse 还存在什么问题?

clickhosue 作为一款 olap 引擎,处于数据平台中的最顶层,直接对接平台用户。查询性能的好坏,直接决定着用户的使用体验。

  1. clickhouse 的查询性能虽然已经非常完美,但是面对超大数据量的场景还是会存在一定的问题,原因是 clickhouse 是基于内存计算的 MPP 架构分析型数据库,与 Spark, Hive, MR 等计算框架不同,计算 过程中的临时数据没有磁盘选项。查询过程中,数据会加载到内存中。如果内存配置不够,将会导致查询失败,对 clickhouse 集群的稳定也会有一定的影响。

  2. 用户在数据查询的场景中,会有着一定的使用习惯。比如,每天定时都会查看一些特定的图表。这些图表中包含全量的数据统计,复杂的数据查询逻辑等。这些查询相较于其他查询,可以归属于异常查询。这些查询可能因为内存问题导致查询失败,也可能因为复杂的计算逻辑导致查询时间过长,影响平台上其他用户的查询。

clickhouse projection 如何解决的问题?

在 OLAP 领域中,根据数据模型主要分为 ROLAP(Relational OLAP) 关系 OLAP,MOLAP(Multidimension OLAP) 多维 OLAP 两种。ROLAP 将数据表达为二维关系模型,类似关系型数据库模型,数据表达能力较好,对外提供 SQL 接口。MOLAP 将 OLAP 分析所用到的多维数据物理上存储为多维数组的形式,形成“立方体”的结构。维的属性值被映射成多维数组的下标值或下标的范围,而汇总数据作为多维数组的值存储在数组的单元中,采用预聚合的思想,加速数据查询,但是数据模型不够灵活。


clickhouse 作为 ROLAP 典型代表之一,纯列式存储单表查询性能几乎没有对手。projection 名字起源于 vertica,相当于传统意义上的物化视图。它借鉴 MOLAP 预聚合的思想,在数据写入的时候,根据 projection 定义的表达式,计算写入数据的聚合数据同原始数据一并写入。数据查询的过程中,如果查询 SQL 通过分析可以通过聚合数据得出,直接查询聚合数据减少计算的开销,解决了由于数据量导致的内存问题。


projeciton 底层存储上属于 part 目录下数据的扩充,可以理解为查询索引的一种形式。


从数据写入逻辑的核心代码上看(clickhouse version 21.7),多个 projection 在 part 目录下以多个子目录存储,projection 目录下存储基于原始数据聚合的数据。所以,projection 写入与原始数据写入同步,只有创建 projection 之后写入的数据才会被物化,保证数据的一致性。

MergeTreeDataWrite.cpp.390
如果存在projection配置,将projection part添加new_data_part中。if (metadata_snapshot->hasProjections()){ for (const auto & projection : metadata_snapshot->getProjections()) { /// 1. 获取projection query的执行计划。 /// 2. 当前Block作为输入,计算聚合结果 /// 3. 获取数据流 auto in = InterpreterSelectQuery( projection.query_ast, context, Pipe(std::make_shared<SourceFromSingleChunk>(block, Chunk(block.getColumns(), block.rows()))), SelectQueryOptions{ projection.type == ProjectionDescription::Type::Normal ? QueryProcessingStage::FetchColumns : QueryProcessingStage::WithMergeableState}) .execute() .getInputStream(); in = std::make_shared<SquashingBlockInputStream>(in, block.rows(), std::numeric_limits<UInt64>::max()); in->readPrefix(); // 4. 读取prjeciton计算的数据块 auto projection_block = in->read(); if (in->read()) throw Exception("Projection cannot grow block rows", ErrorCodes::LOGICAL_ERROR); in->readSuffix(); if (projection_block.rows()) { // 5. 将聚合的数据(.proj)添加到new_data_part中 new_data_part->addProjectionPart(projection.name, writeProjectionPart(projection_block, projection, new_data_part.get())); } }}
复制代码

从文件系统目录上看,p2.proj 为 data part 下 p2 projection 的数据目录,目录下聚合列,聚合函数作为单独的列存文件存储。


├── dim1.bin ├── dim1.mrk2 ├── dim2.bin ├── dim2.mrk2 ├── dim3.bin ├── dim3.mrk2 ├── event_key.bin ├── event_key.mrk2 ├── event_time.bin ├── event_time.mrk2 ├── p2.proj │ ├── checksums.txt │ ├── columns.txt │ ├── count%28%29.bin │ ├── count%28%29.mrk2 │ ├── count.txt │ ├── default_compression_codec.txt │ ├── dim3.bin │ ├── dim3.mrk2 │ ├── groupBitmap%28user%29.bin │ ├── groupBitmap%28user%29.mrk2 │ └── primary.idx
复制代码

clickhouse projection 适用于哪些场景?

为了探索 projection 适用于哪些场景,准备了典型的用户行为数据集,数据量为 1 亿条, 数据模型选择事件模型,模型中包含了用户做过什么事件,以及事件对应的维度。

维度选择上,dim1,dim2 为普通维度值,维度值种类有 10 种。dim3 为高基维维度,维度值种类有 100000 种。


如何为数据表构建 projection?

  1. 建表的时候指定多个 projection 定义,projection 中为基本的 select 语句,可以省略 from table 子句,默认与源表保持一致。

CREATE TABLE event_projection1 (     `event_key` String,     `user` UInt32,     `event_time` DateTime64(3, 'Asia/Shanghai'),     `dim1` String,     `dim2` String,     `dim3` String,     PROJECTION p1     (         SELECT             groupBitmap(user),             count(1)         GROUP BY dim1     ) ) ENGINE = MergeTree() ORDER BY (event_key, user, event_time) 
复制代码

2. alter table 语句补充 projection 定义

ALTER TABLE event_projection1     ADD PROJECTION p2     (         SELECT             count(1),             groupBitmap(user)         GROUP BY dim1, dim3     ) 
复制代码


怎么查询才能命中 projection?

  1. select 表达式必须为 projection 定义中 select 表达式的子集。

  2. group by clause 必须为 projection 定义中 group by clause 的子集。

  3. where clause key 必须为 projeciton 定义中的 group by column 的子集。


如何知道是否命中了 projection?

  1. explain 查看执行计划,ReadFromStorage (MergeTree(with projection)) 表示命中 projection

EXPLAIN SQL expain actions=1 select dim, count(1) from event_projection group by dim1   执行计划: Expression ((Projection + Before ORDER BY))                               Actions: INPUT :: 0 -> dim1 String : 0                                              INPUT :: 1 -> count() UInt64 : 1                                  Positions: 0 1                                                               SettingQuotaAndLimits (Set limits and quota after reading from storage)      ReadFromStorage (MergeTree(with projection)) 
复制代码

2. clickouse 查询关键日志

查询命中了projection p (SelectExecutor): Choose aggregate projection p (SelectExecutor): projection required columns: dim1, count() (SelectExecutor): Reading approx. 63 rows with 4 streams 
复制代码


查询效果如何?


  1. 命中 projection 相比没有命中 projection 对于查询性能的提升非常明显。

  2. 构建 projection 对于存储,数据插入有一定的额外开销。

  3. 如果构建 projection 的时候混入了高基维度,查询耗时相比没有混入高基维度,查询性能同比降低了近 200 倍,存储与插入时间也付出了更多的额外开销。



  1. 相同的条件下 groupBitmap 没有 count 聚合函数的性能提升效果好,

  2. 高基维的场景下,即使命中了 projection 与没有命中 projection,查询效果几乎相同, 而且付出了额外的存储计算开销。


综上以上测试可以得出,高基维度对于 projection 特性并不友好,查询性能提升有限,并且还有付出不小的额外开销,不建议 projection 构建的时候应用高基维度。

clickhouse projection 有什么要注意的吗?

  1. 额外的存储开销

上面有提到,每个 projection 在 part 目录下存到单独的目录独立存储,projection 目录下存储基于原始数据计算的聚合数据。projection 数据可以抽象理解为一张聚合表,按照不同的维度聚合,聚合度不同,projection 的存储开销也会同。


2. 影响数据写入速度

通过源代码分析可以发现, projection 写入与原始数据写入过程保持一致。每一份数据 part 写入都会基于原始数据 Block 结合 projection 定义计算聚合数据,增加了数据写入的额外开销,也增加了数据写入的时间,降低了数据的时效性。


3. 历史数据不会自动物化

projection 基于 part 粒度存储,并且与数据写入保证一致,创建了 projection 之后插入的数据才会被物化。同时,part 之间的 merge 包含 projection 之间的 merge,如果 part 之间的 projeciton 定义不一致,将会导致 part merge 失败,可以通过 projection materialization 操作将 part 中 projection 数据拉齐。

projection materialization: projection 计算基于原始数据 block,对于比较大 part 计算的过程中很容易出现内存问题。可以构建 insert select pipeline 模拟新数据产生的过程,中间会生成多个临时小 part,小 part 中的 proejction 进行多段 merge。


4. part 过多导致 projection 不能命中

数据查询命中 projeciton 其中的一个条件为 50%以上的 part 覆盖 projection。存在部分场景,由于数据频繁写入,导致生成很多小 part,part 数量增加增大了计算覆盖率的分母,导致没有达成命中 projection 的条件。但是,伴随着 part 的合并 part 数量的减少,之后的查询有可能命中 projection。


本篇文章只是针对 clickhouse proejction 特性进行了简单的介绍,并进行了基础的性能测试。


在性能测试中,也发现了高基维度对于 clickhouse projection 的影响。后续将会其他文章对 clickhouse 的查询流程,底层存储进行细致的详解,分析其影响的内部原因。

发布于: 15 小时前阅读数: 8
用户头像

GrowingIO 技术团队经验分享 2020.05.09 加入

GrowingIO(官网网站www.growingio.com)的官方技术专栏,内容涵盖微服务架构,前端技术,数据可视化,DevOps,大数据方面的经验分享。 公众号:GrowingIO技术团队

评论

发布
暂无评论
Clickhouse Projection 特性探索