转化
单个pair
- reduceByKey(func) 合并相同建的value
- groupByKey() 对相同键的value分组
- combineByKey(createCombiner,mergeValue,mergeCombiners,partitioner) 合并相同key的value并返回不同的类型
- aggregate()
- mapValues(func)
- flatMapValues(func)
- keys 没有括号
- values 没有括号
- sortByKey()
两个pair
- subtractByKey
- join 内连接
- rightOuterJoin 右外连接
- leftOuterJoin 左外连接
- cogroup
聚合
- reduceByKey()
- foldByKey()
下面是几个聚合的例子
scala> val input = sc.parallelize(Array(("panda",0),("pink",3),("private",3),("panda",1),("pink",4)))
input: org.apache.spark.rdd.RDD[(String, Int)] = ParallelCollectionRDD[0] at parallelize at <console>:24
scala> input.mapValues(x=>(x,1)).reduceByKey((x,y)=>(x._1+y._1,x._2+y._2))
res0: org.apache.spark.rdd.RDD[(String, (Int, Int))] = ShuffledRDD[2] at reduceByKey at <console>:26
scala> val lines = sc.textFile("file:///home/hadoop/software/spark/spark-2.4.4-bin-hadoop2.7/README.md")
lines: org.apache.spark.rdd.RDD[String] = file:///home/hadoop/software/spark/spark-2.4.4-bin-hadoop2.7/README.md MapPartitionsRDD[4] at textFile at <console>:24
scala> val words = lines.flatMap(line=>line.split(" "))
words: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[5] at flatMap at <console>:25
scala> val result = words.map(x=>(x,1)).reduceByKey((x,y)=>x+y)
result: org.apache.spark.rdd.RDD[(String, Int)] = ShuffledRDD[7] at reduceByKey at <console>:25
scala> result.take(5).foreach(println)
(package,1)
(For,3)
(Programs,1)
(processing.,1)
(Because,1)
combineByKey()有多个参数对应聚合操作的各个阶段,可以用来解释聚合操作各个阶段的功能划分。
scala> val input = sc.parallelize(Array(("panda",0),("pink",3),("private",3),("panda",1),("pink",4)))
input: org.apache.spark.rdd.RDD[(String, Int)] = ParallelCollectionRDD[0] at parallelize at <console>:24
scala> val result = input.combineByKey(
(v)=>(v,1), //创建 createCombiner V=>C
(acc:(Int,Int),v)=>(acc._1+v,acc._2+1), //合并值 mergeValue
(acc1:(Int,Int),acc2:(Int,Int))=>(acc1._1+acc2._1,acc1._1+acc2._2)) // 合并C
.map{case (key,value)=>(key,value._1/value._2.toFloat)}
result: org.apache.spark.rdd.RDD[(String, Float)] = MapPartitionsRDD[2] at map at <console>:25
scala> result.collectAsMap().map(println(_))
(private,3.0)
(pink,3.5)
(panda,0.5)
res0: Iterable[Unit] = ArrayBuffer((), (), ())
并行度调优:在执行聚合或分组操作时,可以要求Spark使用给定的分区数,Spark会尝试根据集群的大小推断出一个有意义的默认值。
scala> val data = Seq(("a",3),("b",4),("a",1))
data: Seq[(String, Int)] = List((a,3), (b,4), (a,1))
scala> sc.parallelize(data).reduceByKey((x,y)=>x+y)
res2: org.apache.spark.rdd.RDD[(String, Int)] = ShuffledRDD[4] at reduceByKey at <console>:27
scala> sc.parallelize(data).reduceByKey((x,y)=>x+y,10)
res3: org.apache.spark.rdd.RDD[(String, Int)] = ShuffledRDD[6] at reduceByKey at <console>:27
关于分区
rdd.partitions.size
获取分区数repartition()
创建新的分区集合
分组
- groupByKey()
- cogroup() =>[(K,(Iterable[V],Iterable[W]))]
连接
- join()
- rightOuterJoin()
- leftOuterJoin()
排序
- sortByKey() 支持自定义比较函数
val input:RDD[(Int,Venue)] = ...
implicit val sortIntegerByString = new Ordering[Int]{
override def compare(a:Int,b:Int) = a.toString.compare(b.toString)
}
rdd.sortByKey()
action
- countByKey() 对key计数
- collectAsMap() 将结果以映射表形式返回,现在是不是都为
collect()
? - lookup(key) 返回key对应的value
数据分区
合理的分组
以下是一个简单的应用,假设过去存在一个[UserID,UserInfo]
用户订阅表,周期性的会产生一个过去五分钟的事件,是[UserID,LinkInfo]
的表。
val sc = new Sparkcontext(...)
val userData = sc.sequenceFile[UserID,UserInfo]("hdfs://...").persist()
def processNewLogs(logFileName:String){
val events = sc.sequenceFile[UserID,LinkInfo](logFileName)
val joined = userData.join(events)
val offTopicVisits = joined.filter{
case (userId,(userInfo,linkInfo)) => !userInfo.topics.contains(linkInfo.topic)
}.count()
println("Number of visits to non-subscribed topics:" + offTopicVisits)
}
上述代码不够高效,在于join()执行,每次都会计算所有键的哈希值传输到同一台机器然后对所有键相同的记录连接操作。(哈希值计算和跨节点数据混洗)
通过partitionBy()
将表转化为哈希分区。
val sc = new SparkContext(...)
val userData = sc.sequenceFile[UserID,UserInfo]("hdfs://...").partitionBy(new HashPartitioner(100)).persist()
- 由于构建userData时调用了partitionBy(),Spark就知道RDD根据键的哈希值来分区的,这样在调用join()时,Spark只会对events混洗操作,将events中特定UserID的记录发送到userdata的对应分区所在的机器上。
- 事实上,除了join()的很多操作也会利用已有的分区信息,比如sortByKey()和groupByKey()会分别生成范围分区的RDD和哈希分区的RDD;另一方面,map()这样的操作会导致新的RDD失去父RDD的分区信息
- 如果没有将partitionBy()转化操作的结果持久化,RDD都会重复地对数据进行分区操作。
获取RDD的分区方式
通过partitoner属性获取分区信息
scala> import org.apache.spark
import org.apache.spark
scala> val pairs = sc.parallelize(List((1,1),(2,2),(3,3)))
pairs: org.apache.spark.rdd.RDD[(Int, Int)] = ParallelCollectionRDD[7] at parallelize at <console>:25
scala> pairs.partitioner
res4: Option[org.apache.spark.Partitioner] = None
scala> val partitioned = pairs.partitionBy(new spark.HashPartitioner(2))
partitioned: org.apache.spark.rdd.RDD[(Int, Int)] = ShuffledRDD[8] at partitionBy at <console>:26
scala> partitioned.partitioner
res5: Option[org.apache.spark.Partitioner] = Some(org.apache.spark.HashPartitioner@2)
- Spark内部知道各操作如何影响分区方式,并将对数据进行分区的操作的结果RDD自动设置为对应的分区器。如join()连接两个RDD,由于键相同的元素会被hash到同一台机器上,Spark直到输出结果也是hash分区的;不过转化操作不一定按已知方式分区,如map会改变键值,此时就不会有固定的分区方式,不过我们可以采用另外两个操作mapValues()和flatMapValues()作为替代方法,可以保证每个二元组的键保持不变。以下是为生成的RDD设定好分区方式的操作:cogroup()、groupWith()、join()、leftOuterJoin()、rightOuterJoin()、groupByKey()、reduceByKey()、combineByKey()、partitionBy()、sort()、mapValues()、flatMapValues()、filter()
示例:PageRank
算法维护两个数据集一个(pageID,linkList)包含每个页面的相邻页面表,一个(pageID,rank)。计算每个页面的PageRank值
- 1.排序值初始化为1.0
- 2.每次迭代向其相邻页面发送一个值为
rank(p)/numNeighbors(p)
- 3.将每个页面的排序值设为
0.15+0.85*contributionReceived
- 4.重复循环,算法会逐渐收敛于每个页面的实际PageRank值
val links = sc.objectFile[(String,Seq[String])]("links").partitionBy(new HashPartitioner(100)).persist()
val ranks = links.mapValues(v=>1.0)
for(i <- 0 until 10){
val contributions = links.join(ranks).flatMap{
// dest相邻的link rank/links.size传递的值
case (pageId,(links,rank))=>links.map(dest=>(dest,rank/links.size))
}
ranks = contributions.reduceByKey((x,y)=>x+y).mapValues(v=>0.15+0.85*v)
}
result.saveAsTextFile("ranks")
这里解释一下,根据pageid合并上述两个表=>(pageId,(links,rank))
,将links中的每一个项更新为一个pair
:(dest,rank/links/size)
,即每一个项的计算贡献值,最后合并贡献值更新rank值。
上述代码看起来简单,还是做了不少事情来保持高效的方式:
- links是一个静态数据集,所以我们程序一开始进行了分区操作,这样就不需要把它通过网络进行数据混洗,相比于普通MapReduce节省了相当可观的网络通信开销。
- 我们对links调用了持久化,将它保存在内存中以供每次迭代使用
- 第一次创建ranks,我们通过mapValues()而不是map()来保留父RDD的分区方式,这样对于它进行的第一次连接操作开销很小
- 循环体中,我们用reduceByKey()后使用mapValues():因为reduceByKey()的结果已经是哈希分区,下一次循环将映射操作的结果再次与links进行连接操作更加高效。
- 短短的几行代码简直great!!
注意:为了最大化分区优化的潜在作用,在无需改变键的情况下金亮使用mapValues()或flatMapValues()
自定义分区方式
还是以上面的例子,如果pageid是url,不同的url域名可能相同,相同域名下的网页可能相互链接,因此把相同域名分在同一个分区会更好。这里我们自定义分区实现仅根据域名而不是整个url分区。
定义自定义的分类器,需要继承org.apache.spark.Partitioner
并实现下面三个方法:
- numPartitions:Int:返回创建出来的分区数
- getPartition(key:Any):Int:返回给定键的分区编号(0到numPartition-1)
- equals():Java判断相等性的方法,Spark需要用这个方法检查你的分区器对象是否和其他分区器实例相同,这样Spark才能判断RDD的分区方式是否相同。
下面编写一个基于域名的分区器,这个分区器只对url中的域名部分hash
class DomainNamePartitioner(numParts:Int) extends Partitioner{
override def numpartitions: Int = numParts
override def getPartition(key:Any): Int = {
val domain = new Java.net.URL(key.toString).getHost()
val code = (domain.hashCode % numPartitions)
if(code<0){
code + numPartitions
}
else{
code
}
}
override def equals(other:Any):Boolean = other match{
case dnp:DomainNamePartitioner=>dnp.numPartitions == numPartitions
case _ => false
}
}
以上是Spark键值对操作的简单学习,下一篇是数据读取与保存