使用scala来编写
Apache Spark RDD教程会帮你开始明白使用SparkRDD(弹性分布式数据集). 你可以学习到什么RDD及他的优势,限制,创建RDD,使用transformations,actions和操作KV RDD.
什么是RDD(强性分布式数据集)
RDD是spark数据结构的基础,配线架spark spark core数据的核心抽象. RDDs是容错的,不变的分布式数据集,也就是说你创建后的RDD不能改变.每个 RDD会划分成逻辑分区,这些分区可以在集群中不同节点上运算.
换言之,RDD是一个数据集和Scala中数据集相似, 不同时RDD计算是在不同的JVM上, 分布在不同的物理服务器,这些服务器也叫集群中节点,而scala集群只在一个JVM上.
另外,RDD分区数据的抽象和数据的分区被设置计算在并行方式在不同 的节点,当RDD做转换操作操作,大多数时间,我们不需要担心并发,spark默认已提供.
Spark RDD教程描述RDD的基本可用操作,像map, filter和persist等等.另外也解释KV RDD函数,操作KV RDD像groupByKey 和join等.
RDD 优点
- 内存计算
- 不可变
- 容错性
- 懒计算
- 分区
- 并行
限制
RDD 不是适合更新存储状态系统的应用,像web应用.对于这些应用,使用执行传统更新日志或数据检查的系统会更高效,像数据库. RDD的目标是提供高效的编译模型来做批处理分区和允许异步处理的应用.
RDD只有在一个SparkContext出现,并且RDD有名字和一个唯一标识.
创建RDD
创建RDD主要有两种不同方式, 1.通过已存在集合创建. 2 引入外部存储系统作为数据集(HDFS,S3等等).
在看例子之前 ,首先需要实例化SparkSeesion,使用builer模式方法来定义SparkSession类.当初始化时,需要提供master和应用名字来作为下边展示.
val spark:SparkSession = SparkSession.builder()
.master("local[1]")
.appName("SparkByExamples.com")
.getOrCreate()
使用SparkContext.parallelize()
sparkContext.parallelize在driver上用使程序并行计算已存在的集合.这是最基础的方法来创建RDD也是主要当POC或是原型开发,它需要所有 数据在driver上创建RDD,因此在生产上不常用.
//Create RDD from parallelize
val dataSeq = Seq(("Java", 20000), ("Python", 100000), ("Scala", 3000))
val rdd=spark.sparkContext.parallelize(dataSeq)
使用sparkContext.textFile()
读取一个文本文件到RDD
//Create RDD from external Data source
val rdd2 = spark.sparkContext.textFile("/path/textFile.txt")
使用sparkContext.wholeTextFiles()
wholeTextFiles函数返回一个PairRDD, 带一个key是文件路径,value是内容
//Reads entire file into a RDD as single record.
val rdd3 = spark.sparkContext.wholeTextFiles("/path/textFile.txt")
除了使用文本文件,也可以创建RDD从csv,json不同格式
使用sparkContext.emptyRDD
在SparkContext使用emptyRDD()方法会得到一个没有数据的RDD.这样创建的空RDD没有任何分区.
//Creates empty RDD with no partition
val rdd = spark.sparkContext.emptyRDD // creates EmptyRDD[0]
val rddString = spark.sparkContext.emptyRDD[String] // creates EmptyRDD[1]
用分区状态空的RDD
有时需要可能需要通过分区写空RDD文件,在这个例子中,你应该创建一个带有分区的空RDD.
//Create empty RDD with partition
val rdd2 = spark.sparkContext.parallelize(Seq.empty[String])
RDD并行计算和重分区
当使用SparkContext的parallelize()或是textFile()或wholeTextFile()方法初始化RDD时,根据资源可用性会自动切分数据到不同分区.
getNumPartitions 返回数据集中parition个数.任何转换操作会在应用并行计算.Spark会运行一个任务到每个集群中的分区.
println("initial partition count:"+rdd.getNumPartitions)
//Outputs: initial partition count:2
手动设置并行数也可以手动设置并行数量,所需做的是传入一个分区个数到这个函数中sparkContext.parallelize(dataSeq,10)
重分区使用repartition 和coalesce 有时需要重分区RDD,Spark提供两种方式, 1 使用repartition()方法,在所有节点重组数据,被称为完全重组. 2 coalesce()方法,重组数据从最少的节点,例如你有4个分区的数据,并且做coalesce(2)移动数据为2个节点.
两个函数都是需要分区数量来分区rdd,如下图.注意reparition()方法非常耗性能的操作,因为他重组数据是集群里的所有节点.
val reparRdd = rdd.repartition(4)
println("re-partition count:"+reparRdd.getNumPartitions)
//Outputs: "re-partition count:4
注意: repartition() 或 coalesce()都返回新的RDD
RDD操作
RDD转换操作 是延时操作,替代RDD更新操作,这些操作返回一个新的RDD
RDD action 操作触发计算并返回RDD值.
RDD转换操作例子
转换操作返回另外一个RDD,并且转换操作是延时的,意味着至到你调用RDD action操作才会运算.一种转换操作在RDD上有flatMap,map,reduceByKey,sortByKey, 返回的样的RDD代替当前更新状态.
在Spark RDD 转换教程中,我会解释转换用计算单词数例子.下边图演示我们要用的不同RDD转换.
首先通过读文件来创建一个RDD.
val rdd:RDD[String] = spark.sparkContext.textFile("src/main/scala/test.txt")
flatMap flatmap()应用打平RDD返回一个新RDD.下边例子中,首先在RDD中通过空格切分记录,最后打平.结果RDD由每个记录的单独的单词组成.
val rdd2 = rdd.flatMap(f=>f.split(" "))
map - map()转换被 应用到任何复杂的操作像,增加列,更新列等等. map转换操作的输出和输入问题有相同数量.
单词计数的例子中,为每个单词增加一个值为1的列. RDD的结果是PairRDD函数,它包含KV 对,单词是键,1为值.为了更清楚,定义变量rdd3用这个类型
val rdd3:RDD[(String,Int)]= rdd2.map(m=>(m,1))
filter转换被用来过滤RDD.此例中过滤单词开始为"a"的
val rdd4 = rdd3.filter(a=> a._1.startsWith("a"))
reduceByKey 为每个key合并值,用指定的函数.此例中,缩减单词字符串通过应用sum函数在值上.RDD结果包含唯一的单词和他们的数量.
val rdd5 = rdd4.reduceByKey(_ + _)
sortByKey sortByKey()转换被用来排序RDD元素在key. 首先转换RDD[(String,Int)] 到RDD[(Int,String)]使用map转换,再应用sortByKey,理想上是通过整型排序. 最后foreach 打印出所有RDD中单词和他们的count作为KV对.
val rdd6 = rdd5.map(a=>(a._2,a._1)).sortByKey()
//Print rdd6 result to console
rdd6.foreach(println)
RDD action例子
RDD action操作返回RDD中原始的值.换言之,任何RDD函数会返回不是RDD[T]被考虑是一个Action操作.
在RDD Action教程中,会继续用单词计数例子,最后语句foreach()是一个action,返回所有RDD的数据并且打印在控制台.看下更多的Action操作.
count 返回RDD中记录数
//Action - count
println("Count : "+rdd6.count())
first 返回第一条记录
//Action - first
val firstRec = rdd6.first()
println("First Record : "+firstRec._1 + ","+ firstRec._2)
max返回最大记录
//Action - max
val datMax = rdd6.max()
println("Max Record : "+datMax._1 + ","+ datMax._2)
reduce 缩减记录为单条,用来返回数量或是累加.
//Action - reduce
val totalWordCount = rdd6.reduce((a,b) => (a._1+b._1,a._2))
println("dataReduce Record : "+totalWordCount._1)
take 返回指定条数记录
//Action - take
val data3 = rdd6.take(3)
data3.foreach(f=>{
println("data3 Key:"+ f._1 +", Value:"+f._2)
})
collect 返回从RDD中所有数据为数组.这个操作需要小心,返回的数据在driver端.
//Action - collect
val data = rdd6.collect()
data.foreach(f=>{
println("Key:"+ f._1 +", Value:"+f._2)
})
saveAsTextFile RDD保存到文本文件
rdd6.saveAsTextFile("/tmp/wordCount")
更多查看RDD actions.
RDD类型
- PaireRDDFunction或PairRDD PairRDD是一个KV对,是最常用的RDD.
- ShuffleRDD
- DoubleRDD
- SequenceFileRDD
- HadoopRDD
- ParallelCollectionRDD
Shuffle 操作
Shuffling是一种机制,Spark用重新分配数据向不同的执行器,甚至向不同机器.Spark shuff触发当我位执行确定转换操作像在RDD上groupByKey(),reduceByKey(),join().
Spark Shuffle是非常耗性能的操作,因为它涉及如下
- 磁盘I/O
- 包含数据序列化与把序列化
- 网络I/O
当创建RDD时,Spark没有必要为在分区的key存储数据,因为这时,我们根本不能为数据集设置key.
因此,当运行reduceByKey()操作来聚合在key上数据时,spark做如下操作,需要首先运行任务来搜集分区上所有数据,并且处理数据在不同的key上做聚合.如下
- Spark先运行map任务在所有的分区中,分组所有数据到各自的key中.
- map的任务结果保存在内存
- 当结果内存不能保存时,spark把结果存储到磁盘.
- Spark重组映射的数据通过分区,有时它也会存储数据到磁盘来复用,当它需要在计算时.
- 运行垃圾回收器
- 最后运行reduce任务在不同分区基于key
Shuffle分区大小和性能
基于数据集大小,核数量,Spark内存shuffling可以得益或是损害你的job.当你处理少量数据时,你应该通常减少shuffle分区,否则最后会有非常多的分区中非常少的记录.这样会运行非常多的任务处理更少的数据.
另一方向,当你有太多数据和少分区会导致更少的时间运行任务,更多的时间来获得内存,也会内存溢出错误.
获得shuffle合适的分区大小总是棘手,并且获取很多运行不同值来获得优化的数量.这是一个关键属性,来查找什么,当你在Spark上有性能总是.
RDD持久化教程
Spark Cache和persist是一种优化技术来提高RDD job性能,在迭代和交互时用.这里会解释怎么用persist()和cache().
尽管Spark提供计算比MapReduce快100倍,如果没有设计好你任务复用重复计算,性能会被降级,当你处理百亿或万亿数据.因此我们需要考虑计算和使用优化技术做为提高性能的一种方式.
使用cache()和persist()方法,spark提供一种优化机制来存储RDD中间计算,所以可以重用在随后的行为.
当你persist 或 cache RDD,每个工作节点存储他的分区数据到内存或是磁盘,并且复用他们在其它RDD的行为. Spark persist数据在节点上,是容错的,意味着如果分区丢失,它也会自动重计算使用转换来创建出来.
persist RDD优点
- 消耗高效 Spark计算非常有代价,因此复用计算的使用是节约开销.
- 处理时间高效 复用重复计算来节约时间
- 执行时间 节约任务的执行时间,允许我们在集群上执行更多job
RDD Cache
Spark RDD cache()
被默认保存RDD计算到存储级别MEMORY_ONLY
意味着存储数据在JVM堆做为未序列化对象.
Spark RDD cache()
内部调用persist(),又调用sparkSession.sharedState.cacheManager.cacheQuery
来缓存RDD结果数据.看下例子
val rdd = sc.textFile("src/main/resources/zipcodes-noheader.csv")
val rdd2:RDD[ZipCode] = rdd.map(row=>{
val strArray = row.split(",")
ZipCode(strArray(0).toInt,strArray(1),strArray(3),strArray(4))
})
val rdd3 = rdd2.cache()
println(rdd3.count())
RDD persist
persist()
被用来存储RDD指定一个存储级别,MEMORY_ONLY
,MEMORY_AND_DISK
,MEMORY_ONLY_SER
,EMORY_AND_DISK_SER
,DISK_ONLY
,MEMORY_ONLY_2
,MEMORY_AND_DISK_2
等.
Spark persiste有两个签名,1 没有默认MEMORY_ONLY
没有参数.2 用StorageLevel作为参数来存储不同存储级别.
val dfPersist = rdd.persist(StorageLevel.MEMORY_ONLY)
dfPersist.show(false)
RDD Unpersist
Spark自动监控每个你调用persist()和cache(),会检查每个节点使用或丢弃数据,如果是不还使用或是最小的最近使用(LRU)算法.也可以手动调用unpersist()来删除.unpersist()标记RDD作为非保存,并且 删除所有的内存和磁盘上的块.
val rddPersist2 = rddPersist.unpersist()
unpersist(Boolean)
用布尔类型参数,直到所有块被删除.
Persistence 存储级别
org.apache.spark.storage.StorageLevel
类下有可用的所有不同的存储级别.
MEMORY_ONLY
,MEMORY_ONLY_SER
,MEMORY_ONLY_2
,MEMORY_ONLY_SER_2
,MEMORY_AND_DISK
,MEMORY_AND_DISK_SER
,MEMORY_AND_DISK_2
,MEMORY_AND_DISK_SER_2
,DISK_ONLY
,DISK_ONLY_2
Spark 共享变量
这里了解Spark共享变量的不同,并且它在转换时是怎么使用的.
当Spark执行转换操作map() 或reduce(),转换操作在远程的节点执行,通过使用变量,这个变量包含任务和那些不发送回driver端的变量,因此没有 能力复用和分享这些变量在其它任务中.Spark共享变量使用两种技术解决了这样问题.
- Broadcast变量(只读)
- Accumulator变量(可更新)
Broadcast变量
Broadcast变量是只读共享变量,在集群中所有节点上缓存并可用,可以被任务访问和使用.替代发送数据需要在不同任务中,spark分发broadcast变量到每个机器,使用非常高效算法来减少数据交换的开销.
Spark RDD broadcast最常见例子的使用是查找zip代码,状态,城市等等.
当你运行Spark RDD job使用Broadcast变量定义及使用如下
- Saprk拆分job为stage,分布shuffling和执行在stage中
- 过后stage被拆分成task
- Spark broadcast数据(通常复用)在不同的stage中被需要
- broadcast数据被缓存用序列化的格式,并且当任务执行进行反序列化.
Spark Broadcast创建需要使用SparkContext broadcast(v)方法.这个方法需要你想要广播的v参数.
broadcastVar: org.apache.spark.broadcast.Broadcast[Array[Int]] = Broadcast(0)
scala> broadcastVar.value
res0: Array[Int] = Array(0, 1, 2, 3)
注意:broadcast 变量不是在sc.broadcast(variable)
调用后诮发送到不同的执行节点,而是在执行节点第一次使用时才发送.
参考: Spark RDD Broadcast shared variable
Accumulators
Spark Accumulators是另外一个共享变量,仅仅通过 关联和交互操作,被用来执行计数(类似于Mapreduce中counter)或是sum操作.
默认Spark支持创建任意数值类型的accumulator,并且提供自定义累加器类型.创建累加器步骤如下:
- 累加器名字
- 无名累加器
当你创建一个有名累加器,你可以在sparkUI中"Accumulator"tab中看到.在这个tab,可以看到两张表,1 累加器- 由所有累加器和他们的值. 2 任务 - 每个任务中被修改的累加器
在SparkUIk中没有展示的没有命名的累加器在哪里?对于所有实践建议,用有名累加器.
累加变量被创建使用SparkContext.longAccumulator(v)
scala> val accum = sc.longAccumulator("SumAccumulator")
accum: org.apache.spark.util.LongAccumulator = LongAccumulator(id: 0, name: Some(SumAccumulator), value: 0)
scala> sc.parallelize(Array(1, 2, 3)).foreach(x => accum.add(x))
-----
-----
scala> accum.value
res2: Long = 6
默认Spark支持累加器方法有long,double和集合类型.所有这些方法在SparkContext类中,并分别返回LongAccumulator,DoubleAccumulator 和CollectionAccumulator.
高级API Dataframe & Dataset
RDD和DataFrame可以相互转换
//Converts RDD to DataFrame
val dfFromRDD1 = rdd.toDF()
//Converts RDD to DataFrame with column names
val dfFromRDD2 = rdd.toDF("col1","col2")
//using createDataFrame() - Convert DataFrame to RDD
val df = spark.createDataFrame(rdd).toDF("col1","col2")
//Convert RDD to Dataset
val ds = spark.createDataset(rdd)
//Convert DataFrame to RDD
val rdd = df.rdd
更多查看学习dataframe/dataset教程