cover_image

维度爆炸背景下uv计算在Feed业务的高效实践

百度Geek说 百度Geek说
2024年11月18日 10:01
图片

点击蓝字,关注我们

图片

作者 | FB

导读 
introduction
本文介绍了优化大数据计算中多维度用户数统计的方法,通过数据打标的方式避免数据膨胀,提高性能并减少计算成本。首先分析了大数据计算中遇到的多维度数据统计问题,然后提出了利用数据打标进行处理的解决方案,详细阐述了优化方案的实施步骤和效果。通过对比实验结果,验证了优化方案在提升性能和降低成本方面的显著效果。最后,总结了优化方案的优势和适用场景。

全文6039字,预计阅读时间16分钟。


GEEK TALK

01

背景



Feed是百度App的一个重要业务组成部分,日均DAU(活跃用户数)规模在亿级别。在做数据分析和统计的时候,常常需要从不同日志维度去看对应的用户数。由于用户数不同维度不可累加的特性,基本上所有维度的用户数都需要单独计算,维度少的时候可以直接 count(distinct xx) 计算,维度多的话这种计算就相当痛苦了。

一个典型的场景如下:业务方需要从产品线、付费类型、资源类型、频道类型、页面类型等维度来看Feed的消费用户数。除了计算各个维度组合的用户数外,每个维度还需要看到整体的用户数。所需的结果数据表格如下(其中维度与指标均为虚构):

维度(所有维度都需要 ALL)

指标

产品线

付费类型

资源类型

频道类型

页面类型

用户数

基础版

付费

视频

推荐

列表页

200万

基础版

免费

视频

推荐

列表页

300万

基础版

ALL

视频

推荐

列表页

400万

基础版

ALL

ALL

ALL

ALL

1000万

... ...

ALL

ALL

ALL

ALL

ALL

2000万


GEEK TALK

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_daufrom( 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) tablateral 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_alllateral view explode(array(page_type, 'all')) B as page_type_allgroup by 1,2,3,4,5


GEEK TALK

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() )

PS:基于可读性,只列举两个字段(appid,pay_type)

原始数据维度

编码维度

编码值

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


3.2.3 将1产出的编码表,通过原始数据维度关联,写回到用户明细上(可使用 MAPJOIN)

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]


3.2.4 编码汇总到用户粒度上(可使用array_distinct)

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]

3.2.5 统计每个编码id出现的次数,并关联产出编码原始维度

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 (    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_daufrom( 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) t0join ( 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_idorder 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执行情况


GEEK TALK

04

方案总结&后续跟进


常见的基于数据膨胀的用户数计算方法,数据计算大小和过程数据传输量将随着维度的数量呈指数爆炸增长,维度数越多,花费在数据膨胀与Shuffle传输的资源和耗时占比越高。

为了解决数据膨胀过程中产生的大量过程数据,基于数据标签的思路反向操作,先对数据聚合为cuid+日志维度粒度,过程中将需要的维度组合转化编码数字并赋予cuid数据上,整个计算过程数据呈收敛聚合状,数据计算过程较为稳定,数据条数、shuffle量不会随着维度组合的进一步增加而大幅增加。

综上,当前的方案整体性能相较于以往有大幅度的提升,运行成本不会随着维度组合的增加而指数增加。但当前的方案也有不足之处,即代码的可理解性和可维护性。另外,当维度较少的时候,两者的性能差异不大;但当维度增加时,可以改用这种数据打标的思路进行压缩,此时的性能优势开始凸显,并且维度数越多,此方案的性能优势越大。

目前,这种计算方案已经落地应用到Feed核心场景以及短剧业务多维用户数计算。支持Feed业务 10+维度、亿级用户数的计算。

后续,我们计划针对维度编码的方案进一步优化。将代码里一些复杂的功能逻辑封装成udf,包括数组字段聚合、数据字段聚合去重等功能函数;同时针对例行任务,提前将维度组合进行排序编码。进一步加强代码的可读性与运行成本。



 END

  推荐阅读 

数据湖系列之四  | 数据湖存储加速方案的发展和对比分析

大模型时代,云原生数据底座的创新和实践

百度沧海·存储统一技术底座架构演进

计算不停歇,百度沧海数据湖存储加速方案 2.0 设计和实践

AI 原生时代,更要上云:百度智能云云原生创新实践


图片

一键三连,好运连连,bug不见👇


继续滑动看下一个
百度Geek说
向上滑动看下一个