在 Apache Spark 中,对于大数据应用程序,建议使用 Kryo 序列化而不是 java 序列化。与 java 序列化相比,当您移动和缓存大量数据时,与 java 序列化相比,Kryo 占用的内存更少。

虽然 kryo 支持 RDD 缓存 和 shuffle,但它本身并不支持序列化到磁盘。RDD 上的 saveAsObjectFile 方法和 SparkContext 上的 objectFile 方法都只支持 java 序列化。

随着自定义数据类型数量的增加,支持多个序列化变得非常繁琐。因此,如果我们可以在任何地方使用 kryo 序列化,那就太好了。在这篇文章中,我们将讨论如何使用 kryo 序列化来保存和读取磁盘。

Write

通常我们使用 rdd.saveAsObjectFile api将序列化的对象保存到磁盘中。下面的代码展示了如何编写自己的 saveAsObjectFile 方法,该方法将对象保存为 kryo 序列化格式。

def saveAsObjectFile[T: ClassTag](rdd: RDD[T], path: String)

我们将写入的 rdd 和输出路径作为输入参数。

val kryoSerializer = new KryoSerializer(rdd.context.getConf)

KryoSerializer 是 spark 提供的一个帮助类,用于处理 kryo。我们创建一个 KryoSerializer 的实例,它配置所需缓冲区大小。

rdd.mapPartitions(iter => iter.grouped(10)
      .map(_.toArray))
      .map(splitArray => {}

每个 objectFile 都保存为 HDFS 序列文件。因此,我们循环遍历每个 rdd 分割,然后将这些分割转换为字节数组。

val kryo = kryoSerializer.newKryo()

对于每个 splitArray,首先创建一个 kryo 实例。kryo 实例不是线程安全的。这就是为什么我们为每个 map 操作创建一个。当我们调用 kryoSerializer.newKryo() 时,它会创建一个新的 kryo 实例,并调用我们的自定义注册器(如果有的话)。

//create output stream and plug it to the kryo output
val bao = new ByteArrayOutputStream()
val output = kryoSerializer.newKryoOutput()
output.setOutputStream(bao)
kryo.writeClassAndObject(output, splitArray)
output.close()

一旦我们有了 kryo 实例,我们就创建 kryo 输出。然后我们写入类信息并对输出进行处理。

val byteWritable = new BytesWritable(bao.toByteArray)
      (NullWritable.get(), byteWritable)
    }).saveAsSequenceFile(path)

一旦我们有了 kryo 的字节表示,我们就把 bytearray 封装到 BytesWritable 中并保存为序列文件。

因此,只需几行代码,现在就可以将kryo对象保存到磁盘中。

Read

如果你不仅仅把数据写入磁盘。您还应该能够从这些数据创建 RDD。通常我们在 sparkContext 上使用 objectFile api 从磁盘读取数据。在这里,我们将编写自己的 objectFile api 来读取 kryo 对象文件。

def objectFile[T](sc: SparkContext, path: String, minPartitions: Int = 1)(implicit ct: ClassTag[T]) = {
    val kryoSerializer = new KryoSerializer(sc.getConf)
    sc.sequenceFile(path, classOf[NullWritable], classOf[BytesWritable],
       minPartitions)
       .flatMap(x => {
       val kryo = kryoSerializer.newKryo()
       val input = new Input()
       input.setBuffer(x._2.getBytes)
       val data = kryo.readClassAndObject(input)
       val dataObject = data.asInstanceOf[Array[T]]
       dataObject
    })
  }

大多数步骤与写入相同,唯一的区别是我们使用输入而不是使用输出。我们从 BytesWritable 读取字节并使用 readClassAndObject api 进行反序列化。

Example

下面的示例使用上述两个方法来序列化和反序列化一个名为 Person 的自定义对象。

// user defined class that need to serialized
  class Person(val name: String)

 def main(args: Array[String]) {

    if (args.length < 1) {
      println("Please provide output path")
      return
    }
    val outputPath = args(0)

    val conf = new SparkConf().setMaster("local").setAppName("kryoexample")
    conf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
    val sc = new SparkContext(conf)

    //create some dummy data
    val personList = 1 to 10000 map (value => new Person(value + ""))
    val personRDD = sc.makeRDD(personList)

    saveAsObjectFile(personRDD, outputPath)
    val rdd = objectFile[Person](sc, outputPath)
    println(rdd.map(person => person.name).collect().toList)
  }

因此,如果您在您的项目中使用kryo序列化,现在您也可以将相同的序列化保存到磁盘中。

附录代码

Write

package com.madhu.spark.kryo

import java.io.ByteArrayOutputStream

import com.esotericsoftware.kryo.io.Input
import org.apache.hadoop.io.{BytesWritable, NullWritable}
import org.apache.spark.SparkContext._
import org.apache.spark.rdd.RDD
import org.apache.spark.serializer.KryoSerializer
import org.apache.spark.{SparkConf, SparkContext}

import scala.reflect.ClassTag


/**
 * Example code showing how to kryo serialization for disk
 */

object KryoExample {

  def main(args: Array[String]) {

    if (args.length < 1) {
      println("Please provide output path")
      return
    }
    val outputPath = args(0)

    val conf = new SparkConf().setMaster("local").setAppName("kryoexample")
    conf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
    val sc = new SparkContext(conf)

    //create some dummy data
    val personList = 1 to 10000 map (value => new Person(value + ""))
    val personRDD = sc.makeRDD(personList)

    saveAsObjectFile(personRDD, outputPath)
    val rdd = objectFile[Person](sc, outputPath)
    println(rdd.map(person => person.name).collect().toList)
  }

  /*
   * Used to write as Object file using kryo serialization
   */
  def saveAsObjectFile[T: ClassTag](rdd: RDD[T], path: String) {
    val kryoSerializer = new KryoSerializer(rdd.context.getConf)

    rdd.mapPartitions(iter => iter.grouped(10)
      .map(_.toArray))
      .map(splitArray => {
      //initializes kyro and calls your registrator class
      val kryo = kryoSerializer.newKryo()

      //convert data to bytes
      val bao = new ByteArrayOutputStream()
      val output = kryoSerializer.newKryoOutput()
      output.setOutputStream(bao)
      kryo.writeClassAndObject(output, splitArray)
      output.close()

      // We are ignoring key field of sequence file
      val byteWritable = new BytesWritable(bao.toByteArray)
      (NullWritable.get(), byteWritable)
    }).saveAsSequenceFile(path)
  }

  /*
   * Method to read from object file which is saved kryo format.
   */
  def objectFile[T](sc: SparkContext, path: String, minPartitions: Int = 1)(implicit ct: ClassTag[T]) = {
    val kryoSerializer = new KryoSerializer(sc.getConf)

    sc.sequenceFile(path, classOf[NullWritable], classOf[BytesWritable], minPartitions)
      .flatMap(x => {
      val kryo = kryoSerializer.newKryo()
      val input = new Input()
      input.setBuffer(x._2.getBytes)
      val data = kryo.readClassAndObject(input)
      val dataObject = data.asInstanceOf[Array[T]]
      dataObject
    })

  }

  // user defined class that need to serialized
  class Person(val name: String)

}