文章目录
SparkRDD - 基础编程
RDD序列化
闭包检测
- 从计算的角度,算子以外的代码都是在Driver端执行,算子里面的代码都是在Executor端执行。那么在scala的函数式编程中,就会导致算子内经常会用到算子外的数据,这样就形成了闭合的效果。
- 如果使用的算子外的数据无法序列化,就意味着无法传值给Executor端执行,就会发生错误。所以,需要在执行任务计算前,检测闭包内的对象是否可以进行序列化,这个操作我们称之为闭包检测。
- Scala2.12版本后闭包编译方式发生了改变。
源码说明
我们执行collect等action算子,底层都会调用此方法来提交job。在下面的代码中可以看到,在提交job之前会进行clean操作,闭包检测就在里面。
// SparkContext
def runJob[T, U: ClassTag](
rdd: RDD[T],
func: Iterator[T] => U,
partitions: Seq[Int]): Array[U] = {
val cleanedFunc = clean(func)
runJob(rdd, (ctx: TaskContext, it: Iterator[T]) => cleanedFunc(it), partitions)
}
我们点击clean(func),进入到如下代码:
// SparkContext
/**
* 清理一个闭包,使其准备好序列化并发送到tasks(移除$outer中未引用的变量,更新REPL变量)
* 如果设置了checkSerializable,clean还将主动检测f是否可序列化,如果不可序列化,则抛出SparkException
*
* @param f 要清洗的闭包
* @param checkSerializable 是否立即检查f的可序列化性
* @throws SparkException 如果设置了checkSerializable但f不可序列化
* @return 清洗后的闭包
*/
private[spark] def clean[F <: AnyRef](f: F, checkSerializable: Boolean = true): F = {
ClosureCleaner.clean(f, checkSerializable)
f
}
我们再进入到ClosureCleaner.clean方法中:
/**
* 在适当的地方清洗给定的闭包。
* 更具体地说,只要给定的闭包没有显式地引用不可序列化的对象,它就会呈现为可序列化的。
*
* @param closure 要清洗的闭包
* @param checkSerializable 清洗后是否验证闭包是可序列化的
* @param cleanTransitively 是否传递清除封闭闭包
*/
def clean(
closure: AnyRef,
checkSerializable: Boolean = true,
cleanTransitively: Boolean = true): Unit = {
clean(closure, checkSerializable, cleanTransitively, Map.empty)
}
继续往里面走,由于代码篇幅过长,所以就摘了核心的那句:
/**
* Helper方法来就地清洗给定的闭包。
*
* 其机制是遍历封闭闭包的层次结构,并将启动闭包过程中,实际上没有使用但仍包含在已编译的匿名类中的所有引用清空。
* 注意,简单地对封闭闭包进行就地转换是不安全的,因为其他代码路径可能依赖于它们。
* 相反,我们克隆每个封闭闭包并相应地设置父指针。
*
* 默认情况下,闭包以传递方式清除。
* 这意味着我们检测封闭对象是否被开始对象实际引用,无论是直接引用还是传递引用,如果不是,则从层次结构中分离这些闭包。
* 换句话说,除了清空未使用的字段引用之外,我们还清空了任意父指针,这些父指针引用的封闭对象实际上不是开始闭包需要的。
* 我们通过跟踪内部闭包最终调用的所有方法的树来确定传递性,并记录流程中引用的所有字段。
*
* 例如,在下面的场景中,传递式清理是必需的:
* class SomethingNotSerializable {
* def someValue = 1
* def scope(name: String)(body: => Unit) = body
* def someMethod(): Unit = scope("one") {
* def x = someValue
* def y = 2
* scope("two") { println(y + 1) }
* }
* }
*
* 在本例中,scope "two" 不可序列化,因为它引用了scope "one",后者引用了SomethingNotSerializable。
* 但是,请注意,scope "two" 的主体实际上并不依赖于SomethingNotSerializable。
* 这意味着我们可以安全地将克隆的scope "one" 的父指针空出来,
* 并将其设置为scope "two"的父指针,这样scope "two"就不再以可传递方式引用SomethingNotSerializable。
*
* @param func 要清理的开始闭包
* @param checkSerializable 清理后是否验证闭包是可序列化的
* @param cleanTransitively 是否传递地清理封闭闭包
* @param accessedFields 从类到开始闭包访问的类的一组字段的map
*/
private def clean(
func: AnyRef,
checkSerializable: Boolean,
cleanTransitively: Boolean,
accessedFields: Map[Class[_], Set[String]]): Unit = {
。。。
if (checkSerializable) {
ensureSerializable(func)
}
}
终于看到确认序列化的方法了:
private def ensureSerializable(func: AnyRef) {
try {
if (SparkEnv.get != null) {
SparkEnv.get.closureSerializer.newInstance().serialize(func)
}
} catch {
case ex: Exception => throw new SparkException("Task not serializable", ex)
}
}
可以看见:如果不可序列化就会throw new SparkException(“Task not serializable”, ex)
至此,闭包检测结束。回到上~层继续runJob
Kryo序列化框架
参考地址: https://github.com/EsotericSoftware/kryo
- Java的序列化能够序列化任何的类。但是比较重(字节多),序列化后,对象的提交也比较大。
- Spark出于性能的考虑,Spark2.0开始支持另外一种Kryo序列化机制。Kryo速度是Serializable的10倍。
- 当RDD在Shuffle数据的时候,简单数据类型、数组和字符串类型已经在Spark内部使用Kryo来序列化。
注意:即使使用Kryo序列化,也要继承Serializable接口。
val conf: SparkConf = new SparkConf()
.setAppName("SerDemo")
.setMaster("local[*]")
// 替换默认的序列化机制
.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
// 注册需要使用 kryo 序列化的自定义类
.registerKryoClasses(Array(classOf[Searcher]))
RDD依赖关系
RDD血缘关系
- RDD只支持粗粒度转换,即在大量记录上执行的单个操作。
- 将创建RDD的一系列Lineage(血统)记录下来,以便恢复丢失的分区。
- RDD的Linage会记录RDD的元数据信息和转换行为(数据源&计算),当该RDD的部分分区数据丢失时,它可以根据这些信息来重新运算和恢复丢失的数据分区。
案例:
def main(args: Array[String]): Unit = {
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD Lineage")
val sc = new SparkContext(sparkConf)
val rdd: RDD[String] = sc.makeRDD(List("hello world", "hello spark"))
println(rdd.toDebugString)
println("-------------------------")
val wordRDD: RDD[String] = rdd.flatMap(_.split(" "))
println(wordRDD.toDebugString)
println("-------------------------")
val mapRDD: RDD[(String, Int)] = wordRDD.map((_, 1))
println(mapRDD.toDebugString)
println("-------------------------")
val resRDD: RDD[(String, Int)] = mapRDD.reduceByKey(_+_)
println(resRDD.toDebugString)
resRDD.collect()
}
运行结果:
(8) ParallelCollectionRDD[0] at makeRDD at LineageTest.scala:16 []
-------------------------
(8) MapPartitionsRDD[1] at flatMap at LineageTest.scala:20 []
| ParallelCollectionRDD[0] at makeRDD at LineageTest.scala:16 []
-------------------------
(8) MapPartitionsRDD[2] at map at LineageTest.scala:24 []
| MapPartitionsRDD[1] at flatMap at LineageTest.scala:20 []
| ParallelCollectionRDD[0] at makeRDD at LineageTest.scala:16 []
-------------------------
(8) ShuffledRDD[3] at reduceByKey at LineageTest.scala:28 []
+-(8) MapPartitionsRDD[2] at map at LineageTest.scala:24 []
| MapPartitionsRDD[1] at flatMap at LineageTest.scala:20 []
| ParallelCollectionRDD[0] at makeRDD at LineageTest.scala:16 []
RDD依赖关系
- 这里所谓的依赖关系,其实就是RDD之间的关系
案例:
def main(args: Array[String]): Unit = {
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD Lineage")
val sc = new SparkContext(sparkConf)
val rdd: RDD[String] = sc.makeRDD(List("hello world", "hello spark"))
println(rdd.dependencies)
println("-------------------------")
val wordRDD: RDD[String] = rdd.flatMap(_.split(" "))
println(wordRDD.dependencies)
println("-------------------------")
val mapRDD: RDD[(String, Int)] = wordRDD.map((_, 1))
println(mapRDD.dependencies)
println("-------------------------")
val resRDD: RDD[(Strin