Spark 序列化和kryo序列化器详解

建议看本文前先看看另外一篇文章Java序列化和反序列化介绍

1.Java序列化含义

Spark是基于JVM运行的进行,其序列化必然遵守Java的序列化规则。

序列化就是指将一个对象转化为二进制的byte流(注意,不是bit流),然后以文件的方式进行保存或通过网络传输,等待被反序列化读取出来。序列化常被用于数据存取和通信过程中。

对于java应用实现序列化一般方法:

  1. class实现序列化操作是让class 实现Serializable接口,但实现该接口不保证该class一定可以序列化,因为序列化必须保证该class引用的所有属性可以序列化。

  2. 这里需要明白,static和transient修饰的变量不会被序列化,这也是解决序列化问题的方法之一,让不能序列化的引用用static和transient来修饰。(static修饰的是类的状态,而不是对象状态,所以不存在序列化问题。transient修饰的变量,是不会被序列化到文件中,在被反序列化后,transient变量的值被设为初始值,如int是0,对象是null)

  3. 此外还可以实现readObject()方法和writeObject()方法来自定义实现序列化。

2.Spark的transformation操作为什么需要序列化

Spark是分布式执行引擎,其核心抽象是弹性分布式数据集RDD,其代表了分布在不同节点的数据。Spark的计算是在executor上分布式执行的,故用户开发的关于RDD的map,flatMap,reduceByKey等transformation 操作(闭包)有如下执行过程:

  1. 代码中对象在driver本地序列化
  2. 对象序列化后传输到远程executor节点
  3. 远程executor节点反序列化对象
  4. 最终远程节点执行

故对象在执行中需要序列化通过网络传输,则必须经过序列化过程。

在spark中4个地方用到了序列化:

  1. 算子中用到了driver定义的外部变量的时候
  2. 将自定义的类型作为RDD的泛型类型,所有的自定义类型对象都会进行序列化
  3. 使用可序列化的持久化策略的时候。比如:MEMORY_ONLY_SER,spark会将RDD中每个分区都序列化成一个大的字节数组。
  4. shuffle的时候

任何分布式系统中,序列化都扮演着一个很重要的角色。如果使用的序列化技术操作很慢,或者序列化之后数据量还是很大的话,那么会严重影响分布式系统的性能。

下面看看一个案例

package com.cw.spark.core.rdd.serial

import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

/**
  * @author 陈小哥cw
  * @date 2021/3/12 8:51
  */
object Spark01_RDD_Serial {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Operator")
    val sc = new SparkContext(sparkConf)

    val rdd: RDD[String] = sc.makeRDD(Array("hello world", "hello spark", "hive", "flink"))

    val search = new Search("h")

    search.getMatch1(rdd).collect().foreach(println)

    println("----------------------------------")

    search.getMatch2(rdd).collect().foreach(println)

    sc.stop()
  }


  class Search(query: String){
    def isMatch(s: String): Boolean = {
      s.contains(query)
    }

    // 函数序列化案例
    def getMatch1(rdd: RDD[String]): RDD[String] = {
      //rdd.filter(this.isMatch)
      // 在executor端执行,而isMatch是Search对象中的方法,Search对象存在于Driver端,需要将Search传到executor端才可执行isMatch
      // driver向executor端传数据涉及到网络传输,网络只能传字符串,不能传对象和数字,所以需要将对象进行序列化才可进行传递
      rdd.filter(isMatch)
    }

    // 属性序列化案例
    def getMatch2(rdd: RDD[String]): RDD[String] = {
      //rdd.filter(x => x.contains(this.query))
      rdd.filter(x => x.contains(query))
    }
  }

}

算子以外的代码都是在 Driver 端执行, 算子里面的代码都是在 Executor端执行。

如果不进行序列化直接运行程序会发现报错: Task not serializable
在这里插入图片描述
将上述代码从scala反编译为java可以看到

  1. 类的构造参数其实是类的属性,构造参数需要进行闭包检测,其实就等同于类进行闭包检测,闭包概念见Spark中的闭包和闭包检测
  2. getMatch1方法中所调用的方法isMatch()是定义在Search这个类中的,实际上调用的是this.isMatch()this表示Searcher这个类的对象,程序在运行过程中需要将Searcher对象序列化以后传递到Executor端。
  3. getMatch2方法中所调用的方法query是定义在Searcher这个类中的字段,实际上调用的是this.querythis表示Searcher这个类的对象,程序在运行过程中需要将Search对象序列化以后传递到Executor端。
 public static class Search {
      private final String query;

      public boolean isMatch(final String s) {
         return s.contains(this.query);
      }

      public RDD getMatch1(final RDD rdd) {
         return rdd.filter((s) -> {
            return BoxesRunTime.boxToBoolean($anonfun$getMatch1$1(this, s));
         });
      }

      public RDD getMatch2(final RDD rdd) {
         return rdd.filter((x) -> {
            return BoxesRunTime.boxToBoolean($anonfun$getMatch2$1(this, x));
         });
      }

3.如何解决Spark序列化问题(Scala语言)

3.1 解决序列化方案1:类继承scala.Serializable

class Search(query: String) extends Serializable{
    def isMatch(s: String): Boolean = {
      s.contains(query)
    }

    // 函数序列化案例
    def getMatch1(rdd: RDD[String]): RDD[String] = {
      rdd.filter(isMatch)
    }

    // 属性序列化案例
    def getMatch2(rdd: RDD[String]): RDD[String] = {
      rdd.filter(x => x.contains(query))
    }
  }

3.2 解决序列化方案2:使用case class修饰类

case class Search(query: String) {
    def isMatch(s: String): Boolean = {
      s.contains(query)
    }

    // 函数序列化案例
    def getMatch1(rdd: RDD[String]): RDD[String] = {
      rdd.filter(isMatch)
    }

    // 属性序列化案例
    def getMatch2(rdd: RDD[String]): RDD[String] = {
      rdd.filter(x => x.contains(query))
    }
 }

样例类就是使用 case 关键字修饰的类,样例类默认是实现了序列化接口的

我们将上述scala代码反编译为java就可以很清晰看到样例类底层默认实现了Serializable接口,所以使用case修饰类即可让类序列化

public static class Search implements Product, Serializable {
      private final String query;

      public String query() {
         return this.query;
      }

      public boolean isMatch(final String s) {
         return s.contains(this.query());
      }

      public RDD getMatch1(final RDD rdd) {
         return rdd.filter((s) -> {
            return BoxesRunTime.boxToBoolean($anonfun$getMatch1$1(this, s));
         });
      }

      public RDD getMatch2(final RDD rdd) {
         return rdd.filter((x) -> {
            return BoxesRunTime.boxToBoolean($anonfun$getMatch2$1(this, x));
         });
      }
 }

3.3 解决序列化方案3:传递局部变量而不是属性,将类变量query赋值给局部变量

当Executor内需要的是一个属性时,可以使用局部变量接收这个属性值,传递局部变量而不是属性,将类变量query赋值给局部变量
修改getMatch2为

def getMatch2(rdd: RDD[String]): RDD[String] = {
      val q = query// 在Driver端,q为字符串类型,可以序列化,传给executor时不会出错
      rdd.filter(x => x.contains(q))
}

4.kryo序列化机制

spark使用的默认序列化机制是java提供的序列化机制,即基于ObjectInputStreamObjectOutputStream的序列化机制。

这种序列化机制使用起来便捷,只要你的类实现了Serializable接口,那么都是可以序列化的。而且Java序列化机制是提供了自定义序列化支持的,只要你实现Externalizable接口即可实现自己的更高性能的序列化算法。但是这种方式的性能并不是很高,序列化的速度也相对较慢,并且序列化之后数据量也是比较大,占用较多的内存空间。

除了默认使用的序列化机制以外,spark还提供了另一种序列化机制,Kryo序列化机制。

这种序列化机制比java的序列化机制更快,并且序列化之后的数据占用空间更少,通常比java序列化小10倍。那么Kryo序列化机制为什么不是默认机制?原因是即使有些类实现了Seriralizable接口它也不一定能进行序列化,而且如果你想实现某些类的序列化,需要在spark程序中进行注册。

参考地址: https://github.com/EsotericSoftware/kryo

Java 的序列化能够序列化任何的类。但是比较重(字节多),序列化后,对象的提交也比较大。Spark 出于性能的考虑,Spark2.0 开始支持另外一种 Kryo 序列化机制。Kryo 速度是 Serializable 的 10 倍。当 RDD 在 Shuffle 数据的时候,简单数据类型、数组和字符串类型已经在 Spark 内部使用 Kryo 来序列化。

类别优点缺点备注
java native serialization兼容性好、和scala更好融合序列化性能较低、占用内存空间大(一般是Kryo Serialization 的10倍)默认的serializer
Kryo Serialization序列化速度快、占用空间小(即更紧凑)不支持所有的Serializable类型、且需要用户注册要进行序列化的类classshuffle的数据量较大或者较为频繁时建议使用

注意:即使使用 Kryo 序列化,也要继承 Serializable 接口。

5.Spark 中使用 Kryo Serialization的几种方式

Spark 中使用 Kryo 序列化主要经过两个步骤

  1. 声明使用 kryo 序列化
  2. 注册序列化类

5.1 声明使用 kryo 序列化

1.配置文件方式

可以在配置文件spark-default.conf中添加该配置项(全局生效)

spark.serializer   org.apache.spark.serializer.KryoSerializer

2.业务代码中配置

在业务代码中通过SparkConf进行配置(针对当前application生效)

val spark = SparkSession.builder().master("local[*]").appName("test").getOrCreate()
val conf = new SparkConf
conf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
    

3.在spark-shell、spark-submit脚本中启动

可以在命令中加上

--conf spark.serializer=org.apache.spark.serializer.KryoSerializer

5.2 注册序列化类(非必须,但是强烈建议做)

......
conf.registerKryoClasses(Array(classOf[Test1], classOf[Test2]))
// 其中Test1.java 和 Test2.java 是自定义的类

如果是scala类Test1(scala中的trait就相当于java中的接口):

class Test1 extends Serializable {
    ......
}

如果是java类Test2:

public class Test2 implements Serializable {
    ......
}

注意:虽说该步不是必须要做的(不做Kryo仍然能够工作),但是如果不注册的话,Kryo会存储自定义类中用到的所有对象的类名全路径,这将会导致耗费大量内存,耗费内存比使用java更大。

序列化方式是否注册空间占用
kyro21.1 MB
kyro38.3 MB
Java25.1 MB

5.3 配置 spark.kryoserializer.buffer

如果要被序列化的对象很大,这个时候就最好将配置项spark.kryoserializer.buffer 的值(默认64k)设置的大些,使得其能够hold要序列化的最大的对象。

6.在Spark中使用kryo的案例

package com.cw.spark.core.rdd.serial

import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

/**
  * @author 陈小哥cw
  * @date 2021/3/12 15:58
  */
object serializable_Kryo {
  def main(args: Array[String]): Unit = {
    val conf: SparkConf = new SparkConf()
      .setAppName("SerDemo")
      .setMaster("local[*]")
      // 替换默认的序列化机制
      .set("spark.serializer",
      "org.apache.spark.serializer.KryoSerializer")
      // 注册需要使用 kryo 序列化的自定义类(非必须,但是强烈建议做)
      // 虽说该步不是必须要做的(不做Kryo仍然能够工作),但是如果不注册的话,
      //  Kryo会存储自定义类中用到的所有对象的类名全路径,这将会导致耗费大量内存。
      .registerKryoClasses(Array(classOf[Searcher]))

    val sc = new SparkContext(conf)
    val rdd: RDD[String] = sc.makeRDD(Array("hello world", "hello spark",
      "spark", "hahah"), 2)
    val searcher = new Searcher("hello")
    val result: RDD[String] = searcher.getMatchedRDD1(rdd)
    result.collect.foreach(println)
  }
}

case class Searcher(val query: String) {
  def isMatch(s: String) = {
    s.contains(query)
  }

  def getMatchedRDD1(rdd: RDD[String]) = {
    rdd.filter(isMatch)
  }

  def getMatchedRDD2(rdd: RDD[String]) = {
    val q = query
    rdd.filter(_.contains(q))
  }
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值