Spark 中的序列化陷阱

本文探讨了在Spark中常见的序列化错误,如传递非序列化类和更改静态域导致的结果不一致问题,并提供了详细的解决方案,包括实现Serializable接口、使用静态方法、lambda表达式和mapPartition方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

陷阱1: 没有序列化

最常见的一个错误就是传递的类不可序列化,如下面的例子:



  • package test;
     


  • import ...
     


  • /**
     


  • * Created by PerfectDay20.
     


  • */
     


  • public class Main {
     


  •     public static void main(String[] args) {
     


  •         SparkConf conf = new SparkConf().setAppName("test");
     


  •         JavaSparkContext javaSparkContext = new JavaSparkContext(conf);
     



  •  


  •         JavaRDD<Integer> rdd =
     


  •                 javaSparkContext.parallelize(IntStream.range(1, 10000).boxed().collect(Collectors.toList()), 10);
     



  •  


  •         Util util = new Util();
     


  •         rdd.map(util::process); // 序列化错误
     


  •     }
     



  •  


  • }
     



  •  


  • class Util implements Serializable{
     


  •     public int process(int i) {
     


  •         return i + 1;
     


  •     }
     


  • }

     

这里的 Util 类没有实现 Serializable 接口,由 Driver 创建实例后,在 map 中传递给各个 Executor,导致序列化失败报错:



  • Exception in thread "main" org.apache.spark.SparkException: Task not serializable
     


  •     at org.apache.spark.util.ClosureCleaner$.ensureSerializable(ClosureCleaner.scala:298)
     


  •     at org.apache.spark.util.ClosureCleaner$.org$apache$spark$util$ClosureCleaner$$clean(ClosureCleaner.scala:288)
     


  •     at org.apache.spark.util.ClosureCleaner$.clean(ClosureCleaner.scala:108)
     


  •     at org.apache.spark.SparkContext.clean(SparkContext.scala:2094)
     


  •     at org.apache.spark.rdd.RDD$$anonfun$map$1.apply(RDD.scala:370)
     


  •     ...
     


  • Caused by: java.io.NotSerializableException: test.Util
     


  • Serialization stack:
     


  •     - object not serializable (class: test.Util, value: test.Util@1290ed28)
     


  •     ...

     

这种错误根据不同的需求有不同的解决方法:

  • 最简单的方法就是让Util类可序列化: class Util implements Serializable
  • 如果是工具类,比如上例,没有必要创建Util实例,直接将process替换为静态方法:public static int process(int i),然后在map方法中:rdd.map(Util::process)
  • 如果调用的方法比较简单,就不用创建Util类,直接在map中写 lambda 表达式即可:rdd.map( i -> i + 1 );这种方法其实是创建了一个实现Function接口的匿名类,而Function接口的定义是:public interface Function<T1, R> extends Serializable,所以自然就可序列化了
  • 另外可以在map中创建Util实例,这样的话,每个实例都是在 Executor 端创建的,因为不需要序列化传递,就不存在序列化问题了:


  •         rdd.map(i->{
     


  •             Util util = new Util();
     


  •             LOG.info(""+util);
     


  •             return util.process(i);
     


  •         })

     

但是这种情况对于每一个i都要创建一个实例,在一些重量级操作,比如创建数据库链接时,可以考虑采用mapPartition,这样如上面的例子,就只需要创建10个Util实例:



  •         rdd.mapPartitions(iterator->{
     


  •             Util util = new Util();
     


  •             List<Integer> list = new ArrayList<>();
     


  •             iterator.forEachRemaining(i -> list.add(util.process(i)));
     


  •             return list.iterator();
     


  •         })

     

陷阱2: 更改静态域导致结果不一致

Java 的序列化结果中,只包括类的实例域部分,静态域在恢复实例时是由本地的 JVM 负责创建的,所以,假如在 Driver 端更改了静态域,而在 Driver 端是看不到的。所以要在 Executor 端使用的静态域,就不要在 Driver端更改,这和Broadcast创建后不要更改的要求是类似的。由于出现这种问题一般不会报异常,只会体现在结果中,所以比较难以发现。



  • package test;
     


  • import ...
     


  • /**
     


  • * Created by PerfectDay20.
     


  • */
     


  • public class Main {
     


  •     private static final Logger LOG = LoggerFactory.getLogger(Main.class);
     


  •     private static String word = "hello";
     


  •     public static void main(String[] args) {
     


  •         SparkConf conf = new SparkConf().setAppName("test");
     


  •         JavaSparkContext javaSparkContext = new JavaSparkContext(conf);
     


  •         JavaRDD<Integer> rdd =
     


  •                 javaSparkContext.parallelize(IntStream.range(1, 10000).boxed().collect(Collectors.toList()), 10);
     


  •         word = "world";
     


  •         rdd.foreach(i -> LOG.info(word));
     


  •     }
     


  • }

     

上面的例子中,word初始化为"hello",在 Driver 端的main方法中修改为"world",但该值并没有序列化到 Executor 端,Executor 本地仍然是"hello",输出的 log 结果自然也全都是 "hello"。

解决方案:

  • 最好一次性初始化好静态域,修饰为final ,避免二次更改
  • 在 Executor 端修改静态域,如


  •         rdd.foreach(i -> {
     


  •             word = "world";
     


  •             LOG.info(word);
     


  •         });

     

假如要在 Executor 端使用一个大的对象,比如一个Map,最好的方法还是利用Broadcast。

此外,由于多个 task 可能在同一 JVM 中运行,使用静态域可能会导致多线程问题,这也是需要注意的地方。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值