【大資料開發】SparkCore——利用廣播變數優化ip地址統計、Spark2.x自定義累加器

這個妹妹我見過發表於2020-10-12

一、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怎麼實現的

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
}

相關文章