RDD 序列化

本文详细解析了Spark中RDD序列化的问题,包括为何RDD操作需要序列化以及如何处理序列化错误。通过案例分析了foreach和过滤操作中遇到的序列化问题,提出两种解决方案:使类实现Serializable接口或使用局部变量。此外,还介绍了Kryo序列化框架,它比Java序列化更快,是Spark性能优化的重要手段。
摘要由CSDN通过智能技术生成

RDD 序列化

我们为了区分RDD的方法和scala集合对象的方法,所以把RDD的方法称为算子,这两者主要区别是:

  1. 集合对象的方法同一个节点的内存中完成的
  2. 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
}

分析:

  1. 创建了一个类User,及类User有一个属性age
  2. 在main函数中,创建了一个类对象及一个rdd
  3. 用rdd算子foreach分布式实现 user.age 的累加及打印,因为是分布式计算,所以输出的顺序不一定是31,32,33
  4. 但运行阶段报错,User类未序列化
  5. 原因也很简单,因为类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 未序列化,出现此结果的原因:

  1. scala中有一个闭包的概念,闭包就是一个函数与其相关的引用环境组合的一个整体,返回一个匿名函数,而这个匿名函数与这个引用环境形成一个闭包;其实可以这样理解,就是类似对象与属性的关系,把这个函数跟引用的外部环境进行绑定,形成一个闭包,变成一个整体
  2. foreach方法使用的是函数式编程,传进去一个匿名函数,而RDD算子中传递的函数是会包含闭包操作,那么就会进行检测功能,即闭包检测
  3. 而在进行闭包检测时,会进行序列化的检查
    private[spark] def clean[F <: AnyRef](f: F, checkSerializable: Boolean = true): F = {
        ClosureCleaner.clean(f, checkSerializable)
        f
      }
    
  4. 所以即使没有元素进行遍历,但在编译时,会提前进行闭包检测,然后只需判断检查传进来的变量有没有序列化,而不需要去执行,而在本次实例中,引用到了外部环境变量 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))
        }
    }

分析:

  1. 代码功能是实现筛选出以“h”开头的字符串
  2. main函数中,如果在类Search未混入序列化特质时,调用getMatch1()方法,则会报相同的类Search未序列化错误,因为isMatch()引用了Search的属性query
  3. 调用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序列化,在类的序列化报错时,即用到类的属性方法等,提供两种思路:

  1. 将类序列化,比如混入特质,或变成样例类case
  2. 将属性赋值给方法中的一个临时变量,改变生命周期,形成闭包,即将这个属性与类剥离

文章仅作知识点的记录,欢迎大家指出错误,一起探讨~~~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值