在写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这个类改为:
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数据库(内存数据库)中。(尤其是需要不断更新的规则)