Spark 的核心概念 RDD

喬二爺發表於2019-04-20

1.RDD 概述

1.1 什麼是 RDD ?

RDD(Resilient Distributed Dataset) 叫著 彈性分散式資料集 ,是Spark 中最基本的抽象,它代表一個不可變、可分割槽、裡面元素可以平行計算的集合。

RDD 具有資料流模型特點:自動容錯、位置感知性排程和可伸縮。

RDD 允許使用者在執行多個查詢時,顯示地將工作集快取在記憶體中,後續的查詢能夠重用工作集,這將會極大的提升查詢的效率。

我們可以認為 RDD 就是一個代理,我們操作這個代理就像操作本地集合一樣,不需去關心任務排程、容錯等問題。

1.2 RDD 的屬性

在 RDD 原始碼中這樣來描述 RDD

*  - 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),即資料集的基本組成單位。 對於RDD來說,每個分片都會被一個計算任務處理,並決定平行計算的粒度。使用者可以在建立RDD 的時候指定RDD的分片個數,如果沒有指定,那麼就會採用預設值。預設值就是程式所分配到的CPU Cores 的數目;
  2. 對於RDD來說,每個分片都會被一個計算任務處理,並決定平行計算的粒度。使用者可以在建立RDD 的時候指定RDD的分片個數,如果沒有指定,那麼就會採用預設值。預設值就是程式所分配到的CPU Cores 的數目;
  3. RDD 之間互相存在依賴關係。 RDD 的每次轉換都會生成一個新的 RDD ,所以 RDD 之前就會形成類似於流水線一樣的前後依賴關係。在部分分割槽資料丟失時,Spark 可以通過這個依賴關係重新計算丟失部分的分割槽資料,而不是對 RDD 的所有分割槽進行重新計算。
  4. 一個Partitioner ,即 RDD 的分片函式 。當前Spark 中實現了兩種型別的分片函式,一個是基於雜湊的 HashPartitioner ,另外一個是基於範圍的 RangePartitioner。只有對於key-value的RDD ,才會有 Partitioner,非 key-value 的RDD 的 Partitioner 的值是None。Partitioner 函式不但決定了RDD 本身的分片數量,也決定了 Parent RDD Shuffle 輸出時的分片數量。
  5. 一個列表,儲存存取每個Partition 的優先位置(preferred location)。 對於一個HDFS 檔案來說,這個列表儲存的就是每個 Partition 所在的塊位置。安裝“移動資料不如移動計算”的理念,Spark 在進行任務排程的時候,會盡可能地將計算任務分配到其所要處理資料塊的儲存位置。

2 建立 RDD

2.1 由一個存在的 Scala 集合進行建立

#通過並行化scala集合建立RDD,一般在測試的時候使用
scala> var rdd = sc.parallelize(List(1,2,3,4,5,6,7,8,9))
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[0] at parallelize at <console>:24
複製程式碼

2.2 由外部的儲存系統的資料集建立,包括本地的檔案系統,還有所有 Hadoop 支援的資料集,比如 HDFS、Cassandra、Hbase

var rdd1 = sc.textFile("/root/words.txt")
var rdd2 = sc.textFile("hdfs:192.168.80.131:9000/words.text")
複製程式碼

2.3 呼叫一個已經存在了的RDD 的 Transformation,會生成一個新的 RDD。

3 RDD 的程式設計 API

3.1 Transformation

這種 RDD 中的所有轉換都是延遲載入的,也就是說,他們並不會直接就計算結果。相反的,他們只是記住這些應用到基礎資料集(例如一個檔案)上的轉換動作。只有當發生一個返回結果的 Driver 的動作時,這些操作才會真正的執行。這種設計會讓Spark 更加有效率的執行。

常用的 Transformation 操作:

轉換 含義
map(func) 返回一個新的RDD,該RDD由每一個輸入元素經過func函式轉換後組成
filter(func) 返回一個新的RDD,該RDD由經過func函式計算後返回值為true的輸入元素組成
flatMap(func) 類似於map,但是每一個輸入元素可以被對映為0或多個輸出元素(所以func應該返回一個序列,而不是單一元素)
mapPartitions(func) 類似於map,但獨立地在RDD的每一個分片上執行,因此在型別為T的RDD上執行時,func的函式型別必須是Iterator[T] => Iterator[U]
mapPartitionsWithIndex(func) 類似於mapPartitions,但func帶有一個整數參數列示分片的索引值,因此在型別為T的RDD上執行時,func的函式型別必須是(Int, Interator[T]) => Iterator[U]
sample(withReplacement, fraction, seed) 根據fraction指定的比例對資料進行取樣,可以選擇是否使用隨機數進行替換,seed用於指定隨機數生成器種子
union(otherDataset) 對源RDD和引數RDD求並集後返回一個新的RDD
intersection(otherDataset) 對源RDD和引數RDD求交集後返回一個新的RDD
distinct([numTasks])) 對源RDD進行去重後返回一個新的RDD
groupByKey([numTasks]) 在一個(K,V)的RDD上呼叫,返回一個(K, Iterator[V])的RDD
reduceByKey(func, [numTasks]) 在一個(K,V)的RDD上呼叫,返回一個(K,V)的RDD,使用指定的reduce函式,將相同key的值聚合到一起,與groupByKey類似,reduce任務的個數可以通過第二個可選的引數來設定
aggregateByKey(zeroValue)(seqOp, combOp, [numTasks]) 先按分割槽聚合 再總的聚合 每次要跟初始值交流 例如:aggregateByKey(0)(+,+) 對k/y的RDD進行操作
sortByKey([ascending], [numTasks]) 在一個(K,V)的RDD上呼叫,K必須實現Ordered介面,返回一個按照key進行排序的(K,V)的RDD
sortBy(func,[ascending], [numTasks]) 與sortByKey類似,但是更靈活
join(otherDataset, [numTasks]) 在型別為(K,V)和(K,W)的RDD上呼叫,返回一個相同key對應的所有元素對在一起的(K,(V,W))的RDD
cogroup(otherDataset, [numTasks]) 在型別為(K,V)和(K,W)的RDD上呼叫,返回一個(K,(Iterable,Iterable))型別的RDD
cartesian(otherDataset) 笛卡爾積
pipe(command, [envVars]) 呼叫外部程式
coalesce(numPartitions) 重新分割槽 第一個引數是要分多少區,第二個引數是否shuffle 預設false ;少分割槽變多分割槽 true ; 多分割槽變少分割槽 false
repartition(numPartitions) 重新分割槽 必須shuffle 引數是要分多少區 少變多
repartitionAndSortWithinPartitions(partitioner) 重新分割槽+排序 比先分割槽再排序效率高 對K/V的RDD進行操作

3.2 Action

觸發程式碼的執行操作,我們一個Spark 應用,至少需要一個 Action 操作。

動作 含義
reduce(func) 通過func函式聚集RDD中的所有元素,這個功能必須是課交換且可並聯的
collect() 在驅動程式中,以陣列的形式返回資料集的所有元素
count() 返回RDD的元素個數
first() 返回RDD的第一個元素(類似於take(1))
take(n) 返回一個由資料集的前n個元素組成的陣列
takeSample(withReplacement,num, [seed]) 返回一個陣列,該陣列由從資料集中隨機取樣的num個元素組成,可以選擇是否用隨機數替換不足的部分,seed用於指定隨機數生成器種子
takeOrdered(n, [ordering])
saveAsTextFile(path) 將資料集的元素以textfile的形式儲存到HDFS檔案系統或者其他支援的檔案系統,對於每個元素,Spark將會呼叫toString方法,將它裝換為檔案中的文字
saveAsSequenceFile(path) 將資料集中的元素以Hadoop sequencefile的格式儲存到指定的目錄下,可以使HDFS或者其他Hadoop支援的檔案系統。
saveAsObjectFile(path)
countByKey() 針對(K,V)型別的RDD,返回一個(K,Int)的map,表示每一個key對應的元素個數。
foreach(func) 在資料集的每一個元素上,執行函式func進行更新。
foreachPartition(func) 在每個分割槽上,執行函式 func

3.3 Spark WordCount 程式碼示例

執行流程圖:

wc執行流程圖

pom.xml 依賴

<!-- 匯入scala的依賴 -->
<dependency>
    <groupId>org.scala-lang</groupId>
    <artifactId>scala-library</artifactId>
    <version>2.2.0</version>
</dependency>
<!-- 匯入spark的依賴 -->
<dependency>
    <groupId>org.apache.spark</groupId>
    <artifactId>spark-core_2.11</artifactId>
    <version>2.2.0</version>
</dependency>

<!-- 指定hadoop-client API的版本 -->
<dependency>
    <groupId>org.apache.hadoop</groupId>
    <artifactId>hadoop-client</artifactId>
    <version>2.6.0</version>
</dependency>
複製程式碼

scala 版本程式碼實現:

package com.zhouq.spark

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

/**
  * scala 版本實現 wc
  *
  */
object ScalaWordCount {
  def main(args: Array[String]): Unit = {
    //這行程式碼是因為我在windows 上直接跑,需要去讀取 hadoop 上的檔案,設定我的使用者名稱。如果是linux 環境可以不設定。視情況而定
    System.setProperty("HADOOP_USER_NAME", "root")
    //建立spark 配置,設定應用程式名字
//    val conf = new SparkConf().setAppName("scalaWordCount")
    val conf = new SparkConf().setAppName("scalaWordCount").setMaster("local[4]")

//    conf.set("spark.testing.memory","102457600")
    //建立spark 執行的入口
    val sc = new SparkContext(conf)

    //指定以後從哪裡讀取資料建立RDD (彈性分散式資料集)
    //取到一行資料
    val lines: RDD[String] = sc.textFile(args(0))

    //切分壓平
    val words: RDD[String] = lines.flatMap(_.split(" "))

    //按單詞和一組合
    val wordAndOne: RDD[(String, Int)] = words.map((_, 1))

    //按key 進行聚合
    val reduced: RDD[(String, Int)] = wordAndOne.reduceByKey(_ + _)

    // 排序, false 表示倒序
    val sorted = reduced.sortBy(_._2, false)

    //將結果儲存到hdfs中
    sorted.saveAsTextFile(args(1))

    //釋放資源
    sc.stop()
  }
}
複製程式碼

Java7 版本:

package com.zhouq.spark;

import org.apache.spark.SparkConf;
import org.apache.spark.api.java.JavaPairRDD;
import org.apache.spark.api.java.JavaRDD;
import org.apache.spark.api.java.JavaSparkContext;
import org.apache.spark.api.java.function.FlatMapFunction;
import org.apache.spark.api.java.function.PairFunction;
import scala.Tuple2;
import java.util.Arrays;
import java.util.Iterator;

/**
* Java 版WordCount
*/
public class JavaWordCount {
    public static void main(String[] args) {
        SparkConf conf = new SparkConf().setAppName("JavaWordCount");
        //建立SparkContext
        JavaSparkContext jsc = new JavaSparkContext(conf);
        //指定讀取資料的位置
        JavaRDD<String> lines = jsc.textFile(args[0]);

        //切分壓平
        JavaRDD<String> words = lines.flatMap(new FlatMapFunction<String, String>() {
            @Override
            public Iterator<String> call(String line) throws Exception{
                return Arrays.asList(line.split(" ")).iterator();
            }
        });

        //將單詞進行組合 (a,1),(b,1),(c,1),(a,1)
        JavaPairRDD<String, Integer> wordAndOne = words.mapToPair(new PairFunction<String, String, Integer>() {
            @Override
            public Tuple2<String, Integer> call(String tp) throws Exception {
                return new Tuple2<>(tp, 1);
            }
        });

        //先交換再排序,因為 只有groupByKey 方法
        JavaPairRDD<Integer, String> swaped = wordAndOne.mapToPair(new PairFunction<Tuple2<String, Integer>, Integer, String>() {
            @Override
            public Tuple2<Integer, String> call(Tuple2<String, Integer> tp) throws Exception {
//                return new Tuple2<>(tp._2, tp._1);
                return tp.swap();
            }
        });

        //排序
        JavaPairRDD<Integer, String> sorted = swaped.sortByKey(false);

        //再次交換順序
        JavaPairRDD<String, Integer> result = sorted.mapToPair(new PairFunction<Tuple2<Integer, String>, String, Integer>() {
            @Override
            public Tuple2<String, Integer> call(Tuple2<Integer, String> tp) throws Exception {
                return tp.swap();
            }
        });

        //輸出到hdfs
        result.saveAsTextFile(args[1]);

        jsc.stop();
    }
}
複製程式碼

Java8 版本:

package com.zhouq.spark;

import org.apache.spark.SparkConf;
import org.apache.spark.api.java.JavaPairRDD;
import org.apache.spark.api.java.JavaRDD;
import org.apache.spark.api.java.JavaSparkContext;
import org.apache.spark.api.java.function.FlatMapFunction;
import org.apache.spark.api.java.function.PairFunction;
import scala.Tuple2;
import java.util.Arrays;

/**
* Java Lambda 表示式版本的  WordCount
*/
public class JavaLambdaWordCount {

    public static void main(String[] args) {

        SparkConf conf = new SparkConf().setAppName("JavaWordCount");
        //建立SparkContext
        JavaSparkContext jsc = new JavaSparkContext(conf);
        //指定讀取資料的位置
        JavaRDD<String> lines = jsc.textFile(args[0]);

        //切分壓平
//        lines.flatMap(line -> Arrays.asList(line.split(" ")).iterator());
        JavaRDD<String> words = lines.flatMap((FlatMapFunction<String, String>) line -> Arrays.asList(line.split(" ")).iterator());

        //將單詞進行組合 (a,1),(b,1),(c,1),(a,1)
//        words.mapToPair(tp -> new Tuple2<>(tp,1));
        JavaPairRDD<String, Integer> wordAndOne = words.mapToPair((PairFunction<String, String, Integer>) tp -> new Tuple2<>(tp, 1));

        //先交換再排序,因為 只有groupByKey 方法
//        swaped.mapToPair(tp -> tp.swap());
        JavaPairRDD<Integer, String> swaped = wordAndOne.mapToPair((PairFunction<Tuple2<String, Integer>, Integer, String>) tp -> {
//                return new Tuple2<>(tp._2, tp._1);
            return tp.swap();
        });

        //排序
        JavaPairRDD<Integer, String> sorted = swaped.sortByKey(false);

        //再次交換順序
//        sorted.mapToPair(tp -> tp.swap());
        JavaPairRDD<String, Integer> result = sorted.mapToPair((PairFunction<Tuple2<Integer, String>, String, Integer>) tp -> tp.swap());

        //輸出到hdfs
        result.saveAsTextFile(args[1]);

        jsc.stop();
    }
}
複製程式碼

4 RDD 的依賴關係

RDD 和它依賴的 父 RDD(可能有多個) 的關係有兩種不同的型別,即 窄依賴(narrow dependency)和寬依賴(wide dependency)。

在這裡插入圖片描述

窄依賴:窄依賴指的是每一個父 RDD 的 Partition 最多被子 RDD 的一個分割槽使用。可以比喻為獨生子女。 寬依賴:寬依賴是多個字 RDD 的Partition 會依賴同一個父 RDD 的 Partition

5 RDD 的持久化

5.1 RDD 的 cache(持久化)

Spark中最重要的功能之一是跨操作在記憶體中持久化(或快取)資料集。當您持久儲存RDD時,每個節點都會儲存它在記憶體中計算的任何分割槽,並在該資料集(或從中派生的資料集)的其他操作中重用它們。這使得未來的行動更快(通常超過10倍)。快取是迭代演算法和快速互動使用的關鍵工具。

您可以使用persist()或cache()方法標記要保留的RDD 。第一次在動作中計算它,它將保留在節點的記憶體中。Spark的快取是容錯的 - 如果丟失了RDD的任何分割槽,它將使用最初建立它的轉換自動重新計算。

5.2 什麼時候我們需要持久化?

  1. 要求的計算速度快
  2. 叢集的資源要足夠大
  3. 重要: cache 的資料會多次觸發Action
  4. 建議先進行資料過濾,然後將縮小範圍後的資料再cache 到記憶體中.

5.3 如何使用

使用 rdd.persist()或者rdd.cache()

val lines: RDD[String] = sc.textFile("hdfs://xxx/user/accrss")
//使用cache 方法來快取資料到記憶體
val cache = lines.cache()
//注意檢視下面兩次count 的時間
cached.count
cached.count

複製程式碼

5.4 資料快取的儲存級別 StorageLevel

我們在 StorageLevel.scala 原始碼中可以看到:

val NONE = new StorageLevel(false, false, false, false)
val DISK_ONLY = new StorageLevel(true, false, false, false)
val DISK_ONLY_2 = new StorageLevel(true, false, false, false, 2)
val MEMORY_ONLY = new StorageLevel(false, true, false, true)
val MEMORY_ONLY_2 = new StorageLevel(false, true, false, true, 2)
val MEMORY_ONLY_SER = new StorageLevel(false, true, false, false)
val MEMORY_ONLY_SER_2 = new StorageLevel(false, true, false, false, 2)
val MEMORY_AND_DISK = new StorageLevel(true, true, false, true)
val MEMORY_AND_DISK_2 = new StorageLevel(true, true, false, true, 2)
val MEMORY_AND_DISK_SER = new StorageLevel(true, true, false, false)
val MEMORY_AND_DISK_SER_2 = new StorageLevel(true, true, false, false, 2)
val OFF_HEAP = new StorageLevel(true, true, true, false, 1)
複製程式碼

解釋一下各個引數的意思:

第一個參數列示: 放到磁碟 第二個參數列示: 放到記憶體 第三個參數列示: 磁碟中的資料是否以Java 物件的方式儲存,true 表示是, false表示以序列化的方式存放 第四個參數列示: 記憶體中的資料是否以Java 物件的方式儲存,true 表示是, false表示以序列化的方式存放 第五個參數列示: 存放幾份資料(目的是為了怕executor 出現故障導致分割槽資料丟失,當重新分配任務時,去另外的機器讀取備份資料進行重新計算)

OFF_HEAP : 堆外記憶體,以序列化的格式儲存RDD到Tachyon(一個分散式記憶體儲存系統)中

5.5 如何選擇儲存級別

Spark的多個儲存級別意味著在記憶體利用率和cpu利用效率間的不同權衡。我們推薦通過下面的過程選擇一個合適的儲存級別:

  1. 如果你的RDD適合預設的儲存級別(MEMORY_ONLY),就選擇預設的儲存級別。因為這是cpu利用率最高的選項,會使RDD上的操作儘可能的快。
  2. 如果不適合用預設的級別,選擇MEMORY_ONLY_SER。選擇一個更快的序列化庫提高物件的空間使用率,但是仍能夠相當快的訪問。
  3. 除非函式計算RDD的花費較大或者它們需要過濾大量的資料,不要將RDD儲存到磁碟上,否則,重複計算一個分割槽就會和重磁碟上讀取資料一樣慢。
  4. 如果你希望更快的錯誤恢復,可以利用重複(replicated)儲存級別。所有的儲存級別都可以通過重複計算丟失的資料來支援完整的容錯,但是重複的資料能夠使你在RDD上繼續執行任務,而不需要重複計算丟失的資料。
  5. 在擁有大量記憶體的環境中或者多應用程式的環境中,OFF_HEAP具有如下優勢:
    1. 它執行多個執行者共享Tachyon中相同的記憶體池
    2. 它顯著地減少垃圾回收的花費
    3. 如果單個的執行者崩潰,快取的資料不會丟失

5.6 刪除 cache

Spark自動的監控每個節點快取的使用情況,利用最近最少使用原則刪除老舊的資料。如果你想手動的刪除RDD,可以使用 RDD.unpersist()方法

5.7 RDD 的 checkpoint機制

我們除了把資料快取到記憶體中,還可以把資料快取到HDFS 中,保證中間資料不丟失.

什麼時候我們需要做chechpoint?

  1. 做複雜的迭代計算,要求保證資料安全,不丟失
  2. 對速度要求不高(跟 cache 到記憶體進行對比)
  3. 將中間結果儲存到 hdfs 中

怎麼做 checkpoint ?

首先設定 checkpoint 目錄,然後再執行計算邏輯,再執行 checkpoint() 操作。

下面程式碼使用cache 和 checkpoint 兩種方式實現計算每門課最受歡迎老師的 topN

package com.zhouq.spark

import java.net.URL
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

/**
  * 求每門課程最受歡迎老師TopN  --2
  *   -- 使用cache
  *   -- 使用checkpoint 一般設定hdfs 目錄
  */
object GroupFavTeacher2_cache_checkpoint {
  def main(args: Array[String]): Unit = {
    //前 N
    val topN = args(1).toInt
    //學科集合
    val subjects = Array("bigdata", "javaee", "php")
    val conf = new SparkConf().setAppName("FavTeacher").setMaster("local[4]")
    //建立spark 執行入口
    val sc = new SparkContext(conf)
    //checkpoint 得先設定 sc 的checkpoint 的dir
//    sc.setCheckpointDir("hdfs://hdfs://hadoop1:8020/user/root/ck20190215")

    //指定讀取資料
    val lines: RDD[String] = sc.textFile(args(0))
    val subjectTeacherAndOne: RDD[((String, String), Int)] = lines.map(line => {
      val index = line.lastIndexOf("/")
      var teacher = line.substring(index + 1)
      var httpHost = line.substring(0, index)
      var subject = new URL(httpHost).getHost.split("[.]")(0)
      ((subject, teacher), 1)
    })
    //將學科,老師聯合當做key
    val reduced: RDD[((String, String), Int)] = subjectTeacherAndOne.reduceByKey(_ + _)

    //第一種使用cache RDD 把資料快取在記憶體中.標記為cache 的RDD 以後被反覆使用,才使用cache
    val cached: RDD[((String, String), Int)] = reduced.cache()

    //第二種 使用checkpoint,得先設定 sc 的 checkpointDir
//   val cached: RDD[((String, String), Int)] = reduced.checkpoint()

    /**
      * 先對學科進行過濾,然後再進行排序,呼叫RDD 的sortBy進行排序,避免scala 的排序當資料量大時,記憶體不足的情況.
      * take 是Action 操作,每次take 都會進行一次任務提交,具體檢視日誌列印情況
      */
    for (sub <- subjects) {
      //過濾出當前的學科
      val filtered: RDD[((String, String), Int)] = cached.filter(_._1._1 == sub)
      //使用RDD 的 sortBy ,記憶體+磁碟排序,避免scala 中的排序因記憶體不足導致異常情況.
      //take 是Action 的,所以每次迴圈都會觸發一次提交任務,祥見日誌列印情況
      val favTeacher: Array[((String, String), Int)] = filtered.sortBy(_._2, false).take(topN)
      println(favTeacher.toBuffer)
    }

    /**
      * 前面cache的資料已經計算完了,後面還有很多其他的指標要計算
      * 後面計算的指標也要觸發很多次Action,最好將資料快取到記憶體
      * 原來的資料佔用著記憶體,把原來的資料釋放掉,才能快取新的資料
      */

    //把原來快取的資料釋放掉
    cached.unpersist(true)

    sc.stop()
  }
}
複製程式碼

6 DAG 的生成

DAG(Directed Acyclic Graph)叫做有向無環圖,原始的RDD通過一系列的轉換就就形成了DAG,根據RDD之間的依賴關係的不同將DAG劃分成不同的Stage,對於窄依賴,partition的轉換處理在Stage中完成計算。對於寬依賴,由於有Shuffle的存在,只能在parent RDD處理完成後,才能開始接下來的計算,因此寬依賴是劃分Stage的依據。

在這裡插入圖片描述

微信公眾號文章連結:Spark RDD

有興趣歡迎關注,大家一起交流學習。

在這裡插入圖片描述

相關文章