SparkCore 筆記(二)

目錄

 

一、RDD中的函數傳遞(序列化問題)

1、傳遞一個方法

2、傳遞一個屬性

二、RDD依賴關係

1、Lineage(容錯機制)

2、窄依賴                            

3、寬依賴

4、DAG

5、任務劃分(面試重點)

6、RDD緩存

7、RDD CheckPoint

三、鍵值對RDD數據分區器

1、獲取分區

2、Hash分區

3、Ranger分區

4、自定義分區

四、數據讀取與保存

1、MySQL數據庫連接

2、HBase數據庫

五、RDD編程進階

1、累加器

2、廣播變量(調優策略)


一、RDD中的函數傳遞(序列化問題)

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

1、傳遞一個方法

package com.xin
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.rdd.RDD

class SearchTwo(query: String) extends java.io.Serializable{
  // 必須加extends //Serializable 因爲類初始化在Driver端
  // 而實際在executor端需要調用類中的方法
  //因此涉及到了網絡通信需要序列化
  def isMatch(s:String):Boolean={
    s.contains(query)
  }
  def getMatchRdd(rdd:RDD[String])={
    rdd.filter(isMatch)    //isMatch相當於x => x.contains(query)
  }
}
object serializeable {
  def main(args: Array[String]): Unit = {
    //1.初始化配置信息及SparkContext
    val conf = new SparkConf().setMaster("local[*]").setAppName("serializeable")
    val sc = new SparkContext(conf)
    //2.創建一個RDD
    val rdd:RDD[String] = sc.parallelize(Array("hadoop", "spark","hive"))
    //3.創建一個Search對象
    val search = new SearchTwo("h")
    //4.運用第一個過濾函數並打印結果
    search.getMatchRdd(rdd).collect().foreach(println)
  }
}

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

2、傳遞一個屬性

package com.xin
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.rdd.RDD

//class SearchTwo(query: String) extends java.io.Serializable{
class SearchTwo(query: String) {
  def isMatch(s:String):Boolean={
    s.contains(query)
  }
  def getMatchlRdd2(rdd:RDD[String])={
    val query_ = this.query
    rdd.filter(x => x.contains(query_))  
    //必須賦值給局部變量,x.contains(this.query)不管用
  }
}
object serializeable {

  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setMaster("local[*]").setAppName("serializeable")
    val sc = new SparkContext(conf)
    val rdd:RDD[String] = sc.parallelize(Array("hadoop", "spark","hive"))
    val search = new SearchTwo("h")
    search.getMatchlRdd2(rdd).collect().foreach(println)
  }
}

個人總結:調用類中定義的屬性,需要實例化,因爲類是在Driver中實例化,而調用是在Executer。也可以聲明一個局部變量來傳遞類中的屬性,即可以不實例化

二、RDD依賴關係

1、Lineage(容錯機制)

RDD只支持粗粒度轉換(寫是粗顆粒,批處理,讀是細顆粒,一行一行),即在大量記錄上執行的單個操作。將創建RDD的一系列Lineage(血統)記錄下來,以便恢復丟失的分區。RDD的Lineage會記錄RDD的元數據信息和轉換行爲,當該RDD的部分分區數據丟失時,它可以根據這些信息來重新運算和恢復丟失的數據分區。(chekpoint)

實踐代碼:

object Lineage {
  def main(args: Array[String]): Unit = {
    val sc = new SparkContext(new SparkConf().setAppName("myspark2").setMaster("local[*]"))
    val wordAndOne = sc.textFile("in2/test.txt").flatMap(_.split(",")).map((_,1))
    val wordAndCount = wordAndOne.reduceByKey(_+_)

    val debugString: String = wordAndOne.toDebugString               //查看“wordAndOne”的Lineage
    val toDebugString: String = wordAndCount.toDebugString
    val dependencies: Seq[Dependency[_]] = wordAndOne.dependencies
    val dependencies1: Seq[Dependency[_]] = wordAndCount.dependencies

    println("----------------wordAndOne toDebugString------------------")
    println(debugString)     //HadoopRDD[0] at textFile---MapPartitionsRDD[1] at textFile--flatMap--map
    println("----------------wordAndCount toDebugString------------------")
    println(toDebugString)  //HadoopRDD[0] at textFile---MapPartitionsRDD[1] at textFile--flatMap--map--ShuffledRDD[4] at reduceByKey
    println("----------------wordAndOne dependencies------------------")
    println(dependencies.toString())   //org.apache.spark.OneToOneDependency@79e66b2f
    println("----------------wordAndCount dependencies------------------")
    println(dependencies1.toBuffer)    //org.apache.spark.ShuffleDependency@17273273
  }
}

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

2、窄依賴                            

窄依賴(narrow dependency)指每一個父RDD的Partition最多被子RDD的一個Partition使用,窄依賴形象的比喻爲獨生子女

3、寬依賴

寬依賴(wide dependency)指多個子RDD的Partition會依賴同一個父RDD的Partition,會引起shuffle

總結:寬依賴我們形象的比喻爲超生

總結:寬窄依賴區別在於父RDD的分區被一個還是多個子RDD分區繼承,多個RDD繼承即產生了shuffle

4、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。

4)Task:Stage是一個TaskSet,將Stage劃分的結果發送到不同的Executor執行即爲一個Task。

注意:Application->Job->Stage-> Task每一層都是1對n的關係。

6、RDD緩存

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

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

 val mapRDD: RDD[String] = LineRDD.map(_+System.currentTimeMillis())
    val cacheRDD: RDD[String] = LineRDD.map(_+System.currentTimeMillis()).cache() //緩存
    for( a <- 1 to 5){
      mapRDD.foreach(println)             //五次輸出時間戳不同
    }
    for( a <- 1 to 5){
      if(a==1)println("------------------")
      cacheRDD.foreach(println)          // 五次輸出結果一致
    }

7、RDD CheckPoint

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

案例實操:

(1)設置檢查點

scala> sc.setCheckpointDir("hdfs://hadoop102:9000/checkpoint")

(2)創建一個RDD

scala> val rdd = sc.parallelize(Array("atguigu"))

rdd: org.apache.spark.rdd.RDD[String] = ParallelCollectionRDD[14] at parallelize at <console>:24

(3)將RDD轉換爲攜帶當前時間戳並做checkpoint

scala> val ch = rdd.map(_+System.currentTimeMillis)

ch: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[16] at map at <console>:26



scala> ch.checkpoint

(4)多次打印結果

scala> ch.collect
res55: Array[String] = Array(atguigu1538981860336)

scala> ch.collect
res56: Array[String] = Array(atguigu1538981860504)

scala> ch.collect
res57: Array[String] = Array(atguigu1538981860504)

scala> ch.collect
res58: Array[String] = Array(atguigu1538981860504)

三、鍵值對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、獲取分區

(1)創建一個pairRDD

scala> val pairs = sc.parallelize(List((1,1),(2,2),(3,3)))

pairs: org.apache.spark.rdd.RDD[(Int, Int)] = ParallelCollectionRDD[3] at parallelize at <console>:24

(2)查看RDD的分區器

scala> pairs.partitioner

res1: Option[org.apache.spark.Partitioner] = None

(3)導入HashPartitioner類

scala> import org.apache.spark.HashPartitioner

import org.apache.spark.HashPartitioner

(4)使用HashPartitioner對RDD進行重新分區

scala> val partitioned = pairs.partitionBy(new HashPartitioner(2))

partitioned: org.apache.spark.rdd.RDD[(Int, Int)] = ShuffledRDD[4] at partitionBy at <console>:27

(5)查看重新分區後RDD的分區器

scala> partitioned.partitioner

res2: Option[org.apache.spark.Partitioner] = Some(org.apache.spark.HashPartitioner@2)

2、Hash分區

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

3、Ranger分區

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

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

第一步:先從整個RDD中抽取出樣本數據,將樣本數據排序,計算出每個分區的最大key值,形成一個Array[KEY]類型的數組變量rangeBounds;

第二步:判斷key在rangeBounds中所處的範圍,給出該key值在下一個RDD中的分區id下標;該分區器要求RDD中的KEY類型必須是可以排序的

4、自定義分區

要實現自定義的分區器,你需要繼承 org.apache.spark.Partitioner 類

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

將RDD使用自定義的分區類進行重新分區

val par = data.partitionBy(new CustomerPartitioner(2))

四、數據讀取與保存

1、MySQL數據庫連接

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.27</version>
</dependency>

讀取mysql數據,轉換成RDD

import java.sql.DriverManager
import org.apache.spark.rdd.JdbcRDD
import org.apache.spark.{SparkConf, SparkContext}
//TODO 讀取mysql數據轉換成RDD
object MysqlRDD {
  def main(args: Array[String]): Unit = {
    //2.創建SparkContext
    val sc = new SparkContext(new SparkConf().setMaster("local[*]").setAppName("JdbcRDD"))

    //3.定義連接mysql的參數
    val driver = "com.mysql.jdbc.Driver"
    val url = "jdbc:mysql://hdp-1:3306/sqoop"
    val userName = "root"
    val passWd = "123456"

    //創建JdbcRDD
    val rdd = new JdbcRDD(sc, () => {
      Class.forName(driver)
      DriverManager.getConnection(url, userName, passWd)
    },
      "select * from tb3 where id >= ? and id <= ?;",
      1,     //id最小範圍  對應第一個問號
      5,   //id最大範圍   對應第二個問號
      1,   //分區數
      r => (r.getInt(1), r.getString(2))   //結果集處理
    )

    //打印最後結果
    println(rdd.count())
    rdd.foreach(println)

    sc.stop()
  }
}

把RDD數據寫入Mysql

//TODO 把RDD寫入Mysql:
object writeMysql{
  def main(args: Array[String]) {
    val sc = new SparkContext(new SparkConf().setMaster("local[2]").setAppName("MysqlApp"))
    val data = sc.parallelize(List("Female", "Male","Female"))

    data.foreachPartition(insertData)
  }

  def insertData(iterator: Iterator[String]): Unit = {
    Class.forName ("com.mysql.jdbc.Driver").newInstance()
    val conn = java.sql.DriverManager.getConnection("jdbc:mysql://hdp-1:3306/sqoop", "root", "123456")
    iterator.foreach(data => {
      val ps = conn.prepareStatement("insert into tb3(username) values (?)")
      ps.setString(1, data)
      ps.executeUpdate()
    })
  }
}

2、HBase數據庫

<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>
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.mapred.TableOutputFormat
import org.apache.hadoop.hbase.mapreduce.TableInputFormat
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.hadoop.hbase.util.Bytes
import org.apache.hadoop.mapred.JobConf

object ReadHbase {
  def main(args: Array[String]): Unit = {
    //創建SparkContext
    val sc = new SparkContext(new SparkConf().setMaster("local[*]").setAppName("HBaseRDD"))

    //構建HBase配置信息
    val conf: Configuration = HBaseConfiguration.create()
    conf.set("hbase.zookeeper.quorum", "hdp-1,hdp-2,hdp-3")
    conf.set(TableInputFormat.INPUT_TABLE, "stu")
    //從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("infos"), Bytes.toBytes("name")))
        val age: String = Bytes.toString(result.getValue(Bytes.toBytes("infos"), Bytes.toBytes("age")))
        println("RowKey:" + key + ",Name:" + name + ",age:" + age)
    }
    //關閉連接
    sc.stop()
  }
}

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

      //創建HBaseConf
      val conf = HBaseConfiguration.create()
      conf.set("hbase.zookeeper.quorum", "hdp-1,hdp-2,hdp-3")
      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: (String, String, String)) = {
        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    數值型容易出現編碼錯誤,可以改成 "1"
      val initialRDD = sc.parallelize(List(("1","apple","11"), ("2","banana","12"), ("3","pear","13")))

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

五、RDD編程進階

1、累加器

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

系統累加器:

package com.xin
import org.apache.spark.{SparkConf, SparkContext}

//TODO 針對一個輸入的日誌文件,如果我們想計算文件中所有空行的數量,我們可以編寫以下程序:
object leijia {
  def main(args: Array[String]): Unit = {
    val sc = new SparkContext(new SparkConf().setMaster("local[*]").setAppName("HBaseRDD"))
    val notice = sc.textFile("in2/test.txt")
//  創建出存有初始值0的累加器
    val blanklines = sc.accumulator(0)
    val tmp = notice.flatMap(line => {
      if (line == "") {
        blanklines += 1    //累加器進行計算
      }
      line.split(",")
    })

    val count: Long = tmp.count()
    println("單詞個數:"+count)
    val num: Int = blanklines.value
    println("空行個數:"+num)    //如果文本最後一行是空值,不進行計算
  }
}

通過在驅動器Driver中調用SparkContext.accumulator(initialValue)方法,創建出存有初始值的累加器。Spark閉包(局部變量)裏的執行器代碼可以使用累加器的 += 方法(在Java中是 add)增加累加器的值。 驅動器程序可以調用累加器的value屬性(在Java中使用value()或setValue())來訪問累加器的值。

注意:工作節點上的任務不能訪問累加器的值。從這些任務的角度來看,累加器是一個只寫變量。對於要在行動操作中使用的累加器,Spark只會把每個任務對各累加器的修改應用一次。因此,如果想要一個無論在失敗還是重複計算時都絕對可靠的累加器,我們必須把它放在 foreach() 這樣的行動操作中,轉化操作中累加器可能會發生不止一次更新。

自定義累加器:

繼承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 sc=new SparkContext(new SparkConf().setAppName("LogAccumulator").setMaster("local[3]"))

    val accum = new LogAccumulator    //new自定義的累加器
    sc.register(accum, "logAccum")    //通過sc註冊累加器
    val sum: Int = sc.parallelize(Array("1", "2a", "3", "4b", "5", "xin", "7cd", "8", "9"), 2).filter(line => {
      val pattern = """^-?(\d+)"""    //^:行開頭  (.\d+)?:括號裏內出現0或1次   表數字
      val flag: Boolean = line.matches(pattern)   //匹配是否符合pattern
      if (!flag) {    //!flag -->包含字母
        accum.add(line)
      }
      flag
    }).map(_.toInt).reduce(_ + _)

    println("sum: " + sum)
    for (v <- accum.value) print(v + " ")
    println()
    sc.stop()
  }
}

2、廣播變量(調優策略)

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

val broadcastVar = sc.broadcast(Array(1, 2, 3))
print(broadcastVar.value.mkString(" "))

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

(1) 調用 SparkContext.broadcast 創建出一個 Broadcast[T] 對象。 任何可序列化的類型都可以這麼實現。

(2) 通過 value 屬性訪問該對象的值(在 Java 中爲 value() 方法)。

(3) 變量只會被髮到各個節點一次,應作爲只讀值處理(修改這個值不會影響到別的節點)。

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