第二天:Spark Core

在這裏插入圖片描述

Java IO回憶

字節跟字符區別(基礎圖),InputStream、OutputStream、Writer、Reader 。
字節流,分2種:

a.輸入
b.輸出

在這裏插入圖片描述
2.字符流,也分2種:

a.輸入(讀,即讀取)
b.輸出(寫,即寫入)

在這裏插入圖片描述
PS: 字節流是萬能的,方便人類讀寫纔出來的字符流。字符流就包裝後的字節流。

文件字節流讀入
InputStream in = new FIleInputStream("xxx")
緩衝流, 體現了Java裝飾者模式
InputStream bufferIn = new BufferedInputStream(new FileInputStream("xxxx"))

在這裏插入圖片描述

InputStream in = new FIleInputStream("xxx")
一行行的讀取字符, 字節到字符的轉換需要注意下
Reader  reader =  new BufferedReader(new InputStreamReader(new FileInputStream("xxx"),"UTF-8"))
當我們真正當readLine的時候纔會不斷的向前請求需求讀取數據,真正數據數據的只有一個類,其餘的無非就是裝飾再裝飾而已。

InputStreamReader 類是從字節流到字符流的橋接器:它使用指定的字符集讀取字節並將它們解碼爲字符
在這裏插入圖片描述
End:Java中的IO不斷包裝,包裝的是結構的變化,只有在readLine的時候纔會不斷向上反帶調。

1. RDD

模仿上面的Java IO 不斷的對基礎類進行裝飾
在這裏插入圖片描述

    val sc = new SparkContext(conf)
    //3.使用sc創建RDD並執行相應的transformation和action
    val line: RDD[String] = sc.textFile(args(0))
    val words: RDD[String] = line.flatMap(_.split(" "))
    val wordToOne: RDD[(String, Int)] = words.map((_, 1))
    val wordSum: RDD[(String, Int)] = wordToOne.reduceByKey(_ + _)
    val result: Array[(String, Int)] = wordSum.collect()

在這裏插入圖片描述
這裏面基類就是RDD。上面的流程圖就是在封裝邏輯思維,用到的就是Scala中的 控制抽象 功能。也就是把一個邏輯思維傳遞給一個函數,就是把函數當參數傳遞的意思。上面的各種邏輯封裝只有調用了Collect 纔會觸發收集數據對功能

控制抽象:

def myShop(block: => Unit) {

    println("Welcome in!")
    block
    println("Thanks for coming!")

}

def main(args: Array[String]){
    myShop( println("I wanna buy a condom") )
}

什麼是RDD

RDD(Resilient Distributed Dataset)叫做彈性分佈式數據集,是Spark中最基本的數據處理邏輯抽象。,工作中可以認爲是一個Seq,代碼中是一個抽象類,它代表一個彈性的、不可變、可分區、裏面的元素可並行計算的集合。
不可變:可以類比Java中的String,只是不提供可以改變的接口。操作了數據就返回一個新的String或者RDD。
可分區:Kafka、HBase都是可分區的,注意理解併發跟並行的概念。可以將一個大的數據集放到不同的分區中,而不同的分區正好可以對應上spark中的多個executor

RDD屬性

 * Internally, each RDD is characterized by five main properties:
 *
 *  - A list of partitions
 *  - A function for computing each split
 *  - A list of dependencies on other RDDs
 *  - Optionally, a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned)
 *  - Optionally, a list of preferred locations to compute each split on (e.g. block locations for
 *    an HDFS file)
  1. 一組分區(Partition),即數據集的基本組成單位;
  2. 一個計算每個分區的函數;
  3. RDD之間的依賴關係,有直接依賴或者間接依賴,也可以稱血緣。
  4. 一個Partitioner,即RDD的分片函數;
  5. 一個列表,存儲存取每個Partition的優先位置(preferred location)。

PS :大數據計算時候移動數據不如移動計算划算,把數據copy到代碼所在位置不如把代碼copy到數據所在位置。對應第五個特性。

RDD特性

RDD表只讀的分區的數據集,對RDD進行改動,只能通過RDD的轉換操作,由一個RDD得到一個新的RDD,新的RDD包含了從其他RDD衍生所必需的信息。RDDs之間存依賴,RDD的執行是按照血緣關係延時計算的。如果血緣關係較長,可以通過持久化RDD來切斷血緣關係。

1. 彈性

存儲的彈性:內存與磁盤的自動切換;
容錯的彈性:數據丟失可以自動恢復;
計算的彈性:計算出錯重試機制;
分片的彈性:可根據需要重新分片。

2. 分區

RDD邏輯上是分區的,每個分區的數據是抽象存在的,計算的時候會通過一個compute函數得到每個分區的數據。如果RDD是通過已有的文件系統構建,則compute函數是讀取指定文件系統中的數據,如果RDD是通過其他RDD轉換而來,則compute函數是執行轉換邏輯將其他RDD的數據進行轉換。

3. 只讀

如下圖所示,RDD是隻讀的,要想改變RDD中的數據,只能在現有的RDD基礎上創建新的RDD。
在這裏插入圖片描述
由一個RDD轉換到另一個RDD,可以通過豐富的操作算子實現,不再像MapReduce那樣只能寫map和reduce了。
RDD的操作算子包括兩類

  1. 一類叫做Transformations(轉換算子),它是用來將RDD進行轉化,構建RDD的血緣關係;
  2. 另一類叫做Actions(行動算子),它是用來觸發RDD的計算,得到RDD的相關計算結果或者將RDD保存的文件系統中。

算子:

從認知心理學角度看解決問題其實是將問題的初始狀態通過一系列的操作(算子(Operate))來對問題的狀態進行轉換,然後最終達到完成解決的狀態。Spark 中的函數就是算子

4. 依賴

RDDs通過操作算子進行轉換,轉換得到的新RDD包含了從其他RDDs衍生所必需的信息,RDDs之間維護着這種血緣關係,也稱之爲依賴。如下圖所示,依賴包括兩種,一種是窄依賴,RDDs之間分區是一一對應的,另一種是寬依賴,下游RDD的每個分區與上游RDD(也稱之爲父RDD)的每個分區都有關,是多對多的關係。
在這裏插入圖片描述

5. 緩存

如果在應用程序中多次使用同一個RDD,可以將該RDD緩存起來,該RDD只有在第一次計算的時候會根據血緣關係得到分區的數據,在後續其他地方用到該RDD的時候,會直接從緩存處取而不用再根據血緣關係計算,這樣就加速後期的重用。如下圖所示,RDD-1經過一系列的轉換後得到RDD-n並保存到hdfs,RDD-1在這一過程中會有個中間結果,如果將其緩存到內存,那麼在隨後的RDD-1轉換到RDD-m這一過程中,就不會計算其之前的RDD-0了。
在這裏插入圖片描述

6. CheckPoint

雖然RDD的血緣關係天然地可以實現容錯,當RDD的某個分區數據失敗或丟失,可以通過血緣關係重建。但是對於長時間迭代型應用來說,隨着迭代的進行,RDDs之間的血緣關係會越來越長,一旦在後續迭代過程中出錯,則需要通過非常長的血緣關係去重建,勢必影響性能。爲此,RDD支持checkpoint將數據保存到持久化的存儲中,這樣就可以切斷之前的血緣關係,因爲checkpoint後的RDD不需要知道它的父RDDs了,它可以從checkpoint處拿到數據。

2. RDD 編程

編程模型

在Spark中,RDD被表示爲對象,通過對象上的方法調用來對RDD進行轉換。經過一系列的transformations定義RDD之後,就可以調用actions觸發RDD的計算,action可以是嚮應用程序返回結果(count, collect等),或者是向存儲系統保存數據(saveAsTextFile等)。在Spark中,只有遇到action,纔會執行RDD的計算(即延遲計算 lazy module),這樣在運行時可以通過管道的方式傳輸多個轉換。
要使用Spark,開發者需要編寫一個Driver程序(這裏說的都是在Spark自帶的集羣中哦,如果依託於YARN 則只要隨便找一個spark服務器連接Hadoop集羣吧任務提交上去即可),它被提交到集羣以調度運行Worker,如下圖所示。Driver中定義了一個或多個RDD,並調用RDD上的action,Worker則執行RDD分區計算任務。

在這裏插入圖片描述
在這裏插入圖片描述

RDD創建

在Spark中創建RDD的創建方式可以分爲三種:

  1. 從集合中創建RDD
  2. 從外部存儲創建RDD
  3. 從其他RDD創建
從集合中創建RDD

從集合中創建RDD,Spark主要提供了兩種函數:parallelizemakeRDD


object SparkRDD01 {
  def main(args: Array[String]): Unit = {
    Logger.getLogger("org").setLevel(Level.OFF)
    Logger.getLogger("akka").setLevel(Level.OFF)

    //1.創建SparkConf並設置App名稱
    val conf: SparkConf = new SparkConf().setAppName("WC").setMaster("local[*]")
    //2.創建SparkContext,該對象是提交Spark App的入口
    val sc = new SparkContext(conf)
    // 1. 從內存中創建RDD 底層實現就是 parallelize
    val listRDD: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4))
    // 2.  從內存中創建  看調用底層會發現有關聯
    val arrayRDD:RDD[Int] = sc.parallelize(Array(1, 2, 3, 4, 5, 6))
    listRDD.collect().foreach(println)
  }
}
從外部存儲創建RDD

包括本地的文件系統,還有所有Hadoop支持的數據集,比如HDFS、Cassandra、HBase等。默認從文件中讀取的都是String類型。

val lines:RDD[String] = sc.textFile("in")
scala> val rdd2= sc.textFile("hdfs://hadoop102:9000/RELEASE")
rdd2: org.apache.spark.rdd.RDD[String] = hdfs://hadoop102:9000/RELEASE MapPartitionsRDD[4] at textFile at <console>:24
從其他RDD創建

參考WordCount即可。

分區

    val listRDD: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4))
    listRDD.saveAsTextFile("outputfile")

輸出結果:
在這裏插入圖片描述

  def makeRDD[T: ClassTag](
      seq: Seq[T],
      numSlices: Int = defaultParallelism): RDD[T] = withScope {
    parallelize(seq, numSlices)
  }
  ---
    def defaultParallelism: Int = {
    assertNotStopped()
    taskScheduler.defaultParallelism
  }
  ---
  def defaultParallelism(): Int  ctrl +H 看當前類子類的實現
  ---
    override def defaultParallelism(): Int = backend.defaultParallelism()
  ----
SchedulerBackend的 特質的父類,
  ---
   CoarseGrainedSchedulerBackend
  override def defaultParallelism(): Int = {
    conf.getInt("spark.default.parallelism", math.max(totalCoreCount.get(), 2))
    // 此處的conf  就是sc 中定義的conf
  }
  ---
  def getInt(key: String, defaultValue: Int): Int = {
    getOption(key).map(_.toInt).getOrElse(defaultValue)
  }
  --- 
   totalCoreCount.addAndGet(cores)
   // 本地模式 所用核數 
   
    val conf: SparkConf = new SparkConf().setAppName("WC").setMaster("local[*]")

結論: 分區跟 local[*]有關。內存中的默認跟配置的核數有關。
在這裏插入圖片描述
在這裏插入圖片描述
指定分區個數:
在這裏插入圖片描述
看下 讀入文件寫入後的分區數:

    val listRDD: RDD[String] = sc.textFile("in")
    listRDD.saveAsTextFile("outputfile")

結果是:
在這裏插入圖片描述

  def textFile(
      path: String,
      minPartitions: Int = defaultMinPartitions): RDD[String] = withScope {
    assertNotStopped()
    hadoopFile(path, classOf[TextInputFormat], classOf[LongWritable], classOf[Text],
      minPartitions).map(pair => pair._2.toString).setName(path)
  }
  ---
    def defaultMinPartitions: Int = math.min(defaultParallelism, 2)

讀入文件時候指定分區個數寫出的的時候就按照分區個數輸出:

    val listRDD: RDD[String] = sc.textFile("in",3)
    listRDD.saveAsTextFile("outputfile")

sc.textFile 注意點:
比如in文件夾下的word.text

12345

在這裏插入圖片描述
邏輯思維應該是將數據分成了2份,可是我們看下里面代碼:
在這裏插入圖片描述
用到了Hadoop切分(Spark分區規則跟Hadoop規則完全一樣), 5/2 = 2,2,1 這樣的三份。
並且:你可以看到part-00000有數據其餘兩個是沒有的!這說明計算分區跟往分區中寫入數據是兩個不同的操作!,Hadoop默認是按照行來搞的!更細節要參考 20P
End:

  1. 讀取文件時,傳遞的分區參數爲最小分區數,但是不一定是這個分區數, 取決於Hadoop讀取文件時候分片規則。
  2. 內存中分區相對來說很簡單,看參數即可。

RDD轉換(重點)

RDD整體上分爲Value類型和Key-Value類型

Value型

1. map(fun)

在這裏插入圖片描述
作用:返回一個新的RDD,該RDD由每一個輸入元素經過func函數轉換後組成。每個元素操作,分區不變。同時這個_ * 2應該會把執行任務分發到若干個Executor中。

    val listRDD :RDD[Int] = sc.makeRDD(1 to 10)
    val mapRDD:RDD[Int] = listRDD.map(_ * 2)
    mapRDD.collect().foreach(println)
2. mapPartitions(func)

在這裏插入圖片描述
作用:類似於map,但獨立地在RDD的每一個分片(分區)上運行,因此在類型爲T的RDD上運行時,func的函數類型必須是Iterator[T] => Iterator[U]。假設有N個元素,有M個分區,那麼map的函數的將被調用N次,而mapPartitions被調用M次,一個函數一次處理所有分區。
PS:這個跟Map的算子差別就是通過網絡傳輸分發任務的次數,效率上這個更好點。

    val listRDD :RDD[Int] = sc.makeRDD(1 to 10)
    // mapPartitions 可以對一個RDD中所有分區進行遍歷
    // mapPartitions 效率優於Map 算子,減少來餓發送到執行器交互次數
    // mapPartitions 單獨算子更加喫內存,可能出現內存溢出(OOM)
    val mapPartitionsRDD:RDD[Int] = listRDD.mapPartitions(datas=>{
      datas.map(data=>data*2)
      // 這裏對 datas 是Scala中的 iterator, map 也是Scala中的map 。{}裏面的整體邏輯算一個Spark中的一個計算。
    })
    mapPartitionsRDD.collect().foreach(println)
3. mapPartitionsWithIndex

類似於python中的enumerate,在2的基礎上想要知道數據都在那個分區中。
作用:類似於mapPartitions,但func帶有一個整數參數表示分片的索引值,因此在類型爲T的RDD上運行時,func的函數類型必須是(Int, Interator[T]) => Iterator[U];
在這裏插入圖片描述

   val listRDD: RDD[Int] = sc.makeRDD(1 to 10)
    val tupleRDD: RDD[(Int, String)] = listRDD.mapPartitionsWithIndex {
      case (num, datas) => {
        datas.map((_, " 分區號:" + num))
      }
      // 模式匹配
    }
    tupleRDD.collect().foreach(println)
4. map、mapPartition、mapPartitionWithIndex區別。
  1. map():每次處理一條數據。
  2. mapPartition():每次處理一個分區的數據,這個分區的數據處理完後,原RDD中分區的數據才能釋放,可能導致OOM。
  3. 開發指導:當內存空間較大的時候建議使用mapPartition(),以提高處理效率。
  4. mapPartitionWithIndex則關注的是數據來自那個分區。
5. flatMap

可以認爲是跟Scala中的類似,就是個扁平化的操作。

    val listRDD: RDD[List[Int]] = sc.makeRDD(Array(List(1, 2), List(3, 4)))
    val flatMapRDD: RDD[Int] = listRDD.flatMap(datas => datas)
    flatMapRDD.collect().foreach(println)
6. glom

作用:將每一個分區形成一個數組,形成新的RDD類型時RDD[Array[T]],相當於系統自動提供了一個分區函數,然後接下來你可以按照分區來求的每個分區中的min,max,sum,avg等等。

    val listRDD: RDD[Int] = sc.makeRDD(1 to 16, 4)
    //  如果是 1 to 18 ,4 則劃分爲 4 ,4,4,6
    // 將一個分區的數據自動放到一個數組中
    val glomRDD: RDD[Array[Int]] = listRDD.glom()
    glomRDD.collect().foreach(array => println(array.mkString(",")))

結果:

5,6,7,8
9,10,11,12
13,14,15,16
7. groupBy

作用:分組,按照傳入函數的返回值進行分組。將相同的key對應的值放入一個迭代器。


    // 按照指定規則進行分組
    // 分組後的數據形成2元祖 K-V,K表示分組的key,V表示分組數據集合。
    val listRDD: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4))
    val groupByRDD: RDD[(Int, Iterable[Int])] = listRDD.groupBy(_ % 2)
    groupByRDD.collect().foreach(println)
8. filter

作用:過濾。返回一個新的RDD,該RDD由經過func函數計算後返回值爲true的輸入元素組成。

    // 按照規則過濾數據
    val listRDD: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4))
    val filterRDD: RDD[Int] = listRDD.filter(_ % 2 == 0)
    filterRDD.collect().foreach(print)
9. sample(withReplacement, fraction, seed)

功能:隨機性抽取一些數據。
withReplacement:表示抽出樣本後是否在放回去,true表示會放回去,這也就意味着抽出的樣本可能有重複
fraction :這是一個double類型的參數(0-1之間),設定一個出現概率閾值,通過seed給每個數據評價出一個概率,大於閾值留下,否則不留。

seed:表示一個種子,根據這個seed隨機抽取,一般情況下只用前兩個參數就可以,那麼這個參數是幹嘛的呢,這個參數一般用於調試,有時候不知道是程序出問題還是數據出了問題,就可以將這個參數設置爲定值。一般用System.currentTimeMilles確保種子的不同性。可以參考下python的 numpy.seed,挺好玩的一個隨機數選擇器。


---
  def sample(
      withReplacement: Boolean,
      fraction: Double,
      seed: Long = Utils.random.nextLong): RDD[T] = {
    require(fraction >= 0,
      s"Fraction must be nonnegative, but got ${fraction}")

    withScope {
      require(fraction >= 0.0, "Negative fraction value: " + fraction)
      if (withReplacement) {
        new PartitionwiseSampledRDD[T, T](this, new PoissonSampler[T](fraction), true, seed)
        // 柏松分佈 放回
      } else {
        new PartitionwiseSampledRDD[T, T](this, new BernoulliSampler[T](fraction), true, seed)
        // 伯努利分佈 不放回
      }
    }
  }
10. distinct([numPartitions]))

作用:對源RDD進行去重後返回一個新的RDD。
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

    Logger.getLogger("org").setLevel(Level.OFF)
    Logger.getLogger("akka").setLevel(Level.OFF)
    //1.創建SparkConf並設置App名稱
    val conf: SparkConf = new SparkConf().setAppName("WC").setMaster("local[*]")
    //2.創建SparkContext,該對象是提交Spark App的入口
    val sc = new SparkContext(conf)
    val listRDD: RDD[Int] = sc.makeRDD(List(1, 2, 1, 5, 2, 9, 6, 1))
    // 默認是 系統全部進程來分區數據 我這裏是8個
    val distinctRDD: RDD[Int] = listRDD.distinct()
    // 對數據進行 shuffle 刷新重組  然後數據還是8個分區, 因爲 去重後可能少於8個,可以指定分區數
    val distinctRDD1: RDD[Int] = listRDD.distinct(2)
    distinctRDD.collect().foreach(println)
    distinctRDD.saveAsTextFile("output")

shuffle: 表示對一個分區的數據進行打亂拆分到不同的分區 ,不過如果是下面這樣不算是shuffle打亂哦。
在這裏插入圖片描述

11. coalesce(numPartitions)

作用:縮減分區數,用於大數據集過濾後,提高小數據集的執行效率。

// 縮減分區可以認爲簡單對分區合併,沒有涉及到shuffle
    val listRDD: RDD[Int] = sc.makeRDD(1 to 16, 4)
    println("縮減分區前:" + listRDD.partitions.size)
    val coalesceRDD: RDD[Int] = listRDD.coalesce(3)
    println("縮減分區後:" + coalesceRDD.partitions.size)
    coalesceRDD.saveAsTextFile("output")
12. repartition(numPartitions)

作用:根據分區數,重新通過網絡隨機洗牌所有數據,repartitionAndSortWithinPartitions

    val listRDD: RDD[Int] = sc.makeRDD(1 to 16, 4)
    println("縮減分區前:" + listRDD.partitions.size)
    val reRDD: RDD[Int] = listRDD.repartition(2)
    println("縮減分區後:" + reRDD.partitions.size)
    reRDD.saveAsTextFile("output")

在這裏插入圖片描述

13. coalesce和repartition的區別
  1. coalesce重新分區,suffle=false,可以選擇是否進行shuffle過程。由參數shuffle: Boolean = false/true決定。
  2. repartition實際上是調用的coalesce,進行shuffle。源碼如下:
  def repartition(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T] = withScope {
    coalesce(numPartitions, shuffle = true)
  }
14. sortBy(func,[ascending], [numTasks])

作用:使用func先對數據進行處理,按照處理後的數據比較結果排序,默認爲正序,排序後的數據分區數多少也可以指定。

    val listRDD: RDD[Int] = sc.makeRDD(1 to 16, 4)
    listRDD.sortBy(x=>x).collect().foreach(println) // 從小到大
    listRDD.sortBy(x=>x,false).collect().foreach(println) // 從大到小
15. pipe(command, [envVars])

作用:管道,針對每個分區,都執行一個shell腳本,返回輸出的RDD。
注意:腳本需要放在Worker節點可以訪問到的位置

#!/bin/sh
echo "Running shell script"
while read LINE; do
   echo ${LINE}    
done

scala:

package test

import org.apache.spark.SparkConf
import org.apache.spark.SparkContext

object PipeTest {

  def main(args: Array[String]) {
    val sparkConf = new SparkConf().setAppName("pipe Test")
    val sc = new SparkContext(sparkConf)
    val data = List("hi", "hello", "how", "are", "you")
    val dataRDD = sc.makeRDD(data)
    val scriptPath = "/home/gt/spark/bin/echo.sh"
    val pipeRDD = dataRDD.pipe(scriptPath)
    print("! "+pipeRDD.count()+" !!!")
    //輸出6,如果是sc.makeRDD(data,2)輸出7
    sc.stop()
  }
}

雙Value類型交互

1. union(otherDataset)

作用:對源RDD和參數RDD求並集後返回一個新的RDD,有重複的也不會消除哦

    val listRDD1: RDD[Int] = sc.makeRDD(1 to 6)
    val listRDD2: RDD[Int] = sc.makeRDD(5 to 8)
    val totalRDD: RDD[Int] = listRDD1.union(listRDD2)
    //  1234565678
2. subtract (otherDataset)

計算差的一種函數,去除兩個RDD中相同的元素,不同的RDD將保留下來

   val listRDD1: RDD[Int] = sc.makeRDD(1 to 5)
    val listRDD2: RDD[Int] = sc.makeRDD(2 to 4)
    val totalRDD: RDD[Int] = listRDD1.subtract(listRDD2) // 在1 不在2 
3. intersection(otherDataset)

作用:對源RDD和參數RDD求交集後返回一個新的RDD

   val listRDD1: RDD[Int] = sc.makeRDD(1 to 5)
    val listRDD2: RDD[Int] = sc.makeRDD(2 to 4)
    val totalRDD: RDD[Int] = listRDD1.intersection(listRDD2) // 在1且在2 
4. cartesian(OtherDataset)

作用:笛卡爾積(儘量避免使用)
在這裏插入圖片描述

5. zip(OtherDataset)

作用:將兩個RDD組合成Key/Value形式的RDD,這裏默認兩個RDD的partition數量以及元素數量都相同,否則會拋出異常。

    val listRDD1: RDD[Int] = sc.makeRDD(1 to 3,3)
    val listRDD2: RDD[Int] = sc.makeRDD(Array("a","b","c"),3)
    val zipRDD: RDD[(Int, Int)] = listRDD1.zip(listRDD2)

Key-Value型

RDD集合中的每一個item 都是KV形式的。裏面用到了隱式轉換,在makeRDD後根據參數的不同有不同的RDD子類實現。比如KV的RDD

PairRDDFunctions
1. partitionBy

作用:對pairRDD進行分區操作,如果原有的partionRDD和現有的partionRDD是一致的話就不進行分區, 否則會生成ShuffleRDD,即會產生shuffle過程。


package com.sowhat

import org.apache.log4j.{Level, Logger}
import org.apache.spark.rdd.RDD
import org.apache.spark.{Partitioner, SparkConf, SparkContext}

/**
 * @author sowhat
 * @create 2020-06-12 15:55
 */
object WordCountLocal {
  def main(args: Array[String]): Unit = {
    Logger.getLogger("org").setLevel(Level.OFF)
    Logger.getLogger("akka").setLevel(Level.OFF)
    //1.創建SparkConf並設置App名稱
    val conf: SparkConf = new SparkConf().setAppName("WC").setMaster("local[*]")
    //2.創建SparkContext,該對象是提交Spark App的入口
    val sc = new SparkContext(conf)
    // 縮減分區可以認爲簡單對分區合併,沒有涉及到shuffle
    val rdd = sc.parallelize(Array((1,"aaa"),(2,"bbb"),(3,"ccc"),(4,"ddd")),4)
    println(rdd.partitions.size)
    var rdd2 = rdd.partitionBy(new org.apache.spark.HashPartitioner(2))
    rdd2 = rdd.partitionBy(new MyPartition(2))
    println(rdd2.partitions.size)
    sc.stop()
  }
}
class MyPartition(num:Int) extends Partitioner {
  override def numPartitions: Int = {
    num
  }

  override def getPartition(key: Any): Int = {
    1
  }
}
2. groupByKey

作用:groupByKey也是對每個key進行操作,但只生成一個seq。

    val words = Array("one","two","two","three","three","three")
    val wordPairsRDD: RDD[(String, Int)] = sc.makeRDD(words).map((_,1))
    val group: RDD[(String, Iterable[Int])] = wordPairsRDD.groupByKey()
    val res: RDD[(String, Int)] = group.map(t=>(t._1,t._2.sum))
    res.collect().foreach(println)
    res.saveAsTextFile("output")
    ---
    (two,2)
    (one,1)
    (three,3)
3. reduceByKey(func, [numTasks])

在一個(K,V)的RDD上調用,返回一個(K,V)的RDD,使用指定的reduce函數,將相同key的值聚合到一起,reduce任務的個數可以通過第二個可選的參數來設置。

  val rdd: RDD[(String, Int)] = sc.makeRDD(List(("so",1),("so",2),("what",3),("what",5)))
    val reduce: RDD[(String, Int)] = rdd.reduceByKey(_ + _)
    reduce.collect().foreach(println)
    ---
    (what,8)
    (so,3)
4. reduceByKey和groupByKey的區別
  1. reduceByKey:按照key進行聚合,在shuffle之前有combine(預聚合)操作,返回結果是RDD[k,v].
  2. groupByKey:按照key進行分組,直接進行shuffle。
  3. 開發指導:reduceByKey比groupByKey,建議使用。但是需要注意是否會影響業務邏輯。在對大數據進行復雜計算時,reduceByKey優於groupByKey。

另外,如果僅僅是group處理,那麼以下函數應該優先於 groupByKey :
  (1)、combineByKey 組合數據,但是組合之後的數據類型與輸入時值的類型不一樣。
  (2)、foldByKey合併每一個 key 的所有值,在級聯函數和“零值”中使用。
細節分析如下 :

val words = Array("one", "two", "two", "three", "three", "three")  
  
val wordPairsRDD = sc.parallelize(words).map(word => (word, 1))  
  
val wordCountsWithReduce = wordPairsRDD.reduceByKey(_ + _)  
  
val wordCountsWithGroup = wordPairsRDD.groupByKey().map(t => (t._1, t._2.sum))  

wordCountsWithReducewordCountsWithGroup是完全一樣的,但是,它們的內部運算過程是不同的。

  1. 當採用reduceByKey時,Spark可以在每個分區移動數據之前將待輸出數據與一個共用的key結合。藉助下圖可以理解在reduceByKey裏究竟發生了什麼。 注意在數據對被搬移前同一機器上同樣的key是怎樣被組合的(reduceByKey中的lamdba函數)。然後lamdba函數在每個區上被再次調用來將所有值reduce成一個最終結果。整個過程如下:
    在這裏插入圖片描述
  2. 當採用groupByKey時,由於它不接收函數,spark只能先將所有的鍵值對(key-value pair)都移動,這樣的後果是集羣節點之間的開銷很大,導致傳輸延時。整個過程如下:在這裏插入圖片描述
5. aggregateByKey

參數:

  def aggregateByKey[U: ClassTag](zeroValue: U)(seqOp: (U, V) => U,
      combOp: (U, U) => U): RDD[(K, U)] = self.withScope {
    aggregateByKey(zeroValue, defaultPartitioner(self))(seqOp, combOp)
  }
  // 注意這裏U 類型有個ClassTag 有個運行時候的反射類型推斷。所以不用寫類型在後面參數中。
  1. 作用:在KV對的RDD中,按key將value進行分組合並,合併時,將每個value和初始值作爲seq函數的參數,進行計算,返回的結果作爲一個新的KV對,然後再將結果按照key進行合併,最後將每個分組的value傳遞給combine函數進行計算(先將前兩個value進行計算,將返回結果和下一個value傳給combine函數,以此類推),將key與計算結果作爲一個新的kv對輸出。
  2. 參數解釋

(1)zeroValue:給每一個分區中的每一個key一個初始值;
(2)seqOp:函數用於在每一個分區中用初始值逐步迭代value;
(3)combOp:函數用於合併每個分區中的結果。

  1. 需求:創建一個pairRDD,取出每個分區相同key對應值的最大值,然後相加。python版解釋
    在這裏插入圖片描述
    val rdd = sc.parallelize(List(("a", 3), ("a", 2), ("c", 4), ("b", 3), ("c", 6), ("c", 8)), 2)
    val aggRDD: RDD[(String, Int)] = rdd.aggregateByKey(0)(math.max(_, _), _ + _)
    aggRDD.collect().foreach(println)
    ---
    (b,3)
    (a,3)
    (c,12)
6. foldByKey
  1. 作用:aggregateByKey的簡化操作,seqop和combop相同
  2. 需求:創建一個pairRDD,計算相同key對應值的相加結果
  def foldByKey(zeroValue: V)(func: (V, V) => V): RDD[(K, V)] = self.withScope {
    foldByKey(zeroValue, defaultPartitioner(self))(func)
  }

代碼:

    val rdd = sc.parallelize(List(("a", 3), ("a", 2), ("c", 4), ("b", 3), ("c", 6), ("c", 8)), 2)
    val flodRDD: RDD[(String, Int)] = rdd.foldByKey(0)(_ + _)
    flodRDD.collect().foreach(println)
---
(b,3)
(a,5)
(c,18)
---
val intRDD = sc.parallelize(List((1, 3), (1, 2), (1, 4), (2, 3), (3, 6), (3, 8)), 3)
val intFoldRDD = rdd.foldByKey(0)(_ + _)
 //  Array((3,14), (1,9), (2,3))
7. combineByKey[C]

原型:

  def combineByKey[C](
  // 注意這裏的C 就是個範型,我們是不知道這個C是什麼類型的,所以需要在調用的時候指明C的類型。
      createCombiner: V => C,
      // 一個分區中 每一個key下面的 value賦予個初始值
      mergeValue: (C, V) => C, 
      // 相當於在一個分區中把 同一個K下面 不同value 值進行相同操作,然後還要對出現次數操作
      mergeCombiners: (C, C) => C,
      //  不同分區相同key的 數據進行彙總
      numPartitions: Int): RDD[(K, C)] = self.withScope {
    combineByKeyWithClassTag(createCombiner, mergeValue, mergeCombiners, numPartitions)(null)
  }
  1. 作用:對相同K,把V合併成一個集合。
  2. 參數描述:
  1. createCombiner: combineByKey() 會遍歷分區中的所有元素,因此每個元素的鍵要麼還沒有遇到過,要麼就和之前的某個元素的鍵相同。如果這是一個新的元素,combineByKey()會使用一個叫作createCombiner()的函數來創建那個鍵對應的累加器的初始值
  2. mergeValue: 如果這是一個在處理當前分區之前已經遇到的鍵,它會使用mergeValue()方法將該鍵的累加器對應的當前值與這個新的值進行合併
  3. mergeCombiners: 由於每個分區都是獨立處理的, 因此對於同一個鍵可以有多個累加器。如果有兩個或者更多的分區都有對應同一個鍵的累加器, 就需要使用用戶提供的 mergeCombiners() 方法將各個分區的結果進行合併。
  1. 需求:創建一個pairRDD,根據key計算每種key的均值。(先計算每個key出現的次數以及可以對應值的總和再相除得到結果
  2. 需求分析:
    在這裏插入圖片描述
    val rdd = sc.parallelize(Array(("a", 88), ("b", 95), ("a", 91), ("b", 93), ("a", 95), ("b", 98)), 2)
    val combine: RDD[(String, (Int, Int))] = rdd.combineByKey(
      (_, 1),
      (acc: (Int, Int), v) => (acc._1 + v, acc._2 + 1), // 數值相加, 次數+1,
      (acc1: (Int, Int), acc2: (Int, Int)) => (acc1._1 + acc2._1, acc1._2 + acc2._2) // 不同分區數據的彙總
    )
    combine.collect().foreach(println) // (b,(286,3))  (a,(274,3))

    val result = combine.map { case (key, value) => (key, value._1 / value._2.toDouble) }
    result.collect.foreach(println)
    // (b,95.33333333333333)
    // (a,91.33333333333333)
---
rdd.combineByKey{
	x=>x,
	(x:Int,y)=> x + y,
	(x:Int,y:Int) => x + y
}
理解類型有時候指明 有時候不指明的原因
8. sortByKey([ascending], [numTasks])
  1. 作用:在一個(K,V)的RDD上調用,K必須實現Ordered接口,返回一個按照key進行排序的(K,V)的RDD
  2. 需求:創建一個pairRDD,按照key的正序和倒序進行排序
    val rdd = sc.parallelize(Array((3, "aa"), (6, "cc"), (2, "bb"), (1, "dd")))
    val sortRDD1: RDD[(Int, String)] = rdd.sortByKey(true)
    sortRDD1.collect().foreach(print) // (1,dd)(2,bb)(3,aa)(6,cc)
    println('-' * 20)
    val sortRDD2: RDD[(Int, String)] = rdd.sortByKey(false)
    sortRDD2.collect().foreach(print) // (6,cc)(3,aa)(2,bb)(1,dd)
9. mapValues
  1. 針對於(K,V)形式的類型只對V進行操作
  2. 需求:創建一個pairRDD,並將value添加字符串"1412"
   val listRDD: RDD[(Int, String)] = sc.makeRDD(Array((1,"a"),(2,"b"),(3,"c"),(4,"d")))
    val resultRDD: RDD[(Int, String)] = listRDD.mapValues(_ + "1412")
    resultRDD.collect().foreach(println)
    /**
      * (1,a1412)
      * (2,b1412)
      * (3,c1412)
      * (4,d1412)
      * */
10. join(otherDataset, [numTasks])
  1. 作用:在類型爲(K,V)和(K,W)的RDD上調用,返回一個相同key對應的所有元素對在一起的(K,(V,W))的RDD。可以認爲是mysql的join操作,兩個RDD個數不同時候也允許。不過只顯示同時有的!
  2. 需求:創建兩個pairRDD,並將key相同的數據聚合到一個元組。
    val rdd1: RDD[(Int, String)] = sc.makeRDD(Array((1,"a"),(2,"b"),(3,"c"),(4,"d")))
    val rdd2: RDD[(Int, Int)] = sc.makeRDD(Array((1,1),(2,2),(3,3),(4,4)))
    val result1: RDD[(Int, (String, Int))] = rdd1.join(rdd2)
    result1.collect().foreach(println)
    // (4,(d,4)) (1,(a,1)) (2,(b,2)) (3,(c,3))

    val result2: RDD[(Int, (Int,String))] = rdd2.join(rdd1)
    result2.collect().foreach(println)
    // (4,(4,d)) (1,(1,a)) (2,(2,b)) (3,(3,c))
11. cogroup(otherDataset, [numTasks])
  1. 作用:在類型爲(K,V)和(K,W)的RDD上調用,返回一個(K,(Iterable,Iterable))類型的RDD,兩個RDD不同的時候,會返回(獨有key,(CompactBuffer(),CompactBuffer(獨有V)))
  2. 需求:創建兩個pairRDD,並將key相同的數據聚合到一個迭代器。
    val rdd1: RDD[(Int, String)] = sc.makeRDD(Array((1,"a"),(2,"b"),(3,"c"),(4,"d")))
    val rdd2: RDD[(Int, Int)] = sc.makeRDD(Array((1,1),(2,2),(3,3),(4,4)))
    val coRDD: RDD[(Int, (Iterable[String], Iterable[Int]))] = rdd1.cogroup(rdd2)
    coRDD.collect().foreach(println)
    /**
      * (4,(CompactBuffer(d),CompactBuffer(4)))
      * (1,(CompactBuffer(a),CompactBuffer(1)))
      * (2,(CompactBuffer(b),CompactBuffer(2)))
      * (3,(CompactBuffer(c),CompactBuffer(3)))
      * */

實戰

  1. 數據結構:時間戳,省份,城市,用戶,廣告,中間字段使用空格分割。樣本如下:
1516609143867 6 7 64 16
1516609143869 9 4 75 18
1516609143869 1 7 87 12
  1. 需求: 統計出每一個省份廣告被點擊次數的TOP3

  2. 解題思路:

  1. 先把省 + 廣告 當整體看 點擊數總量。
  2. 再把省當K,廣告 + 點擊數 當V,
  3. 對2 進行groupByKey
  4. 對3 進行排序 取值前三。
package com.sowhat

import org.apache.log4j.{Level, Logger}
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

/**
  * @author sowhat
  * @create 2020-06-12 15:55
  */
object WordCountLocal {
  def main(args: Array[String]): Unit = {
    Logger.getLogger("org").setLevel(Level.OFF)
    Logger.getLogger("akka").setLevel(Level.OFF)
    //1.創建SparkConf並設置App名稱
    val conf: SparkConf = new SparkConf().setAppName("WC").setMaster("local[*]")
    //2.創建SparkContext,該對象是提交Spark App的入口
    val sc = new SparkContext(conf)

    // 時間戳,省份,城市,用戶,廣告,中間字段使用空格分割。
    val line: RDD[String] = sc.textFile("in/agent.log")

    //3.按照最小粒度聚合:((Province,AD),1)
    val provinceAdToOne = line.map { x =>
      val fields: Array[String] = x.split(" ")
      ((fields(1), fields(4)), 1)
    }

    //4.計算每個省中每個廣告被點擊的總數:((Province,AD),sum)
    val provinceAdToSum = provinceAdToOne.reduceByKey(_ + _)

    //5.將省份作爲key,廣告加點擊數爲value:(Province,(AD,sum))
    val provinceToAdSum = provinceAdToSum.map(x => (x._1._1, (x._1._2, x._2)))

    //6.將同一個省份的所有廣告進行聚合(Province,List((AD1,sum1),(AD2,sum2)...))
    val provinceGroup = provinceToAdSum.groupByKey()

    //7.對同一個省份所有廣告的集合進行排序並取前3條,排序規則爲廣告點擊總數
    val provinceAdTop3 = provinceGroup.mapValues { x =>
      x.toList.sortWith((x, y) => x._2 > y._2).take(3)
    }

    //8.將數據拉取到Driver端並打印
    provinceAdTop3.collect().foreach(println)

    /**
      * (4,List((12,25), (2,22), (16,22)))
      * (8,List((2,27), (20,23), (11,22)))
      * (6,List((16,23), (24,21), (22,20)))
      * (0,List((2,29), (24,25), (26,24)))
      * (2,List((6,24), (21,23), (29,20)))
      * (7,List((16,26), (26,25), (1,23)))
      * (5,List((14,26), (21,21), (12,21)))
      * (9,List((1,31), (28,21), (0,20)))
      * (3,List((14,28), (28,27), (22,25)))
      * (1,List((3,25), (6,23), (5,22)))
      */
    sc.stop()
  }
}

Action

前面說到的都是邏輯轉換算子,接下來說下行動算子,它是用來觸發RDD的計算,得到RDD的相關計算結果或者將RDD保存的文件系統中。行動算子一定調用了sc.runJob,而邏輯算子一般返回一個新的MapPartitionsRDD

1. reduce(func)
  1. 作用:通過func函數聚集RDD中的所有元素,先聚合分區內數據,再聚合分區間數據。
  2. 需求:創建一個RDD,將所有元素聚合得到結果。
    val rdd1: RDD[Int] = sc.makeRDD(1 to 10 ,2)
    val result: Int = rdd1.reduce(_ + _)
    println(result) // 55
2. collect()
  1. 作用:在驅動程序中,以數組的形式返回數據集的所有元素。
  2. 需求:創建一個RDD,並將RDD內容收集到Driver端打印
 val rdd1: RDD[Int] = sc.makeRDD(1 to 10 ,2)
    println(rdd1) // ParallelCollectionRDD[7] at makeRDD at WordCountLocal.scala:63
    println("-"*10)
    println(rdd1.collect()) // 返回一個Array   [I@322ec9
    val ints: Array[Int] = rdd1.collect() //此時數據已經返回到Driver了。
    ints.foreach(println) // 此時是在driver中執行的!
    val str: String = rdd1.collect().mkString(",")
3. count()
  1. 作用:返回RDD中元素的個數
  2. 需求:創建一個RDD,統計該RDD的條數
    val rdd1: RDD[Int] = sc.makeRDD(1 to 10 ,2)
    val num: Long = rdd1.count()
4. first()
  1. 作用:返回RDD中的第一個元素
  2. 需求:創建一個RDD,返回該RDD中的第一個元素
    val rdd1: RDD[Int] = sc.makeRDD(1 to 10, 2)
    val fir: Int = rdd1.first() // 第一個元素
5. take(num:Int)
  1. 作用:返回一個由RDD的前n個元素組成的數組
  2. 需求:創建一個RDD,統計該RDD的條數
    val rdd: RDD[Int] = sc.parallelize(Array(2, 5, 4, 6, 8, 3))
    val ints: Array[Int] = rdd.take(4) // 獲取前N 個數據,以Array 形式返回。
    val str: String = ints.mkString(",")
6. takeOrdered(n:Int)
  1. 作用:返回該RDD排序後的前n個元素組成的數組
  2. 需求:創建一個RDD,統計該RDD的條數
    val rdd: RDD[Int] = sc.parallelize(Array(2, 5, 4, 6, 8, 3))
    val ints: Array[Int] = rdd.takeOrdered(4) // 獲取前N 個數據,以Array 形式返回。
    val str: String = ints.mkString(",") //  2,3,4,5
    println(str)
7. aggregate
  1. 參數:(zeroValue: U)(seqOp: (U, T) ⇒ U, combOp: (U, U) ⇒ U)
  2. 作用:aggregate函數將每個分區裏面的元素通過seqOp和初始值進行聚合,然後用combine函數將每個分區的結果和初始值(zeroValue)進行combine操作。這個函數最終返回的類型不需要和RDD中元素類型一致。分區間合併的時候也要用到zeroValue
  3. 需求:創建一個RDD,將所有元素相加得到結果
  val rdd: RDD[Int] = sc.parallelize(Array(2, 5, 4, 6, 8, 3),3)
    val i: Int = rdd.aggregate(0)(math.max(_,_),_ + _) //  5  +  6 +  8
    println(i) // 19 
    ---
    val rdd1 = sc.makeRDD(1 to 10,2)
    rdd1.aggregate(0)(_ + _,_ + _)// 55
    rdd1.aggregate(10)(_ + _,_ + _) // 55 + 10 + 10 + 10 ,兩個分區內一個分區見
8. fold(num)(func)
  1. 作用:摺疊操作,aggregate的簡化操作,seqop和combop一樣。原理通上。
  2. 需求:創建一個RDD,將所有元素相加得到結果
    val rdd: RDD[Int] = sc.parallelize(Array(2, 5, 4, 6, 8, 3),3)
    val i: Int = rdd.fold(0)(_ + _)
    println(i) // 28
9. saveAsTextFile(path)

作用:將數據集的元素以textfile的形式保存到HDFS文件系統或者其他支持的文件系統,對於每個元素,Spark將會調用toString方法,將它裝換爲文件中的文本

10. saveAsSequenceFile(path)

作用:將數據集中的元素以Hadoop sequencefile的格式保存到指定的目錄下,可以使HDFS或者其他Hadoop支持的文件系統。

11. saveAsObjectFile(path)

作用:用於將RDD中的元素序列化成對象,存儲到文件中。

12. countByKey()
  1. 作用:針對(K,V)類型的RDD,返回一個(K,Int)的map,表示每一個key對應的元素個數。
  2. 需求:創建一個PairRDD,統計每種key的個數
    val rdd = sc.parallelize(List((1,3),(1,2),(1,4),(2,3),(3,6),(3,8)),3)
    val intToLong: collection.Map[Int, Long] = rdd.countByKey()
    intToLong.foreach(println)
    println("-"*10)
    // https://blog.csdn.net/weixin_42181200/article/details/80696369
    // countByValue把一個KV看成一個整體了
    val tupleToLong: collection.Map[(Int, Int), Long] = rdd.countByValue()
    tupleToLong.foreach(println)
----------
(3,2)
(1,3)
(2,1)
----------
((3,6),1)
((1,4),1)
((1,3),1)
((2,3),1)
((1,2),1)
((3,8),1)
13. foreach(func)
  1. 作用:在數據集的每一個元素上,運行函數func進行更新。
  2. 需求:創建一個RDD,對每個元素進行打印
rdd = sc.makeRDD(1 to 5,2)
rdd.foreach(println) // 這個代碼是在 executor中執行的 跟 collect後再foreach位置不同。

rdd.foreach{
  case i =>{println(i)} //跟上面一樣 執行在executor中!
}

RDD中的函數傳遞

在實際開發中我們往往需要自己定義一些對於RDD的操作,那麼此時需要主要的是,初始化工作是在Driver端進行的,而實際運行程序是在Executor端進行的,這就涉及到了跨進程通信,是需要序列化的。下面我們看幾個例子:

1. 傳遞方法
/**
  * @author sowhat
  * @create 2020-06-15 17:40
  */
class Search() {

  def isMatch(s: String) = s.contains("H")

  //過濾出包含字符串的RDD 需考慮序列化
  def getMatch1(rdd: RDD[String]): RDD[String] = rdd.filter(isMatch)

  //過濾出包含字符串的RDD 這個不需要考慮序列化
  def getMatche2(rdd: RDD[String]): RDD[String] = rdd.filter(x => x.contains("H"))


}

object SeriTest {
  def main(args: Array[String]): Unit = {
    Logger.getLogger("org").setLevel(Level.OFF)
    Logger.getLogger("akka").setLevel(Level.OFF)
    //1.創建SparkConf並設置App名稱
    val conf: SparkConf = new SparkConf().setAppName("SeriTest").setMaster("local[*]")
    //2.創建SparkContext,該對象是提交Spark App的入口
    val sc = new SparkContext(conf)
    val search = new Search()
    val rdd: RDD[String] = sc.parallelize(Array("Hadoop","Spark","Hive","Sowhat"))
    val unit: RDD[String] = search.getMatch1(rdd)
    unit.collect().foreach(println)

   /**
     * Exception in thread "main" org.apache.spark.SparkException: Task not serializable
     * Caused by: java.io.NotSerializableException: com.sowhat.Search
     * */
  }
}

原因:

在getMatch1 這個方法中所調用的方法isMatch()是定義在Search這個類中的,實際上調用的是this. isMatch(),this表示Search這個類的對象,程序在運行過程中需要將Search對象序列化以後傳遞到Executor端。

解決辦法:

使類繼承scala.Serializable即可。
class Search() extends Serializable{…}

2. 傳遞屬性
class Search() {
  val query = "H"

  def isMatch(s: String) = s.contains(query)

  //過濾出包含字符串的RDD 需考慮序列化
  def getMatch1(rdd: RDD[String]): RDD[String] = rdd.filter(isMatch)

  //過濾出包含字符串的RDD 這個不需要考慮序列化
  def getMatche2(rdd: RDD[String]): RDD[String] = rdd.filter(x => x.contains(query))

}

object SeriTest {
  def main(args: Array[String]): Unit = {
    Logger.getLogger("org").setLevel(Level.OFF)
    Logger.getLogger("akka").setLevel(Level.OFF)
    //1.創建SparkConf並設置App名稱
    val conf: SparkConf = new SparkConf().setAppName("SeriTest").setMaster("local[*]")
    //2.創建SparkContext,該對象是提交Spark App的入口
    val sc = new SparkContext(conf)
    val search = new Search()
    val rdd: RDD[String] = sc.parallelize(Array("Hadoop","Spark","Hive","Sowhat"))
    val unit: RDD[String] = search.getMatche2(rdd)
    unit.collect().foreach(println)
  }
}

此時還是會報錯,說我們任務中沒有序列化,因爲我們傳遞的屬性中的query是 Search的對象,所以我們必須解決這個調用類對象屬性序列化問題,
解決方法一:

class Search() extends Serializable 繼承序列化類實現

解決方法二:
將類變量query賦值給局部變量,修改getMatche2爲

  def getMatche2(rdd: RDD[String]): RDD[String] = {
    val query_ : String = this.query//將類變量賦值給局部變量
    rdd.filter(x => x.contains(query_))
  }

RDD依賴關係

在這裏插入圖片描述
RDD只支持粗粒度轉換,即在大量記錄上執行的單個操作。將創建RDD的一系列Lineage(血統)記錄下來,以便恢復丟失的分區。RDD的Lineage會記錄RDD的元數據信息和轉換行爲,當該RDD的部分分區數據丟失時,它可以根據這些信息來重新運算和恢復丟失的數據分區。比如上圖的ABCD四個任務存在依賴關係然後在不同的executor中,需要記錄其依賴關係來保證系統穩定性。
在這裏插入圖片描述

scala> val listRDD = sc.makeRDD(List(1,2,3,4,4,3,2,5,4))
listRDD: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[1] at makeRDD at <console>:24

scala> val mapRDD = listRDD.map((_,1))
mapRDD: org.apache.spark.rdd.RDD[(Int, Int)] = MapPartitionsRDD[2] at map at <console>:26

scala> val resultRDD = mapRDD.reduceByKey( _ + _)
resultRDD: org.apache.spark.rdd.RDD[(Int, Int)] = ShuffledRDD[3] at reduceByKey at <console>:28

scala> resultRDD.toDebugString 
res0: String =
(16) ShuffledRDD[3] at reduceByKey at <console>:28 []
 +-(16) MapPartitionsRDD[2] at map at <console>:26 []
    |   ParallelCollectionRDD[1] at makeRDD at <console>:24 []
	這裏是有一個任務調度依賴的記錄的

scala> listRDD.toDebugString
res1: String = (16) ParallelCollectionRDD[1] at makeRDD at <console>:24 []

scala> mapRDD.toDebugString
res2: String =
(16) MapPartitionsRDD[2] at map at <console>:26 []
 |   ParallelCollectionRDD[1] at makeRDD at <console>:24 []

scala> resultRDD.dependencies
res3: Seq[org.apache.spark.Dependency[_]] = List(org.apache.spark.ShuffleDependency@28488f2e)


scala> listRDD.dependencies
res5: Seq[org.apache.spark.Dependency[_]] = List()

scala> mapRDD.dependencies
res6: Seq[org.apache.spark.Dependency[_]] = List(org.apache.spark.OneToOneDependency@ce51a54)

注意:RDD和它依賴的父RDD(s)的關係有兩種不同的類型,即窄依賴(narrow dependency)和寬依賴(wide dependency)。

1. 窄依賴

窄依賴指的是每一個父RDD的Partition最多被子RDD的一個Partition使用,窄依賴我們形象的比喻爲獨生子女
在這裏插入圖片描述

2. 寬依賴

寬依賴指的是多個子RDD的Partition會依賴同一個父RDD的Partition,會引起shuffle,你可以稱爲shuffle依賴,總結:寬依賴我們形象的比喻爲超生。
在這裏插入圖片描述

3. DAG

DAG(Directed Acyclic Graph)叫做有向無環圖,原始的RDD通過一系列的轉換就就形成了DAG,根據RDD之間的依賴關係的不同將DAG劃分成不同的Stage,對於窄依賴,partition的轉換處理在Stage中完成計算。對於寬依賴,由於有Shuffle的存在,只能在parent RDD處理完成後,才能開始接下來的計算,因此寬依賴是劃分Stage的依據
在這裏插入圖片描述
ps: 一般是從後往前推斷stage,並且 Stage個數 = 1 + shuffle個數,重點理解下上圖。

4. 任務劃分(面試重點)

RDD任務切分中間分爲:ApplicationJobStageTask

  1. Application:初始化一個SparkContext即生成一個Application,也就是一個AppMaster。
  2. Job:一個Action算子就會生成一個Job
  3. Stage:根據RDD之間的依賴關係的不同將Job劃分成不同的Stage,遇到一個寬依賴則劃分一個Stage。
    在這裏插入圖片描述
  4. Task:Stage是一個TaskSet,將Stage劃分的結果發送到不同的Executor執行即爲一個Task。TaskSet的個數要看接受方有幾個RDD。
    注意:Application == > Job ==> Stage ==> Task每一層都是1對n的關係。
Term(術語) Meaning(解釋)
Application(Spark應用程序) 運行於Spark上的用戶程序,由集羣上的一個driver program(包含SparkContext對象)和多個executor線程組成
Application jar(Spark應用程序JAR包) Jar包中包含了用戶Spark應用程序,如果Jar包要提交到集羣中運行,不需要將其它的Spark依賴包打包進行,在運行時
Driver program 包含main方法的程序,負責創建SparkContext對象
Cluster manager 集羣資源管理器,例如Mesos,Hadoop Yarn
Deploy mode 部署模式,用於區別driver program的運行方式:集羣模式(cluter mode),driver在集羣內部啓動;客戶端模式(client mode),driver進程從集羣外部啓動
Worker node 工作節點, 集羣中可以運行Spark應用程序的節點
Executor Worker node上的進程,該進程用於執行具體的Spark應用程序任務,負責任務間的數據維護(數據在內存中或磁盤上)。不同的Spark應用程序有不同的Executor
Task 運行於Executor中的任務單元,Spark應用程序最終被劃分爲經過優化後的多個任務的集合
Job 由多個任務構建的並行計算任務,具體爲Spark中的action操作,如collect,save等)
Stage 每個job將被拆分爲更小的task集合,這些任務集合被稱爲stage,各stage相互獨立(類似於MapReduce中的map stage和reduce stage),由於它由多個task集合構成,因此也稱爲TaskSet
5. RDD緩存

RDD通過persist方法或cache方法可以將前面的計算結果緩存,默認情況下 persist()會把數據以序列化的形式緩存在 JVM 的堆空間中。 但是並不是這兩個方法被調用時立即緩存,而是觸發後面的action時,該RDD將會被緩存在計算節點的內存中,並供後面重用。

但是並不是這兩個方法被調用時立即緩存,而是觸發後面的action時,該RDD將會被緩存在計算節點的內存中,並供後面重用。
在這裏插入圖片描述
通過查看源碼發現cache最終也是調用了persist方法,不過persist可以設置存儲的等級跟格式,默認的存儲級別都是僅在內存存儲一份,Spark的存儲級別還有好多種,存儲級別在object StorageLevel中定義的。
在這裏插入圖片描述
無非就是在內存中存儲,在內存中存儲兩份, 在磁盤中存儲,在磁盤中存儲兩份。 特別注意下OFF_HEAP是在JVM的堆外申請一個空間來存儲數據。在存儲級別的末尾加上_2來把持久化數據存爲兩份。
在這裏插入圖片描述
緩存有可能丟失,或者存儲存儲於內存的數據由於內存不足而被刪除,RDD的緩存容錯機制保證了即使緩存丟失也能保證計算的正確執行。通過基於RDD的一系列轉換,丟失的數據會被重算,由於RDD的各個Partition是相對獨立的,因此只需要計算丟失的部分即可,並不需要重算全部Partition。
在這裏插入圖片描述

6. RDD CheckPoint

Spark中對於數據的保存除了持久化操作之外,還提供了一種檢查點的機制,檢查點(本質是通過將RDD寫入Disk做檢查點)是爲了通過lineage做容錯的輔助,lineage過長會造成容錯成本過高,這樣就不如在中間階段做檢查點容錯,如果之後有節點出現問題而丟失分區,從做檢查點的RDD開始重做Lineage,就會減少開銷。檢查點通過將數據寫入到HDFS文件系統實現了RDD的檢查點功能
爲當前RDD設置檢查點。該函數將會創建一個二進制的文件,並存儲到checkpoint目錄中,該目錄是用SparkContext.setCheckpointDir()設置的。在checkpoint的過程中,因爲已經磁盤化所以該RDD的所有依賴於父RDD中的信息將全部被移除。對RDD進行checkpoint操作並不會馬上被執行,必須執行Action操作才能觸發。

    sc.setCheckpointDir("output") //  指定生成checkpoint目錄
    val rdd = sc.parallelize(Array("sowhat"))
    val ch: RDD[String] = rdd.map(_.toString + System.currentTimeMillis())
    ch.checkpoint() // 要顯示調用下
    ch.collect().foreach(println)
    Thread.sleep(1000)
    ch.collect().foreach(println) // 從顯示結果來看 好像 在這一次會出發checkpoint
    Thread.sleep(1000)
    ch.collect().foreach(println)

    println("-" * 10)
    println(ch.toDebugString)
---
sowhat1592357507062
sowhat1592357508581 
sowhat1592357508581
----------
(4) MapPartitionsRDD[8] at map at WordCountLocal.scala:62 []
 |  ReliableCheckpointRDD[9] at collect at WordCountLocal.scala:64 [] // 依賴到checkpoint 即可, 

3. 鍵值對RDD數據分區器

Spark目前支持Hash分區和Range分區,用戶也可以自定義分區,Hash分區爲當前的默認分區,Spark中分區器直接決定了RDD中分區的個數、RDD中每條數據經過Shuffle過程屬於哪個分區和Reduce的個數
注意:

  1. 只有Key-Value類型的RDD纔有分區器的,非Key-Value類型的RDD分區器的值是None
  2. 每個RDD的分區ID範圍:0~numPartitions-1,決定這個值是屬於那個分區的。
1. 獲取RDD分區

可以通過使用RDD的partitioner 屬性來獲取 RDD 的分區方式。它會返回一個 scala.Option 對象, 通過get方法獲取其中的值。相關源碼如下:

    val pairs: RDD[(Int, Int)] = sc.makeRDD(Array((1, 1), (2, 2), (3, 3)))
    val partitioner: Option[Partitioner] = pairs.partitioner
    println(partitioner) // None
    val pared: RDD[(Int, Int)] = pairs.partitionBy(new org.apache.spark.HashPartitioner(2))
    println(pared.partitioner) // Some(org.apache.spark.HashPartitioner@2)
    println(pared.partitioner.get) // org.apache.spark.HashPartitioner@2  分區方式獲取
    val partitions: Int = pared.getNumPartitions
    println(pared.glom().collect().foreach(println)) //
	
---  源碼部分如下:
class HashPartitioner(partitions: Int) extends Partitioner {
  require(partitions >= 0, s"Number of partitions ($partitions) cannot be negative.")

  def numPartitions: Int = partitions

  def getPartition(key: Any): Int = key match { // 對於每個KV數據要判斷該數據需要存儲在哪個partition中
    case null => 0
    case _ => Utils.nonNegativeMod(key.hashCode, numPartitions)
  }

  override def equals(other: Any): Boolean = other match {
    case h: HashPartitioner =>
      h.numPartitions == numPartitions
    case _ =>
      false
  }

  override def hashCode: Int = numPartitions
}

  def nonNegativeMod(x: Int, mod: Int): Int = {
    val rawMod = x % mod
    rawMod + (if (rawMod < 0) mod else 0)
  }
2. Hash分區

HashPartitioner分區的原理:對於給定的key,計算其hashCode,併除以分區的個數取餘,如果餘數小於0,則用餘數+分區的個數(否則加0),最後返回的值就是這個key所屬的分區ID。
使用Hash分區的實操。

    val nopair: RDD[(Int, Int)] = sc.parallelize(List((1, 3), (1, 2), (2, 4), (2, 3), (3, 6), (3, 8)), 8)
    nopair.mapPartitionsWithIndex((index, iter) => {
      Iterator(index.toString + " : " + iter.mkString("|"))
    }).collect.foreach(println)
    // 對數據進行分區, 一共8個區域,然後
    val hashpair: RDD[(Int, Int)] = nopair.partitionBy(new org.apache.spark.HashPartitioner(7))
    println(hashpair.partitions.size) // 數據分區 一共7個,
    // 以分區未單位 處理數據,
    hashpair.mapPartitions(iter => Iterator(iter.length)).collect().foreach(print) // 0222000,注意看partitions的輸入跟輸出
    hashpair.saveAsTextFile("output")
-----
0 : 
1 : (1,3)
2 : (1,2)
3 : (2,4)
4 : 
5 : (2,3)
6 : (3,6)
7 : (3,8)
7
0222000
3. Ranger分區

HashPartitioner分區弊端:可能導致每個分區中數據量的不均勻,極端情況下會導致某些分區擁有RDD的全部數據。
RangePartitioner作用:將一定範圍內的數映射到某一個分區內,儘量保證每個分區中數據量的均勻,而且分區與分區之間是有序的,一個分區中的元素肯定都是比另一個分區內的元素小或者大,但是分區內的元素是不能保證順序的。簡單的說就是將一定範圍內的數映射到某一個分區內。實現過程爲:

  1. 先從整個RDD中抽取出樣本數據,將樣本數據排序,計算出每個分區的最大key值,形成一個Array[KEY]類型的數組變量rangeBounds;
  2. 判斷key在rangeBounds中所處的範圍,給出該key值在下一個RDD中的分區id下標;該分區器要求RDD中的KEY類型必須是可以排序的。
4. 自定義分區

要實現自定義的分區器,你需要繼承 org.apache.spark.Partitioner 類並實現下面三個方法。

  1. numPartitions: Int:返回創建出來的分區數
  2. getPartition(key: Any): Int:返回給定鍵的分區編號(0到numPartitions-1)。
  3. equals():Java 判斷相等性的標準方法。這個方法的實現非常重要,Spark 需要用這個方法來檢查你的分區器對象是否和其他分區器實例相同,這樣 Spark 纔可以判斷兩個 RDD 的分區方式是否相同。
    需求:將相同後綴的數據寫入相同的文件,通過將相同後綴的數據分區到相同的分區並保存輸出來實現。
class CustomerPartitioner(numParts: Int) extends org.apache.spark.Partitioner {

  //覆蓋分區數
  override def numPartitions: Int = numParts

  //覆蓋分區號獲取函數
  override def getPartition(key: Any): Int = {
    val ckey: String = key.toString
    ckey.substring(ckey.length - 1).toInt % numParts
  }
}
---
    val data: RDD[(Int, Int)] = sc.parallelize(Array((1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6)))
    val parRDD: RDD[(Int, Int)] = data.partitionBy(new CustomerPartitioner(2))
    val result: RDD[(Int, (Int, Int))] = parRDD.mapPartitionsWithIndex((index, items) => items.map((index, _)))
    result.collect.foreach(println)
---
(0,(2,2))
(0,(4,4))
(0,(6,6))
(1,(1,1))
(1,(3,3))
(1,(5,5))

4. 數據讀取與保存

Spark的數據讀取及數據保存可以從兩個維度來作區分:文件格式以及文件系統。文件格式分爲:Text文件、Json文件、Csv文件、Sequence文件以及Object文件;
文件系統分爲:本地文件系統HDFSHBASE以及數據庫

文件類型讀取保存

1. Text文件
  1. 數據讀取:textFile(String)
scala> val hdfsFile = sc.textFile("hdfs://hadoop102:9000/fruit.txt")
hdfsFile: org.apache.spark.rdd.RDD[String] = hdfs://hadoop102:9000/fruit.txt MapPartitionsRDD[21] at textFile at <console>:24
  1. 數據保存: saveAsTextFile(String)
scala> hdfsFile.saveAsTextFile("/fruitOut")
2. Json文件

如果JSON文件中每一行就是一個JSON記錄,那麼可以通過將JSON文件當做文本文件來讀取,然後利用相關的JSON庫對每一條數據進行JSON解析。
注意:使用RDD讀取JSON文件處理很複雜,同時SparkSQL集成了很好的處理JSON文件的方式,所以應用中多是採用SparkSQL處理JSON文件

  1. 導入解析json所需的包
scala> import scala.util.parsing.json.JSON
  1. 上傳json文件到HDFS
[atguigu@hadoop102 spark]$ hadoop fs -put ./examples/src/main/resources/people.json /
  1. 讀取文件
scala> val json = sc.textFile("/people.json")
json: org.apache.spark.rdd.RDD[String] = /people.json MapPartitionsRDD[8] at textFile at <console>:24
  1. 解析json數據
scala> val result  = json.map(JSON.parseFull)
result: org.apache.spark.rdd.RDD[Option[Any]] = MapPartitionsRDD[10] at map at <console>:27
  1. 打印
scala> result.collect
res11: Array[Option[Any]] = Array(Some(Map(name -> Michael)), Some(Map(name -> Andy, age -> 30.0)), Some(Map(name -> Justin, age -> 19.0)))
3. Sequence文件

SequenceFile文件是Hadoop用來存儲二進制形式的key-value對而設計的一種平面文件(Flat File)。Spark 有專門用來讀取 SequenceFile 的接口。在 SparkContext 中,可以調用 sequenceFile[ keyClass, valueClass]。
注意:SequenceFile文件只針對PairRDD

  1. 創建一個RDD
scala> val rdd = sc.parallelize(Array((1,2),(3,4),(5,6)))
rdd: org.apache.spark.rdd.RDD[(Int, Int)] = ParallelCollectionRDD[13] at parallelize at <console>:24
  1. 將RDD保存爲Sequence文件
scala> rdd.saveAsSequenceFile("file:///opt/module/spark/seqFile")
  1. 查看該文件
[atguigu@hadoop102 seqFile]$ pwd
/opt/module/spark/seqFile
[atguigu@hadoop102 seqFile]$ ll
總用量 8
-rw-r--r-- 1 atguigu atguigu 108 109 10:29 part-00000
-rw-r--r-- 1 atguigu atguigu 124 109 10:29 part-00001
-rw-r--r-- 1 atguigu atguigu   0 109 10:29 _SUCCESS
[atguigu@hadoop102 seqFile]$ cat part-00000
SEQ org.apache.hadoop.io.IntWritable org.apache.hadoop.io.IntWritableط
  1. 讀取Sequence文件
scala> val seq = sc.sequenceFile[Int,Int]("file:///opt/module/spark/seqFile")
seq: org.apache.spark.rdd.RDD[(Int, Int)] = MapPartitionsRDD[18] at sequenceFile at <console>:24
  1. 打印讀取後的Sequence文件
scala> seq.collect
res14: Array[(Int, Int)] = Array((1,2), (3,4), (5,6))
4. 對象文件

對象文件是將對象序列化後保存的文件,採用Java的序列化機制。可以通過objectFile k,v 函數接收一個路徑,讀取對象文件,返回對應的 RDD,也可以通過調用saveAsObjectFile() 實現對對象文件的輸出。因爲是序列化所以要指定類型。

  1. 創建一個RDD
scala> val rdd = sc.parallelize(Array(1,2,3,4))
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[19] at parallelize at <console>:24
  1. 將RDD保存爲Object文件
scala> rdd.saveAsObjectFile("file:///opt/module/spark/objectFile")
  1. 查看該文件
[atguigu@hadoop102 objectFile]$ pwd
/opt/module/spark/objectFile
[atguigu@hadoop102 objectFile]$ ll
總用量 8
-rw-r--r-- 1 atguigu atguigu 142 109 10:37 part-00000
-rw-r--r-- 1 atguigu atguigu 142 109 10:37 part-00001
-rw-r--r-- 1 atguigu atguigu   0 109 10:37 _SUCCESS
[atguigu@hadoop102 objectFile]$ cat part-00000 
SEQ!org.apache.hadoop.io.NullWritable"org.apache.hadoop.io.BytesWritableW@`l
  1. 讀取Object文件
scala> val objFile = sc.objectFile[Int]("file:///opt/module/spark/objectFile")
objFile: org.apache.spark.rdd.RDD[Int] = MapPartitionsRDD[31] at objectFile at <console>:24
  1. 打印讀取後的Sequence文件
scala> objFile.collect
res19: Array[Int] = Array(1, 2, 3, 4)

文件系統類數據讀取與保存

1. HDFS

Spark的整個生態系統與Hadoop是完全兼容的,所以對於Hadoop所支持的文件類型或者數據庫類型,Spark也同樣支持,另外由於Hadoop的API有新舊兩個版本,所以Spark爲了能夠兼容Hadoop所有的版本,也提供了兩套創建操作接口,對於外部存儲創建操作而言,hadoopRDD和newHadoopRDD是最爲抽象的兩個函數接口,主要包含以下四個參數.

  1. 輸入格式(InputFormat): 制定數據輸入的類型,如TextInputFormat等,新舊兩個版本所引用的版本分別是org.apache.hadoop.mapred.InputFormatorg.apache.hadoop.mapreduce.InputFormat(NewInputFormat)
  2. 鍵類型: 指定[K,V]鍵值對中K的類型
  3. 值類型: 指定[K,V]鍵值對中V的類型
  4. 分區值: 指定由外部存儲生成的RDD的partition數量的最小值,如果沒有指定,系統會使用默認值defaultMinSplits

注意:其他創建操作的API接口都是爲了方便最終的Spark程序開發者而設置的,是這兩個接口的高效實現版本.例如,對於textFile而言,只有path這個指定文件路徑的參數,其他參數在系統內部指定了默認值
5. 在Hadoop中以壓縮形式存儲的數據,不需要指定解壓方式就能夠進行讀取,因爲Hadoop本身有一個解壓器會根據壓縮文件的後綴推斷解壓算法進行解壓.
6. 如果用Spark從Hadoop中讀取某種類型的數據不知道怎麼讀取的時候,上網查找一個使用map-reduce的時候是怎麼讀取這種這種數據的,然後再將對應的讀取方式改寫成上面的hadoopRDD和newAPIHadoopRDD兩個類就行了。

2. MySQL

pom文件 添加依賴:

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.27</version>
</dependency>
package com.sowhat

import java.sql.{Connection, DriverManager, PreparedStatement}

import org.apache.spark.rdd.{JdbcRDD, RDD}
import org.apache.spark.{SparkConf, SparkContext}

/**
  * @author sowhat
  * @create 2020-06-17 13:50
  * spark 跟MySQL之間的 讀寫數據
  *
  */
object MysqlRDD {

  def main(args: Array[String]): Unit = {

    //1.創建spark配置信息
    val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("JdbcRDD")

    //2.創建SparkContext
    val sc = new SparkContext(sparkConf)

    //3.定義連接mysql的參數
    val driver = "com.mysql.jdbc.Driver"
    val url = "jdbc:mysql://hadoop102:3306/rdd"
    val userName = "root"
    val passWd = "000000"

    //創建JdbcRDD
    val rdd = new JdbcRDD(sc, () => {
      Class.forName(driver)
      DriverManager.getConnection(url, userName, passWd)
    },
      "select * from rddtable where id > ? and id < ?", //  mysql 語句
      1, // 從mysql中讀取數據的時候一般劃區域的讀取數據,然後需要獲得數據的起始點位置跟終止點位置
      10,
      1, // 讀取數據後分區數指定,
      r => (r.getInt(1), r.getString(2))
    )
    //打印最後結果
    println(rdd.count())
    rdd.foreach(println)

    // 數據庫連接比較重要,給每一個分區分配一個連接 而不是給每個數據分配一個連接。
    val dataRDD: RDD[(String, Int)] = sc.makeRDD(List(("zhangsan", 20), ("lisi", 30), ("wangsu", 40)))
    dataRDD.foreachPartition(data => {
      Class.forName(driver)
      val connection: Connection = java.sql.DriverManager.getConnection(url, userName, passWd)
      data.foreach { case (username, age) => {
        val sql = "insert into user (name,age) values (?,?)"
        val statement: PreparedStatement = connection.prepareStatement(sql)
        statement.setString(1, username)
        statement.setInt(2, age)
        statement.executeUpdate()
        statement.close()
        }
      }
    }
    )
    sc.stop()
  }
}
3.HBase

由於org.apache.hadoop.hbase.mapreduce.TableInputFormat 類的實現,Spark 可以通過Hadoop輸入格式訪問HBase。這個輸入格式會返回鍵值對數據,
其中鍵的類型爲org.apache.hadoop.hbase.io.ImmutableBytesWritable,而值的類型爲org.apache.hadoop.hbase.client.Result。
Result。

  1. 添加依賴
<dependency>
	<groupId>org.apache.hbase</groupId>
	<artifactId>hbase-server</artifactId>
	<version>1.3.1</version>
</dependency>

<dependency>
	<groupId>org.apache.hbase</groupId>
	<artifactId>hbase-client</artifactId>
	<version>1.3.1</version>
</dependency>
  1. 從HBase讀取數據
package com.sowhat


import org.apache.hadoop.conf.Configuration
import org.apache.hadoop.hbase.{HBaseConfiguration, HColumnDescriptor, HTableDescriptor, TableName}
import org.apache.hadoop.hbase.client.{HBaseAdmin, Put, Result}
import org.apache.hadoop.hbase.io.ImmutableBytesWritable
import org.apache.hadoop.hbase.mapreduce.{TableInputFormat, TableOutputFormat}
import org.apache.hadoop.hbase.util.Bytes
import org.apache.hadoop.mapred.JobConf
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}


/**
  * @author sowhat
  * @create 2020-06-17 14:14
  */
object HBaseSpark {

  def main(args: Array[String]): Unit = {

    //創建spark配置信息
    val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("JdbcRDD")

    //創建SparkContext
    val sc = new SparkContext(sparkConf)

    //構建HBase配置信息
    val conf: Configuration = HBaseConfiguration.create()
    conf.set("hbase.zookeeper.quorum", "hadoop102,hadoop103,hadoop104")
    conf.set(TableInputFormat.INPUT_TABLE, "rddtable")

    //從HBase讀取數據形成RDD
    val hbaseRDD: RDD[(ImmutableBytesWritable, Result)] = sc.newAPIHadoopRDD(
      conf,
      classOf[TableInputFormat],
      classOf[ImmutableBytesWritable],
      classOf[Result])

    val count: Long = hbaseRDD.count()
    println(count)

    //對hbaseRDD進行處理
    hbaseRDD.foreach {
      case (_, result) =>
        val key: String = Bytes.toString(result.getRow)
        val name: String = Bytes.toString(result.getValue(Bytes.toBytes("info"), Bytes.toBytes("name")))
        val color: String = Bytes.toString(result.getValue(Bytes.toBytes("info"), Bytes.toBytes("color")))
        println("RowKey:" + key + ",Name:" + name + ",Color:" + color)
    }

    //關閉連接
    sc.stop()
  }

  def main1(args: Array[String]) {
    //獲取Spark配置信息並創建與spark的連接
    val sparkConf = new SparkConf().setMaster("local[*]").setAppName("HBaseApp")
    val sc = new SparkContext(sparkConf)

    //創建HBaseConf
    val conf = HBaseConfiguration.create()
    val jobConf = new JobConf(conf)
    jobConf.setOutputFormat(classOf[TableOutputFormat])
    jobConf.set(TableOutputFormat.OUTPUT_TABLE, "fruit_spark")

    //構建Hbase表描述器
    val fruitTable = TableName.valueOf("fruit_spark")
    val tableDescr = new HTableDescriptor(fruitTable)
    tableDescr.addFamily(new HColumnDescriptor("info".getBytes))

    //創建Hbase表
    val admin = new HBaseAdmin(conf)
    if (admin.tableExists(fruitTable)) {
      admin.disableTable(fruitTable)
      admin.deleteTable(fruitTable)
    }
    admin.createTable(tableDescr)

    //定義往Hbase插入數據的方法
    def convert(triple: (Int, String, Int)) = {
      val put = new Put(Bytes.toBytes(triple._1))
      put.addImmutable(Bytes.toBytes("info"), Bytes.toBytes("name"), Bytes.toBytes(triple._2))
      put.addImmutable(Bytes.toBytes("info"), Bytes.toBytes("price"), Bytes.toBytes(triple._3))
      (new ImmutableBytesWritable, put)
    }

    //創建一個RDD
    val initialRDD = sc.parallelize(List((1, "apple", 11), (2, "banana", 12), (3, "pear", 13)))

    //將RDD內容寫到HBase
    val localData = initialRDD.map(convert)

    localData.saveAsHadoopDataset(jobConf)
  }
}

Spark 累加器

Spark 三大數據結構
RDD:彈性分佈式數據集
廣播變量: 分佈式只讀共享變量
累加器: 分佈式只寫共享變量
RDD的情況下 若干executor從Driver獲得的都是數據副本,各自操作不受干擾。

累加器用來對信息進行聚合,通常在向 Spark傳遞函數時,比如使用 map() 函數或者用 filter() 傳條件時,可以使用驅動器程序中定義的變量,但是集羣中運行的每個任務都會得到這些變量的一份新的副本,更新這些副本的值也不會影響驅動器中的對應變量。如果我們想實現所有分片處理時更新共享變量的功能,那麼累加器可以實現我們想要的效果。

val dataRDD:RDD[Int] = sc.makeRDD(List(1,2,3,4),2)
val result:Int = dataRDD.reduce(_ + _)
var sum = 0
dataRDD.foreach(i=> sum = sum + i)
println(sum) // 0

在這裏插入圖片描述

需要Spark把數據結構返回到driver中。

val dataRDD:RDD[Int] = sc.makeRDD(List(1,2,3,4),2)
val accumulator:LongAccumulator = sc.LongAccumulator
dataRDD.foreach{
	case i => {
		accumulator.add(i)
	}
}
println("sum = " + accumulator.value) // 10
  1. 工作節點上的任務不能訪問累加器的值。從這些任務的角度來看,累加器是一個只寫變量
  2. 對於要在行動操作中使用的累加器,Spark只會把每個任務對各累加器的修改應用一次。因此,如果想要一個無論在失敗還是重複計算時都絕對可靠的累加器,我們必須把它放在 foreach() 這樣的行動操作中。轉化操作中累加器可能會發生不止一次更新。
自定義累加器

自定義累加器類型的功能在1.X版本中就已經提供了,但是使用起來比較麻煩,在2.0版本後,累加器的易用性有了較大的改進,而且官方還提供了一個新的抽象類:AccumulatorV2來提供更加友好的自定義類型累加器的實現方式。實現自定義類型累加器需要繼承AccumulatorV2並覆寫要求的方法。
需求:實現一個自定義的數值累加器

package com.sowhat
import org.apache.spark.util.AccumulatorV2
class MyAccu extends AccumulatorV2[Int, Int] {

  var sum = 0

  //判斷是否爲空
  override def isZero: Boolean = sum == 0

  //複製
  override def copy(): AccumulatorV2[Int, Int] = {
    val accu = new MyAccu
    accu.sum = this.sum
    accu
  }

  //重置
  override def reset(): Unit = sum = 0

  //累加
  override def add(v: Int): Unit = sum += v

  // 合併
  override def merge(other: AccumulatorV2[Int, Int]): Unit = sum += other.value

  //返回值
  override def value: Int = sum
}

main

package com.sowhat

import org.apache.spark.rdd.RDD
import org.apache.spark.{Accumulator, SparkConf, SparkContext}
object AccuTest {
  def main(args: Array[String]): Unit = {
    //創建SparkConf
    val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("AccuTest")
    //創建SC
    val sc = new SparkContext(sparkConf)
    //創建自定義累加器對象
    val accu = new MyAccu
    //註冊累加器
    sc.register(accu)
    //創建RDD
    val value: RDD[Int] = sc.parallelize(Array(1, 2, 3, 4))
    //在行動算子中對累加器的值進行修改
    value.foreach { x =>
      accu.add(1)
      println(x)
    }
    //打印累加器的值
    println(accu.value)
    //關閉SparkContext
    sc.stop()
  }
}

Spark 廣播變量(調優策略)

如果一個任務有兩個Executor,每個Executor有3個Task,則Driver需要發送6次重要數據到Task,用了廣播變量可以直接將數據傳送到Executor中,並且是隻讀的。

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

  1. 通過對一個類型T的對象調用SparkContext.broadcast創建出一個Broadcast[T]對象,任何可序列化的類型都可以這麼實現。
  2. 通過value屬性訪問該對象的值(在Java中爲value()方法)。
  3. 變量只會被髮到各個節點一次,應作爲只讀值處理(修改這個值不會影響到別的節點)。
    在這裏插入圖片描述
val rdd1 = sc.makeRDD(List((1,"a"),(2,"b"),(3,"c")))

val list  = List((1,1),(2,2),(3,3))

val joinRDD:RDD[(Int,(String,Int))] = rdd1.join(list)
// 會涉及到笛卡爾乘積的過程,耗時大,可以參考下面的 廣播變量的操作

val broadcast:Broadcast[List[(Int,Int)]] = sc.broadcast(list)

val resultRDD:RDD[(Int,(String,Any))] = rdd1.map {
	case(key,value) =>{
		var v2:Any = null 
		for(t <- broadcast.value){
			if(key == t._1){
				v2 = t._2
			}
		}
		(key,(value,v2))
	}
}
resultRDD.foreach(println)

參考

spark運行原理

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章