ITPub博客

首页 > 数据库 > 数据库开发技术 > mapPartitions比map效率要高?

mapPartitions比map效率要高?

原创 数据库开发技术 作者:安瑞哥是码农 时间:2022-09-21 18:29:09 0 删除 编辑

对于spark而言,这两个算子应该算是用的最多的了,而关于这两个算子的数据处理效率问题,讨论其实一直都有很多,尤其对于很多刚入行不久的小伙伴来说,会单纯的以为map这个函数因为针对的是一条数据,而mapPartitions针对的是整个分区的数据,所以会自然的以为:mapPartitions会比map的效率高。

而我一直坚持的认为,这两者单纯从对数据的处理效率来说是不存在高低优劣势之分的,这两者最大的区别仅仅只是暴露给使用者的API中面对的数据粒度不一样而已,这个区别只在你对数据处理过程中,如果涉及到【外部IO】情况下才会区别明显。

最典型就是在算子内部调用数据库连接,如果这个连接是在算子内部创建的,那么会涉及到一个创建【过频】问题,但是这问题其实解决办法有很多,连接池+单例就能很容易搞定。

对于这两者的效率问题,我们来分析一波。


|理论分析
我们知道,对于spark而言,数据进行分布式处理的单位是partition,当我们运行一个spark任务时,如果对一个executor分配N个CPU核心,那么就意味着这个executor进程可以并行处理N个partition的数据,但是对于任何一个独立的partition而言,你的业务处理逻辑都是要串行运用到每一条数据上的,因此无论对于map还是mapPartitions来说,其运行业务处理逻辑的效率理论是一样的。

只不过对于开发者而言,当你用这两者提供的api时,暴露的数据粒度不一样,map面对的是一个具体的数据对象,而mapPartition面对的是一个迭代器,但是从spark的运行层面来说,无论你用map还是mapPartitions都是调用MapPartitionsRDD这个隐藏类中的对应方法。

这个是理论分析,但是实际情况真的这样吗?

那么不妨,咱来验证一下!


|验证准备
我用之前提供给大家做实战项目的数据集来充当验证的数据源,然后将该数据经过一番简单的转换处理,最终把结果以CSV格式写入到HDFS中,其处理流程如下图:


为了比较快达到验证目的,我截取了约500MB数据作为测试集,数据量约400W条,为了尽可能让验证效果准确,对数据的处理逻辑只用map和mapPartitions算子,对于其数据处理逻辑完全一样。

且在实际操作过程中,为了尽可能避免程序对数据处理过程中的其他因素干扰,对数据的处理逻辑不涉及任何IO操作、不涉及shuffle,单纯只是CPU运算,而且为了防止本地电脑的网络抖动问题,将程序放到我的测试集群中运行。其代码分别如下:

1,map算子代码:
object MapTest {
    def main(args: Array[String]): Unit = {
        val conf = new SparkConf().setAppName("MapTest")/*.setMaster("local")*/
        val spark = SparkSession.builder().config(conf).getOrCreate()
        val rawDF = spark.read.csv(args(0))//读取HDFS的数据源
        import spark.implicits._
        rawDF.printSchema() //spark job 1
        rawDF.show() //spark job 2
        val resultDF = rawDF.map(row => {
            val clientIP = row.getAs[String]("_c0")
            val domain = row.getAs[String]("_c1").toLowerCase//将域名转成小写
            val time = row.getAs[String]("_c2")
            val targetIP = row.getAs[String]("_c3")
            val rcode = row.getAs[String]("_c4")
            val queryType = row.getAs[String]("_c5")
            val authRecord = if (row.getAs[String]("_c6") == null ) "" else row.getAs[String]("_c6").toLowerCase
            val addMsg = if (row.getAs[String]("_c7") == null ) "" else row.getAs[String]("_c7")
            val dnsIP = row.getAs[String]("_c8")
            (clientIP,domain,time,targetIP,rcode,queryType,authRecord,addMsg,dnsIP)
        }).toDF("client_ip","domain","time","target_ip","rcode","query_type","authority_record","add_msg","dns_ip")

        /**将转换后的数据写入HDFS*/
        resultDF.write.csv(args(1))//spark job 3
    }
}

2,mapPartitions算子代码:
object MapPartitionTest {
    def main(args: Array[String]): Unit = {
        val conf = new SparkConf().setAppName("MapPartitionTest")/*.setMaster("local")*/
        val spark = SparkSession.builder().config(conf).getOrCreate()
        val rawDF = spark.read/*.option("header",true)*/.csv(args(0))

        import spark.implicits._
        rawDF.printSchema()
        rawDF.show()
        val resultDF = rawDF.mapPartitions(iterator => {
            val array = new mutable.ArrayBuffer[(String,String,String,String,String,String,String,String,String)]
            while (iterator.hasNext){
                val next = iterator.next()
                val clientIP = next.getAs[String]("_c0")
                val domain = next.getAs[String]("_c1").toLowerCase//将域名转成小写
                val time = next.getAs[String]("_c2")
                val targetIP = next.getAs[String]("_c3")
                val rcode = next.getAs[String]("_c4")
                val queryType = next.getAs[String]("_c5")
                val authRecord = if (next.getAs[String]("_c6") == null ) "" else next.getAs[String]("_c6").toLowerCase
                val addMsg = if (next.getAs[String]("_c7") == null ) "" else next.getAs[String]("_c7")
                val dnsIP = next.getAs[String]("_c8")
                array.+=((clientIP,domain,time,targetIP,rcode,queryType,authRecord,addMsg,dnsIP))
            }
            array.toIterator
        }).toDF("client_ip","domain","time","target_ip","rcode","query_type","authority_record","add_msg","dns_ip")

        resultDF.write.csv(args(1))
    }
}
然后为了屏蔽因为分布式任务,不同服务器之间的资源分配速度不同,导致不同executor之间运行效率会存在差异而带来的干扰,实验只用1个executor,1个CPU核心。启动命令如下:
spark-submit --master yarn --deploy-mode client --conf spark.driver.memory=1g --conf spark.yarn.max.executor.failures=1 --conf spark.memory.fraction=0.8 --conf spark.task.maxFailures=1 --class com.anryg.bigdata.test.map_pk_mappartition.MapTest --executor-cores 1 --num-executors 1 --executor-memory 2g --name MapTest --jars ./spark-coding-1.0-SNAPSHOT-with-dependencies.jar ./spark-coding-1.0-SNAPSHOT.jar /the/path/to/source/data /dir/of/output
总之,目的就是为了让比对结果尽可能的【公平】。


|验证过程
通过4个不同的时间点,和控制单一变量原则,每次分别运行map和mapPartitions两个不同的程序,真实记录每一次的运行结果。


|验证结果
通过在4个不同时间点的4组测试对比,测试结果可能会打破很多同学对这两个算子的固有认知。

对,那就是每一次的测试结果都表明:当前场景下,map的实际效率比mapPartitions高。

来看下任务运行的spark UI截图,先来看map的4次运行截图如下:


可以看出来,其运行速度每次都差不多,大概在38秒左右,然后每次都只有一个stage,该stage有4个Partition。


再来看下mapPartitions的运行截图:


如果说,偶尔的一次对比不能说明问题,但是经过4次的对比每次的结论都一样,而且这个测试场景足够的简单,能够对其造成干扰的因素很少,那就可以下结论:
当前数据处理场景下,map的效率就是比mapPartitions高。

对比次数 map耗时
mapPartitions耗时
1
37s
54s
2
37s
54s
3
41s
54s
4
38s
1.1min
再来看一眼,两者运行后的结果数据,不能说很像,那就是一毛一样:

可能有人会觉得,4次测试是不是有点少,是不是有点以偏概全?如果你有兴趣,可以去亲自验证一番,看你验证的跟我的是否一样,代码我稍后提交到GitHub。


|结果分析
有同学可能会好奇,明明之前理论分析说map跟mapPartitions效率应该一样啊,怎么结果来看是反而map的效率更高呢?

先来看下spark UI中这两个算子生成的DAG:

map的DAG长这样:

而mapPartitions的DAG长这样:

为什么mapPartitions会多出中间那两个步骤?

我的分析是,在mapPartitions的处理逻辑中,因为需要把转换后的数据最后再次转换为一个迭代器,于是就需要一个中间容器来存放这个转换过程中的数据:

而就是这个中间容器,导致的效率变低,通过查看这个容易的源码可以知道,它是一个数组,而这个数组的初始大小为16,但是很明显我们一个Partition的数据不止16条。

于是,这个过程中,这个数组会不断的产生扩容、对象拷贝,于是就拖慢了这个过程的效率,而用map算子,则没有这些破事,所以用mapPartitions比map慢就不难理解了。

我是这么理解的,你觉得呢?


我这有一个高质量的大数据学习讨论群,不怎么扯淡、偶尔活跃气氛、大部分时间都在讨论技术,你在群里的几乎任何大数据相关的问题,都可以得到解答,要不要进来试试呢?拉你进群啊...


原文作者:安瑞哥是码农;如有侵权,请联系管理员删除。

请登录后发表评论 登录
全部评论

注册时间:2022-02-16

  • 博文量
    228
  • 访问量
    115429