理解Spark序列化
Spark序列化的理解和解决方法
在Spark中,序列化是一个常见的问题,有很多很好的博客已经很好地解释了这个问题,比如这篇spark序列化挑战。 简而言之,我们可以得出以下结论(仅针对Spark,而不是JVM):
1. 由于JVM的限制,只有对象可以被序列化(函数也是对象)。
2. 如果一个对象需要被序列化,它的父对象也需要被序列化。
3. 任何Spark操作(如map、flatMap、filter、foreachPartition、mapPartition等),如果内部部分引用了外部部分对象,那么这个对象需要被序列化。因为外部部分对象在Driver中,而不在Executors中。而序列化策略遵循我的第二点。
4. 对于Scala的object(也称为Scala单例),它不会被序列化(仅适用于mapPartition和foreachPartition,UDF始终从Driver序列化到Executors)。Executors直接引用其本地JVM的对象,因为它是单例的,它将存在于Executors的JVM中。这意味着Driver对其本地对象的更改在Executors中将不可见。
为了补充第4点,单例对象并不总是不会被序列化的情况。我刚刚使用了一个单例对象和一些辅助函数(UDF)来创建一个新的Dataframe列,然后遇到了序列化错误,并且不得不在代码中将该对象扩展为Serializable以继续进行,而且没有在map/flatMap等中使用该对象。这可能是一个Spark问题,但是它确实存在。
感谢你的观点,所以为了澄清,我的第4点只适用于mapPartition和foreachPartition函数。Spark将UDF从Driver序列化到Executors,因此UDF和UDF引用的任何对象都需要进行序列化。
Spark序列化(serialization)是将对象的状态转换为字节流,以便可以将字节流恢复为对象的副本。如果Java对象的类或任何父类实现了java.io.Serializable接口或其子接口java.io.Externalizable,则该类可以进行序列化。
只有类的对象才会被序列化。如果需要持久化或通过网络传输对象,则需要进行对象序列化。
下面是一个示例Spark代码和不同场景的讨论:
在RDD内部编写的所有lambda函数都是在驱动程序上实例化的,并且对象被序列化并发送到执行器。
如果在内部类中访问外部类的实例变量,则编译器会应用不同的逻辑来访问它们,因此外部类是否被序列化取决于您访问的内容。
在Java中,整个讨论是关于外部类与内部类之间的关系,以及如何通过访问外部类的引用和变量来解决序列化问题。
不同场景:
1. 匿名类中访问的外部类实例变量:
- 实例变量(外部类):可访问和序列化
- 静态实例变量(外部类):可访问但不序列化
- 局部变量(外部类):可访问和序列化
2. 在静态内部类中访问的外部类变量:
- 实例变量(外部类):无法访问
- 局部变量(外部类):无法访问
- 静态实例变量(外部类):可访问但不序列化
需要考虑的问题:
- 遵循Java序列化规则来选择需要序列化的类对象。
- 使用javap -p -c "abc.class"命令来查看编译器生成的字节码。
- 根据在外部类的内部类中尝试访问的内容,编译器会生成不同的字节码。
- 不需要使只在驱动程序上访问的类实现序列化。
- 在RDD中使用的任何匿名/静态类都将在驱动程序上实例化。
- 在RDD中使用的任何类/变量都将在驱动程序上实例化并发送到执行器。
- 声明为transient的实例变量在驱动程序上不会被序列化。
- 默认情况下,匿名类将强制使外部类可序列化。
- 任何只在匿名类中使用的局部变量/对象不需要序列化。
- 仅当在匿名类中使用局部变量时,才需要对其进行序列化。
- 可以在pair、mapToPair函数的call()方法中创建单例,以确保它们永远不会在驱动程序上初始化。
- 静态变量永远不会被序列化,因此不会从驱动程序发送到执行器。
如果需要仅在执行器上执行的服务,请将它们作为lambda函数内的静态字段,或将它们声明为transient和单例,并检查为空的条件来实例化它们。
以上是关于Spark序列化的理解。