维度爆炸背景下uv计算在Feed业务的高效实践
导读
本文介绍了优化大数据计算中多维度用户数统计的方法,通过数据打标的方式避免数据膨胀,提高性能并减少计算成本。首先分析了大数据计算中遇到的多维度数据统计问题,然后提出了利用数据打标进行处理的解决方案,详细阐述了优化方案的实施步骤和效果。通过对比实验结果,验证了优化方案在提升性能和降低成本方面的显著效果。最后,总结了优化方案的优势和适用场景。
01 背景
Feed是百度App的一个重要业务组成部分,日均DAU(活跃用户数)规模在亿级别。在做数据分析和统计的时候,常常需要从不同日志维度去看对应的用户数。由于用户数不同维度不可累加的特性,基本上所有维度的用户数都需要单独计算,维度少的时候可以直接 count(distinct xx) 计算,维度多的话这种计算就相当痛苦了。
一个典型的场景如下:业务方需要从产品线、付费类型、资源类型、频道类型、页面类型等维度来看Feed的消费用户数。除了计算各个维度组合的用户数外,每个维度还需要看到整体的用户数。所需的结果数据表格如下(其中维度与指标均为虚构):
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##用户标识
select
appid_all, -- 产品线
pay_type_all, -- 付费类型
r_type_all, -- 资源类型
tab_type_all, -- 频道类型
page_type_all, -- 页面类型
count(distinct cuid) as feed_dau
from(
select
cuid,
appid,
pay_type,
r_type,
tab_type,
page_type
from feed_dws_kpi_dau_1d
group by 1,2,3,4,5,6
) tab
lateral view explode(array(appid, 'all')) B as appid_all
lateral view explode(array(pay_type, 'all')) B as pay_type_all
lateral view explode(array(r_type, 'all')) B as r_type_all
lateral view explode(array(tab_type, 'all')) B as tab_type_all
lateral view explode(array(page_type, 'all')) B as page_type_all
group by 1,2,3,4,5
03 优化方式
新的优化思路可以理解为在数据处理过程中采用一种“数据打标”策略,通过在数据去重的基础上生成用户粒度的中间数据,并在此基础上动态附加所需的结果维度信息。这样做的好处是可以将数据处理的重点集中在用户粒度的中间数据上,避免数据膨胀和冗余传输,同时通过编号化结果维度信息,采用更小的数据结构进行存储,从而降低数据处理的计算成本。
这种优化方法实际上是在数据处理过程中引入了一种“增量式”计算思想,即随着计算的进行,数据量逐渐收敛而不会无限增加。通过在中间数据上动态附加结果维度信息,可以避免在计算过程中重复传输和处理大量冗余数据,提高数据处理的效率和性能。
总的来说,这种优化思路旨在通过精细化数据处理流程,减少不必要的数据传输和计算成本,从而提升整体数据处理的效率和性能。
3.1 核心思路
核心计算思路如上图,普通的数据膨胀计算用户数的方法,中间需要对数据进行膨胀,再聚合,其中数据膨胀的倍数是维度数的平方(只扩展“整体”的情况),如上两个维度预计数据膨胀 2^2=4 倍,三个维度的话就是膨胀 8倍。
而新的数据聚合方法,通过一定的策略方法将维度组合拆解为维度小表并进行编号,然后将原始数据聚合至用户粒度的中间过程数据,其中各类组合维度转换为数字标记录至用户维度的数据记录上,理论上整个计算过程数据量是呈收敛聚合的,不会膨胀。
3.2 逻辑分析
3.2.1 原创数据样例
3.2.2 基于明细数据产出维度结果数据,并进行编码(可使用窗口函数 DENSE_RANK() )
PS:基于可读性,只列举两个字段(appid,pay_type)
3.2.3 将1产出的编码表,通过原始数据维度关联,写回到用户明细上(可使用 MAPJOIN)
3.2.4 编码汇总到用户粒度上(可使用array_distinct)
3.2.5 统计每个编码id出现的次数,并关联产出编码原始维度
3.3 代码实现
-- 基于明细数据产出维度结果数据,并进行编码
with dim_res as (
select
distinct
appid_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_id
from(
select
appid,
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_key
from feed_dws_kpi_dau_1d
group by 1,2,3,4,5,6
) t0
lateral view explode(array(appid, 'all')) B as appid_all
lateral view explode(array(pay_type, 'all')) B as pay_type_all
lateral view explode(array(r_type, 'all')) B as r_type_all
lateral view explode(array(tab_type, 'all')) B as tab_type_all
lateral 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_arry
from(
select
cuid,
concat_ws(
'#',
coalesce(appid,'unknow'),
coalesce(pay_type,'unknow'),
coalesce(r_type,'unknow'),
coalesce(tab_type,'unknow'),
coalesce(page_type,'unknow')
) as dim_key
from feed_dws_kpi_dau_1d
group by 1,2
) t0
join (
-- 生成每个维度原始值对应的编码数组,减少shuffle过程的数据量
select
dim_key,
collect_set(dim_id) as dim_id_arry
from dim_res
group by dim_key
) t1 on t0.dim_key = t1.dim_key
group by cuid
)
-- 将维度编码回写为原始日志
select /*+ MAPJOIN(t1) */
appid_all, -- 产品线
pay_type_all, -- 付费类型
r_type_all, -- 资源类型
tab_type_all, -- 频道类型
page_type_all, -- 页面类型
feed_dau
from(
select
-- 基于维度编码进行计数
dim_id,
sum(feed_dau) as feed_dau
from(
-- 将维度数组转为字符串直接求和
select
concat_ws(',',click_dim_id_arry) as dim_id_str,
count(1) as feed_dau
from cuid_dim
group by 1
) tab
lateral view explode(split(dim_id_str,',')) B as dim_id
group by dim_id
) t0
join (
select
distinct
appid_all, -- 产品线
pay_type_all, -- 付费类型
r_type_all, -- 资源类型
tab_type_all, -- 频道类型
page_type_all, -- 页面类型
dim_id
from dim_res
) t1 on t0.dim_id = t1.dim_id
order by 1,2,3,4,5,6
3.4 实现案例分析
本部分展示的是我们业务过程中的实际案例,原始日志 4.5 亿条,业务多维分析所需维度 9 个,每个维度都需要保留“整体”项。以下列出了不同方式的实际执行情况,任务运行基于相同的运行队列与资源配置,经验证数据产出的结果一致。
3.4.1 lateral view + distinct 方式
结论:整体运行时间 49分钟,最耗时的stage为数据扩展阶段,stage shuffle量达16TB,不具备优化空间。
△Stage执行情况
△lateral view将数据从 3.3亿条扩展到 1707亿条
3.4.2 维度编码方式
结论:整体运行时间 14 分钟,耗时主要集中在对所需维度进行编码排序的过程(Job 1/2),这部分如果例行的话,可以提前进行缓存,可优化。最大shuffle量 800GB,是针对cuid对应的维度编码进行聚合,去重的过程。
△Job执行情况,Job 1/2为维度编码排序阶段
△Stage执行情况
3.4.3 当所需维度增加
后续,业务所需维度增加至12个,lateral view + distinct 预计shuffle量会达到 120TB 左右,执行失败不出来。
采用维度编码的方案可以顺利执行,耗时主要集中在对所需维度进行编码排序的过程(Job 1/2)。
△Stage执行情况
04 方案总结&后续跟进
常见的基于数据膨胀的用户数计算方法,数据计算大小和过程数据传输量将随着维度的数量呈指数爆炸增长,维度数越多,花费在数据膨胀与Shuffle传输的资源和耗时占比越高。
为了解决数据膨胀过程中产生的大量过程数据,基于数据标签的思路反向操作,先对数据聚合为cuid+日志维度粒度,过程中将需要的维度组合转化编码数字并赋予cuid数据上,整个计算过程数据呈收敛聚合状,数据计算过程较为稳定,数据条数、shuffle量不会随着维度组合的进一步增加而大幅增加。
综上,当前的方案整体性能相较于以往有大幅度的提升,运行成本不会随着维度组合的增加而指数增加。但当前的方案也有不足之处,即代码的可理解性和可维护性。另外,当维度较少的时候,两者的性能差异不大;但当维度增加时,可以改用这种数据打标的思路进行压缩,此时的性能优势开始凸显,并且维度数越多,此方案的性能优势越大。
目前,这种计算方案已经落地应用到Feed核心场景以及短剧业务多维用户数计算。支持Feed业务 10+维度、亿级用户数的计算。
后续,我们计划针对维度编码的方案进一步优化。将代码里一些复杂的功能逻辑封装成udf,包括数组字段聚合、数据字段聚合去重等功能函数;同时针对例行任务,提前将维度组合进行排序编码。进一步加强代码的可读性与运行成本。
———— END ————
推荐阅读

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
-
上一篇
得物彩虹桥架构演进之路-负载均衡篇
文 / 得物技术-新一 一、前言 一年一更的彩虹桥系列又来了,在前面两期我们分享了在稳定性和性能2个层面的一些演进&优化思路。近期我们针对彩虹桥 Proxy 负载均衡层面的架构做了一次升级,目前新架构已经部署完成,生产环境正在逐步升级中,借此机会更新一下彩虹桥架构演进之路系列的第三篇。 阅读本文预计需要20~30分钟,建议不熟悉彩虹桥的同学在阅读本文前,可以先看一下前两篇彩虹桥架构演进的文章: 得物数据库中间件平台“彩虹桥”演进之路 彩虹桥架构演进之路-性能篇|得物技术 二、背景 彩虹桥目前依赖 SLB 做负载均衡和节点发现,随着业务发展流量越来越高,SLB 带宽瓶颈逐渐暴露,虽然在半年前做过一次双 SLB 改造临时解决了带宽瓶颈,但运维成本也随之变高。除了带宽瓶颈外,SLB 无法支持同区优先访问,导致难以适配双活架构。所以准备去除彩虹桥对 SLB 的强依赖,自建彩虹桥元数据中心,提供负载均衡和节点发现等能力,同时支持同区访问等能力来更好的适配双活架构。下面会详细介绍一下彩虹桥元数据中心以及 SDK 相关能力的相关细节。 三、核心名称解释 四、现有架构回顾 在开始介绍彩...
-
下一篇
Harmony 应用开发常用布局介绍
在 Harmony 应用开发中,合理的布局是构建美观且易用界面的关键。以下是几种常用的布局方式。 Column 布局 特点:Column 是一种垂直方向的线性布局容器。它将子组件按照从上到下的顺序依次排列。 示例代码: Column({ space: 5 }) { Text("Column") Button('Button 1') Button('Button 2') }.width('90%') .borderRadius(16) .borderColor(Color.Red) .borderWidth(2) .padding(10) Row 布局 特点:与 Column 相反,Row 是水平方向的线性布局。子组件在水平方向上从左到右排列。 示例代码: Row({ space: 5 }) { Text("Row") ForEach(this.list, (index: number) => { Button('Button' + index) }) }.width('100%') .borderRadius(16) .borderColor(Color.Red) .border...
相关文章
文章评论
共有0条评论来说两句吧...