维度爆炸背景下uv计算在Feed业务的高效实践
点击蓝字,关注我们
01
Feed是百度App的一个重要业务组成部分,日均DAU(活跃用户数)规模在亿级别。在做数据分析和统计的时候,常常需要从不同日志维度去看对应的用户数。由于用户数不同维度不可累加的特性,基本上所有维度的用户数都需要单独计算,维度少的时候可以直接 count(distinct xx) 计算,维度多的话这种计算就相当痛苦了。
一个典型的场景如下:业务方需要从产品线、付费类型、资源类型、频道类型、页面类型等维度来看Feed的消费用户数。除了计算各个维度组合的用户数外,每个维度还需要看到整体的用户数。所需的结果数据表格如下(其中维度与指标均为虚构):
维度(所有维度都需要 ALL) | 指标 | ||||
产品线 | 付费类型 | 资源类型 | 频道类型 | 页面类型 | 用户数 |
基础版 | 付费 | 视频 | 推荐 | 列表页 | 200万 |
基础版 | 免费 | 视频 | 推荐 | 列表页 | 300万 |
基础版 | ALL | 视频 | 推荐 | 列表页 | 400万 |
基础版 | ALL | ALL | ALL | ALL | 1000万 |
... ... | |||||
ALL | ALL | ALL | ALL | ALL | 2000万 |
02
常见的实现方式是直接计算,单独计算每个维度的用户数指标。
将原始数据按 cuid+初始维度 去重后,使用 lateral view explode 将数据从一行膨胀成多行,然后直接 distinct 的数据计算方式。
2.1 核心思路
2.2 代码实现
-- 表名:feed_dws_kpi_dau_1d-- 字段名及注释:appid##产品线,pay_type##付费类型,r_type##资源类型,tab_type##频道类型,page_type##页面类型,cuid##用户标识selectappid_all, -- 产品线pay_type_all, -- 付费类型r_type_all, -- 资源类型tab_type_all, -- 频道类型page_type_all, -- 页面类型count(distinct cuid) as feed_daufrom(selectcuid,appid,pay_type,r_type,tab_type,page_typefrom feed_dws_kpi_dau_1dgroup by 1,2,3,4,5,6) tablateral view explode(array(appid, 'all')) B as appid_alllateral view explode(array(pay_type, 'all')) B as pay_type_alllateral view explode(array(r_type, 'all')) B as r_type_alllateral view explode(array(tab_type, 'all')) B as tab_type_alllateral view explode(array(page_type, 'all')) B as page_type_allgroup by 1,2,3,4,5
03
新的优化思路可以理解为在数据处理过程中采用一种“数据打标”策略,通过在数据去重的基础上生成用户粒度的中间数据,并在此基础上动态附加所需的结果维度信息。这样做的好处是可以将数据处理的重点集中在用户粒度的中间数据上,避免数据膨胀和冗余传输,同时通过编号化结果维度信息,采用更小的数据结构进行存储,从而降低数据处理的计算成本。
这种优化方法实际上是在数据处理过程中引入了一种“增量式”计算思想,即随着计算的进行,数据量逐渐收敛而不会无限增加。通过在中间数据上动态附加结果维度信息,可以避免在计算过程中重复传输和处理大量冗余数据,提高数据处理的效率和性能。
总的来说,这种优化思路旨在通过精细化数据处理流程,减少不必要的数据传输和计算成本,从而提升整体数据处理的效率和性能。
3.1 核心思路
核心计算思路如上图,普通的数据膨胀计算用户数的方法,中间需要对数据进行膨胀,再聚合,其中数据膨胀的倍数是维度数的平方(只扩展“整体”的情况),如上两个维度预计数据膨胀 2^2=4 倍,三个维度的话就是膨胀 8倍。
而新的数据聚合方法,通过一定的策略方法将维度组合拆解为维度小表并进行编号,然后将原始数据聚合至用户粒度的中间过程数据,其中各类组合维度转换为数字标记录至用户维度的数据记录上,理论上整个计算过程数据量是呈收敛聚合的,不会膨胀。
3.2 逻辑分析
3.2.1 原创数据样例
cuid 用户标识 | appid 产品线 | pay_type 付费类型 | r_type 资源类型 | tab_type 频道类型 | page_type 页面类型 |
ID_001 | 1 | 付费 | 视频 | 推荐 | 列表页 |
ID_001 | 1 | 免费 | 视频 | 推荐 | 列表页 |
ID_002 | 1 | 免费 | 视频 | 推荐 | 落地页 |
ID_003 | 1 | 付费 | 视频 | 推荐 | 列表页 |
ID_004 | 1 | 付费 | 图文 | 发现 | 落地页 |
ID_001 | 10001 | 免费 | 视频 | 推荐 | 落地页 |
ID_002 | 10001 | 付费 | 动态 | 推荐 | 列表页 |
3.2.2 基于明细数据产出维度结果数据,并进行编码(可使用窗口函数 DENSE_RANK() )
原始数据维度 | 编码维度 | 编码值 | ||
appid | pay_type | appid_all | pay_type_all | dim_id |
1 | 免费 | 1 | ALL | 1 |
ALL | 免费 | 3 | ||
1 | 免费 | 5 | ||
ALL | ALL | 9 | ||
1 | 付费 | 1 | ALL | 1 |
ALL | 付费 | 4 | ||
1 | 付费 | 6 | ||
ALL | ALL | 9 | ||
10001 | 免费 | 10001 | ALL | 2 |
ALL | 免费 | 3 | ||
10001 | 免费 | 7 | ||
ALL | ALL | 9 | ||
10001 | 付费 | 10001 | ALL | 2 |
ALL | 付费 | 4 | ||
10001 | 付费 | 8 | ||
ALL | ALL | 9 | ||
cuid 用户标识 | appid 产品线 | pay_type 付费类型 | dim_id_arry 编码组合-clk |
ID_001 | 1 | 付费 | [1,4,6,9] |
ID_001 | 1 | 免费 | [1,3,5,9] |
ID_002 | 1 | 免费 | [1,3,5,9] |
ID_003 | 1 | 付费 | [1,4,6,9] |
ID_004 | 1 | 付费 | [1,4,6,9] |
ID_001 | 10001 | 免费 | [2,3,7,9] |
ID_002 | 10001 | 付费 | [2,4,8,9] |
cuid | dim_id_arry 编码组合 |
ID_001 | [1,2,3,4,5,6,7,9] |
ID_002 | [1,2,3,4,5,8,9] |
ID_003 | [1,4,6,9] |
ID_004 | [1,4,6,9] |
dim_id | appid_all | pay_type_all | 用户数 |
1 | 1 | ALL | 4 |
2 | 10001 | ALL | 2 |
3 | ALL | 免费 | 2 |
4 | ALL | 付费 | 4 |
5 | 1 | 免费 | 2 |
6 | 1 | 付费 | 3 |
7 | 10001 | 免费 | 1 |
8 | 10001 | 付费 | 1 |
9 | ALL | ALL | 4 |
3.3 代码实现
-- 基于明细数据产出维度结果数据,并进行编码with dim_res as (selectdistinctappid_all, -- 产品线pay_type_all, -- 付费类型r_type_all, -- 资源类型tab_type_all, -- 频道类型page_type_all, -- 页面类型dim_key,DENSE_RANK() OVER(ORDER BY appid_all,pay_type_all,r_type_all,tab_type_all,page_type_all) AS dim_idfrom(selectappid,pay_type,r_type,tab_type,page_type,concat_ws('#',coalesce(appid,'unknow'),coalesce(pay_type,'unknow'),coalesce(r_type,'unknow'),coalesce(tab_type,'unknow'),coalesce(page_type,'unknow')) as dim_keyfrom feed_dws_kpi_dau_1dgroup by 1,2,3,4,5,6) t0lateral view explode(array(appid, 'all')) B as appid_alllateral view explode(array(pay_type, 'all')) B as pay_type_alllateral view explode(array(r_type, 'all')) B as r_type_alllateral view explode(array(tab_type, 'all')) B as tab_type_alllateral view explode(array(page_type, 'all')) B as page_type_all),-- 生成cuid聚合数据+对应的维度编码组合cuid_dim as(select /*+ MAPJOIN(t1) */cuid,array_distinct(split(concat_ws(',',collect_set(concat_ws(',',dim_id_arry))),',')) as click_dim_id_arryfrom(selectcuid,concat_ws('#',coalesce(appid,'unknow'),coalesce(pay_type,'unknow'),coalesce(r_type,'unknow'),coalesce(tab_type,'unknow'),coalesce(page_type,'unknow')) as dim_keyfrom feed_dws_kpi_dau_1dgroup by 1,2) t0join (-- 生成每个维度原始值对应的编码数组,减少shuffle过程的数据量selectdim_key,collect_set(dim_id) as dim_id_arryfrom dim_resgroup by dim_key) t1 on t0.dim_key = t1.dim_keygroup by cuid)-- 将维度编码回写为原始日志select /*+ MAPJOIN(t1) */appid_all, -- 产品线pay_type_all, -- 付费类型r_type_all, -- 资源类型tab_type_all, -- 频道类型page_type_all, -- 页面类型feed_daufrom(select-- 基于维度编码进行计数dim_id,sum(feed_dau) as feed_daufrom(-- 将维度数组转为字符串直接求和selectconcat_ws(',',click_dim_id_arry) as dim_id_str,count(1) as feed_daufrom cuid_dimgroup by 1) tablateral view explode(split(dim_id_str,',')) B as dim_idgroup by dim_id) t0join (selectdistinctappid_all, -- 产品线pay_type_all, -- 付费类型r_type_all, -- 资源类型tab_type_all, -- 频道类型page_type_all, -- 页面类型dim_idfrom dim_res) t1 on t0.dim_id = t1.dim_idorder by 1,2,3,4,5,6
3.4 实现案例分析
本部分展示的是我们业务过程中的实际案例,原始日志 4.5 亿条,业务多维分析所需维度 9 个,每个维度都需要保留“整体”项。以下列出了不同方式的实际执行情况,任务运行基于相同的运行队列与资源配置,经验证数据产出的结果一致。
△Stage执行情况
△lateral view将数据从 3.3亿条扩展到 1707亿条
△Job执行情况,Job 1/2为维度编码排序阶段
△Stage执行情况
△Stage执行情况
04
常见的基于数据膨胀的用户数计算方法,数据计算大小和过程数据传输量将随着维度的数量呈指数爆炸增长,维度数越多,花费在数据膨胀与Shuffle传输的资源和耗时占比越高。
为了解决数据膨胀过程中产生的大量过程数据,基于数据标签的思路反向操作,先对数据聚合为cuid+日志维度粒度,过程中将需要的维度组合转化编码数字并赋予cuid数据上,整个计算过程数据呈收敛聚合状,数据计算过程较为稳定,数据条数、shuffle量不会随着维度组合的进一步增加而大幅增加。
综上,当前的方案整体性能相较于以往有大幅度的提升,运行成本不会随着维度组合的增加而指数增加。但当前的方案也有不足之处,即代码的可理解性和可维护性。另外,当维度较少的时候,两者的性能差异不大;但当维度增加时,可以改用这种数据打标的思路进行压缩,此时的性能优势开始凸显,并且维度数越多,此方案的性能优势越大。
目前,这种计算方案已经落地应用到Feed核心场景以及短剧业务多维用户数计算。支持Feed业务 10+维度、亿级用户数的计算。
后续,我们计划针对维度编码的方案进一步优化。将代码里一些复杂的功能逻辑封装成udf,包括数组字段聚合、数据字段聚合去重等功能函数;同时针对例行任务,提前将维度组合进行排序编码。进一步加强代码的可读性与运行成本。
END
推荐阅读