Spark 複習

簡介

Spark是一種基於內存的快、通用、可擴展的大數據分析引擎

特點

  1. Spark與Map Reduce相比,基於內存的運行要快100倍,基於硬盤的運算要快10倍以上。其中間結果可以緩存在內存中,達到複用的目的。

  2. 易用

    Spark支持Java、Python、Scala的API,還支持超過80種高級算法,使用戶可以快速的構建不同的應用。而且Spark支持交互式的Python和Scalal的Shell。

  3. 通用

    Spark提供了統一的解決方案,且這些方案可以應用在同一個應用中,如批處理、交互式查詢、實時流處理、機器學習和圖計算。減少了開發和維護的成本和部署平臺的物力成本。

  4. 兼容性

    Spark可以非常方便地和其它的開源產品進行融合。

Spark快於Map Reduce的原因?

網上大部分說是因爲Spark是基於內存的,而MapReduce是基於磁盤的。

還有說Spark中具有DAG有向無環圖,DAG有向無環圖在此過程中減少了shuffle以及落地磁盤的次數。

上述描述確實都對,但是我更相信下面的說法。

以下內容來自

作者:連城
鏈接:https://www.zhihu.com/question/23079001/answer/23569986
來源:知乎

在Spark內部,單個executor進程內RDD的分片數據是用Iterator流式訪問的,Iterator的hasNext方法和next方法是由RDD lineage上各個transformation攜帶的閉包函數複合而成的。該複合Iterator每訪問一個元素,就對該元素應用相應的複合函數,得到的結果再流式地落地(對於shuffle stage是落地到本地文件系統留待後續stage訪問,對於result stage是落地到HDFS或送回driver端等等,視選用的action而定)。如果用戶沒有要求Spark cache該RDD的結果,那麼這個過程佔用的內存是很小的,一個元素處理完畢後就落地或扔掉了(概念上如此,實現上有buffer),並不會長久地佔用內存。只有在用戶要求Spark cache該RDD,且storage level要求在內存中cache時,Iterator計算出的結果纔會被保留,通過cache manager放入內存池。

簡單起見,暫不考慮帶shuffle的多stage情況和流水線優化。這裏拿最經典的log處理的例子來具體說明一下(取出所有以ERROR開頭的日誌行,按空格分隔並取第2列):

val lines = spark.textFile("hdfs://<input>")
val errors = lines.filter(_.startsWith("ERROR"))
val messages = errors.map(_.split(" ")(1))
messages.saveAsTextFile("hdfs://<output>")

按傳統單機immutable FP的觀點來看,上述代碼運行起來好像是:

  1. 把HDFS上的日誌文件全部拉入內存形成一個巨大的字符串數組,
  2. Filter一遍再生成一個略小的新的字符串數組,
  3. 再map一遍又生成另一個字符串數組。

真這麼玩兒的話Spark早就不用混了……

如前所述,Spark在運行時動態構造了一個複合Iterator。就上述示例來說,構造出來的Iterator的邏輯概念上大致長這樣:

new Iterator[String] {
  private var head: String = _
  private var headDefined: Boolean = false

  def hasNext: Boolean = headDefined || {
    do {
      try head = readOneLineFromHDFS(...)     // (1) read from HDFS
      catch {
        case _: EOFException => return false
      }
    } while (!head.startsWith("ERROR"))       // (2) filter closure
    true
  }

  def next: String = if (hasNext) {
    headDefined = false
    head.split(" ")(1)                        // (3) map closure
  } else {
    throw new NoSuchElementException("...")
  }
}

模塊

重要角色

Driver

  1. 把用戶程序轉爲作業(JOB)
  2. 跟蹤Executor的運行狀況
  3. 爲執行器節點調度任務
  4. UI展示應用運行狀況

Executor

  1. 負責運行組成 Spark 應用的任務,並將結果返回給驅動器進程;

  2. 通過自身的塊管理器(Block Manager)爲用戶程序中要求緩存的RDD提供內存式存儲。RDD是直接緩存在Executor進程內的,因此任務可以在運行時充分利用緩存數據加速運算。

運行邏輯

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-2pnUlnyD-1573975929640)(assets/1573956622450.png)]

Spark Core

實現了Spark的基本功能,包含任務調度、內存管理、錯誤恢復、與存儲系統交互等模塊。
Spark Core中還包含了對彈性分佈式數據集(Resilient Distributed DataSet,簡稱RDD)的API定義

RDD

RDD(Resilient Distributed Dataset)彈性分佈式數據集,是Spark中最基本的數據抽象。
代表一個不可變的、可分區、裏面的元素可並行計算的集合。

官方這樣定義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)
 
 在內部,每個RDD是由以下5個主要特徵組成的:
 1. 每個RDD都有一組分區
 2. 每個分區上都是一種計算邏輯
 3. 各個RDD之間都有依賴關係
 4. 可選,對於K-V類型的數據有一個分區器。(就是說,如果是K-V類型數據,就要告訴它怎麼去分區,默認是根據Keyhash)
 5. 可選,一個存儲每個分區優先計算位置的列表(因爲計算是由Driver發給Executor的,
    在HDFS上並不是每個節點都有所有的數據,而且節點之間的距離也影響着IO傳輸,所以需要知道哪個節點運行效率最高)
知識點
  1. RDD的算子分爲兩種,一種叫轉換算子,一種叫行動算子。只有當行動算子觸發時,轉換算子纔會依次執行。
  2. RDD裏面並沒有數據、RDD可以理解爲對於真實數據的計算描述
RDD的創建
object RDDLearning {
    def main(args: Array[String]): Unit = {
        val conf = new SparkConf().setAppName("RDDLearning").setMaster("local[*]")
        val sc = new SparkContext(conf)

        // 1. 由集合創建
        //有兩種方式  parallelize 和 makeRDD
        //其中makeRDD底層調用的就是parallelize  爲了好記,我常用makeRDD
        //def makeRDD[T: ClassTag](
        //  seq: Seq[T],
        //  numSlices: Int = defaultParallelism): RDD[T] = withScope {
        //  parallelize(seq, numSlices)
        //}
        val rdd: RDD[Int] = sc.makeRDD(Array(1, 2, 3, 4, 5, 6, 7))
        val rdd2: RDD[Int] = sc.parallelize(Array(1, 2, 3, 4, 5, 6, 7))

        // 2. 由外部文件創建
        val fileRdd: RDD[String] = sc.textFile("dir/in/data.txt")

        // 3. 由其它RDD創建  其實就是轉換後的RDD
        val rdd3: RDD[Array[String]] = fileRdd.map(_.split(" "))
        sc.stop
    }
}
RDD的分區
object RDDLearning {
  def main(args: Array[String]): Unit = {
    //local[3]  代表三個分區
    val conf = new SparkConf().setAppName("RDDLearning").setMaster("local[3]")
    val sc = new SparkContext(conf)

    val rdd1: RDD[Int] = sc.parallelize(List(1, 2, 3, 4, 5, 6, 7))
//    val rdd1: RDD[Int] = sc.parallelize(List(1, 2, 3, 4, 5, 6, 7),1)  也可以這樣指定分區
    println("分區數:"+rdd1.partitions.length)//3
    rdd1.mapPartitionsWithIndex {
      case (num, datas) => {
        datas.map((_, "分區號:" + num))
      }
    }.collect.foreach(println)
    //(1,分區號:0)
    //(2,分區號:0)
    //(3,分區號:1)
    //(4,分區號:1)
    //(5,分區號:2)
    //(6,分區號:2)
    //(7,分區號:2)

    sc.stop
  }
}
分區邏輯
  1. 由集合生成的RDD分區邏輯

    // 1. 我們可以跟蹤源碼
    def parallelize[T: ClassTag](
        seq: Seq[T],
        // 2. 如果沒有指定分區數,就會使用默認的並行度
        numSlices: Int = defaultParallelism): RDD[T] = withScope {
        assertNotStopped()
        new ParallelCollectionRDD[T](this, seq, numSlices, Map[Int, Seq[String]]())
    }
    // 3. defaultParallelism 默認並行的的邏輯
    def defaultParallelism: Int = {
        assertNotStopped()
        taskScheduler.defaultParallelism  //這個 
        //org.apache.spark.scheduler.TaskSchedulerImpl#defaultParallelism
        //TaskSchedulerImpl是個特性 trait  所以找它的實現類
    }
    
    // 4. 這裏隨便找了一個LocalSchedulerBackend
    override def defaultParallelism(): Int =
        scheduler.conf.getInt("spark.default.parallelism", totalCores)
    //結論:  如果設置中設置了spark.default.parallelism   就選用它的值
    //沒有的話就算totalCores  總共的內核數  這也就是爲什麼Local[3] 就三個分區的原因
    
  2. 由外部文件生成的RDD

    //1. 依舊看源碼
    def textFile(
        path: String,
        //2. 這裏可以指定最小的分區數(之所以最小,是因爲最後還是根據文件大小分區)
        //如果沒指定
        minPartitions: Int = defaultMinPartitions): RDD[String] = withScope {
        assertNotStopped()
        hadoopFile(path, classOf[TextInputFormat], classOf[LongWritable], classOf[Text],minPartitions)
        .map(pair => pair._2.toString).setName(path)
    }
    
    //3. 沒指定就和普通集合生成的並行度與2取最小值
    def defaultMinPartitions: Int = math.min(defaultParallelism, 2)
    
    //4. 再來看具體的分區運輸規則
    hadoopFile(path,...,minPartitions)  //分區數給了hadoopFile
    
    //5. hadoopFile裏面
    new HadoopRDD(
          this,
          confBroadcast,
          Some(setInputPathsFunc),
          inputFormatClass,
          keyClass,
          valueClass,
          minPartitions).setName(path)
    //6. hadoopRDD裏面有這個方法
    override def getPartitions: Array[Partition] = {
        val inputSplits = inputFormat.getSplits(jobConf, minPartitions)
    }
    
    //7. 這裏看一下org.apache.hadoop.mapred.FileInputFormat#getSplits 別的邏輯都差不多
    //因爲篇幅,我只取了分區邏輯部分,不是所有代碼
    public InputSplit[] getSplits(JobConf job, int numSplits(這是minPartitions)){
       
        long totalSize = 0;                          
        for (FileStatus file: files) {
          totalSize += file.getLen(); //獲得所有文件的大小,因爲路徑可以是個目錄
        }
    	//常理走下來 goalSize=numSplits 這裏假設我們設置是local[*]
        //goalSize=所有文件的大小/2
        long goalSize = totalSize / (numSplits == 0 ? 1 : numSplits); 
        
        //minSize=1
        long minSize = Math.max(job.getLong(org.apache.hadoop.mapreduce.lib.input.
          FileInputFormat.SPLIT_MINSIZE, 1), minSplitSize);
    
        // generate splits
        ArrayList<FileSplit> splits = new ArrayList<FileSplit>(numSplits);
        
        for (FileStatus file: files) {
            long length = file.getLen();
            if (isSplitable(fs, path)) {
              //本地跑的所以是32M
              long blockSize = file.getBlockSize();
             
              long splitSize = computeSplitSize(goalSize=2, minSize=1, blockSize=32M);
    		  //邏輯Math.max(minSize, Math.min(goalSize, blockSize));
              //splitSize=超過塊大小的文件就是塊大小,沒超過的就是文件總的大小
              
              //未切分文件大小
              long bytesRemaining = length;
              
              //以下的切片邏輯差不多就是如果小文件就一片 因爲bytesRemaining=splitSize
              //如果是大文件的話  就是以一個塊大小爲一片,如果剩餘文件大小/塊大小<=1.1  就直接一片
              while (((double) bytesRemaining)/splitSize > 1.1) {
                String[][] splitHosts = getSplitHostsAndCachedHosts(blkLocations,
                    length-bytesRemaining, splitSize, clusterMap);
                splits.add(makeSplit(path, length-bytesRemaining, splitSize,
                    splitHosts[0], splitHosts[1]));
                bytesRemaining -= splitSize;
              }
    
              if (bytesRemaining != 0) {
                String[][] splitHosts = getSplitHostsAndCachedHosts(blkLocations, length
                    - bytesRemaining, bytesRemaining, clusterMap);
                splits.add(makeSplit(path, length - bytesRemaining, bytesRemaining,
                    splitHosts[0], splitHosts[1]));
              }
            } else {
              String[][] splitHosts = getSplitHostsAndCachedHosts(blkLocations,0,length,clusterMap);
              splits.add(makeSplit(path, 0, length, splitHosts[0], splitHosts[1]));
            }
          } else { 
            //Create empty hosts array for zero length files
            splits.add(makeSplit(path, 0, length, new String[0]));
          }
        }
      }
    
    
重分區
object RDDLearning {
    def main(args: Array[String]): Unit = {
        val conf = new SparkConf().setAppName("RDDLearning").setMaster("local[3]")
        val sc = new SparkContext(conf)

        val rdd1: RDD[Int] = sc.parallelize(List(1, 2, 3, 4, 5, 6, 7))
        //重分區也有兩個repartition  和coalesce
        //def repartition(numPartitions: Int) RDD[T] =  {
        //  coalesce(numPartitions, shuffle = true)
        //}
        rdd1.repartition(4).mapPartitionsWithIndex {
            case (num, datas) => {
                datas.map((_, "分區號:" + num))
            }
        }.collect.foreach(println)
        //很奇怪,但是確實是hash分區的
        //(2,分區號:0)
        //(4,分區號:0)
        //(6,分區號:0)
        //(7,分區號:1)
        //(1,分區號:3)
        //(3,分區號:3)
        //(5,分區號:3)
        sc.stop
    }
}
序列化

由於Spark的分佈式的分析引擎,數據的初始化在Driver端,但是實際運行程序是在Executor端,這就涉及到了跨進程通信,是需要序列化的。

class SearchFunctions(val query: String) extends Serializable {
  //第一個方法是判斷輸入的字符串是否存在query 存在返回true,不存在返回false
  def isMatch(s: String): Boolean = {
    s.contains(query)
  }

  def getMatchesFunctionReference(rdd: org.apache.spark.rdd.RDD[String]): org.apache.spark.rdd.RDD[String] = {
    // 需要序列化:"isMatch"表示"this.isMatch",因此我們要傳遞整個"this"
    rdd.filter(isMatch)
  }

  def getMatchesFieldReference(rdd: org.apache.spark.rdd.RDD[String]): org.apache.spark.rdd.RDD[String] = {
    // 需要序列化:"query"表示"this.query",因此我們要傳遞整個"this"
    rdd.filter(x=>x.contains(query))
  }

  def getMatchesNoReference(rdd: org.apache.spark.rdd.RDD[String]): org.apache.spark.rdd.RDD[String] = {
    // 不需要序列化:只把我們需要的字段拿出來放入局部變量中
    val query_ = this.query
    rdd.filter(x => x.contains(query_))
  }
}

object SearchFunctions {
  def main(args: Array[String]): Unit = {
    val conf: SparkConf = new SparkConf()
    conf.setAppName("SearchFunctions")
    conf.setMaster("local[2]")
    val sc: SparkContext = new SparkContext(conf)

    val rdd = sc.parallelize(List("hello java", "hello scala hello", "hello hello"))

    val sf = new SearchFunctions("hello")
    val unit: RDD[String] = sf.getMatchesNoReference(rdd)
    unit.foreach(println)
  }
}
緩存

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

存儲級別
NONE   不緩存
DISK_ONLY	只存磁盤
DISK_ONLY_2	 只存磁盤,存兩份
MEMORY_ONLY
MEMORY_ONLY_2
MEMORY_ONLY_SER		只存內存中並且序列化存儲
MEMORY_ONLY_SER_2
MEMORY_AND_DISK
MEMORY_AND_DISK_2
MEMORY_AND_DISK_SER
MEMORY_AND_DISK_SER_2
OFF_HEAP	存堆外內存中

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

object RDD_Cache {
  def main(args: Array[String]): Unit = {
    val conf: SparkConf = new SparkConf().setAppName("RDD_Cache").setMaster("local[2]")
    val sc = new SparkContext(conf)
    val timeRdd = sc.makeRDD(List("zzy"))
    val mapRDD1 = timeRdd.map(x => (x, System.currentTimeMillis()))
    println(mapRDD1.collect.toBuffer)
    println(mapRDD1.collect.toBuffer)
    println(mapRDD1.collect.toBuffer) //上面三個時間都不同
    mapRDD1.cache() //mapRDD1.persist()
    println(mapRDD1.collect.toBuffer)
    println(mapRDD1.collect.toBuffer)
    println(mapRDD1.collect.toBuffer) //上面三個時間都相同   緩存了
  }
}
分區器
  1. 只有Key-Value類型的RDD纔有分區器的,非Key-Value類型的RDD分區器的值是None
  2. 每個RDD的分區ID範圍:0~numPartitions-1,決定這個值是屬於那個分區的。
object SubjectDemo3 {
    def main(args: Array[String]): Unit = {
        val conf = new SparkConf().setAppName("SubjectDemo").setMaster("local")
        val sc = new SparkContext(conf)
        // 1.對數據進行切分
        val tuples: RDD[(String, Int)] =
        sc.textFile("C:\\Users\\Administrator\\Desktop\\subjectaccess\\access.txt").map(line => {
            val fields: Array[String] = line.split("\t")
            //取出url
            val url = fields(1)
            (url, 1)
        })
        //將相同url進行聚合,得到了各個學科的訪問量
        val sumed: RDD[(String, Int)] = tuples.reduceByKey(_ + _).cache()
        //從url中獲取學科的字段 數據組成式 學科, url 統計數量
        val subjectAndUC = sumed.map(tup => {
            val url = tup._1 //用戶url
            val count = tup._2 // 統計的訪問數量
            val subject = new URL(url).getHost //學科
            (subject, (url, count))
        })
        //將所有學科取出來
        val subjects: Array[String] = subjectAndUC.keys.distinct.collect
        //創建自定義分區器對象
        val partitioner: SubjectPartitioner = new SubjectPartitioner(subjects)
        //分區
        val partitioned: RDD[(String, (String, Int))] = subjectAndUC.partitionBy(partitioner)
        //取top3
        val rs = partitioned.mapPartitions(it => {
            val list = it.toList
            val sorted = list.sortBy(_._2._2).reverse
            val top3: List[(String, (String, Int))] = sorted.take(3)
            //因爲方法的返回值需要一個iterator
            top3.iterator
        })
        //存儲數據
        rs.saveAsTextFile("out2")
        sc.stop()
    }
}
/**
  * 自定義分區器需要繼承Partitioner並實現對應方法
  */
class SubjectPartitioner(subjects: Array[String]) extends Partitioner {
    //創建一個map集合用來存到分區號和學科
    val subject = new mutable.HashMap[String, Int]()
    //定義一個計數器,用來生成分區好
    var i = 0
    for (s <- subjects) {
        //存學科和分區
        subject += (s -> i)
        i += 1 //分區自增
    } // 獲取分區數

    override def numPartitions: Int = subjects.size

    //獲取分區號(如果傳入的key不存在,默認將數據存儲到0分區)
    override def getPartition(key: Any): Int = subject.getOrElse(key.toString, 0)
}
累加器

解決的問題:分佈式只寫共享變量

object AccumulatorDemo {
  def main(args: Array[String]): Unit = {
    val conf: SparkConf = new SparkConf().setAppName("AccumulatorDemo").setMaster("local[2]")
    val sc = new SparkContext(conf)
    val dataRDD= sc.makeRDD(1 to 10)
    var sum = 0  //由於sum只存在在driver端,而foreach(內得內容要發送到Executor上執行,並沒有sum,所以累計失敗)
    dataRDD.foreach(x=>sum=sum+x)
    println(sum) // 0
    //1. 創建一個累加器
    val acc=new LongAccumulator
    //2. 註冊
    sc.register(acc)
    //3. 使用
    dataRDD.foreach(x=>acc.add(x))
    println(acc.value) //55
    sc.stop()
  }
}
自定義累加器
class MyAccumulator extends  AccumulatorV2[Int,Int]{
  //創建一個輸出值的變量
  private var sum:Int = _ 

  //必須重寫如下方法:
  //檢測方法是否爲空
  override def isZero: Boolean = sum == 0
  //拷貝一個新的累加器
  override def copy(): AccumulatorV2[Int, Int] = {
    //需要創建當前自定累加器對象
    val myaccumulator = new MyAccumulator()
    //需要將當前數據拷貝到新的累加器數據裏面
   //也就是說將原有累加器中的數據拷貝到新的累加器數據中
    //ps:個人理解應該是爲了數據的更新迭代
    myaccumulator.sum = this.sum
    myaccumulator
  }
  //重置一個累加器 將累加器中的數據清零
  override def reset(): Unit = sum = 0
  //每一個分區中用於添加數據的方法(分區中的數據計算)
  override def add(v: Int): Unit = {
    //v 即 分區中的數據
     //當累加器中有數據的時候需要計算累加器中的數據
     sum += v
  }
  //合併每一個分區的輸出(將分區中的數進行彙總)
  override def merge(other: AccumulatorV2[Int, Int]): Unit = {
          //將每個分區中的數據進行彙總
            sum += other.value

  }
 //輸出值(最終累加的值)
  override def value: Int = sum
}

object  MyAccumulator{
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setAppName("MyAccumulator").setMaster("local[*]")
    //2.創建SparkContext 提交SparkApp的入口
    val sc = new SparkContext(conf)
    val numbers = sc .parallelize(List(1,2,3,4,5,6),2)
    val accumulator = new MyAccumulator()
    //需要註冊 
    sc.register(accumulator,"acc")
    //切記不要使用Transformation算子 會出現無法更新數據的情況
    //應該使用Action算子
    //若使用了Map會得不到結果
    numbers.foreach(x => accumulator.add(x))
    println(accumulator.value)
  }
}
廣播變量

解決的問題:分佈式只讀共享變量

object BroadcastDemo {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setAppName("BroadcastDemo").setMaster("local[2]")
    val sc = new SparkContext(conf)
    //list是在driver端創建也相當於是本地變量
    val list = List("hello java")
    
    val lines = sc.textFile("dir/file")
    //算子部分是在Excecutor端執行
    val filterStr = lines.filter(list.contains(_))
    filterStr.foreach(println)
  }
}

上面的代碼有個問題,就是Driver會把list以task的方式發送到executor上執行,可以粗略的認爲一個分區就算一個task,那麼list就可能會在一個executor上重複多份。如果list稍微大點可能就會造成內存溢出。

object BroadcastDemo {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf().setAppName("BroadcastDemo").setMaster("local[2]")
    val sc = new SparkContext(conf)
    //list是在driver端創建也相當於是本地變量
    val list = List("hello java")
    //封裝廣播變量
    val broadcast = sc.broadcast(list)
    //算子部分是在Excecutor端執行
    val lines = sc.textFile("dir/file")
    //使用廣播變量進行數據處理 value可以獲取廣播變量的值
    val filterStr = lines.filter(broadcast.value.contains(_))
    filterStr.foreach(println)
  }
}
發佈了64 篇原創文章 · 獲贊 8 · 訪問量 4560
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章