Apache Kylin 4.0精确去重的全局字典原理

全局字典讲解

为什么需要全局字典

在OLAP数据分析领域,去重计数(Count distinct)是非常常见的需求,而根据去重结果的要求又分为近似去重和精确去重。

在大规模数据集下,要想做到精确去重还要保证查询快速响应还是很有挑战性的。我们知道精确去重经常用到的处理方式就是位图法(Bit map)。对于整型数据,我们可以将统计信息保存在Bit map中,但是实际处理的数据中除了整型还会有String等其他类型,如果想做到精确去重首先就需要构建一个字典来为这些数据进行统一映射,然后再通过Bit map法进行统计。

我们都知道Kylin通过预计算技术来加速大数据分析。在增量构建Cube的时候,为了避免不同的segment单独构建字典导致最终去重结果出错,一个Cube中所有的Segment会使用同一个字典,也就是全局字典(Global Dictionary)。

全局字典的演变

Kylin从1.5.3版本就开始支持全局字典功能,但是这时的构建方式也具有明显的缺陷:

全局字典在Job Server上进行单点构建,随着数据增多构建时长变得不可控
随着数据积累,全局字典的构建对Kylin 构建节点的内存的需求会不断增多
受限于整型最大数量的限制
其实在Kylin3.1中已经加入了基于Hive的分布式全局字典构建,它已经解决了以上问题,详情可以查看Kylin分布式全局字典 。但是Kylin 4.0为了适应全新的构建查询引擎,基于spark实现了另外一种分布式构建全局字典的方式,今天我们就来详细描述一下Kylin 4.0的全局字典是如何实现的。

基于Spark的全局字典

Kylin 4.0构建全局字典的方式基于Spark进行分布式的编码处理,减小了单机节点的压力,构建字典数量能够突破整型最大数量的限制。

设计与原理

全局字典的结构

  • 每一次构建任务会生成一份新的全局字典
  • 每次新的构建任务的字典按版本号进行保存, 旧的全局字典会被逐渐删除
  • 一份字典包括一份meta数据文件和多个字典文件, 每个字典文件称之为桶(Bucket)
  • 每个桶分为两个映射( Map<Object, Long>), 两者合并为完整的映射关系


Bucket的概念与转化

Kylin引入了桶这一概念,可以理解为在处理数据的时候,将数据分到若干个桶(即多个分区)中进行并行处理。 第一次构建字典的时候会对每个桶内的值从1开始编码,在所有桶的编码完成之后再根据每个桶的offset值进行整体字典值的分配。在代码中两次编码是通过两个HashMap进行存储的,其中一个存储桶内相对的字典值,另一个存储所有桶之间绝对的字典值。

下图所示的是编号为1的桶多次构建任务中,桶内字典的传递,每一次构建都会为桶创建一个新的版本(即v1, v2, v3等),加入版本控制的原因后面会有解释。Curr(current)和Prev(Previous)是一个桶内的两个HashMap,分别存储着当前桶内字典的相对(Relative)编码值和之前已经构建的所有字典值的绝对(Absolute)编码值。

构建步骤

  • 通过 Spark 创建平表并获取需精确去重列的 distinct 值
  • 根据确定去重后的字面值数量来确认分片数, 并且根据需求判断是否需要扩容
  • 将数据分配(repartition)到多个分片(Partition)中,分别进行编码, 存储到各自的字典文件中
  • 为当前构建任务分配版本号
  • 保存字典文件和 metadata数据(桶数量和桶的 offset 值)
  • 根据条件判断需要删除旧版本

初次构建

  • 计算桶的大小
    取需要构建字典的数量处理单个桶阈值和桶数量默认值的最大值。
  • 创建桶并分配数据进行编码
  • 生成meta文件记录桶的offsets

以下是相关配置项及其默认值。

kylin.dictionary.globalV2-min-hash-partitions=10
kylin.dictionary.globalV2-threshold-bucket-size=500000

非初次构建

  • 根据字典数量确定桶是否需要扩容
  • 已编码的字典值对扩容后的桶进行重新分配
  • 读取之前最新版本的字典数据,并分配到各个桶中
  • 将新的值分配到桶中
  • 前一次构建的字典值不会改变

版本控制

全局字典会通过给单次构建分配基于时间戳的版本号来进行隔离。加入版本控制的原因是构建任务可能会并发执行,而当前构建全局字典过程中的编码是不支持并发。通过版本控制,每一次编码都能够完整的读取之前构建好的全局字典,这样就保证了最新版本的字典拥有最完整的全局字典编码,而且一个Cube的全局字典每次被读取的时候都会选取最新版本的字典。字典最终在文件存储系统(此处为HDFS)上按版本存储如下图所示。

常见问题

  1. 为什么在一个BucketDIctionary需要使用两个Map?

构建过程开始需要对分配到各个桶内的字典从1开始做一个相对(Relative)编码,这一部分字典相对编码值会存储在一个HashMap中,在相对字典值编码完成后,会得到每个桶的offset值,也就是桶内字典的数量,然后根据这个字典值计算出每个桶(桶是有顺序的)内字典值相对于所有桶的offset值的绝对(Absolute)编码,字典的绝对编码也会用另一个HashMap进行存储。

  1. 会不会存在数据倾斜问题?

现在测试下来因为热点构建不出来的概率很小,一般倾斜十亿级别才会过不去,列很多的确可能会造成这个问题,不过编码的桶数是可以无限放大的 除非单key热点,否则调整参数也是很轻松完成构建。

  1. 为什么全局字典的数量能够超过整型最大基数(21亿)的限制?

因为引入了全新的BitMap数据结构Roaring64BitMap,在全局字典编码完成之后,编码会被压缩成二进制存储在Roaring64BitMap对象中,BitMap实际上是通过Long而不再是Integer进行存储的。

    作者:淡蘫铯の迗悾原文地址:https://segmentfault.com/a/1190000038481589

    %s 个评论

    要回复文章请先登录注册