spark(二)--spark-core---RDD進階知識(圖文詳解,基於IDEA開發)

前言

spark系列教程

spark-core–RDD入門實戰(詳解各大api,基於IDEA開發)

目錄:

RDD函數傳值

RDD依賴關係

RDD緩存

鍵值對RDD分區器

數據的讀取與保存

連接mysql數據庫

RDD累加器

廣播變量

## RDD函數傳值方法 在實際開發中我們往往需要自己定義一些對於RDD的操作,那麼此時需要主要的是,初始化工作是在Driver端進行的,而實際運行程序是在Executor端進行的,這就涉及到了跨進程通信,是需要序列化的。下面我們看個例子:
  • search類:
class  search(query:String) {

  def isMatch(s:String): Boolean ={
    s.contains(query);
  }
  def Match(s:RDD[String]):RDD[String]={
    s.filter(x=>isMatch(x));
  }

}

  • 主函數:
    var conf=new SparkConf().setAppName("WordCount").setMaster("local");
    var sc=new SparkContext(conf);
    sc.setLogLevel("ERROR")
    val rdd: RDD[String] = sc.parallelize(Array("hadoop", "spark", "hive", "atguigu"))

    val s=new search("had");

    val r=s.Match(rdd);

    r.collect().foreach(println);

這個時候運行一下報錯:
在這裏插入圖片描述

  • 問題說明
    在這個方法中所調用的方法isMatch()是定義在Search這個類中的,實際上調用的是this. isMatch(),this表示Search這個類的對象,所以我們需要將search對象傳遞到Executor,需要先將search對象序列化
  • 解決方案
    使類繼承scala.Serializable即可。
    class Search() extends Serializable{…}

RDD依賴關係

1 Lineage
RDD只支持粗粒度轉換,即在大量記錄上執行的單個操作。將創建RDD的一系列Lineage(血統)記錄下來,以便恢復丟失的分區。RDD的Lineage會記錄RDD的元數據信息和轉換行爲,當該RDD的部分分區數據丟失時,它可以根據這些信息來重新運算和恢復丟失的數據分區。
在這裏插入圖片描述

現在以讀取文件爲例:

    val words=sc.textFile("in/word.txt").flatMap(_.split("\t")).map((_,1))

    val count=words.reduceByKey(_+_);

    查看words的Lineage
    println(words.toDebugString);

    查看“count”的Lineage

    println(count.toDebugString);

結果:

(1) MapPartitionsRDD[3] at map at test.scala:17 []
 |  MapPartitionsRDD[2] at flatMap at test.scala:17 []
 |  in/word.txt MapPartitionsRDD[1] at textFile at test.scala:17 []
 |  in/word.txt HadoopRDD[0] at textFile at test.scala:17 []
(1) ShuffledRDD[4] at reduceByKey at test.scala:19 []
 +-(1) MapPartitionsRDD[3] at map at test.scala:17 []
    |  MapPartitionsRDD[2] at flatMap at test.scala:17 []
    |  in/word.txt MapPartitionsRDD[1] at textFile at test.scala:17 []
    |  in/word.txt HadoopRDD[0] at textFile at test.scala:17 []

會發現有層層的依賴關係,每個操作記錄都被記錄了下來,以便於數據的恢復

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

窄依賴
窄依賴指的是每一個父RDD的Partition最多被子RDD的一個Partition使用,窄依賴我們形象的比喻爲獨生子女
在這裏插入圖片描述
寬依賴
寬依賴指的是多個子RDD的Partition會依賴同一個父RDD的Partition,會引起shuffle,總結:寬依賴我們形象的比喻爲超生
在這裏插入圖片描述

DAG
DAG(Directed Acyclic Graph)叫做有向無環圖,原始的RDD通過一系列的轉換就就形成了DAG,根據RDD之間的依賴關係的不同將DAG劃分成不同的Stage,對於窄依賴,partition的轉換處理在Stage中完成計算。對於寬依賴,由於有Shuffle的存在,只能在parent RDD處理完成後,才能開始接下來的計算,因此寬依賴是劃分Stage的依據。
在這裏插入圖片描述
5 任務劃分
RDD任務切分中間分爲:Application、Job、Stage和Task
1)Application:初始化一個SparkContext即生成一個Application
2)Job:一個Action算子就會生成一個Job
3)Stage:根據RDD之間的依賴關係的不同將Job劃分成不同的Stage,遇到一個寬依賴則劃分一個Stage。
在這裏插入圖片描述

  1. Task:Stage是一個TaskSet,將Stage劃分的結果發送到不同的Executor執行即爲一個Task。
    注意:Application->Job->Stage-> Task每一層都是1對n的關係。

RDD緩存

  • RDD通過persist方法或cache方法可以將前面的計算結果緩存,默認情況下 persist() 會把數據以序列化的形式緩存在 JVM 的堆空間中。

  • 但是並不是這兩個方法被調用時立即緩存,而是觸發後面的action時,該RDD將會被緩存在計算節點的內存中,並供後面重用。
    在這裏插入圖片描述

  • 通過查看源碼發現cache最終也是調用了persist方法,默認的存儲級別都是僅在內存存儲一份,Spark的存儲級別還有好多種,存儲級別在object StorageLevel中定義的。
    在這裏插入圖片描述

  • 緩存有可能丟失,或者存儲存儲於內存的數據由於內存不足而被刪除,RDD的緩存容錯機制保證了即使緩存丟失也能保證計算的正確執行。通過基於RDD的一系列轉換,丟失的數據會被重算,由於RDD的各個Partition是相對獨立的,因此只需要計算丟失的部分即可,並不需要重算全部Partition。

    val rdd=sc.makeRDD(Array("test"));

    將rdd轉化爲攜帶時間戳且不緩存
    var noCache=rdd.map(_.toString+System.currentTimeMillis());
    多次打印
    noCache.collect().foreach(println);
    noCache.collect().foreach(println);
    noCache.collect().foreach(println);


    帶緩存
    var haveCache=rdd.map(_.toString+System.currentTimeMillis()).cache();
    haveCache.collect().foreach(println)
    haveCache.collect().foreach(println)
    haveCache.collect().foreach(println)


-----------
test1589891383596
test1589891383718
test1589891383751
test1589891383829
test1589891383829
test1589891383829

鍵值對RDD數據分區器

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

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

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

def getPartition(key: Any): Int = key match {
  case null => 0
  case _ => Utils.nonNegativeMod(key.hashCode, numPartitions)
}
def nonNegativeMod(x: Int, mod: Int): Int = {
  val rawMod = x % mod
  rawMod + (if (rawMod < 0) mod else 0)
}

Ranger分區

HashPartitioner分區弊端:可能導致每個分區中數據量的不均勻,極端情況下會導致某些分區擁有RDD的全部數據。

RangePartitioner作用:將一定範圍內的數映射到某一個分區內,儘量保證每個分區中數據量的均勻,而且分區與分區之間是有序的,一個分區中的元素肯定都是比另一個分區內的元素小或者大,但是分區內的元素是不能保證順序的。簡單的說就是將一定範圍內的數映射到某一個分區內。實現過程爲:

  • 第一步:先從整個RDD中抽取出樣本數據,將樣本數據排序,計算出每個分區的最大key值,形成一個Array[KEY]類型的數組變量rangeBounds;
  • 第二步:判斷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 myPatitioner(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 = sc.parallelize(Array((1,1),(2,2),(3,3),(4,4),(5,5),(6,6)))

    使用自定義分區
    val result=data.partitionBy(new myPatitioner(2));

    輸出
    result.mapPartitionsWithIndex((index,items)=>items.map((index,_))).collect.foreach(println);



---0-結果
(0,(2,2))
(0,(4,4))
(0,(6,6))
(1,(1,1))
(1,(3,3))
(1,(5,5))

數據讀取與保存

Spark的數據讀取及數據保存可以從兩個維度來作區分:文件格式以及文件系統。

  • 文件格式分爲:Text文件、Json文件、Csv文件、Sequence文件以及Object文件;
  • 文件系統分爲:本地文件系統、HDFS、HBASE以及數據庫。

.1文件類數據讀取與保存

1 Text文件
1)數據讀取:textFile(String)
2)數據保存: saveAsTextFile(String)

2 JSON文件
如果JSON文件中每一行就是一個JSON記錄,那麼可以通過將JSON文件當做文本文件來讀取,然後利用相關的JSON庫對每一條數據進行JSON解析。

注意:每一行都是json格式,不是原有的Json文件格式,如下。:
{“name”: “王小二”, “age”: 25.2, “school”: “福州大學”}

  //讀取
    val json=sc.textFile("in/test.json");

    val result  = json.map(JSON.parseFull);

    result.collect.foreach(println);

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

將RDD保存爲Sequence文件
rdd.saveAsSequenceFile("file:///opt/module/spark/seqFile")

讀取Sequence文件
 val seq = sc.sequenceFile[Int,Int]("file:///opt/module/spark/seqFile")打印讀取後的Sequence文件
 
seq.collect.foreach(println)
 Array[(Int, Int)] = Array((1,2), (3,4), (5,6))

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

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

連接mysql數據庫

讀數據
pom.xml:

<dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.20</version>
        </dependency>
  def main(args: Array[String]): Unit = {
    //上下文
    var conf=new SparkConf().setAppName("WordCount").setMaster("local");
    var sc=new SparkContext(conf);
    sc.setLogLevel("ERROR")

    val getConnection=()=>{
       DriverManager.getConnection("jdbc:mysql://localhost:3306/edupfm?serverTimezone=GMT%2B8", "root", "123456")
    }
    val sql="select id,username from user where id>=? and id<=?"
    val rdd=new JdbcRDD(sc,
      getConnection,
      sql,
      1,
      20,
      //分區數
      2,
      (rs)=>{
        (rs.getInt(1),rs.getString(2));
       // println(rs.getInt(1)+"--"+rs.getString(2));
      }
    )
    println(rdd.collect().toBuffer);
  }

------
ArrayBuffer((1,小衡), (2,小紅), (3,小章), (13,李四), (14,張三), (19,王二麻), (20,王五))

注,關於如何連接hdfs和es等數據庫,我會另外寫一篇文章來記錄

RDD累加器

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

1 系統累加器
針對一個輸入的日誌文件,如果我們想計算文件中所有空行的數量,我們可以編寫以下程序:

 val notice = sc.textFile("./NOTICE")
 val blanklines = sc.accumulator(0)

 val tmp = notice.flatMap(line => {
        if (line == "") {
            blanklines += 1
         }
        line.split(" ")
      })


 tmp.count()
res31: Long = 3213

 blanklines.value
res32: Int = 171

累加器的用法如下所示。

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

2自定義累加器

AccumulatorV2來提供更加友好的自定義類型累加器的實現方式。實現自定義類型累加器需要繼承AccumulatorV2並至少覆寫下例中出現的方法,下面這個累加器可以用於在程序運行過程中收集一些文本類信息,最終以Set[String]的形式返回。1

在這裏插入代碼片package com.atguigu.spark

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()
  }
}

廣播變量(調優策略)

廣播變量用來高效分發較大的對象。向所有工作節點發送一個較大的只讀值,以供一個或多個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)

使用廣播變量的過程如下:

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

今天先學到這裏,明天學習>>>spark-sql,會有相應的文章更新!

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