RDD

版本: 2.3.0

原文鏈接: http://spark.apache.org/docs/latest/rdd-programming-guide.html

Overview

在上層來看,每個spark應用程序都包含一個driver 用于運行用戶的main函數和在集群上執行并行操作 。 RDD(Resilient distributed dataset)彈性數據集是spark的主要抽象,它是分布在集群上的可以并行操作的元素的數據集。 RDD可以在hadoop上文件上創建,或者dirver上已經存在的scala集合創建 和轉換。 RDD駐留在內存中。 RDD可以自動從節點失敗中恢復。
共享變量是spark的第二個重要概念,它可以在并行操作中使用 。 默認情況下, spark以在不同的節點上運行task來實現并行計算, 每個變量的副本都會送達每個task上。 有時,需要在task之間共享變量,或在task和driver之間共享變量。spark支持兩種類型的共享變量: 廣播變量,用于在所有的節點上內存中緩存值; 累加器變量(accumulators),用于累計或統計,如count 、sum。

連接spark

scala

spark 2.3.0 版本默認需要scala 2.11 。
要寫saprk應用,需要maven 依賴:

groupId = org.apache.spark
artifactId = spark-core_2.11
version = 2.3.0

另外,如果需要訪問HDFS集群,需要依賴hadoop-client :

groupId = org.apache.hadoop
artifactId = hadoop-client
version = <your-hdfs-version>

最后,導入spark相關類:

import org.apache.spark.SparkContext
import org.apache.spark.SparkConf

spark應用程序首先要創建 SparkContext對象 ,用于告訴應用如何訪問集群。 要創建SparkContex ,需要先構建SparkConf對象 。 每個JVM只能有一個SparkContext 。 如果已有,則在創建前需要關閉stop() 。

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

其中,appName是應用程序的名稱,會在集群UI上顯示。 master是個url用于連接集群。 實踐中,不會寫死master 。
各種模式下的master url : Spark, Mesos or YARN cluster URL

圖片.png

在spark shell中,SparkContext已經創建好,名稱叫sc 。 使用自建的SparkContext可能不能工作。
--master : 指定連接參數 ;
--jars :逗號分隔列表,用于向classpath添加jar包;
--packages: 添加maven坐標指定的依賴包到classpath ;
--repositores : 可能會包含依賴包的附加倉庫。

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

Or, to also add code.jar to its classpath, use:
$ ./bin/spark-shell --master local[4] --jars code.jar

To include a dependency using Maven coordinates:
$ ./bin/spark-shell --master local[4] --packages "org.example:example:0.1"

更詳細的信息請使用幫助, spark-shell --help 。

java

spark2.3 版本支持lambda表達式 。
從2.2版本起不支持java 7 。
maven 依賴:

groupId = org.apache.spark
artifactId = spark-core_2.11
version = 2.3.0

若訪問hdfs ,則還要添加如下:
groupId = org.apache.hadoop
artifactId = hadoop-client
version = <your-hdfs-version>

需要import的類:

import org.apache.spark.api.java.JavaSparkContext;
import org.apache.spark.api.java.JavaRDD;
import org.apache.spark.SparkConf;

在使用時,首先也要創建 JavaSparkContext對象,創建時需要使用sparkconf類配置相關信息:

SparkConf conf = new SparkConf().setAppName(appName).setMaster(master);
JavaSparkContext sc = new JavaSparkContext(conf);

appName 和 master參數解釋同scala 。

python

spark2.3版本要求的python版本為 python2.7+ 或 3.4+ 或PyPy2.3+ 。
可以使用標準的CPython解釋器,這樣可以使用NumPy。

從spark2.2開始, 不支持Python2.6。

Python寫的spark應用可以使用spark-submit提交 或 在setup.py中包含如下:

    install_requires=[
        'pyspark=={site.SPARK_VERSION}'
    ]

當沒有使用pip install PySpark時, 使用 spark-submit提交應用。 該腳本會加載spark的java/scala庫并允許你提交應用到集群中。 也可以使用pyspark腳本啟動交互式窗口。

若需要方法HDFS數據, 你需要使用當前的hdfs版本構建PySpark。
編程時你需要導入一些module:

from pyspark import SparkContext, SparkConf

PySpark要求在driver和worker端的python版本一致。 他使用PATH中設置的默認的python, 可以通過如下參數修改之:

$ PYSPARK_PYTHON=python3.4 bin/pyspark
$ PYSPARK_PYTHON=/opt/pypy-2.5/bin/pypy bin/spark-submit examples/src/main/python/pi.py

python 下 SparkContext對象的創建:

conf = SparkConf().setAppName(appName).setMaster(master)
sc = SparkContext(conf=conf)

appName 和 master的解釋同scala。

在pyspark shell中 , 已經創建好了SparkContext ,名稱為sc 。 不能自建。 --master參數同上;
--py-files: 指定傳給python 運行時的逗號分隔的 zip或py文件列表。
--packages: 逗號分隔的,maven坐標指定的依賴。
--repositories: 附加的可能存在依賴包的倉庫。
任何spark包依賴的python模塊(在requirements.txt中列出的)需要手工使用pip安裝。

$ ./bin/pyspark --master local[4]

Or, to also add code.py to the search path (in order to later be able to import code), use:
$ ./bin/pyspark --master local[4] --py-files code.py

pyspark的完整選項列表, 使用 --help 。

也可以在IPython中啟動pyspark 。 pyspark要的版本1.0.0+ 。 要使用IPython , 需要設置如下參數:

$ PYSPARK_DRIVER_PYTHON=ipython ./bin/pyspark

要使用 Jupyter notebook :

$ PYSPARK_DRIVER_PYTHON=jupyter 
$ PYSPARK_DRIVER_PYTHON_OPTS=notebook ./bin/pyspark

你可以通過PYSPARK_DRIVER_PYTHON_OPTS參數 定做ipython 或 jupyter 命令。

彈性數據集 RDD

可并行操作, 容錯 。
兩種方式創建RDD : parallelizing dirver中已有的數據集; 引用外部存儲中的已有數據集 (如共享文件系統、HDFS、HBase、或提供hadoop InputFormat的任何數據源)。

Parallelized Collections

(對數據集進行分區,一遍可以并行計算,分區方法稱為sc的 parallelize方法)

scala

從drvier應用中的已經存在的數據集來創建Parallelized collection ,這些方法被稱為SparkContex的parallelize方法 。 示例:

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

創建后,分布式數據集(distData)可以被并行操作。 例如,我們可以調用 distData.reduce((a, b) => a + b) 來處理array中的各元素相加。

一個非常重要的參數partitions 是指并行操作數據集的并行度。 spark會為每個partition啟動一個task 。 典型一個cpu分2-4個partition 。 spark會根據集群配置自動進行partition 。 但是也可以手工指定,如parallelize (e.g. sc.parallelize(data, 10)). (第二個參數指定并行數量)。

java

由JavaSparkcontext的Parallelize 方法創建 。

List<Integer> data = Arrays.asList(1, 2, 3, 4, 5);
JavaRDD<Integer> distData = sc.parallelize(data);

python

data = [1, 2, 3, 4, 5]
distData = sc.parallelize(data)

External Datasets

scala

spark可以從文件創建分布式datasets ,文件可以是本地文件、hdfs、 cassandra、hbase、 Amazon s3 等。 支持 text 文件、 SequenceFiles 和其他任何Hadoop InputFormat 。

文本文件RDD可以使用 sc的textFile方法創建。 該方法接收文件的URI( 可以是本地文件路徑,或 hdfs:// 或 s3a:// 等) , 讀取為按行分割的集合。 例如:

scala> val distFile = sc.textFile("data.txt")
distFile: org.apache.spark.rdd.RDD[String] = data.txt MapPartitionsRDD[10] at textFile at <console>:26

一旦創建, distFile即可使用dtaset操作。 例如,可以使用map或reduce操作來計算長度 : distFile.map(s => s.length).reduce((a, b) => a + b).

注意:

  • 當使用本地文件時, 該文件必須在各個worker節點都存在,并且路徑名也要相同。
  • 所有的spark的文件輸入方法,包括textFile, 支持文件目錄、壓縮文件和通配符。 例如: textFile("/my/directory"), textFile("/my/directory/.txt"), and textFile("/my/directory/.gz").
  • textFile方法支持第二個參數, 來指定并行分區數量。 默認spark為每個block創建一個分區, 可以通過傳入一個大值,來提供分區數量。 注意,分區數量不能少于blocks數量。

scala api 支持其他的數據格式:

  • SparkContext.wholeTextFiles 支持讀取包含多個小的text 文件的目錄, 返回 (filename, content)對。 對比textFile方法,返回的是每條記錄一行。 分區數量由數據局部性決定,在某些情況下可能導致分區太少。對于此情況,wholeTextFile提供第二個參數來控制最小分區數量。
  • 使用SparkContext’s sequenceFile[K, V] 方法來處理sequenceFile , k 和v 是文件中的key和value的類型。 這應該是Hadoop Writable接口的子類,如 IntWritable 和 Text 。 另外,spark允許你指定本地類型, 如sequenceFile[Int, String] 將自動讀取 IntWritables and Texts.
  • 對于其他hadoop InputFormats , 可以使用 SparkContext.hadoopRDD 方法, 該方法接收一個 JobConf 和 input 格式類、key 類 和 value 類。 也可以使用 SparkContext.newAPIHadoopRDD方法,來處理基于“new” MapReduce API (org.apache.hadoop.mapreduce) 的InputFormat 。
  • RDD.saveAsObjectFile and SparkContext.objectFile 支持以簡單格式來保存RDD, 由序列號的Java對象組成。 雖然這不像Avro格式有效的保存RDD, 但是他提供了一個簡單方式。

java

使用類似textFile 來創建之

JavaRDD<String> distFile = sc.textFile("data.txt");

其他內容同scala。

python

>>> distFile = sc.textFile("data.txt")

其他內容同sacla。

RDD.saveAsPickleFile and SparkContext.pickleFile 支持保存RDD ,以簡單格式包含序列化 pickle python對象。 批量序列化,默認值10.

注意: 該組件當前被標記為 Experimental ,為高級用戶提供。 將來將被替換為支持sparksql 。

writable support

pyspark SequenceFile 支持加載java key-value RDD , 轉換Writables為java類型,使用Pyrolite序列化Java 對象 。 當保存RDD key-value 到SequenceFile , pyspark再反向處理。 將python對象轉換為java讀寫,然后轉換Writables。 下表列出會自動轉換內容:

圖片.png

Array類型不會被處理。 用戶需要自定義ArrayWritable來支持之。

保存及加載SequenceFile

同text file一樣, SequenceFile在加載和保存時可以指定路徑。 key vlaue 類需要指定,但是對于標準的Writables 不需要:

>>> rdd = sc.parallelize(range(1, 4)).map(lambda x: (x, "a" * x))
>>> rdd.saveAsSequenceFile("path/to/file")
>>> sorted(sc.sequenceFile("path/to/file").collect())
[(1, u'a'), (2, u'aa'), (3, u'aaa')]

保存和加載其他 hadoop Input/Output 格式

pysaprk可以讀寫其他任何的hadoop 輸入出格式。 如果需要, hadoop配置可以以python 字典的形式傳遞, 如下實例使用Elasticsearch ESInputFormat:

$ ./bin/pyspark --jars /path/to/elasticsearch-hadoop.jar
>>> conf = {"es.resource" : "index/type"}  # assume Elasticsearch is running on localhost defaults
>>> rdd = sc.newAPIHadoopRDD("org.elasticsearch.hadoop.mr.EsInputFormat",
                             "org.apache.hadoop.io.NullWritable",
                             "org.elasticsearch.hadoop.mr.LinkedMapWritable",
                             conf=conf)
>>> rdd.first()  # the result is a MapWritable that is converted to a Python dict
(u'Elasticsearch ID',
 {u'field1': True,
  u'field2': u'Some Text',
  u'field3': 12345})

注意:
請注意,如果InputFormat僅依賴于Hadoop配置和/或輸入路徑,并且可以根據上表輕松轉換鍵和值類,那么此方法應該適用于這種情況。

如果你有自定義的序列化二進制數據(比如從Cassandra / HBase加載數據),那么你首先需要將Scala / Java端的數據轉換為Pyrolite pickler可以處理的數據。 為此提供了轉換器特性。 只需擴展此特征并在轉換方法中實現您的轉換代碼即可。 請記住確保此類以及訪問InputFormat所需的任何依賴項都打包到Spark作業jar中并包含在PySpark類路徑中。

有關使用Cassandra / HBase InputFormat和OutputFormat以及自定義轉換器的示例,請參閱 Python examples and Converter examples

RDD 操作

RDD 支持兩種操作, transformations: 用于從已經存在的dataset生成新的; actions: 將計算結果返回driver 。
例如: map 是個 transformations , 傳送每個dataset的元素到一個function ,然后以一個新RDD的形式表示結果 。 reduce 是個action ,使用一些function來處理所有的rDD元素, 返回最終結果到driver (也有通過reducebyKey返回并行計算結果)。

spark中的所有的transformations 都是延遲加載的, 他們不會立馬計算結果。 只有當遇到action時才會計算結果 。 該設計使spark運行的更高效 。 例如: 一個由map生成的dataset ,后面將通過reduce返回結果給driver , 不會將中間結果返回給driver 。

Basics

python

要說明RDD 基礎操作, 看下面實例:

lines = sc.textFile("data.txt")
lineLengths = lines.map(lambda s: len(s))
totalLength = lineLengths.reduce(lambda a, b: a + b)

第一行從外部文件生成RDD 。 數據不會加載到內存中, lines只是指向了該文件。
第二行定義了lineLengths 作為map轉換后的結果 。 lineLengths不會立即開始計算。
最后一行, 運行reduce , 是個action 。 此時spark將計算轉換為task 可以運行在不同的機器上, 每臺機器上運行各自的map和reduce ,返回給driver應用程序。

如果我們想在后續繼續使用lineLengths , 我可以在reduce之前加上:

lineLengths.persist()

這會使lineLengths在第一次計算完成后保存到memory中。

scala

示例如下:

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

三行的解釋如上。
若想在后續繼續使用lineLengths , 在reduce之前加上 lineLengths.persist() , 會將其在第一次計算完成后保存到內存中 。

java

示例如下:

JavaRDD<String> lines = sc.textFile("data.txt");
JavaRDD<Integer> lineLengths = lines.map(s -> s.length());
int totalLength = lineLengths.reduce((a, b) -> a + b);

三行的解釋同上。
若想在后續繼續使用lineLengths , 在reduce之前加上lineLengths.persist(StorageLevel.MEMORY_ONLY()); , 會將其在第一次計算完成后保存到內存中 。

傳遞函數給spark

python

spark API 依賴driver傳遞過來的functions。 有如下三種建議方式:

  • lambda 表達式 : 以表達式的形式表示的簡單函數 。(lambda不支持多行表示或者返回值 )
  • 對于長代碼, 定義在傳遞給spark的代碼中,以def形式。
  • module中的 functions

例如,傳遞不能用lambda 表達式表示的長代碼函數:

"""MyScript.py"""
if __name__ == "__main__":
    def myFunc(s):
        words = s.split(" ")
        return len(words)

    sc = SparkContext(...)
    sc.textFile("file.txt").map(myFunc)

注意:可以傳遞方法的引用到一個類實例中,需要將包含對象的類一塊發送到方法中, 如:

class MyClass(object):
    def func(self, s):
        return s
    def doStuff(self, rdd):
        return rdd.map(self.func)

當我們創建一個類Myclass 的實例,并調用doStuff實例時,里面調用的map需要函數func , 整個對象都要發送到集群中。

同樣, 訪問一個對象的域 需要引用整個對象 :

class MyClass(object):
    def __init__(self):
        self.field = "Hello"
    def doStuff(self, rdd):
        return rdd.map(lambda s: self.field + s)

要避免這個問題, 最簡單的方式是將域拷貝到本地變量:

def doStuff(self, rdd):
    field = self.field
    return rdd.map(lambda s: field + s)

java

使用java 傳遞函數到集群, 這些函數需要實現接口org.apache.spark.api.java.function 。 創建這種函數有兩種方式:

  • 在自己的類中實現Function 接口, 作為匿名類或有名類,傳遞引用至spark 。
  • 使用lambda表達式 。

例如:

JavaRDD<String> lines = sc.textFile("data.txt");
JavaRDD<Integer> lineLengths = lines.map(new Function<String, Integer>() {
  public Integer call(String s) { return s.length(); }
});
int totalLength = lineLengths.reduce(new Function2<Integer, Integer, Integer>() {
  public Integer call(Integer a, Integer b) { return a + b; }
});

或者, 可以使用內聯函數,但很笨拙:

class GetLength implements Function<String, Integer> {
  public Integer call(String s) { return s.length(); }
}
class Sum implements Function2<Integer, Integer, Integer> {
  public Integer call(Integer a, Integer b) { return a + b; }
}

JavaRDD<String> lines = sc.textFile("data.txt");
JavaRDD<Integer> lineLengths = lines.map(new GetLength());
int totalLength = lineLengths.reduce(new Sum());

java的匿名內部類也可以訪問封閉范圍內的final標識的變量 。 spark將這些變量副本發送給每個節點(跟其他語言一樣)。

scala

有兩種方式傳遞scala函數:

  • 匿名函數語法, 可用來寫短片代碼。
  • 全局單例對象中的靜態方法 , 例如可以定義對象MyFunctions ,并傳遞MyFunctions.func1 如下:
object MyFunctions {
  def func1(s: String): String = { ... }
}

myRdd.map(MyFunctions.func1)

也可以也可以向類實例中的方法傳遞引用,這要求發送的對象要包含該方法的類, 如:

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

當我們創建MyClass實例 并調用doStuff方法, map 引用func1 方法, 因此整個對象都要發送到集群。 寫法類似于 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) 。 要避免這個問題, 最簡單的方式是將變量拷貝到本地 :

def doStuff(rdd: RDD[String]): RDD[String] = {
  val field_ = this.field
  rdd.map(x => field_ + x)
}

閉包

在集群上執行代碼的變量和方法的scop和生命周期比較難理解。 RDD操作修改他們范圍之外的變量經常會引發混亂。 下面的例子, 可以看到foreach()增加counter:

scala

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

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

println("Counter value: " + counter)

java

int counter = 0;
JavaRDD<Integer> rdd = sc.parallelize(data);

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

println("Counter value: " + counter);

python

counter = 0
rdd = sc.parallelize(data)

# Wrong: Don't do this!!
def increment_counter(x):
    global counter
    counter += x
rdd.foreach(increment_counter)

print("Counter value: ", counter)

本地模式 vs 集群模式

上面的代碼不會正常工作 。 為了執行job, spark將RDD操作分割成task ,交給exector進行執行 。 在開始計算之前, spark計算task中的閉包。 這些閉包在RDD執行計算期間必須可見。閉包被序列化并發送給各個executor 。

閉包中的變量發送到各個executor上的是當時的副本, 當在foreach中引用counter時, 它不是driver上的那個counter。 在driver節點上一直會有一個counter 變量,但是對各個executor已經不可見了! executor只會看到序列化后的閉包中的counter 。 因此,打印出的counter值會是0 。

在local 模式, 某些情況下 foreach函數所在的jvm與drvier的jvm一樣,這時會更新到counter。

要使上述代碼正確執行應該使用 Accumulator(累加器)。 累加器在spark中提供了一種更新變量值的安全機制。 本篇的Accumulator有更加詳細的信息。

一般情況下, 閉包- 如循環 或者本地定義方法不應該修改全局狀態。 spark不定義和保證修改閉包引用的外部對象的正確行為。 一些代碼在本地模式下工作正常,但是在分布式模式下不如預期。 在這種需要全局聚合的情況下使用累加器。

打印RDD的元素

另外一個常見的習慣用法是打印RDD的元素, 使用方法: rdd.foreach(println) or rdd.map(println). 在單機上,這回輸出期望的結果。 但是在集群模式下, executors調用的stdout 不會將輸出輸出值driver,會在execotor本地打印。 要打印所有的元素,可以使用 collect() , 他會將所有的RDD元素聚合到drvier節點, 但這可能會撐爆driver的內存( rdd.collect().foreach(println)) 。 比較安全的方式是使用 take方法: rdd.take(100).foreach(println).

使用 key-value 對

大多數的spark RDD操作支持所有的類型, 少部分支持只支持key-value對。 最常用的是shuffle操作, 如按key進行分組或聚合。

python

在Python中, 支持python內置的tupe 。
例如, 下面例子在鍵值對上使用reducebyKey操作, 以統計行的出現次數:

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

我們也可以使用 counts.sortbyKey() 將鍵值對按字母排序, 最后使用counts.collect()發送到driver。

java

在java中, 鍵值對使用Scala標準庫scala.Tuple2 scala.Tuple2
表示 。使用 new Tuple2(a,b) 來創建一個tuple , 然后使用tuple._1() 和 tuple._2() 來方式減值。

RDDs鍵值對使用類 JavaPairRDD 表示。 可以從JavaRDDs 使用特殊的map操作(如: mapToPair and flatMapToPair) 來創建JavaPairRDD 實例。 JavaPairRDD 有標準的RDD函數和特殊的鍵值對函數。

例如, 下面示例使用reducebyKey 操作來統計每行在文本中出現的次數:

JavaRDD<String> lines = sc.textFile("data.txt");
JavaPairRDD<String, Integer> pairs = lines.mapToPair(s -> new Tuple2(s, 1));
JavaPairRDD<String, Integer> counts = pairs.reduceByKey((a, b) -> a + b);

我們也可以使用 counts.sortByKey() 排序 , 然后使用counts.collect() 將結果發送給driver。

注意: 當在鍵值對操作中,使用自定義的類作為key時, 必須確保自定義的equals()函數同時定義hashCode()函數。 詳細信息參見: Object.hashCode() documentation.

scala

在scala , 這些操作只在包含Tuple2的RDD上可用。 鍵值對操作在類PairRDDFunctions中, 他自動wrap一個RDD。
例:

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上。
注意: 當使用自定義類作為key時, 該類需要實現equals() 和 hashCode()方法。 詳情請參見 Object.hashCode() documentation.

transformations

下表列出了常用的transformations操作, 參見RDD API文檔 (Scala, Java, Python, R)
, 和 鍵值對RDD 文檔 (Scala, Java)。

Transformation Meaning
map(func) Return a new distributed dataset formed by passing each element of the source through a function func. (通過將每個元素傳給func返回一個新的數據集)
filter(func) Return a new dataset formed by selecting those elements of the source on which funcreturns true. (返回經過func函數為true的元素組成的新的數據集)
flatMap(func) Similar to map, but each input item can be mapped to 0 or more output items (so func should return a Seq rather than a single item). (類似map, 但是每個輸入的item可以被映射為0或多個輸出item,func函數應該返回一個序列而不是一個item)
mapPartitions(func) Similar to map, but runs separately on each partition (block) of the RDD, so func must be of type Iterator<T> => Iterator<U> when running on an RDD of type T. (類似map, 但是在RDD的每個分區單獨計算, func應該是iterator類型)
mapPartitionsWithIndex(func) Similar to mapPartitions, but also provides func with an integer value representing the index of the partition, so func must be of type (Int, Iterator<T>) => Iterator<U> when running on an RDD of type T.(類似mapPartitions,但是會指定一個整形來指定分區,所以func的參數形式應該是 ) (Int, Iterator<T>) => Iterator<U>
sample(withReplacement, fraction, seed) Sample a fraction fraction of the data, with or without replacement, using a given random number generator seed.(從數據中抽樣fraction(分數)的數據,使用或不使用替換,使用給定的隨機數種子。)
union(otherDataset) Return a new dataset that contains the union of the elements in the source dataset and the argument.(返回并集)
intersection(otherDataset) Return a new RDD that contains the intersection of elements in the source dataset and the argument. (返回交集)
distinct([numPartitions])) Return a new dataset that contains the distinct elements of the source dataset.(返回新的唯一的數據集)
groupByKey([numPartitions]) When called on a dataset of (K, V) pairs, returns a dataset of (K, Iterable<V>) pairs. Note: If you are grouping in order to perform an aggregation (such as a sum or average) over each key, using reduceByKey or aggregateByKey will yield much better performance. Note: By default, the level of parallelism in the output depends on the number of partitions of the parent RDD. You can pass an optional numPartitions argument to set a different number of tasks.(當使用在K,V對上,返回一個新的數據集對(K,Iterable<V>). 若為了聚集而對每個key進行grouping,像sum 或 average, 使用reduceByKey 或aggregateByKey性能會更好; 默認情況下并行度參數依賴于RDD的分區數,可以傳入一個分區數參數以設置并行度。)
reduceByKey(func, [numPartitions]) When called on a dataset of (K, V) pairs, returns a dataset of (K, V) pairs where the values for each key are aggregated using the given reduce function func, which must be of type (V,V) => V. Like in groupByKey, the number of reduce tasks is configurable through an optional second argument. (對應(K,V)使用, 返回一個新的(K,V),每個key對應的值會通過func函數聚合,func的參數應該是(V,V)=> V. reduce任務的數量可以通過的第二個參數控制。)
aggregateByKey(zeroValue)(seqOp, combOp, [numPartitions]) When called on a dataset of (K, V) pairs, returns a dataset of (K, U) pairs where the values for each key are aggregated using the given combine functions and a neutral "zero" value. Allows an aggregated value type that is different than the input value type, while avoiding unnecessary allocations. Like in groupByKey, the number of reduce tasks is configurable through an optional second argument.(對于(K,V)對返回一個新的(K,U)對, 對每個key的所有值和一個初始的中性值使用給定的combine函數聚集。允許聚集的值的類型與輸入不同。 第二個參數指定并行度)
sortByKey([ascending], [numPartitions]) When called on a dataset of (K, V) pairs where K implements Ordered, returns a dataset of (K, V) pairs sorted by keys in ascending or descending order, as specified in the boolean ascending argument.(當對數據集對(K,V)使用時,要求K實現了Ordered接口,返回排序后結果,參數可以指定升序或降序,第二個參數指定并行度)
join(otherDataset, [numPartitions]) When called on datasets of type (K, V) and (K, W), returns a dataset of (K, (V, W)) pairs with all pairs of elements for each key. Outer joins are supported through leftOuterJoin, rightOuterJoin, and fullOuterJoin. (對兩個數據集(K,V)和(K,U)使用, 返回(K,(V,U))。外連接使用 leftOuterJoin 、rightOuterJoin 和fullOutJoin )
cogroup(otherDataset, [numPartitions]) When called on datasets of type (K, V) and (K, W), returns a dataset of (K, (Iterable<V>, Iterable<W>)) tuples. This operation is also called groupWith. (對(K,V)和(K,W)使用, 返回 (K,(Iterable<V>, Iterable<W>)), 也可以使用groupwith)
cartesian(otherDataset) When called on datasets of types T and U, returns a dataset of (T, U) pairs (all pairs of elements). (當對類型T和U使用, 返回(I,U)對)
pipe(command, [envVars]) Pipe each partition of the RDD through a shell command, e.g. a Perl or bash script. RDD elements are written to the process's stdin and lines output to its stdout are returned as an RDD of strings. (對每個RDD使用shell命令, RDD元素被作為stdin寫入, 返回的stdout 作為一個新的RDD)
coalesce(numPartitions) Decrease the number of partitions in the RDD to numPartitions. Useful for running operations more efficiently after filtering down a large dataset.(降低分區的并行數。當對一個大數據集過濾后使用非常有用,會提供操作效率)
repartition(numPartitions) Reshuffle the data in the RDD randomly to create either more or fewer partitions and balance it across them. This always shuffles all data over the network.(對RDD的數據重新洗牌,創建多余或少于的分區以平衡集群數據,這會引起網絡傳輸數據。)
repartitionAndSortWithinPartitions(partitioner) Repartition the RDD according to the given partitioner and, within each resulting partition, sort records by their keys. This is more efficient than calling repartition and then sorting within each partition because it can push the sorting down into the shuffle machinery. (根據給定的分區參數進行重新分區并在每個分區按key進行排序。這筆repartition更高效,因為他將排序放到了shuffle機器中做。)

Actions

下表列出了通用actions。 詳細信息請參考 RDD API doc (Scala, Java, Python, R)
和 pair RDD functions doc (Scala, Java) for details.

Action Meaning
reduce(func) Aggregate the elements of the dataset using a function func (which takes two arguments and returns one). The function should be commutative and associative so that it can be computed correctly in parallel.(使用func聚合dataset中的每個元素, func輸入兩個參數返回一個, func應該是可以關聯的且可交換的,以保證并行計算的正確性。)
collect() Return all the elements of the dataset as an array at the driver program. This is usually useful after a filter or other operation that returns a sufficiently small subset of the data.(向driver返回所有的dataset元素,以數組的形式,這適用當過濾或其他操作后,數據集中數據比較少的情況)
count() Return the number of elements in the dataset. (返回數據集中元素個數)
first() Return the first element of the dataset (similar to take(1)). (返回數據集第一個元素,同take(1))
take(n) Return an array with the first n elements of the dataset. (以數組的形式返回數據集中前n個元素)
takeSample(withReplacement, num, [seed]) Return an array with a random sample of num elements of the dataset, with or without replacement, optionally pre-specifying a random number generator seed. (以數據的形式返回隨機num個樣本, 可以使用替換,可以輸入隨機種子)
takeOrdered(n, [ordering]) Return the first n elements of the RDD using either their natural order or a custom comparator. (返回前n個RDD元素,使用自然順序或自定義比較器)
saveAsTextFile(path) Write the elements of the dataset as a text file (or set of text files) in a given directory in the local filesystem, HDFS or any other Hadoop-supported file system. Spark will call toString on each element to convert it to a line of text in the file. (向給定的路徑目錄寫入數據集,寫為text文本文件形式,支持本地文件系統、hdfs或任何支持的hadoop文件系統。 每個元素的toString方法被調用,作為一行寫入文件中。)
saveAsSequenceFile(path)
(Java and Scala) Write the elements of the dataset as a Hadoop SequenceFile in a given path in the local filesystem, HDFS or any other Hadoop-supported file system. This is available on RDDs of key-value pairs that implement Hadoop's Writable interface. In Scala, it is also available on types that are implicitly convertible to Writable (Spark includes conversions for basic types like Int, Double, String, etc). (向給定的文件路徑將數據集的元素寫為SequenceFile , 這對實現了Writable接口的的RDD key-value對有用。)
saveAsObjectFile(path) (Java and Scala) Write the elements of the dataset in a simple format using Java serialization, which can then be loaded usingSparkContext.objectFile().(將數據集寫為簡單文件格式,使用java的序列號接口,該文件可以使用 SparkContext.objectFile() 加載)
countByKey() Only available on RDDs of type (K, V). Returns a hashmap of (K, Int) pairs with the count of each key. (只對RDD的(K,V)有用,返回(K,int)值為每個key的數量)
foreach(func) Run a function func on each element of the dataset. This is usually done for side effects such as updating an Accumulator or interacting with external storage systems. Note: modifying variables other than Accumulators outside of the foreach() may result in undefined behavior. See Understanding closures for more details. (對數據集的每個元素使用func 。 這通常用于更新累加器或與外部存儲交互。注意,修改foreach之外的累加器可能會導致未定義的操作,參見閉包)

RDD API還提供上述aciton的異步版本, 如foreach的 foreachAsync , 他立即將FutureAction返回給調用者, 而不是等待計算完成。 可以使用FutrueAction進行異步執行的管理等。

Shuffle 操作

Spark中的某些操作會觸發一個稱為shuffle的事件。shuffle是Spark重新分配數據的機制,以便在不同分區之間進行分組。這通常涉及在執行者和機器之間復制數據,使得洗牌成為復雜而昂貴的操作。

Background

以reduceByKey操作來看下究竟發生了什么? reducebykey操作會生成一個新的RDD , 同一個key的所有的value組成一個元組。 但是并非同一個key的元素都在同一個分區、或同一個機器。
在計算時,需要從所有的分區中找同一key的數據,然后將他們匯總一塊, 這個過程稱為shuffle。

若希望在shuffle時對元素進行排序, 使用下面的方法:

  • mapPartitions , 使用sorted 排序
  • repartitionAndSortWithinPartitions 在重新分區同時進行排序
  • sortBy 對RDD全局排序

會引起shuffle的操作有 repartition類操作,如 repartition and coalesce,
ByKey類操作(除了counting)如 groupByKey and reduceByKey,
Join類操作: 如 cogroup and join.

性能影響

shuffle操作非常昂貴, 他涉及到 disk I/O, data serialization, and network I/O. 為了shuffle, spark 生成一組map 任務來組織數據, 生成一組reduce任務來聚合之。這里的概念來自mapreduce , 并不直接與spark的map reduce關聯。

在內部, map任務的結果會保存在內存中,知道不能存下。 然后會根據分區進行排序并寫入文件。 對于reduce 任務相關的排序blocks。

一些shuffle操作會占用大量的內存,因為數據結構是存在內存中的,數據組織、轉換都是內存, 當內存不足時, 會寫入到磁盤上, 這會增加磁盤io和觸發垃圾回收。

shuffle會在磁盤上生產大量中間文件, 從spark1.3開始這些文件不會被自動清理,直到相應的RDD不再使用并被垃圾收集為止。 這是為了避免再次使用時進行重新shuffle 。若應用程序保留對RDD的引用或GC啟動不頻繁,則垃圾收集可能會很久才會觸發一次。 spark.local.dir指定的臨時目錄可能會很大。

可以通過參數調優shuffle 。 See the ‘Shuffle Behavior’ section within the Spark Configuration Guide.

RDD 持久化

在spark的操作中會持久化和緩存內存中的數據集。 持久化時,每個節點都會存儲他在內存中的所有分區,并在使用時重用他們。 這可以使將來的行動更快。 緩存是迭代計算 和交互式計算的關鍵。

在使用RDD時可以標記使用其persist() 或 cache() 方法。 在第一次action時, 會緩存在內存中。 spark的緩存是容錯的, 若分區丟失,會自動重新計算。

每個持久化RDD可以使用不同的存款級別, 如,保存在磁盤、內存、序列化java對象等。 通過向persist()方法傳遞StorageLevel對象來指定 (Scala,Java, Python)。 cache()的使用默認的存儲級別, StorageLevel.MEMORY_ONLY 。 存儲級別如下:

圖片.png

注意: 在python中, 對象的存儲總是使用 Pickle 庫, 因此會忽略使用的序列級別。 python可用的存儲級別有 MEMORY_ONLY, MEMORY_ONLY_2, MEMORY_AND_DISK, MEMORY_AND_DISK_2, DISK_ONLY, and DISK_ONLY_2.
spark會自動持久化shuffle操作的中間結果, 即時用戶不調用persist 。 這是為了避免在洗牌過程中節點失敗時重新計算整個輸入。我們仍建議用戶調用persist生成的RDD,如果他們打算重用它。

存儲級別的選擇

Spark的存儲級別旨在提供內存使用和CPU效率之間的不同折衷。我們建議通過以下流程來選擇一個:

  • If your RDDs fit comfortably with the default storage level (MEMORY_ONLY), leave them that way. This is the most CPU-efficient option, allowing operations on the RDDs to run as fast as possible.

  • If not, try using MEMORY_ONLY_SER and selecting a fast serialization library to make the objects much more space-efficient, but still reasonably fast to access. (Java and Scala)

  • Don’t spill to disk unless the functions that computed your datasets are expensive, or they filter a large amount of the data. Otherwise, recomputing a partition may be as fast as reading it from disk.

  • Use the replicated storage levels if you want fast fault recovery (e.g. if using Spark to serve requests from a web application). All the storage levels provide full fault tolerance by recomputing lost data, but the replicated ones let you continue running tasks on the RDD without waiting to recompute a lost partition.

刪除數據

spark 自動監控緩存 , 使用LRU 刪除就數據分區。 若想要手動處理, 使用 RDD.unpersist() 方法。

共享變量

通常,當spark操作函數執行時, 它將為函數中所有的變量單獨拷貝一個副本傳遞到每個機器上, 這些副本變量不會回傳回driver 。 在task之間支持通用的、 可讀寫的共享變量是非常低效的。 但是park提供了兩種方式: 廣播變量 和 累加器。

廣播變量

廣播變量允許程序員將一個只讀的變量緩存在每臺機器上,而不用在任務之間傳遞變量。廣播變量可被用于有效地給每個節點一個大輸入數據集的副本。Spark還嘗試使用高效地廣播算法來分發變量,進而減少通信的開銷。
Spark的動作通過一系列的步驟執行,這些步驟由分布式的洗牌操作分開。Spark自動地廣播每個步驟每個任務需要的通用數據。這些廣播數據被序列化地緩存,在運行任務之前被反序列化出來。這意味著當我們需要在多個階段的任務之間使用相同的數據,或者以反序列化形式緩存數據是十分重要的時候,顯式地創建廣播變量才有用。
通過在一個變量v上調用SparkContext.broadcast(v)可以創建廣播變量。廣播變量是圍繞著v的封裝,可以通過value方法訪問這個變量。舉例如下:

scala :

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

scala> broadcastVar.value
res0: Array[Int] = Array(1, 2, 3)

java:

Broadcast<int[]> broadcastVar = sc.broadcast(new int[] {1, 2, 3});

broadcastVar.value();
// returns [1, 2, 3]

python:

>>> broadcastVar = sc.broadcast([1, 2, 3])
<pyspark.broadcast.Broadcast object at 0x102789f10>

>>> broadcastVar.value
[1, 2, 3]

在創建了廣播變量之后,在集群上的所有函數中應該使用它來替代使用v.這樣v就不會不止一次地在節點之間傳輸了。另外,為了確保所有的節點獲得相同的變量,對象v在被廣播之后就不應該再修改。

累加器

累加器是僅通過關聯和交換操作“添加”的變量,因此可以有效地支持并行。它們可以用來實現計數器(如在MapReduce中)或sums 。Spark本身支持數字類型的累加器,程序員可以添加對新類型的支持。

作為用戶,您可以創建命名或未命名的累加器。如下圖所示,命名累加器(在本例中counter)將顯示在Web用戶界面中。Spark在“任務”表中顯示由任務修改的每個累加器的值。


圖片.png

跟蹤UI中的累加器對于理解運行階段的進度很有用(注意:Python尚未支持)。

scala

數字類型的累加器可以通過分別調用SparkContext.longAccumulator()或SparkContext.doubleAccumulator() 累加Long或Double類型的值來創建。在集群上運行的任務使用add方法進行累計。但是,它們無法讀取其價值。只有驅動程序可以使用其value方法讀取累加器的值。

下面的代碼顯示了一個累加器,用于累加數組的元素:

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

scala> sc.parallelize(Array(1, 2, 3, 4)).foreach(x => accum.add(x))
...
10/09/29 18:41:08 INFO SparkContext: Tasks finished in 0.317106 s

scala> accum.value
res2: Long = 10

雖然此代碼使用對Long類型的累加器的內置支持,但程序員還可以通過繼承AccumulatorV2來創建它們自己的類型。AccumulatorV2抽象類有幾個方法必須覆蓋:reset將累加器重置為零,add將另一個值添加到累加器中,merge將另一個相同類型的累加器合并到該累加器中 。其他必須被覆蓋的方法包含在API文檔中。例如,假設我們有一個MyVector表示數學向量的類,我們可以這樣寫:

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")

請注意,當程序員定義自己的AccumulatorV2類型時,生成的類型可能與添加的元素的類型不同。

累加器的更新只會在action中, spark保證每個task只會更新一次累加器。 重啟任務不會更新該值 。 在transformation中 , 用戶需要注意若任務或job階段被重新執行,則每個任務只會更新一次。

累加器不會改變spark的延遲計算模式。 注意只有在RDD的action中時才可能會更新。 在延遲執行的map中不會更新。 如下:

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

java

java提供數值類型的累加器Long和Double , 通過 SparkContext.longAccumulator() or SparkContext.doubleAccumulator() 創建 。 (同scala)

LongAccumulator accum = jsc.sc().longAccumulator();

sc.parallelize(Arrays.asList(1, 2, 3, 4)).foreach(x -> accum.add(x));
// ...
// 10/09/29 18:41:08 INFO SparkContext: Tasks finished in 0.317106 s

accum.value();
// returns 10

自定義累加器類型 同scala :

class VectorAccumulatorV2 implements AccumulatorV2<MyVector, MyVector> {

  private MyVector myVector = MyVector.createZeroVector();

  public void reset() {
    myVector.reset();
  }

  public void add(MyVector v) {
    myVector.add(v);
  }
  ...
}

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

累加器延遲更新機制 同scala

LongAccumulator accum = jsc.sc().longAccumulator();
data.map(x -> { accum.add(x); return f(x); });
// Here, accum is still 0 because no actions have caused the `map` to be computed.

python

使用 SparkContext.accumulator(v) 創建, 使用add方法 或 += 操作符累計。 但是exector不能讀取其值, 只能driver讀取。

>>> accum = sc.accumulator(0)
>>> accum
Accumulator<id=0, value=0>

>>> sc.parallelize([1, 2, 3, 4]).foreach(lambda x: accum.add(x))
...
10/09/29 18:41:08 INFO SparkContext: Tasks finished in 0.317106 s

>>> accum.value
10

雖然此代碼使用的內置支持的Int類型的累加器,但程序員也可以通過繼承AccumulatorParam來創建自己的類型。AccumulatorParam接口有兩種方法:zero為您的數據類型提供“零值”,并將addInPlace兩個值一起添加。例如,假設我們有一個Vector表示數學向量的類,我們可以這樣寫:

class VectorAccumulatorParam(AccumulatorParam):
    def zero(self, initialValue):
        return Vector.zeros(initialValue.size)

    def addInPlace(self, v1, v2):
        v1 += v2
        return v1

# Then, create an Accumulator of this type:
vecAccum = sc.accumulator(Vector(...), VectorAccumulatorParam())

累加器的延時特性:

accum = sc.accumulator(0)
def g(x):
    accum.add(x)
    return f(x)
data.map(g)
# Here, accum is still 0 because no actions have caused the `map` to be computed.

部署到集群

提交申請指南介紹了如何提交申請到集群。簡而言之,一旦將應用程序打包為JAR(用于Java / Scala)或一組.py或多個.zip文件(用于Python),該bin/spark-submit腳本可讓您將其提交給任何受支持的集群管理器。

從java/scala啟動作業

org.apache.spark.launcher 包提供類啟動Spark作 。

單元測試

Spark支持任何流行的單元測試框架。只需創建SparkContext,運行您的操作,然后調用SparkContext.stop()把它關閉。確保在finally塊或測試框架tearDown方法中停止它,因為Spark不支持在同一程序中同時運行的兩個上下文。

后續參考哪些

您可以在Spark網站上看到一些Spark程序示例。另外,Spark在examples目錄中包含了幾個樣本(ScalaJavaPythonR)。您可以通過將類名傳遞給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

有關優化程序的幫助,配置調整指南提供最佳做法的信息。它們對于確保您的數據以高效格式存儲在內存中特別重要。有關部署的幫助,群集模式概述描述了分布式操作中涉及的組件以及支持的群集管理器。

最后,完整的API文檔可以在 ScalaJavaPythonR中找到

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

推薦閱讀更多精彩內容