暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Clickhouse Projection 特性探索

GrowingIO技术团队 2021-07-12
647

↑ 点击关注 ↑ 开启增长

年初的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种。

      user

      唯一用户标识

      event_key

      事件标识

      event_time

      事件时间

      dim1

      普通维度

      dim2

      普通维度

      dim3

      高基维度


      如何为数据表构建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
              复制


              查询效果如何?

              场景:select dim1, count(1) from event_projection group by dim1

              projection定义

              查询耗时

              存储

              插入时间

              无projection

              5.347s

              650M

              7min

              dim1聚合

              0.018s

              654M

              12min

              (dim1 + dim3) 聚合

              0.319s

              923M

              20min

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

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

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


              场景:不同聚合函数性能提升效果对比

              聚合函数

              没有projection

              普通维度聚合

              高基维度聚合

              count(1)

              5.347s

              0.018s

              0.319s

              groupBitmap(user)

              7.936s

              0.040s

              5.840s

              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 的查询流程,底层存储进行细致的详解,分析其影响的内部原因。



              作者:李子坤
              GrowingIO 大数据开发工程师。主要负责用户洞察功能开发,专注于Growing数据平台建设 。


              关于 GrowingIO


              GrowingIO是国内领先的一站式数据增长引擎整体方案服务商,以数据智能分析为核心,通过构建客户数据平台,打造增长营销闭环,帮助企业提升数据驱动能力,赋能商业决策、实现业务增长。自成立以来,GrowingIO已经服务超过1500家企业级客户,涵盖零售、电商、保险、酒旅航司、教育、内容社区等行业及领域。


              招聘信息


              GrowingIO技术团队是一个活力四射、对技术充满激情的团队,多个岗位持续招聘中!诚招前端工程师/大数据工程师/Java工程师等,欢迎有兴趣的同学投递简历至:jianli@growingio.com(邮件标题请注明具体岗位名称),更多职位及信息可进入招聘官网查看。


              点击「阅读原文」获取 GrowingIO 15 天免费试用!

              ↓↓↓

              文章转载自GrowingIO技术团队,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

              评论