携手创作,共同成长!这是我参与「掘金日新计划 · 12 月更文挑战」的第11天,点击查看活动详情
一、性能优化分析
- 一个计算任务的执行主要依赖于CPU、内存、带宽
- 实际工作中计算任务的性能瓶颈一般会出现在内存上
1.1、内存都去哪了
- 每个Java对象,都有一个对象头,会占用16个字节
- Java的String对象的对象头,会比它内部的原始数据,要多出40个字节
- Java中的集合类型,比如HashMap内部使用的链表数据结构,链表中的每一个数据,都使用了Entry对象来包装,Entry对象不光有对象头,还有指向下一个Entry的指针,通常占用8个字节
二、高性能序列化类库
- Spark倾向于序列化的便捷性,默认使用了Java序列化机制
- Java序列化机制的性能并不高,序列化的速度相对较慢,而且序列化以后的数据,相对于比较大
- Spark提供了两种序列化机制:Java序列化和Kryo序列化
- Kryo序列化比Java序列化更快,而且序列化后的数据更小,通常比Java序列化的数据要小10倍左右
如何使用:
- 首先要用SparkConf将Spark序列化器设置为KryoSerializer
- 使用Kryo时,针对需要序列化的类,需要预先进行注册,这样才能获得最佳的性能,如果不注册,Kryo必须时刻保存类的全类名,反而占用不少内存
- Spark默认对Scala中常用的类型自动在Kryo进行了注册
- 如果在自己的算子中,使用了外部的自定义类型的对象,那么还是需要对其进行注册
- 格式:conf.registerKryoClasses()
注意:
- 如果要序列化的自定义的类型,字段特别多,此时就需要对Kryo本身进行优化,因为Kryo内部的缓存可能不够存放那么大的Class对象
- 需要调用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
如果不使用kyro序列化,则占用144字节,如果数据量比较大,原始的Java序列化方式占用内存更多
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的优化
- 针对程序中多次被transformation或者action操作的RDD进行持久化操作,避免对一个RDD反复进行计算,再进一步优化,使用
序列化(kryo)的持久化级别 - 为了保证RDD持久化数据在可能丢失的情况下还能实现高可靠,则需要对RDD执行Checkpoint操作
四、JVM垃圾回收调优
- 如果内存设置不合理会导致大部分时间都消耗在垃圾回收上
- 默认情况下,Spark使用每个executor 60%的内存空间来缓存RDD,那么只有40%的内存空间来存放算子执行期间创建的对象
- 如果垃圾回收频繁发生,就需要对这个比例进行调优,通过参数spark.storage.memoryFraction来修改比例
4.1、JVM堆空间划分
- 分为两块
年轻代与老年代 - 年轻代存放短时间存活的对象,老年代存放长时间存活的对象
- 年轻代又划分为三块空间:Eden、Survivor1(简称s1)、Survivor2(简称s2)
五、数据本地化
| 数据本地化级别 | 解释 |
|---|---|
| PROCESS_LOCAL | 数据和计算它的代码在同一个JVM进程中 |
| NODE_LOCAL | 数据和计算它的代码在一个节点上,但是不在一个JVM进程中 |
| NO_PREF | 数据从哪里过来,性能都是一样的 |
| RACK_LOCAL | 数据和计算它的代码在一个机架上 |
| ANY | 数据可能在任意地方,比如其它网络环境内,或者其它机架上 |
- Spark倾向于使用最好的本地化级别调度task,但这是不现实的
- Spark默认会等待指定时间,期望task要处理的数据所在的节点上的executor空闲出一个cpu,从而将task分配过去,只要超过了时间,那么Spark就会将task分配到其它任意一个空闲的executor上
- 可以设置spark.locality系列参数,来调节Spark等待task可以进行数据本地化的时间
- spark.locality.wait默认等待3秒(3000)
- spark.locality.wait.process
- spark.locality.wait.node
- sparl.locality.wait.rack