大数据面试技能|Spark性能优化-开发调优篇
大家好,我是威5!
今天给大家分享大数据面试中关于spark会问到的开发方面调优的问题,仅供参考,希望对大家有所帮助。
原则一:避免创建重复的RDD
在开发过程中要注意:对于同一份数据,只应该创建一个RDD,不能创建多个RDD来代表同一份数据。否则,Spark作业会进行多次重复计算来创建多个代表相同数据的RDD,进而增加了作业的性能开销。一个简单的例子
1 需要对名为“hello.txt”的HDFS文件进行一次map操作,再进行一次reduce操作 2 //也就是说,需要对一份数据执行两次算子操作。 3 //错误的做法:对于同一份数据执行多次算子操作时,创建多个RDD。 4 //这里执行了两次textFile方法,针对同一个HDFS文件,创建了两个RDD出来, 5 //然后分别对每个RDD都执行了一个算子操作。 6 //这种情况下,Spark需要从HDFS上两次加载hello.txt文件的内容,并创建两个单独的RDD; 7 //第二次加载HDFS文件以及创建RDD的性能开销,很明显是白白浪费掉的。 8 val rdd1 = sc.textFile("[hdfs://192.168.0.1:9000/hello.txt](hdfs://192.168.0.1:9000/hello.txt)") 9 rdd1.map(...) 10 val rdd2 = sc.textFile("[hdfs://192.168.0.1:9000/hello.txt](hdfs://192.168.0.1:9000/hello.txt)") 11 rdd2.reduce(...) 12 //正确的用法:对于一份数据执行多次算子操作时,只使用一个RDD。 13 //这种写法很明显比上一种写法要好多了,因为我们对于同一份数据只创建了一个RDD, 14 //然后对这一个RDD执行了多次算子操作。 15 //但是要注意到这里为止优化还没有结束,由于rdd1被执行了两次算子操作,第二次执行reduce操作的时候, 16 //还会再次从源头处重新计算一次rdd1的数据,因此还是会有重复计算的性能开销。 17 //要彻底解决这个问题,必须结合“原则三:对多次使用的RDD进行持久化”, 18 //才能保证一个RDD被多次使用时只被计算一次。 19 val rdd1 = sc.textFile("[hdfs://192.168.0.1:9000/hello.txt](hdfs://192.168.0.1:9000/hello.txt)") 20 rdd1.map(...) 21 rdd1.reduce(...)
原则二:尽可能复用同一个RDD
除了要避免在开发过程中对一份完全相同的数据创建多个RDD之外,在对不同的数据执行算子操作时还要尽可能地复用一个RDD。比如说,有一个RDD的数据格式是key-value类型的,另一个是单value类型的,这两个RDD的value数据是完全一样的。那么此时我们可以只使用key-value类型的那个RDD,因为其中已经包含了另一个的数据。对于类似这种多个RDD的数据有重叠或者包含的情况,我们应该尽量复用一个RDD,这样可以尽可能地减少RDD的数量,从而尽可能减少算子执行的次数。
一个简单的例子
1 // 错误的做法。 2 // 有一个<long , String>格式的RDD,即rdd1。 3 // 接着由于业务需要,对rdd1执行了一个map操作,创建了一个rdd2, 4 //而rdd2中的数据仅仅是rdd1中的value值而已,也就是说,rdd2是rdd1的子集。 5 JavaPairRDD</long><long , String> rdd1 = ... 6 JavaRDD<string> rdd2 = rdd1.map(...) 7 // 分别对rdd1和rdd2执行了不同的算子操作。 8 rdd1.reduceByKey(...) 9 rdd2.map(...) 10 // 正确的做法。 11 // 上面这个case中,其实rdd1和rdd2的区别无非就是数据格式不同而已, 12 //rdd2的数据完全就是rdd1的子集而已,却创建了两个rdd,并对两个rdd都执行了一次算子操作。 13 // 此时会因为对rdd1执行map算子来创建rdd2,而多执行一次算子操作,进而增加性能开销。 14 // 其实在这种情况下完全可以复用同一个RDD。 15 // 我们可以使用rdd1,既做reduceByKey操作,也做map操作。 16 // 在进行第二个map操作时,只使用每个数据的tuple._2,也就是rdd1中的value值,即可。 17 JavaPairRDD<long , String> rdd1 = ... 18 rdd1.reduceByKey(...) 19 rdd1.map(tuple._2...) 20 // 第二种方式相较于第一种方式而言,很明显减少了一次rdd2的计算开销。 21 // 但是到这里为止,优化还没有结束,对rdd1我们还是执行了两次算子操作,rdd1实际上还是会被计算两次。 22 // 因此还需要配合“原则三:对多次使用的RDD进行持久化”进行使用, 23 //才能保证一个RDD被多次使用时只被计算一次。
原则三:对多次使用的RDD进行持久化
当你在Spark代码中多次对一个RDD做了算子操作后,恭喜,你已经实现Spark作业第一步的优化了,也就是尽可能复用RDD。此时就该在这个基础之上,进行第二步优化了,也就是要保证对一个RDD执行多次算子操作时,这个RDD本身仅仅被计算一次。Spark中对于一个RDD执行多次算子的默认原理是这样的:每次你对一个RDD执行一个算子操作时,都会重新从源头处计算一遍,计算出那个RDD来,然后再对这个RDD执行你的算子操作。这种方式的性能是很差的。因此对于这种情况,我们的建议是:对多次使用的RDD进行持久化。此时Spark就会根据你的持久化策略,将RDD中的数据保存到内存或者磁盘中。以后每次对这个RDD进行算子操作时,都会直接从内存或磁盘中提取持久化的RDD数据,然后执行算子,而不会从源头处重新计算一遍这个RDD,再执行算子操作。对多次使用的RDD进行持久化的代码示例
1 // 如果要对一个RDD进行持久化,只要对这个RDD调用cache()和persist()即可。 2 // 正确的做法。 3 // cache()方法表示:使用非序列化的方式将RDD中的数据全部尝试持久化到内存中。 4 // 此时再对rdd1执行两次算子操作时,只有在第一次执行map算子时,才会将这个rdd1从源头处计算一次。 5 // 第二次执行reduce算子时,就会直接从内存中提取数据进行计算,不会重复计算一个rdd。 6 val rdd1 = sc.textFile("[hdfs://192.168.0.1:9000/hello.txt](hdfs://192.168.0.1:9000/hello.txt)").cache() 7 rdd1.map(...) 8 rdd1.reduce(...) 9 // persist()方法表示:手动选择持久化级别,并使用指定的方式进行持久化。 10 // 比如说,StorageLevel.MEMORY_AND_DISK_SER表示,内存充足时优先持久化到内存中, 11 //内存不充足时持久化到磁盘文件中。 12 // 而且其中的_SER后缀表示,使用序列化的方式来保存RDD数据,此时RDD中的每个partition 13 //都会序列化成一个大的字节数组,然后再持久化到内存或磁盘中。 14 // 序列化的方式可以减少持久化的数据对内存/磁盘的占用量,进而避免内存被持久化数据占用过多, 15 //从而发生频繁GC。 16 val rdd1 = sc.textFile("[hdfs://192.168.0.1:9000/hello.txt](hdfs://192.168.0.1:9000/hello.txt)") 17 .persist(StorageLevel.MEMORY_AND_DISK_SER) 18 rdd1.map(...) 19 rdd1.reduce(...)
对于persist()方法而言,我们可以根据不同的业务场景选择不同的持久化级别。Spark的持久化级别
如何选择一种最合适的持久化策略?
1、默认情况下,性能最高的当然是MEMORY_ONLY,但前提是你的内存必须足够足够大,可以绰绰有余地存放下整个RDD的所有数据。因为不进行序列化与反序列化操作,就避免了这部分的性能开销;对这个RDD的后续算子操作,都是基于纯内存中的数据的操作,不需要从磁盘文件中读取数据,性能也很高;而且不需要复制一份数据副本,并远程传送到其他节点上。但是这里必须要注意的是,在实际的生产环境中,恐怕能够直接用这种策略的场景还是有限的,如果RDD中数据比较多时(比如几十亿),直接用这种持久化级别,会导致JVM的OOM内存溢出异常。
2、如果使用MEMORY_ONLY级别时发生了内存溢出,那么建议尝试使用MEMORY_ONLY_SER级别。该级别会将RDD数据序列化后再保存在内存中,此时每个partition仅仅是一个字节数组而已,大大减少了对象数量,并降低了内存占用。这种级别比MEMORY_ONLY多出来的性能开销,主要就是序列化与反序列化的开销。但是后续算子可以基于纯内存进行操作,因此性能总体还是比较高的。此外,可能发生的问题同上,如果RDD中的数据量过多的话,还是可能会导致OOM内存溢出的异常。
3、如果纯内存的级别都无法使用,那么建议使用MEMORY_AND_DISK_SER策略,而不是MEMORY_AND_DISK策略。因为既然到了这一步,就说明RDD的数据量很大,内存无法完全放下。序列化后的数据比较少,可以节省内存和磁盘的空间开销。同时该策略会优先尽量尝试将数据缓存在内存中,内存缓存不下才会写入磁盘。
4、通常不建议使用DISK_ONLY和后缀为_2的级别:因为完全基于磁盘文件进行数据的读写,会导致性能急剧降低,有时还不如重新计算一次所有RDD。后缀为_2的级别,必须将所有数据都复制一份副本,并发送到其他节点上,数据复制以及网络传输会导致较大的性能开销,除非是要求作业的高可用性,否则不建议使用。
原则四:尽量避免使用shuffle类算子
如果有可能的话,要尽量避免使用shuffle类算子。因为Spark作业运行过程中,最消耗性能的地方就是shuffle过程。shuffle过程,简单来说,就是将分布在集群中多个节点上的同一个key,拉取到同一个节点上,进行聚合或join等操作。比如reduceByKey、join等算子,都会触发shuffle操作。shuffle过程中,各个节点上的相同key都会先写入本地磁盘文件中,然后其他节点需要通过网络传输拉取各个节点上的磁盘文件中的相同key。而且相同key都拉取到同一个节点进行聚合操作时,还有可能会因为一个节点上处理的key过多,导致内存不够存放,进而溢写到磁盘文件中。因此在shuffle过程中,可能会发生大量的磁盘文件读写的IO操作,以及数据的网络传输操作。磁盘IO和网络数据传输也是shuffle性能较差的主要原因。因此在我们的开发过程中,能避免则尽可能避免使用reduceByKey、join、distinct、repartition等会进行shuffle的算子,尽量使用map类的非shuffle算子。这样的话,没有shuffle操作或者仅有较少shuffle操作的Spark作业,可以大大减少性能开销。Broadcast与map进行join代码示例
1 // 传统的join操作会导致shuffle操作。 2 // 因为两个RDD中,相同的key都需要通过网络拉取到一个节点上,由一个task进行join操作。 3 val rdd3 = rdd1.join(rdd2) 4 // Broadcast+map的join操作,不会导致shuffle操作。 5 // 使用Broadcast将一个数据量较小的RDD作为广播变量。 6 val rdd2Data = rdd2.collect() 7 val rdd2DataBroadcast = sc.broadcast(rdd2Data) 8 // 在rdd1.map算子中,可以从rdd2DataBroadcast中,获取rdd2的所有数据。 9 // 然后进行遍历,如果发现rdd2中某条数据的key与rdd1的当前数据的key是相同的, 10 //那么就判定可以进行join。 11 // 此时就可以根据自己需要的方式,将rdd1当前数据与rdd2中可以连接的数据, 12 //拼接在一起(String或Tuple)。 13 val rdd3 = rdd1.map(rdd2DataBroadcast...) 14 // 注意,以上操作,建议仅仅在rdd2的数据量比较少(比如几百M,或者一两G)的情况下使用。 15 // 因为每个Executor的内存中,都会驻留一份rdd2的全量数据。
原则五:使用map-side预聚合的shuffle操作
如果因为业务需要,一定要使用shuffle操作,无法用map类的算子来替代,那么尽量使用可以map-side预聚合的算子。
所谓的map-side预聚合,说的是在每个节点本地对相同的key进行一次聚合操作,类似于MapReduce中的本地combiner。map-side预聚合之后,每个节点本地就只会有一条相同的key,因为多条相同的key都被聚合起来了。其他节点在拉取所有节点上的相同key时,就会大大减少需要拉取的数据数量,从而也就减少了磁盘IO以及网络传输开销。
通常来说,在可能的情况下,建议使用reduceByKey或者aggregateByKey算子来替代掉groupByKey算子。因为reduceByKey和aggregateByKey算子都会使用用户自定义的函数对每个节点本地的相同key进行预聚合。而groupByKey算子是不会进行预聚合的,全量的数据会在集群的各个节点之间分发和传输,性能相对来说比较差。比如下图,就是典型的例子,分别基于reduceByKey和groupByKey进行单词计数。其中第一张图是groupByKey的原理图,可以看到,没有进行任何本地聚合时,所有数据都会在集群节点之间传输;第二张图是reduceByKey的原理图,可以看到,每个节点本地的相同key数据,都进行了预聚合,然后才传输到其他节点上进行全局聚合。
原则六:使用高性能的算子
除了shuffle相关的算子有优化原则之外,其他的算子也都有着相应的优化原则。使用reduceByKey/aggregateByKey替代groupByKey详情见“原则五:使用map-side预聚合的shuffle操作”。
使用mapPartitions替代普通map
mapPartitions类的算子,一次函数调用会处理一个partition所有的数据,而不是一次函数调用处理一条,性能相对来说会高一些。但是有的时候,使用mapPartitions会出现OOM(内存溢出)的问题。因为单次函数调用就要处理掉一个partition所有的数据,如果内存不够,垃圾回收时是无法回收掉太多对象的,很可能出现OOM异常。所以使用这类操作时要慎重!
使用foreachPartitions替代foreach
原理类似于“使用mapPartitions替代map”,也是一次函数调用处理一个partition的所有数据,而不是一次函数调用处理一条数据。在实践中发现,foreachPartitions类的算子,对性能的提升还是很有帮助的。比如在foreach函数中,将RDD中所有数据写MySQL,那么如果是普通的foreach算子,就会一条数据一条数据地写,每次函数调用可能就会创建一个数据库连接,此时就势必会频繁地创建和销毁数据库连接,性能是非常低下;但是如果用foreachPartitions算子一次性处理一个partition的数据,那么对于每个partition,只要创建一个数据库连接即可,然后执行批量插入操作,此时性能是比较高的。实践中发现,对于1万条左右的数据量写MySQL,性能可以提升30%以上。
使用filter之后进行coalesce操作
通常对一个RDD执行filter算子过滤掉RDD中较多数据后(比如30%以上的数据),建议使用coalesce算子,手动减少RDD的partition数量,将RDD中的数据压缩到更少的partition中去。因为filter之后,RDD的每个partition中都会有很多数据被过滤掉,此时如果照常进行后续的计算,其实每个task处理的partition中的数据量并不是很多,有一点资源浪费,而且此时处理的task越多,可能速度反而越慢。因此用coalesce减少partition数量,将RDD中的数据压缩到更少的partition之后,只要使用更少的task即可处理完所有的partition。在某些场景下,对于性能的提升会有一定的帮助。
分区多 -》 分区减少/合并 coalesce
分区少 -》 分区扩大 reparation
coalesce 和 reparation的区别:
repartition: 重新分区, 有shuffle
coalesce: 合并分区 / 减少分区,默认不shuffle,默认 coalesce 不能扩大分区数量。除非添加true的参数,或者使用repartition。
适用场景: 1、如果要shuffle,都用 repartition 2、不需要shuffle,仅仅是做分区的合并,coalesce 3、repartition常用于扩大分区。
使用repartitionAndSortWithinPartitions替代repartition与sort类操作
repartitionAndSortWithinPartitions是Spark官网推荐的一个算子,官方建议,如果需要在repartition重分区之后,还要进行排序,建议直接使用repartitionAndSortWithinPartitions算子。因为该算子可以一边进行重分区的shuffle操作,一边进行排序。shuffle与sort两个操作同时进行,比先shuffle再sort来说,性能可能是要高的。
必须要熟悉的算子操作:
1)transformation算子
转换 |
含义 |
map(func) |
返回一个新的RDD,该RDD由每一个输入元素经过func函数转换后组成 |
mapPartitions(func) |
类似于map,但独立地在RDD的每一个分片上运行,因此在类型为T的RDD上运行时,func的函数类型必须是Iterator[T] => Iterator[U] |
mapPartitionsWithIndex(func) |
类似于mapPartitions,但func带有一个整数参数表示分片的索引值,因此在类型为T的RDD上运行时,func的函数类型必须是(Int, Interator[T]) => Iterator[U] |
filter(func) |
返回一个新的RDD,该RDD由经过func函数计算后返回值为true的输入元素组成 |
flatMap(func) |
类似于map,但是每一个输入元素可以被映射为0或多个输出元素(所以func应该返回一个序列,而不是单一元素)---扁平化操作 |
union(otherDataset) |
对源RDD和参数RDD求并集后返回一个新的RDD |
intersection(otherDataset) |
对源RDD和参数RDD求交集后返回一个新的RDD |
distinct([numTasks])) |
对源RDD进行去重后返回一个新的RDD |
groupByKey([numTasks]) |
在一个(K,V)的RDD上调用,返回一个(K, Iterator[V])的RDD |
reduceByKey(func, [numTasks]) |
在一个(K,V)的RDD上调用,返回一个(K,V)的RDD,使用指定的reduce函数,将相同key的值聚合到一起,与groupByKey类似,reduce任务的个数可以通过第二个可选的参数来设置 |
sortByKey([ascending], [numTasks]) |
在一个(K,V)的RDD上调用,K必须实现Ordered接口,返回一个按照key进行 排序的(K,V)的RDD |
sortBy(func,[ascending], [numTasks]) |
与sortByKey类似,但是更灵活 |
join(otherDataset, [numTasks]) |
在类型为(K,V)和(K,W)的RDD上调用,返回一个相同key对应的所有元素对在一起的(K,(V,W))的RDD |
cogroup(otherDataset, [numTasks]) |
在类型为(K,V)和(K,W)的RDD上调用,返回一个(K,(Iterable<V>,Iterable<W>))类型的RDD |
coalesce(numPartitions) |
减少 RDD 的分区数到指定值。 |
repartition(numPartitions) |
重新给 RDD 分区 |
repartitionAndSortWithinPartitions(partitioner) |
重新给 RDD 分区,并且每个分区内以记录的 key 排序 |
2)action算子
动作 |
含义 |
reduce(func) |
reduce将RDD中元素前两个传给输入函数,产生一个新的return值, 新产生的return值 与RDD中下一个元素(第三个元素)组成两个元素,再被传给输入函数, 直到最后只有一个值为止。 |
collect() |
在驱动程序中,以数组的形式返回数据集的所有元素。以数组的形式返回给Driver端 |
count() |
返回RDD的元素个数 |
first() |
返回RDD的第一个元素(类似于take(1)) |
take(n) |
返回一个由数据集的前n个元素组成的数组 |
takeOrdered(n, [ordering]) |
返回自然顺序或者自定义顺序的前 n 个元素 |
saveAsTextFile(path) |
将数据集的元素以textfile的形式保存到HDFS文件系统或者其他支持的文件系统, 对于每个元素,Spark将会调用toString方法,将它装换为文件中的文本 |
saveAsSequenceFile(path) |
将数据集中的元素以Hadoop sequencefile的格式保存到指定的目录下, 可以使HDFS或者其他Hadoop支持的文件系统。 |
saveAsObjectFile(path) |
将数据集的元素,以 Java 序列化的方式保存到指定的目录下 |
countByKey() |
针对(K,V)类型的RDD,返回一个(K,Int)的map,表示每一个key对应的元素个数。 |
foreach(func) |
在数据集的每一个元素上,运行函数func |
foreachPartition(func) |
在数据集的每一个分区上,运行函数func |
原则七:广播大变量(只能在Driver端定义)
有时在开发过程中,会遇到需要在算子函数中使用外部变量的场景(尤其是大变量,比如100M以上的大集合),那么此时就应该使用Spark的广播(Broadcast)功能来提升性能。在算子函数中使用到外部变量时,默认情况下,Spark会将该变量复制多个副本,通过网络传输到task中,此时每个task都有一个变量副本。如果变量本身比较大的话(比如100M,甚至1G),那么大量的变量副本在网络中传输的性能开销,以及在各个节点的Executor中占用过多内存导致的频繁GC,都会极大地影响性能。因此对于上述情况,如果使用的外部变量比较大,建议使用Spark的广播功能,对该变量进行广播。广播后的变量,会保证每个Executor的内存中,只驻留一份变量副本,而Executor中的task执行时共享该Executor中的那份变量副本(在Driver端可以修改广播变量的值,在Executor端无法修改广播变量的值)。这样的话,可以大大减少变量副本的数量,从而减少网络传输的性能开销,并减少对Executor内存的占用开销,降低GC的频率。广播大变量的代码示例
1 // 以下代码在算子函数中,使用了外部的变量。 2 // 此时没有做任何特殊操作,每个task都会有一份list1的副本。 3 val list1 = ... 4 rdd1.map(list1...) 5 // 以下代码将list1封装成了Broadcast类型的广播变量。 6 // 在算子函数中,使用广播变量时,首先会判断当前task所在Executor内存中,是否有变量副本。 7 // 如果有则直接使用;如果没有则从Driver或者其他Executor节点上远程拉取一份放到本地Executor内存中。 8 // 每个Executor内存中,就只会驻留一份广播变量副本。 9 val list1 = ... 10 val list1Broadcast = sc.broadcast(list1) 11 rdd1.map(list1Broadcast...)
原则八:使用Kryo优化序列化性能
在Spark中,主要有三个地方涉及到了序列化:
1、在算子函数中使用到外部变量时,该变量会被序列化后进行网络传输(见“原则七:广播大变量”中的讲解)。
2、将自定义的类型作为RDD的泛型类型时(比如JavaRDD,Student是自定义类型),所有自定义类型对象,都会进行序列化。因此这种情况下,也要求自定义的类必须实现Serializable接口。
3、使用可序列化的持久化策略时(比如MEMORY_ONLY_SER),Spark会将RDD中的每个partition都序列化成一个大的字节数组。
对于这三种出现序列化的地方,我们都可以通过使用Kryo序列化类库,来优化序列化和反序列化的性能。Spark默认使用的是Java的序列化机制,也就是ObjectOutputStream/ObjectInputStream API来进行序列化和反序列化。但是Spark同时支持使用Kryo序列化库,Kryo序列化类库的性能比Java序列化类库的性能要高很多。官方介绍,Kryo序列化机制比Java序列化机制,性能高10倍左右。Spark之所以默认没有使用Kryo作为序列化类库,是因为Kryo要求最好要注册所有需要进行序列化的自定义类型,因此对于开发者来说,这种方式比较麻烦。以下是使用Kryo的代码示例,我们只要设置序列化类,再注册要序列化的自定义类型即可(比如算子函数中使用到的外部变量类型、作为RDD泛型类型的自定义类型等):
1 // 创建SparkConf对象。 2 val conf = new SparkConf().setMaster(...).setAppName(...) 3 // 设置序列化器为KryoSerializer。 4 conf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer") 5 // 注册要序列化的自定义类型。 6 conf.registerKryoClasses(Array(classOf[MyClass1], classOf[MyClass2]))
原则九:优化数据结构
Java中,有三种类型比较耗费内存:
1、对象,每个Java对象都有对象头、引用等额外的信息,因此比较占用内存空间。
2、字符串,每个字符串内部都有一个字符数组以及长度等额外信息。
3、集合类型,比如HashMap、LinkedList等,因为集合类型内部通常会使用一些内部类来封装集合元素,比如Map.Entry。因此Spark官方建议,在Spark编码实现中,特别是对于算子函数中的代码,尽量不要使用上述三种数据结构,尽量使用字符串替代对象,使用原始类型(比如Int、Long)替代字符串,使用数组替代集合类型,这样尽可能地减少内存占用,从而降低GC频率,提升性能。但是在笔者的编码实践中发现,要做到该原则其实并不容易。因为我们同时要考虑到代码的可维护性,如果一个代码中,完全没有任何对象抽象,全部是字符串拼接的方式,那么对于后续的代码维护和修改,无疑是一场巨大的灾难。同理,如果所有操作都基于数组实现,而不使用HashMap、LinkedList等集合类型,那么对于我们的编码难度以及代码可维护性,也是一个极大的挑战。因此笔者建议,在可能以及合适的情况下,使用占用内存较少的数据结构,但是前提是要保证代码的可维护性。
总的来说:
(1) 能用json字符串的不要用对象表示,因为对象头额外占16个字节,所以能不使用对象就使用字符串;
(2) 能不用字符串就不使用字符串,因为字符串额外占40个字节,比如,能用1(int 4字节) 就不要用”1” (string 40+1字节)
(3) 尽量用数组array代替集合类型(HashMap、LinkedList.....)
(4) 当然不要为了性能好而性能好,我们还是要兼顾代码的可读性和开发效率。
原则十:尽可能数据本地化
进程本地化级别:
1.PROCESS_LOCAL:进程本地化,代码和数据在同一个进程中,也就是在同一个executor中;计算数据的task由executor执行,数据在executor的BlockManager中;性能最好.
2.NODE_LOCAL:节点本地化代码和数据在同一个节点中;比如说,数据作为一个HDFS block块,就在节点上,而task在节点上某个executor中运行;或者是,数据和task在一个节点上的不同executor中;数据需要在进程间进行传输
3.NO_PREF;对于task来说,数据从哪里获取都一样,没有好坏之分
4.RACK_LOCAL:机架本地化数据和task在一个机架的两个节点上;数据需要通过网络在节点之间进行传输
5.ANY:数据和task可能在集群中的任何地方,而且不在一个机架中,性能最差
我们提交任务后有Spark任务的监控界面,可以看到这个task的数据本地性是NODE_LOCAL说明是极好的,但是如果有你的task任务的数据本地性较差,可以尝试如下调优:图略
如何调优:
spark.locality.wait 默认值是3s。这个代表的意思是,task任务分配的时候,先是按照_PROCESS_LOCAL的这种方式去分配task的,但是如果PROCESS_LOCAL这个不满足,那么默认就等3秒,看能不能按照这级别去分配,但是如果等了3秒也实现不了。那么就按NODE_LOCAL 这个级别去分配,以此类推,每次都是等三秒。但是我们知道,如果想代码运行速度快,那么就尽可能的让task分配在PROCESS_LOCAL 和 NODE_LOCAL 级别,所以调优的时候,就让task 在这两种级别的时候多等一会儿,这样尽可能的把任务分配到这两个级别。所以默认3秒就有点少了。
spark.locality.wait.process 30s
spark.locality.wait.node 30s
在这两个级别的时候多等一会儿
更多精彩关注wx公众号:威5编程