map 和 flatMap:
通过一个实验来看Spark 中 map 与 flatMap 的区别。
步骤一:将测试数据放到hdfs上面
hadoopdfs -put data1/test1.txt /tmp/test1.txt
该测试数据有两行文本:
line one
line two
步骤二:在Spark中创建一个RDD来读取hdfs文件/tmp/test1.txt
val textFile = sc.textFile("/tmp/test1.txt")
步骤三:查看map函数的返回值
得到map函数返回的RDD:
val mapres = textFile.map(_,split("\\s+"))
查看map函数的返回值——文件中的每一行数据返回了一个数组对象
mapres.collect ====>>
Array(Array(line,one),Array(line,two))
步骤四:查看flatMap函数的返回值
得到flatMap函数返回的RDD:
val flatRes = textFile.flatMap(_.split("\\s+"))
查看flatMap函数的返回值——文件中的所有行数据仅返回了一个数组对象
Array(line,one,line,two)
总结:
- Spark 中 map函数会对每一条输入进行指定的操作,然后为每一条输入返回一个对象;
- 而flatMap函数则是两个操作的集合——正是“先映射后扁平化”:
操作1:同map函数一样:对每一条输入进行指定的操作,然后为每一条输入返回一个对象
操作2:最后将所有对象合并为一个对象
mapPartitions
def mapPartitions[U](f: (Iterator[T]) => Iterator[U], preservesPartitioning: Boolean = false)(implicit arg0: ClassTag[U]): RDD[U]
该函数和map函数类似,只不过映射函数的参数由RDD中的每一个元素变成了RDD中每一个分区的迭代器。如果在映射的过程中需要频繁创建额外的对象,使用mapPartitions要比map高效的过。
比如,将RDD中的所有数据通过JDBC连接写入数据库,如果使用map函数,可能要为每一个元素都创建一个connection,这样开销很大,如果使用mapPartitions,那么只需要针对每一个分区建立一个connection。
参数preservesPartitioning表示是否保留父RDD的partitioner分区信息。
- var rdd1 = sc.makeRDD(1 to 5,2)
- //rdd1有两个分区
- scala> var rdd3 = rdd1.mapPartitions{ x => {
- | var result = List[Int]()
- | var i = 0
- | while(x.hasNext){
- | i += x.next()
- | }
- | result.::(i).iterator
- | }}
- rdd3: org.apache.spark.rdd.RDD[Int] = MapPartitionsRDD[84] at mapPartitions at :23
-
- //rdd3将rdd1中每个分区中的数值累加
- scala> rdd3.collect
- res65: Array[Int] = Array(3, 12)
- scala> rdd3.partitions.size
- res66: Int = 2
-
mapPartitionsWithIndex
def mapPartitionsWithIndex[U](f: (Int, Iterator[T]) => Iterator[U], preservesPartitioning: Boolean = false)(implicit arg0: ClassTag[U]): RDD[U]
函数作用同mapPartitions,不过提供了两个参数,第一个参数为分区的索引。
- 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)
-
mapPartitions(function)
map()的输入函数是应用于RDD中每个元素,而mapPartitions()的输入函数是应用于每个分区
<code class="hljs scala has-numbering" style="display: block; padding: 0px; background-color: transparent; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-top-left-radius: 0px; border-top-right-radius: 0px; border-bottom-right-radius: 0px; border-bottom-left-radius: 0px; word-wrap: normal; background-position: initial initial; background-repeat: initial initial;">
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">package</span> test
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">import</span> scala.Iterator
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">import</span> org.apache.spark.SparkConf
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">import</span> org.apache.spark.SparkContext
<span class="hljs-class" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">object</span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">TestRdd</span> {</span>
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">def</span> sumOfEveryPartition(input: Iterator[Int]): Int = {
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">var</span> total = <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0</span>
input.foreach { elem =>
total += elem
}
total
}
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">def</span> main(args: Array[String]) {
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">val</span> conf = <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">new</span> SparkConf().setAppName(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"Spark Rdd Test"</span>)
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">val</span> spark = <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">new</span> SparkContext(conf)
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">val</span> input = spark.parallelize(List(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">2</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">3</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">4</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">5</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">6</span>), <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">2</span>)<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">//RDD有6个元素,分成2个partition</span>
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">val</span> result = input.mapPartitions(
partition => Iterator(sumOfEveryPartition(partition)))<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">//partition是传入的参数,是个list,要求返回也是list,即Iterator(sumOfEveryPartition(partition))</span>
result.collect().foreach {
println(_)<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">//6 15</span>
}
spark.stop()
}
}</code>
在很多应用场景都需要对结果数据进行排序,Spark中有时也不例外。在Spark中存在两种对RDD进行排序的函数,分别是 sortBy和sortByKey函数。sortBy是对标准的RDD进行排序,它是从Spark 0.9.0之后才引入的(可以参见SPARK-1063
)。而sortByKey函数是对PairRDD进行排序,也就是有Key和Value的RDD。下面将分别对这两个函数的实现以及使用进行说明。
一、sortBy函数实现以及使用
sortBy函数是在org.apache.spark.rdd.RDD
类中实现的,它的实现如下:
06 | ascending : Boolean = true , |
07 | numPartitions : Int = this .partitions.size) |
08 | ( implicit ord : Ordering[K], ctag : ClassTag[K]) : RDD[T] = |
10 | .sortByKey(ascending, numPartitions) |
该函数最多可以传三个参数:
第一个参数是一个函数,该函数的也有一个带T泛型的参数,返回类型和RDD中元素的类型是一致的;
第二个参数是ascending,从字面的意思大家应该可以猜到,是的,这参数决定排序后RDD中的元素是升序还是降序,默认是true,也就是升序;
第三个参数是numPartitions,该参数决定排序后的RDD的分区个数,默认排序后的分区个数和排序之前的个数相等,即为this.partitions.size
。
从sortBy函数的实现可以看出,第一个参数是必须传入的,而后面的两个参数可以不传入。而且sortBy函数函数的实现依赖于sortByKey函数,关于sortByKey函数后面会进行说明。keyBy函数也是RDD类中进行实现的,它的主要作用就是将将传进来的每个元素作用于f(x)中,并返回tuples类型的元素,也就变成了Key-Value类型的RDD了,它的实现如下:
4 | def keyBy[K](f : T = > K) : RDD[(K, T)] = { |
那么,如何使用sortBy函数呢?
10 | scala> val data = List( 3 , 1 , 90 , 3 , 5 , 12 ) |
11 | data : List[Int] = List( 3 , 1 , 90 , 3 , 5 , 12 ) |
13 | scala> val rdd = sc.parallelize(data) |
14 | rdd : org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[ 0 ] at parallelize at <console> : 14 |
17 | res 0 : Array[Int] = Array( 3 , 1 , 90 , 3 , 5 , 12 ) |
19 | scala> rdd.sortBy(x = > x).collect |
20 | res 1 : Array[Int] = Array( 1 , 3 , 3 , 5 , 12 , 90 ) |
22 | scala> rdd.sortBy(x = > x, false ).collect |
23 | res 3 : Array[Int] = Array( 90 , 12 , 5 , 3 , 3 , 1 ) |
25 | scala> val result = rdd.sortBy(x = > x, false ) |
26 | result : org.apache.spark.rdd.RDD[Int] = MappedRDD[ 23 ] at sortBy at <console> : 16 |
28 | scala> result.partitions.size |
31 | scala> val result = rdd.sortBy(x = > x, false , 1 ) |
32 | result : org.apache.spark.rdd.RDD[Int] = MappedRDD[ 26 ] at sortBy at <console> : 16 |
34 | scala> result.partitions.size |
上面的实例对rdd中的元素进行升序排序。并对排序后的RDD的分区个数进行了修改,上面的result就是排序后的RDD,默认的分区个数是2,而我们对它进行了修改,所以最后变成了1。
二、sortByKey函数实现以及使用
sortByKey函数作用于Key-Value形式的RDD,并对Key进行排序。它是在org.apache.spark.rdd.OrderedRDDFunctions
中实现的,实现如下
1 | def sortByKey(ascending : Boolean = true , numPartitions : Int = self.partitions.size) |
4 | val part = new RangePartitioner(numPartitions, self, ascending) |
5 | new ShuffledRDD[K, V, V](self, part) |
6 | .setKeyOrdering( if (ascending) ordering else ordering.reverse) |
从函数的实现可以看出,它主要接受两个函数,含义和sortBy一样,这里就不进行解释了。该函数返回的RDD一定是ShuffledRDD类型的,因为对源RDD进行排序,必须进行Shuffle操作,而Shuffle操作的结果RDD就是ShuffledRDD。其实这个函数的实现很优雅,里面用到了RangePartitioner,它可以使得相应的范围Key数据分到同一个partition中,然后内部用到了mapPartitions对每个partition中的数据进行排序,而每个partition中数据的排序用到了标准的sort机制,避免了大量数据的shuffle。下面对sortByKey的使用进行说明:
10 | scala> val a = sc.parallelize(List( "wyp" , "iteblog" , "com" , "397090770" , "test" ), 2 ) |
11 | a : org.apache.spark.rdd.RDD[String] = |
12 | ParallelCollectionRDD[ 30 ] at parallelize at <console> : 12 |
14 | scala> val b = sc. parallelize ( 1 to a.count.toInt , 2 ) |
15 | b : org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[ 31 ] at parallelize at <console> : 14 |
17 | scala> val c = a.zip(b) |
18 | c : org.apache.spark.rdd.RDD[(String, Int)] = ZippedPartitionsRDD 2 [ 32 ] at zip at <console> : 16 |
20 | scala> c.sortByKey().collect |
21 | res 11 : Array[(String, Int)] = Array(( 397090770 , 4 ), (com, 3 ), (iteblog, 2 ), (test, 5 ), (wyp, 1 )) |
上面对Key进行了排序。细心的读者可能会问,soryKy函数中的第一个参数可以对排序方式进行重写。为什么sortByKey没有呢?难道只能用默认的排序规则。不是,是有的。其实在OrderedRDDFunctions类中有个变量ordering它是隐形的:private val ordering = implicitly[Ordering[K]]
。他就是默认的排序规则,我们可以对它进行重写,如下:
01 | scala> val b = sc.parallelize(List( 3 , 1 , 9 , 12 , 4 )) |
02 | b : org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[ 38 ] at parallelize at <console> : 12 |
04 | scala> val c = b.zip(a) |
05 | c : org.apache.spark.rdd.RDD[(Int, String)] = ZippedPartitionsRDD 2 [ 39 ] at zip at <console> : 16 |
07 | scala> c.sortByKey().collect |
08 | res 15 : Array[(Int, String)] = Array(( 1 ,iteblog), ( 3 ,wyp), ( 4 ,test), ( 9 ,com), ( 12 , 397090770 )) |
10 | scala> implicit val sortIntegersByString = new Ordering[Int]{ |
11 | | override def compare(a : Int, b : Int) = |
12 | | a.toString.compare(b.toString)} |
13 | sortIntegersByString : Ordering[Int] = $iwC$$iwC$$iwC$$iwC$$iwC$$anon$ 1 @ 5 d 533 f 7 a |
15 | scala> c.sortByKey().collect |
16 | res 17 : Array[(Int, String)] = Array(( 1 ,iteblog), ( 12 , 397090770 ), ( 3 ,wyp), ( 4 ,test), ( 9 ,com)) |
例子中的sortIntegersByString就是修改了默认的排序规则。这样将默认按照Int大小排序改成了对字符串的排序,所以12会排序在3之前。
fold vs flodleft vs foldright
从本质上说,fold函数将一种格式的输入数据转化成另外一种格式返回。fold, foldLeft和foldRight这三个函数除了有一点点不同外,做的事情差不多。我将在下文解释它们的共同点并解释它们的不同点。
我将从一个简单的例子开始,用fold计算一系列整型的和。
1 | val numbers = List( 5 , 4 , 8 , 6 , 2 ) |
2 | numbers.fold( 0 ) { (z, i) = > |
List中的fold方法需要输入两个参数:初始值以及一个函数。输入的函数也需要输入两个参数:累加值和当前item的索引。那么上面的代码片段发生了什么事?
代码开始运行的时候,初始值0作为第一个参数传进到fold函数中,list中的第一个item作为第二个参数传进fold函数中。
1、fold函数开始对传进的两个参数进行计算,在本例中,仅仅是做加法计算,然后返回计算的值;
2、Fold函数然后将上一步返回的值作为输入函数的第一个参数,并且把list中的下一个item作为第二个参数传进继续计算,同样返回计算的值;
3、第2步将重复计算,直到list中的所有元素都被遍历之后,返回最后的计算值,整个过程结束;
4、这虽然是一个简单的例子,让我们来看看一些比较有用的东西。早在后面将会介绍foldLeft函数,并解释它和fold之间的区别,目前,你只需要想象foldLeft函数和fold函数运行过程一样。
下面是一个简单的类和伴生类:
1 | class Foo( val name : String, val age : Int, val sex : Symbol) |
4 | def apply(name : String, age : Int, sex : Symbol) = new Foo(name, age, sex) |
假如我们有很多的Foo实例,并存在list中:
1 | val fooList = Foo( "Hugh Jass" , 25 , 'male) :: |
2 | Foo("Biggus Dickus", 43, ' male) :: |
3 | Foo( "Incontinentia Buttocks" , 37 , 'female) :: |
我们想将上面的list转换成一个存储[title] [name], [age]格式的String链表:
01 | val stringList = fooList.foldLeft(List[String]()) { (z, f) = > |
02 | val title = f.sex match { |
06 | z : + s "$title ${f.name}, ${f.age}" |
和第一个例子一样,我们也有个初始值,这里是一个空的String list,也有一个操作函数。在本例中,我们判断了性别,并构造了我们想要的String,并追加到累加器中(这里是一个list)。
fold, foldLeft, and foldRight之间的区别
主要的区别是fold函数操作遍历问题集合的顺序。foldLeft是从左开始计算,然后往右遍历。foldRight是从右开始算,然后往左遍历。而fold遍历的顺序没有特殊的次序。
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, serializer: Serializer = null): RDD[(K, C)]
第一个和第二个函数都是基于第三个函数实现的,使用的是HashPartitioner,Serializer为null。而第三个函数我们可以指定分区,如果需要使用Serializer的话也可以指定。combineByKey函数比较重要,我们熟悉地诸如aggregateByKey、foldByKey、reduceByKey等函数都是基于该函数实现的。默认情况会在Map端进行组合操作。
combineByKey函数主要接受了三个函数作为参数,分别为createCombiner、mergeValue、mergeCombiners。这三个函数足以说明它究竟做了什么。理解了这三个函数,就可以很好地理解combineByKey。
要理解combineByKey(),要先理解它在处理数据时是如何处理每个元素的。由于combineByKey()会遍历分区中的所有元素,因此每个元素的键要么还没有遇到过,要么就和之前的键相同。combineByKey()的处理流程如下:
1.如果是一个新的元素,此时使用createCombiner()来创建那个键对应的累加器的初始值。(!注意:这个过程会在每个分区第一次出现各个键时发生,而不是在整个RDD中第一次出现一个键时发生。)
2.如果这是一个在处理当前分区中之前已经遇到键,此时combineByKey()使用mergeValue()将该键的累加器对应的当前值与这个新值进行合并。
3.由于每个分区都是独立处理的,因此对于同一个键可以有多个累加器。如果有两个或者更多的分区都有对应同一个键的累加器,就需要使用用户提供的mergeCombiners()将各个分区的结果进行合并。
combineByKey的功能是对RDD中的数据集按照Key进行聚合(想象下Hadoop MapReduce的Combiner,用于Map端做Reduce)。聚合的逻辑是通过自定义函数提供给combineByKey。
从上面的源代码中可以看到,combineByKey是把(K,V)类型的RDD转换为(K,C)类型的RDD,C和V可以不一样。
combineByKey函数需要三个重要的函数作为参数
createCombiner:在遍历RDD的数据集合过程中,对于遍历到的(k,v),如果combineByKey第一次遇到值为k的Key(类型K),那么将对这个(k,v)调用combineCombiner函数,它的作用是将v转换为c(类型是C,聚合对象的类型,c作为局和对象的初始值)
mergeValue:在遍历RDD的数据集合过程中,对于遍历到的(k,v),如果combineByKey不是第一次(或者第二次,第三次…)遇到值为k的Key(类型K),那么将对这个(k,v)调用mergeValue函数,它的作用是将v累加到聚合对象(类型C)中,mergeValue的
类型是(C,V)=>C,参数中的C遍历到此处的聚合对象,然后对v进行聚合得到新的聚合对象值
mergeCombiners:因为combineByKey是在分布式环境下执行,RDD的每个分区单独进行combineByKey操作,最后需要对各个分区的结果进行最后的聚合,它的函数类型是(C,C)=>C,每个参数是分区聚合得到的聚合对象。
假设有一组气象数据,每行数据包含日期和当天的气温(比如,20150601 27),那么可以用combineByKey求每月的平均温度
val rdd = sc.textFile("气象数据")
val rdd2 = rdd.map(x=>x.split(" ")).map(x => (x(0).substring("从年月日中提取年月"),x(1).toInt))
val createCombiner = (k: String, v: Int)=> {(v,1)}
val mergeValue = (c:(Int, Int), v:Int) => {(c._1 + v, c._2 + 1)}
val mergeCombiners = (c1:(Int,Int),c2:(Int,Int))=>{(c1._1 + c2._1, c1._2 + c2._2)}
val vdd3 = vdd2.combineByKey(createCombiner, mergeValue, mergeCombiners)
rdd3.foreach(x=>println(x._1 + ": average tempreture is " + x._2._1/x._2._2)
示例1、
val data = sc.parallelize(List(("iteblog", 1), ("bbs", 1), ("iteblog", 3)))
val createCombiner = (v: Int)=> {(v,1)}
val mergeValue = (c:(Int, Int), v:Int) => {(c._1 + v, c._2 + 1)}
val mergeCombiners = (c1:(Int,Int),c2:(Int,Int))=>{(c1._1 + c2._1, c1._2 + c2._2)}
val rdd3 = data.combineByKey( createCombiner, mergeValue, mergeCombiners )
rdd3.foreach(x=>println(x._1 + ": average tempreture is " + x._2._1/x._2._2))
示例2、
/**
* User: 过往记忆
* Date: 15-03-19
* Time: 上午08:24
* bolg: http://www.iteblog.com
* 本文地址:http://www.iteblog.com/archives/1291
* 过往记忆博客,专注于hadoop、hive、spark、shark、flume的技术博客,大量的干货
* 过往记忆博客微信公共帐号:iteblog_hadoop
*/
scala> val data = sc.parallelize(List((1, "www"), (1, "iteblog"), (1, "com"),
(2, "bbs"), (2, "iteblog"), (2, "com"), (3, "good")))
data: org.apache.spark.rdd.RDD[(Int, String)] =
ParallelCollectionRDD[15] at parallelize at <console>:12
scala> val result = data.combineByKey(x=>List(x),
(x: List [String], y: String) => y :: x, (x: List[String], y: List[String]) => x ::: y)
result: org.apache.spark.rdd.RDD[(Int, List[String])] =
ShuffledRDD[19] at combineByKey at <console>:14
scala> result.collect
res20: Array[(Int, List[String])] = Array((1,List(www, iteblog, com)),
(2,List(bbs, iteblog, com)), (3,List(good)))
scala> val data = sc.parallelize(List(("iteblog", 1), ("bbs", 1), ("iteblog", 3)))
data: org.apache.spark.rdd.RDD[(String, Int)] =
ParallelCollectionRDD[24] at parallelize at <console>:12
scala> val result = data.combineByKey(x => x,
(x: Int, y:Int) => x + y, (x:Int, y: Int) => x + y)
result: org.apache.spark.rdd.RDD[(String, Int)] =
ShuffledRDD[25] at combineByKey at <console>:14
scala> result.collect
res27: Array[(String, Int)] = Array((iteblog,4), (bbs,1))
自定义分区:
我们都知道Spark内部提供了HashPartitioner
和RangePartitioner
两种分区策略(这两种分区的代码解析可以参见:《Spark分区器HashPartitioner和RangePartitioner代码详解》),这两种分区策略在很多情况下都适合我们的场景。但是有些情况下,Spark内部不能符合咱们的需求,这时候我们就可以自定义分区策略。为此,Spark提供了相应的接口,我们只需要扩展Partitioner
抽象类,然后实现里面的三个方法:
01 | package org.apache.spark |
07 | abstract class Partitioner extends Serializable { |
09 | def getPartition(key : Any) : Int |
def numPartitions: Int
:这个方法需要返回你想要创建分区的个数;
def getPartition(key: Any): Int
:这个函数需要对输入的key做计算,然后返回该key的分区ID,范围一定是0到numPartitions-1
;
equals()
:这个是Java标准的判断相等的函数,之所以要求用户实现这个函数是因为Spark内部会比较两个RDD的分区是否一样。
假如我们想把来自同一个域名的URL放到一台节点上,比如:http://www.iteblog.com
和http://www.iteblog.com/archives/1368
,如果你使用HashPartitioner
,这两个URL的Hash值可能不一样,这就使得这两个URL被放到不同的节点上。所以这种情况下我们就需要自定义我们的分区策略,可以如下实现:
01 | package com.iteblog.utils |
03 | import org.apache.spark.Partitioner |
15 | class IteblogPartitioner(numParts : Int) extends Partitioner { |
16 | override def numPartitions : Int = numParts |
18 | override def getPartition(key : Any) : Int = { |
19 | val domain = new java.net.URL(key.toString).getHost() |
20 | val code = (domain.hashCode % numPartitions) |
28 | override def equals(other : Any) : Boolean = other match { |
29 | case iteblog : IteblogPartitioner = > |
30 | iteblog.numPartitions == numPartitions |
35 | override def hashCode : Int = numPartitions |
因为hashCode
值可能为负数,所以我们需要对他进行处理。然后我们就可以在partitionBy()
方法里面使用我们的分区:
1 | iteblog.partitionBy( new IteblogPartitioner( 20 )) |
类似的,在Java中定义自己的分区策略和Scala类似,只需要继承org.apache.spark.Partitioner
,并实现其中的方法即可。
在Python中,你不需要扩展Partitioner类,我们只需要对iteblog.partitionBy()
加上一个额外的hash函数,如下:
3 | def iteblog_domain(url): |
4 | return hash (urlparse.urlparse(url).netloc) |
6 | iteblog.partitionBy( 20 , iteblog_domain) |
mvn 创建scala工程
一般我们都是用SBT来维护Scala工程,不过我们也是可以使用Maven来创建Scala工程。在命令行使用下面语句即可创建Scala工程:
07 | * 过往记忆博客,专注于hadoop、hive、spark、shark、flume的技术博客,大量的干货 |
08 | * 过往记忆博客微信公共帐号:iteblog_hadoop |
11 | mvn archetype:generate -DarchetypeGroupId=net.alchim31.maven |
12 | -DarchetypeArtifactId=scala-archetype-simple |
16 | -Dversion=1.0-SNAPSHOT |
在创建的时候使用到scala-archetype-simple
模版,如果不指定该模版的版本,默认是使用最新版的。目前最新版的scala-archetype-simple
为1.5,其中的Scala版本是2.10.0,已经不是最新版的Scala了,大家可以根据自己的情况去修改pom.xml文件里面的Scala、ScalaTest、Surefire以及scala-maven-plugin的版本。
当运行完上面的命令之后,会在运行命令的当前文件夹下面生成名字为iteblog的Scala工程,文件夹的目录结构树如下:
01 | [iteblog@localhost ~/iteblog]$ tree |
17 | 8 directories, 5 files |
创建的工程默认的包名是使用-DgroupId
的值。默认的情况会生成一个用于测试的App.scala
的文件。
当然,在使用scala-archetype-simple
模版的时候我们还可以选择版本,只需要加上-DarchetypeVersion
选项即可。同时我们还可以通过-Dpackage
选项指定我们创建工程的包名
1 | mvn archetype:generate -DarchetypeGroupId=net.alchim31.maven |
2 | -DarchetypeArtifactId=scala-archetype-simple |