萬字長文,帶你輕鬆學習 Spark

Data跳動發表於2022-05-21

大家好,我是大D。

今天給大家分享一篇 Spark 核心知識點的梳理,對知識點的講解秉承著能用圖解的就不照本宣科地陳述,力求精簡、通俗易懂。希望能為新手的入門學習掃清障礙,從基礎概念入手、再到原理深入,由淺入深地輕鬆掌握 Spark。

圖片

1、初識 Spark

Spark不僅能夠在記憶體中進行高效運算,還是一個大一統的軟體棧,可以適用於各種各樣原本需要多種不同的分散式平臺的場景。

背景

Spark作為一個用來快速實現大規模資料計算的通用分散式大資料計算引擎,是大資料開發工程師必備的一項技術棧。Spark相對Hadoop具有較大優勢,但Spark並不能完全替代Hadoop。實際上,Spark已經很好地融入了Hadoop家族,作為其中一員,主要用於替代Hadoop中的MapReduce計算模型。

Spark的優勢

Spark擁有Hadoop MapReduce所具備的優點,但不同的是,Hadoop每次經過job執行的中間結果都會儲存在HDFS上,而Spark執行job的中間過程資料可以直接儲存在記憶體中,無需讀寫到HDFS磁碟上。因為記憶體的讀寫速度與磁碟的讀寫速度不在一個數量級上,所以Spark利用記憶體中的資料可以更快地完成資料的計算處理。

此外,由於Spark在內部使用了彈性分散式資料集(Resilient Distributed Dataset,RDD),經過了資料模型的優化,即便在磁碟上進行分散式計算,其計算效能也是高於Hadoop MapReduce的。

Spark的特點

  • 計算速度快
    Spark將處理的每個任務都構造出一個有向無環圖(Directed Acyclic Graph,DAG)來執行,實現原理是基於RDD在記憶體中對資料進行迭代計算的,因此計算速度很快。官方資料表明,如果計算資料是從磁碟中讀取,Spark計算速度是Hadoop的10倍以上;如果計算資料是從記憶體中讀取,Spark計算速度則是Hadoop的100倍以上。
  • 易於使用
    Spark提供了80多個高階運算操作,支援豐富的運算元。開發人員只需呼叫Spark封裝好的API來實現即可,無需關注Spark的底層架構。
  • 通用大資料框架
    大資料處理的傳統方案需要維護多個平臺,比如,離線任務是放在Hadoop MapRedue上執行,實時流計算任務是放在Storm上執行。而Spark則提供了一站式整體解決方案,可以將即時查詢、離線計算、實時流計算等多種開發庫無縫組合使用。
  • 支援多種資源管理器
    Spark支援多種執行模式,比如Local、Standalone、YARN、Mesos、AWS等部署模式。使用者可以根據現有的大資料平臺靈活地選擇執行模式。
  • Spark生態圈豐富
    Spark不僅支援多種資源管理器排程job,也支援HDFS、HBase等多種持久化層讀取資料,來完成基於不同元件實現的應用程式計算。目前,Spark生態圈已經從大資料計算和資料探勘擴充套件到機器學習、NLP、語音識別等領域。

2、Spark 的模組組成

Spark 是包含多個緊密整合的元件,這些元件結合密切並且可以相互呼叫,這樣我們可以像在平常軟體專案中使用程式庫一樣,組合使用這些的元件。

Spark的各個組成模組如下:
在這裡插入圖片描述

  • Spark 基於 Spark Core 建立了 Spark SQL、Spark Streaming、MLlib、GraphX、SparkR等核心元件;
  • 基於這些不同元件又可以實現不同的計算任務;
  • 這些計算任務的執行模式有:本地模式、獨立模式、YARN、Mesos等;
  • Spark任務的計算可以從HDFS、HBase、Cassandra等多種資料來源中存取資料。

Spark Core

Spark Core實現了Spark基本的核心功能,如下:

  • 基礎設施
    SparkConf :用於定義Spark應用程式的配置資訊;
    SparkContext :為Spark應用程式的入口,隱藏了底層邏輯,開發人員只需使用其提供的API就可以完成應用程式的提交與執行;
    SparkRPC :Spark元件之間的網路通訊依賴於基於Netty實現的Spark RPC框架;
    SparkEnv :為Spark的執行環境,其內部封裝了很多Spark執行所需要的基礎環境元件;
    ListenerBus :為事件匯流排,主要用於SparkContext內部各元件之間的事件互動;
    MetricsSystem :為度量系統,用於整個Spark叢集中各個元件狀態的監控;

  • 儲存系統
    用於管理Spark執行過程中依賴的資料的儲存方式和儲存位置,Spark的儲存系統首先考慮在各個節點的記憶體中儲存資料,當記憶體不足時會將資料儲存到磁碟上,並且記憶體儲存空間和執行儲存空間之間的邊界也可以靈活控制。

  • 排程系統
    DAGScheduler :負責建立job、將DAG中的RDD劃分到不同Stage中、為Stage建立對應的Task、批量提交Task等;
    TaskScheduler :負責按照FIFO和FAIR等排程演算法對Task進行批量排程;

  • 計算引擎
    主要由記憶體管理器、工作管理員、Task、Shuffle管理器等組成。

Spark SQL

Spark SQL 是 Spark 用來操作結構化資料的程式包,支援使用 SQL 或者 Hive SQL 或者與傳統的RDD程式設計的資料操作結合的方式來查詢資料,使得分散式資料的處理變得更加簡單。

Spark Streaming

Spark Streaming 提供了對實時資料進行流式計算的API,支援Kafka、Flume、TCP等多種流式資料來源。此外,還提供了基於時間視窗的批量流操作,用於對一定時間週期內的流資料執行批量處理。

MLlib

Spark MLlib 作為一個提供常見機器學習(ML)功能的程式庫,包括分類、迴歸、聚類等多種機器學習演算法的實現,其簡單易用的 API 介面降低了機器學習的門檻。

GraphX

GraphX 用於分散式圖計算,比如可以用來操作社交網路的朋友關係圖,能夠通過其提供的 API 快速解決圖計算中的常見問題。

SparkR

SparkR 是一個R語言包,提供了輕量級的基於 R 語言使用 Spark 的方式,使得基於 R 語言能夠更方便地處理大規模的資料集。

3、Spark 的執行原理

Spark 是包含多個緊密整合的元件,這些元件結合密切並且可以相互呼叫,這樣我們可以像在平常軟體專案中使用程式庫一樣,組合使用這些的元件。

Spark的執行模式

就底層而言,Spark 設計為可以高效地在一個到數千個計算節點之間伸縮計算。為了實現這樣的要求,同時獲得最大靈活性,Spark支援在各種叢集管理器上執行。

Spark 的執行模式主要有:

  • Local 模式 :學習測試使用,分為 Local 單執行緒和 Local-Cluster 多執行緒兩種方式;
  • Standalone 模式 :學習測試使用,在 Spark 自己的資源排程管理框架上執行;
  • ON YARN :生產環境使用,在 YARN 資源管理器框架上執行,由 YARN 負責資源管理,Spark 負責任務排程和計算;
  • ON Mesos :生產環境使用,在 Mesos 資源管理器框架上執行,由 Mesos 負責資源管理,Spark 負責任務排程和計算;
  • On Cloud :執行在 AWS、阿里雲、華為雲等環境。

Spark的叢集架構

Spark 的叢集架構主要由 Cluster Manager(資源管理器)、Worker (工作節點)、Executor(執行器)、Driver(驅動器)、Application(應用程式) 5部分組成,如下圖:
在這裡插入圖片描述

  • Cluster Manager :Spark 叢集管理器,主要用於整個叢集資源的管理和分配,有多種部署和執行模式;
  • Worker :Spark 的工作節點,用於執行提交的任務;
  • Executor :真正執行計算任務的一個程式,負責 Task 的執行並且將執行的結果資料儲存到記憶體或磁碟上;
  • Driver :Application 的驅動程式,可以理解為驅動程式執行中的 main() 函式,Driver 在執行過程中會建立 Spark Context;
  • Application :基於 Spark API 編寫的應用程式,包括實現 Driver 功能的程式碼和在叢集中多個節點上執行的 Executor 程式碼。

Worker 的工作職責

  1. 通過序號產生器制向 Cluster Manager彙報自身的 CPU 和記憶體等資源使用資訊;
  2. 在 Master 的指示下,建立並啟動 Executor(真正的計算單元);
  3. 將資源和任務進一步分配給 Executor 並執行;
  4. 同步資源資訊和 Executor 狀態資訊給 Cluster Manager。

Driver 的工作職責

Application 通過 Driver 與 Cluster Manager 和 Executor 進行通訊。

  1. 執行 Application 的 main() 函式;
  2. 建立 SparkContext;
  3. 劃分 RDD 並生成 DAG;
  4. 構建 Job 並將每個 Job 都拆分為多個 Stage,每個 Stage 由多個 Task 構成,也被稱為 Task Set;
  5. 與 Spark 中的其他元件進行資源協調;
  6. 生成併傳送 Task 到 Executor。

4、RDD 概念及核心結構

本節將介紹 Spark 中一個抽象的概念——RDD,要學習 Spark 就必須對 RDD 有一個清晰的認知,RDD是 Spark 中最基本的資料抽象,代表一個不可變、可分割槽、元素可平行計算的集合。

RDD的概念

RRD全稱叫做彈性分散式資料集(Resilient Distributed Dataset),從它的名字中可以拆解出三個概念。

  • Resilient :彈性的,包括儲存和計算兩個方面。RDD 中的資料可以儲存在記憶體中,也可以儲存在磁碟中。RDD 具有自動容錯的特點,可以根據血緣重建丟失或者計算失敗的資料;
  • Distributed :RDD 的元素是分散式儲存的,並且用於分散式計算;
  • Dataset : 本質上還是一個存放元素的資料集。

RDD的特點

RDD 具有自動容錯、位置感知性排程以及可伸縮等特點,並且允許使用者在執行多個查詢時,顯式地將資料集快取在記憶體中,後續查詢能夠重用該資料集,這極大地提升了查詢效率。

下面是原始碼中對RDD類介紹的註釋:

Internally, each RDD is characterized by five main properties:

 - A list of partitions
 - A function for computing each split
 - A list of dependencies on other RDDs
 - Optionally, a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned)
 - Optionally, a list of preferred locations to compute each split on (e.g. block locations for an HDFS file)
  • 一組分割槽(Partition)的列表,其中分割槽也就是RDD的基本組成單位;
  • 一個函式會被作用到每個分割槽上,RDD 的計算是以分割槽為單位的;
  • 一個 RDD 會依賴其他多個 RDD,它們之間具有依賴關係;
  • 可選,對於K-V型的RDD會有一個分割槽函式,控制key分到哪個reduce;
  • 一個儲存每個分割槽優先位置的列表。

從原始碼對 RDD 的定義中,可以看出 RDD 不僅能表示存有多個元素的資料集,而且還能通過依賴關係推算出這個資料集是從哪來的,在哪裡計算更合適。

RDD的核心結構

在學習 RDD 轉換操作運算元之前,根據 RDD 的特點對 RDD 中的核心結構進行一下梳理,這樣對 Spark 的執行原理會有一個更深的理解。

  1. 分割槽(Partition)
    RDD 內部的資料集在邏輯上和物理上都被劃分為了多個分割槽(Partition),每一個分割槽中的資料都可以在單獨的任務中被執行,這樣分割槽數量就決定了計算的並行度。如果在計算中沒有指定 RDD 中的分割槽數,那麼 Spark 預設的分割槽數就是為 Applicaton 執行分配到的 CPU 核數。

  2. 分割槽函式(Partitioner)
    分割槽函式不但決定了 RDD 本身的分割槽數量,也決定了其父 RDD (即上一個衍生它的RDD)Shuffle 輸出時的分割槽數量。Spark 實現了基於 HashPartitioner 的和基於 RangePartitoner 的兩種分割槽函式。
    需要特別說明的是,只有對 K-V 型別的 RDD 才會有分割槽函式。

  3. 依賴關係
    RDD 表示只讀的分割槽的資料集,如果對 RDD 中的資料進行改動,就只能通過轉化操作,由一個或多個 RDD 計算得到一個新的 RDD,並且這些 RDD 之間存在著前後依賴關係,前面的稱為父 RDD,後面的稱為子 RDD。RDD 之間的依賴可分為寬依賴和窄依賴。
    在這裡插入圖片描述
    當計算過程中出現異常情況導致部分分割槽資料丟失時,Spark 可以通過這種依賴關係從父 RDD 中重新計算丟失的分割槽資料,而不需要對 RDD 中的所有分割槽全部重新計算。

  4. Stage
    當 Spark 執行作業時,會根據 RDD 之間的依賴關係,按照寬窄依賴生成一個最優的執行計劃。如果 RDD 之間為窄依賴,則會被劃到一個 Stage 中;如果 RDD 之間為寬依賴,則會被劃分到不同的 Stage 中,這樣做的原因就是每個 Stage 內的 RDD 都儘可能在各個節點上並行地被執行,以提高執行效率。
    在這裡插入圖片描述

  5. 優先列表(PreferredLocation)
    用於儲存每個分割槽優先位置的列表,對於每個 HDFS 檔案來說,就是儲存下每個分割槽所在 block 的位置。按照“移動資料不如移動計算”的理念,Spark 在執行任務排程時會優先選擇有儲存資料的 Worker 節點進行任務運算。

  6. CheckPoint
    CheckPoint 是 Spark 提供的一種基於快照的快取機制,如果在任務運算中,多次使用同一個 RDD,可以將這個 RDD 進行快取處理。這樣,該 RDD 只有在第一次計算時會根據依賴關係得到分割槽資料,在後續使用到該 RDD 時,直接從快取處取而不是重新進行計算。如下圖,對 RDD-1 做快照快取處理,那麼當RDD-n 在用到 RDD-1 資料時,無需重新計算 RDD-1,而是直接從快取處取數重算。
    在這裡插入圖片描述
    此外,Spark 還提供了另一種快取機制 Cache,其中的資料是由 Executor 管理的,當 Executor 消失時,Cache 快取的資料也將會消失。而 CheckPoint 是將資料儲存到磁碟或者 HDFS 中的,當任務執行錯誤時,Job 會從 CheckPoint 快取位置取數繼續計算。

5、Spark RDD 的寬窄依賴關係

RDD的依賴關係

在 Spark 中,RDD 分割槽的資料不支援修改,是隻讀的。如果想更新 RDD 分割槽中的資料,那麼只能對原有 RDD 進行轉化操作,也就是在原來 RDD 基礎上建立一個新的RDD。

那麼,在整個任務的運算過程中,RDD 的每次轉換都會生成一個新的 RDD,因此 RDD 們之間會產生前後依賴的關係。

說白了,就是相當於將對原始 RDD 分割槽資料的整個運算進行了拆解,當運算中出現異常情況導致分割槽資料丟失時,Spark 可以還通過依賴關係從上一個 RDD 中重新計算丟失的資料,而不是對最開始的 RDD 分割槽資料重新進行計算。

在 RDD 的依賴關係中,我們將上一個 RDD 稱為父RDD,下一個 RDD 稱為子RDD。

如何區分寬窄依賴

RDD 們之間的依賴關係,也分為寬依賴和窄依賴。

在這裡插入圖片描述

  • 寬依賴 :父 RDD 中每個分割槽的資料都可以被子 RDD 的多個分割槽使用(涉及到了shuffle);
  • 窄依賴 :父 RDD 中每個分割槽的資料最多隻能被子 RDD 的一個分割槽使用。

說白了,就是看兩個 RDD 的分割槽之間,是不是一對一的關係,若是則為窄依賴,反之則為寬依賴。

有個形象的比喻,如果父 RDD 中的一個分割槽有多個孩子(被多個分割槽依賴),也就是超生了,就為寬依賴;反之,如果只有一個孩子(只被一個分割槽依賴),那麼就為窄依賴。

常見的寬窄依賴運算元:

  • 寬依賴的運算元 :groupByKey、partitionBy、join(非hash-partitioned);
    在這裡插入圖片描述
  • 窄依賴的運算元 :map、filter、union、join(hash-partitioned)、mapPartitions、mapValues;在這裡插入圖片描述

為何設計要寬窄依賴

從上面的分析,不難看出,在窄依賴中子 RDD 的每個分割槽資料的生成操作都是可以並行執行的,而在寬依賴中需要所有父 RDD 的 Shuffle 結果完成後再被執行。

在 Spark 執行作業時,會按照 Stage 劃分不同的 RDD,生成一個完整的最優執行計劃,使每個 Stage 內的 RDD 都儘可能在各個節點上並行地被執行。

如下圖,Stage3 包含 Stage1 和 Stage2,其中, Stage1 為窄依賴,Stage2 為寬依賴。
在這裡插入圖片描述
因此,劃分寬窄依賴也是 Spark 優化執行計劃的一個重要步驟,寬依賴是劃分執行計劃中 Stage 的依據,對於寬依賴必須要等到上一個 Stage 計算完成之後才能計算下一個階段。

6、Spark RDD 的轉換操作與行動操作

1. RDD 的建立

Spark 提供了兩種建立 RDD 的方式:對一個集合進行並行化操作和利用外部資料集生成 RDD 。

對一個集合進行並行化操作

Spark 建立 RDD 最簡單的方式就是把已經存在的集合傳給 parallelize() 方法,不過,這種方式在開發中並不常用,畢竟需要將整個的資料集先放到一個節點記憶體中。

利用 parallelize() 方法將已經存在的一個集合轉換為 RDD,集合中的資料也會被複制到 RDD 中並參與平行計算。

val lines = sc.parallelize(Arrays.asList(1,2,3,4,5),n)

其中,n 為並行集合的分割槽數量,Spark 將為叢集的每個分割槽都執行一個任務。該引數設定太小不能很好地利用 CPU,設定太大又會導致任務阻塞等待,一般 Spark 會嘗試根據叢集的 CPU 核數自動設定分割槽數量。

利用外部資料集生成 RDD

在開發中,Spark 建立 RDD 最常用的一個方式就是從 Hadoop 或者其他外部儲存系統建立 RDD,包括本地檔案系統、HDFS、Cassandra、HBase、S3 等。

通過 SparkContext 的 textFile() 方法來讀取文字檔案建立 RDD 的程式碼,如下:

val lines = sc.textFile("../temp.txt")

其中,textFile() 方法的 url 引數可以是本地檔案或路徑、HDFS路徑等,Spark 會讀取該路徑下所有的檔案,並將其作為資料來源載入到記憶體生成對應的 RDD。

當然, RDD 也可以在現有 RDD 的基礎上經過運算元轉換生成新的 RDD,這是接下來要講的RDD 運算元轉換的內容,Spark RDD 支援兩種型別的操作:轉換操作(Transformation) 和行動操作(Action)。

2. RDD 的轉換操作

轉換操作是指從現有 RDD 的基礎上建立新的 RDD,是返回一個新 RDD 的操作。轉換操作是惰性求值的,也就是不會立即觸發執行實際的轉換,而是先記錄 RDD 之間的轉換關係,只有當觸發行動操作時才會真正地執行轉換操作,並返回計算結果。

舉個例子,有一個日誌檔案 log.txt,需要從裡面若干條資訊中,篩選出其中錯誤的報警資訊,我們可以用轉化操作 filter() 即可完成,程式碼如下:

val inputRDD = sc.textFile("log.txt")
val errorsRDD = inputRDD.filter(line => line.contains("error"))

其中,textFile() 方法定義了名為 inputRDD 的RDD,但是此時 log.txt 檔案並沒有載入到記憶體中,僅僅是指向檔案的位置。然後通過 filter() 方法進行篩選定義了名為 errorsRDD 的轉換RDD,同樣也屬於惰性計算,並沒有立即執行。

3. RDD 的行動操作

瞭解瞭如何通過轉換操作從已有的 RDD 中建立一個新的 RDD,但有時我們希望可以對資料集進行實際的計算。行動操作就是接下來要講的第二種 RDD 操作,它會強制執行那些求值必須用到的 RDD 的轉換操作,並將最終的計算結果返回給 Driver 程式,或者寫入到外部儲存系統中。

繼續剛才的栗子,我們需要將上一步統計出來的報警資訊的數量列印出來,我們可以藉助count() 方法進行計數。

val countRDD = errorsRDD.count()

其中,count() 可以觸發實際的計算,強制執行前面步驟中的轉換操作。實際上,Spark RDD 會將 RDD 計算分解到不同的 Stage 並在不同的節點上進行運算,每個節點都會執行 count() 結果,所有運算完成之後會聚合一個結果返回給 Driver 程式。

如果分不清楚給定的一個 RDD 操作方法是屬於轉換操作還是行動操作,去看下它的返回型別,轉換操作返回的是 RDD 型別,而行動操作則返回的是其他的資料型別。

4. 惰性求值

前面,我們多次提到轉換操作都是惰性求值,這也就意味著呼叫的轉換操作(textFile、filter等)時,並不會立即去執行,而是 Spark 會記錄下所要求執行的操作的相關資訊。

因此,我們對 RDD 的理解應該更深一步,不僅要它看作是一個存放分散式資料的資料集,而且也可以把它當作是通過轉換操作構建出來的、記錄如何計算資料的指令列表。

惰性操作避免了所有操作都進行一遍 RDD 運算,它可以將很多操作合併在一起,來減少計算資料的步驟,提高 Spark 的運算效率。

7、Spark RDD 中常用的操作運算元

1. 向Spark 傳遞函式

Spark API 依賴 Driver 程式中的傳遞函式完成在叢集上執行 RDD 轉換並完成資料計算。在 Java API 中,函式所在的類需要實現 org.apache.spark.api.java.function 包中的介面。

Spark 提供了 lambda 表示式和 自定義 Function 類兩種建立函式的方式。前者語法簡潔、方便使用;後者可以在一些複雜應用場景中自定義需要的 Function 功能。

舉個例子,求 RDD 中資料的平方,並只保留不為0的資料。
可以用 lambda 表示式簡明地定義 Function 實現,程式碼如下:

val input = sc.parallelize(List(-2,-1,0,1,2))
val rdd1 = input.map(x => x * x)
val rdd2 = rdd1.filter(x => x != 0 )

首先用 map() 對 RDD 中所有的資料進行求平方,然後用到 filter() 來篩選不為0的資料。

其中,map() 和 filter() 就是我們最常用的轉換運算元,map() 接收了 一個 lambda 表示式定義的函式,並把這個函式運用到 input 中的每一個元素,最後把函式計算後的返回結果作為結果 rdd1 中對應元素的值。

同樣, filter() 也接收了一個 lambda 表示式定義的函式,並將 rdd1 中滿足該函式的資料放入到了新的 rdd2 中返回。
在這裡插入圖片描述
Spark 提供了很豐富的處理 RDD 資料的操作運算元,比如使用 distinct() 還可以繼續對 rdd2 進行去重處理。

如果需要對 RDD 中每一個元素處理後生成多個輸出,也有相應的運算元,比如 flatMap(),它和 map() 類似,也是將輸入函式應用到 RDD 中的每個元素,不過返回的不是一個元素了,而是一個返回值序列的迭代器。

最終得到的輸出是一個包含各個迭代器可訪問的所有元素的 RDD,flatMap() 最經典的一個用法就是把輸入的一行字串切分為一個個的單詞。

舉個例子,將行資料切分成單詞,對比下 map() 與 flat() 的不同。

val lines = sc.parallelize(List("hello spark","hi,flink"))
val rdd1 = lines.map(line => line.split(","))
val rdd2 = lines.flatMap(line => line.split(","))

在這裡插入圖片描述
可以看到,把 lines 中的每一個 line,使用所提供的函式執行一遍,map() 輸出的 rdd1 中仍然只有兩個元素;而 flatMap() 輸出的 rdd2 則是將原 RDD 中的資料“拍扁”了,這樣就得到了一個由各列表中元素組成的 RDD,而不是一個由列表組成的 RDD。

2. RDD 的轉換運算元

Spark 中的轉換運算元主要用於 RDD 之間的轉化和資料處理,常見的轉換運算元具體如下:

轉換運算元含義
map(func) 返回每一個元素經過 func 方法處理後生成的新元素所組成的資料集合
filter(func) 返回一個通過 func 方法計算返回 true 的元素所組成的資料集合
flatMap(func) 與 map 操作類似,但是每一個輸入項都能被對映為 0 個或者多個輸出項
mapPartitions(func) 與 map 操作類似,但是 mapPartitions 單獨執行在 RDD 的一個分割槽上
mapPartitionsWithIndex(func) 與 mapPartitions 操作類似,但是該操作提供一個整數值代表分割槽的下表
union(otherDataset) 對兩個資料集執行合併操作
intersection(otherDataset) 對兩個資料集執行求交集運算
distinct([numTasks]) 對資料集執行去重操作
groupByKey([numTasks]) 返回一個根據 Key 分組的資料集
reduceByKey(func,[numTasks]) 返回一個在不同 Key 上進行了聚合 Value 的新的 <Key, Value> 資料集,聚合方式由 func 方法指定
sortByKey([ascending],[numTasks]) 返回排序後的鍵值對
join(otherDataset,[numTasks]) 按照 Key 將源資料集合與另一資料集合進行 join 操作,<Key, Value1> 和 <Key, Value2> 的 join 結果就是 <Key, <Value1,Value2>>
repatition(numPartitions) 通過修改 Partiton 的個數對 RDD 中的資料重新進行分割槽平衡

3. RDD 的行動運算元

Spark 中行動運算元主要用於對分散式環境中 RDD 的轉化操作結果進行統一地執行處理,比如結果收集、資料儲存等,常用的行動運算元具體如下:

行動運算元含義
reduce(func) 使用一個 func 方法來聚合一個資料集
collect() 以陣列的形式返回在驅動器上的資料集的所有元素
collectByKey() 按照資料集中的 Key 進行分組,計算各個 Key 對應的個數
foreach(func) 在資料集的每個元素上都遍歷執行 func 方法
first() 返回資料集行的第一個元素
take(n) 以陣列的形式返回資料集上的前 n 個元素
takeOrdered(n,[ordering]) 返回 RDD 排序後的前 n 個元素。排序方式要麼使用原生的排序方式,要麼使用自定義的比較器排序
saveAsTextFile(path) 將資料集中的元素寫成一個或多個文字檔案。引數就是檔案路徑,可以寫在本地檔案系統、HDFS,或者其他 Hadoop 支援的檔案系統
saveAsSequenceFile(path) 將 RDD 中的元素寫成 Hadoop SequenceFile 儲存到本地檔案系統、HDFS,或者其他 Hadoop 支援的檔案系統,並且 RDD 中可用的鍵值對必須實現 Hadoop 的 Writable 介面
saveAsObjectFile(path) 使用 Java 序列化方式將 RDD 中的元素進行序列化並儲存到檔案系統中,可以使用 SparkContext.objectFile() 方法來載入該資料

8、Spark 的共享變數之累加器和廣播變數

本節將介紹下 Spark 程式設計中兩種型別的共享變數:累加器和廣播變數。
簡單說,累加器是用來對資訊進行聚合的,而廣播變數則是用來高效分發較大物件的。
1. 閉包的概念

在講共享變數之前,我們先了解下啥是閉包,程式碼如下。

var n = 1
val func = (i:Int) => i + n

函式 func 中有兩個變數 n 和 i ,其中 i 為該函式的形式引數,也就是入參,在 func 函式被呼叫時, i 會被賦予一個新的值,我們稱之為繫結變數(bound variable)。而 n 則是定義在了函式 func 外面的,該函式並沒有賦予其任何值,我們稱之為自由變數(free variable)。

像 func 函式這樣,返回結果依賴於宣告在函式外部的一個或多個變數,將這些自由變數捕獲並構成的封閉函式,我們稱之為“閉包”。

先看一個累加求和的栗子,如果在叢集模式下執行以下程式碼,會發現結果並非我們所期待的累計求和。

var sum = 0
val arr = Array(1,2,3,4,5)
sc.parallelize(arr).foreach(x => sum + x)
println(sum)

sum 的結果為0,導致這個結果的原因就是存在閉包。

在叢集中 Spark 會將對 RDD 的操作處理分解為 Tasks ,每個 Task 由 Executor 執行。而在執行之前,Spark會計算 task 的閉包(也就是 foreach() )。閉包會被序列化併傳送給每個 Executor,但是傳送給 Executor 的是副本,所以在 Driver 上輸出的依然是 sum 本身。

在這裡插入圖片描述

如果想對 sum 變數進行更新,則就要用到接下來我們要講的累加器。

2. 累加器的原理

累加器是對資訊進行聚合的,通常在向 Spark 傳遞函式時,比如使用 map() 或者 filter() 傳條件時,可以使用 Driver 中定義的變數,但是叢集中執行的每個任務都會得到這些變數的一份新的副本,然而,正如前面所述,更新這些副本的值,並不會影響到 Driver 中對應的變數。

累加器則突破了這個限制,可以將工作節點中的值聚合到 Driver 中。它的一個典型用途就是對作業執行過程中的特定事件進行計數。

舉個例子,給了一個日誌記錄,需要統計這個檔案中有多少空行。

val sc = new SparkContext(...)
val logs = sc.textFile(...)
val blanklines = sc.accumulator(0)
val callSigns = logs.flatMap(line => {
	if(line == ""){
		blanklines += 1
	}
	line.split("")
})
callSigns.count()
println("日誌中的空行數為:" + blanklines.value)

總結下累加器的使用,首先 Driver 呼叫了 SparkContext.accumulator(initialValue) 方法,建立一個名為 blanklines 且初始值為0的累加器。然後在遇到空行時,Spark 閉包裡的執行器程式碼就會對其 +1 。執行完成之後,Driver 可以呼叫累加器的 value 屬性來訪問累加器的值。

需要說明的是,只有在行動運算元 count() 執行之後,才可以 println 出正確的值,因為我們之前講過 flatMap() 是惰性計算的,只有遇到行動操作之後才會出發強制執行運算進行求值。

另外,工作節點上的任務是不可以訪問累加器的值,在這些任務看來,累加器是一個只寫的變數。

對於累加器的使用,不僅可以進行資料的 sum 加法,也可以跟蹤資料的最大值 max、最小值 min等。

3. 廣播變數的原理

前面說了,Spark 會自動把閉包中所有引用到的自由變數傳送到工作節點上,那麼每個 Task 的閉包都會持有自由變數的副本。如果自由變數的內容很大且 Task 很多的情況下,為每個 Task 分發這樣的自由變數的代價將會巨大,必然會對網路 IO 造成壓力。

廣播變數則突破了這個限制,不是把變數副本發給所有的 Task ,而是將其分發給所有的工作節點一次,這樣節點上的 Task 可以共享一個變數副本。

Spark 使用的是一種高效的類似 BitTorrent 的通訊機制,可以降低通訊成本。廣播的資料只會被髮動各個節點一次,除了 Driver 可以修改,其他節點都是隻讀,並且廣播資料是以序列化形式快取在系統中的,當 Task 需要資料時對其反序列化操作即可。

在使用中,Spark 可以通過呼叫 SparkContext.broadcast(v) 建立廣播變數,並通過呼叫 value 來訪問其值,舉慄程式碼如下:

val broadcastVar = sc.broadcast(Array(1,2,3))
broadcastVar.value

相關文章