一、MapPartitions提升Map类型操作性能
Spark中,每个task处理一个RDD的partition。
①MapPartitions的优点
如果是普通的map,比如一个partition中有一万条数据,那么function需要执行和计算一万次。如果使用了MapPartitions,一个task只执行一次function,function一次接受所有的partition数据。只要执行一次就可以了,性能比较高。
②MapPartitions的缺点
当数据量很大的情况下,比如有一千万条数据。如果使用普通的map,一次只处理一条数据,在处理的过程中,如果内存不足,则会将处理完的数据从内存中垃圾回收掉,或者使用其他方法,清理出内存,程序可以正常运行。如果是MapPartitions操作,一次需要读入所有的数据,内存不足的话也无法腾出空间,会导致内存溢出。
③适用场景
数据量不是很大的时候,内存足够存放一次function操作所需的所有数据。
二、filter之后使用coalesce减少分区数量
①filter只有可能会产生的后果
- 每个partition数据量变少了,但是在后面进行处理的时候,还是要跟partition数量一样的task来进行处理,造成task资源的浪费。
- 每个partition的数据量不一样,会导致后面的每个task处理每个partition的时候,每个task要处理的数据量就不一样,造成数据的倾斜。
②解决方案
- 针对第一个问题,因为数据量变少了,那么partition其实也可以相应地变少,可以考虑进行partition的压缩。
- 针对第二个问题,解决方案和第一个问题一样,也可以考虑partition的压缩,尽量让每个partition的数据量差不多。这样,后面的task需要处理的数据量就差不多,处理速度差不多,避免数据倾斜带来的问题。
③实现
在代码中,filter算子后面添加.coalesce(numPartitions),比如:
RDD.filter(XXXXXXXXXXXXXXX).coalesce(100);
三、repartition解决Spark SQL低并行度的性能问题
①问题描述
通过spark.default.parallelism设置的并行度,对Spark SQL操作不生效,Spark SQL依然使用自身默认的并行度。这种情况下,可能我们的环境中可以使用的cpu core数量为200,手动设置的并行度为600。但是,Spark SQL操作执行后生成的task数量为10,那么后续的所有操作,并行度都是10,而不是我们设置的600,。这样,就会对后续的操作带来性能上的问题。
②解决方案
对Spark SQL操作产生的partition,使用reparation操作,设置并行度。
③代码示例
xxxRDD.repartition(numPartitions);
四、foreachPartition优化写数据库性能
①问题描述
默认的foreach性能有很大的缺陷:
- 对于每条数据,task都要单独执行一次function
- 每次执行function的时候,都需要发送SQL语句往数据库写数据,需要创建和销毁数据库连接,而数据库的创建和连接对资源的消耗很大。即使使用数据库连接池,也只是创建有限个数据库连接,很难满足实际需求。
以上两点,多次数据库连接和多次发送SQL语句,非常消耗资源,影响性能。
②使用foreachPartition的好处
在function函数中,对一个partition的数据进行批量处理,只调用一次function函数,一次传入partition中的所有数据。这样,数据库连接只需要创建一次,想数据库发送一次SQL语句,只是需要传入多组参数即可。
③foreachPartition的缺陷
如果一个partition的数据量特别大,比如达到上百万条,一次性传入,可能会造成OOM内存溢出。
所以,foreachPartition的适用环境还需要在现实的生产环境中慢慢调试。
五、reduceByKey本地聚合
相比较于普通的shuffle,reduceByKey会对map端的数据进行本地聚合。也就是说,map端给下一个stage的每个task创建的输出文件,在写数据之前,会对数据进行一次本地聚合combiner操作。
好处在于:
- 在本地进行聚合以后,在map端的数据量就会变少,减少磁盘IO,减少磁盘空间的占用。
- 下一个stage的task,在拉取数据的时候,数据量减少,减少网络传输的性能消耗。
- reduce端拉取的数据减少,需要进行聚合的数据减少,需要的缓存也减少。