问题背景
在用scala编写spark应用程序的时候,如果在executor中要用到一些公参变量、配置或自定义样例类,如case class
等等,直接依照常规方法定义局部变量使用会报错“xxx 不可序列化(Task not serializable: java.io.NotSerializableException)”,以及对象不可序列化(object not serializable),这时可以通过把报错变量提升到main方法外或继承Serializable
来解决,but why?
这个问题涉及java静态关键字和spark闭包的知识,研究整理如下,欢迎一起讨论。
Java的static关键字
在类中,变量的前面有修饰符static的称为静态变量(类变量),方法的前面有修饰符static的称为静态方法(类方法)。
静态方法和静态变量属于某一个类,而不属于类的对象。静态变量和静态方法在类被加载的时候就分配了内存空间 。
Scala的object与class
-
object
在Scala中没有静态修饰符,在object
下的成员全部都是静态的,可以理解为Scala把java类中的static
集中放到了object对象中,定义在object中的所有成员变量和方法默认都是static
的,但定义在main方法里的则仅仅是一个普通的局部变量。 -
class
在scala中,类名可以和对象名相同,该对象称为该类的伴生对象。类只会被编译加载,不能直接被执行。类和主构造器在一起被申明,主构造器会执行类定义中的所有语句。伴生对象在第一次使用的时候会调用构造器。
Spark的闭包
RDD相关操作都需要传入自定义闭包函数(closure),如果这个函数需要访问外部变量,那么需要遵循一定的规则,否则会抛出运行时异常。闭包函数传入到节点时,需要经过下面的步骤:
- driver通过反射,运行时找到闭包访问的所有变量,并封装成一个对象,然后序列化该对象(serialized on the driver node)
- 将序列化后的对象通过网络传输到worker节点(shipped to the appropriate nodes in the cluster)
- worker节点反序列化闭包对象(deserialized)
- worker节点执行闭包函数(and finally executed on the nodes)
总之,就是要通过网络传递函数然后执行,所以被传递的变量必须可序列化,否则传递失败。本地执行时,仍然会执行上面4步。
因此解决不可序列化问题无非以下两种方法:
- 避免序列化:
不在map、filter等闭包内部直接引用某类(通常是当前类)的成员函数或成员变量,可以定义在map、filter等操作内部((或使用lazy声明))或者定义在object对象中(也就是main方法外部)。 - 序列化它:
对相应的引用某类的成员函数或变量做好序列化处理,主要是继承序列化类(with Serializable)。
加上了lazy,相当于不对这个值进行序列化了,而是把这个隐式转换对象整个打包发送到worker上,然后用的时候才去加载。
还有,对于可以不需要序列化的成员变量可使用“@transient ”标注,被transient关键字修饰的变量不再能被序列化(慎用),一个静态变量不管是否被transient修饰,均不能被序列化。
小结
静态成员不属于对象,属于类,不能被序列化,还有瞬态的变量也不能被序列化 。
Java对象中static,transient修饰的成员不能被序列化。
static变量保存在全局数据区,在对象未实例化时就已经生成,属于类的状态。
Scala object中定义的变量默认被static修饰,所以定义在main方法外可以不被序列化(spark闭包)。
最后,节点容器中,在本地的JVM中实例化静态变量,对象加载类时直接使用它,从而绕过了网络传输那一步,最终避免了序列化错误。
网友评论