大数据开发Spark程序性能优化(第三十四篇)

237 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 12 月更文挑战」的第11天,点击查看活动详情

一、性能优化分析

  1. 一个计算任务的执行主要依赖于CPU、内存、带宽
  2. 实际工作中计算任务的性能瓶颈一般会出现在内存上
1.1、内存都去哪了
  1. 每个Java对象,都有一个对象头,会占用16个字节
  2. Java的String对象的对象头,会比它内部的原始数据,要多出40个字节
  3. Java中的集合类型,比如HashMap内部使用的链表数据结构,链表中的每一个数据,都使用了Entry对象来包装,Entry对象不光有对象头,还有指向下一个Entry的指针,通常占用8个字节

二、高性能序列化类库

  1. Spark倾向于序列化的便捷性,默认使用了Java序列化机制
  2. Java序列化机制的性能并不高,序列化的速度相对较慢,而且序列化以后的数据,相对于比较大
  3. Spark提供了两种序列化机制:Java序列化和Kryo序列化
  4. Kryo序列化比Java序列化更快,而且序列化后的数据更小,通常比Java序列化的数据要小10倍左右
如何使用:
  1. 首先要用SparkConf将Spark序列化器设置为KryoSerializer
  2. 使用Kryo时,针对需要序列化的类,需要预先进行注册,这样才能获得最佳的性能,如果不注册,Kryo必须时刻保存类的全类名,反而占用不少内存
  3. Spark默认对Scala中常用的类型自动在Kryo进行了注册
  4. 如果在自己的算子中,使用了外部的自定义类型的对象,那么还是需要对其进行注册
  5. 格式:conf.registerKryoClasses()
注意:
  1. 如果要序列化的自定义的类型,字段特别多,此时就需要对Kryo本身进行优化,因为Kryo内部的缓存可能不够存放那么大的Class对象
  2. 需要调用SparkConf.set()方法,设置spark.kryoserializer.buffer.mb参数的值,将其调大,默认值为2,单位是MB

Q:什么情况下适合使用Kryo序列化?

一般针对自定义对象,这个对象里面包含了几十兆或者上百兆的数据,里面可能有一些集合,集合数据存储的数据比较多

package com.strivelearn.scala
​
import org.apache.spark.storage.StorageLevel
import org.apache.spark.{SparkConf, SparkContext}
​
/**
 * @author strivelearn
 * @version KryoSerScala.java, 2022年11月27日
 */
object KryoSerScala {
  def main(args: Array[String]): Unit = {
    //创建SparkContext
    val conf = new SparkConf()
    conf.setAppName("KryoSerScala")
      .setMaster("local")
      //指定使用Kryo序列化机制。如果使用了registerKryoClasses可以省略这行
      .set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
      //注册自定义数据类型
      .registerKryoClasses(Array(classOf[Person]))
    val context = new SparkContext(conf)
​
    val dataRDD = context.parallelize(Array("hello you", "hello me"))
    val wordsRDD = dataRDD.flatMap(_.split(" "))
    val personRDD = wordsRDD.map(word => Person(word, 18)).persist(StorageLevel.MEMORY_ONLY_SER)
    personRDD.foreach(println)
    //这边的死循环是为了不让程序停止,在本地查看spark界面
    while (true) {
      ;
    }
  }
}
​
case class Person(name: String, age: Int) extends Serializable

查看本地任务的网址:http://localhost:4040/jobs/

查看Storage

image-20221127125546932

如果不使用kyro序列化,则占用144字节,如果数据量比较大,原始的Java序列化方式占用内存更多

image-20221127125709259

Java代码
package com.strivelearn.java;
​
import java.io.Serializable;
import java.util.Arrays;
​
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.apache.spark.SparkConf;
import org.apache.spark.api.java.JavaRDD;
import org.apache.spark.api.java.JavaSparkContext;
import org.apache.spark.storage.StorageLevel;
​
/**
 * @author strivelearn
 * @version KryoSerJava.java, 2022年11月27日
 */
public class KryoSerJava {
    public static void main(String[] args) {
        //1.创建sparkContext
        SparkConf sparkConf = new SparkConf();
        sparkConf.setAppName("KryoSerJava");
        sparkConf.setMaster("local");
        sparkConf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer");
        sparkConf.set("spark.kryo.classesToRegister", "com.strivelearn.java.Person");
        JavaSparkContext javaSparkContext = new JavaSparkContext(sparkConf);
        JavaRDD<String> dataRDD = javaSparkContext.parallelize(Arrays.asList("hello you", "hello me"));
        JavaRDD<String> wordsRDD = dataRDD.flatMap(line -> Arrays.asList(line.split(" ")).iterator());
        JavaRDD<Person> personRDD = wordsRDD.map(line -> new Person(line, 18)).persist(StorageLevel.MEMORY_ONLY_SER());
        personRDD.foreach(person -> System.out.println(person.toString()));
        while (true) {
            ;
        }
    }
}
​
@Getter
@Setter
@ToString
@AllArgsConstructor
class Person implements Serializable {
    private String  name;
    private Integer age;
}

三、持久化或者checkpoint的优化

  1. 针对程序中多次被transformation或者action操作的RDD进行持久化操作,避免对一个RDD反复进行计算,再进一步优化,使用序列化(kryo)的持久化级别
  2. 为了保证RDD持久化数据在可能丢失的情况下还能实现高可靠,则需要对RDD执行Checkpoint操作

四、JVM垃圾回收调优

  1. 如果内存设置不合理会导致大部分时间都消耗在垃圾回收上
  2. 默认情况下,Spark使用每个executor 60%的内存空间来缓存RDD,那么只有40%的内存空间来存放算子执行期间创建的对象
  3. 如果垃圾回收频繁发生,就需要对这个比例进行调优,通过参数spark.storage.memoryFraction来修改比例
4.1、JVM堆空间划分
  1. 分为两块年轻代老年代
  2. 年轻代存放短时间存活的对象,老年代存放长时间存活的对象
  3. 年轻代又划分为三块空间:Eden、Survivor1(简称s1)、Survivor2(简称s2)

五、数据本地化

数据本地化级别解释
PROCESS_LOCAL数据和计算它的代码在同一个JVM进程中
NODE_LOCAL数据和计算它的代码在一个节点上,但是不在一个JVM进程中
NO_PREF数据从哪里过来,性能都是一样的
RACK_LOCAL数据和计算它的代码在一个机架上
ANY数据可能在任意地方,比如其它网络环境内,或者其它机架上
  1. Spark倾向于使用最好的本地化级别调度task,但这是不现实的
  2. Spark默认会等待指定时间,期望task要处理的数据所在的节点上的executor空闲出一个cpu,从而将task分配过去,只要超过了时间,那么Spark就会将task分配到其它任意一个空闲的executor上
  3. 可以设置spark.locality系列参数,来调节Spark等待task可以进行数据本地化的时间
  • spark.locality.wait默认等待3秒(3000)
  • spark.locality.wait.process
  • spark.locality.wait.node
  • sparl.locality.wait.rack