3.2 RDD编程
在 Spark 中,RDD 被表示为对象,通过对象上的方法调用来对 RDD 进行转换。经过一系列的transformations定义 RDD 之后,就可以调用 actions 触发 RDD 的计算action可以是向应用程序返回结果(count, collect等),或者是向存储系统保存数据(saveAsTextFile等)。在Spark中,只有遇到action,才会执行 RDD 的计算(即延迟计算),这样在运行时可以通过管道的方式传输多个转换(计算逻辑)。要使用 Spark,开发者需要编写一个 Driver 程序,它被提交到集群以调度运行 Worker,Driver 中定义了一个或多个 RDD,并调用 RDD 上的 action,Worker 则执行 RDD 分区计算任务。
3.2.1 RDD的创建方式
-
本地集合转换 RDD ---算子--> RDD
-
加载外部数据
3.2.1.1 由集合转换
/**
* Author: Hang.Z
* Description:
* 说明:
* •一旦 RDD 创建成功, 就可以通过并行的方式去操作这个分布式的数据集了.
* •parallelize和makeRDD还有一个重要的参数就是把数据集切分成的分区数.
* •Spark 会为每个分区运行一个任务(task). 正常情况下, Spark 会自动的根据你的集群来设置分区数
*/
object CreateRDD {
def main(args: Array[String]): Unit = {
//创建spark环境
val conf = new SparkConf()
conf.setAppName(CreateRDD.getClass.getSimpleName).setMaster("local[*]")
val sc = new SparkContext(conf)
//调用函数 创建RDD
val rdd1: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4, 5, 6, 7))
//也可以调用创建RDD
val rdd2: RDD[String] = sc.parallelize(Seq[String]("scala", "java", "c++", "SQL"))
// 将数据收集到本地 打印
rdd1.collect().foreach(println)
// 释放资源
sc.stop()
}
示例
package com._51doit.spark.day02
import org.apache.log4j.{Level, Logger}
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
/**
* Author: Hang.Z
* Date: 21/06/02
* Description:
* 本地集合生成RDD
* 1 RDD的分区个数 默认是所有的可用核数为分区数 local[6]
* 2 分区数据的控制
* 1) local[N] N <= 核数
* 2) parallelize(seq , num) num > 0 最好num不要大于核数
* [一个核处理一个Task ,有6个核 7个分区 产生7个Task 有一个Task阻塞
* 4个分区 产生4个Task 造成资源浪费
* 并发7 并行度是6
* ]
*/
object _03MakeRDD {
// 设置日志级别
Logger.getLogger("org").setLevel(Level.ERROR)
def main(args: Array[String]): Unit = {
// this.getClass.getSimpleName 类名
// 环境
val conf: SparkConf = new SparkConf()
.setMaster("local[6]") // 使用本机器的所有核数 16
.setAppName(this.getClass.getSimpleName)
val sc = new SparkContext(conf)
val arr = Array(1,2,3,4)
val mp = Map[String,Int](("wb",23),("duanlang",34))
// 创建RDD
val rdd1: RDD[Int] = sc.parallelize(arr,4)
// 创建RDD
val rdd2: RDD[(String, Int)] = sc.parallelize(mp.toList,7)
val size1: Int = rdd1.partitions.size
val size2: Int = rdd2.partitions.size
/**
* 因为我的机器是16Core ,本地集合的RDD 默认就是机器的所有的可用核数为分区数
*/
println(size1)// 16分区-->16Task
println(size2)// 16分区
sc.stop()
}
}
package com._51doit.spark.day02
import org.apache.log4j.{Level, Logger}
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
/**
* Author: Hang.Z
* Date: 21/06/02
* Description:
* 本地集合生成RDD
* 1 RDD的分区个数 默认是所有的可用核数为分区数 local[6]
*
*/
object _02MakeRDD {
// 设置日志级别
Logger.getLogger("org").setLevel(Level.ERROR)
def main(args: Array[String]): Unit = {
// this.getClass.getSimpleName 类名
// 环境
val conf: SparkConf = new SparkConf()
.setMaster("local[6]") // 使用本机器的所有核数 16
.setAppName(this.getClass.getSimpleName)
val sc = new SparkContext(conf)
val arr = Array(1,2,3,4)
val mp = Map[String,Int](("wb",23),("duanlang",34))
// 创建RDD
val rdd1: RDD[Int] = sc.parallelize(arr)
// 创建RDD
val rdd2: RDD[(String, Int)] = sc.parallelize(mp.toList)
val size1: Int = rdd1.partitions.size
val size2: Int = rdd2.partitions.size
/**
* 因为我的机器是16Core ,本地集合的RDD 默认就是机器的所有的可用核数为分区数
*/
println(size1)// 16分区-->16Task
println(size2)// 16分区
sc.stop()
}
}
package com._51doit.spark.day02
import org.apache.log4j.{Level, Logger}
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
/**
* Author: Hang.Z
* Date: 21/06/02
* Description:
* 本地集合生成RDD
* 1 RDD的分区个数 默认是所有的可用核数为分区数 local[6]
* 2 分区数据的控制
* 1) local[N] N <= 核数
* 2) parallelize(seq , num) num > 0 最好num不要大于核数
* [一个核处理一个Task ,有6个核 7个分区 产生7个Task 有一个Task阻塞
* 4个分区 产生4个Task 造成资源浪费
* 并发7 并行度是6
* ]
*/
object _03MakeRDD {
// 设置日志级别
Logger.getLogger("org").setLevel(Level.ERROR)
def main(args: Array[String]): Unit = {
// this.getClass.getSimpleName 类名
// 环境
val conf: SparkConf = new SparkConf()
.setMaster("local[6]") // 使用本机器的所有核数 16
.setAppName(this.getClass.getSimpleName)
val sc = new SparkContext(conf)
val arr = Array(1,2,3,4)
val mp = Map[String,Int](("wb",23),("duanlang",34))
// 创建RDD
val rdd1: RDD[Int] = sc.parallelize(arr,4)
// 创建RDD
val rdd2: RDD[(String, Int)] = sc.parallelize(mp.toList,7)
val size1: Int = rdd1.partitions.size
val size2: Int = rdd2.partitions.size
/**
* 因为我的机器是16Core ,本地集合的RDD 默认就是机器的所有的可用核数为分区数
*/
println(size1)// 16分区-->16Task
println(size2)// 16分区
sc.stop()
}
}
总结:
本地集合创建RDD 默认有分区
1 分区个数 默认是可用的核数
2 修改分区 设置可用核数 local[*]
3 在创建RDD的时候执行分区个数
注意: 创建的分区的个数和节点的可用核数对应
// 设置日志级别
Logger.getLogger("org").setLevel(Level.ERROR)
def main(args: Array[String]): Unit = {
// this.getClass.getSimpleName 类名
// 环境
val conf: SparkConf = new SparkConf()
.setMaster("local[6]") // 使用本机器的所有核数 16
.setAppName(this.getClass.getSimpleName)
val sc = new SparkContext(conf)
val arr = Array(1,2,3,4)
val mp = Map[String,Int](("wb",23),("duanlang",34))
val rdd1: RDD[Int] = sc.makeRDD(arr, 4)
println(rdd1.partitions.size)
sc.stop()
}
3.2.1.2 读取外部文件
可以是本地文件系统, HDFS, Cassandra, HVase, Amazon S3 等等.
Spark 支持 文本文件, SequenceFiles, 和其他所有的 Hadoop InputFormat
object CreatRDD_File {
//设置控制台日志级别
Logger.getLogger("org").setLevel(Level.ERROR)
def main(args: Array[String]): Unit = {
val sc: SparkContext = SparkUtil.getSc
// 1 相对路劲读取本项目下的文件创建RDD
val rdd1: RDD[String] = sc.textFile("spark-core/data/a.txt")
// 2 绝对路径读取本地磁盘上的文件创建RDD
val rdd2: RDD[String] = sc.textFile("D://word.txt")
// 3 读取HDFS分布式文件系统中的文件创建RDD
val rdd3: RDD[String] = sc.textFile("hdfs://doit01:8020/word.txt")
// 4 使用 统配符加载目录下指定规则文件创建RDD
val rdd4: RDD[String] = sc.textFile("spark-core/data/a*.txt")
// 5 加载文件的时候获取文件名 创建RDD 返回(文件名 , 内容)的元组
val rdd5: RDD[(String, String)] = sc.wholeTextFiles("spark-core/data/")
// 收集数据 并打印
rdd5.collect().foreach(println)
sc.stop()
}
}
1 url可以是本地文件系统文件, hdfs://..., s3n://...等等
2 如果是使用的本地文件系统的路径, 则必须每个节点都要存在这个路径
3 所有基于文件的方法, 都支持目录, 压缩文件, 和通配符(***). 例如:
textFile("/my/directory"), textFile("/my/directory/*.txt"), and textFile("/my/directory/*.gz").
4 textFile还可以有第二个参数, 表示分区数. 默认情况下, 每个块对应一个分区.(对 HDFS 来说, 块大小默认是 128M). 可以传递一个大于块数的分区数, 但是不能传递一个比块数小的分区数.
3.2.1.3 其他RDD转换
val conf = new SparkConf()
conf.setAppName("wc").setMaster("local")
val sc = new SparkContext(conf)
// 创建RDD
val rdd1: RDD[String] = sc.textFile("d://word.txt")
//转换 返回新的RDD
val rdd2: RDD[String] = rdd1.flatMap(_.split("\\s+"))
//转换 返回新的RDD
val rdd3: RDD[(String, Iterable[String])] = rdd2.groupBy(word => word)
//转换 返回新的RDD
val rdd4: RDD[(String, Int)] = rdd3.map(tp => {
(tp._1, tp._2.size)
})
sc.stop()
3.2.2 RDD分区
RDD在被创建的时候已经有了分区 , RDD的分区是RDD并行计算的基础 ,一般情况下一个分区域会被封装成一个Task任务,如果资源足够多的情况下,RDD的分区数就是RDD计算的并行度 . 如果分区多,但是资源核数不够的情况下,分区数是大于并行度的!
3.2.2.1 分区个数
1 使用集合创建的RDD 的分区数
def main(args: Array[String]): Unit = {
val sc: SparkContext = SparkUtil.getSc
// 创建RDD
val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4, 5, 6))
// 手动设置分区数
// val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4, 5, 6) . 6)
// 查看RDD的分区数 , 在不指定分区数的情况下,RDD使用默认分区数
/**
* 1 加载使用集合创建RDD local模式下默认的分区数是 当前环境的可用核数
* 1) setMaster("local[*]") 本地的所有核数
* 2) setMaster("local[8]") 分配的8个核数
* 3) conf.set("spark.default.parallelism", 4) 参数设置
*/
println(rdd.partitions.size)
sc.stop()
}
2 加载文件创建RDD
def main(args: Array[String]): Unit = {
val sc: SparkContext = SparkUtil.getSc
// 1 相对路劲读取本项目下的文件创建RDD
val rdd1: RDD[String] = sc.textFile("spark-core/data/a.txt")
// 2 读取HDFS分布式文件系统中的文件创建RDD 1K 70M 212M 4
val rdd3: RDD[String] = sc.textFile("hdfs://doit01:8020/wc/input/")
/**
* 根据文件数据创建RDD分区数至少为2
* SplitSize 的计算逻辑
* -- FileInputFormat.getSplits(JobConf job, int numSplits)
* -- val goalSize: Long = totalSize / (if (numSplits == 0) { 1}
* else { numSplits}).toLong
* -- this.computeSplitSize(goalSize, minSize, blockSize);
* -- protected long computeSplitSize(long goalSize, long minSize, long blockSize) {
* return Math.max(minSize, Math.min(goalSize, blockSize));(1,3)}
*/
println(rdd3.partitions.size)
sc.stop()
}
3.2.2.2 分区数据划分
集合RDD
def main(args: Array[String]): Unit = {
val sc: SparkContext = SparkUtil.getSc
val ls = List(1,2,3,4,5)
//创建RDD 指定分区为3
val rdd: RDD[Int] = sc.makeRDD(ls, 3)
//生成3个结果文件 [1] [2,3] [4,5]
rdd.saveAsTextFile("data/output1")
sc.stop()
}
---------------------------------------源码解析--------------------------
1) val rdd: RDD[Int] = sc.makeRDD(ls, 3)
2) makeRDD(ls, 3)-->
def makeRDD[T: ClassTag](
seq: Seq[T], // data
numSlices: Int = defaultParallelism): RDD[T] = withScope { //3
parallelize(seq, numSlices) // (data,3)
}
3) parallelize(seq,numSlices) -->
def parallelize[T: ClassTag](
seq: Seq[T],
numSlices: Int = defaultParallelism): RDD[T] = withScope {
assertNotStopped()
new ParallelCollectionRDD[T](this, seq, numSlices, Map[Int, Seq[String]]()) // 看这里两个参数
}
4) new ParallelCollectionRDD[T](this, seq, numSlices, Map[Int, Seq[String]]())-->
override def getPartitions: Array[Partition] = {
val slices = ParallelCollectionRDD.slice(data, numSlices).toArray // 看这里
slices.indices.map(i => new ParallelCollectionPartition(id, i, slices(i))).toArray
}
5)slice(data, numSlices)--->
case _ => // 看这里
val array = seq.toArray // To prevent O(n^2) operations for List etc
positions(array.length, numSlices).map { case (start, end) => // 查看方法
array.slice(start, end).toSeq
}.toSeq
6) positions(array.length, numSlices)--->
def positions(length: Long, numSlices: Int): Iterator[(Int, Int)] = { // 数据核心分配逻辑
(0 until numSlices).iterator.map { i => [0,3) length=seq.length=5
val start = ((i * length) / numSlices).toInt
val end = (((i + 1) * length) / numSlices).toInt
(start, end)
}
(0,1)
(1,3)
(3,5)
7) array.slice---->
override def slice(from : scala.Int, until : scala.Int) : scala.Array[T] = { /* compiled code */ }
-
文件RDD
读取文件数据时,数据是按照Hadoop文件读取的规则进行切片分区,而切片规则和数据读取的规则不一样
加载文件的时候 , 计算任务切片! 任务切片的个数就是分区的个数 [分大小]g
class HadoopRDD[K, V](
sc: SparkContext,
broadcastedConf: Broadcast[SerializableConfiguration],
initLocalJobConfFuncOpt: Option[JobConf => Unit],
inputFormatClass: Class[_ <: InputFormat[K, V]],
keyClass: Class[K],
valueClass: Class[V],
minPartitions: Int)
def defaultMinPartitions: Int = math.min(defaultParallelism, 2)
defaultParallelism 默认是当前机器的所有核数
******************************
minPartitions = 2
******************************
val rdd: RDD[String] = sc.textFile("D:\\spark_data")
1 返回 hadoopFile 任务切片的代码就是hadoop中的任务切片
def textFile(
path: String,
minPartitions: Int = defaultMinPartitions): RDD[String] = withScope {
assertNotStopped()
hadoopFile(path, classOf[TextInputFormat], classOf[LongWritable], classOf[Text],
minPartitions).map(pair => pair._2.toString).setName(path)
}
2 TextInputFormat
public class TextInputFormat extends FileInputFormat<LongWritable, Text>
3 FileInputFormat
4 public InputSplit[] getSplits(JobConf job, int numSplits) throws IOException { // numSplits = 2
谁调用了这个方法 传入参数
5 long goalSize = totalSize / (long)(numSplits == 0 ? 1 : numSplits);
goalSize = (273+5)/2 TOINT
6 long splitSize = this.computeSplitSize(goalSize, minSize, blockSize);
7
protected long computeSplitSize(long goalSize, long minSize, long blockSize) {
return Math.max(minSize, Math.min(goalSize, blockSize));
}
long minSize = Math.max(job.getLong("mapreduce.input.fileinputformat.split.minsize", 1L), this.minSplitSize);
8 Math.max(1, Math.min(139, 128m));
splitSize = 139B
9 计算任务切片个数
for(bytesRemaining = length; (double)bytesRemaining / (double)splitSize > 1.1D; bytesRemaining -= splitSize) {
splitHosts = this.getSplitHostsAndCachedHosts(blkLocations, length - bytesRemaining, splitSize, clusterMap);
splits.add(this.makeSplit(path, length - bytesRemaining, splitSize, splitHosts[0], splitHosts[1]));
}
5B ------------> 1
273B ----------> 2
1+2 任务切片计算