1. 资源调优
1.1 搭建 Spark 集群的时候配置 CPU 和内存
在 Spark 安装包的 conf 下 spark-env.sh 配置
配置 | 说明 | 默认值 |
---|---|---|
SPARK_WORKER_CORES | 作业可用的 CPU 内核数量 | 所有可用 CPU 核数 |
SPARK_WORKER_INSTANCES | 每台机器上运行的 worker 数量 | 1 |
SPARK_WORKER_CORES × SPARK_WORKER_INSTANCES | 每台机器总cores | |
SPARK_WORKER_MEMORY | 作业可使用的内存容量 | 1G |
SPARK_WORKER_INSTANCES × SPARK_WORKER_MEMORY | 每个节点使用的最大内存数 | |
SPARK_DAEMON_MEMORY | 分配给Spark master和worker守护进程的内存空间 | 512M |
1.2 在提交 Application 的时候给 Application 分配更多的资源
提交命令选项:
每个executor使用的core数,spark on yarn 默认为1,standalone默认为worker上所有可用的core。
--executor-cores
每个executor内存大小(例如:2G),默认1G
--executor-memory
executor使用的总核数,仅限于SparkStandalone、Spark on Mesos模式
--total-executor-cores
2. 并行调优
原则:一个 core 一般分配 2~3 个task,每个 task 一般处理 1G 数据。
提高并行度的方式:
-
如果读取的数据在 HDFS 上,降低 block 块的大小。
-
sc.textFile(path,numPartitions)
-
sc.parallelize(list, numPartitions)
一般用于测试 -
coalesce
、repartition
可以提高 RDD 的分区数。 -
修改配置信息:
spark.default.parallelism:4
:不设置默认 executor core 的总个数spark.sql.shuffle.partitions 200
-
自定义分区器
3. 代码调优
3.1 避免创建重复的 RDD
val rdd1 = sc.textFile("xxx")
val rdd2 = sc.textFile("xxx")
在执行效率上没有区别,但是代码乱。
-
在其他得 job 中对于重复使用的 RDD 要使用持久化算子
-
cache:
MEMORY_ONLY
-
persist:
MEMORY_ONLY
MEMORY_ONLY_SER
MEMORY_AND_DISK_SER
一般不要选择带有_2的持久化级别
-
checkpoint
如果一个 RDD 的计算时间比较长或者计算起来比较复杂,一般将这个 RDD 的计算结果保存到 HDFS 上,这样数据会更安全。
如果一个 RDD 的依赖关系非常长,也会使用 checkpoint,会切断依赖关系,提高容错的效率。
-
3.2 尽量使用广播变量
开发过程中,会遇到需要在算子函数中使用外部变量的场景(尤其是大变量,比如 100M 以上的大集合)。那么此时就应该使用 Spark 的广播变量(Broad
cast) 功能来提升性能,函数中使用到外部变量时,默认情况下,Spark 会将该变量复制多个副本,通过网络传输到 task 中,此时每个 task 都有一个变量副本。如果变量本身比较大的话(比如 100M,甚至 1G),那么大量的变量副本在网络中传输的性能开销,以及在各个阶段的 Executor 中占用过多内存导致频繁的 GC,都会极大的影响性能。如果使用的外部变量比较大,建议使用 Spark 的广播功能,对该变量进行广播,广播后的变量,会爆炸每个 Executor的内存中,只驻留一份变量副本,而 Executor 中的 task 执行时共享该 Executor 中的那份变量副本。这样的话,可以大大减少变量副本的数量,从而减少网络传输的性能开销,并减少对 Eexcutor 内存占用,降低 GC 的频率。
广播大变量发送方式:Executor 一开始并没有广播变量,而是 task 运行需要用到广播变量会找 Executor 的 BlockManager 要。BlockManager 找 Driver 里面的 BlockManagerMaster 要。
使用广播变量可以大大的降低集群中变量的副本数。
不使用广播变量:变量的副本数和 task 数一致。
使用广播变量:变量的副本数与 Executor 数一致。
广播变量最大占用内存:ExecutorMemory * 60% * 90% * 80%
3.3 尽量避免使用 shuffle 类的算子
使用广播变量来模拟使用 join,使用场景:一个 RDD 比较大,一个RDD 比较小。
join 算子 = 广播变量 + filter、广播变量 + map、广播变量 + flatMap
3.4 使用 map-side 预聚合的 shuffle 操作
尽量使用 combiner 的 shuffle 类算子。
combiner 概念:在 map 端,每个 map task 计算完毕后进行的局部聚合。
combiner 好处:
- 降低 shuffle write 写磁盘的数据量。
- 降低shuffle read 拉取数据量的大小。
- 降低 reduce 端聚合的次数。
有 commbiner 的 shuffle 类算子:
reduceByKey
:这个算子在 map 端是有 combiner 的,在一些场景中可以使用 reduceByKey 代替 groupByKey。aggregateByKey
。combineByKey
。
3.5 尽量使用高性能的算子
- 使用
reduceByKey
替代groupBkKey。
- 使用
mapPartition
代替map。
- 使用
foreachPartition
替代foreach。
filter
后使用coalesce
减少分区数。- 使用
repartitionAndSortWithinPartitions
替代repartition
与sort
操作。
3.6 使用 Kryo 优化序列化性能
3.6.1 在 Spark 中,主要有三个地方涉及到了序列化:
- 在算子函数中使用到外部变量时,该变量会被序列化后进行网络传输。
- 将自定义的类型作为 RDD 的泛型时(比如
JavaRDD<User>
,User 是自定义类型),所有自定义类型对象,都会进行序列化。因此,这种情况下,也要求自定义的类必须实现Serializable
接口。 - 使用可序列化的持久化策略时 (比如 MEMORY_ONLY_SER),Spark 会将 RDD 中的 partition都序列化成一个大的字节数组。
3.6.2 Kryo 序列化器介绍
Spark 支持使用 Kryo 序列化机制。Kryo 序列化机制比默认的 Java 序列化机制,速度要快,序列化后的数据要更小,大概是 Java 序列化机制的 1/10 。所以 Kryo 序列化后,可以让网络传输的数据变少。在集群中耗费的内存资源大大减少。
对于这三种出现序列化的地方,我们都可以通过使用 Kryo 序列化类库来优化序列化和反序列化的性能。Spark 默认使用的是 Java 的序列化机制,也就是 ObjectOutputStream/ObjectInputStream API
来进行序列化和反序列化。但是 Spark 同时支持使用 Kryo 序列化库,Kryo 序列化库类库的性能比 Java 序列化类库的性能要高的多。官方介绍,Kryo 序列化机制比 Java 序列化机制,性能高 10 倍左右。Spark 之所以默认没有使用 Kryo 序列化类库,是因为 Kryo 要求最好需要注册所需要进行序列化的自定义类型,因此对于开发者来说,这种方式比较麻烦。
3.6.3 Spark 中使用 Kryo
SparkConf conf = new SparkConf();
conf.setMaster("local");
conf .set("spark.serializer","org.apache.spark.serializer.KryoSerializer");
conf.registerKryoClasses(new Class[]{CameraAggInfo.class});
3.7 优化数据结构
java 中有三种类型比较消耗内存:
- 对象,每个 Java 对象都有对象头、引用等额外的信息,因此比较占用内存空间。
- 字符串,每个字符串内都有一个字符数组已经长度等额外信息。
- 集合类型,比如 HashMap、List 等,因为集合内部通常会使用一些内部类来封装集合元素,比如:Map.Entry。
因此 Spark官方建议,在Spark编码中,特别是对算子函数中的代码,jinlbuy使用上述三种数据结构。尽量使用字符串替代对象,使用原始数据类型(比如Int、Long)代替字符串,使用数组代替集合类型,这样尽可能的减少内存占用,从而降低 GC 频率,提示性能。
3.8 使用高性能的库 fastutil
fastutil 是扩展了 Java 标准集合框架(Map、List、Set)的类库,提供类特殊类型的 map、set、list 和 queue;fastutil 能够提供更小的内存占用,更快的存取速度。使用 fastutil 提供的集合类来代替自己平时使用的 JDK 原生的集合好处在于 fastutil 集合类,可以减少内存的占用,并且在进行集合的遍历、根据索引(或者 key) 获取元素的值和设置元素的值的时候,提供更快的存取速度。fastutil 的每一种集合类型,都实现了对应的 Java 中的标准接口(比如 fastutil 的 map,实现了 Java 的 Map 接口),因此可以直接放入已有系统的任何代码中。
fastutil 最新版要求 Java 7 及以上版本。