概述

本文讲述map和mapPartitions的相同点和区别。并对mapPartitions优缺点进行总结,并总结了mapPartitions的使用例子。

map和mapPartitions

map

mapPartitions

transformation

transformation

基于一行进行操作

基于一个分区的数据操作

没处理完一行就返回一个对象

处理完一个分区的所有行才返回

不将输出结果保存在内存中

输出保留在内存中,因为它可以在处理完所有行后返回

易于实例化服务(可重用对象)

易于实例化服务(可重用对象)

无法确定何时结束服务(No CleanupMethod)

返回前可以关闭服务

mapPartitions要点

mapPartitions的优势

  • mapPartitions是一个特殊的map,它在每个分区中只调用一次。
  • 由于它是针对每个分区进行处理,所以,它在数据处理过程中产生的对象会远小于map产生的对象。
  • 当需要处理的数据量很大时mapPartitions不会把数据都加载到内存中,避免由于数据量过大而导致的内存不足的错误。
  • mapPartitions为了提升运行的效率,在数据处理时它还会进行优化,把该放的数据放到内存中,把其他一些数据放到磁盘中。
  • mapPartitions函数的处理是通过迭代器进行的,输出的也是迭代器,通过输入参数(Iterarator [T]),各个分区的全部内容都可以作为值的顺序流。
    自定义函数必须返回另一个Iterator [U]。组合的结果迭代器会自动转换为新的RDD。
  • mapPartitions函数返回的是一个RDD类,具体来说是一个:MapPartitionsRDD。
  • 和map不同,由于 mapPartitions是基于每个数据的分区进行处理的,所以在生成对象时也会基于每个分区来生成,而不是针对每条记录来生成。例如:若需要连接外部数据库比如:hbase或mysql等,只会针对每个分区生成一个连接对象。

mapPartitions要注意的问题

  • mapPartitions是针对每个分区进行处理的,若最后的结果想要得到一个全局范围内的,需要慎重考虑。

mapPartitions使用实战

简单的例子

该例子要实现的功能很简单:把一个整数RDD[Int]的元组修改成RDD[(Int, Int)],并且设置元组中第二个元素的值为第一个元素的值的2倍。

  • 使用mapPartitions来实现
val a = sc.parallelize(1 to 9, 3)

def doubleFunc(iter: Iterator[Int]) : Iterator[(Int,Int)] = {
    var res = List[(Int,Int)]()
    while (iter.hasNext)
    {
      val cur = iter.next;
      res .::= (cur,cur*2)
    }
    res.iterator
}
  
val result = a.mapPartitions(doubleFunc)
println(result.collect().mkString)
  • 使用map来实现
val a = sc.parallelize(1 to 9, 3)
def mapDoubleFunc(a : Int) : (Int, Int) = {
    (a,a*2)
}
val mapResult = a.map(mapDoubleFunc)

println(mapResult.collect().mkString)
  • 观察性能

    在spark-shell终端中,我们可以打开info日志,这样可以看到运行的时间。
sc.setLogLevel("INFO")

我们把整个rdd的数据量扩大到1000000,可以看一下各自处理的需要的时间。
要注意:把数据量扩大后,不需要再打印这些数据的值了,所以不需要执行println这一步,但为了触发action动作,和job的提交,我们需要执行以下简单的一步:

result.take(1)
或
mapResult.take(1)

可以看到,当数据量到达一百万时,通过mapPartitions函数来处理效率更高。

文本单词计数

本例子要实现的功能是:大文件单词计数。
我准备了一个12M的文件(其实不算大),下面分别通过map和mapPartitions来处理该文件,对文件中的单词进行单词计数。

  • 使用mapPartitions来实现
val dataHDFSPath = "hdfs://hadoop3:7078/user/ubuntu/mldata/txtdata2"
val wordCount = sc.textFile(dataHDFSPath, 3).mapPartitions(lines => {
        lines.flatMap(_.split(" ")).map((_, 1))
  }).
  reduceByKey((total, agg) => total + agg).take(100)

在我的测试环境中,使用mapPartitions共消耗: 0.144277s

  • 通过map来实现
val dataHDFSPath = "hdfs://hadoop3:7078/user/ubuntu/mldata/txtdata2"
val wordCount = sc.textFile(dataHDFSPath, 3).
                    flatMap(line => line.split(" ")).
                    map(word => (word, 1)).
                    reduceByKey { (x, y) => x + y }

wordCount.take(100)

在我的测试环境中,使用map消耗:0.176129s

mapPartitions和Dataframe结合使用

import spark.implicits._
val dataDF = spark.read.format("json").load("basefile")

// 注意:这里遍历时,每一行的类型是RDD[Row]
val newDF = dataDF.mapPartitions( iterator  => {
  // 这里的p是Row类型的数据,这里把它变成了Seq的数据,这里其实是一个List(1,2)
  iterator.map(p => Seq(1, 2)))
}).toDF("value")

newDF.write.json("newfile")

mapPartition使用范式

这里收集了一些使用mapPartition的例子,供后续使用时进行参考。

用法1

def func(it):
    r = f(it)
    try:
        return iter(r)
    except TypeError:
        return iter([])
self.mapPartitions(func).count()  # Force evaluation

用法2

def aggregatePartition(iterator):
            acc = zeroValue
            for obj in iterator:
                acc = seqOp(acc, obj)
            yield acc

partiallyAggregated = self.mapPartitions(aggregatePartition)
numPartitions = partiallyAggregated.getNumPartitions()
scale = max(int(ceil(pow(numPartitions, 1.0 / depth))), 2)

用法3

val OneDocRDD = sc.textFile("myDoc1.txt", 2)
  .mapPartitions(iter => {
    // here you can initialize objects that you would need 
    // that you want to create once by worker and not for each x in the map. 
    iter.map(x => (x._1 , x._2.sliding(n)))
  })

用法4

def onlyEven(numbers: Iterator[Int]) : Iterator[Int] = 
  numbers.filter(_ % 2 == 0)

def partitionSize(numbers: Iterator[Int]) : Iterator[Int] = 
  Iterator.single(numbers.length)

val rdd = sc.parallelize(0 to 10)
rdd.mapPartitions(onlyEven).collect()
// Array[Int] = Array(0, 2, 4, 6, 8, 10)

rdd.mapPartitions(size).collect()
// Array[Int] = Array(2, 3, 3, 3)

用法5

当需要在mapPartitions或map中进行外部连接初始化时,mapPartitions只会为每个分区初始化一次,而map会为每条记录都初始化一次,如下面的例子:

val newRd = myRdd.mapPartitions(partition => {
  // 只会为每个分区创建一个数据库连接
  val connection = new DbConnection /*creates a db connection per partition*/

  // 对分区中的数据进行迭代访问和处理,调用readMatchingFromDB函数来处理每条记录。
  val newPartition = partition.map(record => {
    readMatchingFromDB(record, connection)
  }).toList

  // 关闭数据库连接
  connection.close()
  // 返回新List结果的迭代器
  newPartition.iterator
})

或者使用以下更加简洁的方式:

rdd.mapPartition(
  partitionIter => {
    partitionIter.map(
        line => func() do your logic
        ).toList.toIterator
  }
)

用法6

val rdd1 = sc.parallelize(List(1,2,3,4,5,6,7,8,9,10), 3)
def myfunc(index: Int, iter: Iterator[Int]) : Iterator[String] = {
    iter.map(x => index + "," + x)
}
val rdd2 = rdd1.mapPartitionsWithIndex(myfunc)