1. 簡介
Spark 的身世
Spark 是一個通用的并行計算框架,由加州伯克利大學(UC Berkeley)的 AMP 實驗室開發于 2009 年,并于 2010 年開源,2013 年成長為 Apache 旗下在大數據領域最活躍的開源項目之一。
目前 Spark 的版本已經更新到了 2.4.5,并且預上線了 3.0 版本,相信未來會有更精彩的地方值得我們期待。
Spark 編程模型示意圖
Spark 特性
快速:采用先進的 DAG 調度程序,查詢優化器和物理執行引擎,實現了批處理和流數據處理的高性能,比 Hadoop 的 Map-Reduce 計算速度提升了很多倍。
易使用:支持多種編程語言,比如:Java、Scala、Sql、Python、R。提供了 80 多個高級操作符,可以輕松構建并行應用程序,并且可以在 Scala、Python、R 和 SQL shell 中交互式地使用它。
通用性:提供了一套完善的生態體系,支持交互式查詢,流處理,批處理,機器學習算法和圖形處理,可以在同一個應用程序中無縫的組合使用他們。
到處運行:支持單機、YARN、Mesos 等多種部署方式,并且支持豐富的數據源和文件格式的讀取。
Spark 針對 Hadoop-MR 做的改進
- 減少了磁盤的 I/O
Spark 將 map 端的中間輸出和結果存儲在內存中,避免了 reduce 端在拉取 map 端數據的時候造成大量的磁盤 I/O;并且 Spark 將應用程序上傳的資源文件緩沖到了 Driver 端本地文件服務的內存中,Executor 在執行任務時直接從 Driver 的內存中讀取,也節省了一部分磁盤的 I/O。
- 增加了并行度
由于將中間結果寫到磁盤與從磁盤讀取中間結果屬于不同的環節,Hadoop 將它們簡單地通過串行執行銜接起來。Spark 把不同的環節抽象為 Stage,允許多個 Stage 既可以串行執行,又可以并行執行。
- 避免重新計算
當某個 Stage 中的一個 Task 失敗之后,Spark 會重新對這個 Stage 進行調度,并且會過濾掉已經執行成功的 Task,避免造成重復的計算和資源的浪費。
- 可選的 Shuffle 排序
MR 在 Shuffle 的時候有著固定的排序操作,但是 Spark 卻可以根據不用的場景選擇在 map 端排序還是在 reduce 端排序。
- 更加靈活的內存管理
Spark 將內|存劃分為堆內存儲內存、堆內執行內存、堆外存儲內存和堆外執行內存。Spark 即提供了執行內存和存儲內存之間固定邊界的實現,也提供了執行內存和存儲內存之間"軟"邊界的實現。Spark 默認使用第二種實現方式,無論存儲或是執行內存,當哪一方的資源不足時,都可以借用另一方的資源,從而最大限度地提高了資源的利用率。
2. 運行時組件
spark-cluster-overview:
Driver
Spark 任務運行調度的核心,負責創建 SparkContext 上下文環境,內部包含 DAGScheduler、TaskScheduler、SchedulerBackend 等重要組件。
負責向 Master 注冊當前應用程序并申請計算資源,注冊成功后 Master 會為其分配申請的資源。
負責切分任務,并將 Task 分發到不同的 Executor 上執行。
與 Executor 保持通信,任務運行成功或是失敗都會向 Driver 進行匯報,當任務執行完成之后,Driver 會關閉 SparkContext。
Master
Master 是在 local 和 standalone 模式部署下 Spark 集群的一名重要成員,它負責管理整個集群中所有資源的分配,接收 Worker、Driver、Application 的注冊,Master 會獲得所有已經注冊的 Worker 節點的資源信息 (包括:ID、host、port、cpu、memory 等等),用于后續的資源分配。
為了保證集群的高可用,可以同時啟動多個 Master,但是這些 Master 只有一個是 Active 狀態的,其余的全部為 Standby 狀態。Master 實現了 LeaderElectable 接口,當有 Master 發生故障時,會通過 electedLeader() 方法選舉新的 Master 領導。
Master 會按照一定的資源調度策略將 Worker 上的資源分配給 Driver 或者 Application。
Master 給 Driver 分配了資源以后,會向 Worker 發送啟動 Driver 的命令,Worker 接收到命令后啟動 Driver。
Master 根據 Application 申請的資源,選擇合適的 Worker 進行資源分配,然后會向 Worker 發送啟動 Executor 的命令,Worker 接到命令后啟動 Executor。
Master 會和 Worker 保持心跳連接,一是檢查 Worker 的存活狀態;二是當 Master 出現故障后選舉了新的 Master,新的 Master 中并沒有保存 Worker 的信息,當 Worker 向 Master 發送心跳的時候,Master 會通知 Worker 重新向新的 Master 進行注冊。
Worker
組成 Spark 集群的成員之一,啟動之后會主動向 Master 進行注冊,負責向 Master 匯報自身所管理的資源信息,當接到 Master 的命令之后,啟動相應的 Driver 或者 Executor。
因為 Worker 啟動之后會主動的向 Master 進行注冊,因此可以動態的擴展 Worker 節點。
Worker 向 Master 注冊成功之后,會以 HEART-BEAT_MILLIS 作為間隔向 Worker 自身發送 SendHeartbeat 消息的定時任務,Worker 接收到 SendHeartbeat 消息后,將向 Master 發送 Heartbeat 消息,Master 也會以 WORKER_TIME-OUT_MS 為時間間隔定時向 Master 自身發送 CheckForWorkerTimeOut 消息,用來檢查連接超時的 Worker。
如果 Master 發現了連接超時的 Worker,但是 Worker 的狀態并不是 DEAD,此時 Master 會將 Worker 的信息從 idToWorker 中移除,但是 workers 中任然保留著 Worker 的信息,并且會再次向 Worker 發出重新注冊的信息。
如果 Master 發現了連接超時的 Worker,并且 Worker 的狀態并是 DEAD,那么 Worker 的信息將會從 workers 中被移除。
Executor
負責執行 Spark 任務的容器,在 Worker 上啟動,通過 launchTask() 方法創建 TaskRunner 對象來執行任務,初始化完成后會和 Driver 建立通信,并將任務最后的執行結果發送給 Driver。
3. 編程模型
Job 提交到執行過程(來源:https://blog.csdn.net/pelick/article/details/44495611)
SparkContext
SparkContext 是 Spark 各種功能的主要入口點,表示和 Spark 集群的一種連接,它可以為這個集群創建 RDD、累加器、廣播變量等等。
一個 JVM 環境下只能有一個活躍的 SparkContext,你可以通過調用 stop() 方法,停掉活躍的 SparkContext 來創建新的,這種限定在將來可能會被廢棄。
重要組成:SparkEnv、SparkUI、LiveListenerBus、SparkStatusTracker、ConsoleProgressBar、JobProgressListener、TaskScheduler、DAGScheduler、HeartBeatReceiver、ContextCleaner、ShutdownHookManager。
SparkConf
Spark 支持各種各樣的配置參數來調整任務的運行,SparkConf 是統一管理這些配置的一個配置類,所有的配置項都由 SparkConf 來進行管理。
所有的配置都保存在一個
ConcurrentHashMap[String,String]
中,因此配置 SparkConf 時,無論是 key 還是 value 都是 String 類型的。通過調用 set(key: String, value: String) 方法來給 Spark 設置參數,類似 setMaster() 的方法,內部也是調用了 set() 方法進行參數配置。
在創建 SparkConf 的時候,可以指定一個 Boolean 類型的構造器屬性 loadDefaults,當設置為 true 時,會從系統屬性中加載以 spark. 字符串為前綴的 key 值,并調用 set() 方法進行賦值。
由于 SparkConf 繼承了 Cloneable 特質并實現了 clone 方法,雖然 ConcurrentHashMap 是線程安全的,但是在高并發的情況下,鎖機制可能會帶來性能上的問題,因此當多個組件共用 SparkConf 的時候,可以通過 clone 方法來創建出多個 SparkConf。
SparkEnv
SparkEnv 是 Spark 運行時的環境對象,其中包含了 Executor 執行任務時需要的各種對象,例如 RpcEnv、ShuffleManager、BroadcastManager、BlockManager 等,用來管理節點之間的通信、數據的 shuffle、內存空間、數據的計算存儲等,所有的 Executor 都持有自己的 SparkEnv 環境對象。此外,在 local 模式下,Driver 會創建 Executor,所以在 Driver 和 CoarseGrainedExecutorBackend 進行中都有 SparkEnv 的存在。
SparkEnv 不是為了提供給外部使用的,有可能會在將來的版本變為私有。
RDD
RDD 是 Spark 的核心數據結構,全稱是彈性分布式數據集( ResilientDistributed Dataset ),其本質是一種分布式的內存抽象,表示一個只讀的數據分區(Partition)集合。
一個 RDD 通常只能通過其他的 RDD 轉換而創建。RDD 定義了各種豐富的轉換操作(如 map、join 和 filter 等),通過這些轉換操作,新的 RDD 包含了如何從其他 RDD 衍生所必需的信息,這些信息構成了 RDD 之間的依賴關系( Dependency )。依賴具體分為兩種,一種是窄依賴,RDD 之間分區是一一對應的;另一種是寬依賴,下游 RDD 的每個分區與上游 RDD(也稱之為父 RDD)的每個分區都有關,是多對多的關系。窄依賴中的所有轉換操作可以通過類似管道(Pipeline)的方式全部執行,寬依賴意味著數據需要在不同節點之間 Shuffle 傳輸。
RDD 計算的時候會通過一個 compute 函數得到每個分區的數據。若 RDD 是通過已有的文件系統構建的,則 compute 函數讀取指定文件系統中的數據;如果 RDD 是通過其他 RDD 轉換而來的,則 compute 函數執行轉換邏輯,將其他 RDD 的數據進行轉換。RDD 的操作算子包括兩類,一類是 transformation ,用來將 RDD 進行轉換,構建 RDD 的依賴關系;另一類稱為 action,用來觸發 RDD 的計算,得到 RDD 的相關計算結果或將 RDD 保存到文件系統中。
在 Spark 中,RDD 可以創建為對象,通過對象上的各種方法調用來對 RDD 進行轉換。經過一系列的 transformation 邏輯之后,就可以調用 action 來觸發 RDD 的最終計算。通常來講,action 包括多種方式,可以是向應用程序返回結果(show、count 和 collect 等),也可以是向存儲系統保存數據( saveAsTextFile 等)。在 Spark 中,只有遇到 action,才會真正地執行 RDD 的計算(注:這被稱為惰性計算,英文為 Lazy Evqluation ),這樣在運行時可以通過管道的方式傳輸多個轉換。
總結而言,基于 RDD 的計算任務可描述為:從穩定的物理存儲(如分布式文件系統 HDFS)中加載記錄,記錄被傳入由一組確定性操作構成的 DAG(有向無環圖),然后寫回穩定存儲。RDD 還可以將數據集緩存到內存中,使得在多個操作之間可以很方便地重用數據集。總的來講,RDD 能夠很方便地支持 MapReduce 應用、關系型數據處理、流式數據處理( Stream Processing )和迭代型應用(圖計算、機器學習等)。
在容錯性方面,基于 RDD 之間的依賴,一個任務流可以描述為 DAG。在實際執行的時候,RDD 通過 Lineage 信息(血緣關系)來完成容錯,即使出現數據分區丟失,也可以通過 Lineage 信息重建分區。如果在應用程序中多次使用同一個 RDD,則可以將這個 RDD 緩存起來,該 RDD 只有在第一次計算的時候會根據 Lineage 信息得到分區的數據,在后續其他地方用到這個 RDD 的時候,會直接從緩存處讀取而不用再根據 Lineage 信息計算,通過重用達到提升性能的目的。 雖然 RDD 的 Lineage 信息可以天然地實現容錯(當 RDD 的某個分區數據計算失敗或丟失時,可以通過 Lineage 信息重建),但是對于長時間迭代型應用來說,隨著迭代的進行,RDD 與 RDD 之間的 Lineage 信息會越來越長,一旦在后續迭代過程中出錯,就需要通過非常長的 Lineage 信息去重建,對性能產生很大的影響。為此,RDD 支持用 checkpoint 機制將數據保存到持久化的存儲中,這樣就可以切斷之前的 Lineage 信息,因為 checkpoint 后的 RDD 不再需要知道它的父 RDD,可以從 checkpoint 處獲取數據。
DAG
有向無環圖,在 Spark 中對 RDD 的操作分為兩種,一種是 transformation 的,另一種是 action 的,當不斷的對 RDD 使用 transformation 算子時,會不斷的生成新的 RDD,這些 RDD 之間是存在 ' 血緣關系 ' 的,因此也被稱為 lineage,直到觸發了 action 動作的算子之后,整個 DAG 圖就結束了。DAG 的具體實現在 DAGScheduler 中。
DAGScheduler
DAGScheduler 是采用 RDD 依賴關系的邏輯計劃并將其轉換為實際物理計劃的組件。 是一個高層次的調度器,負責將 DAG 有向無環圖劃分成不同的 Stage,劃分的依據即為 RDD 之間的寬窄依賴,劃分完成之后,構建這些 Stage 之間的父子關系,最后將每個 Stage 按照 Partition 切分成多個 Task,并且以 TaskSet 的形式提交給 TaskScheduler。
Stage
當 RDD 觸發了 action 算子之后,DAGScheduler 會開始分析最終 RDD 形成的依賴關系,逆向往前推導,前一個 RDD 被看做是父 RDD。在 Spark 中 RDD 之間的依賴關系存在兩種情況,一種是窄依賴 一種是寬依賴,每當遇到一個寬依賴的時候,便會以此為分界線,劃分出一個 Stage。
Stage 分為兩種,最后一個 Stage 之前的全部是 ShuffleMapStage,最后一個 Stage 是 ResultStage。
TaskScheduler
TaskScheduler 是用于向 Worker 上的 Executor 提交任務的組件。
調度程序應用由 spark.scheduler.mode 配置參數設置的調度策略。策略有兩種 FAIR(default) / FIFO
TaskScheduler 接收 DAGScheduler 提交過來的 TaskSet 集合,并向 Driver 請求分配任務運行資源,Driver 將可用的 ExecutorBackend 資源發給 TaskScheduler,TaskScheduler 將 Task 合理的分配給所有的 ExecutorBackend,最后會向 ExecutorBackend 發送 launchTask 請求,這時 Executor 會啟動 TaskRunner 線程并放到線程池中執行任務。
TaskScheduler 通過 SchedulerBackend 來給 Task 分配資源,并與相應的 Executor 進行通信,讓其運行任務。
Job
用戶提交的一個作業,當 RDD 及其 DAG 被提交給 DAGScheduler 調度后,DAGScheduler 會將所有 RDD 中的轉換及 Action 動作視為一個 Job。
一個 Job 由一到多個 Task 組成。
Task
Task 是任務真正的執行者,每個 Stage 會根據 Partition 的數量生成 Task,一個 Stage 中的 Task 最終會包裝成一個 TaskSet 傳給 TaskScheduler。
每個 Stage 中,一個 Partition 對應一個 Task。
Task 分為兩種,一種是 ShuffleMapTask,另一種是 ResultTask。
參考資料:
- 《Spark SQL 內核剖析》
- Apache Spark 源代碼演練-Spark 論文閱讀筆記以及作業提交和運行
- spark-job-execution-model