Spark學習三:RDD介紹及編程

Overview(總覽)

Spark提供的主要抽象就是彈性分佈式數據集 - - RDD(resilient distributed dataset),它是跨集羣節點的分區元素的集合(RDD是有分區的),是可以並行操作的。
RDD的創建有兩種方式:(一)從Hadoop文件系統(或任何其他Hadoop支持的文件系統)的文件讀取創建 (二)從driver program的已存在的scala集合中創建並轉化。
用戶可以要求Spark將RDD持久化到內存中,這樣在並行操作時就能有效地重用它。最後RDD可以從節點故障中自動恢復。

Spark的第二個抽象就是在並行操作中使用的共享變量shared variables。當Spark執行一個並行操作的時候,會將函數中使用到的變量複製到每一個task裏。有時候,一個變量需要在多個task之間、或者是task和driver program之間進行共享。
Spark支持兩種類型的共享變量:(一)broadcast variables(廣播變量),能將一個變量緩存到所有節點的內存中 (二)accumators(累加器),只能用來做"added"追加操作,例如計數和求和操作。

Linking with Spark

添加maven依賴,可以參考Spark學習一:安裝、IDEA編寫代碼

<properties>
    <!--解決maven打包時編碼不對的問題-->
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <spark.version>2.4.5</spark.version>
    <scala.version>2.11</scala.version>
</properties>
<dependencies>
    <dependency>
        <groupId>org.apache.spark</groupId>
        <artifactId>spark-core_${scala.version}</artifactId>
        <version>${spark.version}</version>
    </dependency>
    <dependency>
        <groupId>org.apache.spark</groupId>
        <artifactId>spark-streaming_${scala.version}</artifactId>
        <version>${spark.version}</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

Initializing Spark

一個spark program首先要做的就是創建一個SparkContext對象,它告訴spark如何訪問一個集羣。爲了創建一個SparkContext對象首先要做的就是創建一個SparkConf對象,SparkConf對象包含了application的信息。
每個JVM只會有一個active的SparkContext,在創建新的SparkContext對象之前必須要執行SparkContext.stop()來停止active的SparkContext

val conf = new SparkConf().setAppName(appName).setMaster(master)
val sparkContext = new SparkContext(conf)

appName參數是顯示在ui界面的application的名字,master參數是application選擇哪個cluster manager(spark standalone、mesos、yarn)或者本地模式local。
在正式環境下一般都不會在代碼裏對master做硬編碼,會在使用spark-submit提交作業時用參數--master來指定。

Using the Shell

當你啓動 spark-shell 的時候,會自動的創建一個 SparkContext 對象,叫 “sc”,如果你打算手動創建一個 SparkContext,它是不會起作用的。可以用 --master 參數來告訴 SparkContext 連接到哪個master上。可以用 --jars 參數(多個jar用逗號隔開)來將一些jar包導入到driver program和executor的classpath裏。可以用--packages--repositories來導入更復雜的依賴。
可以通過 spark-shell --help 來查詢更詳細的信息,下圖是截取的部分信息
在這裏插入圖片描述

spark-shell --master local[*] --jars code.jar

Resilient Distributed Datasets (RDDs)

彈性分佈式數據集RDD 是具有高容錯性且能並行執行的數據集合。有兩種方式去創建RDDS:(1) 對driver program程序上的已存在的集合做parallelizing化; (2) 引用外部存儲系統的數據集來創建,例如HDFS、HBase、或任何Hadoop支持的存儲系統。

Parallelized Collections(並行化集合)

Parallelized collections是通過在driver program程序中對已存在的collection調用SparkContext.parallelize方法來創建的。集合的元素會被複制稱爲一個可以並行操作的分佈式數據集RDD。如下面所示,根據1-5的數據集合來創建一個Parallelized collections即RDD

val data = Array(1, 2, 3, 4, 5)
val distData = sc.parallelize(data)

在這裏插入圖片描述
一旦創建,分佈式數據集RDD就能夠並行操作了。例如我們可以做累加操作distData.reduce((a, b) => a + b)。具體對RDD可以做哪些操作後面有詳細的介紹。
在這裏插入圖片描述
對於parallel collections一個很重要的參數就是將數據集分成多少分區即the number of partitionsSpark會對集羣上的每個partition都會執行一個task,即partition數決定了task的並行度。通常而言,集羣中每個CPU對應2-4個partition。正常情況下,Spark會自動根據你的集羣來設置分區數。你也可以通過傳遞第二個參數手動地設置分區數(如sparkContext.textFile(path=path,minPartitions = 3))。注意:代碼裏有些地方用了術語slices(和partitions是一樣的意思)來保持向後兼容。

External Datasets(外部數據集)

Spark可以從任何Hadoop支持的存儲系統來創建彈性分佈式數據集RDD,包括本地文件系統、HDFS、Cassandra、HBase、 Amazon S3等。Spark支持text files、SequenceFIles和任意的Hadoop InputFormat
可以使用sparkContext.textFile方法來創建Text file RDDs。該方法以文件的URI路徑(如本地路徑、hdfs://、s3d://等)作爲參數,然後將文件的內容按行讀取成一個集合即RDD。如下所示
默認讀取的就是hdfs路徑

scala> val path = "/user/root/input/words.txt"
path: String = /user/root/input/words.txt

scala> val textFileRDD = sc.textFile(path)
textFileRDD: org.apache.spark.rdd.RDD[String] = /user/root/input/words.txt MapPartitionsRDD[1] at textFile at <console>:26

獲取到RDD後就可以進行操作了,例如可以看下文件有多少個字符,先做map操作獲取每行的字符數,然後做reduce操作進行累加。

scala> textFileRDD.map(s=>s.length).reduce(_+_)
res2: Int = 186

用Spark讀取文件時需要注意以下幾點

  • 如果使用的是本地文件系統,那麼文件的路徑必須在所有的worker node上都是可訪問的(因爲是分佈式計算任務)。要麼拷貝文件到所有的worker node上,要麼使用網絡掛載的共享文件系統。
  • Spark 所有讀取文件的方法 ,包括textFile,同樣支持對文件夾directory、壓縮文件compressed files的讀取,還允許讀取的路徑中帶有通配符,也可以用逗號分隔讀取多個文件。例如textFile("/my/directory"), textFile("/my/directory/*.txt"), and textFile("/my/directory/*.gz"), textFile("/my/directory1,/my/directory2")
  • textFile方法也提供一個可選的第二個參數來控制分區數。默認情況下,Spark會爲文件的每個block創建一個partition(HDFS的block size默認是128MB) ,但是你也可以通過傳入一個更大的值,來指定更多的 partitions。注意不能指定比block數還要小的分區數

除了文本文件,Spark的Scala API還支持其他幾種數據格式:

  • SparkContext.wholeTextFiles 會讀取目錄裏所有的文件(建議都是小文件),然後返回給一個元素類型爲Tuple:(文件路徑, 文件內容)的RDD。這個方法與textFile是完全不同的,textFile是按行讀取內容成爲一個元素類型爲String的RDD。分區由數據位置data locality決定,在某些情況下,可能導致分區太少。對於這些情況,wholeTextFiles也提供了一個可選的第二個參數來控制分區的最小數量。
  • 對於 SequenceFiles,可以使用 SparkContext.sequenceFile[K, V](path: String,keyClass: Class[K],valueClass: Class[V]): RDD[(K, V)]方法創建。keyClass/valueClass必須是 Hadoop 的 Writable interface 的子類,例如IntWritable 和 Text 。另外,對於一些通用的 Writable,Spark 允許你指定原生類型native types來替代。如:sequencFile[Int, String] 將會自動讀取 IntWritablesTextssc.sequenceFile[Int,String]("hdfs:///path")
  • 對於其他的Hadoop InputFormats,可以使用SparkContext.hadoopRDD方法。最好是使用SparkContext.newAPIHadoopRDD,因爲這個是基於新的MapReduce API (org.apache.hadoop.mapreduce)
    可以參考下面的例子
val path = "/user/root/input/words.txt"
val conf = new Configuration()
conf.set(FileInputFormat.INPUT_DIR,path);
//注意newAPIHadoopRDD方法裏註釋給的提示,在使用返回的RDD前最好是用map做下複製
//因爲Hadoop的RecordReader類爲每個記錄重用相同的可寫對象
val kvRDD = sc.newAPIHadoopRDD(conf,
  //如果不加上asSubclass會報錯 do not conform to method newAPIHadoopRDD's type parameter bounds [K,V,F <: org.apache.hadoop.mapreduce.InputFormat[K,V]]
  // https://stackoverflow.com/questions/37938695/spark-cannot-compile-newapihadooprdd-with-mongo-hadoop-connectors-bsonfileinput
  classOf[TextInputFormat].asSubclass(classOf[InputFormat[LongWritable,Text]]),
  classOf[LongWritable],
  classOf[Text])

//注意返回給driver之前需要做序列化,如果不做map這個過程會報錯
//Job aborted due to stage failure: Task 0.0 in stage 1.0 (TID 1) had a not serializable result: org.apache.hadoop.io.LongWritable
// https://stackoverflow.com/questions/28159185/streaming-from-hbase-using-spark-not-serializable?r=SearchResults
kvRDD.map(x=>x._2.toString()).collect().foreach(println)
val wcRDD = kvRDD.map(x=>x._2.toString()).flatMap(x=>x.split(" ")).map(x=>(x,1)).reduceByKey(_+_)
wcRDD.collect().foreach(println)
  • RDD.saveAsObjectFileSparkContext.objectFile 支持以由序列化Java對象組成的簡單格式保存RDD。雖然這不如Avro這樣的專門格式有效,但是它提供了一種簡單的方法來保存任何RDD。其實RDD.saveAsObjectFile底層就是將RDD的數據保存爲Hadoop的SequenceFile,然後SparkContext.objectFile就是通過調用SparkContext.sequenceFile來讀取文件。也可以關注下RDD.saveAsTextFile方法,將RDD數據以元素的string形式保存爲文本文件,也支持壓縮方法,然後用SparkContext.textFile方法讀取時如果是壓縮的也會自動解壓縮
    在這裏插入圖片描述
    在這裏插入圖片描述

RDD Operations(RDD操作)

RDD支持兩種操作:
(1)transformation,用於從一個已存在的RDD來創建新的RDD
(2)action,對RDD數據進行計算然後返回值給driver program
例如,map就是一個transformation,會將RDD的每個元素都經過function(map的參數,外部傳過來的)然後返回一個表示結果的新的RDD。另一方面,reduce就是一個action,會用一個function(reduce的參數,外部傳過來的)來聚合RDD裏的所有元素,然後把最終結果返回給driver program(注意:有一個並行的reduceByKey會返回一個RDD,不過這是一個transformation)

Spark裏所有的transformation都是lazy的,它們並不會立即計算它們的結果。然而,它們僅僅記錄應用到RDD的transformation。僅當一個action需要結果返回給driver programtransformation纔會真正的開始計算。這種設計讓Spark運行的更加高效。例如,從map過來的RDD會在reduce中用到並且僅需要返回reduce的結果給driver,而不是更大的mapper數據集。

默認情況下,每次有在執行一個action時每個ransformed RDD都會被重新計算。然而,你也可以通過persist (or cache)方法來在內存裏保存RDD,這樣的話Spark就會將數據保存到集羣上,那麼下次你需要再次用到這個RDD時就不需要重算就能工作的更快。也支持持久化RDD到磁盤上,也支持給RDD在節點間創建副本。

Basics(基礎)

看下面的程序段來理解RDD基礎

val lines = sc.textFile("data.txt")
val lineLengths = lines.map(s => s.length)
val totalLength = lineLengths.reduce((a, b) => a + b)

第一行就是從外部文件來創建一個RDD。該數據集是沒有加載到內存上的:lines僅僅只是一個對這個文件的pointer。第二行就是獲取每行的長度作爲map transformation的結果。同樣地,lineLengths由於laziness也不會立刻計算。最後,我們執行reduce這個action。此時,Spark將計算分解爲在不同的機器上運行的task,每臺機器都運行其part of the map and a local reduction,只向driver program返回其答案。
如果我們後面程序段會再次用到lineLengths,可以在執行reduce操作前通過lineLengths.persist()來持久化lineLengths這個RDD,這樣會在lineLengths第一次被計算時會持久化到內存裏。

Passing Functions to Spark(傳遞function給Spark)

Spark的API嚴重依賴於driver program中傳遞的函數去在集羣上運行。有兩個比較建議的方式去傳遞function:

  • 匿名函數(Anonymous function syntax),用在代碼段量小的地方
  • 單例object的靜態方法(scala是通過object關鍵字來定義單例對象的)。看下面的例子,定義了object MyFunctions然後傳遞MyFunctions.func1
object MyFunctions {
  def func1(s: String): String = { ... }
}

myRdd.map(MyFunctions.func1)

注意儘管也可以通過傳遞class實例(和singleton object做比較)中方法的引用,但這需要傳遞包含這個方法的class的實例對象。考慮下面的場景:

class MyClass {
  def func1(s: String): String = { ... }
  def doStuff(rdd: RDD[String]): RDD[String] = { rdd.map(func1) }
}

這裏如果我們創建一個新的MyClass實例對象並且調用它的doStuff方法,doStuff方法裏面的map引用了該MyClass實例對象的func1方法,因此整個MyClass實例對象就需要傳遞給集羣。這等同於rdd.map(x => this.func1(x))

同樣地,訪問外部對象的字段將引用整個對象:

class MyClass {
  val field = "Hello"
  def doStuff(rdd: RDD[String]): RDD[String] = { rdd.map(x => field + x) }
}

上面的代碼等同於rdd.map(x => this.field + x),因此也需要傳遞整個MyClass實例對象給集羣。
爲了避免這種情況,最簡單的方法是將字段複製到一個局部變量中,而不是從外部訪問它,如:

def doStuff(rdd: RDD[String]): RDD[String] = {
  val field_ = this.field
  rdd.map(x => field_ + x)
}
Understanding closures(理解閉包)

Spark的難點之一是理解跨集羣執行代碼時變量和方法的範圍和生命週期。在範圍之外修改變量的RDD操作可能經常引起混淆。在下面的示例中,我們將查看使用foreach()遞增counter的代碼,但是其他操作也可能出現類似的問題。

Example

考慮下面簡單的RDD元素求和,它的結果可能會根據是否在相同的JVM中執行而有所不同。
比較通用的例子就是以本地模式(--master = local[n])運行Spark和以Spark on yarn(spark-submit to YARN)的模式運行Spark來做比較。

var counter = 0
var rdd = sc.parallelize(data)

// Wrong: Don't do this!!
rdd.foreach(x => counter += x)

println("Counter value: " + counter)
Local vs. cluster modes(本地模式VS集羣模式)

上面代碼的結果可能並不像我們想象中那樣累計求和。爲了執行作業,Spark將RDD操作分解爲tasks,每個task由一個executor執行。在執行之前,Spark會先計算task的closure(閉包)。closure是那些在RDD上執行其計算時必須可見的變量和方法(在上面的例子中爲foreach())。這個closure被序列化併發送給每個executor

發送給每個executorclosure裏的變量其實是copies(副本)。因此在foreach函數中引用counter時,它不再是driver program節點上的counter。在driver program節點的內存裏仍然存在一個counter,但是對executors是不可見的!!!executor僅僅能看到序列化的closure裏的副本。因此,counter的最終值仍然是零,因爲counter上的所有操作都引用了序列化closure中的值。
在這裏插入圖片描述
在本地模式下,在某些情況下,foreach函數實際上會在與驅動程序相同的JVM中執行,並引用相同的原始計數器,並可能實際更新它。

爲了確保在該類場景下的正確行爲,應該要使用累加器(Accumulator)。Spark中的Accumulator專門用於提供一種機制,以便當任務在集羣中的工作節點上執行時安全地更新變量。下面有專門的部分更加詳細地講述了Accumulator

一般來說,像循環或局部定義的方法這樣的閉包結構(closures - constructs)不應該用來改變全局狀態。Spark不定義也不保證閉包外部引用的對象的行爲。一些這樣做的代碼可能在本地模式下工作,但那只是偶然的,而且這樣的代碼在分佈式模式下不會像預期的那樣工作。如果需要全局聚合( global aggregation),則使用累加器(Accumulator)。

Printing elements of an RDD(打印RDD中的元素)

另一常見的習慣用法就是使用rdd.foreach(println)或者rdd.map(println)來打印RDD中的元素。在一臺機器上,這將生成預期的輸出並打印所有的RDD元素。然而,在集羣模式下executors調用stdout的輸出是寫入executors的stdout文件上,而不是driver program的stdout上。所有driver program的stdout上是看不到這些元素的! 我們可以首先使用collect()方法把RDD數據收集到driver program節點上然後再打印即rdd.collect().foreach(println)。這可能會導致driver program節點耗盡內存,因爲collect()會把整個RDD數據收集到
driver program節點這一臺機器上。如果你僅僅需要打印RDD中的少量元素,更加安全的操作是使用take(),如rdd.take(100).foreach(println)

Working with Key-Value Pairs(使用Key-Value對)

雖然大多數Spark操作是在包含任何類型對象的RDD上工作,但也有少數特殊操作在鍵值對的RDDs上工作。最常見的就是分佈式“shuffke”操作,如groupByreduceByKey
在Scala中,這些操作在包含Tuple2對象(scala語言中內置的元組,通過簡單的編寫(a, b)創建)的RDDs上自動可用。鍵值對操作在PairRDDFunctions類中是可用的,它自動包裝元組的RDD。

例如,下面的代碼使用基於key-value對的reduceByKey操作來計算每行文本在文件中出現的次數

val lines = sc.textFile("data.txt")
val pairs = lines.map(s => (s, 1))
val counts = pairs.reduceByKey((a, b) => a + b)

我們也可以使用counts.sortByKey()來按字母順序對鍵值對進行排序,並且最後可以使用counts.collect()來把所有的數據作爲數組返回給driver program
注意:在鍵-值對操作中使用自定義對象作爲key時,必須確保自定義equals()方法和hashCode()方法。更加詳細的內容可以參考Object.hashCode() documentation.
在這裏插入圖片描述

Transformations And Actions(兩種算子)

請參考我寫的另一篇博客RDD兩種算子

Shuffle operations(shuffle操作)

shuffle是Spark用於重新分佈數據的機制,以便在分區之間以不同的方式進行分組。這通常會涉及到跨executor和節點複製數據,使得shuffle成爲一個複雜而昂貴的操作。

Background(背景)

爲了明白在shuffle期間到底發生了,我們以reduceByKey爲例來說明下。reduceByKey操作生成一個新的RDD,其中單個key的所有值都被組合成一個元組即key和對與該鍵關聯的所有值執行reduce函數的結果。難點在於,單個key的所有值不一定都位於相同的分區,甚至也不一定位於同一臺機器上,但它們必須位於同一位置才能計算結果。

在Spark中,數據通常不會跨分區分佈到特定操作所需的位置。在計算期間,單個task將在單個partition上操作——因此,要組織單個reduceByKey reduce任務執行的所有數據,Spark需要執行all-to-all操作。它必須從所有分區中讀取所有鍵的值,然後將各個分區的值放在一起,以計算每個key的最終結果——這稱爲shuffle。

儘管shuffle後的數據的每個分區中的元素集是確定的,分區本身的排序也是確定的,但是這些元素的排序是不確定的。如果一個人想要在shuffle之後得到有序數據,那麼可以使用:

  • mapPartitions對每個分區進行排序,例如 .sorted對分區內的數據進行排序
  • repartitionAndSortWithinPartitions可以在進行重新分區的同時對分區進行排序
  • 使用sortBy函數來排序,其實底層用的就是sortByKey,參數是一個函數
/**
   * Return this RDD sorted by the given key function.
   */
  def sortBy[K](
      f: (T) => K,
      ascending: Boolean = true,
      numPartitions: Int = this.partitions.length)
      (implicit ord: Ordering[K], ctag: ClassTag[K]): RDD[T]
val numRDD = sc.parallelize(Array(2,3,1,0,5,9,6,7,8,20,11,4,18),3)
scala> numRDD.mapPartitionsWithIndex((index,iter)=>{
     |       iter.toList.sorted.iterator
     |     }).collect()
res4: Array[Int] = Array(0, 1, 2, 3, 5, 6, 7, 9, 4, 8, 11, 18, 20)
scala> numRDD.sortBy(x=>x).collect()
res8: Array[Int] = Array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 18, 20)

scala> numRDD.sortBy(x=>x,false).collect()
res9: Array[Int] = Array(20, 18, 11, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)
Performance Impact(性能影響)

shuffle是一個非常昂貴的操作因爲它涉及到了磁盤IO,數據序列化以及網絡IO。爲了在shuffle期間組織數據,Spark生成了很多任務---- map task用於組織數據,reduce task用於聚合。這個術語來自MapReduce,但是與Spark的map和reduce操作沒有直接關係。

在內部,單個map task的結果會保存到內存裏直到內存放不下。然後數據基於目標分區進行排序然後寫到一個文件裏(這個過程可以參考MapReduce裏的shuffle過程)。在reduce端,reduce task獲取相對應的排序數據塊。

某些shuffle操作會消耗大量的堆內存因爲它們需要在傳輸數據之前和之後用到內存裏的數據結構去組織數據。特別地,reduceByKeyaggregateByKey在map端創建這些數據結構,然後ByKey操作在reduce端生成這些結構。當內存放不下數據的時候,Spark會溢出一些數據到磁盤,會導致額外的磁盤IO開銷及增加GC操作。

shuffle也會在磁盤生成大量的中間文件。從Spark1.3開始,這些文件會一直保存直到對應的RDD不再使用並且已經開始GC回收。這麼做的原因是在根據rdd lineage重新計算時就沒必要重新創建shuffle文件了。如果application保留對這些RDDs的引用,或者GC不經常啓動,那麼GC回收操作可能只會在很長一段時間之後纔會發生。這意味着長時間運行的Spark作業可能會消耗大量磁盤空間。臨時存儲目錄是根據spark.local.dir來指定的。如果是spark on yarn的話,該屬性會被yarn集羣的yarn.nodemanager.local-dirs替代。

可以參考shuffle-behavior的配置信息來對shuffle進行調整。

RDD Persistence(RDD持久化)

Spark一個最重要的能力就是可以對RDD進行持久化存儲在內存或磁盤。當你持久化一個RDD後,每個節點都會存儲該RDD的一些partition這樣它就能在內存裏計算並且在某些引用該RDD的action算子觸發時可以重用這個RDD。這會運行後面的action算子更快(通常會快十倍以上)。緩存對於迭代算法和快速交互使用是一個非常重要的工具。

可以通過persist()或者cache()方法來持久化RDD(這兩個方法都是將RDD只存放在內存裏)。當第一次在一個action算子裏計算的時候,該RDD就會保存在各個節點的內存裏。Spark的緩存是有容錯機制的----如果一個RDD中的任何partition丟失了,它都會根據lineage自動重算的。

另外。每個持久化的RDD都可以使用不同的存儲級別去存儲,例如,持久化到磁盤,持久化到內存但是作爲序列化的Java對象(爲了節省空間),在節點間進行復制。這些級別可以通過傳遞參數StorageLevelpersist()方法裏。cache()方法就是使用默認的存儲級別即StorageLevel.MEMORY_ONLY(在內存裏存儲反序列化對象)。完整的存儲級別如下:

Storage Level Meaning
MEMORY_ONLY 將RDD作爲反序列化的Java對象存儲在JVM中。如果內存不夠,那麼一些分區將不會被緩存,而是在需要它們時動態地重新計算。這是默認級別。
MEMORY_AND_DISK 將RDD作爲反序列化的Java對象存儲在JVM中。如果內存不夠,那麼沒有存到內存的分區會存儲在磁盤上,並在需要時從磁盤讀取它們。
MEMORY_ONLY_SER 將RDD存儲爲序列化的Java對象(每個分區一個字節數組(byte Array))。這通常比反序列化對象更節省空間,特別是在使用fast serializer時,但讀取時需要更多cpu。
MEMORY_AND_DISK_SER 類似於MEMORY_ONLY_SER
DISK_ONLY RDD數據只存放在磁盤上
MEMORY_ONLY_2, MEMORY_AND_DISK_2, etc. 和上面的一樣,只不過多個一個副本
OFF_HEAP (experimental) 類似於MEMORY_ONLY_SER,但將數據存儲在堆外內存中。這需要啓用堆外內存。

注意:如果是使用Python的話,存儲的對象總是會用Pickle庫去序列化對象,所以無論選擇哪個存儲級別都不重要。Python裏只存的存儲級別有MEMORY_ONLY, MEMORY_ONLY_2, MEMORY_AND_DISK, MEMORY_AND_DISK_2, DISK_ONLY, and DISK_ONLY_2。

Spark在shuffle操作期間(如reduceByKey)也會自動地去持久化中間數據,及時用於沒有顯示調用persist這樣做是爲了在shuffle期間節點失敗時避免要重算整個輸入數據。我們也建議用戶可以持久化結果RDD如果計算後面要重用這個結果RDD的話。

Which Storage Level to Choose?(選取哪個存儲級別)

Spark的存儲級別就意味着在內存使用和CPU效率之間做trade-off(取捨)。建議通過以下步驟選取一個合適的:

  • 如果你的RDD對於默認存儲級別很合適的話,就不用修改了。這是CPU最高效的選項,運行在這些RDD上的操作跑的儘可能快。
  • 如果不合適的話,試着使用MEMORY_ONLY_SER並且選擇一個很快的serialization庫使對象更加節省空間,但是仍然可以快速訪問。
  • 除非在RDD上的計算是非常昂貴的,或者它們過濾了很龐大的數據,否則不要溢出數據到磁盤。否則重新計算一個分區的速度可能與從磁盤讀取的速度是一樣的。
  • 如果你想快速的故障恢復的話(例如使用Spark爲來自web應用程序的請求提供服務),可以選擇帶有複製的存儲級別。所有的存儲級別都有完整的容錯能力通過重算丟失的數據,但是帶有複製的存儲級別可以讓你在RDD上繼續運行任務而不用等着去重算分區數據。
Removing Data(清理數據)

Spark會自動監控每個節點的緩存使用情況然後使用LRU算法清理掉老的分區數據。如果想手動清理的話可以通過RDD.unpersist()

Shared Variables(共享變量)

正常情況下,當傳遞給Spark操作(如map, reduce)的函數在遠程集羣節點上運行時,它會基於函數裏使用的所有變量的單獨副本工作的。這些變量會複製到每臺機器上,對遠程機器上的變量的更新是不會傳播到driver program節點的。在task之間支持通用的、讀寫共享的變量是效率很低的。但是Spark確實爲兩種常見的常見提供了兩類共享變量:廣播變量(broadcast variables)和累加器(accumulators)。

Broadcast Variables(廣播變量)

廣播變量允許程序員在每臺機器上緩存一個只讀變量而不是在task之間拷貝變量。例如,它們可以用來以一個有效的方式給每個節點一個大量輸入數據的拷貝。Spark也嘗試着用有效的廣播算法去分發廣播變量來減少連接交流成本。

Saprk的action算子的執行是通過一系列的stage,由分佈式shuffle操作來分割( 具體stage如何劃分請看另一篇博客)。Spark在每個stage內自動廣播task所需要的公共數據。用這種方式廣播的數據是以序列化的形式緩存然後在執行每個task之前會反序列化回去。這意味着,只有當跨多個stage的task需要相同的數據,或者以反序列化的形式緩存數據很重要時,顯式地創建廣播變量纔有用。

廣播變量可以通過SparkContext.broadcast(v)來創建。廣播變量是對變量v的一個包裝,它的值可以通過value方法來獲取。如下所示:

scala> val bcVar = sc.broadcast(Array(1 to 5))
bcVar: org.apache.spark.broadcast.Broadcast[Array[scala.collection.immutable.Range.Inclusive]] = Broadcast(19)

scala> bcVar.value
res14: Array[scala.collection.immutable.Range.Inclusive] = Array(Range(1, 2, 3, 4, 5))

在廣播變量被創建後,應該使用廣播變量,而不是使用集羣上任意函數的v的值,這樣v就不會多次發送到節點上。另外,變量v在被廣播後是不應該被修改的,以確保所有的節點獲取廣播變量同樣的值(如變量拷貝到一個新的節點上)。

Accumulators(累加器)

累加器是通過符合交換律和結合律操作的用於“added”的變量,因此可以有效地支持並行操作。它們可以用來實現counters(如MapReduce中的程序計數器)或者求和。Spark本身支持數字類型的累加器,程序員可以添加對新類型的支持。

作爲一個用戶,你可以創建已命名或未命名的累加器。正如下面圖片所示。一個命名的累加器(例子中的counter)將會展示在web UI。Spark在“Tasks”表中顯示由task修改的每個累加器的值。
在這裏插入圖片描述
在UI界面追蹤累加器對於理解運行的stage的過程是非常有用的(注意:這個暫時不支持python)

scala> val accum = sc.longAccumulator("My Accumulator")
accum: org.apache.spark.util.LongAccumulator = LongAccumulator(id: 475, name: Some(My Accumulator), value: 0)

scala> sc.parallelize(Array(1, 2, 3, 4)).foreach(x => accum.add(x))

scala> accum.value
res16: Long = 10

可以通過調用SparkContext.longAccumulator()或者SparkContext.doubleAccumulator()來創建一個數字型的累加器用於累加Long類型或者Double類型的數據。在集羣上運行的task可以通過使用add方法來累加它。然而,task是不能讀累加器的值。只有driver program才能通過value方法獲取到累加器的值

程序員可以通過繼承AccumulatorV2來創建自定義類型的累加器。但是AccumulatorV2是抽象類,有幾個抽象方法需要覆蓋。
在這裏插入圖片描述
看下面的自定義累加器的例子

class VectorAccumulatorV2 extends AccumulatorV2[MyVector, MyVector] {

  private val myVector: MyVector = MyVector.createZeroVector

  def reset(): Unit = {
    myVector.reset()
  }

  def add(v: MyVector): Unit = {
    myVector.add(v)
  }
  ...
}

// Then, create an Accumulator of this type:
val myVectorAcc = new VectorAccumulatorV2
// Then, register it into spark context:
sc.register(myVectorAcc, "MyVectorAcc1")

注意,程序員定義自己的累加器時,結果類型可以和待添加的元素類型不一樣。

對於僅僅在action算子操作的累加器的更新,Spark會保證每個task對累加器的更新只會執行一次,例如重啓任務不會更新該值。在transformation算子操作裏,用戶需要意識到每個task對累加器的更新在task或者job stage重執行時可能會執行多次

累加器不會改變Spark的lazy evaluation模式。看下面代碼,執行了map操作後累加器的值並不會更新。

scala> accum.value
res19: Long = 20

scala> sc.parallelize(Array(1, 2, 3, 4)).map(x=>{accum.add(x);x})
res20: org.apache.spark.rdd.RDD[Int] = MapPartitionsRDD[36] at map at <console>:27

scala> accum.value
res21: Long = 20

Deploying to a Cluster(部署spark程序到集羣)

如何提交代碼給集羣。將應用打包成jar(scala、Java)或者打包成zip包(python),然後通過腳本spark-submit提交給集羣

Launching Spark jobs from Java / Scala(從Java/Scala啓動Spark job)

org.apache.spark.launcher提供class用簡單的Java API的方式以子進程的形式啓動Spark job

Unit Testing(單元測試)

Spark對任何流行的單元測試框架都很友好。只需在您的測試中創建一個master URL設置爲local的SparkContext,運行您的操作,然後調用SparkContext.stop()將其銷燬。確保在finally塊或測試框架的tearDown方法中停止SparkContext因爲Spark不支持在同一個程序中同時運行兩個SparkContext

參考網址

Spark官網 - - RDD編程指南

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