转载地址:https://blog.csdn.net/JIESA/article/details/79996053
https://blog.csdn.net/weixin_42653621/article/details/82534820
在Spark应用开发中,很容易出现如下报错:
org.apache.spark.SparkException: Task not serializable
at org.apache.spark.util.ClosureCleaner$.ensureSerializable(ClosureCleaner.scala:304)
at org.apache.spark.util.ClosureCleaner$.org$apache$spark$util$ClosureCleaner$$clean(ClosureCleaner.scala:294)
at org.apache.spark.util.ClosureCleaner$.clean(ClosureCleaner.scala:122)
at org.apache.spark.SparkContext.clean(SparkContext.scala:2058)
...
Caused by: java.io.NotSerializableException
该报错意思是用户代码的transformation操作中包含不可序列化的对象引用。
本文主要从以下三个方面解释Spark 应用中序列化问题 。
1、Java序列化含义?
2、Spark代码为什么需要序列化?
3、如何解决Spark序列化问题?
1、Java序列化含义?
Spark是基于JVM运行的进行,其序列化必然遵守Java的序列化规则。
序列化就是指将一个对象转化为二进制的byte流(注意,不是bit流),然后以文件的方式进行保存或通过网络传输,等待被反序列化读取出来。序列化常被用于数据存取和通信过程中。
对于java应用实现序列化一般方法:
-
class实现序列化操作是让class 实现Serializable接口,但实现该接口不保证该class一定可以序列化,因为序列化必须保证该class引用的所有属性可以序列化。
-
这里需要明白,static和transient修饰的变量不会被序列化,这也是解决序列化问题的方法之一,让不能序列化的引用用static和transient来修饰。(static修饰的是类的状态,而不是对象状态,所以不存在序列化问题。transient修饰的变量,是不会被序列化到文件中,在被反序列化后,transient变量的值被设为初始值,如int是0,对象是null)
-
此外还可以实现readObject()方法和writeObject()方法来自定义实现序列化。(具体用例见参考链接)
2、Spark的transformation操作为什么需要序列化?
Spark是分布式执行引擎,其核心抽象是弹性分布式数据集RDD,其代表了分布在不同节点的数据。Spark的计算是在executor上分布式执行的,故用户开发的关于RDD的map,flatMap,reduceByKey等transformation 操作(闭包)有如下执行过程:
1. 代码中对象在driver本地序列化
2. 对象序列化后传输到远程executor节点
3. 远程executor节点反序列化对象
4. 最终远程节点执行
故对象在执行中需要序列化通过网络传输,则必须经过序列化过程。
3、如何解决Spark序列化问题?
如果出现NotSerializableException报错,可以在spark-default.xml文件中加入如下参数来开启SerializationDebugger功能类,从而可以在日志中打印出序列化出问题的类和属性信息。
spark.executor.extraJavaOptions -Dsun.io.serialization.extendedDebugInfo=true
spark.driver.extraJavaOption -Dsun.io.serialization.extendedDebugInfo=true
对于scala语言开发,解决序列化问题主要如下几点:
- 在Object中声明对象 (每个class对应有一个Object)
- 如果在闭包中使用SparkContext或者SqlContext,建议使用SparkContext.get() and SQLContext.getActiveOrCreate()
- 使用static或transient修饰不可序列化的属性从而避免序列化。
注:scala语言中,class的Object
对于java语言开发,对于不可序列化对象,如果本身不需要存储或传输,则可使用static或trarnsient修饰;如果需要存储传输,则实现writeObject()/readObject()使用自定义序列化方法。
此外注意
对于Spark Streaming作业,注意哪些操作在driver,哪些操作在executor。因为在driver端(foreachRDD)实例化的对象,很可能不能在foreach中运行,因为对象不能从driver序列化传递到executor端(有些对象有TCP链接,一定不可以序列化)。所以这里一般在foreachPartitions或foreach算子中来实例化对象,这样对象在executor端实例化,没有从driver传输到executor的过程。
dstream.foreachRDD { rdd =>
val where1 = "on the driver"
rdd.foreach { record =>
val where2 = "on different executors"
}
}
}
4、transient关键字及Serializable的序列化与反序列化
4.1、序列化和反序列化
序列化:简单理解就是把程序里面生成的对象以文件的形式保存到本地硬盘中,序列化写入文件的IO是ObjectOutputStream流。
反序列化:就是把序列化的对象文件导入到程序中,并生成为一个对象,供程序使用。反序列化的读取对象文件的IO流是ObjectInputStream。
java中,经典的方式实现对象序列化,序列化的类需要实现Serializable接口和定义serialVersionUID常量。
Serializable接口:只是一个标记,JVM在序列化的时候,会去判断要序列化的对象是否有实现Serializable接口,如果没有实现就会报错,不允许系列化。
serialVersionUID常量:是指JVM在序列化对象的时候,会把这个常量表示序列化对象所属的类的类ID。在反序列化时,反序列化对象的serialVersionUID能匹配上程序里面的类的serialVersionUID时,就判断这个反序列化的对象就是这个类生成的,因此允许反序列化。
还有一点需要注意的,就是如果需要序列化对象本身的属性,那么该属性的类也要实现Serializable接口,否则不能序列化。
4.2、transient关键字
java 的transient关键字的作用是需要实现Serilizable接口,将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会序列化到指定的目的地中。
transient使用小结:
1)一旦变量被transient修饰,变量将不再是对象持久化的一部分,会自动对该字段不进行序列化,该变量内容在序列化后无法获得访问。
2)transient关键字只能修饰变量,而不能修饰方法和类。注意,本地变量是不能被transient关键字修饰的。变量如果是用户自定义类变量,则该类需要实现Serializable接口。
3)被transient关键字修饰的变量不再能被序列化,一个静态变量不管是否被transient修饰,均不能被序列化。
这里有一个简单的实例化的例子:
1、先定义Emp接口实现Serializable接口。
public interface Emp extends Serializable{
public void work();
}
2、Employee对象实现Emp接口,需要序列化这个对象的属性empNo、dept、 name。
注意,这里的属性时Stirng对象,查看源代码知道,String也是实现了Serializable接口,因此可以序列化。
public class Employee implements Emp{
/* 序列化编号 */
private static final long serialVersionUID = 3694902274397865665L;
private String empNo;
//透明化处理,不能持久化
private transient String dept;
private String name;
public Employee(String empNo, String dept, String name) {
super();
this.empNo = empNo;
this.dept = dept;
this.name = name;
}
public Employee() {
super();
}
public void work() {
System.out.println("name:" + this.name + ",empNo:" + this.empNo + ",dept:" + this.dept + " is working.");
}
@Override
public String toString() {
return "Employee [empNo=" + empNo + ", dept=" + dept + ", name=" + name
+ "]";
}
}
3、写一个简单的实例化实现:
public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
String file = "D:\\test\\IO\\serialize\\e1.dat";
Emp e1 = new Employee("001", "技术部", "张三");
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file));
out.writeObject(e1);
out.close();
System.out.println(e1 + "序列化成功");
ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
Emp e2 = (Emp)in.readObject();;
in.close();
System.out.println(e2 + "反序列化成功");
}
这里生成了一个e1.dat的Employee 对象文件。
看看控制台输出的日志:
Employee [empNo=001, dept=技术部, name=张三]序列化成功
Employee [empNo=001, dept=null, name=张三]反序列化成功
这里就发现一个比较严重的问题,就是生成的对象是: [empNo=001, dept=技术部, name=张三],但最终序列化的对象是: [empNo=001, dept=null, name=张三],dept属性不能被序列化。
为什么呢?
这里就是我们上面说到的,由于Employee类中定义的dept属性,使用了transient关键字修饰。使用了transient修饰的对象是不能被序列化的。
那怎么办呢?
总不能把transient关键字删除吧,因为这个是项目中已经投产了,不能随意更改属性信息。
其实SUN还是提供了一些后门给我们处理这种问题的,就是实现了Serializable接口的类。如果需要序列化被transient修饰的属性,可以通过定义:writeObject(java.io.ObjectOutputStream s)序列化对象和readObject(java.io.ObjectInputStream s)反序列化对象。这两个方法来实现序列化和反序列化transient的属性。
还是刚才那个例子,我们在Employee类中添加两个方法,实现序列化transient的属性。
public class Employee implements Emp{
/* 序列化编号 */
private static final long serialVersionUID = 3694902274397865665L;
private String empNo;
//透明化处理,不能持久化
private transient String dept;
private String name;
public Employee(String empNo, String dept, String name) {
super();
this.empNo = empNo;
this.dept = dept;
this.name = name;
}
public Employee() {
super();
}
public void work() {
System.out.println("name:" + this.name + ",empNo:" + this.empNo + ",dept:" + this.dept + " is working.");
}
@Override
public String toString() {
return "Employee [empNo=" + empNo + ", dept=" + dept + ", name=" + name
+ "]";
}
// 强行序列化对象属性的方法
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
s.defaultWriteObject();
s.writeObject(this.dept);
}
// 强行反序列化对象属性的方法
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
this.dept = (String)s.readObject();
}
}
再次执行刚才的main方法,得到的结果是,文件重新生成了:
控制台打印的信息:
Employee [empNo=001, dept=技术部, name=张三]序列化成功
Employee [empNo=001, dept=技术部, name=张三]反序列化成功
这次终于把transient修饰的属性进行序列化和反序列化的操作成功了。
因此在这里稍微总结下吧:
通过实现Serializable接口的,默认是不会序列化transient修饰的属性,除非重写writeObject和readObject两个方法。其实这两个方法就是JVM在执行Serializable接口序列化对象时执行的方法。
其实这里还有一点需要注意的,如果对象有多个属性都被transient修饰了,例如是empNo和dept,那么在重写writeObject和readObject这两个方法时,一定要注意这两个属性的写入顺序和读取顺序必须保持一直,否则会导致反序列化失败。我们的例子中由于empNo和dept都是String对象,因此写错顺序也不会报错,只是会出现属性颠倒。
还是代码展示下吧,例如:
// 把写入和读取的属性顺序错乱了,就会导致反序列化对象的属性也错乱
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
s.defaultWriteObject();
s.writeObject(this.dept);
s.writeObject(this.empNo);
}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
this.empNo = (String)s.readObject();
this.dept = (String)s.readObject();
}
打印的日志,empNo和dept属性错乱了:
Employee [empNo=001, dept=技术部, name=张三]序列化成功
Employee [empNo=技术部, dept=001, name=张三]反序列化成功
好了,本文就先写到这里了,下次有机会在介绍下另外一种序列化和反序列化的接口:Externalizable,其实原理也是一样的,有兴趣的可以自己去尝试。
先把一些序列化和反序列化的结论整理下:
/**
* 这里实现了两种序列化方式,一种是经典的Serializable,另外一种是Externalizable
* 1、Serializable是自动序列化,因此直接编写序列化编号即可,如果需要序列化transient属性,
* 就需要重写writeObject(ObjectOutputStream s)、readObject(ObjectInputStream s),原则与下面一致。
* 2、Externalizable不是自动序列化,需要重写writeExternal(ObjectOutput out)、readExternal(ObjectInput in)方法,
* 另外也是需要对象有空的构造函数。
* 1)writeExternal、readExternal可以指定具体的序列化的属性。写一个序列化一个属性。
* 2)序列化属性和反序列化,都是按照顺序的。
* 3、其实实现这两个接口是一样效果的,只是Serializable可以自动序列化非transient的变量,而Externalizable不会序列化
* 任何变量。两者需要序列化transient都需要重写一些特定方法。
* 而Serializable的方法是JVM默认的方法,Externalizable则是接口定义的方法。
* 4、序列化数组,反序列化也要使用集合对接,否则会报类转义异常。
* 5、更改了类名,无法反序列化。
*/
参考资料:
Avoid NotSerializable Error in Spark Job
spark not serializable problem
Spark Streaming / Tips on Running Streaming Apps inside Databricks
Java 序列化的高级认识
什么是writeObject 和readObject?可定制的序列化过程