《Spark快速大數據分析》——讀書筆記(3)

只看書是快,但是動手時會遇到種種問題,不可怠慢!

第3章 RDD編程

彈性分佈式數據集(Resilient Distributed Dataset,RDD)其實就是分佈式的元素集合。在Spark中,對數據的所有操作不外乎創建RDD、轉化已有RDD以及調用RDD操作進行求值。

3.1 RDD基礎

Spark中RDD是一個不可變的分佈式對象集合。每個RDD都被分爲多個分區,分區運行在集羣的不同節點上。RDD可以包含任意類型的對象。
創建RDD的兩種方法:

  • 讀取一個外部數據集。如Python中lines=sc.textFile(“README.md”)
  • 在驅動器程序裏分發驅動器程序中的對象集合(如list或set) 。

創建後,RDD支持兩種類型的操作:

  • 轉化操作——由一個RDD生成一個新的RDD。如pythonLines=lines.filter(lambda line: “Python” in line)
  • 行動操作——對RDD計算出一個結果,並把結果返回到驅動器程序或存儲。如pythonLines.first()

轉化操作和行動操作的區別在於Spark計算RDD的方式不同。雖然可以在任何時候定義新的RDD,但Spark只會惰性計算這些RDD——即第一次在行動操作中用到時,纔會計算。
最後,默認情況下,Spark的RDD會在你每次對他們進行行動操作時重新計算。如果想在多個行動操作中重用一個RDD,可以使用RDD.persist()讓Spark把這個RDD緩存下來。
在實際操作中,persist()經常被用來把數據的一部分讀取到內存中,並反覆查詢這部分數據。

Spark程序或shell會話的工作方式:

  • 從外部數據創建出輸入RDD
  • 使用諸如filter()這樣的轉化操作對RDD進行轉化,以及定義新的RDD。
  • 告訴Spark對需要被重用的中間結果RDD執行persist()操作。
  • 使用行動操作(例如count()和first()等)來觸發一次並行計算,Spark會對計算進行優化後再執行。
    cache()與使用默認存儲級別調用persist()是一樣的。

3.2 創建RDD

Spark提供了兩種創建RDD的方式:讀取外部數據集,以及在驅動器程序中對一個集合進行並行化。
創建RDD的最簡單的方式是把程序中一個已有的集合傳給SparkContext的parallelize()方法。但實際中用的不多,因爲該方法需把整個數據集先放在一臺機器的內存中。

Python中的parallelize方法

lines=sc.parallelize(["panda","i like pandas"])

更常用的方式是從外部存儲中讀取數據創建RDD。如SparkContext方法將文本文件讀入爲一個存儲字符串的RDD。

Python中的textFile()方法

lines=sc.textFile("/path/to/README.md")

3.3 RDD操作

操作類型 操作結果 典型函數 特點
轉化操作 返回新的RDD map()和filter() 返回RDD
行動操作 向驅動器程序返回結果或把結果寫出 count()和fist() 返回其他類型

3.3.1 轉化操作

RDD的轉化操作是返回新RDD的操作(並不改變原RDD的內容)。許多轉化操作都是針對各個元素的,即只操作RDD中的一個元素。

例:從log.txt中選出錯誤信息及union()函數的用法(python)

inputRDD=sc.textFile("log.txt")
errorsRDD=inputRDD.filter(lambda x: "error" in x)
warningsRDD=inputRDD.filter(lambda x:"warnning" in x)
badLinesRDD=errorsRDD.union(warningsRDD)

轉化操作從已有的RDD中派生出新的RDD,**Spark會使用譜系圖,來記錄這些不同RDD之間的依賴關係。**Spark需要用這些信息來按需計算每個RDD,也可以依靠譜系圖在持久化的RDD丟失部分數據時恢復所丟失的數據。下圖是上例中的譜系圖。
譜系圖示例

3.3.2 行動操作

行動操作會把最終求得的結果返回到驅動器程序,或者寫入外部存儲系統中。由於行動操作需要生成實際的輸出,他們會強制執行那些求值必須用到的RDD的轉化操作

例3-15:在Python中使用行動操作對錯誤進行計數

print "Input had "+badlinesRDD.count()+" concerning lines"
print "Here are 10 examples: "
for lines in badLines RDD.take(10):
    print line

該例中使用take()獲取了少量元素,在本地處理。RDD還有一個collect()函數,可以獲取整個RDD的數據。如果RDD規模較小,且想在本地處理時,可以使用它(只有當整個數據集能在單臺機器的內存中放得下時,才能使用collect())。因此該方法不常用,通常將數據寫到HDFS或Amazon S3這樣的分佈式存儲系統。

注:每當調用一個新的行動操作時,整個RDD都會從頭開始計算。爲避免這種低效的行爲,需將中間結果持久化。

3.3.3 惰性求值

惰性求值指我們對RDD調用轉化操作時,操作不會立即執行。Spark會在內部記錄下所要求執行的操作的相關信息。

我們不應該吧RDD看做存放着特定數據的數據集,而最好把每個RDD當做我們通過轉化操作構建出來的、記錄如何計算數據的指令列表。

另外把數據讀取到RDD的操作同樣是惰性的。如調用sc.textFile()時,數據並沒有讀取進來,而是在必要時纔會讀取。

Spark使用惰性求值,這樣就可以把一些操作合併到一起來減少計算數據的步驟。在類似hadoop MapReduce的系統中,開發者常常花費大量時間考慮如何把操作組合到一起,以減少MapReduce的週期數。而在Spark中,寫出一個非常複雜的映射並不見得能比使用很多簡單的連續操作獲得好很多的性能。因此用戶可以用更小的操作來組織他們的程序。(加黑部分不是很懂)

3.4 向Spark傳遞函數

Spark的大部分轉化操作和一部分行動操作,都需要依賴用戶傳遞的函數來計算。

3.4.1 Python

在Python中,我們有三種方式來把函數傳遞給Spark。傳遞比較短的函數時,可以使用lambda表達式來傳遞。除了lambda表達式,我們也可以傳遞頂層函數或是定義的局部函數。

例3-18:在Python中傳遞函數

word=rdd.filter(lambda s: "error" in s)

def containsError(s)
    return "error" in s
word=rdd.filter(containsError)

傳遞函數時需要注意,Python會把函數所在的對象也序列化傳出去。當你傳遞的對象是某個對象的成員,或者包含了對某個對象中一個字段的引用時(例如self.field),Spark就會把整個對象發到工作節點上,這可能比想傳遞的東西大得多(見例3-19)。另外,如果傳遞的類裏面包含Python不知道如何序列化傳輸的對象,也會導致程序的失敗。(這裏的序列化傳輸不懂)

例3-19:傳遞一個帶字段引用的函數(別這麼做!!)

class SearchFunctions(object):
    def __init__(self, query):
        self.query=query
    def isMatch(self,s):
        return self.query in s
    def getMatchesFunctionReference(self, rdd):
        # 問題:在“self.isMatch”中引用了整個self
        return rdd.filter(self.isMatch)
    def getMatchesMemberReference(self, rdd):
        #問題:在"self.query"中引用了整個self
        return rdd.filter(lambda x: self.query in x)

替代的方案是,吧所需的字段從對象中拿出來放在一個局部變量中,然後傳遞這個局部變量。

例3-20:傳遞不帶字段引用的Python函數

class WordFunctions(object):
    ---
    def getMatchesNoReference(self, rdd):
        #安全:只把需要的字段提取到局部變量中
        query=self.query
        return rdd.filter(lambda x: query in x)

3.4.2 Scala

在Scala中,可以把定義的內聯函數、方法的引用或靜態方法傳遞給Spark,就像Scala的其他函數式API一樣。

注:所傳遞的函數及其引用的數據需要時可序列化的(實現了Java的serializable接口),另外,和Python類似傳遞一個對象的方法或者字段時,會包含對整個對象的引用。

例3-21:Scala中的函數傳遞

class SearchFunctions(val query: String) {
    def isMatch(s:String):Boolean={
        s.contains(query)
    }
    def getMatchesFunctionReference(rdd: RDD[String]): RDD[String]={
        //問題:"isMatch"表示整個"this.ismatch",因此我們要傳遞整個"this"
        rdd.map(isMatch)
    }
    def getMatchesFieldReference(rdd: RDD[String]): RDD[String] = {
        // 問題:"query"表示"this.query",因此我們要傳遞整個"this"     
        rdd.map(x => x.split(query))

    }   
    def getMatchesNoReference(rdd: RDD[String]): RDD[String] = {    
        // 安全:只把我們需要的字段拿出來放入局部變量中
        val query_ = this.query
        rdd.map(x => x.split(query_)
    }
}

3.4.3 Java

在Java中,函數需要作爲實現了Spark的org.apche.spark.api.java.function包中的任一函數接口的對象來傳遞。
Java函數接口
可以把我們的函數類內聯定義爲匿名內部類(例3-22),也可以創建一個具名類(例3-23)

例3-22:在Java中使用匿名內部類進行函數傳遞

RDD<String> errors=lines.filter(new Function<String,Boolean>(){
    public Boolean call(String x){return x.contains("error");}
});

例3-23:在Java中使用具名類進行函數傳遞

class ContainsError implements Function<String ,Boolean>(){
    public Boolean call(String x){return x.contains("error");}
}
RDD<String> errors=lines.filter(new ContainsError());

使用頂級函數的另一個好處在於你可以給他們的構造函數添加參數,如例3-24所示。

例3-24:帶參數的Java函數類

class Contains implements Function<String, Boolean>(){
    private String query;
    public Contains(String query){ this.query=query;}
    public boolean call(String x){return x.contains(query);}
}
RDD<String> errors=lines.filter(new Contains("error"));

也可以使用lambda表達式來簡潔地實現函數接口,如例3-25所示。

例3-25:在Java中使用Java 8 地lambda表達式進行函數傳遞

RDD<String> errors=lines.filter(s->s.contains("error"));

(lambda表達式不懂!)

3.5 常見的轉化操作和行動操作

3.5.1 基本RDD

1.針對各個元素的轉化操作
最常用的是map()和filter()。

轉化操作map()接受一個函數,把這個函數用於RDD中的每個元素,將函數的返回結果作爲結果RDD中對應元素的值。輸入類型和返“`
類型可gg
不同。
轉化操作filter()則接收一個函數,並將RDD中滿足該函數的元素放入新的RDD中返回。

例3-26:Python版計算RDD中各值的平方

nums=sc.parallelize([1,2,3,4])
squared=nums.map(lambda x: x*x).collect()
for num in squard:
    print "%i " % (num)

flatMap()對每個輸入元素生成多個輸出元素。和map()類似,提供給flatMap()的函數被分別應用到了輸入RDD的每個元素上。不過返回的不是一個元素,而是一個返回值序列的迭代器。我們得到的是一個包含各個迭代器可訪問的所有元素的RDD。

例3-29:Python中的flatMap()將行數據切分爲單詞(並與map()進行對比)

這裏寫圖片描述
注:Spark的配置不光要在環境變量中加上bin目錄,還要對SPARK_LOCAL_IP配置爲127.0.0.1纔可以順利的在命令行中使用pyspark
這裏寫圖片描述
2. 僞集合操作
RDD雖不是嚴格意義上的集合,但也支持許多數學上的集合操作,比如合併和相交。這些操作都要求操作的RDD是相同數據類型的。
這裏寫圖片描述

  • distinct()去除重複元素。注意,distinct()操作開銷很大,因爲它需要將所有數據通過網絡進行混洗(shuffle)。
  • union(other)返回包含兩個RDD中所有元素的RDD。注意這裏並不會剔除重複數據。
  • intersection(other)只返回兩個RDD中都有的元素。注意這裏會去除掉所有的重複元素,該操作的性能很差,因爲需要通過網絡混洗數據來發現共有的元素。
  • subtract(other)和intersection一樣需要數據混洗,性能差。
  • cartesian(other)計算笛卡爾積,返回所有可能的(a,b)對。在我們希望考慮所有可能的組合的相似度時比較有用。注意RDD規模大時該操作開銷巨大。
    這裏寫圖片描述
    表3-2和表3-3總結了這些常見的RDD轉化操作。
    這裏寫圖片描述
    3. 行動操作
    最常見的行動操作reduce()。它接受一個函數作爲參數,該函數操作兩個RDD的元素類型的數據並返回一個同樣類型的新元素。

例3-32:Python中的reduce()——計算元素的總和

sum=rdd.reduce(lambda x,y:x+y)
#x指代的是返回值,y是對rdd中元素的遍歷

fold()和reduce()類似,接受一個與reduce()接受的函數簽名相同的函數,再加上一個“初始值”來作爲每個分區第一次調用時的結果。你所提供的初始值應當是你提供的操作的單位元素;也就是說,使用你的函數對這個初始值進行多次計算不會改變結果(例如+對應的0,*對應的1,或拼接操作對應的空列表)。如下例:

val l = List(1,2,3,4)
l.reduce((x, y) => x + y)
val l = List(1,2,3,4)
l.fold(0)((x, y) => x + y)

這個計算其實 0 + 1 + 2 + 3 + 4,而reduce()的計算是:1 + 2 + 3 + 4,沒有初始值,或者說rdd的第一個元素值是它的初始值。
fold()和reduce()都要求函數的返回值類型和所操作的RDD元素類型相同。如果需要返回不同類型的值,則需要使用map函數進行轉化。
aggregate()函數則把我們從返回值類型和所操作的RDD類型可以不同。與fold()類似,需要期待返回類型的初始值。然後通過一個函數把RDD中的元素合併起來放入累加器,考慮到每個節點是在本地進行累加(考慮分佈式的情況~),最終還需要提供第二個函數來講累加器兩兩合併。

例3-35:Python中的aggregate()

sumCount=nums.aggregate((0,0),
              (lambda acc,value:(acc[0]+value,acc[1]+1),
              (lambda acc1,acc2:(acc1[0]+acc2[0],acc1[1]+acc2[1]))))
return sumCount[0]/float(sumCount[1])

RDD的一些行動操作會以普通集合或者值的形式將RDD的部分或全部數據返回驅動器程序中。
把數據返回驅動器程序最簡單、最常見的操作是collect(),它返回整個RDD的內容。所以要求數據不會太大,能放入單臺機器的內存中。
take(n)返回RDD中的n個元素,並且嘗試只訪問儘量少的分區,因此該操作會得到一個不均衡的集合。
top()從RDD中獲取前幾個元素。(需要爲數據定義順序)
takeSample(withReplacement, num, seed)函數可以讓我們從數據中獲取採樣。
foreach()行動操作對RDD每個元素操作,而不需要把RDD發揮本地。
這裏寫圖片描述

3.5.2 在不同RDD類型間轉換

有些函數只能用於特定類型的RDD,比如mean()和variance()只能用在數值RDD上,而join()只能用在鍵值對RDD上。在Scala和Java中,這些函數沒有定義在標準的RDD類中,所以要訪問這些附加功能,必須要確保獲得了正確的專用RDD類。
1. Scala
Scala中,將RDD轉爲由特定函數的RDD是由隱式轉換來自動處理的。我們需要加上import org.apache.spark.SparkContext._來使用這些隱式轉換。
2. Java
Java中有兩個專門的類JavaDoubleRDD和JavaPairRDD,來處理特殊類型的RDD。
這裏寫圖片描述

例3-28:用Java創建DoubleRDD

JavaDoubleRDD result=rdd.mapToDouble(
    new DoubleFunction<Integer>(){
        public double call(Integer x){
            return (double) x*x;
        }
    });
    System.out.println(result.mean());

3. Python
Python中所有的函數都實現在基本的RDD類中。

3.6 持久化(緩存)

爲了避免多次計算同一個RDD,可以讓Spark對數據進行持久化。當我們讓Spark持久化存儲一個RDD時,計算出RDD的節點會分別保存他們所求出的分區數據。
默認情況下,我們會把數據以序列化的形式緩存在JVM的堆空間中。
這裏寫圖片描述
如果要緩存的數據太多,內存中放不下,Spark會自動利用最近最少使用的緩存策略把最老的分區從內存中移除。
RDD的unpersist()方法可以手動把持久化的RDD從緩存中移除。

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