RDD 序列化

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

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. 将属性赋值给方法中的一个临时变量,改变生命周期,形成闭包,即将这个属性与类剥离

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

RDD持久化是Spark非常重要的一个功能特性,下面从原理、方法及应用场景三方面进行介绍: - **原理**:Spark可将RDD持久化到内存中,当对RDD执行持久化操作时,每个节点会把自己操作的RDD的partition持久化到内存里,在后续对该RDD的反复使用中,直接使用内存缓存的partition。这样就在针对一个RDD反复执行多个操作的场景下,只需对RDD计算一次,后续直接使用该RDD,无需反复计算。而且Spark的持久化机制自动容错,若持久化的RDD的任何partition丢失,Spark会自动通过其源RDD,使用transformation操作重新计算该partition。此外,Spark自己也会在shuffle操作时进行数据的持久化,像写入磁盘,这样能在节点失败时避免重新计算整个过程[^2][^3]。 - **方法**:要持久化一个RDD,可调用其cache()或者persist()方法。在该RDD第一次被计算出来时,就会直接缓存在每个节点中。cache()是persist()的简化方式,cache()底层调用的是persist()的无参版本,即调用persist(MEMORY_ONLY),将数据持久化到内存中。若需从内存中清除缓存,可使用unpersist()方法。RDD持久化还能手动选择不同策略,在调用persist()时传入对应的StorageLevel即可,比如可将RDD持久化在内存中、持久化到磁盘上、使用序列化的方式持久化,对持久化的数据进行多路复用[^3]。此外,cache()或者persist()的使用有规则,必须在transformation或者textFile等创建了一个RDD之后,直接连续调用cache()或persist()才有效,若先创建一个RDD,然后单独另起一行执行cache()或persist()方法,不仅无用还会报错[^5]。 - **应用场景**:对于迭代式算法和快速交互式应用,RDD持久化非常重要。因为在Spark中,RDD采用惰性求值机制,每次遇到action操作,Spark都会从头重新计算RDD及其所有的依赖,这对于迭代计算而言代价很大,迭代计算经常需多次重复使用同一组数据,使用RDD持久化可避免这种重复计算的开销。在某些场景下,巧妙使用RDD持久化,可对spark应用程序的性能有很大提升,甚至能将性能提升10倍[^1][^3][^4]。 以下是一个简单的Java代码示例,展示cache()的使用: ```java import org.apache.spark.SparkConf; import org.apache.spark.api.java.JavaRDD; import org.apache.spark.api.java.JavaSparkContext; public class RDDPersistenceExample { public static void main(String[] args) { SparkConf conf = new SparkConf().setAppName("RDDPersistenceExample").setMaster("local"); JavaSparkContext sc = new JavaSparkContext(conf); // 创建RDD并进行持久化 JavaRDD<String> lines = sc.textFile("E:\\server.log").cache(); long beginTime = System.currentTimeMillis(); System.out.println(lines.count()); long endTime = System.currentTimeMillis(); System.out.println("cost " + (endTime - beginTime) + " milliseconds."); beginTime = System.currentTimeMillis(); System.out.println(lines.count()); endTime = System.currentTimeMillis(); System.out.println("cost " + (endTime - beginTime) + " milliseconds."); sc.stop(); } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值