大数据技术Spark之Spark Core(三)
一:action
reduce(func) :作用: 通过 func 函数聚集 RDD 中的所有元素,先聚合分区内数据,再聚合分区间数据。
collect():作用: 在驱动程序中,以数组的形式返回数据集的所有元素。
count():作用: 返回 RDD 中元素的个数
first():作用:返回RDD中的第一个元素
take(n):作用:返回一个由RDD的n个元素组成的数组
takeOrdered(n):作用:返回改RDD排序后的前n个元素组成的数组
aggregate:作用: aggregate 函数将每个分区里面的元素通过 seqOp 和初始值进行聚合,然后用
combine 函数将每个分区的结果和初始值(zeroValue)进行 combine 操作。这个函数最终返回
的类型不需要和 RDD 中元素类型一致。
fold(num)(func): 作用: 折叠操作, aggregate 的简化操作, seqop 和 combop 一样
saveAsTextFile(path):作用: 将数据集的元素以 textfile 的形式保存到 HDFS 文件系统或者其他支持的文件系统,对于每个元素, Spark 将会调用 toString 方法,将它装换为文件中的文本。
saveAsSequenceFile(path):作用: 将数据集中的元素以 Hadoop sequencefile 的格式保存到指定的目录下,可以使 HDFS或者其他 Hadoop 支持的文件系统
countByKey():作用: 针对(K,V)类型的 RDD,返回一个(K,Int)的 map,表示每一个 key 对应的元素个数。
foreach(func):作用:在数据集的每一个元素上运行函数func进行更新
二:RDD中的函数传递
在实际开发中我们往往需要自己定义一些对于 RDD 的操作,那么此时需要主要的是,初始化工作是在 Driver 端进行的,而实际运行程序是在 Executor 端进行的,这就涉及到了跨进程通信,是需要序列化的。 下面我们看几个例子
package com.ityouxin.action
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.rdd.RDD
class Search(query:String) extends Serializable {
def isMatch(s:String):Boolean={
s.contains(query)
}
def getMatch1(rdd:RDD[String]):RDD[String]={
rdd.filter(this.isMatch)
}
def getMatch2(rdd:RDD[String]):RDD[String]={
rdd.filter(x=>x.contains(query))
}
}
object SeriTest{
def main(args: Array[String]): Unit = {
val config: SparkConf = new SparkConf()
.setAppName("Spark_Study")
.setMaster("local[*]") //本地模式
val sc = new SparkContext(config)
val rdd1 = sc.parallelize(Array("hadoop", "spark", "hive", "ityouxin"))
val search = new Search("oop")
val rdd2 = search.getMatch1(rdd1)
println(rdd2.collect().mkString(","))
val rdd3: RDD[String] = search.getMatch2(rdd1)
println(rdd3.collect().mkString(","))
}
}
三:RDD的依赖关系
窄依赖
窄依赖指的是每一个父 RDD 的 Partition 最多被子 RDD 的一个 Partition 使用,窄依赖我们形象的比喻为独生子女
宽依赖
宽依赖指的是多个子 RDD 的 Partition 会依赖同一个父 RDD 的 Partition,会引起 shuffle,总结: 宽依赖我们形象的比喻为超生
DAG:
DAG(Directed Acyclic Graph)叫做有向无环图,原始的 RDD 通过一系列的转换就就形成了 DAG,根据 RDD 之间的依赖关系的不同将 DAG 划分成不同的 Stage,对于窄依赖,partition 的转换处理在 Stage 中完成计算。对于宽依赖,由于有 Shuffle 的存在,只能在 parentRDD 处理完成后,才能开始接下来的计算,因此宽依赖是划分 Stage 的依据 。
任务划分(面试重点)
RDD 任务切分中间分为: Application、 Job、 Stage 和 Task
1) Application:初始化一个 SparkContext 即生成一个 Application
2) Job:一个 Action 算子就会生成一个 Job
3) Stage: 根据 RDD 之间的依赖关系的不同将 Job 划分成不同的 Stage, 遇到一个宽依赖则
划分一个 Stage。
4) Task: Stage 是一个 TaskSet, 将 Stage 划分的结果发送到不同的 Executor 执行即为一个
Task。
注意: Application->Job->Stage-> Task 每一层都是 1 对 n 的关系。
RDD缓存
RDD通过persist方法或cache方法可以将前面的计算结果缓存, 默认情况下 persist() 会把数据以序列化的形式缓存在 JVM 的堆空间中。但是并不是这两个方法被调用时立即缓存,而是触发后面的 action 时, 该 RDD 将会被缓存在计算节点的内存中,并供后面重用。
四:数据读取和保存
Spark 的数据读取及数据保存可以从两个维度来作区分:文件格式以及文件系统。
文件格式分为: Text 文件、 Json 文件、 Csv 文件、 Sequence 文件以及 Object 文件;
文件系统分为:本地文件系统、 HDFS、 HBASE 以及数据库。
- text文件类型的数据
读取:textFile(String) 保存:saveAsTextFile(String)
- Json文件
如果 JSON 文件中每一行就是一个 JSON 记录,那么可以通过将 JSON 文件当做文本
文件来读取,然后利用相关的 JSON 库对每一条数据进行 JSON 解析。
注意:使用 RDD 读取 JSON 文件处理很复杂,同时 SparkSQL 集成了很好的处理 JSON
文件的方式,所以应用中多是采用 SparkSQL 处理 JSON 文件。
读取文件:val json = sc.textFile("/people.json")
解析json数据:val result = json.map(JSON.parseFull)
- Sequence 文件
SequenceFile 文件是 Hadoop 用来存储二进制形式的 key-value 对而设计的一种平面文
件(Flat File)。 Spark 有专门用来读取 SequenceFile 的接口。在 SparkContext 中,可以调用
sequenceFile keyClass, valueClass。
注意: SequenceFile 文件只针对 PairRDD
保存RDD:scala> rdd.saveAsSequenceFile(“file:///opt/module/spark/seqFile”)
读取文件:val seq = sc.sequenceFileInt,Int
- HDFS
Spark 的整个生态系统与 Hadoop 是完全兼容的,所以对于 Hadoop 所支持的文件类型或者数据库类型,Spark 也同样支持.另外,由于 Hadoop 的 API 有新旧两个版本,所以 Spark 为了能够兼容 Hadoop 所有的版本,也提供了两套创建操作接口.对于外部存储创建操作而言,hadoopRDD 和 newHadoopRDD 是最为抽象的两个函数接口,主要包含以下四个参数.
1) 输入格式(InputFormat): 制定数据输入的类型,如 TextInputFormat 等,新旧两个版本所引用的版本分别是 org.apache.hadoop.mapred.InputFormat 和
org.apache.hadoop.mapreduce.InputFormat(NewInputFormat)
2) 键类型: 指定[K,V]键值对中 K 的类型
3) 值类型: 指定[K,V]键值对中 V 的类型
4) 分区值: 指定由外部存储生成的 RDD 的 partition 数量的最小值,如果没有指定,系统
会使用默认值 defaultMinSplits
注意:其他创建操作的 API 接口都是为了方便最终的 Spark 程序开发者而设置的,是这两个接口的高效实现版本.例如,对于 textFile 而言,只有 path 这个指定文件路径的参数,其他参数在系统内部指定了默认值。
1.在 Hadoop 中以压缩形式存储的数据,不需要指定解压方式就能够进行读取,因为Hadoop 本身有一个解压器会根据压缩文件的后缀推断解压算法进行解压.
2.如果用 Spark 从 Hadoop 中读取某种类型的数据不知道怎么读取的时候,上网查找一个
使用 map-reduce 的时候是怎么读取这种数据的,然后再将对应的读取方式改写成上面的
hadoopRDD 和 newAPIHadoopRDD 两个类就行了 - MySQL数据库连接
支持通过 Java JDBC 访问关系型数据库。需要通过 JdbcRDD 进行,示例如下:
1. 添加依赖
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.27</version>
</dependency>
2. mYSQL读取
class MyAvg {
var spark:SparkSession = null
@Before
def init={
spark = SparkSession.builder().appName("sparkSql").master("local[*]").getOrCreate()
}
@Test
def sqlDataSource={
//load 默认的加载文件的格式由参数spark.sql.source.default决定
val df = spark.read.load("datas/users.parquet")
df.show()
// spark.sql("select * from parquet.'datas/users.parquet'").show()
//spark.sql("select * from json.'datas/people.json'").show()
//jdbc
spark.read.format("jdbc")
.option("url","jdbc:mysql://localhost:3306/day10")
.option("dbtable","user")
.option("user","root")
.option("password","123")
.load().show()
println("-------------------")
val option = new Properties
option.setProperty("user","root")
option.setProperty("password","123")
spark.read.jdbc("jdbc:mysql://localhost:3306/day10","user",option).show()
}
- HBASE数据库
由于 org.apache.hadoop.hbase.mapreduce.TableInputFormat 类的实现, Spark 可以通过
Hadoop 输入格式访问 HBase。这个输入格式会返回键值对数据,其中键的类型为 org.
apache.hadoop.hbase.io.ImmutableBytesWritable,而值的类型为 org.apache.hadoop.hbase.client.
Result。
package com.ityouxin.hbase
import org.apache.hadoop.hbase.HBaseConfiguration
import org.apache.hadoop.hbase.client.{Put, Result}
import org.apache.hadoop.hbase.io.ImmutableBytesWritable
import org.apache.hadoop.hbase.mapred.TableOutputFormat
import org.apache.hadoop.hbase.mapreduce.TableInputFormat
import org.apache.hadoop.hbase.util.Bytes
import org.apache.hadoop.mapred.JobConf
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
object Spark_Hbase {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("hbase")
val sc = new SparkContext(conf)
val datas = sc.makeRDD(List(("1","2","3"),("11","22","33")))
val rdd2 = datas.map{
case(rowkey,ename,salay) =>
val put = new Put(Bytes.toBytes(rowkey))
put.addColumn(Bytes.toBytes("info"),Bytes.toBytes("ename"),Bytes.toBytes(ename))
put.addColumn(Bytes.toBytes("info"),Bytes.toBytes("sal"),Bytes.toBytes(salay))
(new ImmutableBytesWritable,put)
}
val hbaseConf = HBaseConfiguration.create
hbaseConf.set(TableOutputFormat.OUTPUT_TABLE,"hbase_emp_table")
val jobConf:JobConf = new JobConf(hbaseConf)
jobConf.setOutputFormat(classOf[TableOutputFormat])
rdd2.saveAsHadoopDataset(jobConf)
//注意netty包的冲突,通过执行mvn dependncy:tree 来进行查找相应jar包,然后排出
//读Hbase表
hbaseConf.set(TableInputFormat.INPUT_TABLE,"hbase_emp_table")
val ReadRdd: RDD[(ImmutableBytesWritable, Result)] =
sc.newAPIHadoopRDD(hbaseConf,classOf[TableInputFormat],classOf[ImmutableBytesWritable],classOf[Result])
sc.stop()
}
}
- RDD编程进阶
- 累加器
累加器用来对信息进行聚合,通常在向 Spark 传递函数时,比如使用 map() 函数或者用 filter() 传条件时,可以使用驱动器程序中定义的变量,但是集群中运行的每个任务都会得到这些变量的一份新的副本,更新这些副本的值也不会影响驱动器中的对应变量。 如果我们想实现所有分片处理时更新共享变量的功能,那么累加器可以实现我们想要的效果。 - 系统累加器
val notice = sc.textFile("./NOTICE")
val blanklines = sc.accumulator(0)
累加器的用法如下所示。
通过在驱动器中调用 SparkContext.accumulator(initialValue)方法,创建出存有初始值的
累加器。返回值为 org.apache.spark.Accumulator[T] 对象,其中 T 是初始值 initialValue 的
类型。 Spark 闭包里的执行器代码可以使用累加器的 += 方法(在 Java 中是 add)增加累加器
的值。 驱动器程序可以调用累加器的 value 属性(在 Java 中使用 value()或 setValue())来访问
累加器的值。
注意: 工作节点上的任务不能访问累加器的值。从这些任务的角度来看,累加器是一个只写
变量。
对于要在行动操作中使用的累加器, Spark 只会把每个任务对各累加器的修改应用一次。
因此,如果想要一个无论在失败还是重复计算时都绝对可靠的累加器,我们必须把它放在
foreach() 这样的行动操作中。转化操作中累加器可能会发生不止一次更新 - 自定义累加器
自定义累加器类型的功能在 1.X 版本中就已经提供了,但是使用起来比较麻烦,在 2.0版本后,累加器的易用性有了较大的改进,而且官方还提供了一个新的抽象类:AccumulatorV2 来提供更加友好的自定义类型累加器的实现方式。实现自定义类型累加器需要继承 AccumulatorV2 并至少覆写下例中出现的方法,下面这个累加器可以用于在程序运行过程中收集一些文本类信息,最终以Set[String]的形式返回。 1
package com.ityouxin
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.util.AccumulatorV2
object accumulator {
def main(args: Array[String]): Unit = {
val conf:SparkConf = new SparkConf().setMaster("local[*]").setAppName("accumulator")
val sc = new SparkContext(conf)
val rdd1 = sc.makeRDD(List(1,2,3,4,5,6),2)
val sum = new Myacc
sc.register(sum)
rdd1.foreach(i=>{
sum.sum =sum.sum+i
})
println(sum.sum)
sc.stop()
}
}
class Myacc extends AccumulatorV2[Int,Int]{
var sum = 0
override def isZero: Boolean = {
if (sum==0)true else false
}
override def copy(): AccumulatorV2[Int, Int] = {
val acc =new Myacc
acc.sum=this.sum
acc
}
override def reset(): Unit = {
this.sum=0
}
override def add(v: Int): Unit = {
this.sum = this.sum +v
}
override def merge(other: AccumulatorV2[Int, Int]): Unit = {
this.sum = this.sum+other.value
}
override def value: Int = {
this.sum
}
}
- 自定义累加器案例(二)
package com.ityouxin
import java.util
import org.apache.spark.util.AccumulatorV2
import org.apache.spark.{SparkConf, SparkContext}
//过滤掉字母
object Spark_Accumulator {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("hbase")
val sc = new SparkContext(conf)
val accumulator = new LogAccumulator
sc.register(accumulator)
//rdd,过滤掉带字母的元素
val sum =sc.makeRDD(Array(("1"),("2a"),("33"),("7lhx"),("5")),2).filter(
line=>{
//正则表达书,匹配带字母的元组
val pattern = """^-?(\d+)"""
val flag =line.matches(pattern)
//当flag匹配到字母后,将字段进行调用LogAccumulator.class类的add方法进行处理
if (!flag){
accumulator.add(line)
}
flag//进行map映射转换,然后reduce算子实现过滤后的value相加
}).map(_.toInt).reduce(_+_)
println("sum : " + sum)
val str: util.Iterator[String] = accumulator.value.iterator()
while (str.hasNext){
println(str.next())
}
}
class LogAccumulator extends AccumulatorV2[String,java.util.Set[String]]{
private val _logArray:java.util.Set[String] = new java.util.HashSet[String]()
override def isZero: Boolean = {
_logArray.isEmpty
}
override def copy():org.apache.spark.util.AccumulatorV2[String, java.util.Set[String]] = {
val accumulator = new LogAccumulator()
accumulator.synchronized(
accumulator._logArray.addAll(_logArray)
)
accumulator
}
override def reset(): Unit = {
_logArray.clear()
}
override def add(v: String): Unit = {
_logArray.add(v)
}
override def merge(other: org.apache.spark.util.AccumulatorV2[String, java.util.Set[String]]): Unit = {
other match {
case o:LogAccumulator => _logArray.addAll(o.value)
}
}
override def value: java.util.Set[String] = {
java.util.Collections.unmodifiableSet(_logArray)
}
}
}
- 广播变量(调优策略)
广播变量用来高效分发较大的对象。向所有工作节点发送一个较大的只读值,以供一个或多个 Spark 操作使用。比如,如果你的应用需要向所有节点发送一个较大的只读查询表,甚至是机器学习算法中的一个很大的特征向量,广播变量用起来都很顺手。 在多个并行操作中使用同一个变量,但是Spark 会为每个任务分别发送 。
使用广播变量的过程如下:
(1) 通过对一个类型 T 的对象调用 SparkContext.broadcast 创建出一个 Broadcast[T]对象。 任何可序列化的类型都可以这么实现。
(2) 通过 value 属性访问该对象的值(在 Java 中为 value() 方法)。
(3) 变量只会被发到各个节点一次,应作为只读值处理(修改这个值不会影响到别的节点)。