一.引言
场景 : 商品 product 每日总销售记录量级 亿 级别起,去重 product 量大概 万 级别。每个商品有一个 state 标识其状态,该状态共3个值,分别为 "A", "B","C"。
统计:
(1) 三个 state 下 product 的总量
(2) 对应 state 下 product 去重后的量
第一个统计很简单,构造 sc.longAccumulator 遍历统计数量即可,第二个统计每个 state 下有亿级别的 value ,去重时有严重的数据倾斜且数据去重规模很大,亿级别去重至万级别,不断优化代码最终达到快速大数据去重。
二. 尝试
1.GroupBy + ToSet
第一版按最基本的去重写法实现,构造 (state, product) 的 pairRDD,随后按 state groupBy 分组,将得到的全部 product toSet 达到去重的效果,最终获取 state + set.size 达到统计目的:
val re = sc.textFile(input).map(line => {
val info = line.split("\t")
val state = info(0)
val productId = info(1)
// 全局计数
countMap(state).add(1L)
// 构建 state+product 的 PairRDD
(state, productId)
}).groupBy(_._1).map(info => {
val state = info._1
// toSet.size
val productSet = info._2.map(kv => {
val productId = kv._2
productId
}).toArray.toSet
state + "_" + productSet.size
}).collect()
该方法会在每个 State 下构造一个亿级别的 Product 的 iterator,toArray.toSet.size 方法不仅占用大量内存且执行效率低,执行时间 20 min + 未结束,kill 掉任务重新优化。
2.GroupBy + RandomIndex + ToSet
因为 state 只有 ABC 三种可能,所以最后全部压力分摊在 3 个节点上,构造 PairRDD 时可以给 state 加上随机索引,从而将任务分散,获得多个小的 Set 再将 Set 合并即可,相当于分治:
val re = sc.textFile(input).map(line => {
val info = line.split("\t")
val state = info(0)
val productId = info(1)
// 全局计数
countMap(state).add(1L)
// 构建 state + randomIndex + product 的 PairRDD
(state + "_" + random.nextInt(100) , productId)
}).groupBy(_._1).map(info => {
val state = info._1.split("_")(0)
// 分治
val productSet = info._2.map(kv => {
val productId = kv._2
productId
}).toArray.toSet
(state, productSet)
}).groupBy(_._1).map(info => {
val state = info._1
val tmpSet = mutable.HashSet[String]()
// 合并
info._2.foreach(kv => {
tmpSet ++= kv._2
})
state + ":" + tmpSet.size
}).collect()
该方法会将原始数据分为 3 x 100 份,缩减了每个 key 要处理的 productId 的量,最后再去重随机索引再 groupBy 一次,汇总得到结果,执行时间 5 min,优化效果显著,继续优化。
3.Distinct + GroupBy (推荐👍 )
上一步方案通过 randomIndex 将数据量分治,减少的百分比和 random 的数值成正比,但是在数据量很大的情况下,分治的每个 key 对应的 value 量还是很大,所以简单的去重执行 5min +,这次将 groupBy 改为 distinct,先去重得到 万 级别数据量,再 GroupBy,此时的数据量本机也可轻松完成:
val re = sc.textFile(input).map(line => {
val info = line.split("\t")
val state = info(0)
val productId = info(1)
sendMap(state).add(1L)
state + "_" + mid
}).distinct().map(info => {
val state = info.split("_")(0)
val productId = info.split("_")(1)
(state, productId)
}).groupBy(_._1).map(info => {
val state = info._1
val num = info._2.map(_._2).toSet.size
state + ":" + num
}).collect()
distinct 去重其实就是 reduceByKey ,reduceByKey 会先在自己的节点汇总再送到统一处理节点,所以思想也是分治,且速度优于 GroupByKey,因为 GroupByKey 是把所有数据拉到一个节点再开始处理,改为上述方法后,运行时间 1min 左右,达到优于预期。
map(x => (x, null)).reduceByKey((x, y) => x, numPartitions).map(_._1)