数据分区
- 获取数据分区
- 调用RDD数据的partitioner属性,得到scala.Option对象,即表示scala中用来存放可能存在的对象的容器类。
val pairs = sc.parallelize(list((1,1), (2,2), (3,3))) // 初始化原始RDD数据
pairs.partitioner // output: Option[spark.Partitioner] = None
//调用partitioner方法,查看该RDD分区情况,得到结果未分区
val partitioned = pairs.partitionBy(new spark.HashPartitioner(2)).persist() // 将数据分成两个区。加上persist保证变换后的数据是可持久化的,以便后续操作中直接使用
pairs.partitioner // output: Option[spark.Partitioner] = Some(spark.HashPartitioner@5147788d)
- 从数据分区中获益的操作
- 从数据分区中获益的操作:cogroup(), groupwith(), join(), leftOuterJoin(), rightOuterJoin(), groupbyKey(), reduceByKey(), combineKey(), lookup()
- 获益的机理:对于上述二元操作而言,预先进行数据分区导致其中至少一个RDD不发生数据混洗。
- 影响分区方式的操作
- 会为生成结果RDD按特定方式分区的操作:cogroup(), groupwith(), join(), leftOuterJoin(), rightOuterJoin(), groupbyKey(), reduceByKey(), combineKey(), partitionBy(), sort(), mapValues()(若父RDD有分区方式), flatMapValues()(若父RDD有分区方式), filter()(若父RDD有分区方式).其他操作的生成结果不会有特定的分区方式
- 对二元操作而言,数据的分区方式取决于父RDD的分区方式(包括哈希分区、分区的数量、操作的并行度)
- 自定义分区方式
需要扩展org.apache.spark.Partitioner类中的三个函数:
- numPartitions
- getPartition
- equals
实例:
- 基于域名的分区。即认为同一个域名下的网页更有可能是相互链接的,而在pagerank例中,需要对相同域名下的网页做join工作,因此将它们放在同一个分区会更好,这可以大大缩短通信时间。
import org.apache.spark.Partitioner._
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
}
}
}
// 用来让spark区分分区函数对象的Java equals 方法
override def equals(other: Any): Boolean = other match {
dnp.numPartitions == numPartitions
case _ => false
}
数据读取与保存
文本文件
读取
- 读取单个文件
val input = sc.textFile(data_dir)
- 一次性读取多个文件(均放在同一目录下)
方法1 用textFile()方法,以数据所在目录为参数
方法2 用wholeTextFiles()方法,该方法返回一个pair RDD
val input = sc.wholeTextFiles(data_dir)
val result = input.mapValues {
y => val nums = y.split(" ").map(x => x.Double)
nums.sum / nums.size.toDouble
}
保存
result.saveAsTextFile(outputFile)
JSON
读取
先读取,再解析。由于数据的格式可能不正确,读取过程中无法顺利读取。对于较小的数据集来说,遇到错误时即停止程序;对于较大的数据集来说,尝试用累加器跟踪错误的个数。
例:
import com.fasterxml.jackson.module.scala.{
DefaultScalaModule, experimental.ScalaObjectMapper}
import com.fasterxml.jackson.databind.{
ObjectMapper, DeserializationFeature}
case class Person(name: String, sex: Boolean)
// 将json文件解析为特定的case class. 使用flatMap实现。若遇格式错误则返回空
val result = input.flatMap(record => {
try {
Some(mapper.readValue(record, classOf[person]))
}
catch {
case e: Exception => None
}
})
保存
result.filter(p => p.sex).map(mapper.writeValueAsString(_)).saveAsTextFile(outputFile)
CSV
读取
先读取,再解析。
import java.io.StringReader
import au.com.bytecode.opencsv.CSVReader
val input = sc.textFile(inputFile)
val result = input.map{
line =>
val reader = new CSVReader(new StringReader(line));
reader.readNext();
}
保存
csvFile.map(x => List(x.name, x.sex).toArray)
.mapPartitions{
people =>
val stringWriter = new stringWriter();
val csvWriter = new csvWriter(stringWriter);
csvWriter.writeAll(people.toList);
Iterator(stringWriter.toString)
}.saveAsTextFile(outFile)
SequenceFile
SequenceFile是由Hadoop的Writable接口的元素组成的。scala/java中常见的数据类型在Hadoop中,有其对应的writable类型。在Hadoop系统中,数据很有可能是以SequenceFile的形式而用的。
读取
val data = sc.SequenceFile(inFile, classOf[Text], classOf[IntWritable]).map{
case (x, y) => (x.toString, y.get())}
存储
- 将scala原生类型转化为SequenceFile
val data = sc.parallelize(List(("apple", 1), ("orange", 2)))
data.saveAsSequenceFile(outputFile)
- 先进行类型转换(变成scala原生类型,比如PairRDD),再保存