Spark核心之03写mysql、写HBase、RDD宽窄依赖、DAG、缓存、Checkpoint

spark内存计算框架

一、主题

  1. RDD的算子操作案例

  2. RDD弹性分布式数据集的依赖关系

  3. RDD弹性分布式数据集的lineage血统机制

  4. RDD弹性分布式数据集的缓存机制

  5. spark任务的DAG有向无环图的构建

  6. spark任务如何划分stage

二、要点

1. 通过spark实现点击流日志分析案例

1.1 统计PV
  • 代码开发 (count())
scala 复制代码
package com.kaikeba.rdd

import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

//TODO:利用spark实现点击流日志分析---------> PV
object PV {
  def main(args: Array[String]): Unit = {
    //1、构建SparkConf
    val sparkConf: SparkConf = new SparkConf().setAppName("PV").setMaster("local[2]")

    //2、构建SparkContext   SparkContext该对象非常重要
      //它是所有spark程序的执行入口,它会构建DAGScheduler和TaskScheduler对象
    val sc = new SparkContext(sparkConf)
    sc.setLogLevel("warn")

    //3、读取数据文件
    val data: RDD[String] = sc.textFile("E:\\data\\access.log")

    //4、统计pv
    val pv: Long = data.count()
    println("PV:"+pv)


    sc.stop()

  }
}
⭐️1.2 统计UV
  • 代码开发 (distinct()+count())
scala 复制代码
package com.kaikeba.rdd

import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.rdd.RDD

//TODO:利用spark实现点击流日志分析-----------UV
object UV {

  def main(args: Array[String]): Unit = {
    //1、构建SparkConf
    val sparkConf: SparkConf = new SparkConf().setAppName("UV").setMaster("local[2]")

    //2、构建SparkContext
    val sc = new SparkContext(sparkConf)
    sc.setLogLevel("warn")

    //3、读取数据文件
    val data: RDD[String] = sc.textFile("E:\\data\\access.log")


    //4、切分每一行,获取第一个元素 也就是ip
    val ips: RDD[String] = data.map( x =>x.split(" ")(0))

    //5、按照ip去重
    val distinctRDD: RDD[String] = ips.distinct()

    //6、统计uv
    val uv: Long = distinctRDD.count()
    println("UV:"+uv)


    sc.stop()

  }
}
1.3 统计TopN
  • 代码开发 (reduceByKey(url)+sortBy(url_times)+take(N))
scala 复制代码
package com.kaikeba.rdd

import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.rdd.RDD

//todo:利用spark实现点击流日志分析-----------TopN(求页面访问次数最多的前N位)
object TopN {
  def main(args: Array[String]): Unit = {
    //1、构建SparkConf
    val sparkConf: SparkConf = new SparkConf().setAppName("TopN").setMaster("local[2]")

    //2、构建SparkContext
    val sc = new SparkContext(sparkConf)
    sc.setLogLevel("warn")

    //3、读取数据文件
    val data: RDD[String] = sc.textFile("E:\\data\\access.log")

    //4、切分每一行,过滤出丢失的字段数据,获取页面地址
    val filterRDD: RDD[String] = data.filter(x=>x.split(" ").length>10)
    val urlAndOne: RDD[(String, Int)] = filterRDD.map(x=>x.split(" ")(10)).map((_,1))

    //5、相同url出现的1累加
    val result: RDD[(String, Int)] = urlAndOne.reduceByKey(_+_)

    //6、按照次数降序
    val sortedRDD: RDD[(String, Int)] = result.sortBy(_._2,false)


    //7、取出url出现次数最多的前5位
    val top5: Array[(String, Int)] = sortedRDD.take(5)
    top5.foreach(println)

    sc.stop()

  }

}

2. 通过spark读取文件数据写入mysql表中

  • 添加pom依赖
xml 复制代码
<dependency>
     <groupId>mysql</groupId>
     <artifactId>mysql-connector-java</artifactId>
     <version>5.1.38</version>
</dependency>
2.1 foreach算子实现
  • 代码开发

    每条数据与mysql建立连接,把数据插入到mysql表操作

scala 复制代码
package com.kaikeba.rdd
import java.sql.{Connection, DriverManager, PreparedStatement}

import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

object Data2MysqlForeach {
  def main(args: Array[String]): Unit = {
    //1、构建SparkConf
    val sparkConf: SparkConf = new SparkConf().setAppName("Data2MysqlForeach").setMaster("local[2]")

    //2、构建SparkContext
    val sc = new SparkContext(sparkConf)
    sc.setLogLevel("warn")

    //3、读取数据文件
    val data: RDD[String] = sc.textFile("E:\\data\\person.txt")

    //4、切分每一行    // id  name  age
    val personRDD: RDD[(String, String, Int)] = data.map(x => x.split(",")).map(x => (x(0), x(1), x(2).toInt))

    //5、把数据保存到mysql表中
        personRDD.foreach(line =>{
          //每条数据与mysql建立连接
          //把数据插入到mysql表操作
            //1、获取连接
              val connection: Connection = DriverManager.getConnection("jdbc:mysql://node03:3306/spark","root","123456")

            //2、定义插入数据的sql语句
              val sql="insert into person(id,name,age) values(?,?,?)"

            //3、获取PreParedStatement

             try {
               val ps: PreparedStatement = connection.prepareStatement(sql)

               //4、获取数据,给?号 赋值
               ps.setString(1, line._1)
               ps.setString(2, line._2)
               ps.setInt(3, line._3)
 				//执行
               ps.execute()
             } catch {
               case e:Exception => e.printStackTrace()
             } finally {
               if(connection !=null){
                 connection.close()
               }

             }
        })

    }
 }
2.2 foreachPartition 算子实现
  • 代码开发

    使用foreachPartition每个分区建立一次链接,减少与mysql链接次数

scala 复制代码
package com.kaikeba.rdd

import java.sql.{Connection, DriverManager, PreparedStatement}

import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

object Data2MysqlForeachPartitions {
  def main(args: Array[String]): Unit = {
    //1、构建SparkConf
    val sparkConf: SparkConf = new SparkConf().setAppName("Data2MysqlForeachPartitions").setMaster("local[2]")

    //2、构建SparkContext
    val sc = new SparkContext(sparkConf)
    sc.setLogLevel("warn")

    //3、读取数据文件
    val data: RDD[String] = sc.textFile("E:\\data\\person.txt")

    //4、切分每一行    // id  name  age
    val personRDD: RDD[(String, String, Int)] = data.map(x => x.split(",")).map(x => (x(0), x(1), x(2).toInt))

    //5、把数据保存到mysql表中
    //使用foreachPartition每个分区建立一次链接,减少与mysql链接次数
    personRDD.foreachPartition( iter =>{
      //把数据插入到mysql表操作
      //1、获取连接
      val connection: Connection = DriverManager.getConnection("jdbc:mysql://node03:3306/spark","root","123456")

      //2、定义插入数据的sql语句
      val sql="insert into person(id,name,age) values(?,?,?)"

      //3、获取PreParedStatement

      try {
        val ps: PreparedStatement = connection.prepareStatement(sql)

        //4、获取数据,给?号 赋值
        iter.foreach(line =>{

          ps.setString(1, line._1)
          ps.setString(2, line._2)
          ps.setInt(3, line._3)
         //设置批量提交
          ps.addBatch()
        })
		//执行批量提交
        ps.executeBatch()
      } catch {
        case e:Exception => e.printStackTrace()
      } finally {
        if(connection !=null){
          connection.close()
        }

      }
    }

  }
}

3. 通过spark读取文件数据写入hbase表中

  • 添加pom依赖
xml 复制代码
        <dependency>
            <groupId>org.apache.hbase</groupId>
            <artifactId>hbase-client</artifactId>
            <version>1.2.1</version>
        </dependency>
  • 代码开发
scala 复制代码
package com.kaikeba.rdd

import java.util

import org.apache.hadoop.conf.Configuration
import org.apache.hadoop.hbase.{HBaseConfiguration, TableName}
import org.apache.hadoop.hbase.client.{Connection, ConnectionFactory, Put, Table}
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

//todo:通过spark读取数据文件,把结果数据保存到hbase表中
object Data2Hbase {

  def main(args: Array[String]): Unit = {
    //1、创建SparkConf
    val sparkConf: SparkConf = new SparkConf().setAppName("Data2Hbase").setMaster("local[2]")

    //2、构建SparkContext
    val sc = new SparkContext(sparkConf)
    sc.setLogLevel("warn")

    //3、读取文件数据
    val usersRDD: RDD[Array[String]] = sc.textFile("E:\\data\\users.dat").map(x=>x.split("::"))

    //4、保存结果数据到hbase表中
    usersRDD.foreachPartition(iter =>{

      //4.1 获取hbase的数据库连接
      val configuration: Configuration = HBaseConfiguration.create()
      //指定zk集群的地址
      configuration.set("hbase.zookeeper.quorum","node01:2181,node02:2181,node03:2181")
      val connection: Connection = ConnectionFactory.createConnection(configuration)

      //4.2 对于hbase表进行操作这里需要一个Table对象
      val table: Table = connection.getTable(TableName.valueOf("person"))

      //4.3 把数据保存在表中
      try {
        iter.foreach(x => {
          val put = new Put(x(0).getBytes)
          val puts = new util.ArrayList[Put]()
          //构建数据
          val put1: Put = put.addColumn("f1".getBytes, "gender".getBytes, x(1).getBytes)
          val put2: Put = put.addColumn("f1".getBytes, "age".getBytes, x(2).getBytes)
          val put3: Put = put.addColumn("f2".getBytes, "position".getBytes, x(3).getBytes)
          val put4: Put = put.addColumn("f2".getBytes, "code".getBytes, x(4).getBytes)

          puts.add(put1)
          puts.add(put2)
          puts.add(put3)
          puts.add(put4)

          //提交数据
          table.put(puts)
        })
      } catch {
        case e:Exception =>e.printStackTrace()
      } finally {
        if(connection !=null){
          connection.close()
        }
      }

    })

    sc.stop()
  }
}

⭐️4. RDD的(宽、窄)依赖关系

  • RDD和它依赖的父RDD的关系有两种不同的类型

  • 窄依赖(narrow dependency)和宽依赖(wide dependency)

    • 窄依赖

      • 窄依赖指的是每一个父RDD的Partition最多被子RDD的一个Partition使用(一个子rdd以有多个父rdd,但父rdd只有一个子rdd)

        • 总结:窄依赖我们形象的比喻为独生子女

          哪些算子操作是窄依赖:
          map/flatMap/filter/union等等

          所有的窄依赖不会产生shuffle
          
    • 宽依赖所有的宽依赖会产生shuffle

      • 宽依赖指的是多个子RDD的Partition会依赖同一个父RDD的Partition(多对多)

        • 总结:宽依赖我们形象的比喻为超生

          哪些算子操作是宽依赖:
          reduceByKey/sortByKey/groupBy/groupByKey/join等等

          所有的宽依赖会产生shuffle
          
    • 补充说明

      由上图可知,join分为宽依赖和窄依赖,如果RDD有相同的partitioner,那么将不会引起shuffle,这种join是窄依赖,反之就是宽依赖
      

⭐️5. lineage(血统)

  • RDD只支持粗粒度转换
    • 即只记录单个块上执行的单个操作。
  • 将创建RDD的一系列Lineage(即血统)记录下来,以便恢复丢失的分区
  • RDD的Lineage会记录RDD的元数据信息和转换行为,lineage保存了RDD的依赖关系,当该RDD的部分分区数据丢失时,它可以根据这些信息来重新运算和恢复丢失的数据分区。

⭐️6. RDD的缓存机制(★★★★★)

6.1 什么是rdd的缓存
	可以把一个rdd的数据缓存起来,后续有其他的job需要用到该rdd的结果数据,可以直接从缓存中获取得到,避免了重复计算。缓存是加快后续对该数据的访问操作。
6.2 如何对rdd设置缓存
  • RDD通过**persist方法或cache方法**可以将前面的计算结果缓存。
    • 但是并不是这两个方法被调用时立即缓存,而是触发后面的action时,该RDD将会被缓存在计算节点的内存中,并供后面重用。
  • 通过查看源码发现cache最终也是调用了persist方法,默认的存储级别都是仅在内存存储一份,Spark的存储级别还有好多种,存储级别在object StorageLevel中定义的。
  • 使用演示
scala 复制代码
val rdd1=sc.textFile("/words.txt")
val rdd2=rdd1.flatMap(_.split(" "))
val rdd3=rdd2.cache
rdd3.collect

val rdd4=rdd3.map((_,1))
val rdd5=rdd4.persist(缓存级别)
rdd5.collect
⭐️6.3 cache和persist区别
  • 面试经常被问到

    • 例如
      • 简述下如何对RDD设置缓存,以及它们的区别是什么?

        对RDD设置缓存成可以调用rdd的2个方法: 一个是cache,一个是persist
        调用上面2个方法都可以对rdd的数据设置缓存,但不是立即就触发缓存执行,后面需要有action,才会触发缓存的执行。

        cache方法和persist方法区别:
        cache: 默认是把数据缓存在内存中,其本质就是调用persist方法;
        persist:可以把数据缓存在内存或者是磁盘,有丰富的缓存级别,这些缓存级别都被定义在StorageLevel这个object中。

6.4 什么时候设置缓存
  • 1、某个rdd的数据后期被使用了多次
如上图所示的计算逻辑: 
(1)当第一次使用rdd2做相应的算子操作得到rdd3的时候,就会从rdd1开始计算,先读取HDFS上的文件,然后对rdd1 做对应的算子操作得到rdd2,再由rdd2计算之后得到rdd3。同样为了计算得到rdd4,前面的逻辑会被重新计算。

(2)默认情况下多次对一个rdd执行算子操作, rdd都会对这个rdd及之前的父rdd全部重新计算一次。 这种情况在实际开发代码的时候会经常遇到,但是我们一定要避免一个rdd重复计算多次,否则会导致性能急剧降低。   

总结:
可以把多次使用到的rdd,也就是公共rdd进行持久化,避免后续需要,再次重新计算,提升效率。
  • 2、为了获取得到一个rdd的结果数据,经过了大量的算子操作或者是计算逻辑比较复杂
    • 总之某个rdd的数据来之不易

      val rdd2=rdd1.flatMap(函数).map(函数).reduceByKey(函数).xxx.xxx.xxx.xxx.xxx

6.5 清除缓存数据
  • 1、自动清除

    一个application应用程序结束之后,对应的缓存数据也就自动清除
    
  • 2、手动清除

    调用rdd的unpersist方法
    

⭐️7. RDD的checkpoint机制(★★★★★)

7.1 checkpoint概念
  • 我们可以对rdd的数据进行缓存,保存在内存或者是磁盘中。

    • 后续就可以直接从内存或者磁盘中获取得到,但是它们不是特别安全。

    • cache

      它是直接把数据保存在内存中,后续操作起来速度比较快,直接从内存中获取得到。但这种方式很不安全,由于服务器挂掉或者是进程终止,会导致数据的丢失。
      
    • persist

      它可以把数据保存在本地磁盘中,后续可以从磁盘中获取得到该数据,但它也不是特别安全,由于系统管理员一些误操作删除了,或者是磁盘损坏,也有可能导致数据的丢失。
      
  • checkpoint(检查点)把数据保存在分布式文件系统

    它是提供了一种相对而言更加可靠的数据持久化方式。它是把数据保存在分布式文件系统,
    比如HDFS上。这里就是利用了HDFS高可用性,高容错性(多副本)来最大程度保证数据的安全性。
    
7.2 如何设置checkpoint
  • 1、在hdfs上设置一个checkpoint目录

    scala 复制代码
    sc.setCheckpointDir("hdfs://node01:8020/checkpoint") 
  • 2、对需要做checkpoint操作的rdd调用checkpoint方法

    scala 复制代码
    val rdd1=sc.textFile("/words.txt")
    rdd1.checkpoint
    val rdd2=rdd1.flatMap(_.split(" ")) 
  • 3、最后需要有一个action操作去触发任务的运行

    scala 复制代码
    rdd2.collect
7.3 cache、persist、checkpoint三者区别
  • cache和persist

    • cache默认数据缓存在内存中
    • persist可以把数据保存在内存或者磁盘中
    • 后续要触发 cache 和 persist 持久化操作,需要有一个action操作
    • 它不会开启其他新的任务,一个action操作就对应一个job
    • 它不会改变rdd的依赖关系,程序运行完成后对应的缓存数据就自动消失
  • checkpoint

    • 可以把数据持久化写入到hdfs上
    • 后续要触发checkpoint持久化操作,需要有一个action操作,后续会开启新的job执行checkpoint操作
    • 它会改变rdd的依赖关系,后续数据丢失了不能够在通过血统进行数据的恢复。
    • 程序运行完成后对应的checkpoint数据就不会消失
scala 复制代码
   sc.setCheckpointDir("/checkpoint")
   val rdd1=sc.textFile("/words.txt")
   val rdd2=rdd1.cache
   rdd2.checkpoint
   val rdd3=rdd2.flatMap(_.split(" "))
   rdd3.collect
   
   checkpoint操作要执行需要有一个action操作,一个action操作对应后续的一个job。该job执行完成之后,它会再次单独开启另外一个job来执行 rdd1.checkpoint操作。
   
   对checkpoint在使用的时候进行优化,在调用checkpoint操作之前,可以先来做一个cache操作,缓存对应rdd的结果数据,后续就可以直接从cache中获取到rdd的数据写入到指定checkpoint目录中

对checkpoint在使用的时候进行优化在调用checkpoint操作之前,可以先来做一个cache操作,缓存对应rdd的结果数据,后续就可以直接从cache中获取到rdd的数据写入到指定checkpoint目录中

8. DAG有向无环图生成

8.1 DAG是什么
  • DAG(Directed Acyclic Graph) 叫做有向无环图(有方向,无闭环,代表着数据的流向),原始的RDD通过一系列的转换就形成了DAG。

  • 下图是基于单词统计逻辑得到的DAG有向无环图

⭐️9. DAG划分stage(★★★★★)

9.1 stage是什么
  • 一个Job会被拆分为多组Task,每组任务被称为一个stage
  • stage表示不同的调度阶段,一个spark job会对应产生很多个stage
    • stage类型一共有2种
      • ShuffleMapStage
        • 最后一个shuffle之前的所有变换的Stage叫ShuffleMapStage
          • 它对应的task是shuffleMapTask
      • ResultStage
        • 最后一个shuffle之后操作的Stage叫ResultStage,它是最后一个Stage。
          • 它对应的task是ResultTask
⭐️9.2 为什么要划分stage
根据RDD之间依赖关系的不同将DAG划分成不同的Stage(调度阶段)
对于窄依赖,partition的转换处理在一个Stage中完成计算
对于宽依赖,由于有Shuffle的存在,只能在parent RDD处理完成后,才能开始接下来的计算,

由于划分完stage之后,在同一个stage中只有窄依赖,没有宽依赖,可以实现流水线计算,
stage中的每一个分区对应一个task,在同一个stage中就有很多可以并行运行的task。

划分完stage之后,在同一个stage中只有窄依赖,没有宽依赖,可以实现流水线计算,
stage中的每一个分区对应一个task,在同一个stage中就有很多可以并行运行的task。

9.3 如何划分stage
  • 划分stage的依据就是宽依赖从最后一个rdd往前推

    (1) 首先根据rdd的算子操作顺序生成DAG有向无环图,接下里从最后一个rdd往前推,创建一个新的stage,把该rdd加入到该stage中,它是最后一个stage。

    (2) 在往前推的过程中运行遇到了窄依赖就把该rdd加入到本stage中,如果遇到了宽依赖,就从宽依赖切开,那么最后一个stage也就结束了。

    (3) 重新创建一个新的stage,按照第二个步骤继续往前推,一直到最开始的rdd,整个划分stage也就结束了

9.4 stage与stage之间的关系

划分完stage之后,每一个stage中有很多可以并行运行的task,后期把每一个stage中的task封装在一个taskSet集合中,最后把一个一个的taskSet集合提交到worker节点上的executor进程中运行。
rdd与rdd之间存在依赖关系,stage与stage之前也存在依赖关系,前面stage中的task先运行,运行完成了再运行后面stage中的task,也就是说后面stage中的task输入数据是前面stage中task的输出结果数据。

三、总结

  • 1、rdd的算子操作案例(★★★★★)
    • 重点是掌握算子的使用
      • flatMap、map、count、distinct 、reduceByKey、sortBy、foreach、foreachParitition
  • 2、rdd的依赖关系
    • 宽依赖
    • 窄依赖
  • 3、rdd的缓存机制(★★★★★)
    • cache
    • persist
  • 4、checkpoint机制(★★★★★)
  • 5、DAG有向无环图是什么
  • 6、如何划分stage(★★★★★)
相关推荐
Pandaconda26 分钟前
【后端开发面试题】每日 3 题(五)
javascript·数据库·mysql·golang·node.js·go·协程
weixin_307779131 小时前
PySpark实现获取S3上Parquet文件的数据结构,并自动在Amazon Redshift里建表和生成对应的建表和导入数据的SQL
数据仓库·python·spark·云计算·aws
weixin_307779132 小时前
PySpark实现获取S3上Parquet文件的数据结构,并自动在Snowflake里建表和生成对应的建表和导入数据的SQL
python·sql·spark·aws
溟洵4 小时前
Linux下学【MySQL】表的连接(inner join、left join、right join)(简单试题理解版)
linux·运维·mysql
冰火同学4 小时前
简述Spark的宽窄依赖以及Stage是怎么划分的以及每个stage又是怎么划分task任务数
大数据·分布式·spark
weixin_307779134 小时前
PySpark实现获取Cloud Storage上Parquet文件的数据结构,并自动在Google BigQuery里建表和生成对应的建表和导入数据的SQL
数据仓库·python·spark·云计算·googlecloud
Watink Cpper4 小时前
[MySQL初阶]MySQL(1)MySQL的理解、库的操作、表的操作
linux·运维·服务器·数据库·c++·后端·mysql
尘世壹俗人4 小时前
spark写数据库用连接池找不到driver类
大数据·数据库·spark
红队it6 小时前
【Spark+Hive】基于Spark大数据技术小红书舆情分析可视化预测系统(完整系统源码+数据库+开发笔记+详细部署教程+虚拟机分布式启动教程)✅
大数据·hive·分布式·spark
007php0077 小时前
企微审批中MySQL字段TEXT类型被截断的排查与修复实践
大数据·开发语言·数据库·后端·mysql·重构·golang