选品平台介绍
从选的角度来讲,可以用于运营范围圈定、业务准入门槛、大促会场和日常频道投放等。在大促招商以及商家运营系统中,商家范围界定就成为整个运营流程的起点也是最值得关注的一点,限定满足预期的商家会让后续的流程更加顺滑也势必会取得更好的效果;在各种官方营销活动中,各行业运营会根据商家层级、货品类目、成交表现等为商家提供特定报名入口,为消费者提供更匹配的货品,选品成为不同层次门槛制定的有力抓手。从供的角度来讲,多货品集交并差供给、离线供给、打标供给、渠道供给等能力让选品成为数据通路的统一出口。这里供给的含义不单纯是将数据以在线或者离线的方式提供给下游,可能是一次打标任务、一次 excel 导出、也可能是持续的增量打去标任务。
Hologres(原交互式分析)是一站式实时数据仓库引擎,支持海量数据实时写入、实时更新、实时分析,支持标准SQL(兼容PostgreSQL协议),支持PB级数据多维分析(OLAP)与自助分析(Ad Hoc),支持高并发低延迟的在线数据服务(Serving),与MaxCompute、Flink、DataWorks深度融合,提供离在线一体化全栈数仓解决方案。
在过去几个月,我们把选品平台的底层数据引擎从Ha3替换成Hologres,并使用DataWorks+Flink构建了一套和Saro类似的离在线数据同步解决方案,打破了使用搜索引擎支撑选品的传统,开创了OLAP数据库在选品平台上应用的先河。
RoaringBitmap科普
Bitmap的基本思想就是用一个bit位来标记某个元素对应的Value,而Key即是该元素。由于采用了Bit为单位来存储数据,因此在存储空间方面,可以大大节省。
假设有这样一个需求:在20亿个随机整数中找出某个数m是否存在其中,并假设32位操作系统,4G内存
在Java中,int占4字节,1字节=8位(1 byte = 8 bit)
如果每个数字用int存储,那就是20亿个int,因而占用的空间约为 (2000000000*4/1024/1024/1024)≈7.45G
如果按位存储就不一样了,20亿个数就是20亿位,占用空间约为 (2000000000/8/1024/1024/1024)≈0.233G
假设有四个整数:int[ ] a = { 0, 3, 5, 7 },使用下图的Bitmap表示,表格标记了0到7的数字,其中0、3、5和7被标记为1:
Bitmap的问题在于,不管业务中实际的元素基数有多少,它占用的内存空间都恒定不变。如果Bitmap中的位的取值范围是1到100亿之间,那么Bitmap就会开辟出100亿Bit的存储空间。但是如果实际上值只有100个的话,100亿Bit的存储空间只有100个Bit为1,其余全部为0,数据存储空间浪费严重,数据越稀疏,空间浪费越严重。
RoaringBitmap是高效压缩位图,简称RBM。RBM的历史并不长,它于2016年由S. Chambi、D. Lemire、O. Kaser等人在论文《Better bitmap performance with Roaring bitmaps》与《Consistently faster and smaller compressed bitmaps with Roaring》中提出。
RoaringBitMap的基本思路:
将数据的前半部分,即高16位作为桶的编号,分为65536个桶,RBM中将这些小桶称之为Contriner(容器)
存储数据时,按照数据的高16位做为Contriner的编号去找对应的Contriner(找不到就创建对应的Contriner),再将低16位放入该Contriner中:
与Bitmap相比的优势
●空间节省:Bitmap比较适用于数据分布比较稠密的存储场景中;RoarringBitMap在稀疏存储上空间更省,稠密和Bitmap一致。
●更高效的做交、并操作:将大块的Bitmap分成各个小块,其中每个小块在需要存储数据的时候才会存在。所以当进行交集或并集运算的时候,只需要去计算存在的一些块,而不需要像Bitmap那样对整个大的块进行计算。这里既不是用空间换时间,也没有用时间换空间,而是用逻辑的复杂度同时换取了空间和时间。
集合运算性能下面这段代码测试RoaringBitmap与数组在交集运算上的性能差异:
RoaringBitmap rbAnd1 = new RoaringBitmap();
for (int k = 100000; k < 200000; k++) {
rbAnd1.add(k);
}
RoaringBitmap rbAnd2 = new RoaringBitmap();
for (int k = 150000; k < 200000; k++) {
rbAnd2.add(k);
}
Long start = System.currentTimeMillis();
rbAnd1.and(rbAnd2);
System.out.println("roaringBitmap and 耗时:" + (System.currentTimeMillis() - start) + "ms");
List<Integer> list1 = new ArrayList<>();
for (int k = 100000; k < 200000; k++) {
list1.add(k);
}
List<Integer> list2 = new ArrayList<>();
for (int k = 150000; k < 200000; k++) {
list2.add(k);
}
start = System.currentTimeMillis();
list1.retainAll(list2);
System.out.println("list and 耗时:" + (System.currentTimeMillis() - start) + "ms");
测试结果:
roaringBitmap and 耗时:1ms
list and 耗时:3056ms
RoaringBitmap rbAnd1 = new RoaringBitmap();
for (int k = 100000; k < 200000; k++) {
rbAnd1.add(k);
}
RoaringBitmap rbAnd2 = new RoaringBitmap();
for (int k = 150000; k < 200000; k++) {
rbAnd2.add(k);
}
Long start = System.currentTimeMillis();
rbAnd1.andNot(rbAnd2);
System.out.println("roaringBitmap andNot 耗时:" + (System.currentTimeMillis() - start) + "ms");
List<Integer> list1 = new ArrayList<>();
for (int k = 100000; k < 200000; k++) {
list1.add(k);
}
List<Integer> list2 = new ArrayList<>();
for (int k = 150000; k < 200000; k++) {
list2.add(k);
}
start = System.currentTimeMillis();
list1.removeAll(list2);
System.out.println("list andNot 耗时:" + (System.currentTimeMillis() - start) + "ms");
roaringBitmap andNot 耗时:1ms
list andNot 耗时:3350ms
RoaringBitmap实践
其中商品指标不乏多值字段,如:商品所属类目、商品已报名活动、商品所属行业...,存储这些多值字段,我们一开始采用数组类型字段进行存储。如果运营小二使用了多值指标进行选品,规则翻译模块会把运营的输入翻译成数组的交集运算提交到Hologres执行。直到我们从Hologres的监控发现,CPU偶有冲高到100%的情况:
通过抓取该段时间的慢SQL,我们发现导致CPU冲高的大部分SQL都是数组取交集的运算。
问题定位到了,当时想到两种优化方案:
●多值字段从数组换成RoaringBitmap,RoaringBitmap是一种高效的Bitmap压缩算法,目前已被广泛应用在各种语言和各种大数据平台。适合计算超高基维的,常用于去重、标签筛选、时间序列等计算中。
●多值字段依旧使用数组,数据落表时先排好序再写入,在查询取交集时,传入比较的数组也排好序,即“先排序再比较”的策略。
两种优化方案优劣比较:
离线写入:即商品大宽表数据由MaxCompute写入到Hologres,选品平台的数据层一般由离线数据和在线数据两部分组成
综上,字段类型修改为RoaringBitmap,离线写入的耗时增加1min左右 CPU消耗增加10%,对离线链路构建时长的影响可以忽略;查询性能大幅优于有序的数组和现有无序的数组。
基于此,我们把商品大宽表的23个多值字段由数组类型升级成RoaringBitmap,整体优化收益如下:
●商品大宽表存储空间下降30% ~ 40%,存储文件个数下降30% ~ 40%(不同国家有所差异,整体在30% ~ 40%)。文件个数减少有以下几个好处:○提高查询性能:更少的文件意味着查询需要扫描的数据量和文件数量都较少,从而提高查询速度和系统整体性能。
○减少存储开销:小文件会在硬盘上产生更多的碎片,导致磁盘空间利用率降低,文件个数减少有助于提高磁盘空间利用率。
○简化管理:当文件数量较多时,管理和监控任务会变得更加复杂。文件个数减少能够简化管理和维护工作,提高运维效率。
○优化备份和恢复:备份大量小文件通常比备份少量大文件所需的时间更长。文件个数减少可以缩短备份和恢复的时间,提高业务连续性。
○减轻元数据压力:文件系统需要跟踪每个文件的信息(如权限、大小等),大量的小文件会增加元数据的压力。文件个数减少有助于减轻元数据服务器的压力,保证系统的稳定运行。
●Hologres CPU利用率下降30%
HologresRoaringBitmap类型字段建表语句参考:
BEGIN;
CREATE TABLE public.test (
product_id bigint NOT NULL,
product_categories roaringbitmap,
ds text NOT NULL,
PRIMARY KEY (product_id, ds)
) PARTITION BY LIST (ds);
CALL set_table_property('public.test', 'orientation', 'column,row');
CALL set_table_property('public.test', 'distribution_key', 'product_id');
END;
“供给”是选品平台的核心能力,围绕着“供给”,我们构建了选品池持续的增量打标/去标的核心能力。打标供给核心流程如下:
●数据冗余:因为需要对选品池今天和昨天的数据进行比较,选品池今天的数据从选品引擎实时拉取,选品池昨天的数据冗余到Mysql以方便第二天进行比较
●选品池运算:今天和昨天的选品池数据对比,在应用内存中进行,如果选品池数据量过大,容易触发应用频繁的FullGC。技术通过设置20w上限来规避这个问题,业务对突破20w的述求越来越强烈
●调度效率低:6000+选品池调度完成,要12h,单个选品池调度完成花费80~90s
在了解到RoaringBitmap在集合存储具有较大的空间优势,在集合运算具有较大的时间优势,我们决定使用RoaringBitmap来存储选品池数据。
在RoaringBitmap落地过程中,遇到了很多困难和挑战,罗列其中一二:
●为了解决数据冗余的问题,我们限定选品池数据不出Hologres数仓,也就是要借助Hologres的RoaringBitmap字段类型来存储选品池数据。但Hologres的RoaringBitmap字段类型只支持32位整数存储,商品ID和商家ID显然已经超过了32位整数的范围。于是,问题就演化成了RoaringBitmap怎么能存下64位整数。从实现复杂度,以及对未来SKU选品池支撑友好程度等多方面综合考虑,我们最终采用分桶法。
○偏移法:
○分桶法:将商品ID或商家ID拆为低位和桶号,这里我们直接将商品ID的高34位作为桶号,将低30位存入RoaringBitmap
○自定义id:因为商品ID或商家ID均存在较大的断层,实际上商品数量和商家数量远未达到32位整数的上限,内部维护一个自增长ID去映射商品ID或商家ID
在攻克了诸多困难和挑战后,打标供给整体实现方案简化为如下流程:
选品池运算核心逻辑:
优化整体收益:
●数据流转效率:6000+选品池调度完成由12h下降至1h,单个选品池调度时间由90s下降至2s
●系统稳定性:应用层FullGC次数下降88%
●存储成本:消除数据冗余,节省75GB存储空间
●业务突破:帮助业务突破选品池20w大小限制,目前暂定50w
Hologres RoaringBitmap分桶表建表语句参考:
CREATE TABLE public.test_pool_dump_rb (
pool_id bigint NOT NULL,
pool_type text NOT NULL,
bucket bigint NOT NULL,
pool_data roaringbitmap NOT NULL,
ext text NOT NULL,
ds text NOT NULL
,PRIMARY KEY (pool_id, bucket, ds)
)
PARTITION BY LIST (ds);
CALL set_table_property('public.test_pool_dump_rb', 'orientation', 'column,row');
CALL set_table_property('public.test_pool_dump_rb', 'clustering_key', 'pool_id:asc');
CALL set_table_property('public.test_pool_dump_rb', 'distribution_key', 'pool_id');
CALL set_table_property('public.test_pool_dump_rb', 'table_storage_mode', 'any');
CALL set_table_property('public.test_pool_dump_rb', 'auto_partitioning.enable', 'true');
CALL set_table_property('public.test_pool_dump_rb', 'auto_partitioning.time_unit', 'day');
CALL set_table_property('public.test_pool_dump_rb', 'auto_partitioning.time_zone', 'ASIA/SHANGHAI');
CALL set_table_property('public.test_pool_dump_rb', 'auto_partitioning.num_precreate', '2');
CALL set_table_property('public.test_pool_dump_rb', 'auto_partitioning.num_retention', '2');
CALL set_table_property('public.test_pool_dump_rb', 'auto_partitioning.num_hot', '-1');
END;
insert into test_pool_dump_rb_${ds}
(pool_id, pool_type, bucket, pool_data, ext, ds)
select
#{poolId} as pool_id,
#{poolType} as pool_type,
product_id >> 30 as bucket,
rb_build(array_agg(${product_id} <![CDATA[&]]> 1073741823)) as pool_data,
#{ext} as ext,
#{ds} as ds
from (
select product_id from test
) A
group by product_id >> 30
select A.pool_id, A.pool_type, A.bucket, rb_to_array(A.pool_data - if(B.pool_data is null, rb_build('{}'), B.pool_data)) as pool_data
from
(
select pool_id, pool_type, bucket, pool_data
from test_pool_dump_rb
where pool_id = #{poolId}
and ds = #{ds}
) A left join
(
select pool_id, bucket, pool_data
from test_pool_dump_rb
where pool_id = #{poolId}
and ds = #{differenceDs}
) B on A.pool_id = B.pool_id and A.bucket = B.bucket
总 结
更多推荐
点击「阅读原文」了解更多实时数仓Hologres产品详情