spark优化--对象序列化问题

在写spark程序时,经常会遇到序列化问题,首先我们应该弄清楚为什么要进行序列化。
因为当我们在Driver端创建一个对象,在Executor端要使用这个对象时,Driver要将这个对象发送给Executor,这个时候要进行序列化,只有通过序列化了,这个对象才能够通过网络进行传输。

在Executor中创建一个类的实例

下面先来看一个例子:

Rules.scala

package XXX

class Rules {

 val rulesMap: Map[String, Int] = Map("hadoop" -> 1,"spark" -> 2)

}

SerTest.scala

package XXX

import java.net.InetAddress

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

object SerTest {
  def main(args: Array[String]): Unit = {

    val conf = new SparkConf().setAppName("SerTest")
    val sc = new SparkContext(conf)

    val lines: RDD[String] = sc.textFile(args(0))

    val r: RDD[(String, String, Int, String)] = lines.map(word => {
      //在map函数中,创建一个rules实例(太浪费资源)
      val rules = new Rules
      //函数的执行是在Executor中执行的
      val hostname = InetAddress.getLocalHost.getHostName
      val threadName = Thread.currentThread().getName
      (hostname, threadName, rules.rulesMap.getOrElse(word, 0), rules.toString)
    })

    r.saveAsTextFile(args(1))

    sc.stop()

  }

}

这个例子中,我们有一个规则类Rules(这个类中可以是处理逻辑,比如读取数据库,比较规则等),然后在Executor中要使用这个类的对象,我们在map函数中创建了这个类的对象。

这个方法有一个很大的坏处:每当执行一次map,就会创建一次这个类的对象,这造成了很大的内存资源的浪费。

函数中引用一个Driver端的一个类的实例

接下来我们进行优化一下:

我们在Driver端创建这个类的对象:

package XXX

import java.net.InetAddress

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

object SerTest {
  def main(args: Array[String]): Unit = {

    //在Driver端实例化这个对象
    val rules = new Rules

    val conf = new SparkConf().setAppName("SerTest")
    val sc = new SparkContext(conf)

    val lines: RDD[String] = sc.textFile(args(0))

    val r: RDD[(String, String, Int, String)] = lines.map(word => {
      //在map函数中,创建一个rules实例(太浪费资源)
      //val rules = new Rules
      //函数的执行是在Executor中执行的
      val hostname = InetAddress.getLocalHost.getHostName
      val threadName = Thread.currentThread().getName
      (hostname, threadName, rules.rulesMap.getOrElse(word, 0), rules.toString)
    })

    r.saveAsTextFile(args(1))

    sc.stop()

  }

}

这种方法是在Driver端创建了一个Rules类的对象,然后在Executor(map方法)中直接使用这个对象,这种方法就是函数式编程中经常出现的闭包(在一个函数内部引用了函数外部的变量)。
在Java中,相当于在一个内部类中使用了外部类的变量。

注意:运行这个程序会出现错误,错误类型就是没有经过序列化,
在这里插入图片描述
出现这个错误的原因就是Rules这个类的对象是在Driver端生成的,但是在发送给Executor端的时候没有经过序列化。

所以我们要让Rules这个类实现序列化,将Rules这个类改为:

扫描二维码关注公众号,回复: 5720799 查看本文章
package XX   
class Rules extends Serializable {

 val rulesMap: Map[String, Int] = Map("hadoop" -> 1,"spark" -> 2)

}

这样就没有错误了,但是,这种情况下,Executor端创建了一个Rules类的对象,然后经过序列化跟随Task发送给Executor,这样每个Task在使用这个对象的时候,会经过反序列化,这样经过发序列化之后,Executor中的每个Task都会有一个Rules类的对象,而这样也是没有必要的,我们希望的是Executor中所有的Task共同使用一份Rules类的对象就可以了,因为这些Task执行的业务逻辑都是相同的。

函数中引用一个Driver端的一个Object

接下来我们继续优化:

我们将Rules这个类变成一个object:

package XXX

object Rules extends Serializable {

 val rulesMap: Map[String, Int] = Map("hadoop" -> 1,"spark" -> 2)

}
在Scala中,object叫做静态对象,也叫做单例对象,在满足一定条件下叫做伴生对象。所以object对象在一个Java进程中只有一份。

关于object的讲解请查看博客:https://blog.csdn.net/weixin_43866709/article/details/88080773

然后在Driver端初始化这个object:

val rules = Rules

这个时候,一个Executor中的Task都共同使用了这个对象,只要有一个Task将这个对象反序列化之后其他的Task就都可以使用了 。

但是这个方法还是有一个缺点:这个实例要经过网络发送给每个Executor

在Executor中初始化一个object

接下来再来优化一下:

我们可以直接在Executor中初始化这个object,这样就不用通过网络发送这个实例了,也不用在进行实例化了。

将代码改为:

Rules.scala

object Rules{
  val rulesMap: Map[String, Int] = Map("hadoop" -> 1,"spark" -> 2)
  val hostname: String = InetAddress.getLocalHost.getHostName
  //打印一下是在那一端被序列化的
  println(hostname + "@@@@@@@@@@@@@@")
}

SerTest.scala

package XXX 

import java.net.InetAddress

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

object SerTest {
  def main(args: Array[String]): Unit = {

    //在Driver端实例化这个对象
    //val rules = new Rules

    //初始化object(在Driver端)
    //val rules = Rules

    val conf = new SparkConf().setAppName("SerTest")
    val sc = new SparkContext(conf)

    val lines: RDD[String] = sc.textFile(args(0))

    val r: RDD[(String, String, Int, String)] = lines.map(word => {
      //在map函数中,创建一个rules实例(太浪费资源)
      //val rules = new Rules
      //函数的执行是在Executor中执行的
      val hostname = InetAddress.getLocalHost.getHostName
      val threadName = Thread.currentThread().getName
      (hostname, threadName, Rules.rulesMap.getOrElse(word, 0), Rules.toString)
    })

    r.saveAsTextFile(args(1))

    sc.stop()

  }

}

这样再执行这个程序,这个object是在Executor端被加载的,而且每个Executor中的Task使用同样一个object实例。

补充

这种方法和广播变量的区别:
广播变量广播出去的规则就不能再改变了,而且当节点特别多的时候,广播的时间可能会很长,
使用这种方法会更简单一些。
但是这种方法也会出现问题,当Executor中的Task很多时,Task是在线程池中一块执行的,那么当多个Task同时访问这个规则(object)时,就会出现多线程问题。

那么如何解决这种多线程问题:

1.使用读写锁,防止多个线程同时访问这个object
2.使用Redis数据库,将这个规则放到Redis数据库(内存数据库)中。(尤其是需要不断更新的规则)

猜你喜欢

转载自blog.csdn.net/weixin_43866709/article/details/88864465
今日推荐