惠善祥,21年入职去哪儿网,在国内酒店数据仓库团队负责酒店离线、实时数仓的开发、维护与优化。专注于计算效率提升、数据治理等工作,负责S级报表SLA保障、链路唯一化建设等项目;长期关注大数据计算演变与性能提升。
随着旅游市场的回暖、出行需求的激增,去哪儿网酒店的单日预订量也刷新了历年的前高还在不断突破产生新高。
与此同时,酒店数仓每天处理的数据也在不断上涨,为了保障日常 SA 级的报表正常产出,需要我们持续优化数据处理的链路,消除存在的瓶颈与卡点。
酒店流量链路产出的核心宽表为:搜索( search S页 )、列表( list L页 )、详情( Detail D 页)、预订( booking B 页)和提交订单( order O 页)流量表,对应了酒店主流程各个页面的用户流量数据。
我们以一个具体的案例 “ L 页流量表” 优化作为切入点,来展开对流量链路的优化实践,承诺 SLA、体量够大、关联够多、逻辑够复杂、使用够广一直是 L 页流量表的内在标签。
酒店列表页流量表(简称L页流量表)为主流程最常用表,是进入酒店主流程业务的第一入口。
同时又承接了用户窄口径转化、搜索排序、分销提流等众多 SA 级别报表,需要确保 L 页流量表产出的及时性来保障我们 SA 级报表的及时产出。
在各页流量表中L页是任务链路最长、产出时间最长的表,随着流量的不断上涨产出时间一再告破 SLA 基线。
能够为S级报表( S 级报表的 SLA 基线为 9 点)预留的执行 buffer 时间越来越少,尤其进入 4 月后对应 S 报表的产出时间出现了明显的延迟。
1、图解说明
图中紫色背景为产出L页流量表的两大任务:L 页中间表和 L 页流量表,其中L 页流量表背景包含了依赖上游的事实表和相关维度表。
数据产出的先后时间通过颜色由浅及深进行了表示,颜色最浅任务产出数据在 1 点前,颜色最深的产出数据时间在 4 点后。
”相对稳定 【 5 分钟】【 1 点 30 】“分别代表了上游依赖中:数据产出时间的稳定性;第一个中扩号为任务的执行的时间;第二个中扩号为执行完成时间。
序号⑩:L 页流量表的产出任务,随流量上涨和上游依赖的不稳定产出时间也一直后延,从年初 6 点左右到四月中已经需要 8 9 点才能产出;关联的都是大表,任务本身执行时间长,其中个别 Stage 并行度非常低;
序号⑥:L 页中间表前置任务,从原始日志中进行了清洗、转化并扩展了外围的基础信息; 该任务逻辑非常重本身执行时间就长,需要 2~3 个小时才能完成,因为依赖上游不稳定的 ③④ 事实,导致该任务迟迟不能执行;
序号①②:L 页的主数据,数据处理的路径通过紫色箭头标识一直到序号⑦,这条路径上游依赖的产出时间为 1 点半左右,可以单独拆分;
序号③:公共埋点数据,为依赖的外部数据产出时间的部位的,一方面需要积极的推动外部进行优化解决;另一方面在后置该数据的使用时间;
序号④:主流程接口请求数据,本身执行时间不稳定的定位原因进行解决,依赖上游③公共埋点数据也是其产出晚的重要原因;
序号⑤:用户访问路径(窄口径)数据,任务本身的执行时长稳定,不稳定的原因在于上游;
序号⑧⑨:基础维表的一系列数据,只使用各表中的几个个别字段对L页流量表的基础信息进行了扩展,这类数据产出时间非常早,可提前进行‘列’、‘行’的剪裁;
综上通过分析各个环节存在的问题,后去定位深挖其根本的原因,我们可以通过数据链路、数据倾斜、数据存储三个方面来入手进行优化解决。
1.1 “拆” 大段长逻辑拆分
可以梳理依赖的上游和逻辑的相关性对大段的 SQL 进行拆分,如序号 ⑥ 中产出的 mid 表任务中依赖了多张数据表,使用了大段的串联逻辑,只有在上游依赖全部完成才能执行。
拆分如下:
SEARCH:搜索信息(④)
通过以上五线路的拆分,非依赖的数据可以在上游依赖任务完成后及时的进入处理状态中。
这里需要注意错峰执行,一个关键数据的产出往往对应非常多的下游依赖任务,我们应该将非重要、非核心任务改为定时执行,选择执行任务个数较少的时间段来执行。
减少瞬时任务堆积造成计算资源的争抢,还可以降低 HiveServer 服务器的负载,避免 HiveServer 因负载过高超过阈值自动重启,导致跑任务失败重新执行,更加延长了数据的产出时间。
“提”这里分为两方面:
一是,提前执行,还是在一个任务中对大段逻辑按照上游依赖拆分出的多个子逻辑,放入不同的执行方法,分别检测各自的上游任务的完成情况,上游完成后可以立即执行。
如对 MID 表 ⑥ 产出任务的拆分,可以拆逻辑而不拆分任务,确保任务的可读性。
二是,提前过滤,对数据过滤这里分为两部分:把过滤条件进行提前,裁剪下游不使用的字段。
如合并任务 ⑦ 中的“剔除非主流程数据” 可以直接提前至序号 ① ODS 的直接下游进行过滤,从源头来减少数据量;
各维表 ⑧ 中的数据不是都会使用到,提前把不需要的字段裁剪掉以减少需要处理的数据量。
并行执行提高资源的利用效率,在更短的时间内完成大量数据处理任务,适用于核心链路的数据产出,需要平衡执行队列确保有足够的资源。
而在产出 MID 表 ⑥ 这个大任务中拆分出的四条执行线,各自上游依赖完成的时间不同,可以在各自的时间线上并行执行,避免了对计算资源的争抢;
对取维表 ⑧ 和新老客 ⑨ 的一系列扩展信息,做提前过滤和列剪裁也都可以并行来完成。
当我们数据链路长的时候,一些产出时间晚、产出时间不稳定的数据,放到执行链路的末尾来最大程度的降低其带来的影响,也可以为其预留更多的 Buffer 时间来执行。
埋点曝光链路 ③ 和主流程搜索请求 ④ 链路受外部依赖不稳定和自身数据倾斜的影响,产出时间晚一直拖累着 MID 表;
将这两条链路的产出数据后置到 DWD 结果表 ⑩ 中进行使用,加 DWD 结果表的 MR 并行度进行提速,关联任务所在的 Stage 可以在 5~10 分钟完成,要比在 mid 表中等待花费的时间小好多倍。
长尾任务是指在一些列任务中,少数的任务处理的数据量远远大于大多数任务的数据量,使得这少数任务的执行时间显著长于其他任务,成为整个任务的瓶颈。
表中为主流程接口请求数据 ④,随着数据量的增加处理时间出现了非常不合理的翻倍的上涨。
数据量(G) | 处理时间(小时) |
25.4 | 01:15 |
30.9 | 01:33 |
34 | 01:43 |
38.4 | 02:19 |
43.3 | 03:35 |
47.2 | 03:50 |
Stage-6 为 MR 数据写入
在 Hive 中最后一个 Stage 的 MR 任务是用于写表的任务(output task),在输出任务的 MR 中通常情况下 Reduce 阶段不一定是必需的。
如果输出数据可以直接写入 HDFS 或其他存储系统,而无需进行聚合或排序操作,则可以省略 Reduce 阶段,这样可以提高任务的性能。
这种情况的 MR 任务是 map-only 任务,但是如果需要进行聚合或排序操作,则需要执行 Reduce 阶段。
search 任务部分清洗逻辑
上面是任务中的部分代码段,主要为对 ods 日志的一个清洗转化过程,最后并没有聚合和排序操作;
分析这里就比较奇怪了,是什么导致了写表任务出现了排序,我们可以通过执行计划来一探究竟。
查看执行计划
执行计划 Stage-6部分
整体的执行计划非常长我们只取 Stage-6 这个阶段,最为关键的是 Reduce Output Operator 这一步
MapReduce 计算引擎,在 Map 阶段和 Reduce 阶段输出的都是键-值对的形式
value expressions:表示为 Map 阶段输出的值(value)所用的数据列
Map 阶段输出的 key 正是我们分区的字段_ col22:platform _col23:hour,对key进行了正序排列
insert overwrite table dw_hotel_search_di partition(dt=${DATE},platform,hour)
到这里就定位到了我们在写入表的时候,dt 是直接指定的静态分区;platform和 hour 是动态分区,现在动态分区出现了排序的情况并且导致了长尾任务。
Hive 中动态分区的分区个数非常大的时候,会出现 OOM。在每个 task 进行数据写出时为每个分区目录开启一个文件写入器(file writer),数据会先进入缓存区后批量写入。
如我们在刷一个大表的历史数据时,当内存中的文件句柄越来越多的时候数据内存会被逐渐填满,导致 OOM 的发生。
而动态分区排序正是为了解决这个问题,开启动态排序后会对分区 key 进行全局排序,排序后每个 task 内对应一个分区的数据这样有效的解决了打开文件句柄多 OOM 的发生。
但是,同样也引入了一个问题,全局排序后某 Reduce 对应分区中的数据量非常多的时候出现倾斜,执行缓慢。
公司的 hive 组件是默认开启动态分区排序的 "hive.optimize.sort.dynamic.partition=true"。
我们现在 dw_hotel_search_di 表中 platform 通常为 adr 和 ios,hour 是 24 小时。分区个数极少开启的文件写入器不会造成内存被吃满 OOM 的发生,将动态分区排序进行关闭。
窄口径是指通过用户的请求 Trace 把访问过的页面逐级串联起来,去统计用户的转化率。
下图是 L 页流量表通过 Trace 去串联各页面的流程图,TraceID 是用户访问当前页面产生的,TraceLog 为透传的上一个页面的 TraceID,这样就可以逐级串联起来。
如图我们有十个用户访问了 L 页,其中两个不太满意也就不会进入下一个页面 D 页,也就是我们所谓的流失用户,同样用这两个用户的 L 页 Traceid 是在 D 页的 TraceLog 中找不到。
这样就会出现一个问题,十个用户:D 页 TraceID 获取的为空 2 个,B 页TraceID 获取的为空 6 个,O 页 TraceID 获取的为空 7 个;
在进行 JOIN 操作时,如果 JOIN KEY 中包含为空的列,会导致数据倾斜。因为 Hive 会将所有空值都映射到同一个 Reduce 任务中,如果某个键对应的数据量很大,会使这个 Reduce 任务成为瓶颈。
这些空的 TraceID 作为 JOIN KEY 在关联下一级页面的时候会被分配到一个 reduce 中造成数据倾斜,严重拖累任务整体的完成进度。
解决方法是在空值列上追加一个随机“盐”,使相同的空值也能映射到不同的 Reduce 任务,从而缓解数据倾斜的问题。
Hive 支持在多个层面上进行数据压缩选择不同的压缩时机,可获得不同的收益如:
在输出阶段压缩可以减少最终结果数据量
需要根据具体场景选择最佳的压缩策略,我们这次的案例是针对中间表和最终产出表来进行的优化压缩实践
临时表的压缩有两个不同的场景小的维表和大的中间结果表
1、先说小的维度表:
在链路优化中将 DIM :基础信息 (⑧) 的关联逻辑从进行了拆分,从多段的 join 子查询中提了出来。
提前错峰执行缓解瞬时任务堆积造成计算资源压力是一方面原因。
更重要的是提前对数据做了行和列的剪裁,然后对产出临时表做数据的压缩,进一步为了降低数据量从而降低到满足 mapjoin 的阈值;
使其放入到 hashTable 里共享至分布式缓存中,在 map 端完成 JOIN 操作,由 CommonJoin 转化为 MapJoin,极大的提高 join 效率。
当然也需要我们来观察剪裁后的表是否满足 hive.mapjoin.smalltable.filesize 阈值,在差异不大的情况下可以适当对该值调大,默认是非常保守的 25M。
2、然后再说中间结果表:
背景:hive 中的表一般都是 ORC 默认用 Zlib 即使我们不指定压缩格式也会进行自动压缩,通常会有十几倍的压缩效率。
如果我产出的一个中间结果表使用了 ORC 格式,占用存储大小就会大大降低,然后在用这个表去 JOIN 其他表的时候,按数据大小切片后产生的 Task 个数(并行度)就会降低很多倍。
问题分析:L 页流量表的产出任务中就存在了这样的情况,下面是任务的各 Stage 执行情况:
共有 15 个 Stage ,开头 Stage-22 是小表 (319kb) 处理,结尾 Stage-12 是写表 Task 这两个任务较小
Stage:17、18、19 是处理详情、预订、提交三个阶段窄口径的,用的都是UserPath 用户窄口径所以并行度一致
Stage1:是整个关联查询 SQL 主表(也就是最左表的第一个关联查询)的执行阶段
最左表为 L 页中间表使用了 ORC 格式,map 任务在数据切片时候才拆分了 187 个任务,需要 40 多分钟才能执行完成
其他 Stage 的并行度都很匀称,平均 map 在 8K 左右、reduce 在 2.5K 左右,执行时间也无卡点
通常 map 任务个数=表数据存储大小 /mapred.max.split.size ,L 页中间表每天平均 ORC 存储大小为几十G数据,分片大小为 128Mb
解决方案:要想提升任务的并行度,一个是降低切片的大小、另一个是增加表数据存储的大小
增加表存储大小:我们只增大 L 页中间表的存储大小也就是 Stage1 处理的数据量来提个其并行度,其他的 Stage 完全不受影响
我们最终选择增加表存储的大小来提高并行度,具体操作为对L页中间表转为 textfile 来存储并且不使用压缩算法,放大其数据量,从而提高数据切片个数增大 map 任务的并行度。
优化效果如下:在其他 Stage 依然保持均衡,极大的提高了 Stage1 的并行度,优化后 Stage1 从原来的 40 分钟缩短至十多分钟。
文件级 (file):这一级的索引信息记录文件中所有 stripe 的位置信息,以及文件中所存储的每列数据的统计信息;
条带级 (stripe):该级别索引记录每个 stripe 所存储数据的统计信息;
count: 当前 stripe 中该值的个数
hasNull: 是否包括 null
min: 最小值
max: 最大值
以上就是本次分享的所有内容啦!
最后,给大家带来一些岗位招聘信息。
你与驼厂只差一份简历的距离
快扫码投递吧!
划重点啦!
内推投递可添加小助手微信跟进进度呦!