Spark 快速入门(2)

了解 Spark

Hadoop 两大重要组件 HDFS(存储) + MapperReduce(计算)。MapperReduce 的操作基于文件存储介质,迭代运算太慢。在 Hadoop 的1.x 版本,MapperReduce 耦合在 Hadoop 中,到了 Hadoop 的 2.x 版,引入 Yarn(资源调度框架),使得 MapperReduce 组件可插拔。Spark 计算是基于内存的,天然适合迭代计算,可替代 Hadoop 的 MapperReduce 组件。

在这里插入图片描述
绿色代表资源(若用 Yarn,就不用关心 Master 和 Worker 了,Yarn 完成资源调度),蓝色代表计算,红色代表把资源和计算解耦。

Spark 的 Driver 是执行开发程序中的 main 方法的进程。它负责开发人员编写的用来创建 SparkContext、创建 RDD,以及进行 RDD 的转化操作和行动操作代码的执行。如果 Driver 程序终止,那么 Spark 应用也就结束了。Driver 主要负责把用户程序转为作业(JOB)、跟踪 Executor 的运行状况、为执行器节点调度任务和UI展示应用运行状况等。

Spark 的 Executor 是一个工作进程,负责在 Spark 作业中运行任务,任务间相互独立。Spark 应用启动时,Executor 节点被同时启动,并且始终伴随着整个 Spark 应用的生命周期存在。如果有 Executor 节点发生了故障或崩溃,Spark 应用也可以继续执行,会将出错节点上的任务调度到其他 Executor 节点上继续运行。Executor 主要负责,运行组成 Spark 应用的任务并将结果返回给 Driver 进程;通过自身的块管理器(Block Manager)为用户程序中要求缓存的 RDD 提供内存式存储。RDD是直接缓存在 Executor 进程内的,因此任务可以在运行时充分利用缓存数据加速运算。

图片来自官网
在这里插入图片描述

安装

下载 Spark,解压即可用(先安装 JDK 和 Scala 环境),本文在 Windows 环境下,采用 Loal 模式(其他还有standalone 模式和 spark on yarn 模式等)。

示例(Pi)

在 Spark 的 bin 目录下执行下面命令,计算 100 次,求 Pi 的值。
在这里插入图片描述

spark-submit --class org.apache.spark.examples.SparkPi --executor-memory 1G --total-executor-cores 2 ../examples/jars/spark-examples_2.11-2.4.6.jar 100

在这里插入图片描述

1)基本语法
bin/spark-submit \
--class <main-class>
--master <master-url> \
--deploy-mode <deploy-mode> \
--conf <key>=<value> \
... # other options
<application-jar> \
[application-arguments]2)参数说明:
--master 指定Master的地址,默认为Local
--class: 你的应用的启动类 (如 org.apache.spark.examples.SparkPi)
--deploy-mode: 是否发布你的驱动到worker节点(cluster) 或者作为一个本地客户端 (client) (default: client)*
--conf: 任意的Spark配置属性, 格式key=value. 如果值包含空格,可以加引号“key=value” 
application-jar: 打包好的应用jar,包含依赖. 这个URL在集群中全局可见。 比如hdfs:// 共享存储系统, 如果是 file:// path, 那么所有的节点的path都包含同样的jar
application-arguments: 传给main()方法的参数
--executor-memory 1G 指定每个executor可用内存为1G
--total-executor-cores 2 指定每个executor使用的cup核数为2

示例(WordCount)

启动 spark-shell(可在 http://localhost:4040/ 访问界面)
在这里插入图片描述
在 spark 解压包目录下准备 input 文件夹,里面存放两个文件如下,
在这里插入图片描述
在这里插入图片描述

scala> val rdd = sc.textFile("../input/*.txt")
rdd: org.apache.spark.rdd.RDD[String] = ../input/*.txt MapPartitionsRDD[14] at textFile at <console>:24

scala> rdd.flatMap(_.split(" ")).map((_, 1)).reduceByKey(_+_).collect
res7: Array[(String, Int)] = Array((Hello,4), (World,1), (Scala,1), (Spark,2))

在这里插入图片描述
在这里插入图片描述

示例(WordCount IDEA 开发)

创建一个 maven 项目,pom.xml 文件如下,

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>spark-00</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-core_2.11</artifactId>
            <version>2.4.6</version>
        </dependency>
    </dependencies>

    <build>
        <finalName>WordCount</finalName>
        <plugins>
            <plugin>
                <groupId>net.alchim31.maven</groupId>
                <artifactId>scala-maven-plugin</artifactId>
                <version>3.2.2</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>compile</goal>
                            <goal>testCompile</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

在这里插入图片描述
在 main 下创建 scala 目录,并标记为源代码目录,
在这里插入图片描述
项目上右键,添加 Scala 支持,笔者本地版本是 scala-2.11.12,注意和 maven 中的保持一致,
在这里插入图片描述
准备数据并书写代码,
在这里插入图片描述

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

object WordCount {
    
    
  def main(args: Array[String]): Unit = {
    
    
    val config: SparkConf = new SparkConf()
                              .setMaster("local[*]")	// 可指定固定 Worker 数,否则设定为 CPU 核个数
                              .setAppName("WordCount")
                              .set("spark.testing.memory","471859200")  // 根据报错设置要求的最小内存
    val sc = new SparkContext(config)
    val lines: RDD[String] = sc.textFile("input")
    val words: RDD[String] = lines.flatMap(_.split(" "));
    val wordToOne: RDD[(String, Int)] = words.map((_, 1))
    val wordToSum: RDD[(String, Int)] = wordToOne.reduceByKey(_+_)
    val result: Array[(String, Int)] = wordToSum.collect()

    result.foreach(println)
  }
}

在这里插入图片描述

Spark分布式计算原理模拟

在这里插入图片描述

package org.example.principle

class Task extends Serializable {
    
    
  val datas = List(1,2,3,4)

//  val logic = ( num:Int )=>{ num * 2 }
  val logic : (Int)=>Int = _ * 2
}

package org.example.principle

class SubTask extends Serializable {
    
    
  var datas : List[Int] = _
  var logic : (Int)=>Int = _

  // 计算
  def compute() = {
    
    
    datas.map(logic)
  }
}

package org.example.principle

import java.io.{
    
    ObjectOutputStream, OutputStream}
import java.net.Socket

object Driver {
    
    

  def main(args: Array[String]): Unit = {
    
    
    // 连接服务器
    val client1 = new Socket("localhost", 9999)
    val client2 = new Socket("localhost", 8888)

    val task = new Task()

    val out1: OutputStream = client1.getOutputStream
    val objOut1 = new ObjectOutputStream(out1)

    val subTask1 = new SubTask()
    subTask1.logic = task.logic
    subTask1.datas = task.datas.take(2)

    objOut1.writeObject(subTask1)
    objOut1.flush()
    objOut1.close()
    client1.close()

    val out2: OutputStream = client2.getOutputStream
    val objOut2 = new ObjectOutputStream(out2)

    val subTask2 = new SubTask()
    subTask2.logic = task.logic
    subTask2.datas = task.datas.takeRight(2)
    objOut2.writeObject(subTask2)
    objOut2.flush()
    objOut2.close()
    client2.close()
    println("客户端数据发送完毕")
  }
}


package org.example.principle

import java.io.{
    
    InputStream, ObjectInputStream}
import java.net.{
    
    ServerSocket, Socket}

object Executor1 {
    
    

  def main(args: Array[String]): Unit = {
    
    

    // 启动服务器,接收数据
    val server = new ServerSocket(9999)
    println("服务器启动,等待接收数据")

    // 等待客户端的连接
    val client: Socket = server.accept()
    val in: InputStream = client.getInputStream
    val objIn = new ObjectInputStream(in)
    val task: SubTask = objIn.readObject().asInstanceOf[SubTask]
    val ints: List[Int] = task.compute()
    println("计算节点[9999]计算的结果为:" + ints)
    objIn.close()
    client.close()
    server.close()
  }
}


package org.example.principle

import java.io.{
    
    InputStream, ObjectInputStream}
import java.net.{
    
    ServerSocket, Socket}

object Executor2 {
    
    

  def main(args: Array[String]): Unit = {
    
    

    // 启动服务器,接收数据
    val server = new ServerSocket(8888)
    println("服务器启动,等待接收数据")

    // 等待客户端的连接
    val client: Socket = server.accept()
    val in: InputStream = client.getInputStream
    val objIn = new ObjectInputStream(in)
    val task: SubTask = objIn.readObject().asInstanceOf[SubTask]
    val ints: List[Int] = task.compute()
    println("计算节点[8888]计算的结果为:" + ints)
    objIn.close()
    client.close()
    server.close()
  }
}


RDD

RDD(Resilient Distributed Dataset)叫做弹性分布式数据集(只读),是Spark中最基本的数据(计算)抽象。代码中是一个抽象类,它代表一个不可变、可分区、里面的元素可并行计算的集合。

来自官方文档
在这里插入图片描述

  • RDD 任务切分为,Application->Job->Stage-> Task每一层都是 1 对 n 的关系。

RDD 的创建

package org.example.chapter02

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

object RDD_01_CreateRDD {
    
    
  def main(args: Array[String]): Unit = {
    
    
    val config: SparkConf = new SparkConf()
      .setMaster("local[*]")
      .setAppName("WordCount")
      .set("spark.testing.memory","471859200")  // 根据报错设置要求的最小内存
    val sc = new SparkContext(config)

    // 1. 从内存中创建,makeRDD(makeRDD调用了 parallelize)
    val rdd1 = sc.makeRDD(Array(1, 2, 3, 4))
    rdd1.foreach(println)	// 顺序不一定是 1 2 3 4

    // 2. 从内存中创建,parallelize(可指定最小分区数)
    //    跟踪 parallelize 查看默认分区数是 max(totalCoreCount, 2)
    val rdd2 = sc.parallelize(List(1, 2, 3, 4))
    rdd2.foreach(println)	// 顺序不一定是 1 2 3 4

    // 3. 从外部文件创建,普通文件/HDFS文件路径(可指定最小分区数)
    //    此种方式默认读取的认为是字符串类型【按行读取】
    //    跟踪 textFile 查看默认【最小】分区数是 min(totalCoreCount, 2)
    val rdd3 = sc.textFile("input/1.txt")
    rdd3.foreach(println)

    // 本例使用 local[*],本机 CPU 共 8 核
    rdd1.saveAsTextFile("output1")  // 8 个分区
    rdd3.saveAsTextFile("output2")  // 最少 2 个分区
  }
}

在这里插入图片描述

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

object Spark_01 {
    
    
  def main(args: Array[String]): Unit = {
    
    
    val config: SparkConf = new SparkConf()
      .setMaster("local[*]")
      .setAppName("WordCount")
      .set("spark.testing.memory","471859200")  // 根据报错设置要求的最小内存
    val sc = new SparkContext(config)

    val listRdd: RDD[Int] = sc.makeRDD(1 to 5)
    val mapRdd: RDD[Int] = listRdd.map(_*2) // 2 4 6 8 10
    mapRdd.collect().foreach(println)

    val listRdd2: RDD[List[Int]] = sc.makeRDD(Array(List(1,2), List(3,4)))
    val flatMapRdd: RDD[Int] = listRdd2.flatMap(datas => datas)
    flatMapRdd.collect().foreach(println)

    val listRdd3: RDD[Int] = sc.makeRDD(List(1,2,3,4,5,6))
    val groupByRdd: RDD[(Int, Iterable[Int])] = listRdd3.groupBy(i => i % 2)
    groupByRdd.collect().foreach(println)

    val filterRdd: RDD[Int] = listRdd3.filter(x => x % 2 == 0)
    filterRdd.collect().foreach(println)

    val listRdd4: RDD[Int] = sc.makeRDD(List(1,1,1,2,2,3))
    listRdd4.distinct().collect().foreach(println)

    listRdd4.sortBy(x => x,false).collect().foreach(println)  // 降序
  }
}

RDD 算子 map

RDD 算子其实就是一些操作,分转换算子和行动算子。跟踪源码,经过每个算子,把原来的 RDD 包一层形成新 RDD(装饰器模式)。

注意,下面代码就是 Driver,真正执行计算的是 Executor,下面代码中 map(*2) 就是计算部分,会由 Driver 发给 Executor 去执行。如果在 Driver 中声明一个变量 i,并 map(*i),这就需要 Driver 发送 i 到 Executor,要注意只有 i 能序列化才可以经过网络传输哦。

package org.example.chapter02

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

object RDD_02_map {
    
    
  def main(args: Array[String]): Unit = {
    
    
    val config: SparkConf = new SparkConf()
      .setMaster("local[*]")
      .setAppName("WordCount")
      .set("spark.testing.memory","471859200")  // 根据报错设置要求的最小内存
    val sc = new SparkContext(config)
    // 查询类型 Ctrl + Q
    // 选中点击左边灯泡显示类型 File-Settings-Editor-Code Style-Scala-Type Annotations-Local definition
    val seqRdd1: RDD[Int] = sc.parallelize(1 to 4)
    val seqRdd2: RDD[Int] = seqRdd1.map(_*2)  // (x => x*2)
    val array: Array[Int] = seqRdd2.collect
    array.foreach(println)
  }
}

RDD 算子 mapPartitions

类似 map,但 map 是依次对每一条数据处理(一条条地),而 mapPartitions 是依次对每个分区进行数据处理(一个分区一个分区地)。

package org.example.chapter02

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

object RDD_03_mapPartitions {
    
    
  def main(args: Array[String]): Unit = {
    
    
    val config: SparkConf = new SparkConf()
      .setMaster("local[*]")
      .setAppName("WordCount")
      .set("spark.testing.memory","471859200")  // 根据报错设置要求的最小内存
    val sc = new SparkContext(config)

    // 自动生成临时变量 Ctrl + Alt + v
    val listRDD: RDD[Int] = sc.parallelize(1 to 4)
    val mapPartitionsRDD: RDD[Int] = listRDD.mapPartitions(datas => {
    
    
      // mapPartitions 依次处理一个分区,datas 是一个 Iterator[Int]
      // datas.map(_*2) 这个计算是由 Driver 发给 Executor,由 Executor 完成计算的,
      // 一个分区一个分区地发给不同的 Executor,与 map 一条一条发给 Executor 对比,减少
      // 发给 Executor 的次数,减少网络传输时间,提高效率!但可能一些分区数据量太大,一时半会
      // 计算不完,但有一部分已经计算,不计算完不释放资源,导致 OOM
      datas.map(_*2)  // 此处 map 是 Scala 的,不是 Spark 的
    })
    val collect: Array[Int] = mapPartitionsRDD.collect
    collect.foreach(println)
  }
}

RDD 算子 mapPartitionsWithIndex

与 mapPartitions 对比,多了个分区号

package org.example.chapter02

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

object RDD_04_mapPartitionsWithIndex {
    
    
  def main(args: Array[String]): Unit = {
    
    
    val config: SparkConf = new SparkConf()
      .setMaster("local[*]")
      .setAppName("WordCount")
      .set("spark.testing.memory","471859200")  // 根据报错设置要求的最小内存
    val sc = new SparkContext(config)

    // 自动生成临时变量 Ctrl + Alt + v 或在后面写 .var
    val listRDD: RDD[Int] = sc.parallelize(1 to 4)
    val tupleRDD: RDD[(Int, String)] = listRDD.mapPartitionsWithIndex(
      (num, datas) => {
    
     // num 分区号
        datas.map(x => (x * 2, s"分区号是 $num")) // 此处 map 是 Scala 的,不是 Spark 的
      })
    val collect: Array[(Int, String)] = mapRDD.collect
    collect.foreach(println)
  }
}

RDD 算子 flatMap

package org.example.chapter02

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

object RDD_05_flatMap {
    
    
  def main(args: Array[String]): Unit = {
    
    
    val config: SparkConf = new SparkConf()
      .setMaster("local[*]")
      .setAppName("WordCount")
      .set("spark.testing.memory","471859200")  // 根据报错设置要求的最小内存
    val sc = new SparkContext(config)

    // 自动生成临时变量 Ctrl + Alt + v 或在后面写 .var
    val listRDD: RDD[List[Int]] = sc.parallelize(List(List(1, 2, 3), List(2, 3, 4)))
    val flatMapRDD: RDD[Int] = listRDD.flatMap(datas => datas)
    val collect: Array[Int] = flatMapRDD.collect
    collect.foreach(println)
  }
}

RDD 算子 glom

package org.example.chapter02

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

object RDD_06_glom {
    
    
  def main(args: Array[String]): Unit = {
    
    
    val config: SparkConf = new SparkConf()
      .setMaster("local[*]")
      .setAppName("WordCount")
      .set("spark.testing.memory","471859200")  // 根据报错设置要求的最小内存
    val sc = new SparkContext(config)

    val rdd: RDD[Int] = sc.parallelize(List(1, 2, 3, 4, 5, 6, 7), 3)
    // 把每个分区搞成一个 Array[Int]
    val glomRDD: RDD[Array[Int]] = rdd.glom
    val collect: Array[Array[Int]] = glomRDD.collect
    collect.foreach(array => {
    
    
      println(array.mkString(","))
    })
    /*
    * 1,2
    * 3,4
    * 5,6,7
    * */
  }
}

RDD 算子 groupBy

package org.example.chapter02

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

object RDD_07_groupBy {
    
    
  def main(args: Array[String]): Unit = {
    
    
    val config: SparkConf = new SparkConf()
      .setMaster("local[*]")
      .setAppName("WordCount")
      .set("spark.testing.memory","471859200")  // 根据报错设置要求的最小内存
    val sc = new SparkContext(config)

    val rdd: RDD[Int] = sc.parallelize(List(1, 2, 3, 4, 5, 6, 7))
    // 按照 x % 2 的值分组
    val groupByRDD: RDD[(Int, Iterable[Int])] = rdd.groupBy(x => x % 2)
    val collect: Array[(Int, Iterable[Int])] = groupByRDD.collect
    collect.foreach(println)
  }
}

RDD 算子 filter

package org.example.chapter02

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

object RDD_08_filter {
    
    
  def main(args: Array[String]): Unit = {
    
    
    val config: SparkConf = new SparkConf()
      .setMaster("local[*]")
      .setAppName("WordCount")
      .set("spark.testing.memory","471859200")  // 根据报错设置要求的最小内存
    val sc = new SparkContext(config)

    val rdd: RDD[Int] = sc.parallelize(List(1, 2, 3, 4, 5, 6, 7))
    val filterRDD: RDD[Int] = rdd.filter(_ % 2 != 0)
    val collect: Array[Int] = filterRDD.collect
    collect.foreach(println)
  }
}

RDD 算子 distinct

package org.example.chapter02

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

object RDD_08_distinct {
    
    
  def main(args: Array[String]): Unit = {
    
    
    val config: SparkConf = new SparkConf()
      .setMaster("local[*]")
      .setAppName("WordCount")
      .set("spark.testing.memory","471859200")  // 根据报错设置要求的最小内存
    val sc = new SparkContext(config)

    val rdd: RDD[Int] = sc.parallelize(List(1, 2, 2, 5, 5, 5, 7))
    // 刚开始数据分布在不同分区,经过 distinct,原来分区的数据可能被放到别的分区(shuffle)
    val distinctRDD: RDD[Int] = rdd.distinct
    val collect: Array[Int] = distinctRDD.collect
    collect.foreach(println)
  }
}

RDD 算子 coalesce、repartition

缩减分区数

package org.example.chapter02

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

object RDD_10_coalesce {
    
    
  def main(args: Array[String]): Unit = {
    
    
    val config: SparkConf = new SparkConf()
      .setMaster("local[*]")
      .setAppName("WordCount")
      .set("spark.testing.memory","471859200")  // 根据报错设置要求的最小内存
    val sc = new SparkContext(config)

    val rdd: RDD[Int] = sc.parallelize(1 to 16, 4)
    println(s"缩减分区前分区数是 ${rdd.partitions.size}")
    val coalesceRDD: RDD[Int] = rdd.coalesce(3)
    println(s"缩减分区后分区数是 ${coalesceRDD.partitions.size}")

	println(sc.isStopped) // false
    sc.stop // 关闭 sc
    println(sc.isStopped) // true
  }
}

repartition 调用的就是 coalesce,只是传入参数 shuffle = true
在这里插入图片描述
改变分区,就改变了任务数,对应并行的任务。

RDD 算子 sortBy

下面例子,按照除以 3 的余数默认升序排列,传入 false,则降序排列在这里插入图片描述

RDD 算子 union/subtract/intersection/Cartesian

在这里插入图片描述
RDD 算子

RDD 算子 zip

在这里插入图片描述
在这里插入图片描述

RDD key-value 算子 partitionBy

在这里插入图片描述

RDD key-value算子 groupByKey/reduceByKey

在这里插入图片描述
词频统计

package org.example.chapter02

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

object RDD_11_kv_groupByKey {
    
    
  def main(args: Array[String]): Unit = {
    
    
    val config: SparkConf = new SparkConf()
      .setMaster("local[*]")
      .setAppName("WordCount")
      .set("spark.testing.memory","471859200")  // 根据报错设置要求的最小内存
    val sc = new SparkContext(config)

    val words: Array[String] = Array("one", "two", "two", "three", "three", "three")

    val wordPairsRDD: RDD[(String, Int)] = sc.parallelize(words).map(word => (word, 1))

    val group: RDD[(String, Iterable[Int])] = wordPairsRDD.groupByKey

    val sum: RDD[(String, Int)] = group.map(t => (t._1, t._2.sum))

    sum.foreach(println)

    sc.stop // 关闭 sc
  }
}

在这里插入图片描述

// 实例(求每个 key 对应的所有值之和的平均值)
val myRDD = sc.parallelize(List(("a",3),("a",2),("c",4),("b",3),("c",6),("c",8)),2)
myRDD.map(t => (t._1, (t._2, 1))).reduceByKey((x, y) => (x._1+y._1, x._2+y._2)).map(t => (t._1, t._2._1 / t._2._2)).collect

RDD key-value算子 aggregateByKey/foldByKey

val rdd = sc.parallelize(List(("a",3),("a",2),("c",4),("b",3),("c",6),("c",8)),2)
// def aggregateByKey[U: ClassTag](zeroValue: U)(seqOp: (U, V) => U, combOp: (U, U) => U): RDD[(K, U)]
// V 对应的类型在本例中就是 key-value 中的 value 类型,即 Int
// 先求各个分区内部每个 key 对应的最大 value,再把各个分区的这些 value 求和
rdd.aggregateByKey(0)((u, v) => math.max(u, v), _+_).collect
rdd.aggregateByKey(6)((u, v) => math.max(u, v), _+_).collect

在这里插入图片描述

// combineByKeyWithClassTag[U](
    //      (v: V) => cleanedSeqOp(createZero(), v),  // 分区内,初始值
    //      cleanedSeqOp,   // 分区内(与分区间计算逻辑可不同)
    //      combOp,         // 分区间(与分区内计算逻辑可不同)
    //      partitioner)
//    PairRDDFunctions.aggregateByKey
    // combineByKeyWithClassTag[V](
    //      (v: V) => cleanedFunc(createZero(), v),   // 分区内,初始值
    //      cleanedFunc,    // 分区内(与分区间计算逻辑相同)
    //      cleanedFunc,    // 分区间(与分区内计算逻辑相同)
    //      partitioner)
//    PairRDDFunctions.foldByKey

在这里插入图片描述

RDD key-value算子 sortByKey

val rdd = sc.parallelize(Array((3,"aa"),(6,"cc"),(2,"bb"),(1,"dd")))
rdd.sortByKey().collect
rdd.sortByKey(false).collect

在这里插入图片描述

RDD key-value算子 mapValues

针对于 (K,V) 形式的类型只对 V 进行操作。
在这里插入图片描述

RDD action算子 reduce

def reduce(f: (T, T) => T): T
在这里插入图片描述

RDD action算子 collect

以数组的形式将结果数据集收集到 Driver 端。

RDD action算子 count

返回 RDD 中元素的个数。
在这里插入图片描述

RDD action算子 first

返回RDD中的第一个元素。
在这里插入图片描述

RDD action算子 take(n)

返回一个由RDD的前n个元素组成的数组。
在这里插入图片描述

RDD action算子 takeOrdered(n)

返回该 RDD 排序后的前 n 个元素组成的数组。
在这里插入图片描述

RDD action算子 aggregate

def aggregate[U: ClassTag](zeroValue: U)(seqOp: (U, T) => U, combOp: (U, U) => U): U

在这里插入图片描述

RDD action算子 fold

def fold(zeroValue: T)(op: (T, T) => T): T

aggregate 的简化操作,分区内 seqop 和 分区间 combop 的计算逻辑一样。
在这里插入图片描述

RDD action算子 saveAsTextFile/saveAsSequenceFile/saveAsObjectFile

在这里插入图片描述

RDD action算子 countByKey

针对 (K,V) 类型的 RDD,返回一个 (K,Int) 的map,表示每一个 key 对应的元素个数。
在这里插入图片描述

RDD action算子 foreach

val rdd: RDD[Int] = sc.parallelize(1 to 6)
    
rdd.foreach{
    
      // 此处 foreach 是 RDD 算子 
  i => println(i) // 此处代码在 Executor 上执行
}

val arr: Array[Int] = rdd.collect // collect 把数据都收集到 Driver 处了
arr.foreach{
    
      // 此处 foreach 是 Scala 函数
  i => {
    
    
    println(i)  // 此处代码在 Driver 上执行
  }
}

RDD 序列化

在实际开发中我们往往需要自己定义一些对于 RDD 的操作,那么此时需要主要的是,初始化工作是在 Driver 端进行的,而实际运行程序是在 Executor 端进行的,这就涉及到了跨进程通信,是需要序列化的。

package org.example.chapter02

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

object RDD_12_functionPass {
    
    
  def main(args: Array[String]): Unit = {
    
    
    val config: SparkConf = new SparkConf()
      .setMaster("local[*]")
      .setAppName("WordCount")
      .set("spark.testing.memory","471859200")  // 根据报错设置要求的最小内存
    val sc = new SparkContext(config)

    val rdd: RDD[String] = sc.parallelize(Array("hadoop", "spark", "hive"))

    val search = new Search("h")

    val matchRDD: RDD[String] = search.matches(rdd)
    matchRDD.collect.foreach(println)
    
    sc.stop // 关闭 sc
  }
}

class Search(s:String) extends Serializable {
    
     // 不实现 Serializable,就会报错,对象没序列化

  def matches (rdd: RDD[String]): RDD[String] = {
    
    
    rdd.filter(r => r.contains(s))
  }
}


注:case类可以自动序列化

RDD 任务划分

RDD 任务切分中间分为:Application、Job、Stage 和 Task

  • Application:初始化一个 SparkContext 即生成一个 Application;
  • Job:一个 Action 算子就会生成一个 Job;
  • Stage:Stage 等于宽依赖(ShuffleDependency)的个数加 1;
  • Task:一个 Stage 阶段中,最后一个 RDD 的分区个数就是 Task 的个数。
    注:Application->Job->Stage->Task 每一层都是 1 对 n 的关系。

RDD 持久化

RDD Cache

RDD 通过 Cache 或者 Persist 方法将前面的计算结果缓存,默认情况下会把数据以缓存在 JVM 的堆内存中。但是并不是这两个方法被调用时立即缓存,而是触发后面的 action 算子时,该 RDD 将会被缓存在计算节点的内存中,并供后面重用。

package org.example.persist

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

object Persist01 {
    
    
  def main(args: Array[String]): Unit = {
    
    
    val sparConf = new SparkConf().setMaster("local").setAppName("Persist")
    val sc = new SparkContext(sparConf)

    val list = List("Hello Scala", "Hello Spark")

    val rdd = sc.makeRDD(list)

    val flatRDD = rdd.flatMap(_.split(" "))

    val mapRDD = flatRDD.map(word=>{
    
    
      println("@@@@@@@@@@@@")
      (word,1)
    })

    val reduceRDD: RDD[(String, Int)] = mapRDD.reduceByKey(_+_)
    reduceRDD.collect().foreach(println)

    println("**************************************")

    val groupRDD = mapRDD.groupByKey()
    groupRDD.collect().foreach(println)

    sc.stop()
  }
}

以上代码的RDD依赖关系如下图,虽然看起来reduceByKey和groupByKey使用了同一个mapRDD,其实只是转换逻辑共用,实则都要从头跑一遍。
在这里插入图片描述

package org.example.persist

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

object Persist02 {
    
    

  def main(args: Array[String]): Unit = {
    
    
    val sparConf = new SparkConf().setMaster("local").setAppName("Persist")
    val sc = new SparkContext(sparConf)

    val list = List("Hello Scala", "Hello Spark")

    val rdd = sc.makeRDD(list)

    val flatRDD = rdd.flatMap(_.split(" "))

    val mapRDD = flatRDD.map(word=>{
    
    
      println("@@@@@@@@@@@@")
      (word,1)
    })
    // cache默认持久化的操作,只能将数据保存到内存中,如果想要保存到磁盘文件,需要更改存储级别
    //mapRDD.cache()

    // 持久化操作必须在行动算子执行时完成的。
    mapRDD.persist(StorageLevel.DISK_ONLY)

    val reduceRDD: RDD[(String, Int)] = mapRDD.reduceByKey(_+_)
    reduceRDD.collect().foreach(println)

    println("**************************************")

    val groupRDD = mapRDD.groupByKey()
    groupRDD.collect().foreach(println)

    sc.stop()
  }
}

上面代码,使用缓存后,RDD血缘依赖关系如下图,groupByKey不必从头跑一遍,直接从cache拿数据。
在这里插入图片描述

缓存有可能丢失,或者存储于内存的数据由于内存不足而被删除,RDD 的缓存容错机制保证了即使缓存丢失也能保证计算的正确执行。通过基于 RDD 的一系列转换,丢失的数据会被重算,由于 RDD 的各个 Partition 是相对独立的,因此只需要计算丢失的部分即可,并不需要重算全部 Partition。

Spark 会自动对一些 Shuffle 操作的中间数据做持久化操作(比如:reduceByKey)。这样做的目的是为了当一个节点 Shuffle 失败了避免重新计算整个输入。但是,在实际使用的时候,如果想重用数据,仍然建议调用 persist 或 cache。

RDD CheckPoint 检查点

所谓的检查点其实就是通过将 RDD 中间结果写入磁盘由于血缘依赖过长会造成容错成本过高,这样就不如在中间阶段做检查点容错,如果检查点之后有节点出现问题,可以从检查点开始重做血缘,减少了开销。对 RDD 进行 checkpoint 操作并不会马上被执行,必须执行 Action 操作才能触发。

package org.example.persist

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

object Persist04 {
    
    
  def main(args: Array[String]): Unit = {
    
    

    // cache : 将数据临时存储在内存中进行数据重用
    //         会在血缘关系中添加新的依赖。一旦,出现问题,可以重头读取数据
    // persist : 将数据临时存储在磁盘文件中进行数据重用
    //           涉及到磁盘IO,性能较低,但是数据安全
    //           如果作业执行完毕,临时保存的数据文件就会丢失
    // checkpoint : 将数据长久地保存在磁盘文件中进行数据重用
    //           涉及到磁盘IO,性能较低,但是数据安全
    //           为了能够提高效率,一般情况下,是需要和cache联合使用
    //           执行过程中,会切断血缘关系。重新建立新的血缘关系
    //           checkpoint等同于改变数据源

    val sparConf = new SparkConf().setMaster("local").setAppName("Persist")
    val sc = new SparkContext(sparConf)
    // checkpoint 需要落盘,需要指定检查点保存路径
    sc.setCheckpointDir("cp")

    val list = List("Hello Scala", "Hello Spark")

    val rdd = sc.makeRDD(list)

    val flatRDD = rdd.flatMap(_.split(" "))

    val mapRDD = flatRDD.map(word=>{
    
    
      (word,1)
    })
    mapRDD.cache()
    // 检查点路径保存的文件,当作业执行完毕后,不会被删除,一般保存路径都是在分布式存储系统:HDFS
    mapRDD.checkpoint()

    // 打印血缘关系
    println(mapRDD.toDebugString)

    val reduceRDD: RDD[(String, Int)] = mapRDD.reduceByKey(_+_)
    reduceRDD.collect().foreach(println)

    println("**************************************")

    // 打印血缘关系
    println(mapRDD.toDebugString)

    sc.stop()
  }
}

RDD 分区器

Spark 目前支持 Hash 分区和 Range 分区,和用户自定义分区。Hash 分区为当前的默认分区。分区器直接决定了 RDD 中分区的个数、RDD 中每条数据经过 Shuffle 后进入哪个分区,进而决定了 Reduce 的个数。只有 Key-Value 类型的 RDD 才有分区器,非 Key-Value 类型的 RDD 分区的值是 None。每个 RDD 的分区 ID 范围:0 ~ (numPartitions - 1),决定这个值是属于那个分区的。

  • Hash 分区:对于给定的 key,计算其 hashCode,并除以分区个数取余
  • Range 分区:将一定范围内的数据映射到一个分区中,尽量保证每个分区数据均匀,而且分区间有序
  • 自定义分区器
package org.example.partition

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

object Partition {
    
    
  def main(args: Array[String]): Unit = {
    
    
    val sparConf = new SparkConf().setMaster("local").setAppName("WordCount")
    val sc = new SparkContext(sparConf)

    val rdd = sc.makeRDD(List(
        ("nba", "xxxxxxxxx"),
        ("cba", "xxxxxxxxx"),
        ("wnba", "xxxxxxxxx"),
        ("nba", "xxxxxxxxx")
      ),3)

    val partRDD: RDD[(String, String)] = rdd.partitionBy( new MyPartitioner )

    partRDD.saveAsTextFile("output")

    sc.stop()
  }

  // 自定义分区器
  class MyPartitioner extends Partitioner{
    
    
    // 分区数量
    override def numPartitions: Int = 3

    // 根据数据的key值返回数据所在的分区索引(从0开始)
    override def getPartition(key: Any): Int = {
    
    
      key match {
    
    
        case "nba" => 0
        case "wnba" => 1
        case _ => 2
      }
    }
  }
}
	

RDD 文件读取与保存

Spark 的数据读取及数据保存可以从两个维度来作区分:文件格式以及文件系统。
文件格式分为:text 文件、csv 文件、sequence 文件以及 Object 文件;文件系统分为:本地文件系统、HDFS、HBASE 以及数据库。

text 文件

// 读取输入文件
val inputRDD: RDD[String] = sc.textFile("input/1.txt")
// 保存数据
inputRDD.saveAsTextFile("output")

sequence 文件

SequenceFile 文件是 Hadoop 用来存储二进制形式的 key-value 对而设计的一种平面文件(Flat File)。在 SparkContext 中,可以调用 sequenceFilekeyClass, valueClass

// 保存数据为 SequenceFile
dataRDD.saveAsSequenceFile("output")
// 读取 SequenceFile 文件
sc.sequenceFile[Int,Int]("output").collect().foreach(println)

object 对象 文件

对象文件是将对象序列化后保存的文件,采用 Java 的序列化机制。可以通过 objectFileT:ClassTag函数接收一个路径,读取对象文件,返回对应的 RDD,也可以通过调用saveAsObjectFile()实现对对象文件的输出。因为是序列化所以要指定类型。

// 保存数据
dataRDD.saveAsObjectFile("output")
// 读取数据
sc.objectFile[Int]("output").collect().foreach(println)

累加器

在这里插入图片描述
在这里插入图片描述

自定义累加器

package org.example.accumulator

import org.apache.spark.util.AccumulatorV2
import org.apache.spark.{
    
    SparkConf, SparkContext}

import scala.collection.mutable

object AccWordCount {
    
    
  def main(args: Array[String]): Unit = {
    
    

    val sparConf = new SparkConf().setMaster("local").setAppName("Acc")
    val sc = new SparkContext(sparConf)

    val rdd = sc.makeRDD(List("hello", "spark", "hello"))

    // 创建累加器对象
    val wcAcc = new MyAccumulator()
    // 向Spark进行注册
    sc.register(wcAcc, "wordCountAcc")

    rdd.foreach(
      word => {
    
    
        // 数据的累加(使用累加器)
        wcAcc.add(word)
      }
    )

    // 获取累加器累加的结果
    println(wcAcc.value)

    sc.stop()

  }
}
/**
 * 自定义数据累加器:WordCount(模仿 sc.collectionAccumulator())
 */
class MyAccumulator extends AccumulatorV2[String, mutable.Map[String, Long]] {
    
    
  private var wcMap = mutable.Map[String, Long]()

  // 判断是否初始状态
  override def isZero: Boolean = {
    
    
    wcMap.isEmpty
  }

  override def copy(): AccumulatorV2[String, mutable.Map[String, Long]] = {
    
    
    new MyAccumulator()
  }

  override def reset(): Unit = {
    
    
    wcMap.clear()
  }

  // 获取累加器需要计算的值
  override def add(word: String): Unit = {
    
    
    val newCnt = wcMap.getOrElse(word, 0L) + 1
    wcMap.update(word, newCnt)
  }

  // Driver合并多个累加器
  override def merge(other: AccumulatorV2[String, mutable.Map[String, Long]]): Unit = {
    
    

    val map1 = this.wcMap
    val map2 = other.value

    map2.foreach{
    
    
      case ( word, count ) => {
    
    
        val newCount = map1.getOrElse(word, 0L) + count
        map1.update(word, newCount)
      }
    }
  }

  // 累加器结果
  override def value: mutable.Map[String, Long] = {
    
    
    wcMap
  }
}


广播变量(调优策略)

广播变量用来高效分发较大的对象。向所有工作节点发送一个较大的只读值,以供一个或多个 Spark 操作使用。

方式一:join

package org.example.broadcast

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

import scala.collection.mutable

object Broadcast01 {
    
    

  def main(args: Array[String]): Unit = {
    
    

    val sparConf = new SparkConf().setMaster("local").setAppName("Acc")
    val sc = new SparkContext(sparConf)

    val rdd1 = sc.makeRDD(List(
      ("a", 1),("b", 2),("c", 3))
    )

    val rdd2 = sc.makeRDD(List(
      ("a", 4),("b", 5),("c", 6))
    )

    // join,导致数据量叉乘增长,会影响shuffle的性能,不推荐使用
    val joinRDD: RDD[(String, (Int, Int))] = rdd1.join(rdd2)
    joinRDD.collect().foreach(println)

    sc.stop()

  }
}


方式二:常规优化

此种方法可能的问题是,若多个分区都在一个Executor(资源不够),每个分区对应的Task都得存一份map,导致一个Executor存在多个重复数据map,如果map还比较大,就很占用Executor的内存。

而一个Executor启动一个JVM,可以在Executor的内存中存一份map,所有Task引用map即可,这就是广播变量(不可修改,避免多个Task修改同一个map引起并发安全问题)。

package org.example.broadcast

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

import scala.collection.mutable

object Broadcast02 {
    
    

  def main(args: Array[String]): Unit = {
    
    

    val sparConf = new SparkConf().setMaster("local").setAppName("Acc")
    val sc = new SparkContext(sparConf)

    val rdd = sc.makeRDD(List(
      ("a", 1),("b", 2),("c", 3)
    ))
    val map = mutable.Map(("a", 4),("b", 5),("c", 6))

    rdd.map {
    
    
      case (w, c) => {
    
    
        val l: Int = map.getOrElse(w, 0)
        (w, (c, l))
      }
    }.collect().foreach(println)

    sc.stop()

  }
}


方式三:广播变量优化

package org.example.broadcast

import org.apache.spark.broadcast.Broadcast
import org.apache.spark.{
    
    SparkConf, SparkContext}

import scala.collection.mutable

object Broadcast03 {
    
    

  def main(args: Array[String]): Unit = {
    
    

    val sparConf = new SparkConf().setMaster("local").setAppName("Acc")
    val sc = new SparkContext(sparConf)

    val rdd1 = sc.makeRDD(List(
      ("a", 1),("b", 2),("c", 3)
    ))
    val map = mutable.Map(("a", 4),("b", 5),("c", 6))

    // 封装广播变量
    val bc: Broadcast[mutable.Map[String, Int]] = sc.broadcast(map)

    rdd1.map {
    
    
      case (w, c) => {
    
    
        // 方法广播变量
        val l: Int = bc.value.getOrElse(w, 0)
        (w, (c, l))
      }
    }.collect().foreach(println)

    sc.stop()


  }
}


RDD 其他算子

sample 抽样
combineByKey
cogroup

SparkSQL

直接操作没有结构的 HDFS 比较麻烦,Hive 把 HDFS 搞成一块一块的有结构的数据,易于对数据查询。Hive SQL 转换成 MapReduce 然后提交到集群上执行,大大简化了编写 MapReduce 的程序的复杂性。模仿 Hive,Spark SQL 同样是为了简化 Spark 程序开发,Spark SQL 转换成 RDD,然后提交到集群执行,执行效率非常快!

后来,Spark 提供了 DataFrame 和 DataSet (底层还是 RDD,但是做了很多优化,具备结构),DataFrame 可理解为 DataSet[Row]。 后续实际操作后可真正理解 RDD/DataFrame/DataSet 的区别和联系。
在这里插入图片描述

在这里插入图片描述

DataFrame

SQL 风格
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
DSL(Domain Specific Language)风格
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
RDD 转 DataFrame(手动方式,告诉 RDD 搞成啥结构)
在这里插入图片描述
RDD 转 DataFrame(反射方式,case 类)
在这里插入图片描述
DataFrame 转 RDD

df.rdd

DataFrame 转 DataSet
在这里插入图片描述

DataSet

在这里插入图片描述
RDD 转 DataSet
在这里插入图片描述
DataSet 转 RDD
在这里插入图片描述
DataSet 转 DataFrame
在这里插入图片描述

RDD&DataFrame&DataSet

在这里插入图片描述

IDEA 开发 Spark SQL

引入 spark-sql 包

<dependency>
   <groupId>org.apache.spark</groupId>
    <artifactId>spark-sql_2.11</artifactId>
    <version>2.4.6</version>
</dependency>

入门示例

package org.example.chapter03

import org.apache.spark.sql.SparkSession
import org.apache.spark.{
    
    SparkConf}

object SparkSQL_01_demo {
    
    
  def main(args: Array[String]): Unit = {
    
    
    val config: SparkConf = new SparkConf()
      .setMaster("local[*]")
      .setAppName("WordCount")
      .set("spark.testing.memory","471859200")  // 根据报错设置要求的最小内存
    val spark: SparkSession = SparkSession.builder().config(config).getOrCreate()

    val df = spark.read.json("input/1.json")
    df.show()

    spark.stop()
  }
}

在这里插入图片描述
RDD/DataFrame/DataSet 互相转换(注意引入隐式转换)

import org.apache.spark.SparkConf
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.{
    
    DataFrame, Dataset, Row, SparkSession}

object SparkSQL_02_transform {
    
    
  def main(args: Array[String]): Unit = {
    
    
    val sparkConf: SparkConf = new SparkConf().setMaster("local[*]")
      .setAppName("SparkSQL_01")
      .set("spark.testing.memory", "471859200")
    val spark: SparkSession = SparkSession.builder().config(sparkConf).getOrCreate()

    import spark.implicits._
    // RDD -> DataFrame -> DataSet
    val rdd: RDD[(Int,String,Int)] = spark.sparkContext.parallelize(List((1,"lia",12),(2,"lib",13),(3,"lic",16)))
    val df: DataFrame = rdd.toDF("id","name","age")
    val ds: Dataset[User] = df.as[User]
    ds.show()

    // DataSet -> DataFrame -> RDD
    val df1: DataFrame = ds.toDF()
    val rdd1: RDD[Row] = df1.rdd
    rdd1.foreach(row => {
    
    
      println(row.get(0) + " " + row.get(1) + " " + row.get(2))
    })

    // RDD -> DataSet & DataSet -> RDD
    val userDS: Dataset[User] = rdd.map(t => User(t._1, t._2, t._3)).toDS()
    val userRDD = userDS.rdd
    userRDD.foreach(println)
  }
}
case class User(id: Int, name: String, age: Int)

更多

Google / Baidu / StackOverflow + 官方资料(官方指导文档官方 API

参考资料

[1] 尚硅谷 Spark
[2] 官方指导文档
[3] 官方 API

猜你喜欢

转载自blog.csdn.net/ccnuacmhdu/article/details/107171043