Spark-利用持久化分批处理大数据集

36 阅读2分钟

在一些特殊的需求里,比如统计最近一个月或者时间跨度更大的数据,数据量的级别可能达到TB级。增大资源当然可以处理这么大的数据量,但如果客户集群资源有限,不允许使用这么大的资源,该怎么办呢?可以利用持久化分批处理。

以下是在个人测试环境中模拟的两种情况:

  1. 全量处理
val uCols = Range(1, 97).map(i => s"U$i")
val sdf19 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
val sdf10 = new SimpleDateFormat("yyyy-MM-dd")

var res: DataFrame = null
for (i <- Range(1, 13)) {
	println(sdf19.format(System.currentTimeMillis()) + s" 第 $i 次")
	val date = sdf10.format(sdf10.parse("2025-09-24").getTime - (i - 1) * 24 * 60 * 60 * 1000L)
	val currRes = spark.sql(s"select id, phase, ${uCols.mkString(",")} from table_name where data_date = '$date'")
	if (res != null) {
		res = res.as("t1")
		.join(currRes.as("t2"), Seq("id", "phase"), "left")
		.select(Seq($"id", $"phase") ++ uCols.map(c => (col(s"t1.$c") + col(s"t2.$c")).as(c)): _*)
	} else {
		res = currRes
	}
}
res.persist()
val resCnt = res.count()
println(sdf19.format(System.currentTimeMillis()) + s" ====================== resCnt: $resCnt")
res.show()

全量处理的模式,reduce阶段要同时处理12天的数据,内存不足时,非常容易堆空间溢出。

这些map阶段的stage,虽然UI界面显示的提交时间是相同的,但实际上是串行执行的,一个cpu同时只处理一个task。

reduce阶段同时处理12天数据,内存溢出。

  1. 使用持久化分批处理
val uCols = Range(1, 97).map(i => s"U$i")
val sdf19 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
val sdf10 = new SimpleDateFormat("yyyy-MM-dd")

var res: DataFrame = null
var tmp: DataFrame = null
for (i <- Range(1, 13)) {
	println(sdf19.format(System.currentTimeMillis()) + s" 第 $i 次")
	val date = sdf10.format(sdf10.parse("2025-09-24").getTime - (i - 1) * 24 * 60 * 60 * 1000L)
	val currRes = spark.sql(s"select id, phase, ${uCols.mkString(",")} from table_name where data_date = '$date'")
	if (res != null) {
		res = res.as("t1")
		.join(currRes.as("t2"), Seq("id", "phase"), "left")
		.select(Seq($"id", $"phase") ++ uCols.map(c => (col(s"t1.$c") + col(s"t2.$c")).as(c)): _*)
	} else {
		res = currRes
	}
	if (i % 3 == 0) {
		res.persist(StorageLevel.MEMORY_AND_DISK)
		val resCnt = res.count()
		println(sdf19.format(System.currentTimeMillis()) + s" ====================== $i resCnt: $resCnt")
		if (tmp != null) tmp.unpersist()
		tmp = res
	}
}
res.persist()
val resCnt = res.count()
println(sdf19.format(System.currentTimeMillis()) + s" ====================== resCnt: $resCnt")
res.show()

利用持久化分批处理,核心理念就是用时间换空间。第一次处理三天的数据,然后用行动算子对结果触发持久化;第二次处理三天的数据加第一次的结果,同样用行动算子对结果触发持久化,然后再对第一次的结果解除持久化;从第三次计算开始,每次计算花费的时间、占用的内存空间、磁盘空间都与第二次计算相同。

总共触发了5个Job。

第二次计算,跳过了前三天的数据,直接使用第一次计算的结果,就是stage 11 的Input部分。

第三次计算,计算时长、数据量与第二次计算基本一致。

如果不想改代码,全量处理还可以采用增大shuffle分区数的方式,这样reduce阶段每个task处理的数据量就小了,更容易成功。但全量处理还有个缺点,就是map阶段数据要落盘,会占用大量的磁盘空间,这部分空间会一直持续到reduce阶段结束才会被释放。