了解 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 算子 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)