Spark 中的序列化陷阱

转自:https://segmentfault.com/a/1190000012353884

Spark 的代码分为 Driver 端执行的部分和 Executor 端执行的部分,Driver 端分发任务的同时,会通过序列化传送 Executor 需要的对象,由于 Java 序列化的一些特性,初学者在使用时容易碰到一些陷阱。

陷阱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)
    ...

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

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

解决方案:

  1. 最好一次性初始化好静态域,修饰为final ,避免二次更改
  2. 在 Executor 端修改静态域,如
        rdd.foreach(i -> {
            word = "world";
            LOG.info(word);
        });

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

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

Kryo是一个快速、高效的Java序列化框架,比Java自带的序列化框架更快、更小、更节省内存。在Spark,使用Kryo作为默认的序列化框架可以显著地提高性能。下面是一个使用Kryo序列化的案例: ```java import org.apache.spark.SparkConf; import org.apache.spark.api.java.JavaRDD; import org.apache.spark.api.java.JavaSparkContext; import org.apache.spark.serializer.KryoRegistrator; import com.esotericsoftware.kryo.Kryo; public class KryoExample { public static void main(String[] args) { SparkConf conf = new SparkConf().setAppName("KryoExample").setMaster("local"); conf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer"); conf.set("spark.kryo.registrator", MyKryoRegistrator.class.getName()); JavaSparkContext sc = new JavaSparkContext(conf); JavaRDD<String> lines = sc.textFile("input.txt"); JavaRDD<MyObject> objects = lines.map(line -> { MyObject obj = new MyObject(); obj.setId(Integer.parseInt(line.split(",")[0])); obj.setName(line.split(",")[1]); return obj; }); objects.foreach(obj -> System.out.println(obj.getId() + ": " + obj.getName())); } public static class MyObject { private int id; private String name; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } } public static class MyKryoRegistrator implements KryoRegistrator { @Override public void registerClasses(Kryo kryo) { kryo.register(MyObject.class); } } } ``` 在上面的代码,我们首先创建了一个SparkConf对象,并设置了应用名称和运行模式。然后,我们设置了序列化器为KryoSerializer,并指定了KryoRegistrator为MyKryoRegistrator。MyKryoRegistrator是一个自定义的Kryo注册器,用于注册我们需要序列化MyObject。 接下来,我们使用JavaSparkContext读取了一个文本文件,并将每一行转换成一个MyObject对象。最后,我们对这些对象进行了遍历,并输出了它们的id和name属性。 需要注意的是,如果我们没有使用KryoSerializer序列化器,程序将默认使用Java自带的序列化器,这样可能会导致性能瓶颈。因此,建议在Spark使用Kryo作为默认的序列化器。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值