文章目录
一、Broadcast广播变量
1.1 广播变量的逻辑过程
两句关键语句
// 封装广播变量
1. val broadcast = sc.broadcost(可序列化对象)
// 使用value可以获取广播变量的值
2. broadcoast.value
⼴播变量的过程如下:
(1) 通过对⼀个类型 T 的对象调⽤ SparkContext.broadcast 创建出⼀个 Broadcast[T] 对象。 任何可序列化的类型都可以这么实现。自定义的类型应实现可序列化特质。
(2) 通过 value 属性访问该对象的值(在 Java 中为 value() ⽅法)。
(3) 变量只会被发到各个节点⼀次,应作为只读值处理(修改这个值不会影响到别的节点)。
能不能将⼀个RDD使⽤⼴播变量⼴播出去?
不能,因为RDD是不存储数据的。可以将RDD的结果⼴播出去。
(4) ⼴播变量只能在Driver端定义,不能在Executor端定义。
1.2 优化ip地址统计
* IP地址统计案例的优化版本 - 使用广播变量
*
* 在之前的做法中,读取IP地址信息的文件到内存中(Driver端)。
* 当在Executor中处理数据的时候,使用到了这个存储了IP地址信息的数组的时候,从Driver端发送一个副本过来。
* 此时,会出现一个问题:
* Executor中的处理的数据,可能在不同的分区中,每一个分区都需要一份IP地址信息的副本。
* 假如说: 存储IP地址信息的集合有10M,一个Executor中有10个Task(即10个分区),就需要在这个Executor中创建这个10M的集合的10个副本,也就是要占用100M。
* 此时会带来的问题:
* 1. 节点之间传输的效率低下,需要在节点之间传输100M的数据。
* 2. 可能会造成内存溢出。
*
* 解决方案: 使用广播变量
* 将这个存储IP地址信息的大的集合,做成广播变量。
* 在Executor中需要用到这个集合的时候,只需要在一个Executor中,拉取一个副本即可。
* 无论Executor中有多少个Task,最终产生的副本数量,只有1个。
import day09_spark._01_examples.ExampleConstants
import org.apache.spark.broadcast.Broadcast
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.rdd.RDD
object IPAnalysePro {
def main(args: Array[String]): Unit = {
val sc: SparkContext = new SparkContext(new SparkConf().setMaster("local").setAppName("ip"))
// 1. 读取ip.txt,解析出每一个城市的地址段
val provinces: Array[(Long, Long, String)] = sc.textFile(ExampleConstants.PATH_IP_IP).map(line => {
val infos: Array[String] = line.split("\\|")
val start: Long = infos(2).toLong // 起始IP十进制表示形式
val end: Long = infos(3).toLong // 结束IP十进制表示形式
val province: String = infos(6) // 省份信息
(start, end, province)
}).collect()
// 将provinces做成广播变量,优化现在的程序
// 因为这个对象,需要在不同的节点之间进行传递,在每一个Task中都需要使用到这个变量
val broadcastProvinces: Broadcast[Array[(Long, Long, String)]] = sc.broadcast(provinces)
// 2. 读取http.log文件,截取出IP信息,带入到第一步的集合中,查出省份
val rdd: RDD[(String, Int)] = sc.textFile(ExampleConstants.PATH_IP_LOG).map(line => {
val infos: Array[String] = line.split("\\|")
// 2.1. 提取出IP地址
val ipStr: String = infos(1)
// 2.2. 将IP地址转成十进制的数字,用来比较范围
val ipNumber: Long = ipStr2Num(ipStr)
// 2.3. 将ipNumber, 带入到所有的IP地址的集合中,查询属于哪一个省份的
val province: String = queryIP(ipNumber)(broadcastProvinces.value)
(province, 1)
})
// 3. 将相同的省份聚合,结果降序排序
val res: RDD[(String, Int)] = rdd.reduceByKey(_ + _).sortBy(_._2, ascending = false)
res.coalesce(1).saveAsTextFile("C:\\Users\\luds\\Desktop\\output")
}
/**
* 将一个ip地址字符串,转成十进制的数字
* @param ipStr ip地址字符串
* @return 转成的十进制的结果
*/
def ipStr2Num(ipStr: String): Long = {
// 1. 拆出每一个部分
val ips: Array[String] = ipStr.split("[.]")
// 2. 定义变量,计算最终的结果
var result: Long = 0L
// 3. 计算
ips.foreach(n => {
result = n.toLong | result << 8L
})
result
}
def queryIP(ipNumber: Long)(implicit provinces: Array[(Long, Long, String)]): String = {
// 使用二分查找法,查询ipNumber属于哪一个城市
var min: Int = 0
var max: Int = provinces.length - 1
while (min <= max) {
// 计算中间下标
val middle: Int = (min + max) / 2
// 进行范围检查
val middleElement: (Long, Long, String) = provinces(middle)
if (ipNumber >= middleElement._1 && ipNumber <= middleElement._2) {
// 说明找到了
return middleElement._3
} else if (ipNumber < middleElement._1) {
max = middle - 1
} else {
min = middle + 1
}
}
""
}
}
二、累加器
collect算子应当慎用,这是因为collect算子将所有数据从worker端拉取到Driver端,可能会导致Driver端内存溢出
知识点引入
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
object AccumulatorTest1 {
def main(args: Array[String]): Unit = {
val sc: SparkContext = new SparkContext(new SparkConf().setMaster("local[*]").setAppName("Accmulator1"))
val rdd: RDD[Int] = sc.parallelize(Array(1, 2, 3, 4, 5))
// 需求: 统计这些数据的和
var sum: Int = 0
// 问题:
// 因为现在的sum是定义在Driver端的变量,需要累加的数据是在Worker的Executor中
// 每一个Task都会拉取一个sum的副本,对这个副本进行求和计算,但是对Driver端的sum没有影响的
// 所以,最后的求和结果是不对的
// rdd.foreach(sum += _)
// println(sum)
// 这种方式,将不同的Executor中的数据,拉取到Driver端
// 变量sum也是定义在Driver端的,此时就可以完成求和的计算
// 但是,这里有问题:
// 尽量不要直接把数据拉取到Driver端,因为如果把每一个Executor的数据都拉取到Driver端,有可能会让Driver端内存溢出
val arr: Array[Int] = rdd.collect()
arr.foreach(sum += _)
println(sum)
// 这里,最合适的方法,就是使用累加器Accmulator来做
}
}
2.1 Spark 1.x版本的累加器(了解)
Spark默认提供了一个累加器,现在已经弃用
两个重要方法:
1. val sum = sc.accumulator(初始值)
2. sum.value // 获取累加器的值
2.2 Spark 2.x版本的累加器(掌握)
常用累加器
基本操作:
1. 实例化累加器对象
val accumulator: LongAccumulator = new LongAccumulator
2. 注册累加器
sc.register(accumulator)
import org.apache.spark.rdd.RDD
import org.apache.spark.util.LongAccumulator
import org.apache.spark.{Accumulator, SparkConf, SparkContext}
import org.junit.Test
class AccumulatorTest2 {
val sc: SparkContext = new SparkContext(new SparkConf().setMaster("local[*]").setAppName("accumulator2"))
// 通过集合,构建RDD
val rdd: RDD[Int] = sc.parallelize(Array(1, 2, 3, 4, 5))
/**
* Spark 1.x版本提供的Accumulator,已经废弃了
*/
@Test def accumulator1(): Unit = {
// 获取到一个累加器,定义一个累加的初始值
val accumulator: Accumulator[Int] = sc.accumulator(0)
// 累加操作
rdd.foreach(accumulator += _)
// 输出结果
println(accumulator.value)
}
/**
* Spark 2.x版本的累加器
* 从Spark2.x开始,描述累加器的类,变成了AccumulatorV2
*/
@Test def accumulator2(): Unit = {
// 1. 实例化一个累加器对象
val accumulator: LongAccumulator = new LongAccumulator
// 2. 注册累加器
sc.register(accumulator)
// 3. 累加数据
rdd.foreach(accumulator.add(_))
// 4. 输出结果
println(accumulator.value) // 输出累加器的值,相当于是sum()
println(accumulator.avg) // 输出平均值
println(accumulator.sum) // 输出和,等价于.value
println(accumulator.count) // 输出数量
}
2.3 自定义累加器
自定义累加器的时候,如果忘记了重写这6个方法,可以参考AccumulatorV2的子类DoubleAccumulator、LongAccumulator怎么实现的
自定义累加器只需要关注两点:
In 的类型是什么
Out的格式和类型是什么
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.util.AccumulatorV2
object AccumulatorTest3 {
def main(args: Array[String]): Unit = {
val sc: SparkContext = new SparkContext(new SparkConf().setMaster("local[*]").setAppName("myAccumulator"))
// 1. 实例化一个累加器对象
val accumulator: MyAccumulator = new MyAccumulator
// 2. 注册累加器
sc.register(accumulator)
// 3. 累加
sc.parallelize(Array(1, 2, 3, 4, 5, 6, 7, 8, 9)).foreach(accumulator.add)
// 4. 输出结果
println(accumulator.value)
println(accumulator.sum)
println(accumulator.count)
println(accumulator.max)
println(accumulator.min)
println(accumulator.avg)
}
}
/**
* 自定义的累加器
* 可以同时统计和、数量、最大值、最小值、平均值
*/
class MyAccumulator extends AccumulatorV2[Int, (Int, Int, Int, Int, Double)] {
private var _count: Int = 0 // 统计总的数量
private var _sum: Int = 0 // 统计和
private var _max: Int = Int.MinValue // 统计最大值
private var _min: Int = Int.MaxValue // 统计最小值
def count: Int = _count
def sum: Int = _sum
def max: Int = _max
def min: Int = _min
def avg: Double = _sum.toDouble / _count
def this(count: Int, sum: Int, max: Int, min: Int) {
this()
_count = count
_sum = sum
_max = max
_min = min
}
// 判断是否是空的累加器
override def isZero: Boolean = _count == 0 && _sum == 0 && _max == Int.MinValue && _min == Int.MaxValue
// 获取一个累加器的副本对象
// 需要获取到一个新的累加器,这个新的累加器对象的每一个属性值都需要和原来的相同
override def copy(): AccumulatorV2[Int, (Int, Int, Int, Int, Double)] = new MyAccumulator(_count, _sum, _max, _min)
// 重置累加器
// 把累加器中的每一个属性都重置为初始值
override def reset(): Unit = {
_sum = 0
_count = 0
_max = Int.MinValue
_min = Int.MaxValue
}
/**
* 加: 分区内的数据累加
* 可以在这个方法中,自定义计算的逻辑
* @param v 累加的新的数据
*/
override def add(v: Int): Unit = {
_sum += v // 求和
_count += 1 // 数量自增1
_max = Math.max(_max, v) // 计算新的最大值
_min = Math.min(_min, v) // 计算新的最小值
}
/**
* 不同分区之间的累加器的聚合
* @param other 需要聚合到一起的累加器
*/
override def merge(other: AccumulatorV2[Int, (Int, Int, Int, Int, Double)]): Unit = {
other match {
case o: MyAccumulator =>
_sum += o._sum // 合并两个累加器,将两个累加器中的和累加到一起
_count += o.count // 合并两个累加器,将两个累加器中的数量合并到一起
_max = Math.max(_max, o._max) // 合并两个累加器,将两个累加器中的最大值重新计算
_min = Math.min(_min, o._min) // 合并两个累加器,将两个累加器中的最小值重新计算
case _ =>
throw new UnsupportedOperationException("合并的累加器类型不一致")
}
}
/**
* 最终累加的结果
* @return 元组,包含了和、数量、最大值、最小值和平均值
*/
override def value: (Int, Int, Int, Int, Double) = (_sum, _count, _max, _min, avg)
}
2.4 自定义wordcount累加器
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.util.AccumulatorV2
import scala.collection.mutable
object AccumulatorTest4 {
def main(args: Array[String]): Unit = {
val sc: SparkContext = new SparkContext(new SparkConf().setMaster("local[*]").setAppName("myAccumulator"))
val rdd: RDD[String] = sc.textFile("C:\\Users\\luds\\Desktop\\access.txt")
// 1. 实例化一个累加器对象
val accumulator: WordcountAccumulator = new WordcountAccumulator
// 2. 注册
sc.register(accumulator)
// 3. 累加
rdd.flatMap(_.split("\t")).foreach(accumulator.add)
// 4. 输出结果
val value: mutable.Map[String, Int] = accumulator.value
for ((k, v) <- value) {
println(s"$k ==> $v")
}
}
}
/**
* 自定义的Wordcount的累加器,实现单词的数量的累加,完成wordcount
*/
class WordcountAccumulator extends AccumulatorV2[String, mutable.Map[String, Int]] {
// 定义一个Map,用来存储每一个单词,以及出现的次数
private val _map = new mutable.HashMap[String, Int]()
override def isZero: Boolean = _map.isEmpty
override def copy(): AccumulatorV2[String, mutable.Map[String, Int]] = {
// 1. 实例化一个新的WordcountAccumulator累加器对象
val accumulator: WordcountAccumulator = new WordcountAccumulator
// 2. 将当前的map中存储的单词出现的次数,拷贝给这个新的累加器对象
// 注意: 不能直接将当前的_map给accumulator进行赋值 accumulator._map = _map
// 因为此时两个accumulator中的_map的地址就相同了,此时修改一个_map,都会对另外一个造成影响
// 所以,这里需要做_map的深拷贝,将当前的_map中的元素,依次添加到accumulator的_map中
_map.synchronized {
accumulator._map ++= _map
}
// 3. 返回副本
accumulator
}
override def reset(): Unit = _map.clear()
override def add(v: String): Unit = {
// 1. 查询这个单词出现的次数,如果没有出现过,返回0次
val count: Int = _map.getOrElse(v, 0)
// 2. 将次数+1,再存入map中
_map.put(v, count + 1)
// _map.get(v) match {
// case Some(x) => _map += ((v, x + 1))
// case None => _map += ((v, 1))
// }
}
override def merge(other: AccumulatorV2[String, mutable.Map[String, Int]]): Unit = {
// 1. 遍历other中的每一个键值对
for ((k, v) <- other.value) {
// 2. 获取_map中这个键对应的值出现了多少次
val count: Int = _map.getOrElse(k, 0)
// 3. 将_map中出现的此时和v累加到一起
_map.put(k, count + v)
}
// for ((k, v) <- other.value) {
// // 判断这个k是否在当前的_map中存在
// _map.get(k) match {
// case Some(x) => _map += ((k, v + x))
// case None => _map += ((k, v))
// }
// }
}
override def value: mutable.Map[String, Int] = _map
}