快速上手写spark代码系列01:RDD transformation函数入门

快速上手写spark代码系列01:RDD transformation函数入门

标签: RDD transformation 函数


学习Spark的第一步就是要了解RDD的相关概念以及操作,RDD操作的operator分为两类,一类被称为transformation,另外一类称为action。

因为transformation属于lazy求值,也就是如果没有action函数触发,transformation函数只做类别映射而不做数据数据的转换。所以要同时了解这两类函数。本篇先只讨论transformation函数,action函数相对简单很多,会在下一篇文章中讨论。

transformation函数分为几种转换操作,我简单做了一下分类,可能很不精确。

  • 元素映射类转换
    如map,flatMap,filter;

  • 分区集合类转换
    如mapPartitions,mapPartitionsWithIndex,mapPartitionsWithContext,sample,union,intersection,join,leftOuterJoin,rightOuterJoin,fullOuterJoin,cogroup,cartesian,randomSplit,subtract,zip,coalesce,repartition等;

  • 聚合转换
    如distinct,groupByKey,reduceByKey,aggregateByKey,combineByKey。

  • 管道类操作
    如pipe。

下面就分类按类别进行分析。

元素映射类转换

map函数

def map[U](f: (T) ? U)(implicit arg0: ClassManifest[U]): RDD[U]

对RDD的所有元素进行map操作后,返回是一个新的RDD。

map是最常见的函数,比如对元素做类型转换、函数变换、取某些元素、组成新的Array或者List,或是Sequence,或者是Vector,有时候是

val testRDD = data.map(_.split("\\|")).map(x=>x(2)).map(_.toDouble)

上面的snippet就是对每一行数据进行操作

data.map(_.split("\\|"))

上面的代码返回的是一个RDD,类型是一个String数组

res10: org.apache.spark.rdd.RDD[Array[String]] = MapPartitionsRDD[21] at map at :24

在scala里面可以用下划线 _ 来表示每一个元素,在第一个里面代表是每一行,也就是每一行变成了一个数组,通过竖线分割,将分割的每一个元素存在数组中,分割的对象默认都是String类型。

然后第二步,对于每一个数组对象做新的映射操作,取第三个元素

.map(x=>x(2))

返回类型是一个String对象

scala> data.map(_.split(“|”)).map(x=>x(2))
res13: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[23] at map at :24

最后可以对对象进行转换,变成double类型

scala> data.map(.split(“|”)).map(x=>x(2)).map(.toDouble)
res16: org.apache.spark.rdd.RDD[Double] = MapPartitionsRDD[26] at map at :24

更简单一些,

data.map(_.split("\\|")).map(x=>x(2).toDouble)

上面之所以写出来可以用

.map(_.toDouble)

是因为分割后的对象其实是一个数组,想对其中的每一个对象进行转换操作有时候是很复杂的,比如分割后元素有200个,对每一个做转换代码会很复杂,可以像下面这样,对同一类的转换再次映射,

val data = trainData.map { line =>
      val parts= line.split("\\|")
      val user_id = parts(0)
      val label = toLabel(parts(1)) //第二列是标签
      val features = Vectors.dense(parts.slice(6,parts.length).map(_.toDouble)) //第7到最后一列是属性,需要转换为Doube类型,如果是对每一个单独转换操作,代码会很长
      LabeledPoint(label, features) //构建LabelPoint格式
    }

上面的代码包含了如下操作:
分割元素

val parts= line.split(“|”)

取某些元素

val user_id = parts(0)

类型转换

.map(_.toDouble)

函数变换

val label = toLabel(parts(1))

这里toLabel属于一个函数变换。

生成新的类型对象

val features = Vectors.dense(parts.slice(6,parts.length).map(_.toDouble))

map操作还有一个最常见的就是生成

scala> data.map(_.split("\\|")).map(x=>(x(0),(x(3),x(4))))
res37: org.apache.spark.rdd.RDD[(String, (String, String))] = MapPartitionsRDD[41] at map at <console>:24

上面x(0)作为key,(x(3),x(4))作为value对象,类型是

org.apache.spark.rdd.RDD[(String, (String, String))]

这个在后面的关联中很有用,key可以是某一个元素,也可以是元素的变形,比如类型转换,拼接,取部分字符等操作,例如:

data.map(_.split("\\|")).map(x=>(x(0)+"-"+x(1),(x(3),x(4))))

这个就是把前面两个元素用-串起来作为一个key,这个有时候非常有用,比如一个用户有多种状态,希望把各种用户和状态结合起来作为key进行统计汇总,如下面的例子。

A 1
A 2
A 1
A 3
B 1
B 2
B 2
C 3

得到如下结果:

A1,2
A2,1
A3,1
B1,1
B2,2
C3,1

当然这个也可以用flatMap实现,下面讨论一下flatMap。

flatMap函数

def flatMap[U](f: (T) ? TraversableOnce[U])(implicit arg0: ClassManifest[U]): RDD[U]

通过首先对这个RDD的所有元素应用一个函数,然后展开结果来返回一个新的RDD。这个用了英文flattening the results来形容最后将结果展开,返回的是一个TraversableOnce对象,类型U是和输入对象的类型一样。TraversableOnce对象是一个 template trait,对集合可以进行遍历操作。

不太好理解,下面我们通过一个例子来看一下。

val arr=sc.parallelize(Array(("A",1),("B",2),("C",3)))

下面看看使用flatMap操作后的结果

scala> arr.flatMap(x=>(x._1+x._2)).foreach(println) A C B 3 1 2

scala> arr.flatMap(x=>(x._1+x._2)).foreach(println) C 3 B 2 A 1

scala> arr.flatMap(x=>(x._1+x._2)).foreach(println) A B 2 C 1 3

我们发现结果有一定的随机性,不是稳定的输出,但是结果集是把每对象的元素进行了展开。

下面换一个例子,统计相邻元素对的个数:

val data = sc.parallelize(Array(("A,B,C"),("B,C"),("C,A")))

data.map(_.split(",")).flatMap(x=>{
         for(i<-0 until x.length-1) yield (x(i)+"-"+x(i+1),1)
     }).reduceByKey(_+_).foreach(println)

结果如下:

(A-B,1)
(C-A,1)
(B-C,2)

这个就和我们上面的map操作有一点相似之处了,当然问题场景不太一样,map操作解决不了这个问题,但是如果只有两个元素的pair进行统计,那么就可以用两种方案了。由此可见,flatMap适用于特殊场景,比如分词统计,或者不确定数量的pair统计。

filter函数

filter函数用来过滤某些元素,定义如下

def filter(f: (T) ? Boolean): RDD[T]

官方文档里对返回类型描述的是:

Return a new RDD containing only the elements that satisfy a predicate.

翻译过来就是“返回一个仅包含满足谓词的元素的新RDD。”,在这里有一个谓词的概念,所谓的谓词就是满足条件的意思,在计算机语言的环境下,谓词是指条件表达式的求值返回真或假的过程。在这里满足了filter里面定义的条件就是满足了谓词。比如满足了等于,大于,小于,不大于,不小于,不等于等谓词的元素组成了新的RDD,返回的就是这个新的RDD。

下面举个例子:

sc.textFile("/user/simon/*").map(_.split("\\t")).map(x=>(x(9)+"-"+x(8),x))
.filter(x=>x._1.equals("6B967FE2534E4C3E13A69881876A9CDF-08A7223E876A045D511074E8C9C8E937"))
.map(x=>x._2.mkString("|"))

找出来等于Key值(这里等于就是谓词)的某个元素,对于多个条件的可以用||拼起来。

sc.textFile("/user/simon/*").map(_.split("\\t")).map(x=>(x(9)+"-"+x(8),x))
.filter(x=>
x._1.equals("6B967FE2534E4C3E13A69881876A9CDF-08A7223E876A045D511074E8C9C8E937")||
x._1.equals("7B967FE2534E4C3E13A69881876A9CDF-08A7223E876A045D511074E8C9C8E937")||
x._1.equals("8B967FE2534E4C3E13A69881876A9CDF-08A7223E876A045D511074E8C9C8E937")
)
.map(x=>x._2.mkString("|"))

谓词的变化这里就不在本篇详细阐述了。

分区集合类转换

mapPartitions函数

def mapPartitions[U](f: (Iterator[T]) ? Iterator[U], preservesPartitioning: Boolean)(implicit arg0: ClassManifest[U]): RDD[U]

返回值:一个新的RDD,由该RDD的各个分区应用对应的函数变换而来。(Return a new RDD by applying a function to each partition of this RDD.)

mapPartitions函数算是map函数的一个变化,和map函数操作RDD中的每一个元素不同,mapPartitions函数对RDD的每一个partition进行转换。
这两个方法的另一个区别是在大数据集情况下的资源初始化开销和批处理处理,map每操作一个记录都要初始化一次资源(比如创建一个对象或者数据库连接),mapPartitons的开销要小很多,便于进行批处理操作。
下面取一个参考例子(来自于https://bzhangusc.wordpress.com/2014/06/19/optimize-map-performamce-with-mappartitions/

定义了一个CSV解析器,使用map操作

    def mLine(line:String)={
      val parser=new CSVParser('\t')
      parser.parseLine(line)
    }
...
    ...myRDD.map(mLine(_).size)...

我们可以看出,每一行操作都需要创建一个parser对象,尽管他们做的事情都是一样的。通过使用mapPartitions函数,我们可以减少这个对象的创建次数。

    def pLines(lines:Iterator[String])={
      val parser=new CSVParser('\t')
      lines.map(parser.parseLine(_).size)
    }
...
    myRDD.mapPartitions(pLines)

这个例子的作者通过测试发现,他的任务从65s降低到了35s。

我们也创建一个例子

val test = sc.parallelize(2 to 1000000,10)

def getSqrt(x:Int):(Int,Double) = {
    (x,Math.sqrt(x))
}

def getSqrt2(iter: Iterator[Int]) : Iterator[(Int,Double)] = {
    var res = List[(Int,Double)]()
    while (iter.hasNext)
    {
      val current = iter.next;
      res .::= (current,Math.sqrt(current))
    }
    res.iterator
  }

//分别使用两种函数进行处理
val result = test.map(x=>getSqrt(x)) //use map operator
val result2 = test.mapPartitions(x=>getSqrt2(x)) //user mapPartitions

scala> val a = System.currentTimeMillis();result.collect.mkString;val b = System.currentTimeMillis();val c = b - a;
a: Long = 1497601263293
b: Long = 1497601263533
c: Long = 240

scala> val result2 = test.mapPartitions(x=>getSqrt2(x))
result2: org.apache.spark.rdd.RDD[(Int, Double)] = MapPartitionsRDD[53] at mapPartitions at <console>:25

scala> val a = System.currentTimeMillis();result2.collect.mkString;val b = System.currentTimeMillis();val c = b - a;
a: Long = 1497601322307
b: Long = 1497601322342
c: Long = 35

从上面的结果我们仿佛觉得,当数据量大的时候,使用mapPartitions应能更好,但是实际上经过多轮测试,差别不大,所以在平时,map就足够了。
但是当这个对象是第三方的库的时候差别就显现出来了,比如下面的例子:

val newRd = myRdd.mapPartitions(partition => {
  val connection = new DbConnection /*creates a db connection per partition*/

  val newPartition = partition.map(record => {
    readMatchingFromDB(record, connection)
  }).toList // consumes the iterator, thus calls readMatchingFromDB 

  connection.close() // close dbconnection here
  newPartition.iterator // create a new iterator
})

mapPartitionsWithIndex函数

mapPartitionsWithIndex函数是mapPartitions函数的一个变种,可以获取分区的索引编号。

def mapPartitionsWithIndex[U](f: (Int, Iterator[T]) ? Iterator[U], preservesPartitioning: Boolean)(implicit arg0: ClassManifest[U]): RDD[U]

返回值:一个新的RDD,由该RDD的各个分区应用对应的函数变换而来,同时为原来的每个分区设置一个跟踪索引。(Return a new RDD by applying a function to each partition of this RDD, while tracking the index of the original partition.)

举个例子(来源于http://lxw1234.com/archives/2015/07/348.htm)

var rdd1 = sc.makeRDD(1 to 5,2)
//rdd1有两个分区
var rdd2 = rdd1.mapPartitionsWithIndex{
        (x,iter) => {
          var result = List[String]()
            var i = 0
            while(iter.hasNext){
              i += iter.next()
            }
            result.::("[" + x + "]" + i).iterator

        }
      }
//rdd2将rdd1中每个分区的数字累加,并在每个分区的累加结果前面加了分区索引
scala> rdd2.collect
res13: Array[String] = Array([0]3, [1]12)

mapPartitionsWithContext函数

mapPartitionsWithContext函数是mapPartitions函数的另外一个变种,可以通过函数获取分区的上下文信息。

def mapPartitionsWithContext[U](f: (TaskContext, Iterator[T]) ? Iterator[U], preservesPartitioning: Boolean)(implicit arg0: ClassManifest[U]): RDD[U]

Return a new RDD by applying a function to each partition of this RDD.
返回一个新的RDD,这个RDD是通过对原来的RDD的每一个分区做函数变换得到的。

查看下面一段代码(参考文档3):

scala>  import org.apache.spark.TaskContext
import org.apache.spark.TaskContext

scala> def myfunc(tc: TaskContext, iter: Iterator[Int]) : Iterator[Int] = {
  tc.addOnCompleteCallback(() => println(
    "Partition: "     + tc.partitionId +
    ", AttemptID: "   + tc.attemptId))

  iter.toList.filter(_ % 2 == 0).iterator
}
warning: there were 2 deprecation warning(s); re-run with -deprecation for details
myfunc: (tc: org.apache.spark.TaskContext, iter: Iterator[Int])Iterator[Int]

scala> a.mapPartitionsWithContext(myfunc).collect
warning: there were 1 deprecation warning(s); re-run with -deprecation for details
Partition: 0, AttemptID: 34
Partition: 1, AttemptID: 35
Partition: 2, AttemptID: 36
res20: Array[Int] = Array(2, 4, 6, 8, 10)

sample函数

def sample(withReplacement: Boolean, fraction: Double, seed: Int): RDD[T]
Return a sampled subset of this RDD.

可以用下面例子说明一下:

 val train_female = sc.textFile("file:\\E:\\f\\f")
 train_female.sample(withReplacement=true,0.001,131L)

withReplacement false表示不重复抽样
第三个参数seed的选择也很重要,不同的参数抽样的数量是不同的。
Spark MLlib利用还提供了一个sampleByKey函数,后面文章单独论述。

union函数

union的使用比较常见,如果从多个数据源导入数据,并且把数据统一成相同格式进行处理,往往使用union函数将统一格式的RDD合并起来一起进行运算,不去重,像数据库里的union all。

union函数的定义如下:

def union(other: RDD[T]): RDD[T]
Return the union of this RDD and another one.

下面就union的使用举个例子:

val train_male = sc.textFile("file:\\E:\\m\\m")
val train_female = sc.textFile("file:\\E:\\f\\f")
val trainData = train_male.union(train_female)

结果如下:
train_male: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[1] at textFile at <console>:21
train_female: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[3] at textFile at <console>:22
trainData: org.apache.spark.rdd.RDD[String] = UnionRDD[4] at union at <console>:23

如果需要去重就需要使用下面的intersection函数。

intersection函数

该函数返回两个RDD的交集,并且去重,类似于数据库的union。

def intersection(other: RDD[T]): RDD[T]
def intersection(other: RDD[T], numPartitions: Int): RDD[T]
def intersection(other: RDD[T], partitioner: Partitioner)(implicit ord: Ordering[T] = null): RDD[T]

参数numPartitions指定返回的RDD的分区数。
参数partitioner用于指定分区函数

举个例子:

scala> val RDD1 = sc.parallelize(Array(("k1","v1"),("k2","v2"),("k3","v3")))
RDD1: org.apache.spark.rdd.RDD[(String, String)] = ParallelCollectionRDD[58] at parallelize at <console>:24

scala> val RDD2 = sc.parallelize(Array(("k2","v2"),("k3","v3"),("k4","v4")))
RDD2: org.apache.spark.rdd.RDD[(String, String)] = ParallelCollectionRDD[59] at parallelize at <console>:24

scala> RDD1.intersection(RDD2).map(x=>(x._1,x._2)).foreach(println)
(k2,v2)
(k3,v3)

join函数

join函数会输出两个RDD中key相同的所有项,并将它们的value联结起来,它联结的key要求在两个表中都存在,类似于SQL中的INNER JOIN。(具体可见参考文档4)
定义如下:

join[W](other: RDD[(K, W)]): RDD[(K, (V, W))]

Return an RDD containing all pairs of elements with matching keys in this and other.

得到的新的RDD,也是一个 (key,value)结构。其中V包含了两个部分,第一部分是joinRDD的value,第二部分是被join的RDD的value,可以用(key1,value1),(key2,value2)表示两个RDD1,RDD2,RDD1.join(RDD2)得到的是(key,(value1,value2)),其中key是两个RDD共同的key。可以通过._1来获取key,._2._1来获取value1,._2._2来获取value2。

举个例子:

scala> val rdd1 = sc.parallelize(Array((1,"x"),(2,"y"),(3,"z")))
rdd1: org.apache.spark.rdd.RDD[(Int, String)] = ParallelCollectionRDD[12] at parallelize at <console>:21

scala> val rdd2 = sc.parallelize(Array((1,"a"),(2,"b")))
rdd2: org.apache.spark.rdd.RDD[(Int, String)] = ParallelCollectionRDD[13] at parallelize at <console>:21

scala> rdd1.join(rdd2).map(x=>x._1+"|"+x._2._1+"|"+x._2._2).foreach(println)
2|y|b
1|x|a

leftOuterJoin函数

定义如下:

leftOuterJoin[W](other: RDD[(K, W)]): RDD[(K, (V, Option[W]))]
Perform a left outer join of this and other.

leftOuterJoin会保留对象的所有key,而用None填充在参数RDD other中缺失的值,因此调用顺序会使结果完全不同。如下面展示的结果,
gender.leftOuterJoin(age)会保留gender中的所有key,而age.leftOuterJoin(gender)会保留age中所有的key。
在结果集中,关联到的数据会是Some(xxxx),关联不到的则是None。

rightOuterJoin函数

定义如下:

rightOuterJoin[W](other: RDD[(K, W)]): RDD[(K, (Option[V], W))]
Perform a right outer join of this and other.

rightOuterJoin与leftOuterJoin基本一致,区别在于它的结果保留的是参数other这个RDD中所有的key。

fullOuterJoin函数

定义如下:

fullOuterJoin[W](other: RDD[(K, W)]): RDD[(K, (Option[V], Option[W]))]
Perform a full outer join of this and other.

fullOuterJoin会保留两个RDD中所有的key,因此所有的值列都有可能出现缺失的情况,所有的值列都会转为Some对象。

下面集中对上面三个函数举个例子:

scala> val RDD1 = sc.parallelize(Array(("k1","v1"),("k2","v2"),("k3","v3")))
RDD1: org.apache.spark.rdd.RDD[(String, String)] = ParallelCollectionRDD[58] at parallelize at <console>:24

scala> val RDD2 = sc.parallelize(Array(("k2","v2"),("k3","v3"),("k4","v4")))
RDD2: org.apache.spark.rdd.RDD[(String, String)] = ParallelCollectionRDD[59] at parallelize at <console>:24

scala> RDD1.join(RDD2).map(x=>x._1+"|"+x._2._1+"|"+x._2._2).foreach(println)
k2|v2|v2
k3|v3|v3

scala> RDD1.leftOuterJoin(RDD2).map(x=>x._1+"|"+x._2._1+"|"+x._2._2).foreach(println)
k1|v1|None
k2|v2|Some(v2)
k3|v3|Some(v3)

scala> RDD1.rightOuterJoin(RDD2).map(x=>x._1+"|"+x._2._1+"|"+x._2._2).foreach(println)
k2|Some(v2)|v2
k3|Some(v3)|v3
k4|None|v4

scala> RDD1.fullOuterJoin(RDD2).map(x=>x._1+"|"+x._2._1+"|"+x._2._2).foreach(println)
k4|None|Some(v4)
k1|Some(v1)|None
k3|Some(v3)|Some(v3)
k2|Some(v2)|Some(v2)

我们可以看到结果集是Some和None,可以通过模式匹配分离可选值,如果匹配的值是Some的话,将Some里的值抽出赋给x变量:

def showCapital(x: Option[String]) = x match {
case Some(s) => s
case None => “”
}

cogroup函数

cogroup函数将多个RDD中同一个Key对应的Value组合到一起。
函数定义如下:

def cogroup[W1, W2, W3](other1: RDD[(K, W1)], 
      other2: RDD[(K, W2)], other3: RDD[(K, W3)], partitioner: Partitioner) :
      RDD[(K, (Iterable[V], Iterable[W1], Iterable[W2], Iterable[W3]))] 
def cogroup[W1, W2, W3](other1: RDD[(K, W1)], 
      other2: RDD[(K, W2)], other3: RDD[(K, W3)], numPartitions: Int) :
      RDD[(K, (Iterable[V], Iterable[W1], Iterable[W2], Iterable[W3]))]
def cogroup[W1, W2, W3](other1: RDD[(K, W1)], 
      other2: RDD[(K, W2)], other3: RDD[(K, W3)])
      : RDD[(K, (Iterable[V], Iterable[W1], Iterable[W2], Iterable[W3]))]
def cogroup[W1, W2](other1: RDD[(K, W1)], other2: RDD[(K, W2)],
       partitioner: Partitioner)
      : RDD[(K, (Iterable[V], Iterable[W1], Iterable[W2]))]
def cogroup[W1, W2](other1: RDD[(K, W1)], other2: RDD[(K, W2)], 
      numPartitions: Int)
      : RDD[(K, (Iterable[V], Iterable[W1], Iterable[W2]))]
def cogroup[W1, W2](other1: RDD[(K, W1)], other2: RDD[(K, W2)])
      : RDD[(K, (Iterable[V], Iterable[W1], Iterable[W2]))]
def cogroup[W](other: RDD[(K, W)], partitioner: Partitioner) :
      RDD[(K, (Iterable[V], Iterable[W]))]
def cogroup[W](other: RDD[(K, W)], numPartitions: Int): RDD[(K, (Iterable[V], Iterable[W]))]
def cogroup[W](other: RDD[(K, W)]): RDD[(K, (Iterable[V], Iterable[W]))]

下面我们举个例子:

scala> val rdd1 = sc.parallelize(Array((1,"x"),(2,"y"),(3,"z")))
rdd1: org.apache.spark.rdd.RDD[(Int, String)] = ParallelCollectionRDD[149] at parallelize at <console>:24

scala> val rdd2 = sc.parallelize(Array((1,"a"),(3,"c"),(4,"d")))
rdd2: org.apache.spark.rdd.RDD[(Int, String)] = ParallelCollectionRDD[150] at parallelize at <console>:24

scala> val rdd3 = rdd1.cogroup(rdd2)
rdd3: org.apache.spark.rdd.RDD[(Int, (Iterable[String], Iterable[String]))] = MapPartitionsRDD[152] at cogroup at <console>:28

scala> rdd3.collect
res79: Array[(Int, (Iterable[String], Iterable[String]))] = Array((1,(CompactBuffer(x),CompactBuffer(a))), (2,(CompactBuffer(y),CompactBuffer())), (3,(CompactBuffer(z),CompactBuffer(c))), (4,(CompactBuffer(),CompactBuffer(d))))

从上面的例子中,我们可以看到,当一个元素不存的时候,它对应的value被设置为CompactBuffer(),类似于leftOuterJoin里面的None。
cogroup返回的结构是CompactBuffer,CompactBuffer并不是scala里定义的数据结构,而是spark里的数据结构,它继承自一个迭代器和序列,所以它的返回值是一个很容易进行循环遍历的集合

cartesian函数

定义如下:

def cartesian[U: ClassTag](other: RDD[U]): RDD[(T, U)]

该函数返回的是Pair类型的RDD,计算结果是当前RDD和other RDD中每个元素进行笛卡儿计算的结果。最后返回的是CartesianRDD。
沿用上面的rdd,举个例子:

scala> rdd1.cartesian(rdd2).collect
res82: Array[((Int, String), (Int, String))] = Array(((1,x),(1,a)), ((1,x),(3,c)), ((1,x),(4,d)), ((2,y),(1,a)), ((2,y),(3,c)), ((2,y),(4,d)), ((3,z),(1,a)), ((3,z),(3,c)), ((3,z),(4,d)))

可以看出,这个笛卡尔成绩把两个RDD的所有元素进行组合,这样的运算复杂度极高,要慎重使用。

randomSplit函数

ranndomSplit根据权重参数数组将RDD随机分成若干个小的RDD,每一个RDD的元素数目根据RDD的总数乘以权重数目计算出来。
定义如下:

def randomSplit(weights: Array[Double], seed: Long = Utils.random.nextLong): Array[RDD[T]]

举个例子:

scala> val RDD3 = sc.parallelize(Array(("a",1),("b",2),("c",3),("d",4),("e",5),("f",6),("g",7),("h",8),("i",9),("j",10)))
RDD3: org.apache.spark.rdd.RDD[(String, Int)] = ParallelCollectionRDD[103] at parallelize at <console>:24

一共有10个元素,将该RDD按0.70.3分割为两个部分,如下:
scala> val Array(x,y) = RDD3.randomSplit(Array(0.7,0.3),11L)
x: org.apache.spark.rdd.RDD[(String, Int)] = MapPartitionsRDD[106] at randomSplit at <console>:26
y: org.apache.spark.rdd.RDD[(String, Int)] = MapPartitionsRDD[107] at randomSplit at <console>:26
可以看出
scala> x.collect()
res77: Array[(String, Int)] = Array((a,1), (b,2), (d,4), (e,5), (g,7), (h,8), (i,9))

scala> y.collect()
res76: Array[(String, Int)] = Array((c,3), (f,6), (j,10))

但是,我们改一下seed再来看一下:

scala> val Array(x,y) = RDD3.randomSplit(Array(0.7,0.3),131L)
x: org.apache.spark.rdd.RDD[(String, Int)] = MapPartitionsRDD[145] at randomSplit at <console>:30
y: org.apache.spark.rdd.RDD[(String, Int)] = MapPartitionsRDD[146] at randomSplit at <console>:30

scala> x.collect()
res74: Array[(String, Int)] = Array((c,3), (d,4), (f,6), (g,7), (i,9))

scala> y.collect()
res75: Array[(String, Int)] = Array((a,1), (b,2), (e,5), (h,8), (j,10))
很奇怪的发现,并没有按照7:3分开成两个RDD,相反,元素数目是一样的。

subtract函数

def subtract(other: RDD[T]): RDD[T]
def subtract(other: RDD[T], numPartitions: Int): RDD[T]
def subtract(other: RDD[T], partitioner: Partitioner)(implicit ord: Ordering[T] = null): RDD[T]

该函数类似于intersection,但返回在RDD中出现,并且不在otherRDD中出现的元素,不去重。
使用上面例子的数据,我们来举个例子:

scala> RDD1.subtract(RDD2).map(x=>(x._1,x._2)).foreach(println)
(k1,v1)

zip函数

定义如下:

def zip[U](other: RDD[U])(implicit arg0: ClassTag[U]): RDD[(T, U)]

Zips this RDD with another one, returning key-value pairs with the first element in each RDD,
second element in each RDD, etc. Assumes that the two RDDs have the same number of
partitions and the same number of elements in each partition(e.g. one was made through
a map on the other).

zip函数用于将两个RDD组合成Key/Value形式的RDD,这里默认两个RDD的partition数量以及元素数量都相同,否则会抛出异常。这两个RDD具有相同的分区数目、每个分区的元素数目也是一样的。
在机器学习中因为数据格式多为

scala> val a = sc.parallelize(1 to 10, 3)
a: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[108] at parallelize at <console>:28

scala> val b = sc.parallelize(11 to 20, 3)
b: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[109] at parallelize at <console>:29

scala> a.zip(b).collect
res49: Array[(Int, Int)] = Array((1,11), (2,12), (3,13), (4,14), (5,15), (6,16), (7,17), (8,18), (9,19), (10,20))

scala> val c = sc.parallelize(11 to 20, 3)
c: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[115] at parallelize at <console>:24

scala> a.zip(b).zip(c).map(x=>x._1._1+"|"+x._1._2+"|"+x._2).collect
res51: Array[String] = Array(1|11|11, 2|12|12, 3|13|13, 4|14|14, 5|15|15, 6|16|16, 7|17|17, 8|18|18, 9|19|19, 10|20|20)

zipParititions

定义如下:

参数是一个RDD
def zipPartitions[B, V](rdd2: RDD[B])(f: (Iterator[T], Iterator[B]) => Iterator[V])(implicit arg0: ClassTag[B], arg1: ClassTag[V]): RDD[V]

def zipPartitions[B, V](rdd2: RDD[B], preservesPartitioning: Boolean)(f: (Iterator[T], Iterator[B]) => Iterator[V])(implicit arg0: ClassTag[B], arg1: ClassTag[V]): RDD[V]

参数是两个RDD
def zipPartitions[B, C, V](rdd2: RDD[B], rdd3: RDD[C])(f: (Iterator[T], Iterator[B], Iterator[C]) => Iterator[V])(implicit arg0: ClassTag[B], arg1: ClassTag[C], arg2: ClassTag[V]): RDD[V]

def zipPartitions[B, C, V](rdd2: RDD[B], rdd3: RDD[C], preservesPartitioning: Boolean)(f: (Iterator[T], Iterator[B], Iterator[C]) => Iterator[V])(implicit arg0: ClassTag[B], arg1: ClassTag[C], arg2: ClassTag[V]): RDD[V]

参数是三个RDD
def zipPartitions[B, C, D, V](rdd2: RDD[B], rdd3: RDD[C], rdd4: RDD[D])(f: (Iterator[T], Iterator[B], Iterator[C], Iterator[D]) => Iterator[V])(implicit arg0: ClassTag[B], arg1: ClassTag[C], arg2: ClassTag[D], arg3: ClassTag[V]): RDD[V]

def zipPartitions[B, C, D, V](rdd2: RDD[B], rdd3: RDD[C], rdd4: RDD[D], preservesPartitioning: Boolean)(f: (Iterator[T], Iterator[B], Iterator[C], Iterator[D]) => Iterator[V])(implicit arg0: ClassTag[B], arg1: ClassTag[C], arg2: ClassTag[D], arg3: ClassTag[V]): RDD[V]

zip与zipPartitions的区别和map与mapPartitions的区别类似,下面举个例子对定义进行说明:

生成一个rdd1,元素从15,分到两个分区中
scala> var rdd1 = sc.makeRDD(1 to 5,2)
rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[119] at makeRDD at <console>:24

生成一个rdd2,元素从A到E,分到两个分区中
scala> var rdd2 = sc.makeRDD(Seq("A","B","C","D","E"),2)
rdd2: org.apache.spark.rdd.RDD[String] = ParallelCollectionRDD[120] at makeRDD at <console>:24

查看rdd1的元素的partition分布
scala> rdd1.mapPartitionsWithIndex{
     |          (x,iter) => {
     |            var result = List[String]()
     |              while(iter.hasNext){
     |                result ::= ("partition_" + x + "|" + iter.next())
     |              }
     |              result.iterator
     |          }
     | }.collect
res56: Array[String] = Array(partition_0|2, partition_0|1, partition_1|5, partition_1|4, partition_1|3)
可以看出,1,2在partition 0,3,4,5在partition 1

查看rdd2的元素的分区分布
scala> rdd2.mapPartitionsWithIndex{
     |          (x,iter) => {
     |            var result = List[String]()
     |              while(iter.hasNext){
     |                result ::= ("partition_" + x + "|" + iter.next())
     |              }
     |              result.iterator
     |          }
     | }.collect
res57: Array[String] = Array(partition_0|B, partition_0|A, partition_1|E, partition_1|D, partition_1|C)
可以看出,A,B在partition 0,C,D,E在partition 1

scala> rdd1.zipPartitions(rdd2){
     |    (rdd1Iter,rdd2Iter) => {
     |      var result = List[String]()
     |      while(rdd1Iter.hasNext && rdd2Iter.hasNext) {
     |        result::=(rdd1Iter.next() + "_" + rdd2Iter.next())
     |      }
     |      result.iterator
     |    }
     | }.collect
res55: Array[String] = Array(2_B, 1_A, 5_E, 4_D, 3_C)
ZipPartitions和zip的结果是一样的,区别在于分区操作的性能。

对于两个及以上的RDD zip,操作如下:
scala> var rdd3 = sc.makeRDD(Seq("a","b","c","d","e"),2)
rdd3: org.apache.spark.rdd.RDD[String] = ParallelCollectionRDD[126] at makeRDD at <console>:24

scala> var rdd4 = rdd1.zipPartitions(rdd2,rdd3){
     |   (rdd1Iter,rdd2Iter,rdd3Iter) => {
     |     var result = List[String]()
     |     while(rdd1Iter.hasNext && rdd2Iter.hasNext && rdd3Iter.hasNext) {
     |       result::=(rdd1Iter.next() + "_" + rdd2Iter.next() + "_" + rdd3Iter.next())
     |     }
     |     result.iterator
     |   }
     | }
rdd4: org.apache.spark.rdd.RDD[String] = ZippedPartitionsRDD3[127] at zipPartitions at <console>:30

scala> rdd4.collect
res58: Array[String] = Array(2_B_b, 1_A_a, 5_E_e, 4_D_d, 3_C_c)

zipWithIndex

该函数将RDD中的元素和这个元素在RDD中的ID(索引号)组合成键/值对,索引号从0开始,在RDD跨多个Partition的时候会启动spark作业。
定义如下:

def zipWithIndex(): RDD[(T, Long)]

举几个例子:

scala> rdd1.zipWithIndex().collect
res60: Array[(Int, Long)] = Array((1,0), (2,1), (3,2), (4,3), (5,4))

scala> rdd2.zipWithIndex().collect
res61: Array[(String, Long)] = Array((A,0), (B,1), (C,2), (D,3), (E,4))

scala> rdd3.zipWithIndex().collect
res62: Array[(String, Long)] = Array((a,0), (b,1), (c,2), (d,3), (e,4))

zipWithUniqueId

zipWithUniqueId在RDD跨多个Partition的时候不会启动spark作业。
定义如下:

def zipWithUniqueId(): RDD[(T, Long)]

举个例子:

scala> rdd3.zipWithUniqueId().collect
res63: Array[(String, Long)] = Array((a,0), (b,2), (c,1), (d,3), (e,5))

可以看出zipWithUniqueId函数生成唯一id的方法是,每个分区中第一个元素的唯一ID值为:该分区索引号,每个分区中第N个元素的唯一ID值为:(前一个元素的唯一ID值) + (该RDD总的分区数)

coalesce函数

定义如下:

def coalesce(numPartitions: Int, shuffle: Boolean = false)(implicit ord: Ordering[T] = null): RDD[T]

该函数用于将RDD进行重分区,使用HashPartitioner。
第一个参数为重分区的数目,第二个为是否进行shuffle,默认为false;

repartition函数

定义如下:

def repartition(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T]

该函数其实就是coalesce函数第二个参数为true的实现

对比一下coalesce与reparation,在某种情况下第二个参数为false的时候,coalesce会比repartition快一些,因为不用做shuffle,但是由于coalesce属于文件合并,数据分布可能不均衡,而repartition数据比较均衡,更利于发挥spark的威力。coalesce无法将文件从少变成多,如果想要把分区数目从2变成3,只能用repartition。
举个例子:

scala> rdd3.partitions.size
res69: Int = 2

scala> rdd3.coalesce(3)
res70: org.apache.spark.rdd.RDD[String] = CoalescedRDD[137] at coalesce at <console>:27

scala> val t1 = rdd3.coalesce(3)
t1: org.apache.spark.rdd.RDD[String] = CoalescedRDD[138] at coalesce at <console>:26

scala> t1.partitions.size
res71: Int = 2

scala> val t2 = rdd3.repartition(3)
t2: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[142] at repartition at <console>:26

scala> t2.partitions.size
res72: Int = 3

可以看出coalesce没办法把分区数目从2变成3,repartition可以、

聚合类转换

distinct函数

定义如下:

def distinct(numSplits: Int = splits.size): RDD[String]

举个例子:

scala> val rdd2 = sc.parallelize(Array((1,"a"),(3,"c"),(4,"d"),(4,"e")))
rdd2: org.apache.spark.rdd.RDD[(Int, String)] = ParallelCollectionRDD[161] at parallelize at <console>:24

scala> rdd2.distinct().collect()
res84: Array[(Int, String)] = Array((4,d), (4,e), (3,c), (1,a))

scala> val rdd2 = sc.parallelize(Array((1,"a"),(3,"c"),(4,"d"),(4,"e"),(4,"d")))
rdd2: org.apache.spark.rdd.RDD[(Int, String)] = ParallelCollectionRDD[168] at parallelize at <console>:24

scala> rdd2.distinct().collect()
res85: Array[(Int, String)] = Array((4,d), (4,e), (3,c), (1,a))

从上面的例子可以看出,当RDD里面的元素(包括key和value,必须是kv对都一样)是一样的时候,distinct会把重复数据剔除。

groupByKey函数

定义如下:

def groupByKey(): RDD[(K, Iterable[V])]

def groupByKey(numPartitions: Int): RDD[(K, Iterable[V])]

def groupByKey(partitioner: Partitioner): RDD[(K, Iterable[V])]

该函数用于将RDD[K,V]中每个K对应的V值,合并到一个集合Iterable[V]中,
参数numPartitions用于指定分区数;
参数partitioner用于指定分区函数;
举个例子:

创建一个RDD,该RDD包含两个分区
scala> val rdd1 = sc.makeRDD(Array(("A",0),("A",2),("B",1),("B",2),("C",1)),2)
rdd1: org.apache.spark.rdd.RDD[(String, Int)] = ParallelCollectionRDD[181] at makeRDD at <console>:24

执行groupByKey操作,groupByKey之后,value就变成了一个可Iterable对象CompactBuffer,把相同key值的value存储到CompactBuffer对象中
scala> rdd1.groupByKey().collect
res91: Array[(String, Iterable[Int])] = Array((B,CompactBuffer(1, 2)), (A,CompactBuffer(0, 2)), (C,CompactBuffer(1)))

使用分区参数再执行
scala> val rdd2 = rdd1.groupByKey(2)
rdd2: org.apache.spark.rdd.RDD[(String, Iterable[Int])] = ShuffledRDD[183] at groupByKey at <console>:26

查看分区数目和结果,我们发现分区数目变成了2
scala> rdd2.mapPartitionsWithIndex{
         (x,iter) => {
           var result = List[String]()
             while(iter.hasNext){
               result ::= ("partition_" + x + "|" + iter.next())
             }
             result.iterator
         }
}.collect
res92: Array[String] = Array(partition_0|(B,CompactBuffer(1, 2)), partition_1|(C,CompactBuffer(1)), partition_1|(A,CompactBuffer(0, 2)))

当调用 groupByKey时,因为数据要在分区之间移动,会造成大量的shuffle。大数据量应避免使用 groupByKey函数。

reduceByKey函数(reduceByKeyLocally,reduceByKeyToDriver函数)

定义如下:

def reduceByKey(func: (V, V) => V): RDD[(K, V)]

def reduceByKey(func: (V, V) => V, numPartitions: Int): RDD[(K, V)]

def reduceByKey(partitioner: Partitioner, func: (V, V) => V): RDD[(K, V)]

def reduceByKeyLocally(func: (V, V) => V): Map[K, V]

def reduceByKeyToDriver(func: (V, V) => V): Map[K, V]

举个例子:

上面我们定义了
val rdd1 = sc.makeRDD(Array(("A",0),("A",2),("B",1),("B",2),("C",1)),2)

直接用这个rdd做reduceByKey转换
scala> rdd1.reduceByKey(_+_).collect
res93: Array[(String, Int)] = Array((B,3), (A,2), (C,1))

下面我们换一个value变成String类型的
scala> val rdd2 = sc.parallelize(Array((1,"x"),(1,"y"),(3,"z")))
rdd2: org.apache.spark.rdd.RDD[(Int, String)] = ParallelCollectionRDD[186] at parallelize at <console>:24

scala> rdd2.reduceByKey(_+_).collect
res94: Array[(Int, String)] = Array((1,xy), (3,z))

我们可以从上面的例子中发现一些有趣的事情,那就是reduceByKey之后,value值被加到了一起,如果是字符串则被串到一起。

aggregateByKey函数

定义如下:

def aggregateByKey[U](zeroValue: U)(seqOp: (U, V) ⇒ U, combOp: (U, U) ⇒ U)(implicit arg0: ClassTag[U]): RDD[(K, U)]
def aggregateByKey[U](zeroValue: U, numPartitions: Int)(seqOp: (U, V) ⇒ U, combOp: (U, U) ⇒ U)(implicit arg0: ClassTag[U]): RDD[(K, U)]
def aggregateByKey[U](zeroValue: U, partitioner: Partitioner)(seqOp: (U, V) ⇒ U, combOp: (U, U) ⇒ U)(implicit arg0: ClassTag[U]): RDD[(K, U)]

combineByKey函数

定义如下:

def combineByKey[C](createCombiner: V => C, mergeValue: (C, V) => C, mergeCombiners: (C, C) => C): RDD[(K, C)]
def combineByKey[C](createCombiner: V => C, mergeValue: (C, V) => C, mergeCombiners: (C, C) => C, numPartitions: Int): RDD[(K, C)]
def combineByKey[C](createCombiner: V => C, mergeValue: (C, V) => C, mergeCombiners: (C, C) => C, partitioner: Partitioner, mapSideCombine: Boolean = true, serializerClass: String = null): RDD[(K, C)]

这个参数有几点说明
首先,在一个partition中,createCombiner为一个key的第一个出现创建一个初始值(combiner),所以这一步仅仅是初始化一个元组,这个元组包含第一个值和一个key计数器1。

然后当在该分区中,如果发现某个key的combiner已经存在,触发mergeValue,将新发现的值添加到存在元组中。

最后mergeCombiner把各个分区的combiner合并。

举个例子(参考文档7):


scala> val a = sc.parallelize(Array((1,"dog"), (1,"cat"), (2,"gnu"), (2,"salmon"), (2,"rabbit"), (1,"turkey"), (2,"wolf"), (2,"bear"), (2,"bee")),2)
a: org.apache.spark.rdd.RDD[(Int, String)] = ParallelCollectionRDD[191] at parallelize at <console>:24

scala> a.collect
res99: Array[(Int, String)] = Array((1,dog), (1,cat), (2,gnu), (2,salmon), (2,rabbit), (1,turkey), (2,wolf), (2,bear), (2,bee))

scala> val b = a.combineByKey(List(_), (x:List[String], y:String) => y :: x, (x:List[String], y:List[String]) => x ::: y)
b: org.apache.spark.rdd.RDD[(Int, List[String])] = ShuffledRDD[192] at combineByKey at <console>:30
在这个例子中:
List(_)为createCombiner: V => C,所以我们的combiner就是一个List,当遇到一个key的时候就创建一个,比如遇见1

(x:List[String], y:String) => y :: x就是mergeValue: (C, V) => CC是combiner,所以是一个List,V是一个值,此处为y,String类型对象。当再遇见key1的时候,把两个value追加到tuple中,此处是把cat和dog添加到一起。

(x:List[String], y:List[String]) => x ::: y是mergeCombiners: (C, C) => C,mergeCombiners的两个对象都是combiner,所以是两个List,假如key=1的turkey在另外一个分区中,就可以把这两个combiner合并了。

scala> b.collect
res100: Array[(Int, List[String])] = Array((2,List(salmon, gnu, bee, bear, wolf, rabbit)), (1,List(cat, dog, turkey)))

-------------------------------------------
说明:y :: x意思是把y加到List x的头部。
:: 该方法被称为cons,意为构造,向队列的头部追加数据,创造新的列表。用法为 x::list,其中x为加入到头部的元素,无论x是列表与否,它都只将成为新生成列表的第一个元素,也就是说新生成的列表长度为list的长度+1(btw, x::list等价于list.::(x))(参考文档8)

x:::y意味着把两个List拼到一起。
::: 该方法只能用于连接两个List类型的集合

举个例子:
scala> val list1 = List(1,2)
val list2 = List(3,4)
list1: List[Int] = List(1, 2)
list2: List[Int] = List(3, 4)

scala> list1:::list2
res101: List[Int] = List(1, 2, 3, 4)

scala> val x = 4
x: Int = 4

scala> x::list1
res102: List[Int] = List(4, 1, 2)

管道类转换

pipe函数

spark在RDD上提供了pipe()方法。通过pipe(),你可以使用任意语言将RDD中的各元素从标准输入流中以字符串形式读出,并将这些元素执行任何你需要的操作,然后把结果以字符串形式写入标准输出,这个过程就是RDD的转化操作过程。
定义如下:

def pipe(command: Seq[String], env: Map[String, String] = Map(), printPipeContext: ((String) ⇒ Unit) ⇒ Unit = null, printRDDElement: (T, (String) ⇒ Unit) ⇒ Unit = null, separateWorkingDir: Boolean = false, bufferSize: Int = 8192, encoding: String = Codec.defaultCharsetCodec.name): RDD[String]

def pipe(command: String, env: Map[String, String]): RDD[String]

def pipe(command: Seq[String]): RDD[String]

解释一下(参考文档9):
command 命令在分叉进程中运行。(command to run in forked process.)

env 环境变量设置。(environment variables to set.)
printPipeContext 在管道元件之前,这个功能被称为管道上下文数据的机会。打印行功能(如out.println)将作为printPipeContext的参数传递。(Before piping elements, this function is called as an opportunity to pipe context data. Print line function (like out.println) will be passed as printPipeContext’s parameter.)

printRDDElement 使用此功能自定义如何管道元素。将使用每个RDD元素作为第一个参数和打印行函数(如out.println())作为第二个参数调用此函数。以流式方式管理groupBy()的RDD数据的示例,而不是构建一个巨大的String来连接所有元素:(Use this function to customize how to pipe elements. This function will be called with each RDD element as the 1st parameter, and the print line function (like out.println()) as the 2nd parameter. An example of pipe the RDD data of groupBy() in a streaming way, instead of constructing a huge String to concat all the elements:)
def printRDDElement(record:(String, Seq[String]), f:String=>Unit) =
for (e <- record._2) {f(e)}

separateWorkingDir 为每个任务使用单独的工作目录。(Use separate working directories for each task.)

bufferSize 用于管道过程的stdin writer的缓冲区大小。(Buffer size for the stdin writer for the piped process.)

encoding 用于通过管道过程交互(通过stdin,stdout和stderr)的Char编码(Char encoding used for interacting (via stdin, stdout and stderr) with the piped process)

函数返回值 结果RDD(the result RDD)

将由管道元素创建的RDD返回到分叉的外部过程。通过每个分区执行给定的进程一次计算得到的RDD。每个输入分区的所有元素将被写入一个进程的标准,作为输入行以换行分隔。结果分区由进程的stdout输出组成,每行stdout都会导出输出分区的一个元素。即使对于空分区也会调用进程。打印行为可以通过提供两个功能进行定制。

举个例子(例子来源于参考文档6):

第一步,准备一个RDD
val data = List("hi","hello","how","are","you")
val dataRDD = sc.makeRDD(data)

第二步,创建一个echo.sh脚本
#!/bin/sh
echo "Running shell script"
while read LINE; do
   echo ${LINE}    
done

第三步,Pipe rdd data to shell script
val scriptPath = "/home/hadoop/echo.sh"
val pipeRDD = dataRDD.pipe(scriptPath)
pipeRDD.collect()

参考资料

  1. http://spark.apache.org/docs/0.7.2/api/core/spark/RDD.html
  2. http://blog.csdn.net/xubo245/article/details/51485443
  3. https://stackoverflow.com/questions/25079830/accessing-hdfs-input-split-path-in-rdd-methods/25151800#25151800
  4. http://www.neilron.xyz/join-in-spark/
  5. http://lxw1234.com/archives/2015/07/352.htm
  6. http://blog.madhukaraphatak.com/pipe-in-spark/
  7. http://homepage.cs.latrobe.edu.au/zhe/ZhenHeSparkRDDAPIExamples.html
  8. https://segmentfault.com/a/1190000005083578
  9. https://gxnotes.com/article/7533.html
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值