RDD 序列化
我们为了区分RDD的方法和scala集合对象的方法,所以把RDD的方法称为算子,这两者主要区别是:
- 集合对象的方法同一个节点的内存中完成的
- RDD的方法可以将计算逻辑发送到Executor端(分布式节点)实现分布式处理
但要注意的是:从计算的角度, RDD的算子外部的操作都是在Driver端执行的,而算子内部的逻辑代码是在Executor端执行,我们可以通过简单示例,外部内部以及序列化的联系
提示:以下是本篇文章正文内容,下面案例可供参考
案例 1 : foreach打印
代码如下(示例):
def main(args: Array[String]): Unit = {
val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
val sc = new SparkContext(sparkConf)
val rdd = sc.makeRDD(List(1,2,3))
val user = new User()
// SparkException: Task not serializable
// Caused by: java.io.NotSerializableException: com.spark.rdd.operator.action.RDD_Operator_Action$User
// RDD算子中传递的函数是会包含闭包操作,那么就会进行检测功能
// 闭包检测
rdd.foreach(
num => {
println("age = " + (user.age + num))
}
)
sc.stop()
}
class User {
var age : Int = 30
}
分析:
- 创建了一个类User,及类User有一个属性age
- 在main函数中,创建了一个类对象及一个rdd
- 用rdd算子foreach分布式实现 user.age 的累加及打印,因为是分布式计算,所以输出的顺序不一定是31,32,33
- 但运行阶段报错,User类未序列化
- 原因也很简单,因为类User是在Driver端(算子外部的操作)创建的,但用算子foreach时,age的累加和打印操作属于算子内部的逻辑代码,是在Executor端执行,如果需要在Executor端操作User的属性或方法,那么就需要把Driver端的User传到Executor端,要想将对象在不同端进行操作,传输于网络,就必须流化,为了避免读写操作时会引发一些问题,而序列化机制就是为了解决这些问题;网络中传输,以流的形式传递内容,不能够直接将对象传递
解决方法:
代码如下(示例):
// 方法1: 定义类时混入特质
class User extends Serializable {
var age : Int = 30
}
// 方法2: 样例类在编译时,会自动混入序列化特质(实现可序列化接口)
case class User() {
var age : Int = 30
}
结果:
age = 32
age = 33
age = 31
通过逐行观察代码,会有小伙伴发现,如果将rdd中没有元素,即不能够用foreach进行遍历,那就不会用到 user.age 了,这会怎样呢?
val rdd = sc.makeRDD(List[Int]())
结果还是一样,会报错,User 未序列化,出现此结果的原因:
- scala中有一个闭包的概念,闭包就是一个函数与其相关的引用环境组合的一个整体,返回一个匿名函数,而这个匿名函数与这个引用环境形成一个闭包;其实可以这样理解,就是类似对象与属性的关系,把这个函数跟引用的外部环境进行绑定,形成一个闭包,变成一个整体
- foreach方法使用的是函数式编程,传进去一个匿名函数,而RDD算子中传递的函数是会包含闭包操作,那么就会进行检测功能,即闭包检测
- 而在进行闭包检测时,会进行序列化的检查
private[spark] def clean[F <: AnyRef](f: F, checkSerializable: Boolean = true): F = { ClosureCleaner.clean(f, checkSerializable) f }
- 所以即使没有元素进行遍历,但在编译时,会提前进行闭包检测,然后只需判断检查传进来的变量有没有序列化,而不需要去执行,而在本次实例中,引用到了外部环境变量 user.age,所以即使没有执行,但如果User类没有序列化,也会报错,我们称之为闭包检测
案例2: 过滤
关于RDD序列化,再提供一个了例子:
代码如下(示例):
def main(args: Array[String]): Unit = {
val sparConf = new SparkConf().setMaster("local").setAppName("WordCount")
val sc = new SparkContext(sparConf)
val rdd: RDD[String] = sc.makeRDD(Array("hello world", "hello spark", "hive", "scala"))
val search = new Search("h")
// search.getMatch1(rdd).collect().foreach(println) // Error
search.getMatch2(rdd).collect().foreach(println)
sc.stop()
}
// 查询对象
// 类的构造参数其实是类的属性, 构造参数需要进行闭包检测,其实就等同于类进行闭包检测
class Search(query:String){
def isMatch(s: String): Boolean = {
s.contains(this.query)
}
// 函数序列化案例
def getMatch1 (rdd: RDD[String]): RDD[String] = {
rdd.filter(isMatch)
}
// 属性序列化案例
def getMatch2(rdd: RDD[String]): RDD[String] = {
/*
将属性query变成方法的局部变量,与类Search剥离,因为字符串本身是已经实现了Serializable接口,已经是序列化了的
*/
val s = query
rdd.filter(x => x.contains(s))
}
}
分析:
- 代码功能是实现筛选出以“h”开头的字符串
- main函数中,如果在类Search未混入序列化特质时,调用getMatch1()方法,则会报相同的类Search未序列化错误,因为isMatch()引用了Search的属性query
- 调用getMatch2()方法,并用一个局部变量s去引用当前对象的query属性,将s作为参数传入到rdd的算子中,此时即使类Search未序列化,也能如期实现功能,这是因为s是一个局部变量,与类Search无关,并且s引用的query是一个字符串,字符串是已实现序列化接口的,所以是可以通过闭包检测的,不会报错
结果:
hello world
hello spark
hive
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]))
val sc = new SparkContext(conf)
sc.stop()
总结
RDD序列化,在类的序列化报错时,即用到类的属性方法等,提供两种思路:
- 将类序列化,比如混入特质,或变成样例类case
- 将属性赋值给方法中的一个临时变量,改变生命周期,形成闭包,即将这个属性与类剥离
文章仅作知识点的记录,欢迎大家指出错误,一起探讨~~~