Spark_7 SparkCore共享變量

共享變量的概述

Spark 一個非常重要的特性就是共享變量。
默認情況下,如果在一個算子的函數中使用到了某個外部的變量,那麼這個變量的值會被拷貝到每個 task 中,此時每個 task 只能操作自己的那份變量副本。如果多個 task 想要共享某個變量,那麼這種方式是做不到的。
Spark 爲此提供了兩種共享變量,一種是 Broadcast Variable( 廣播變量),另一種是 Accumulator( 累加變量)。 Broadcast Variable 會將用到的變量, 僅僅爲每個節點拷貝一份, 即每個 Executor 拷貝一份, 更大的用途是優化性能,減少網絡傳輸以及內存損耗。 Accumulator 則可以讓多個 task 共同操作一份變量,主要可以進行累加操作。 Broadcast Variable 是共享讀變量, task 不能去修改它,而 Accumulator 可以讓多個 task 操作一個變量。

廣播變量

廣播變量概述及底層分析

廣播變量允許編程者在每個 Executor 上保留外部數據的只讀變量,而不是給每個任務發送一個副本。
在沒有使用廣播變量時,每個 task 都會保存一份它所使用的外部變量的副本, 當一個 Executor 上的多個task 都使用一個大型外部變量時, 對於 Executor 內存的消耗是非常大的。因此, 我們可以將大型外部變量封裝爲廣播變量, 此時一個 Executor 保存一個變量副本,此Executor 上的所有 task 共用此變量, 不再是一個 task 單獨保存一個副本, 這在一定程度上降低了 Spark 任務的內存佔用。
Spark 還嘗試使用高效的廣播算法分發廣播變量,以降低通信成本。
Spark 提供的 Broadcast Variable 是隻讀的,並且在每個 Executor 上只會有一個副本,而不會爲每個 task 都拷貝一份副本,因此, 它的最大作用,就是減少變量到各個節點的網絡傳輸消耗,以及在各個節點上的內存消耗。此外, Spark 內部也使用了高效的廣播算法來減少網絡消耗。
可以通過調用 SparkContext 的 broadcast()方法來針對每個變量創建廣播變量。然後在算子的函數內,使用到廣播變量時,每個 Executor 只會拷貝一份副本了,每個 task 可以使用廣播變量的 value()方法獲取值。
在任務運行時, Executor 並不獲取廣播變量,當 task 執行到使用廣播變量的代碼時,會向 Executor 的內存中請求廣播變量;之後 Executor 會通過 BlockManager 向 Driver 拉取廣播變量,然後提供給 task進行使用。
廣播大變量是 Spark 中常用的基礎優化方法, 通過減少內存佔用實現任務執行性能的提升。

廣播變量的使用

廣播變量用來高效分發較大的對象。向所有工作節點發送一個較大的只讀值,以供一個或多個 Spark 操作使用。比如,如果你的應用需要向所有節點發送一個較大的只讀查詢表,甚至是機器學習算法中的一個很大的特徵向量,廣播變量用起來都很順手。 在多個並行操作中使用同一個變量,但是 Spark 會爲每個任務分別發送。

scala> val broadcastVar = sc.broadcast(Array(1, 2, 3))
broadcastVar: org.apache.spark.broadcast.Broadcast[Array[Int]] = Broadcast(35)

scala> broadcastVar.value
res33: Array[Int] = Array(1, 2, 3)

使用廣播變量的過程如下:
(1) 通過對一個類型 T 的對象調用 SparkContext.broadcast 創建出一個 Broadcast[T]對象。任何可序列化的類型都可以這麼實現。
(2) 通過 value 屬性訪問該對象的值(在 Java 中爲 value()方法)。
(3) 變量只會被髮到各個節點一次,應作爲只讀值處理(修改這個值不會影響到別的節點)。

廣播變量應用場景舉例

Join的實現

//Join:commonJoin,BroadcastJoin()
import org.apache.spark.{SparkConf, SparkContext}

/**
  * @author Gru
  * @create 2019-07-15-13:09
  */
object BroadCastApp {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setAppName("BroadCastApp").setMaster("local[2]")
    val sc= new SparkContext(sparkConf)

    broadCastJoin(sc)
    Thread.sleep(20000)
    sc.stop()
  }
  def commonJoin(sc: SparkContext) = {
    val info1 = sc.parallelize(Array(("01","張三"),("02","李四")))
    val info2 = sc.parallelize(Array(("01","清華","20"),
                  ("03","北大","21"),("04","清華","25"))).map(x=>(x._1,x))
    info1.join(info2).foreachPartition(print)
  }

  /**
    * @param sc
    *       broadCastJoin也就是mapJoin
    *
    */
  def broadCastJoin(sc:SparkContext)={
    /**
      * 適用於有一份數據較小的連接情況
      * 做法是直接把該小份數據直接全部加載到內存當中
      * 按鏈接關鍵字建立索引。然後大份數據就作爲 MapTask 的輸入
      * 對 map()方法的每次輸入都去內存當中直接去匹配連接
      * 然後把連接結果按 key 輸出
      * 這種方法要使用 hadoop中的 DistributedCache 把小份數據分佈到各個計算節點
      * 每個 maptask 執行任務的節點都需要加載該數據到內存,並且按連接關鍵字建立索引
      */
      // 將info1作爲小表傳播出去;廣播是要先到driver端的,所以要collect;Map好操作
    val info1 = sc.parallelize(Array(("01","張三"),("02","李四"))).collectAsMap()
    val info1Broadcast=sc.broadcast(info1)
    //broadcast出去之後就不會再用join來實現
    // 大表的數據讀取出來一條就和廣播出去的小表的記錄做匹配
    // 這種情況不會產生shuffle,性能更好,但是小表數據不能太多
    val info2 = sc.parallelize(Array(("01","清華","20"),
                  ("03","北大","21"),("04","清華","25"))).map(x=>(x._1,x))
    info2.mapPartitions(x=>{
      val BroadCastMap = info1Broadcast.value
      for((key,value)<-x if(BroadCastMap.contains(key)))
        yield (key,BroadCastMap.get(key).getOrElse(""),value._2)
    }).foreach(println)
  }

}

累加器

累加器概述

累加器用來對信息進行聚合,通常在向 Spark 傳遞函數時,比如使用 map() 函數或者用 filter() 傳條件時,可以使用driver中定義的變量,但是集羣中運行的每個任務都會得到這些變量的一份新的副本,更新這些副本的值也不會影響driver中的對應變量。 如果我們想實現所有分片處理時更新共享變量的功能,那麼累加器可以實現我們想要的效果。
累加器( accumulator): Accumulator 是僅僅被相關操作累加的變量,因此可以在並行中被有效地支持。它們可用於實現計數器(如 MapReduce)或總和計數。
Accumulator 是存在於 Driver 端的, 集羣上運行的 task 進行 Accumulator 的累加,隨後把值發到 Driver 端,在 Driver 端彙總( Spark UI 在 SparkContext 創建時被創建,即在 Driver 端被創建,因此它可以讀取 Accumulator 的數值), 由於 Accumulator存在於 Driver 端,從節點讀取不到 Accumulator 的數值。
Spark 提供的 Accumulator 主要用於多個節點對一個變量進行共享性的操作。
Accumulator 只提供了累加的功能,但是卻給我們提供了多個 task 對於同一個變量並行操作的功能,但是 task 只能對 Accumulator 進行累加操作,不能讀取它的值,只有 Driver 程序可以讀取 Accumulator 的值。
Accumulator的底層原理如圖:
在這裏插入圖片描述

累加器的使用

累加器分爲系統累加器和自定義的累加器

系統累加器

通過在驅動器中調用 SparkContext.accumulator(initialValue)方法,創建出存有初始值的累加器。返回值爲 org.apache.spark.Accumulator[T] 對象,其中 T 是初始值 initialValue 的類型。 Spark 閉包裏的執行器代碼可以使用累加器的 += 方法(在 Java 中是 add)增加累加器的值。 驅動器程序可以調用累加器的 value 屬性(在 Java 中使用 value()或 setValue())來訪問累加器的值。
注意: 工作節點上的任務不能訪問累加器的值。從這些任務的角度來看,累加器是一個只寫變量。
對於要在行動操作中使用的累加器, Spark 只會把每個任務對各累加器的修改應用一次。因此,如果想要一個無論在失敗還是重複計算時都絕對可靠的累加器,我們必須把它放在foreach() 這樣的行動操作中。轉化操作中累加器可能會發生不止一次更新。
針對一個輸入的日誌文件,如果我們想計算文件中所有空行的數量,我們可以編寫以下程序:

scala> val notice = sc.textFile("./NOTICE")
notice: org.apache.spark.rdd.RDD[String] = ./NOTICE MapPartitionsRDD[40] at textFile at
<console>:32

scala> val blanklines = sc.accumulator(0)
warning: there were two deprecation warnings; re-run with -deprecation for details
blanklines: org.apache.spark.Accumulator[Int] = 0

scala> val tmp = notice.flatMap(line => {
| if (line == "") {
| blanklines += 1
| }
| line.split(" ")
| })
tmp: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[41] at flatMap at <console>:36

scala> tmp.count()
res31: Long = 3213

scala> blanklines.value
res32: Int = 171

自定義累加器

自定義累加器類型的功能在 1.X 版本中就已經提供了,但是使用起來比較麻煩,在 2.0版本後,累加器的易用性有了較大的改進,而且官方還提供了一個新的抽象類:AccumulatorV2 來提供更加友好的自定義類型累加器的實現方式。實現自定義類型累加器需要繼承 AccumulatorV2 並至少覆寫下例中出現的方法,下面這個累加器可以用於在程序運行過程中收集一些文本類信息,最終以 Set[String]的形式返回。

import org.apache.spark.util.AccumulatorV2
import org.apache.spark.{SparkConf, SparkContext}
import scala.collection.JavaConversions._

class LogAccumulator extends org.apache.spark.util.AccumulatorV2[String, java.util.Set[String]]
{
	private val _logArray: java.util.Set[String] = new java.util.HashSet[String]()
	
	override def isZero: Boolean = {
		_logArray.isEmpty
	}
	override def reset(): Unit = {
		_logArray.clear()
	}
	override def add(v: String): Unit = {
		_logArray.add(v)
	}
	override def merge(other: org.apache.spark.util.AccumulatorV2[String, java.util.Set[String]]):
	Unit = {
		other match {
			case o: LogAccumulator => _logArray.addAll(o.value)
		}
	}
	override def value: java.util.Set[String] = {
		java.util.Collections.unmodifiableSet(_logArray)
	}
	override def copy():org.apache.spark.util.AccumulatorV2[String, java.util.Set[String]] = {
		val newAcc = new LogAccumulator()
		_logArray.synchronized{
			newAcc._logArray.addAll(_logArray)
		}
	newAcc
	}
}
// 過濾掉帶字母的
object LogAccumulator {
	def main(args: Array[String]) {
	val conf=new SparkConf().setAppName("LogAccumulator")
	val sc=new SparkContext(conf)
	val accum = new LogAccumulator
	sc.register(accum, "logAccum")
	val sum = sc.parallelize(Array("1", "2a", "3", "4b", "5", "6", "7cd", "8", "9"), 2).filter(line =>
	{
		val pattern = """^-?(\d+)"""
		val flag = line.matches(pattern)
		if (!flag) {
		accum.add(line)
	}
	flag
	}).map(_.toInt).reduce(_ + _)
	println("sum: " + sum)
	for (v <- accum.value) print(v + "")
	println()
	sc.stop()
	}
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章