由于上项目的模块计算部分依赖于spark,那么在spark的使用上,需要针对不同规模和形式的数据,都要能最大限度的做到数据变换,模型计算等计算的稳定性支持。这也是elemental目前急需优化的瓶颈所在。这里,我们针对下面的场景所遇到的问题进行一部分探讨:
在数据规模过大,无法cache到memory上
- DataFrame在transform多次后,进行action后,生成的Physical Plan过长
- DataFrame和RDD在transform多次后,进行action后,生成的DAG过长
理论
- Cache
- 当我们对DataFrame进行cache操作时,我们会对到目前生成的DataFrame的logicalPlan进行一次执行,并将每个partition计算完的结果保存为CachedBatch的格式,最终保存到CachedData的List数组中。对应的RDD也变为PersistRDD格式。
- 而经过Cache的DataFrame,在后续的计算中,
- 正常情况下在数据规模不大的情况下,我们只需要对DataFrame和RDD进行cache操作,就可以解决前面提到的问题。当然我们也可以选择Cache到磁盘上来应对数据规模比较大的情况。
- CheckPoint
- 当我们对RDD进行Checkpoint操作时,只是暂时在此加上标记,表明该RDD需要被CheckPoint。
- 而在后续进行action操作时,在runJob计算完RDD之后,才会进行doCheckpoint的活动,也就是具体RDD进行Checkpoint实际的过程。在这个过程中,RDD的生成过程实际上要进行第二次的计算。
- DataFrame在进行CheckPoint的操作中,默认参数eager为true,也就是对应的InternalRdd在checkpoint函数之后,会默认进行一次简单的count的action操作,这样就完成了DataFrame数据的checkpoint,当然后续还会清理掉相对应的前序依赖,以达到降低DAG和physicalPlan复杂度的目的。
探寻
测试步骤如下:
val df1 = df.withColumn
val df2 = df1.groupBy.sum
val df3 = df2.withColumn
我们用进行count的过程作为DAG与plan分析的样本(进行checkpoint的操作的,分别对df1,df2,df3进行checkpoint之后,在进行count过程)
1.DataFrame进行checkpoint对比
没有使用checkpoint的情况下,logicPlan变化为
Aggregate [count(1) AS count#22L]
+- Project [id#3, double#4, plusOne#5, (id#3 % 9) AS idType#10]
+- LogicalRDD [id#3, double#4, plusOne#5]
Aggregate [count(1) AS count#41L]
+- Aggregate [idType#10], [idType#10, sum(cast(double#4 as bigint)) AS sum(double)#33L]
+- Project [id#3, double#4, plusOne#5, (id#3 % 9) AS idType#10]
+- LogicalRDD [id#3, double#4, plusOne#5]
Aggregate [count(1) AS count#56L]
+- Project [idType#10, sum(double)#33L, (cast(sum(double)#33L as double) / cast(10 as double)) AS rst#46]
+- Aggregate [idType#10], [idType#10, sum(cast(double#4 as bigint)) AS sum(double)#33L]
+- Project [id#3, double#4, plusOne#5, (id#3 % 9) AS idType#10]
+- LogicalRDD [id#3, double#4, plusOne#5]
由于是依赖关系,出现上述情况是合理的。
那么,在使用checkpoint之后
Aggregate [count(1) AS count#83L]
+- Project [id#64, double#65, plusOne#66, (id#64 % 9) AS idType#71]
+- LogicalRDD [id#64, double#65, plusOne#66]
Aggregate [count(1) AS count#108L]
+- Aggregate [idType#71], [idType#71, sum(cast(double#65 as bigint)) AS sum(double)#100L]
+- LogicalRDD [id#64, double#65, plusOne#66, idType#71]
Aggregate [count(1) AS count#129L]
+- Project [idType#71, sum(double)#100L, (cast(sum(double)#100L as double) / cast(10 as double)) AS rst#119]
+- LogicalRDD [idType#71, sum(double)#100L]
很显然,LogicalPlan得到了抑制。与此相对应的PhysicalPlan也会得到缩减。
DAG的变化,这里只枚举df3的过程就可以说明问题
图1.没有checkpoint情况下,df3进行count的DAG
图2.在df2进行checkpoint情况下,df3进行count的DAG
对比,可以知道Stage已经得到了减少(图1在PhysicalPlan优化后才为3个Stage,实际上LogicalPlan已经为4个Stage),而且图1是从最开的df走流程下来的,而图2是直接从前面一个df2的checkpoint点出来的。
2.RDD进行checkpoint对比
使用RDD进行上述类似的操作,DAG的缩减也是一致,这里我们可以看一下RDD的recursive dependencies信息对比
(4) MapPartitionsRDD[64] at map at AlexTestJob.scala:115 []
| ShuffledRDD[63] at groupBy at AlexTestJob.scala:115 []
+-(4) MapPartitionsRDD[62] at groupBy at AlexTestJob.scala:115 []
| MapPartitionsRDD[61] at map at AlexTestJob.scala:109 []
| ParallelCollectionRDD[60] at parallelize at AlexTestJob.scala:106 []
(4) MapPartitionsRDD[71] at map at AlexTestJob.scala:141 []
| ReliableCheckpointRDD[72] at count at AlexTestJob.scala:147 []
这是rdd2过程后的是否使用checkpoint的toDebugString的对比
3.DataFrame在循环体进行checkpoint对比
这里我们采用下面的逻辑代码进行测试
var df
(0 until 5).foreach {idx=>
df = df.withColumn(s"addCol_$idx",df.col("id")+idx)
}
LogicalPlan对比分析
'Project [*, (id#97 + 4) AS idType_4#134]
+- Project [id#97, double#98, plusOne#99, idType_0#104, idType_1#110, idType_2#117, (id#97 + 3) AS idType_3#125]
+- Project [id#97, double#98, plusOne#99, idType_0#104, idType_1#110, (id#97 + 2) AS idType_2#117]
+- Project [id#97, double#98, plusOne#99, idType_0#104, (id#97 + 1) AS idType_1#110]
+- Project [id#97, double#98, plusOne#99, (id#97 + 0) AS idType_0#104]
+- LogicalRDD [id#97, double#98, plusOne#99]
每次迭代都进行checkpoint之后
'Project [*, (id#3 + 4) AS idType_4#75]
+- LogicalRDD [id#3, double#4, plusOne#5, idType_0#15, idType_1#27, idType_2#41, idType_3#57]
这样的缩减,对于在模型计算过程中,多次迭代缩减DAG过程都存在实际意义
4.checkpoint与cache(DISK_ONLY)
cache只保存在DISK_ONLY可以理解为localCheckpoint的过程
结论
- 无论cache还是checkpoint操作,本质上是部分保存中间结果,减少后续过程重复计算。cache更倾向于保存比较频繁使用的,数据规模比较小的数据,且保存在内存中意义更大一些。checkpoint则相对于言没有数据规模的限制。
- checkpoint一次,会进行2次计算,这是额外开销。
- cache到磁盘上,是1次计算,但该次cache的结果仅能在该driver上运行的程序调用。实际上在elemental中是符合使用的。只是需要考虑对应的driver所在的机器的磁盘空间是否足够。
- checkpoint到alluxio,算是方便统一管理。checkpoint更大的优势是在于SparkStream上的优势,具有可恢复性。
- 一个RDD无论是否被标记为checkpoint,只要进行过实际性质action操作之后,该RDD就会被标记为已经checkpoint。例如:
RDD.checkpoint
RDD.count
这样是可以成功checkpoint的,但是:
RDD.count
RDD.checkpoint
RDD.count
无法被checkpoint。因此,选择checkpoint,之后马上action。最佳方案为:
RDD.checkpoint
RDD.persist(DISK_ONLY)