【大資料開發】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怎麼實現的
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
}
相關文章
- spark:自定義分割槽,自定義排序,spark與jdbc,廣播變數等Spark排序JDBC變數
- 通過IP地址和子網掩碼,如何計算出網路地址、廣播地址和主機數?
- 利用自定義流程表單開發的優勢,實現流程化發展!
- 中學校園IP網路廣播系統解決方案-校園數字IP廣播系統方案設計指南
- 徹底弄懂ip掩碼中的網路地址、廣播地址、主機地址
- 【筆記】nrf52832廣播使用--廠商自定義資料應用筆記
- spark的計算器與廣播變數Spark變數
- 共享變數、廣播變數,累計器的工作原理圖。變數
- linux初學者自定義IP地址的方法Linux
- 按自定義週期統計資料
- 小區廣播背景音樂IP網路廣播系統方案設計概要
- 小學校園IP網路廣播-基於校園區域網的小學IP數字廣播系統設計
- 大學校園IP網路廣播-基於校園區域網的大學校園IP廣播方案設計指南
- 子網掩碼、網路地址、廣播地址的計算
- MySQL優化之系統變數優化MySql優化變數
- 利用CAGradientLayer自定義顏色漸變viewView
- Nginx 如何自定義變數?Nginx變數
- 解析原生IP和廣播IP
- 自定義開發資料庫升級程式資料庫
- 自定義開發odoo14的統計線上使用者人數Odoo
- 校園IP網路廣播系統方案
- 廣播丟資料
- CSS 自定義屬性(變數)CSS變數
- 國家廣播電視總局:2020年全國廣播電視行業統計資料行業
- python爬蟲利用代理IP分析大資料Python爬蟲大資料
- Android8 自定義廣播接收不到的問題Android
- 如何區分原生IP跟廣播IP
- 搶購倒數計時自定義控制元件的實現與優化控制元件優化
- 使用 CSS 自定義屬性(變數)CSS變數
- mysql中自定義變數有哪些MySql變數
- 高併發,大資料量系統的資料結構優化思路大資料資料結構優化
- 開發BI大資料分析視覺化系統大資料視覺化
- 最佳實踐:負載均衡SLB支援自定義VPC例項IP地址負載
- 自定義view————Banner輪播View
- 如何來區分原生IP跟廣播IP
- Laravel 數量統計優化Laravel優化
- 智慧公安警務系統開發,視覺化大資料開發。視覺化大資料
- 系列TCP/IP協議-廣播與多播(010)TCP協議