Apache Spark 2.2.0 中文文檔 - Spark 編程指南 | ApacheCN

Spark 編程指南

概述

Spark 依賴

初始化 Spark

使用 Shell

彈性分布式數據集 (RDDs)

并行集合

外部 Datasets(數據集)

RDD 操作

基礎

傳遞 Functions(函數)給 Spark

理解閉包

示例

Local(本地)vs. cluster(集群)模式

打印 RDD 的 elements

與 Key-Value Pairs 一起使用

Transformations(轉換)

Actions(動作)

Shuffle 操作

Background(幕后)

性能影響

RDD Persistence(持久化)

如何選擇存儲級別 ?

刪除數據

共享變量

廣播變量

Accumulators(累加器)

部署應用到集群中

從 Java / Scala 啟動 Spark jobs

單元測試

快速鏈接

概述

在一個較高的概念上來說,每一個 Spark 應用程序由一個在集群上運行著用戶的main函數和執行各種并行操作的driver program(驅動程序)組成。Spark 提供的主要抽象是一個彈性分布式數據集(RDD),它是可以執行并行操作且跨集群節點的元素的集合。RDD 可以從一個 Hadoop 文件系統(或者任何其它 Hadoop 支持的文件系統),或者一個在 driver program(驅動程序)中已存在的 Scala 集合,以及通過 transforming(轉換)來創建一個 RDD。用戶為了讓它在整個并行操作中更高效的重用,也許會讓 Spark persist(持久化)一個 RDD 到內存中。最后,RDD 會自動的從節點故障中恢復。

在 Spark 中的第二個抽象是能夠用于并行操作的shared variables(共享變量),默認情況下,當 Spark 的一個函數作為一組不同節點上的任務運行時,它將每一個變量的副本應用到每一個任務的函數中去。有時候,一個變量需要在整個任務中,或者在任務和 driver program(驅動程序)之間來共享。Spark 支持兩種類型的共享變量 :broadcast variables(廣播變量),它可以用于在所有節點上緩存一個值,和accumulators(累加器),他是一個只能被 “added(增加)” 的變量,例如 counters 和 sums。

本指南介紹了每一種 Spark 所支持的語言的特性。如果您啟動 Spark 的交互式 shell - 針對 Scala shell 使用bin/spark-shell或者針對 Python 使用bin/pyspark是很容易來學習的。

Spark 依賴

Scala

Java

Python

Spark 2.2.0 默認使用 Scala 2.11 來構建和發布直到運行。(當然,Spark 也可以與其它的 Scala 版本一起運行)。為了使用 Scala 編寫應用程序,您需要使用可兼容的 Scala 版本(例如,2.11.X)。

要編寫一個 Spark 的應用程序,您需要在 Spark 上添加一個 Maven 依賴。Spark 可以通過 Maven 中央倉庫獲取:

groupId = org.apache.spark

artifactId = spark-core_2.11

version = 2.2.0

此外,如果您想訪問一個 HDFS 集群,則需要針對您的 HDFS 版本添加一個hadoop-client(hadoop 客戶端)依賴。

groupId = org.apache.hadoop

artifactId = hadoop-client

version =

最后,您需要導入一些 Spark classes(類)到您的程序中去。添加下面幾行:

importorg.apache.spark.SparkContextimportorg.apache.spark.SparkConf

(在 Spark 1.3.0 之前,您需要明確導入org.apache.spark.SparkContext._來啟用必要的的隱式轉換。)

初始化 Spark

Scala

Java

Python

Spark 程序必須做的第一件事情是創建一個SparkContext對象,它會告訴 Spark 如何訪問集群。要創建一個SparkContext,首先需要構建一個包含應用程序的信息的SparkConf對象。

每一個 JVM 可能只能激活一個 SparkContext 對象。在創新一個新的對象之前,必須調用stop()該方法停止活躍的 SparkContext。

valconf=newSparkConf().setAppName(appName).setMaster(master)newSparkContext(conf)

這個appName參數是一個在集群 UI 上展示應用程序的名稱。master是一個Spark, Mesos 或 YARN 的 cluster URL,或者指定為在 local mode(本地模式)中運行的 “local” 字符串。在實際工作中,當在集群上運行時,您不希望在程序中將 master 給硬編碼,而是用使用spark-submit啟動應用并且接收它。然而,對于本地測試和單元測試,您可以通過 “local” 來運行 Spark 進程。

使用 Shell

Scala

Python

在 Spark Shell 中,一個特殊的 interpreter-aware(可用的解析器)SparkContext 已經為您創建好了,稱之為sc的變量。創建您自己的 SparkContext 將不起作用。您可以使用--master參數設置這個 SparkContext 連接到哪一個 master 上,并且您可以通過--jars參數傳遞一個逗號分隔的列表來添加 JARs 到 classpath 中。也可以通過--packages參數應用一個用逗號分隔的 maven coordinates(maven 坐標)方式來添加依賴(例如,Spark 包)到您的 shell session 中去。任何額外存在且依賴的倉庫(例如 Sonatype)可以傳遞到--repositories參數。例如,要明確使用四個核(CPU)來運行bin/spark-shell,使用:

$ ./bin/spark-shell --master local[4]

或者, 也可以添加code.jar到它的 classpath 中去, 使用:

$ ./bin/spark-shell --master local[4]--jars code.jar

為了包含一個依賴,使用 Maven 坐標:

$ ./bin/spark-shell --master local[4]--packages"org.example:example:0.1"

有關選項的完整列表, 請運行spark-shell --help. 在幕后,spark-shell調用了常用的spark-submit腳本.

彈性分布式數據集 (RDDs)

Spark 主要以一個彈性分布式數據集(RDD)的概念為中心,它是一個容錯且可以執行并行操作的元素的集合。有兩種方法可以創建 RDD : 在你的 driver program(驅動程序)中parallelizing一個已存在的集合,或者在外部存儲系統中引用一個數據集,例如,一個共享文件系統,HDFS,HBase,或者提供 Hadoop InputFormat 的任何數據源。

并行集合

Scala

Java

Python

可以在您的 driver program (a ScalaSeq) 中已存在的集合上通過調用SparkContext的parallelize方法來創建并行集合。該集合的元素從一個可以并行操作的 distributed dataset(分布式數據集)中復制到另一個 dataset(數據集)中去。例如,這里是一個如何去創建一個保存數字 1 ~ 5 的并行集合。

valdata=Array(1,2,3,4,5)valdistData=sc.parallelize(data)

在創建后,該 distributed dataset(分布式數據集)(distData)可以并行的執行操作。例如,我們可以調用distData.reduce((a, b) => a + b) 來合計數組中的元素。后面我們將介紹 distributed dataset(分布式數據集)上的操作。

并行集合中一個很重要參數是partitions(分區)的數量,它可用來切割 dataset(數據集)。Spark 將在集群中的每一個分區上運行一個任務。通常您希望群集中的每一個 CPU 計算 2-4 個分區。一般情況下,Spark 會嘗試根據您的群集情況來自動的設置的分區的數量。當然,您也可以將分區數作為第二個參數傳遞到parallelize(e.g.sc.parallelize(data, 10)) 方法中來手動的設置它。注意: 代碼中的一些地方會使用 term slices (a synonym for partitions) 以保持向后兼容.

外部 Datasets(數據集)

Scala

Java

Python

Spark 可以從 Hadoop 所支持的任何存儲源中創建 distributed dataset(分布式數據集),包括本地文件系統,HDFS,Cassandra,HBase,Amazon S3等等。 Spark 支持文本文件,SequenceFiles,以及任何其它的 HadoopInputFormat

可以使用SparkContext的textFile方法來創建文本文件的 RDD。此方法需要一個文件的 URI(計算機上的本地路徑 ,hdfs://,s3n://等等的 URI),并且讀取它們作為一個 lines(行)的集合。下面是一個調用示例:

scala>valdistFile=sc.textFile("data.txt")distFile:org.apache.spark.rdd.RDD[String]=data.txtMapPartitionsRDD[10]attextFileat:26

在創建后,distFile可以使用 dataset(數據集)的操作。例如,我們可以使用下面的 map 和 reduce 操作來合計所有行的數量:distFile.map(s => s.length).reduce((a, b) => a + b)。

使用 Spark 讀取文件時需要注意:

如果使用本地文件系統的路徑,所工作節點的相同訪問路徑下該文件必須可以訪問。復制文件到所有工作節點上,或著使用共享的網絡掛載文件系統。

所有 Spark 基于文件的 input 方法, 包括textFile, 支持在目錄上運行, 壓縮文件, 和通配符. 例如, 您可以使用textFile("/my/directory"),textFile("/my/directory/*.txt"), andtextFile("/my/directory/*.gz").

textFile方法也可以通過第二個可選的參數來控制該文件的分區數量. 默認情況下, Spark 為文件的每一個 block(塊)創建的一 個 partition 分區(HDFS 中塊大小默認是 128MB),當然你也可以通過傳遞一個較大的值來要求一個較高的分區數量。請注意,分區的數量不能夠小于塊的數量。

除了文本文件之外,Spark 的 Scala API 也支持一些其它的數據格式:

SparkContext.wholeTextFiles可以讀取包含多個小文本文件的目錄, 并且將它們作為一個 (filename, content) pairs 來返回. 這與textFile相比, 它的每一個文件中的每一行將返回一個記錄. 分區由數據量來確定, 某些情況下, 可能導致分區太少. 針對這些情況,wholeTextFiles在第二個位置提供了一個可選的參數用戶控制分區的最小數量.

針對SequenceFiles, 使用 SparkContext 的sequenceFile[K, V]方法,其中K和V指的是文件中 key 和 values 的類型. 這些應該是 Hadoop 的Writable接口的子類, 像IntWritableandText. 此外, Spark 可以讓您為一些常見的 Writables 指定原生類型; 例如,sequenceFile[Int, String]會自動讀取 IntWritables 和 Texts.

針對其它的 Hadoop InputFormats, 您可以使用SparkContext.hadoopRDD方法, 它接受一個任意的JobConf和 input format class, key class 和 value class. 通過相同的方法你可以設置你的 input source(輸入源). 你還可以針對 InputFormats 使用基于 “new” MapReduce API (org.apache.hadoop.mapreduce) 的SparkContext.newAPIHadoopRDD.

RDD.saveAsObjectFile和SparkContext.objectFile支持使用簡單的序列化的 Java objects 來保存 RDD. 雖然這不像 Avro 這種專用的格式一樣高效,但其提供了一種更簡單的方式來保存任何的 RDD。.

RDD 操作

RDDs support 兩種類型的操作:transformations(轉換), 它會在一個已存在的 dataset 上創建一個新的 dataset, 和actions(動作), 將在 dataset 上運行的計算后返回到 driver 程序. 例如,map是一個通過讓每個數據集元素都執行一個函數,并返回的新 RDD 結果的 transformation,reducereduce 通過執行一些函數,聚合 RDD 中所有元素,并將最終結果給返回驅動程序(雖然也有一個并行reduceByKey返回一個分布式數據集)的 action.

Spark 中所有的 transformations 都是lazy(懶加載的), 因此它不會立刻計算出結果. 相反, 他們只記得應用于一些基本數據集的轉換 (例如. 文件). 只有當需要返回結果給驅動程序時,transformations 才開始計算. 這種設計使 Spark 的運行更高效. 例如, 我們可以了解到,map所創建的數據集將被用在reduce中,并且只有reduce的計算結果返回給驅動程序,而不是映射一個更大的數據集.

默認情況下,每次你在 RDD 運行一個 action 的時, 每個 transformed RDD 都會被重新計算。但是,您也可用persist(或cache) 方法將 RDD persist(持久化)到內存中;在這種情況下,Spark 為了下次查詢時可以更快地訪問,會把數據保存在集群上。此外,還支持持續持久化 RDDs 到磁盤,或復制到多個結點。

基礎

Scala

Java

Python

為了說明 RDD 基礎,請思考下面這個的簡單程序:

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

第一行從外部文件中定義了一個基本的 RDD,但這個數據集并未加載到內存中或即將被行動:line僅僅是一個類似指針的東西,指向該文件. 第二行定義了lineLengths作為maptransformation 的結果。請注意,由于laziness(延遲加載)lineLengths不會被立即計算. 最后,我們運行reduce,這是一個 action。此時,Spark 分發計算任務到不同的機器上運行,每臺機器都運行在 map 的一部分并本地運行 reduce,僅僅返回它聚合后的結果給驅動程序.

如果我們也希望以后再次使用lineLengths,我們還可以添加:

lineLengths.persist()

在reduce之前,這將導致lineLengths在第一次計算之后就被保存在 memory 中。

傳遞 Functions(函數)給 Spark

Scala

Java

Python

當 driver 程序在集群上運行時,Spark 的 API 在很大程度上依賴于傳遞函數。有 2 種推薦的方式來做到這一點:

Anonymous function syntax(匿名函數語法), 它可以用于短的代碼片斷.

在全局單例對象中的靜態方法. 例如, 您可以定義object MyFunctions然后傳遞MyFunctions.func1, 如下:

objectMyFunctions{deffunc1(s:String):String={...}}myRdd.map(MyFunctions.func1)

請注意,雖然也有可能傳遞一個類的實例(與單例對象相反)的方法的引用,這需要發送整個對象,包括類中其它方法。例如,考慮:

classMyClass{deffunc1(s:String):String={...}defdoStuff(rdd:RDD[String]):RDD[String]={rdd.map(func1)}}

這里,如果我們創建一個MyClass的實例,并調用doStuff,在map內有MyClass實例的func1方法的引用,所以整個對象需要被發送到集群的。它類似于rdd.map(x => this.func1(x))

類似的方式,訪問外部對象的字段將引用整個對象:

classMyClass{valfield="Hello"defdoStuff(rdd:RDD[String]):RDD[String]={rdd.map(x=>field+x)}}

相當于寫rdd.map(x => this.field + x), 它引用this所有的東西. 為了避免這個問題, 最簡單的方式是復制field到一個本地變量,而不是外部訪問它:

defdoStuff(rdd:RDD[String]):RDD[String]={valfield_=this.fieldrdd.map(x=>field_+x)}

理解閉包

在集群中執行代碼時,一個關于 Spark 更難的事情是理解變量和方法的范圍和生命周期. 修改其范圍之外的變量 RDD 操作可以混淆的常見原因。在下面的例子中,我們將看一下使用的foreach()代碼遞增累加計數器,但類似的問題,也可能會出現其他操作上.

示例

考慮一個簡單的 RDD 元素求和,以下行為可能不同,具體取決于是否在同一個 JVM 中執行. 一個常見的例子是當 Spark 運行在local本地模式(--master = local[n])時,與部署 Spark 應用到群集(例如,通過 spark-submit 到 YARN):

Scala

Java

Python

varcounter=0varrdd=sc.parallelize(data)// Wrong: Don't do this!!rdd.foreach(x=>counter+=x)println("Counter value: "+counter)

Local(本地)vs. cluster(集群)模式

上面的代碼行為是不確定的,并且可能無法按預期正常工作。執行作業時,Spark 會分解 RDD 操作到每個 executor 中的 task 里。在執行之前,Spark 計算任務的closure(閉包)。而閉包是在 RDD 上的 executor 必須能夠訪問的變量和方法(在此情況下的foreach())。閉包被序列化并被發送到每個執行器。

閉包的變量副本發給每個counter,當counter被foreach函數引用的時候,它已經不再是 driver node 的counter了。雖然在 driver node 仍然有一個 counter 在內存中,但是對 executors 已經不可見。executor 看到的只是序列化的閉包一個副本。所以counter最終的值還是 0,因為對counter所有的操作均引用序列化的 closure 內的值。

在local本地模式,在某些情況下的foreach功能實際上是同一 JVM 上的驅動程序中執行,并會引用同一個原始的counter計數器,實際上可能更新.

為了確保這些類型的場景明確的行為應該使用的Accumulator累加器。當一個執行的任務分配到集群中的各個 worker 結點時,Spark 的累加器是專門提供安全更新變量的機制。本指南的累加器的部分會更詳細地討論這些。

在一般情況下,closures - constructs 像循環或本地定義的方法,不應該被用于改動一些全局狀態。Spark 沒有規定或保證突變的行為,以從封閉件的外側引用的對象。一些代碼,這可能以本地模式運行,但是這只是偶然和這樣的代碼如預期在分布式模式下不會表現。如果需要一些全局的聚合功能,應使用 Accumulator(累加器)。

打印 RDD 的 elements

另一種常見的語法用于打印 RDD 的所有元素使用rdd.foreach(println)或rdd.map(println)。在一臺機器上,這將產生預期的輸出和打印 RDD 的所有元素。然而,在集群cluster模式下,stdout輸出正在被執行寫操作 executors 的stdout代替,而不是在一個驅動程序上,因此stdout的driver程序不會顯示這些!要打印driver程序的所有元素,可以使用的collect()方法首先把 RDD 放到 driver 程序節點上:rdd.collect().foreach(println)。這可能會導致 driver 程序耗盡內存,雖說,因為collect()獲取整個 RDD 到一臺機器; 如果你只需要打印 RDD 的幾個元素,一個更安全的方法是使用take():rdd.take(100).foreach(println)。

與 Key-Value Pairs 一起使用

Scala

Java

Python

雖然大多數 Spark 操作工作在包含任何類型對象的 RDDs 上,只有少數特殊的操作可用于 Key-Value 對的 RDDs. 最常見的是分布式 “shuffle” 操作,如通過元素的 key 來進行 grouping 或 aggregating 操作.

在 Scala 中,這些操作在 RDD 上是自動可用,它包含了Tuple2objects (the built-in tuples in the language, created by simply writing(a, b)). 在PairRDDFunctionsclass 中該 key-value pair 操作是可用的, 其中圍繞 tuple 的 RDD 進行自動封裝.

例如,下面的代碼使用的Key-Value對的reduceByKey操作統計文本文件中每一行出現了多少次:

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

我們也可以使用counts.sortByKey(),例如,在對按字母順序排序,最后counts.collect()把他們作為一個數據對象返回給 driver 程序。

Note(注意):當在 key-value pair 操作中使用自定義的 objects 作為 key 時, 您必須確保有一個自定義的equals()方法有一個hashCode()方法相匹配. 有關詳情, 請參閱Object.hashCode() documentation中列出的約定.

Transformations(轉換)

下表列出了一些 Spark 常用的 transformations(轉換). 詳情請參考 RDD API 文檔 (Scala,Java,Python,R) 和 pair RDD 函數文檔 (Scala,Java).

Transformation(轉換)Meaning(含義)

map(func)返回一個新的 distributed dataset(分布式數據集),它由每個 source(數據源)中的元素應用一個函數func來生成.

filter(func)返回一個新的 distributed dataset(分布式數據集),它由每個 source(數據源)中應用一個函數func且返回值為 true 的元素來生成.

flatMap(func)與 map 類似,但是每一個輸入的 item 可以被映射成 0 個或多個輸出的 items(所以func應該返回一個 Seq 而不是一個單獨的 item).

mapPartitions(func)與 map 類似,但是單獨的運行在在每個 RDD 的 partition(分區,block)上,所以在一個類型為 T 的 RDD 上運行時func必須是 Iterator => Iterator 類型.

mapPartitionsWithIndex(func)與 mapPartitions 類似,但是也需要提供一個代表 partition 的 index(索引)的 interger value(整型值)作為參數的func,所以在一個類型為 T 的 RDD 上運行時func必須是 (Int, Iterator) => Iterator 類型.

sample(withReplacement,fraction,seed)樣本數據,設置是否放回(withReplacement), 采樣的百分比(fraction)、使用指定的隨機數生成器的種子(seed).

union(otherDataset)反回一個新的 dataset,它包含了 source dataset(源數據集)和 otherDataset(其它數據集)的并集.

intersection(otherDataset)返回一個新的 RDD,它包含了 source dataset(源數據集)和 otherDataset(其它數據集)的交集.

distinct([numTasks]))返回一個新的 dataset,它包含了 source dataset(源數據集)中去重的元素.

groupByKey([numTasks])在一個 (K, V) pair 的 dataset 上調用時,返回一個 (K, Iterable) .

Note:如果分組是為了在每一個 key 上執行聚合操作(例如,sum 或 average),此時使用reduceByKey或aggregateByKey來計算性能會更好.

Note:默認情況下,并行度取決于父 RDD 的分區數。可以傳遞一個可選的numTasks參數來設置不同的任務數.

reduceByKey(func, [numTasks])在 (K, V) pairs 的 dataset 上調用時, 返回 dataset of (K, V) pairs 的 dataset, 其中的 values 是針對每個 key 使用給定的函數func來進行聚合的, 它必須是 type (V,V) => V 的類型. 像groupByKey一樣, reduce tasks 的數量是可以通過第二個可選的參數來配置的.

aggregateByKey(zeroValue)(seqOp,combOp, [numTasks])在 (K, V) pairs 的 dataset 上調用時, 返回 (K, U) pairs 的 dataset,其中的 values 是針對每個 key 使用給定的 combine 函數以及一個 neutral "0" 值來進行聚合的. 允許聚合值的類型與輸入值的類型不一樣, 同時避免不必要的配置. 像groupByKey一樣, reduce tasks 的數量是可以通過第二個可選的參數來配置的.

sortByKey([ascending], [numTasks])在一個 (K, V) pair 的 dataset 上調用時,其中的 K 實現了 Ordered,返回一個按 keys 升序或降序的 (K, V) pairs 的 dataset, 由 boolean 類型的ascending參數來指定.

join(otherDataset, [numTasks])在一個 (K, V) 和 (K, W) 類型的 dataset 上調用時,返回一個 (K, (V, W)) pairs 的 dataset,它擁有每個 key 中所有的元素對。Outer joins 可以通過leftOuterJoin,rightOuterJoin和fullOuterJoin來實現.

cogroup(otherDataset, [numTasks])在一個 (K, V) 和的 dataset 上調用時,返回一個 (K, (Iterable, Iterable)) tuples 的 dataset. 這個操作也調用了groupWith.

cartesian(otherDataset)在一個 T 和 U 類型的 dataset 上調用時,返回一個 (T, U) pairs 類型的 dataset(所有元素的 pairs,即笛卡爾積).

pipe(command,[envVars])通過使用 shell 命令來將每個 RDD 的分區給 Pipe。例如,一個 Perl 或 bash 腳本。RDD 的元素會被寫入進程的標準輸入(stdin),并且 lines(行)輸出到它的標準輸出(stdout)被作為一個字符串型 RDD 的 string 返回.

coalesce(numPartitions)Decrease(降低)RDD 中 partitions(分區)的數量為 numPartitions。對于執行過濾后一個大的 dataset 操作是更有效的.

repartition(numPartitions)Reshuffle(重新洗牌)RDD 中的數據以創建或者更多的 partitions(分區)并將每個分區中的數據盡量保持均勻. 該操作總是通過網絡來 shuffles 所有的數據.

repartitionAndSortWithinPartitions(partitioner)根據給定的 partitioner(分區器)對 RDD 進行重新分區,并在每個結果分區中,按照 key 值對記錄排序。這比每一個分區中先調用repartition然后再 sorting(排序)效率更高,因為它可以將排序過程推送到 shuffle 操作的機器上進行.

Actions(動作)

下表列出了一些 Spark 常用的 actions 操作。詳細請參考 RDD API 文檔 (Scala,Java,Python,R)

和 pair RDD 函數文檔 (Scala,Java).

Action(動作)Meaning(含義)

reduce(func)使用函數func聚合 dataset 中的元素,這個函數func輸入為兩個元素,返回為一個元素。這個函數應該是可交換(commutative )和關聯(associative)的,這樣才能保證它可以被并行地正確計算.

collect()在 driver 程序中,以一個 array 數組的形式返回 dataset 的所有元素。這在過濾器(filter)或其他操作(other operation)之后返回足夠小(sufficiently small)的數據子集通常是有用的.

count()返回 dataset 中元素的個數.

first()返回 dataset 中的第一個元素(類似于 take(1).

take(n)將數據集中的前n個元素作為一個 array 數組返回.

takeSample(withReplacement,num, [seed])對一個 dataset 進行隨機抽樣,返回一個包含num個隨機抽樣(random sample)元素的數組,參數 withReplacement 指定是否有放回抽樣,參數 seed 指定生成隨機數的種子.

takeOrdered(n,[ordering])返回 RDD 按自然順序(natural order)或自定義比較器(custom comparator)排序后的前n個元素.

saveAsTextFile(path)將 dataset 中的元素以文本文件(或文本文件集合)的形式寫入本地文件系統、HDFS 或其它 Hadoop 支持的文件系統中的給定目錄中。Spark 將對每個元素調用 toString 方法,將數據元素轉換為文本文件中的一行記錄.

saveAsSequenceFile(path)

(Java and Scala)將 dataset 中的元素以 Hadoop SequenceFile 的形式寫入到本地文件系統、HDFS 或其它 Hadoop 支持的文件系統指定的路徑中。該操作可以在實現了 Hadoop 的 Writable 接口的鍵值對(key-value pairs)的 RDD 上使用。在 Scala 中,它還可以隱式轉換為 Writable 的類型(Spark 包括了基本類型的轉換,例如 Int, Double, String 等等).

saveAsObjectFile(path)

(Java and Scala)使用 Java 序列化(serialization)以簡單的格式(simple format)編寫數據集的元素,然后使用SparkContext.objectFile()進行加載.

countByKey()僅適用于(K,V)類型的 RDD 。返回具有每個 key 的計數的 (K , Int)pairs 的 hashmap.

foreach(func)對 dataset 中每個元素運行函數func。這通常用于副作用(side effects),例如更新一個Accumulator(累加器)或與外部存儲系統(external storage systems)進行交互。Note:修改除foreach()之外的累加器以外的變量(variables)可能會導致未定義的行為(undefined behavior)。詳細介紹請閱讀Understanding closures(理解閉包)部分.

該 Spark RDD API 還暴露了一些 actions(操作)的異步版本,例如針對foreach的foreachAsync,它們會立即返回一個FutureAction到調用者,而不是在完成 action 時阻塞。 這可以用于管理或等待 action 的異步執行。.

Shuffle 操作

Spark 里的某些操作會觸發 shuffle。shuffle 是spark 重新分配數據的一種機制,使得這些數據可以跨不同的區域進行分組。這通常涉及在 executors 和 機器之間拷貝數據,這使得 shuffle 成為一個復雜的、代價高的操作。

Background(幕后)

為了明白reduceByKey操作的過程,我們以reduceByKey為例。reduceBykey 操作產生一個新的 RDD,其中 key 所有相同的的值組合成為一個 tuple - key 以及與 key 相關聯的所有值在 reduce 函數上的執行結果。面臨的挑戰是,一個 key 的所有值不一定都在一個同一個 paritition 分區里,甚至是不一定在同一臺機器里,但是它們必須共同被計算。

在 spark 里,特定的操作需要數據不跨分區分布。在計算期間,一個任務在一個分區上執行,為了所有數據都在單個reduceByKey的 reduce 任務上運行,我們需要執行一個 all-to-all 操作。它必須從所有分區讀取所有的 key 和 key對應的所有的值,并且跨分區聚集去計算每個 key 的結果 - 這個過程就叫做shuffle.。

Although the set of elements in each partition of newly shuffled data will be deterministic, and so is the ordering of partitions themselves, the ordering of these elements is not. If one desires predictably ordered data following shuffle then it’s possible to use:

盡管每個分區新 shuffle 的數據集將是確定的,分區本身的順序也是這樣,但是這些數據的順序是不確定的。如果希望 shuffle 后的數據是有序的,可以使用:

mapPartitions對每個 partition 分區進行排序,例如,.sorted

repartitionAndSortWithinPartitions在分區的同時對分區進行高效的排序.

sortBy對 RDD 進行全局的排序

觸發的 shuffle 操作包括repartition操作,如repartitioncoalesce,‘ByKey操作 (除了 counting 之外) 像groupByKeyreduceByKey, 和join操作, 像cogroupjoin.

性能影響

該Shuffle是一個代價比較高的操作,它涉及磁盤 I/O、數據序列化、網絡 I/O。為了準備 shuffle 操作的數據,Spark 啟動了一系列的任務,map任務組織數據,reduce完成數據的聚合。這些術語來自 MapReduce,跟 Spark 的map操作和reduce操作沒有關系。

在內部,一個 map 任務的所有結果數據會保存在內存,直到內存不能全部存儲為止。然后,這些數據將基于目標分區進行排序并寫入一個單獨的文件中。在 reduce 時,任務將讀取相關的已排序的數據塊。

某些 shuffle 操作會大量消耗堆內存空間,因為 shuffle 操作在數據轉換前后,需要在使用內存中的數據結構對數據進行組織。需要特別說明的是,reduceByKey和aggregateByKey在 map 時會創建這些數據結構,'ByKey操作在 reduce 時創建這些數據結構。當內存滿的時候,Spark 會把溢出的數據存到磁盤上,這將導致額外的磁盤 I/O 開銷和垃圾回收開銷的增加。

shuffle 操作還會在磁盤上生成大量的中間文件。在 Spark 1.3 中,這些文件將會保留至對應的 RDD 不在使用并被垃圾回收為止。這么做的好處是,如果在 Spark 重新計算 RDD 的血統關系(lineage)時,shuffle 操作產生的這些中間文件不需要重新創建。如果 Spark 應用長期保持對 RDD 的引用,或者垃圾回收不頻繁,這將導致垃圾回收的周期比較長。這意味著,長期運行 Spark 任務可能會消耗大量的磁盤空間。臨時數據存儲路徑可以通過 SparkContext 中設置參數spark.local.dir進行配置。

shuffle 操作的行為可以通過調節多個參數進行設置。詳細的說明請看Spark 配置指南中的 “Shuffle 行為” 部分。

RDD Persistence(持久化)

Spark 中一個很重要的能力是將數據persisting持久化(或稱為caching緩存),在多個操作間都可以訪問這些持久化的數據。當持久化一個 RDD 時,每個節點的其它分區都可以使用 RDD 在內存中進行計算,在該數據上的其他 action 操作將直接使用內存中的數據。這樣會讓以后的 action 操作計算速度加快(通常運行速度會加速 10 倍)。緩存是迭代算法和快速的交互式使用的重要工具。

RDD 可以使用persist()方法或cache()方法進行持久化。數據將會在第一次 action 操作時進行計算,并緩存在節點的內存中。Spark 的緩存具有容錯機制,如果一個緩存的 RDD 的某個分區丟失了,Spark 將按照原來的計算過程,自動重新計算并進行緩存。

另外,每個持久化的 RDD 可以使用不同的storage level存儲級別進行緩存,例如,持久化到磁盤、已序列化的 Java 對象形式持久化到內存(可以節省空間)、跨節點間復制、以 off-heap 的方式存儲在 Tachyon。這些存儲級別通過傳遞一個StorageLevel對象 (Scala,Java,Python) 給persist()方法進行設置。cache()方法是使用默認存儲級別的快捷設置方法,默認的存儲級別是StorageLevel.MEMORY_ONLY(將反序列化的對象存儲到內存中)。詳細的存儲級別介紹如下:

Storage Level(存儲級別)Meaning(含義)

MEMORY_ONLY將 RDD 以反序列化的 Java 對象的形式存儲在 JVM 中. 如果內存空間不夠,部分數據分區將不再緩存,在每次需要用到這些數據時重新進行計算. 這是默認的級別.

MEMORY_AND_DISK將 RDD 以反序列化的 Java 對象的形式存儲在 JVM 中。如果內存空間不夠,將未緩存的數據分區存儲到磁盤,在需要使用這些分區時從磁盤讀取.

MEMORY_ONLY_SER

(Java and Scala)將 RDD 以序列化的 Java 對象的形式進行存儲(每個分區為一個 byte 數組)。這種方式會比反序列化對象的方式節省很多空間,尤其是在使用fast serializer時會節省更多的空間,但是在讀取時會增加 CPU 的計算負擔.

MEMORY_AND_DISK_SER

(Java and Scala)類似于 MEMORY_ONLY_SER ,但是溢出的分區會存儲到磁盤,而不是在用到它們時重新計算.

DISK_ONLY只在磁盤上緩存 RDD.

MEMORY_ONLY_2, MEMORY_AND_DISK_2, etc.與上面的級別功能相同,只不過每個分區在集群中兩個節點上建立副本.

OFF_HEAP (experimental 實驗性)類似于 MEMORY_ONLY_SER, 但是將數據存儲在off-heap memory中. 這需要啟用 off-heap 內存.

Note:在 Python 中, stored objects will 總是使用Picklelibrary 來序列化對象, 所以無論你選擇序列化級別都沒關系. 在 Python 中可用的存儲級別有MEMORY_ONLY,MEMORY_ONLY_2,MEMORY_AND_DISK,MEMORY_AND_DISK_2,DISK_ONLY, 和DISK_ONLY_2.

在 shuffle 操作中(例如reduceByKey),即便是用戶沒有調用persist方法,Spark 也會自動緩存部分中間數據.這么做的目的是,在 shuffle 的過程中某個節點運行失敗時,不需要重新計算所有的輸入數據。如果用戶想多次使用某個 RDD,強烈推薦在該 RDD 上調用 persist 方法.

如何選擇存儲級別 ?

Spark 的存儲級別的選擇,核心問題是在 memory 內存使用率和 CPU 效率之間進行權衡。建議按下面的過程進行存儲級別的選擇:

如果您的 RDD 適合于默認存儲級別 (MEMORY_ONLY), leave them that way. 這是CPU效率最高的選項,允許RDD上的操作盡可能快地運行.

如果不是, 試著使用MEMORY_ONLY_SER和selecting a fast serialization library以使對象更加節省空間,但仍然能夠快速訪問。 (Java和Scala)

不要溢出到磁盤,除非計算您的數據集的函數是昂貴的, 或者它們過濾大量的數據. 否則, 重新計算分區可能與從磁盤讀取分區一樣快.

如果需要快速故障恢復,請使用復制的存儲級別 (e.g. 如果使用Spark來服務 來自網絡應用程序的請求).All存儲級別通過重新計算丟失的數據來提供完整的容錯能力,但復制的數據可讓您繼續在 RDD 上運行任務,而無需等待重新計算一個丟失的分區.

刪除數據

Spark 會自動監視每個節點上的緩存使用情況,并使用 least-recently-used(LRU)的方式來丟棄舊數據分區。 如果您想手動刪除 RDD 而不是等待它掉出緩存,使用RDD.unpersist()方法。

共享變量

通常情況下,一個傳遞給 Spark 操作(例如map或reduce)的函數 func 是在遠程的集群節點上執行的。該函數 func 在多個節點執行過程中使用的變量,是同一個變量的多個副本。這些變量的以副本的方式拷貝到每個機器上,并且各個遠程機器上變量的更新并不會傳播回 driver program(驅動程序)。通用且支持 read-write(讀-寫) 的共享變量在任務間是不能勝任的。所以,Spark 提供了兩種特定類型的共享變量 : broadcast variables(廣播變量)和 accumulators(累加器)。

廣播變量

Broadcast variables(廣播變量)允許程序員將一個 read-only(只讀的)變量緩存到每臺機器上,而不是給任務傳遞一個副本。它們是如何來使用呢,例如,廣播變量可以用一種高效的方式給每個節點傳遞一份比較大的 input dataset(輸入數據集)副本。在使用廣播變量時,Spark 也嘗試使用高效廣播算法分發 broadcast variables(廣播變量)以降低通信成本。

Spark 的 action(動作)操作是通過一系列的 stage(階段)進行執行的,這些 stage(階段)是通過分布式的 “shuffle” 操作進行拆分的。Spark 會自動廣播出每個 stage(階段)內任務所需要的公共數據。這種情況下廣播的數據使用序列化的形式進行緩存,并在每個任務運行前進行反序列化。這也就意味著,只有在跨越多個 stage(階段)的多個任務會使用相同的數據,或者在使用反序列化形式的數據特別重要的情況下,使用廣播變量會有比較好的效果。

廣播變量通過在一個變量v上調用SparkContext.broadcast(v)方法來進行創建。廣播變量是v的一個 wrapper(包裝器),可以通過調用value方法來訪問它的值。代碼示例如下:

Scala

Java

Python

scala>valbroadcastVar=sc.broadcast(Array(1,2,3))broadcastVar:org.apache.spark.broadcast.Broadcast[Array[Int]]=Broadcast(0)scala>broadcastVar.valueres0:Array[Int]=Array(1,2,3)

在創建廣播變量之后,在集群上執行的所有的函數中,應該使用該廣播變量代替原來的v值,所以節點上的v最多分發一次。另外,對象v在廣播后不應該再被修改,以保證分發到所有的節點上的廣播變量具有同樣的值(例如,如果以后該變量會被運到一個新的節點)。

Accumulators(累加器)

Accumulators(累加器)是一個僅可以執行 “added”(添加)的變量來通過一個關聯和交換操作,因此可以高效地執行支持并行。累加器可以用于實現 counter( 計數,類似在 MapReduce 中那樣)或者 sums(求和)。原生 Spark 支持數值型的累加器,并且程序員可以添加新的支持類型。

作為一個用戶,您可以創建 accumulators(累加器)并且重命名. 如下圖所示, 一個命名的 accumulator 累加器(在這個例子中是counter)將顯示在 web UI 中,用于修改該累加器的階段。 Spark 在 “Tasks” 任務表中顯示由任務修改的每個累加器的值.

在 UI 中跟蹤累加器可以有助于了解運行階段的進度(注: 這在 Python 中尚不支持).

Scala

Java

Python

可以通過調用SparkContext.longAccumulator()或SparkContext.doubleAccumulator()方法創建數值類型的accumulator(累加器)以分別累加 Long 或 Double 類型的值。集群上正在運行的任務就可以使用add方法來累計數值。然而,它們不能夠讀取它的值。只有 driver program(驅動程序)才可以使用value方法讀取累加器的值。

下面的代碼展示了一個 accumulator(累加器)被用于對一個數組中的元素求和:

scala>valaccum=sc.longAccumulator("My Accumulator")accum:org.apache.spark.util.LongAccumulator=LongAccumulator(id:0,name:Some(MyAccumulator),value:0)scala>sc.parallelize(Array(1,2,3,4)).foreach(x=>accum.add(x))...10/09/2918:41:08INFOSparkContext:Tasksfinishedin0.317106sscala>accum.valueres2:Long=10

雖然此代碼使用 Long 類型的累加器的內置支持, 但是開發者通過AccumulatorV2它的子類來創建自己的類型. AccumulatorV2 抽象類有幾個需要 override(重寫)的方法:reset方法可將累加器重置為 0,add方法可將其它值添加到累加器中,merge方法可將其他同樣類型的累加器合并為一個. 其他需要重寫的方法可參考API documentation. 例如, 假設我們有一個表示數學上 vectors(向量)的MyVector類,我們可以寫成:

classVectorAccumulatorV2extendsAccumulatorV2[MyVector,MyVector]{privatevalmyVector:MyVector=MyVector.createZeroVectordefreset():Unit={myVector.reset()}defadd(v:MyVector):Unit={myVector.add(v)}...}// Then, create an Accumulator of this type:valmyVectorAcc=newVectorAccumulatorV2// Then, register it into spark context:sc.register(myVectorAcc,"MyVectorAcc1")

注意,在開發者定義自己的 AccumulatorV2 類型時, resulting type(返回值類型)可能與添加的元素的類型不一致。

累加器的更新只發生在action操作中,Spark 保證每個任務只更新累加器一次,例如,重啟任務不會更新值。在 transformations(轉換)中, 用戶需要注意的是,如果 task(任務)或 job stages(階段)重新執行,每個任務的更新操作可能會執行多次。

累加器不會改變 Spark lazy evaluation(懶加載)的模式。如果累加器在 RDD 中的一個操作中進行更新,它們的值僅被更新一次,RDD 被作為 action 的一部分來計算。因此,在一個像map()這樣的 transformation(轉換)時,累加器的更新并沒有執行。下面的代碼片段證明了這個特性:

Scala

Java

Python

valaccum=sc.longAccumulatordata.map{x=>accum.add(x);x}// Here, accum is still 0 because no actions have caused the map operation to be computed.

部署應用到集群中

應用提交指南描述了如何將應用提交到集群中. 簡單的說, 在您將應用打包成一個JAR(針對 Java/Scala) 或者一組.py或.zip文件 (針對Python), 該bin/spark-submit腳本可以讓你提交它到任何所支持的 cluster manager 上去.

從 Java / Scala 啟動 Spark jobs

org.apache.spark.launcherpackage 提供了 classes 用于使用簡單的 Java API 來作為一個子進程啟動 Spark jobs.

單元測試

Spark 可以友好的使用流行的單元測試框架進行單元測試。在將 master URL 設置為local來測試時會簡單的創建一個SparkContext,運行您的操作,然后調用SparkContext.stop()將該作業停止。因為 Spark 不支持在同一個程序中并行的運行兩個 contexts,所以需要確保使用 finally 塊或者測試框架的tearDown方法將 context 停止。

快速鏈接

您可以在 Spark 網站上看一下Spark 程序示例. 此外, Spark 在examples目錄中包含了許多示例 (Scala,Java,Python,R). 您可以通過傳遞 class name 到 Spark 的 bin/run-example 腳本以運行 Java 和 Scala 示例; 例如:

./bin/run-example SparkPi

針對 Python 示例,使用spark-submit來代替:

./bin/spark-submit examples/src/main/python/pi.py

針對 R 示例,使用spark-submit來代替:

./bin/spark-submit examples/src/main/r/dataframe.R

針對應用程序的優化, 該配置優化指南一些最佳實踐的信息. 這些優化建議在確保你的數據以高效的格式存儲在內存中尤其重要. 針對部署參考, 該集群模式概述描述了分布式操作和支持的 cluster managers 集群管理器的組件.

最后,所有的 API 文檔可在Scala,Java,PythonandR中獲取.

我們一直在努力

apachecn/spark-doc-zh

原文地址: http://spark.apachecn.org/docs/cn/2.2.0/rdd-programming-guide.html

網頁地址: http://spark.apachecn.org/

github: https://github.com/apachecn/spark-doc-zh(覺得不錯麻煩給個 Star,謝謝!~)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容