数据研发通用模型聚合压缩方案
一、背景
大数据时代,对数据的使用分析频繁,在数仓层面对数据的管理组织都在考虑用新的技术方案实现更快速的数据计算,算力的发展是底层引擎迭代的驱动力;从最最初的MR计算框架到现在的基于Rdd的内存计算模型,再到向量化计算优化,一步步追求更高性能的计算。
数据模型的发展也是从最初的银行范式建模,到现在迭代快速的维度建模反范式建模,团队发展从最初的中台团队到现在的BP模式,都在追求极致的效率。目前,在对数据模型层面对长周期的数据进行访问都在追求避免对底层明细数据的过多扫描,从而实现模型计算效率的提升。本篇重点剖析几种数据压缩方案。主要是基于二进制压缩的方案进行数据模型的压缩。
目前,在对实体进行历史周期回溯的时候,往往需要从当前实体对应的周期去回顾实体的过去周期状态;例如对用户访问的分析,往往需要看一个用户过去周期内的首次访问,历史访问,历史复访等指标;在对某些维度进行去重计算,往往需要对历史一定周期的数据在某些维度进行聚合,本次我们重点分析精确场景下的技术方案。
二、问题详情
在对数据进行分析过程中往往有以下类似的需求
实体 维度 周期
需求大类 | 分析目标 | 代表性案例 |
🔴 用户[实体]周期去重类 | 1、 分析用户在不同维度,不同周期的去重数据 2、分析用户在不同维度,不同周期的重复行为 |
|
🟠 用户周期时间类 | 1、分析不同维度下不同周期的首次、末次时间 2、分析时间是否存在 |
|
T - T-N 维度 实体 然后再T日的留存 复购 频次
- 以上需求,核心特点就是对历史已经发生的行为、时间进行标记判断分析,因此很多时候我们模型底层不得不频繁扫描底层数据。模型必须保留对应实体的历史行为明细,才能进行分析,
- 这就导致很难进行dws模型的聚合,或者无法做到一定程度的复用,大大降低通用模型收口的能力,同时对资源的消耗膨胀较高
如何解决相关的一些问题,目前有很多成熟的方案可供选择。下面主要对比三类不同的实现。其中,在对历史周期复现的过程中,需要具备,周期内明细复现的可能。
三、方案详情
1.基于BITMAP的衍生方案
BITMAP巧妙利用了整形数据对二进制位的对应关系进行数据的压缩聚合,同时能很好的解决维度聚合的问题。在对整数ID进行聚合已经成为非常成熟的方案,目前基于BITMAP的结构使用较为成熟的方案是RoaringBitmap方案
在对非整形数据进行聚合的过程中,需要进行一次转化,要么是严格的排序编码,要么是有损的HashLong映射。
2.基于字符串构建方案
该方案是基于用户过去的序列行为,进行字符串标记实现,是bitmap方案的一种显示实现。该方案是将用户过去的一段时间的行为进行标记:
说明
- 上述方案弱化了时间的概念,需要根据数据业务分区时间作为数组最后 一个元素的时间,然后往前次序遍历即可获取实体在过去N日内的情况,
- 字符串拼接的元素可以是字符串,或者是有序数组,有序map等,可以是具体的当天业务量等等,较为灵活
- 在流量、用户场景使用较多,扩展泛化强,不受字段类型限制,目前了解,在腾讯、快手、字节、阿里等团队都均有应用。
3.日期压缩方案
- 主要针对时间日期进行极度压缩:压缩的原理如下:
存储一个日期的序列,例如保存用户一年的登录时间序列,20140201,20130102这样两个日期,如果用INT那么需要八个字节,用STRING就更多了。
解决:
通过bit来存储一天,具体的组织形式是这样的
struct daybits {
Year[] before_years;
Year[] after_years;
}
struct Year {
Quarter q1;
Quarter q2;
Quarter q3;
Quarter q4;
}
struct Quarter {
byte[] bytes
}
日期压缩过程
daybits中有若干个年,以2013做为轴分别存储在before和after years数组里。每个年有四个季度,每个季度中有个byte数组,数组中的每个字节代表8天,bytes[i] 代表这个季度的第i*8 - (i+1) * 8天,其实就是Java BitSet不用INT存储,用BYTE存储。还有一点和JAVA的BitSet不同的是,Quarter中的byte数组,使用了延迟加载,当在某一个季度登录过,这个季度才分配存储空间,并且,分配的空间也不是每次把一个季度中的90天(假设一个月30天)全都分配了(最多也就是分配12个字节可以表示96天,够一个季度的天数了),而是最大需要多少天分配多少字节,如果这个季度之后的天又需要存储,那么重新分配空间再memcpy。通过这种分季度,节约了很大一部分的存储。如果一个季度登录过三天应该就比用INT存储登录时间要高效,如果是只登录了一天,而且是在季度的最后一天,那就亏了。
例子
存储20140105
2014年存储在after_years[1]中
1月存储year的q1中
计算2014年1月1日的时间戳,计算2014年1月5日的时间戳,相减除以3600*24秒得到是1月的第五天
存储在byte[0],且值为二进制的00010000,代表第五天
反解的时候通过月的第N天解出是几月几日有点麻烦,如果有Java Calendar处理这个还比较简单,不过这个东西可以提前计算好
daybits的ToString
用"#",区分了上半区和下半区,用";"区分了每年,用","区分了每个季度,每个季度的bytes数组用base64编码。
该方案是10年前后温少在阿里安全团队实现过的方案,后续被广泛应用。
该方案主要是通过较少的年数组、季度数组与byte[]数组实现对日期的极度压缩。能用不到60字节存储一年的日期在一个字段中(几个M的数据可存百年),且支持非常灵活的日期判断、区间截断、维度聚合等操作。
说明
- 需要udf改写,满足sparkudf的编写要求。目前已经基本完成UDF的改写。
- 场景支持函数丰富,已经开源。温少已经有基于odps的版本,需要改UDF适配hive/spark
- 阿里安全部使用场景多java
查看2道真题和解析