Spark复习 Day02: SparkCore
1. RDD的依赖关系
-----------------------------------------------
- 每个RDD都会将一系列的血统关系保存下来,以便恢复失去的分区
- RDD的血统会记录RDD的元数据信息和转换行为
- 当RDD部分分区数据丢失时,它可以根据这些信息来重新运算和恢复丢失的数据分区
2. 有向无环图DAG
-----------------------------------------------
- 原始的RDD经过一系列的转换就形成了DAG
- 根据RDD之间的依赖关系将DAG划分成不同的Stage
- 对于窄依赖:
partition的转换处理在Stage中完成计算
- 对于宽依赖:
由于shuffle的存在,只能在parentRDD处理完成后,才能开始接下来的计算
因为,只有第一个阶段执行完,才能继续执行第二个阶段
窄依赖是不用等的,宽依赖必须等父RDD都执行完,才可以
因此,宽依赖是划分Stage的依据
3. 任务划分
-----------------------------------------------
- RDD任务划分的几个概念: Application、Job、Stage、Task
- Application: 初始化一个SparkContext即生成一个Application
- Job: 一个Action算子就会生成一个Job
- Stage: 遇到一个宽依赖就划分成一个Stage,Stage 是一个Task的集合
- Task: 最小单位。 一个Stage到底有多少个Task, 取决于该Stage最后的RDD算子的分区数
- Application =>Job => Stage=>Task 左到右都是1对n的关系
4. RDD的缓存
-----------------------------------------------
- RDD通过persist方法或者cache方法可以将前面的计算结果缓存
- persist()会把数据以序列化的形式缓存在JVM的堆空间中
- cache(): 底层调用的就是persist
- 但是并不是调用方法就开始缓存,而是调用action的时候,才会被缓存
- 可以设定存储级别 object StorageLevel = StorageLevel.MEMORY_ONLY
1. 缓存到内存
2. 缓存到内存和磁盘
3. 缓存到磁盘
4. ...
- 缓存有可能丢失,或者存储于内存中的数据由于内存不足而被收掉
RDD的容错机制保证了即使缓存丢失,计算也能正确执行[可以通过RDD的血统,重新计算。
而且由于RDD的分区是相互独立的,因此只需要计算丢失的那部分分区即可]
5. RDD的CheckPoint
-----------------------------------------------
- RDD除了缓存之外,还有一种检查点机制
- 检查点的本质是通过将RDD写入Disk做检查点
- 如果一个RDD的血统很深,一旦发生了故障,容错恢复成本很高
因此可以在血统的中间阶段做一个检查点,如果检查点之后出现了故障
可以从检查点处直接恢复。就会减少开销。
- RDD将检查点数据写入到HDFS,路径是通过SparkContext.setCheckPointDir()进行设置
- 例:
def testCheckPoint(): Unit ={
val conf = new SparkConf().setMaster("local").setAppName("sc")
val sc = new SparkContext(conf)
sc.setCheckpointDir("D:\\Test\\checkpoint")
val rdd = sc.makeRDD(1::2::3::4::Nil)
val rdd1: RDD[(Int, Int)] = rdd.map((_,1))
val rdd2: RDD[(Int, Int)] = rdd1.reduceByKey(_ + _)
rdd2.checkpoint()
rdd2.collect()
println(rdd1.toDebugString)
println(rdd2.toDebugString)
//结果:
//(1) MapPartitionsRDD[1] at map at Day02.scala:15 []
// | ParallelCollectionRDD[0] at makeRDD at Day02.scala:14 []
//(1) ShuffledRDD[2] at reduceByKey at Day02.scala:16 []
// | ReliableCheckpointRDD[3] at collect at Day02.scala:18 []
}
6. RDD的分区
-----------------------------------------------
- Spark目前支持两种分区器,Hash分区(默认)和Range分区,也可以自己自定义分区
- 分区器决定了分区的个数,shuffle之后数据流向,以及Reduce的个数
- 只有K-V形式的RDD才有分区器。非K-V类型的RDD分区器是None
- RDD分区ID : 0 ~ numPartitions - 1
- 查看RDD分区
1. pairRDD.partitioner
- Hash分区原理:
1. 对于给定的key,计算其hashcode, 然后对分区个数取余。余数就是分区ID
2. 弊端: 可能导致每个分区中的数据量不均匀。容易引起数据倾斜
- Range分区原理:
1. 将一定范围内的数据映射到某一个分区,尽量保证每个分区数据量均匀。分区与分区之间是有序的,一个分区内的元素
肯定比另一个分区内的元素大或者小,但是分区内的元素是不能保证顺序的。简单来说,就是将一定范围内的数据映射到某一个分区内
2. 实现过程:
- 从抽取出整个RDD的样本数据,将样本按照key排序[注:前提是key得能排序]
- 然后根据key的最大值,计算出每个分区应该装的key的范围
- keymax/numPartitions = 每个分区的key的范围
- 自定义分区器
1. 继承 org.apache.spark.Partitioner类并实现下面三个方法
- numPartitions:Int 创建出来的分区数
- getPartition(key:Any):Int 通过计算key,得出key应该的分区ID
- equals() Java判断相等的标准。Spark需要用这个方法检查你的分区器对象是否和其他分区器实例相同,这样Spark才可以判断两个RDD的分区方式是否相同
2. 定义自定义分区器之后,使用 RDD.partitionBy()方法,传入自定义分区器,对RDD进行分区操作
7. 数据的读取和保存
-----------------------------------------------
- Spark支持的文件格式:
Text, Json, Csv, Sequence, Object文件
- Spark支持的文件系统:
本地文件系统,HDFS,HBASE以及数据库
- Text文件
1. sc.TextFile(path)
2. sc.saveAsTextFile(path)
3. path: 可以为本地,HDFS
- Json文件
1. 一般Json的每一行都是一条规范的记录。可以将Json文件当成文本文件读取,然后使用Json库处理读取的Json数据即可
2. 注:RDD处理Json文件很复杂,不建议直接RDD转换处理。考虑使用SparkSQL方式处理
- Sequence文件
1. Sequence文件时Hadoop用来存储二进制的key-value对,而设计的文件类型
2. Spark有专门读取其格式的接口
3. sc.sequenceFile[keyClass, valueClass](path):PairRDD
4. sc.saveAsSequenceFile(path)
5. path: 可以为本地,HDFS
6. 注:sequence只针对pairRDD
- 对象文件
1. 对象文件时将对象序列化保存的二进制文件。采用java序列化机制
2. sc.objectFile[k,v](path)
3. sc.saveAsObject()
4. 注:因为要序列化,所以要指定类型
8. 操作MySQL数据库
------------------------------------------
- Spark支持通过JDBC访问关系型数据库,需要通过jdbcRDD来实现
- 例:
package com.test.ghf.day02
import java.sql.{DriverManager, ResultSet}
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.rdd.JdbcRDD
object Day02_1 extends java.io.Serializable{
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local").setAppName("sc")
val sc = new SparkContext(conf)
// 配置mysql
def getConn() = {
Class.forName("com.mysql.jdbc.Driver").newInstance()
DriverManager.getConnection("jdbc:mysql://localhost:3306/test","root","root")
}
def extractValues(r: ResultSet) = {
(r.getInt("id"), r.getString(2),r.getInt(3))
}
// 读取Mysql数据
val value = new JdbcRDD(
sc,
getConn,
sql = "select * from student01 where id >=? and id <=? ",
lowerBound = 1,
upperBound = 1000,
numPartitions = 2,
extractValues
)
// 保存数据到mysql
// 注意优化
// 1. sql批处理
def writeToMysql(list: List[(Int, String, Int)]): Unit = {
val conn = getConn()
conn.setAutoCommit(false)
val ps = conn.prepareStatement("insert into student01 values (?,?,?)")
val batch_size = 5
for(i <- 0 until list.size){
val e = list(i)
ps.setInt(1,e._1)
ps.setString(2,e._2)
ps.setInt(3,e._3)
ps.addBatch()
if(i != 0 && i % batch_size == 0){
ps.executeBatch()
conn.commit()
ps.clearBatch()
}
}
ps.close()
conn.close()
}
// 2. rdd.foreachPartition 代替 rdd.foreach, 这样每个分区建立一次conn
value.foreachPartition(x => writeToMysql(x.toList))
}
}
- 注意:
1. 承载main函数的类,要继承java.io.Serializable,不然会报序列化的错误
2. JdbcRDD的参数说明
- getConnection: () => Connection 此处传递一个返回值是Connection函数
- sql:你的sql语句,注意要带上 <=, >= 表示上下界,不然会报数组越界的错误
- lowerBound: 是你的sql语句 >=? 代表的值
- upperBound: 是你的sql语句 <=? 代表的值
- mapRow: (ResultSet) => T = JdbcRDD.resultSetToObjectArray _
此处传递的是一个函数,处理sql返回的结果集ResultSet,返回RDD的元素,一般为Tuple
- 优化
1. sql批处理
2. rdd.foreachPartition 代替 rdd.foreach, 这样每个分区建立一次conn
但是因为一次读取一个分区的数据,容易出现OOM
- 核心
1. 读Mysql数据 -- 主要靠 new JdbcRDD()
2. 写Mysql数据 -- 就是基本的写数据库操作
9. 操作HBASE数据库
-------------------------------------------------
- 借助 org.apache.hadoop.hbase.mapreduce.TableInputFormat类,实现访问HBASE
- 返回键值对数据,
键类型 org.apache.hadoop.hbase.io.ImmuteableBytesWritable
值类型 org.apache.hadoop.hbase.client.Result
- 例:
package com.test.ghf.day02
import org.apache.hadoop.hbase.{HBaseConfiguration, HColumnDescriptor, HTableDescriptor, TableName}
import org.apache.hadoop.hbase.client.{HBaseAdmin, _}
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 Day02_2 {
def main(args: Array[String]): Unit = {
readHbase()
writeHbase()
}
// 读HBASE数据
def readHbase(): Unit ={
val conf = new SparkConf().setMaster("local").setAppName("sc")
val sc = new SparkContext(config = conf)
// 创建Hbase配置信息[需要将hbase-default.xml和 hbase-site.xml放到resource目录下]
val hbaseConf = HBaseConfiguration.create()
hbaseConf.set("hbase.zookeeper.quorum","zk01,zk02,zk03")
hbaseConf.set(TableInputFormat.INPUT_TABLE,"hbase_tablename")
// 从hbase中读取数据
val hbaseRdd: RDD[(ImmutableBytesWritable, Result)] = sc.newAPIHadoopRDD(
conf = hbaseConf,
fClass = classOf[TableInputFormat],
kClass = classOf[ImmutableBytesWritable],
vClass = classOf[Result]
)
// 从hbase数据中获取rowkey 列族和列
val value: RDD[(String, String, Int)] = hbaseRdd.map({
case (_, result) =>
val rowKey: String = Bytes.toString(result.getRow)
val name: String = Bytes.toString(
result.getValue(
Bytes.toBytes("列族name"),
Bytes.toBytes("列name1")
)
)
val age: String = Bytes.toString(
result.getValue(
Bytes.toBytes("列族name"),
Bytes.toBytes("列name2")
)
)
println(s"Rowkey is ${rowKey}, Name is ${name}, age is ${age}")
(rowKey, name, Integer.parseInt(age))
})
sc.stop()
}
//写HBASE
def writeHbase(): Unit ={
val sparkConf = new SparkConf().setMaster("local").setAppName("sc")
val sc = new SparkContext(config = sparkConf)
// 创建Hbase配置信息[需要将hbase-default.xml和 hbase-site.xml放到resource目录下]
val hbaseConf = HBaseConfiguration.create()
// 创建Hbase表
val conn = ConnectionFactory.createConnection(hbaseConf)
val hbaseAdmin:HBaseAdmin = conn.getAdmin().asInstanceOf[HBaseAdmin]
// 检查表是否存在
if(hbaseAdmin.tableExists("hbase_out_table_name")){
println(s"table already exists")
}else{
// 创建hbase表描述器
val tableName = TableName.valueOf("hbase_out_table_name")
val tableDesc = new HTableDescriptor(tableName)
// 增加列族
tableDesc.addFamily(new HColumnDescriptor("列族name1".getBytes()))
tableDesc.addFamily(new HColumnDescriptor("列族name2".getBytes()))
// 创建表
hbaseAdmin.createTable(tableDesc)
}
// 往hbase中写入数据,使用put对象
def putIntoHbase(rowkey:String, name:String, age:Int) ={
val put = new Put(Bytes.toBytes(rowkey))
put.addColumn(Bytes.toBytes("列族name1"),Bytes.toBytes("列name1"),Bytes.toBytes(name))
put.addColumn(Bytes.toBytes("列族name1"),Bytes.toBytes("列name2"),Bytes.toBytes(age))
(new ImmutableBytesWritable(Bytes.toBytes(rowkey)), put)
}
val rdd = sc.makeRDD(List(("1001", "tom1", 11),("1002", "tom2", 22),("1003", "tom3", 33)))
val hbaseRDD: RDD[(ImmutableBytesWritable, Put)] = rdd.map(x => putIntoHbase(x._1, x._2, x._3))
// 保存hbase的put对象
val jobConf = new JobConf(hbaseConf)
jobConf.setOutputFormat(classOf[TableOutputFormat])
jobConf.set(TableOutputFormat.OUTPUT_TABLE, "hbase_out_table_name")
hbaseRDD.saveAsHadoopDataset(jobConf)
}
}
- 核心:
1. 读核心: // 核心APIRDD - 使用sc.newAPIHadoopRDD()
2. 写核心:// 核心 将普通RDD转换成[ImmutableBytesWritable,Put] 格式的RDD,然后调用saveAsNewAPIHadoopDataSet方法进行保存
10. RDD 累加器 accumulator
------------------------------------------
- Spark 三大数据结构
1. RDD: 弹性分布式数据集
2. 广播变量: 分布式只读共享变量[Executor只读]
3. 累加器: 分布式只写共享变量[Executor只写]
- 系统累加器
LongAccumulator,DoubleAccumulator,CollectionAccumulator,PythonAccumulator等
- 自定义累加器代码实现
/**
* 敏感单词累加器
* @tparam String 单词
* @tparam 敏感词出现次数
*/
class WordAccumulator(var sum:Long = 0, var count:Long = 0) extends AccumulatorV2[String,Long] with java.io.Serializable{
val list = List("法轮功", "假币", "毒品")
override def isZero: Boolean = sum == 0 && count == 0
override def copy(): AccumulatorV2[String, Long] = {
new WordAccumulator(this.sum, this.count)
}
override def reset(): Unit = {
this.sum = 0L
this.count = 0L
}
override def add(v: String): Unit = {
for(i <- list){
breakable{
if(v.contains(i)) {
sum += 1
break()
}
}
}
}
override def merge(other: AccumulatorV2[String, Long]): Unit = {
this.sum += other.value
}
override def value: Long = {
this.sum
}
}
- 使用自定义累加器
/**
* 累加器
*/
@Test
def testAccumulator(): Unit ={
val conf = new SparkConf().setMaster("local").setAppName("sc")
val sc = new SparkContext(conf)
val rdd = sc.makeRDD(List("1","2","3","4","法轮功1112311","113毒品2311"),2)
val accumulator: WordAccumulator = new WordAccumulator()
sc.longAccumulator
sc.register(accumulator)
rdd.foreach(accumulator.add(_))
println(accumulator.value)
}
11. 广播变量 Broadcast(调优策略)
---------------------------------------------------
1. 可以将一些小数据集的数据,广播出去,给每个Executor可以直接使用
2. 一般join会进行笛卡尔积,这个时候,将其中的一个小的RDD的数据广播出去,再操作,就避免了join
3. 广播变量用来高效分发较大的对象,向所有工作节点发送一个只读的值,并提供给一个或者多个Spark使用
4. 比如,你的应用要向所有的节点发送一个较大的只读查询表或者是机器学习算法中很大的特征向量
5. 代码实现
/**
* 广播变量
*/
@Test
def broadcast(): Unit ={
val conf = new SparkConf().setMaster("local").setAppName("sc")
val sc = new SparkContext(conf)
// 假如rdd1 很小,但是rdd2很大
val rdd1 = sc.makeRDD(List((1,3),(1,4),(2,4),(3,8),(11,8)),2)
val rdd2 = sc.makeRDD(List((1,"1"),(1,"11"),(1,"11"),(1,"11"),(1,"11"),(1,"11"),(1,"11"),(2,"2"),(3,"3")),4)
//
val list = rdd1.collect().toList
val cast: Broadcast[List[(Int, Int)]] = sc.broadcast(list)
rdd2.flatMap({
case (key, value) =>
val v2 = ListBuffer[(Int,(String,Int))]()
for(t <- cast.value){
if(key == t._1){
v2.+=((key,(value, t._2)))
}
}
v2
}).collect().foreach(println)
}