spark性能调优:基础篇
本文作为Spark性能优化指南的基础篇,主要讲解开发调优以及资源调优。
1:避免创建重复的RDD
对于同一份数据,只应该创建一个RDD,不能创建多个RDD来代表同一份数据。
2:尽可能复用同一个RDD
如果要对一个RDD进行持久化,只要对这个RDD调用cache()和persist()即可。sc.textFile(“hdfs://192.168.0.1:9000/hello.txt”).persist(StorageLevel.MEMORY_AND_DISK_SER)
rdd1.map(…)
Spark的持久化级别
MEMORY_ONLY 使用未序列化的Java对象格式,将数据保存在内存中。如果内存不够存放所有的数据,则数据可能就不会进行持久化。那么下次对这个RDD执行算子操作时,那些没有被持久化的数据,需要从源头处重新计算一遍。这是默认的持久化策略,使用cache()方法时,实际就是使用的这种持久化策略。
MEMORY_AND_DISK 使用未序列化的Java对象格式,优先尝试将数据保存在内存中。如果内存不够存放所有的数据,会将数据写入磁盘文件中,下次对这个RDD执行算子时,持久化在磁盘文件中的数据会被读取出来使用。
MEMORY_ONLY_SER 基本含义同MEMORY_ONLY。唯一的区别是,会将RDD中的数据进行序列化,RDD的每个partition会被序列化成一个字节数组。这种方式更加节省内存,从而可以避免持久化的数据占用过多内存导致频繁GC。
MEMORY_AND_DISK_SER 基本含义同MEMORY_AND_DISK。唯一的区别是,会将RDD中的数据进行序列化,RDD的每个partition会被序列化成一个字节数组。这种方式更加节省内存,从而可以避免持久化的数据占用过多内存导致频繁GC。
3:尽量避免使用shuffle类算子
比如reduceByKey、join等算子,都会触发shuffle操作。
shuffle过程中,各个节点上的相同key都会先写入本地磁盘文件中,然后其他节点需要通过网络传输拉取各个节点上的磁盘文件中的相同key。而且相同key都拉取到同一个节点进行聚合操作时,还有可能会因为一个节点上处理的key过多,导致内存不够存放,进而溢写到磁盘文件中。因此在shuffle过程中,可能会发生大量的磁盘文件读写的IO操作,以及数据的网络传输操作。磁盘IO和网络数据传输也是shuffle性能较差的主要原因。
3.1:stage划分
Spark是根据shuffle类算子来进行stage的划分。如果我们的代码中执行了某个shuffle类算子(比如reduceByKey、join等),那么就会在该算子处,划分出一个stage界限来。
可以大致理解为,shuffle算子执行之前的代码会被划分为一个stage,shuffle算子执行以及之后的代码会被划分为下一个stage。
因此一个stage刚开始执行的时候,它的每个task可能都会从上一个stage的task所在的节点,去通过网络传输拉取需要自己处理的所有key,
然后对拉取到的所有相同的key使用我们自己编写的算子函数执行聚合操作(比如reduceByKey()算子接收的函数)。这个过程就是shuffle。
4:使用高性能的算子
使用reduceByKey/aggregateByKey替代groupByKey
使用mapPartitions替代普通map。mapPartitions类的算子,一次函数调用会处理一个partition所有的数据,而不是一次函数调用处理一条,性能相对来说会高一些。
使用foreachPartitions替代foreach:原理类似于“使用mapPartitions替代map”,实践中发现,对于1万条左右的数据量写MySQL,性能可以提升30%以上。
使用filter之后进行coalesce操作。通常对一个RDD执行filter算子过滤掉RDD中较多数据后(比如30%以上的数据),建议使用coalesce算子,手动减少RDD的partition数量
使用repartitionAndSortWithinPartitions替代repartition与sort类操作
5:广播大变量
有时在开发过程中,会遇到需要在算子函数中使用外部变量的场景(尤其是大变量,比如100M以上的大集合),那么此时就应该使用Spark的广播(Broadcast)功能来提升性能。
在算函数中使用到外部变量时,默认情况下,Spark会将该变量复制多个副本,通过网络传输到task:中,此时每个task都有一个变量副本。如果变量本身比较大的话(比如100M,甚至1G),那么大量的变量副本在网络中传输的性能开销,以及在各个节点的Executor中占用过多内存导致的频繁GC,都会极大地影响性能。
因此对于上述情况,如果使用的外部变量比较大,建议使用Spark的广播功能,对该变量进行广播。广播后的变量,会保证每个Executor的内存中,只驻留一份变量副本,而Executor中的task执行时共享该Executor中的那份变量副本。这样的话,可以大大减少变量副本的数量,从而减少网络传输的性能开销,并减少对Executor内存的占用开销,降低GC的频率。
// 每个Executor内存中,就只会驻留一份广播变量副本。
val list1Broadcast = sc.broadcast(list1)
6:使用Kryo优化序列化性能
原则九:优化数据结构
Java中,有三种类型比较耗费内存:
- 字符串,每个字符串内部都有一个字符数组以及长度等额外信息。
- 对象,每个Java对象都有对象头、引用等额外的信息,因此比较占用内存空间。
- 集合类型,比如HashMap、LinkedList等,因为集合类型内部通常会使用一些内部类来封装集合元素,比如Map.Ent`ry。
尽量使用字符串替代对象,使用原始类型(比如Int、Long)替代字符串,使用数组替代集合类型
7:资源调优
7.1:属性常用参数
(属性名称) Default(默认值) Meaning(含义)
spark.app.name (none) Spark 应用的名字。会在 SparkUI 和日志中出现。
spark.driver.cores 1 在 cluster 模式下,用几个 core 运行 driver 进程。
spark.driver.maxResultSize 1g Spark action 算子返回的结果集的最大数量。至少要 1M,可以设为 0 表示无限制。如果结果超过这一大小,Spark job 会直接中断退出。但是,设得过高有可能导致 driver 出现 out-of-memory 异常(取决于 spark.driver.memory 设置,以及驱动器 JVM 的内存限制)。设一个合理的值,以避免 driver 出现 out-of-memory 异常。
spark.driver.memory 1g driver进程可以使用的内存总量(例如:1g,2g)。注意,在 client 模式下,这个配置不能在 SparkConf 中直接设置,应为在那个时候 driver 进程的 JVM 已经启动了。因此需要在命令行里用 --driver-memory 选项 或者在默认属性配置文件里设置。
spark.executor.memory 1g 每个 executor 进程使用的内存总量(例如,2g,8g)
spark.streaming.concurrentJobs web中最多并发同时执行的job个数,资源够才多并发执行
spark.submit.deployMode (none) Spark driver 程序的部署模式,可以是 "client" 或 "cluster",意味着部署 dirver 程序本地("client")或者远程("cluster")在 Spark 集群的其中一个节点上。
spark.memory.fraction 0.6 用于执行和存储的RDD的空间大小。这个值越低,溢出和缓存数据逐出越频繁。此配置的目的是在稀疏、异常大的记录的情况下为内部元数据,用户数据结构和不精确的大小估计预留内存。推荐使用默认值。有关更多详细信息,包括关于在增加此值时正确调整 JVM 垃圾回收的重要信息,请参阅 this description。
spark.memory.storageFraction 0.5 不会被内存清除的总量,表示为 spark.memory.fraction 留出的区域大小的一小部分。这个越高,工作内存可能越少,执行和任务可能更频繁地溢出到磁盘。推荐使用默认值
spark.executor.cores 在 YARN 模式下默认为 1,standlone 和 Mesos 粗粒度模型中的 worker 节点的所有可用的 core
spark.default.parallelism 对于分布式混洗(shuffle)操作,如 reduceByKey 和 join,父 RDD 中分区的最大数量。对于没有父 RDD 的 parallelize 操作,它取决于集群管理器
spark.driver.host (local hostname) 要监听的 driver 的主机名或 IP 地址。这用于与 executors 和 standalone Master 进行通信。
spark.driver.port (random) 要监听的 driver 的端口。这用于与 executors 和 standalone Master 进行通信。
spark.cores.max (not set) standalon和messon模式采用
spark.streaming.blockInterval 200ms 在这个时间间隔(ms)内,通过 Spark Streaming receivers 接收的数据在保存到 Spark 之前,分为数据块,和task个数一致
spark.streaming.receiver.maxRate not set 每秒钟每个 receiver 将接收的数据的最大速率(每秒钟的记录数目)。有效的情况下,每个流每秒将最多消耗这个数目的记录。
spark.streaming.kafka.maxRetries 1 driver 连续重试的最大次数,以此找到每个分区 leader 的最近的(latest)的偏移量(默认为 1 意味着 driver 将尝试最多两次)。仅应用于新的 kafka direct stream API。
spark.streaming.stopGracefullyOnShutdown false 如果为true,Spark会StreamingContext在JVM关闭时正常关闭,而不是立即关闭。
spark.streaming.receiver.writeAheadLog.enable 假 为接收者启用预写日志。以牺牲接受吞吐量为代价,通过接收器接收的所有输入数据将被保存到预先写入的日志中,以便在驱动程序发生故障后可以将其恢复。
7.1:原理
Executor的内存主要分为三块:第一块是让task执行我们自己编写的代码时使用,默认是占Executor总内存的20%;第二块是让task通过shuffle过程拉取了上一个stage的task的输出后,进行聚合等操作时使用,默认也是占Executor总内存的20%;第三块是让RDD持久化时使用,默认占Executor总内存的60%。
task的执行速度是跟每个Executor进程的CPU core数量有直接关系的。一个CPU core同一时间只能执行一个线程。而每个Executor进程上分配到的多个task,都是以每个task一条线程的方式,多线程并发运行的。如果CPU core数量比较充足,而且分配到的task数量比较合理,那么通常来说,可以比较快速和高效地执行完这些task线程。
7.2:参数配置
num-executors
参数说明:该参数用于设置Spark作业总共要用多少个Executor进程来执行。Driver在向YARN集群管理器申请资源时,YARN集群管理器会尽可能按照你的设置来在集群的各个工作节点上,启动相应数量的Executor进程。这个参数非常之重要,如果不设置的话,默认只会给你启动少量的Executor进程,此时你的Spark作业的运行速度是非常慢的。
参数调优建议:每个Spark作业的运行一般设置50~100个左右的Executor进程比较合适,设置太少或太多的Executor进程都不好。设置的太少,无法充分利用集群资源;设置的太多的话,大部分队列可能无法给予充分的资源。
executor-memory
参数说明:该参数用于设置每个Executor进程的内存。Executor内存的大小,很多时候直接决定了Spark作业的性能,而且跟常见的JVM OOM异常,也有直接的关联。
参数调优建议:每个Executor进程的内存设置4G~8G较为合适。避免你自己的Spark作业占用了队列所有的资源,导致别的作业无法运行。
膨胀系数1.5-2
假设一批数据500w,单条2kb
num-executors=task/executor-cores。executor-cores一般2-4即可。
executor-memory=500w*2k*1.5/1024/1024(G)
批次处理时间:按实际经验,3-4w/core/min,可根据此进行大致的总核数计算,再进行实际数据测试
executor-cores
参数说明:该参数用于设置每个Executor进程的CPU core数量。这个参数决定了每个Executor进程并行执行task线程的能力。因为每个CPU core同一时间只能执行一个task线程,因此每个Executor进程的CPU core数量越多,越能够快速地执行完分配给自己的所有task线程。
参数调优建议:Executor的CPU core数量设置为2~4个较为合适。
driver-memory
参数说明:该参数用于设置Driver进程的内存。
参数调优建议:Driver的内存通常来说不设置,或者设置1G左右应该就够了。唯一需要注意的一点是,如果需要使用collect算子将RDD的数据全部拉取到Driver上进行处理,那么必须确保Driver的内存足够大,否则会出现OOM内存溢出的问题。
spark.default.parallelism
参数说明:该参数用于设置每个stage的默认task数量。这个参数极为重要,如果不设置可能会直接影响你的Spark作业性能。
参数调优建议:Spark作业的默认task数量为500~1000个较为合适。很多同学常犯的一个错误就是不去设置这个参数,那么此时就会导致Spark自己根据底层HDFS的block数量来设置task的数量,默认是一个HDFS block对应一个task。通常来说,Spark默认设置的数量是偏少的(比如就几十个task),如果task数量偏少的话,就会导致你前面设置好的Executor的参数都前功尽弃。试想一下,无论你的Executor进程有多少个,内存和CPU有多大,但是task只有1个或者10个,那么90%的Executor进程可能根本就没有task执行,也就是白白浪费了资源!因此Spark官网建议的设置原则是,设置该参数为num-executors * executor-cores的2~3倍较为合适,比如Executor的总CPU core数量为300个,那么设置1000个task是可以的,此时可以充分地利用Spark集群的资源。
spark.storage.memoryFraction
参数说明:该参数用于设置RDD持久化数据在Executor内存中能占的比例,默认是0.6。也就是说,默认Executor 60%的内存,可以用来保存持久化的RDD数据。根据你选择的不同的持久化策略,如果内存不够时,可能数据就不会持久化,或者数据会写入磁盘。
参数调优建议:如果Spark作业中,有较多的RDD持久化操作,该参数的值可以适当提高一些,保证持久化的数据能够容纳在内存中。避免内存不够缓存所有的数据,导致数据只能写入磁盘中,降低了性能。但是如果Spark作业中的shuffle类操作比较多,而持久化操作比较少,那么这个参数的值适当降低一些比较合适。此外,如果发现作业由于频繁的gc导致运行缓慢(通过spark web ui可以观察到作业的gc耗时),意味着task执行用户代码的内存不够用,那么同样建议调低这个参数的值。
spark.shuffle.memoryFraction
参数说明:该参数用于设置shuffle过程中一个task拉取到上个stage的task的输出后,进行聚合操作时能够使用的Executor内存的比例,默认是0.2。也就是说,Executor默认只有20%的内存用来进行该操作。shuffle操作在进行聚合时,如果发现使用的内存超出了这个20%的限制,那么多余的数据就会溢写到磁盘文件中去,此时就会极大地降低性能。
参数调优建议:如果Spark作业中的RDD持久化操作较少,shuffle操作较多时,建议降低持久化操作的内存占比,提高shuffle操作的内存占比比例,避免shuffle过程中数据过多时内存不够用,必须溢写到磁盘上,降低了性能。此外,如果发现作业由于频繁的gc导致运行缓慢,意味着task执行用户代码的内存不够用,那么同样建议调低这个参数的值。
资源参数的调优,没有一个固定的值,需要根据自己的实际情况(包括Spark作业中的shuffle操作数量、RDD持久化操作数量以及spark web ui中显示的作业gc情况)合理地设置上述参数。