【大数据开发】SparkCore——利用广播变量优化ip地址统计、Spark2.x自定义累加器

一、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
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值