當前位置: 首頁>>編程語言>>正文


最新Spark編程指南Python版[Spark 1.3.0][譯]

本文翻譯自Spark官方文檔:Spark Programming Guide, (spark 1.3.0)去掉了其中Scala/Java的內容,隻保留了Python及公共部分。 

關鍵詞:spark  編程指南  編程入門 python 中文版 spark教程 Spark官方文檔

目錄


概述

從概念上來說,每個spark程序都由一個驅動程序組成,驅動程序運行用戶的main函數並且在集群上執行各種並行操作. Spark中的主要抽象概念是彈性分布式數據集(RDD), RDD是分布在集群各節點上的可並行操作的元素集合。 RDD可以通過hdfs中的文件創建,也可以通過驅動程序中的集合(例如Python中的List)轉化而成。 用戶還可以把RDD保存在內存中,供後續的並行操作高效重複使用. 並且, RDD可以從節點失效中自動恢複.
Spark的另外一個抽象概念是可以並行操作的共享變量(shared variables). By default, when Spark runs a function in parallel as a set of tasks on different nodes默認情況下,Spark以任務(task)集的方式在不同節點上並行執行函數,每個任務都會獲得一份變量的拷貝。 有些情況下,變量需要在任務之間共享,或者再任務和驅動程序之間共享。為了解決這樣的應用場景,Spark支持兩種類型的共享變量:一種是廣播變量( broadcast variables), 這種變量可以緩存在所有節點的內存中;另外一種是累加器(accumulators),種變量隻能增加,例如計數器或者求和.
本文將會以Python語言為例介紹Spark的這些特性。通過啟動Spark交互shell非常容易學習這些特性,Python版的交互shell是bin/pyspark 。

鏈接Spark

Spark 1.3.0 支持Python 2.6+ (但是不支持Python 3)。它使用標準的CPython解釋器,所以可以使用像NumPy這樣的C庫。
在Spark中運行Python程序,需要使用Spark安裝目錄中的bin/spark-submit腳本。 這個腳本會導入Spark的各種Java/Scala庫,然後將程序提交到集群。 我們也可以通過bin/pyspark來啟動Python版的Spark交互shell.
如果我們想訪問HDFS數據,就需要使用hadoop對應的PySpark版本。在第三方發布(third party distributions) 這個網頁上可以看到Cloudera Hadoop和Apache Hadoop的對應關係. 針對通用HDFS版本的預編譯Spark包(Prebuilt packages) 可以在Spark官網找到。
最後,使用Python編寫Spark程序,我們需要在代碼中添加下麵的行:

from pyspark import SparkContext, SparkConf

 

初始化Spark

編寫Spark程序的第一件事情就是創建SparkContext對象,SparkContext負責連接到集群。創建SparkContext先要創建SparkConf對象,該對象可以定義我們Spark程序的相關參數。

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

其中appName是程序名稱,它會顯示在集群狀態界麵上;master是要提交到的集群的地址,相關的值說明如下:

Master值 含義
local 使用本地worker單線程運行
local[K] 使用本地worker K 線程運行
local[*] 使用本地worker,根據機器配置自動選擇線程數
spark://HOST:PORT Spark standalone 集群. Master的域名和端口, 默認端口7077.
mesos://HOST:PORT Spark Mesos 集群. Master的域名和端口,默認端口5050。當Mesos使用Zookeeper, 地址為:mesos://zk://….
yarn-client Spark YARN 集群, 客戶端模式(Yarn Application隻負責給Saprk驅動程序分配資源). 需要在spark-env.sh中配置HADOOP_CONF_DIR.
yarn-cluster Spark YARN 集群,集群模式(Yarn Application接管Spark驅動程序). 需要在spark-env.sh中配置HADOOP_CONF_DIR.

In practice, when running on a cluster, you will not want to hardcodemasterin the program, but rather launch the application withspark-submit and receive it there.在實踐中,當要在集群上運行程序時,我們一般不會把master寫死在程序中,而是通過spark-submit腳本來提交程序,然後在程序中接收master參數值。如果隻是作為測試,那麽直接傳入local做為master在本地運行就可以了。

使用Shell

在PySpark Shell中,SparkContext已經自動創建好了,變量名是sc。 自己創建SparkContext是無效的 。在啟動./bin/pyspark的時候,我們可以通過–master參數指定要連接到的主節點;也可以通過–py-files參數添加Python依賴包, 如.zip/.egg/.py文件,多個文件用英文”,”分隔;還可以通過–packages參數指定各種spark依賴包。另外–repositories參數可以指定依賴依賴的其他庫文件的地址。如果有必要我們可以通過pip命令安裝Spark的python依賴。
我們可以通過下麵的命令本地4線程運行pysparkShell:

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

可以通過下麵的命令添加code.py文件 (從而可以在程序中import code):

$ ./bin/pyspark --master local[4] --py-files code.py

執行./bin/pyspark –help可以拿到完整的參數列表。pyspark實際上調用了更加通用的spark-submit腳本。
另外也可以通過IPython來執行PySpark Shell, 這裏就不多說了。
 

Resilient Distributed Datasets (RDDs)

Spark是圍繞彈性分布式數據集(RDD)的概念展開的,RDD是一種容錯的可分布式操作的數據集合。有兩中方式可以創建RDD:一種是將驅動程序中的已有集合平行化;另外一種是引用外部存儲係統的數據集,例如共享文件係統,HDFS, HBase, 或者其他類似Hadoop的數據源。

並行化集合

在驅動程序中,對已有的可遍曆集合執行SparkContext的parallelize函數,可以創建並行化集合。執行Parallelize函數時,集合元素被複製後用來構成可並行操作的分布式數據集。下麵的代碼給出了如何將元素為數字1~5的鏈表構造成並行化集合:

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

分布式是集合(distData)一經創建,就可以並行操作了。例如,我們可以調用distData.reduce(lambda a, b: a + b) 來給這個整數鏈表求和。我們後續會討論分布式集合上的相關操作。
並行化集合的一個重要參數是切分數據的分區數量,因為Spark會為集群的每個分區啟動一個任務。 典型情況下,集群中的每個CPU需要2~4個分區。通常,Spark會基於集群配置自動設置分區數量。當然,我們也可以通過傳給parallelize函數的第二個參數來手動設置分區數(例如:sc.parallelize(data, 10) )。注意:有些地方也會使用術語slices來表示分區數從而保證向後兼容。
 

外部數據集

PySpark可以從需要hadoop支持的存儲係統創建分布式數據集,包括本地文件係統、HDFS、 Cassandra、HBase以及Amazon S3等等。Spark支持文本文件、sequenceFile以及其他的Hadoop輸入格式。 注:sequenceFile是Hadoop中一個由二進製序列化過的key/value的字節流組成的文本存儲文件。 文本RDD可以通過SparkContext的textFile函數創建。該方法以文件地址(URI)作為輸入並按行讀取。下麵是一個調用的例子:

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

distFile一旦創建,就可以執行數據操作了。例如,求所有行字符串的長度和可以按下麵的方式使用map和reduce: distFile.map(lambda s: len(s)).reduce(lambda a, b: a + b) 。
Spark讀取文件時需要留意的問題:

  • 如果使用本地文件係統地址,文件必須在各worker節點上同樣的位置可讀。要麽將文件拷貝到所有的worker節點,要麽使用網絡共享文件係統。
  • Spark的所有文件輸入方法(包括textFile),都支持目錄、壓縮文件以及通配符。例如,我們可以使用textFile(“/my/directory”)、textFile(“/my/directory/*.txt”)和textFile(“/my/directory/*.gz”)。
  • 可以通過textFile方法的第二個參數來控製文件的分區數量。默認情況下,Spark為每個文件塊(Block)創建一個分區(  HDFS中默認文件塊大小是64M)。但是我們可以通過向函數傳一個大的參數來讓Spark創建更多的分區。注意不能將這個參數設置的比文件塊數量少。

除了textFile, Spark的Python API 也支持幾種另外的數據格式:

  • 文本目錄:使用SparkContext.wholeTextFiles我們可以讀取多個小文本文件的目錄,每個文件會以(文件名, 內容)對的形式返回。這跟textFile按行讀取不同。
  • Python序列化對象:RDD.saveAsPickleFile和SparkContext.pickleFile支持將RDD保存為簡單的Python序列化對象。 序列化過程中,默認每10個元素一起批量處理.
  • sequenceFile和Hadoop輸入/輸出格式

注意:當前上述特性是實驗性的,適合高級用戶。將來這些特性可能會被其他功能例如Spark SQL取代。 寫支持 PySpark對sequenceFile的支持方式是:當導入Key-Value對形式的RDD,將可寫類型轉成JAVA基本類型,然後通過Pyrolite將JAVA基本類型序列化;當保存RDD時,做相反操作,即首先反序列化Python對象轉為Java對象,然後轉為可寫類型。以下的可寫類型會自動進行轉化:

Writable Type Python Type
Text unicode str
IntWritable int
FloatWritable float
DoubleWritable float
BooleanWritable bool
BytesWritable bytearray
NullWritable None
MapWritable dict

當讀寫數組時,不能自動處理,用戶要自定義可寫數組子類型(ArrayWritablesubtypes)。也就是說,寫操作:用戶需要指定自定義轉化器將數組轉為自定義的可寫數組子類型 ; 讀操作, 默認的轉化器會將自定義的可寫數組子類型 轉為Java Object[], 然後序列化為Python元組。 To get Pythonarray.arrayfor arrays of primitive types, users need to specify custom converters.要使用Python數組(array.array)來容納基本類型,用戶需要指定自定義的轉化器。 保存和加載 SequenceFiles 和文本文件一樣,SequenceFiles也可以通過指定路徑來保存和加載。特殊KEY和Value的類需要指定,但是對於標準的可寫類型沒有這個要求。例:

>>> 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 輸入/輸出 格式 PySpark可以讀寫任意的Hadoop輸入輸出格式,兼容新、舊版本的Hadoop MapReduce APIs。如果需要,Hadoop配置也可以作為詞典傳入讀寫文件的API。下麵是使用Elasticsearch ESInputFormat的例子: 注:Elasticsearch是基於Lucene的分布式搜索引擎。

$ SPARK_CLASSPATH=/path/to/elasticsearch-hadoop.jar ./bin/pyspark
>>> 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})

注意,如果輸入格式可以簡單依賴於Hadoop配置或者輸入路徑,並且Key和Value 類可以按照上文表格中列出的進行轉換,那麽例子中的方法能很好的工作。 如果我們有自定義序列化的二進製數據(例如從Cassandra / HBase導入數據),那麽我首先要在Scala/Java環境中把數據轉成Pyrolite序列化能處理的類型。Spark為這種處理提供了一種轉換器特性。在convert方法中,我們可以簡單地擴展這個特性並且實現轉化代碼。要訪問這種輸入格式的數據,還需要確保將這個類以及需要的其他任何依賴打包到Spark job jar中,並且在PySpark classpath中指明。
使用自定義轉換器讀寫Cassandra / HBase輸入/輸出格式的例子在這兩個地方和可以看到:Python examplesConverter examples
 

RDD 操作

RDDs支持兩種類型的操作:一種是轉換(transformations), 該操作從已有數據集創建新的數據集;另外一種是動作(actions),該操作在數據集上執行計算之後返回一個值給驅動程序。例如, map就是一個轉換,這個操作在數據集的每個元素上執行一個函數並返回一個處理之後新的RDD結果。另一方麵,reduce是一個動作,這個操作按照某個函數規則聚集RDD中的所有元素並且把最終結果返回給驅動程序。
Spark中的所有轉換操作都是lazy模式的,也就是說,不是立馬做轉換計算結果,而是將這些轉換操作記錄在相應的數據集上,當需要通過動作(action)把結果返回給驅動程序時才真正執行。這個設計使Spark運行起來更加高效。例如,如果通過map創建的數據集後續會被reduce用到,那麽隻有reduce的結果會返回給驅動程序,而不是更大的map結果。
默認情況下,RDD上的轉換操作在每次做動作時,都會重新執行計算一次。然而,我們可以使用persist(或者cache)函數將RDD存放在內存中,方便後續的快速訪問。另外,Spark也支持將RDD存放在磁盤上,或者在多個節點讓冗餘存儲。

基本用法

為了說明RDD的基本用法,考察下麵的一個簡單程序:

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

第一行定義了一個基本的RDD結果,RDD數據源是一個外部文件。如果沒有實際的操作,這個數據集不會加載到內存,也就是說變量lines僅僅是一個指針而已。第二行將map轉換的結果定義為lineLengths變量。同樣,由於Spark是lazy模式,lineLengths不會立即計算。最後,我們執行reduce, 由於這是一個真正的動作,Spark會將計算任務分發到各機器,每台機器上執行自有數據的map和reduce, 然後隻是把最終結果返回給驅動程序。
If we also wanted to uselineLengthsagain later, we could add:
如果我們後續會再次使用變量lineLengths, 可以添加下麵的命令: lineLengths.persist() 這樣的話,在reduce動作之前,變量lineLengths第一次計算之後就會被保存在內存中。
 

Spark回調函數

Spark的API在很大程度上依賴驅動程序中傳入的函數。這裏推薦三種傳入函

  • Lambda 表達式, 針對可以寫成表達式的簡單函數。(Lambda不支持多行函數,也不支持沒有返回值的函數)
  • 在代碼較多的情況下,可以通過def命令定義成本地函數。
  • 模塊中的頂層函數。

例如,傳入比lambda代碼更長的函數,可以考慮用下麵的方式:

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

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

我們也可以在類實例中傳遞方法的引用(給Spark的RDD操作),這要求將對象(包括類和方法)都發送給集群。例如,看下麵的代碼:

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

如果我們創建一個新的MyClass對象然後調用doStuff,這時map引用的func是MyClass實例的方法,所以整個對象都要發送到集群上。
同樣,訪問外部對象的字段也會導致對整個對象的引用:

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

為了避免這種情況,最簡單的方法是將對象的字段拷貝到一個局部變量而不是直接外部訪問(通過obj.field的方式)。

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

 

使用Key-Value對

雖然絕大部分的Spark操作可以工作在包含任意類型的RDD上,但是還是有一些特殊操作隻在包含Key-Value對的RDD上可用。最常見的是一些分布式”shuffle”(一般指數據按規則重新洗牌, 例如mapreduce中把maper之後相同的key整合到一起再發給reducer)操作,例如通過key對元素進行分組(grouping)或者聚集(aggregating)。
在Python中,這些操作可以在包含Python內置元組的RDD上工作,例如(1,2)。我們可以簡單的創建這樣的元組然後調用想要的操作,例如:下麵的代碼在Key-Value對上使用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()把結果以對象鏈表的形式放回給驅動程序。
 

轉換(Transformations)

下表列出了Spark中一些常見的轉換操作。可以參考RDD API文檔(Python)查看細節。

轉換(Transformation) 含義
map(func) 對每個RDD元素應用func之後,構造成新的RDD
filter(func) 對每個RDD元素應用func, 將func為true的元素構造成新的RDD
flatMap(func) 和map類似,但是flatMap可以將一個輸出元素映射成0個或多個元素。  (也就是說func返回的是元素序列而不是單個元素).
mapPartitions(func) 和map類似,但是在RDD的不同分區上獨立執行。所以函數func的參數是一個Python迭代器,輸出結果也應該是迭代器【即func作用為Iterator<T> => Iterator<U>】。
mapPartitionsWithIndex(func) 和mapPartitions類似, but also provides func with an integer value representing the index of the partition, 但是還為函數func提供了一個正式參數,用來表示分區的編號。【此時func作用為(Int, Iterator<T>) => Iterator<U> 】
sample(withReplacement, fraction, seed) 抽樣: fraction是抽樣的比例0~1之間的浮點數; withRepacement表示是否有放回抽樣, True是有放回, False是無放回;seed是隨機種子。
union(otherDataset) 並集操作,重複元素會保留(可以通過distinct操作去重)
intersection(otherDataset) 交集操作,結果不會包含重複元素
distinct([numTasks])) 去重操作
groupByKey([numTasks]) 把Key相同的數據放到一起【(K, V) => (K, Iterable<V>)】,需要注意的問題:1. 如果分組(grouping)操作是為了後續的聚集(aggregation)操作(例如sum/average), 使用reduceByKey或者aggregateByKey更高效。2.默認情況下,並發度取決於分區數量。我們可以傳入參數numTasks來調整並發任務數。
reduceByKey(func, [numTasks]) 首先按Key分組,然後將相同Key對應的所有Value都執行func操作得到一個值。func必須是(V, V) => V’的計算操作。numTasks作用跟上麵提到的groupByKey一樣。
aggregateByKey(zeroValue, seqOp, combOp, [numTasks]) 首先按Key分組,然後對同Key的Vaue做聚集操作。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 ingroupByKey, the number of reduce tasks is configurable through an optional second argument.
sortByKey([ascending], [numTasks]) 按Key排序。通過第一個參數True/False指定是升序還是降序。
join(otherDataset, [numTasks]) 類似SQL中的連接(內連接),即(K, V) and (K, W) => (K, (V, W)),返回所有連接對。外連接通過:leftOUterJoin(左出現右無匹配為空)、rightOuterJoin(右全出現左無匹配為空)、fullOuterJoin實現(左右全出現無匹配為空)。
cogroup(otherDataset, [numTasks]) 對兩個RDD做groupBy。即(K, V) and (K, W) => (K, Iterable<V>, Iterable(W))。別名groupWith。
cartesian(otherDataset) 笛卡爾積
pipe(command, [envVars]) 將驅動程序中的RDD交給shell處理(外部進程),例如Perl或bash腳本。RDD元素作為標準輸入傳給腳本,腳本處理之後的標準輸出會作為新的RDD返回給驅動程序。
coalesce(numPartitions) 將RDD的分區數減小到numPartitions。當數據集通過過濾減小規模時,使用這個操作可以提升性能。
repartition(numPartitions) 將數據重新隨機分區為numPartitions個。這會導致整個RDD的數據在集群網絡中洗牌。
repartitionAndSortWithinPartitions(partitioner) 使用partitioner函數充分去,並在分區內排序。這比先repartition然後在分區內sort高效,原因是這樣迫使排序操作被移到了shuffle階段。

動作(Actions)

下表列出了一些Spark中常用的動作(actions)。可以參考RDD API文檔(Python)查看細節。

動作(Action) 含義
reduce(func) 使用func函數聚集RDD中的元素(func接收兩個參數返回一個值)。這個函數應該滿足結合律和交換律以便能夠正確並行計算。
collect() 將RDD轉為數組返回給驅動程序。這個在執行filter等操作之後返回足夠小的數據集是比較有用。
count() 返回RDD中的元素數量。
first() 返回RDD中的第一個元素。(通take(1))
take(n) 返回由RDD的前N個元素組成的數組。
takeSample(withReplacement, num, [seed]) 返回num個元素的數組,這些元素抽樣自RDD,withReplacement表示是否有放回,seed是隨機數生成器的種子)。
takeOrdered(n, [ordering]) 返回RDD的前N個元素,使用自然順序或者通過ordering函數對將個元素轉換為新的Key.
saveAsTextFile(path) 將RDD元素寫入文本文件。Spark自動調用元素的toString方法做字符串轉換。
saveAsSequenceFile(path)
(Java and Scala)
將RDD保存為Hadoop SequenceFile.這個過程機製如下:1. Pyrolite用來將序列化的Python RDD轉為Java對象RDD;2. Java RDD中的Key/Value被轉為Writable然後寫到文件。
countByKey() 統計每個Key出現的次數,隻對(K, V)類型的RDD有效,返回(K, int)詞典。
foreach(func) 在所有RDD元素上執行函數func。

RDD持久化

Spark中最重要的能力之一是將數據持久化到內存中方便後續操作。當持久化一個RDD的時候,一旦該RDD在內存中計算出來,每個節點保存RDD的部分分區,在其他動作中就可以重用內存中的這個RDD(以及源於它的新RDD)。這種機製使得後續的動作(actions)快很多(通常在10倍以上)。緩存是迭代算法或者快速交互的利器。
我們可以通過persist() 或者cache()兩個方法將RDD標記為持久化的。該RDD第一次在動作中計算出來之後,就會被保存在各節點的內存中。Spark的緩存是容錯的——任意RDD分區丟失之後,會自動使用原來的轉換動作重新計算出來。
另外,每個持久化的RDD可以按照不同的存儲等級來存儲。例如,可以持久化到磁盤,也可持久化到內存中,還可以放到外部緩存係統(off-Heap)。這些存儲等級可以通過傳一個StorageLevel對象給函數persist()來設置。方法cache()是使用默認存儲等級的快速寫法(Python中存儲等級設置為StorageLevel.MEMORY_ONLY_SER, Scala/Java中是StorageLevel.MEMORY_ONLY)。完整的存儲等級說明如下:

(存儲等級)Storage Level 含義
MEMORY_ONLY 將RDD以反序列化對象保存在JVM中。如果內存容不下RDD,部分分區會再需要的過程中重新計算出來而不是緩存起來。這個是Scala/Java中cache()的默認存儲方式。
MEMORY_AND_DISK 將RDD以反序列化對象保存在JVM中。如果內存容不下RDD,會把容不下的分區放在磁盤上,需要的時候再從磁盤上讀。
MEMORY_ONLY_SER 類似MEMORY_ONLY,不同的對象以序列化JAVA對象的形式存儲(每個分區是一個byte數組)。這種方式比反序列化對象更高效,特別是使用快速序列化器的時候,不過讀數據會消耗更多的CPU。
MEMORY_AND_DISK_SER 類似MEMORY_AND_DISK,不同的對象以序列化JAVA對象的形式存儲
DISK_ONLY 僅將RDD保存在磁盤上。
MEMORY_ONLY_2,MEMORY_AND_DISK_2,
MEMORY_ONLY_SER_2,MEMORY_AND_DISK_SER_2
同上,但是每個分區會在兩個節點上冗餘。
OFF_HEAP (experimental) 將RDD保存在Tachyon上(Tachyon是一個分布式內存文件係統,可以在集群裏以訪問內存的速度來訪問存在tachyon裏的文件)。和MEMORY_ONLY_SER相比,OFF_HEAP可以減少垃圾收集的開銷,還允許執行器(executors)輕量化並共享內存池。這使得OFF_HEAP在大堆內存環境和高並發程序中很有競爭力。而且,由於RDD保存在Tachyon中,執行器的掛掉不會導致內存緩存數據的丟失。

Note: In Python, stored objects will always be serialized with the Pickle library, so it does not matter whether you choose a serialized level. Spark也會自動持久化shuffle操作中的一些即時數據(例如reduceByKey的過程中),即使用戶不顯示調用persist。這樣做可以避免shuffle過程中節點失效時重新計算整個輸入。不過我們還是建議顯示調用persist,如果需要計劃重用RDD的話。

選擇哪個存儲級別?

Spark的存儲級別是為了提供在內存使用CPU效率上的平衡。我們建議通過以下過程來做出選擇。

  • 如果RDD能適應默認的存儲等級(MEORY_ONLY),那就選這種方式。因為這是最利於CPU效率的選擇,允許盡可能快的操作RDD。
  • 如果不行的話就使用(MEMORY_ONLY_SER),並選擇一個快速序列化庫讓對象有更好的空間利用率,並且有相當快的訪問速度。
  • 不要讓RDD溢出到磁盤上,除非計算數據的代價很高,或者產出的數據特別大。
  • 如果想要快速的故障恢複就使用冗餘存儲等級。所有的存儲等級通過重新計算丟失的數據來提供完整的容錯,但是冗餘存儲讓我們可以繼續操作RDD,而不用等待重新計算丟失的分區。
  • 在需要大量內存或者多個應用的環境中,實驗特性OFF_HEAP模式有以下幾個優點:

    • 多個執行器共享內存。
    • 顯劇減少垃圾收集的開銷
    • 個別執行器掛掉不會到值緩存數據丟失。

刪除數據

Spark自動監視每個節點上的緩存使用,並按LRU(最近最少訪問)的方式踢掉數據。如果我們想要手動刪掉RDD而不是等到被踢出緩存,使用RDD.unpersist()方法。

共享變量

通常,當一個函數被傳到遠程集群節點執行的Spark操作中(例如map或者reduce)時,所有的函數變量都會有獨立的工作副本。這些變量會被拷貝到每台機器,遠程機器上變量的修改不會回傳給驅動程序。在任務之間對一般讀寫共享變量的支持比較低效。但是,為了適應通用的引用場景,Spark還是提供了有兩個有限的共享變量類型:廣播變量和累加器。

廣播變量

共享變量允許程序員將一個隻讀的變量緩存在每台機器上,而不是讓每個任務隨帶一個變量的副本。廣播變量為在每個節點上提供海量的輸入數據集提供了一種高效的方式。Spark會嘗試使用高效餓廣播算法來減少分發廣播變量的通信消耗。
廣播變量通過調用SparkContext.broacase(v)創建, v是一個變量。廣播變量是v的封裝, v的值可以通過value方法訪問。下麵的代碼說明了這個用法:

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

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

廣播變量創建之後,應該在所有函數中替代v來使用,以免v多次被發送到集群節點。另外,對象v廣播之後,不應該被修改,從而保證所有的節點看到的是相同的廣播變量值。

累計器

累計器是隻能通過關聯操作做“加”運算的變量,從而可以高效支持並行。它可以用來實現計數器或者求和。Spark原生支持數字類型的累計器,程序員可以增加對新類型的支持。 如果累加器創建時賦給了一個名字,那麽這個累加器會在Spark的UI上展現。這個有利於理解程序的執行過程(遺憾的是這個功能Python中暫不支持)。 累計器通過調用函數SparContext.accumulator(v)並賦予一個初值來創建。然後跑在集群上的任務就可以使用add方法或者+=運算符增加累計器的值。但是,任務是不能讀這個累計器的值得,隻有驅動程序才可以通過方法value來讀。
下麵的代碼展示了將一個數組中的元素都添加到累計器的過程:

>>> accum = sc.accumulator(0)
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

scala> accum.value
10

這段代碼使用的是內置的整數類型累加器,程序員也可以通過子類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())

由於累計器的修改隻能在動作(actions)內執行,Spark可以保證每個任務對累計器的修改隻會執行一次,重啟任務(tasks)也不會導致修改累計器的值。在轉換(transformations)中,如果任務重新執行,用戶需要意識到每個任務中的修改操作都會被執行多次。
累計器不會改變Spark的lazy模式。如果累計器在RDD操作中被修改了,累計器的值隻會在RDD做為動作(actions)操作進行計算時才會被修改。所以,在想map()這樣的lazy轉換中,不能保證累計器的修改被執行完成。下麵的代碼片段說明了這個特性:

accum = sc.accumulator(0)
data.map(lambda x => acc.add(x); f(x))
# Here, acc is still 0 because no actions have cause the `map` to be computed.

部署到集群

程序提交指南描述了如何將程序提交到集群。總之,一旦我們把程序打包成.py文件集或者.zip文件, bin/spark-submit腳本就可以幫助我們將程序提交到任意支持的集群管理器。

單元測試

Spark可以很方便地使用任意流行的UT框架做單元測試。在測試中簡單創建SparkContext並將master URL設置成local,執行操作,然後調用SparkContext.stop()停止Context。確保在結束時停止context或者在測試框架的的tearDown函數中停止context,因為Spark不支持兩個context在同一個程序中運行。

Spark版本遷移(pre-1.0)

Spark 1. 0凍結了Spark 1.X係列的核心API, 也就是說所有現在可用且沒有被標記為實驗性(experimenttal)的或者開發中的API未來版本中也會支持。Python中唯一變化是分組(grouping)操作, 例如groupByKey/cogroup/join, 這些操不再返回(key, list of values),而是返回(key, iterable of values)。其他部分的遷移指南參見:Spark Streaming, MLlib and GraphX.

接下來做什麽

我們可以在Spark網站上閱讀一些Spark程序的例子。另外,Spark安裝目錄下的examples下麵包含了幾個例程,運行方法如下(Python版):

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

為了幫助優化我們的程序,配置優化指南提供了最佳實踐信息。特別重要的是,要確保數據數據在內存中以高效的格式保存。為了幫助我們理解如果部署程序,集群模式總覽介紹了跟分布式操作和集群管理器相關的組件。
Finally, full API documentation is available in Scala, Java and Python.
最後,完整的API文檔參見:Python
【轉載請注明:純淨的天空https://vimsky.com出品】

本文由《純淨天空》出品。文章地址: https://vimsky.com/zh-tw/article/285.html,未經允許,請勿轉載。