Spark CommitCoordinator 保證資料一致性
原創文章,轉載請務必將下面這段話置於文章開頭處。
本文轉發自技術世界,原文連結 http://www.jasongj.com/spark/committer/
本文所述內容均基於 2018年9月17日 Spark 最新 Release 2.3.1 版本,以及 hadoop-2.6.0-cdh-5.4.4
概述
Spark 輸出資料到 HDFS 時,需要解決如下問題:
- 由於多個 Task 同時寫資料到 HDFS,如何保證要麼所有 Task 寫的所有檔案要麼同時對外可見,要麼同時對外不可見,即保證資料一致性
- 同一 Task 可能因為 Speculation 而存在兩個完全相同的 Task 例項寫相同的資料到 HDFS中,如何保證只有一個 commit 成功
- 對於大 Job(如具有幾萬甚至幾十萬 Task),如何高效管理所有檔案
commit 原理
本文通過 Local mode 執行如下 Spark 程式詳解 commit 原理
sparkContext.textFile("/json/input.zstd")
.map(_.split(","))
.saveAsTextFile("/jason/test/tmp")
在詳述 commit 原理前,需要說明幾個述語
- Task,即某個 Application 的某個 Job 內的某個 Stage 的一個 Task
- TaskAttempt,Task 每次執行都視為一個 TaskAttempt。對於同一個 Task,可能同時存在多個 TaskAttemp
- Application Attempt,即 Application 的一次執行
在本文中,會使用如下縮寫
- ${output.dir.root} 即輸出目錄根路徑
- ${appAttempt} 即 Application Attempt ID,為整型,從 0 開始
- ${taskAttemp} 即 Task Attetmp ID,為整型,從 0 開始
檢查 Job 輸出目錄
在啟動 Job 之前,Driver 首先通過 FileOutputFormat 的 checkOutputSpecs 方法檢查輸出目錄是否已經存在。若已存在,則直接丟擲 FileAlreadyExistsException
Driver執行setupJob
Job 開始前,由 Driver(本例使用 local mode,因此由 main 執行緒執行)呼叫 FileOuputCommitter.setupJob 建立 Application Attempt 目錄,即 {appAttempt}
Task執行setupTask
由各 Task 執行 FileOutputCommitter.setupTask 方法(本例使用 local mode,因此由 task 執行緒執行)。該方法不做任何事情,因為 Task 臨時目錄由 Task 按需建立。
按需建立 Task 目錄
本例中,Task 寫資料需要通過 TextOutputFormat 的 getRecordWriter 方法建立 LineRecordWriter。而建立前需要通過 FileOutputFormat.getTaskOutputPath設定 Task 輸出路徑,即 ${output.dir.root}/_temporary/${appAttempt}/_temporary/${taskAttempt}/${fileName}
。該 Task Attempt 所有資料均寫在該目錄下的檔案內
檢查是否需要 commit
Task 執行資料寫完後,通過 FileOutputCommitter.needsTaskCommit 方法檢查是否需要 commit 它寫在 ${output.dir.root}/_temporary/${appAttempt}/_temporary/${taskAttempt}
下的資料。
檢查依據是 ${output.dir.root}/_temporary/${appAttempt}/_temporary/${taskAttempt}
目錄是否存在
如果需要 commit,並且開啟了 Output commit coordination,還需要通過 RPC 由 Driver 側的 OutputCommitCoordinator 判斷該 Task Attempt 是否可以 commit
之所以需要由 Driver 側的 CommitCoordinator 判斷是否可以 commit,是因為可能由於 speculation 或者其它原因(如之前的 TaskAttemp 未被 Kill 成功)存在同一 Task 的多個 Attemp 同時寫資料且都申請 commit 的情況。
CommitCoordinator
當申請 commitTask 的 TaskAttempt 為失敗的 Attempt,則直接拒絕
若該 TaskAttempt 成功,並且 CommitCoordinator 未允許過該 Task 的其它 Attempt 的 commit 請求,則允許該 TaskAttempt 的 commit 請求
若 CommitCoordinator 之前已允許過該 TaskAttempt 的 commit 請求,則繼續同意該 TaskAttempt 的 commit 請求,即 CommitCoordinator 對該申請的處理是冪等的。
若該 TaskAttempt 成功,且 CommitCoordinator 之前已允許該 Task 的其它 Attempt 的 commit 請求,則直接拒絕當前 TaskAttempt 的 commit 請求
OutputCommitCoordinator 為了實現上述功能,為每個 ActiveStage 維護一個如下 StageState
private case class StageState(numPartitions: Int) {
val authorizedCommitters = Array.fill[TaskAttemptNumber](numPartitions)(NO_AUTHORIZED_COMMITTER)
val failures = mutable.Map[PartitionId, mutable.Set[TaskAttemptNumber]]()
}
該資料結構中,儲存了每個 Task 被允許 commit 的 TaskAttempt。預設值均為 NO_AUTHORIZED_COMMITTER
同時,儲存了每個 Task 的所有失敗的 Attempt
commitTask
當 TaskAttempt 被允許 commit 後,Task (本例由於使用 local model,因此由 task 執行緒執行)會通過如下方式 commitTask。
當 mapreduce.fileoutputcommitter.algorithm.version
的值為 1 (預設值)時,Task 將 taskAttemptPath 即 ${output.dir.root}/_temporary/${appAttempt}/_temporary/${taskAttempt}
重命令為 committedTaskPath 即 ${output.dir.root}/_temporary/${appAttempt}/${taskAttempt}
若 mapreduce.fileoutputcommitter.algorithm.version
的值為 2,直接將taskAttemptPath 即 ${output.dir.root}/_temporary/${appAttempt}/_temporary/${taskAttempt}
內的所有檔案移動到 outputPath 即 ${output.dir.root}/
commitJob
當所有 Task 都執行成功後,由 Driver (本例由於使用 local model,故由 main 執行緒執行)執行 FileOutputCommitter.commitJob
若 mapreduce.fileoutputcommitter.algorithm.version
的值為 1,則由 Driver 單執行緒遍歷所有 committedTaskPath 即 ${output.dir.root}/_temporary/${appAttempt}/${taskAttempt}
,並將其下所有檔案移動到 finalOutput 即 ${output.dir.root}
下
若 mapreduce.fileoutputcommitter.algorithm.version
的值為 2,則無須移動任何檔案。因為所有 Task 的輸出檔案已在 commitTask 內被移動到 finalOutput 即 ${output.dir.root}
內
所有 commit 過的 Task 輸出檔案移動到 finalOutput 即 ${output.dir.root}
後,Driver 通過 cleanupJob 刪除 ${output.dir.root}/_temporary/
下所有內容
recoverTask
上文所述的 commitTask 與 commitJob 機制,保證了一次 Application Attemp 中不同 Task 的不同 Attemp 在 commit 時的資料一致性
而當整個 Application retry 時,在之前的 Application Attemp 中已經成功 commit 的 Task 無須重新執行,其資料可直接恢復
恢復 Task 時,先獲取上一次的 Application Attempt,以及對應的 committedTaskPath,即 ${output.dir.root}/_temporary/${preAppAttempt}/${taskAttempt}
若 mapreduce.fileoutputcommitter.algorithm.version
的值為 1,並且 preCommittedTaskPath 存在(說明在之前的 Application Attempt 中該 Task 已被 commit 過),則直接將 preCommittedTaskPath 重新命名為 committedTaskPath
若 mapreduce.fileoutputcommitter.algorithm.version
的值為 2,無須恢復任何資料,因為在之前 Application Attempt 中 commit 過的 Task 的資料已經在 commitTask 中被移動到 ${output.dir.root}
中
abortTask
中止 Task 時,由 Task 呼叫 FileOutputCommitter.abortTask
方法刪除 ${output.dir.root}/_temporary/${appAttempt}/_temporary/${taskAttempt}
abortJob
中止 Job 由 Driver 呼叫 FileOutputCommitter.abortJob
方法完成。該方法通過 FileOutputCommitter.cleanupJob
方法刪除 ${output.dir.root}/_temporary
總結
V1 vs. V2 committer 過程
V1 committer(即 mapreduce.fileoutputcommitter.algorithm.version
的值為 1),commit 過程如下
- Task 執行緒將 TaskAttempt 資料寫入
${output.dir.root}/_temporary/${appAttempt}/_temporary/${taskAttempt}
- commitTask 由 Task 執行緒將
${output.dir.root}/_temporary/${appAttempt}/_temporary/${taskAttempt}
移動到${output.dir.root}/_temporary/${appAttempt}/${taskAttempt}
- commitJob 由 Driver 單執行緒依次將所有
${output.dir.root}/_temporary/${appAttempt}/${taskAttempt}
移動到${output.dir.root}
,然後建立_SUCCESS
標記檔案 - recoverTask 由 Task 執行緒將
${output.dir.root}/_temporary/${preAppAttempt}/${preTaskAttempt}
移動到${output.dir.root}/_temporary/${appAttempt}/${taskAttempt}
V2 committer(即 mapreduce.fileoutputcommitter.algorithm.version
的值為 2),commit 過程如下
- Task 執行緒將 TaskAttempt 資料寫入
${output.dir.root}/_temporary/${appAttempt}/_temporary/${taskAttempt}
- commitTask 由 Task 執行緒將
${output.dir.root}/_temporary/${appAttempt}/_temporary/${taskAttempt}
移動到${output.dir.root}
- commitJob 建立
_SUCCESS
標記檔案 - recoverTask 無需任何操作
V1 vs. V2 committer 效能對比
V1 在 Job 執行結束後,在 Driver 端通過 commitJob 方法,單執行緒序列將所有 Task 的輸出檔案移動到輸出根目錄。移動以檔案為單位,當 Task 個數較多(大 Job,或者小檔案引起的大量小 Task),Name Node RPC 較慢時,該過程耗時較久。在實踐中,可能因此發生所有 Task 均執行結束,但 Job 不結束的問題。甚至 commitJob 耗時比 所有 Task 執行時間還要長
而 V2 在 Task 結束後,由 Task 在 commitTask 方法內,將自己的資料檔案移動到輸出根目錄。一方面,Task 結束時即移動檔案,不需等待 Job 結束才移動檔案,即檔案移動更早發起,也更早結束。另一方面,不同 Task 間並行移動檔案,極大縮短了整個 Job 內所有 Task 的檔案移動耗時
V1 vs. V2 committer 一致性對比
V1 只有 Job 結束,才會將資料檔案移動到輸出根目錄,才會對外可見。在此之前,所有檔案均在 ${output.dir.root}/_temporary/${appAttempt}
及其子檔案內,對外不可見。
當 commitJob 過程耗時較短時,其失敗的可能性較小,可認為 V1 的 commit 過程是兩階段提交,要麼所有 Task 都 commit 成功,要麼都失敗。
而由於上文提到的問題, commitJob 過程可能耗時較久,如果在此過程中,Driver 失敗,則可能發生部分 Task 資料被移動到 ${output.dir.root} 對外可見,部分 Task 的資料未及時移動,對外不可見的問題。此時發生了資料不一致性的問題
V2 當 Task 結束時,立即將資料移動到 ${output.dir.root},立即對外可見。如果 Application 執行過程中失敗了,已 commit 的 Task 資料仍然對外可見,而失敗的 Task 資料或未被 commit 的 Task 資料對外不可見。也即 V2 更易發生資料一致性問題
相關文章
- 冗餘資料一致性,到底如何保證?
- 如何保證MySQL和Redis資料一致性?MySqlRedis
- Spark Streaming使用Kafka保證資料零丟失SparkKafka
- 如何保證mongodb和資料庫雙寫資料一致性?MongoDB資料庫
- MySQL是怎麼保證資料一致性的MySql
- Zookeeper 如何保證分散式系統資料一致性分散式
- 資料庫和快取的一致性如何保證資料庫快取
- 使用雙非同步後,如何保證資料一致性?非同步
- 如何保證快取和資料庫的一致性?快取資料庫
- PHP 併發扣款,保證資料一致性(悲觀鎖)PHP
- 趣說 | 資料庫和快取如何保證一致性?資料庫快取
- FAQ系列|如何保證主從複製資料一致性
- Spark Streaming和Kafka整合是如何保證資料零丟失SparkKafka
- 如何保證快取與資料庫的雙寫一致性?快取資料庫
- 保證分散式系統資料一致性的6種方案分散式
- 如何保證快取(redis)與資料庫的雙寫一致性快取Redis資料庫
- 如何保證資料新增或修改成功失敗的一致性?
- 面試常問:如何保證Redis快取和資料庫的資料一致性NRXW面試Redis快取資料庫
- 針對靜默資料錯誤,如何採用DIX和DIF保證資料一致性?
- exp能在什麼級別保證備份資料的一致性呢?
- 【面試普通人VS高手系列】Redis和Mysql如何保證資料一致性面試RedisMySql
- load data語句如何保證主備複製資料一致性(一)
- 阿里面試題:如何保證快取與資料庫的雙寫一致性?阿里面試題快取資料庫
- 首個徹底保證快取與資料庫一致性的開源方案快取資料庫
- 面試重災區:怎麼保證快取與資料庫的雙寫一致性?面試快取資料庫
- 如何保證 Serverless 業務部署更新的一致性?Server
- Linux:保證資料安全落盤Linux
- Elasticsearch如何保證資料不丟失?Elasticsearch
- volatile足以保證資料同步嗎
- Seata-AT 如何保證分散式事務一致性分散式
- Oracle Goldengate是如何保證資料有序和確保資料不丟失的?OracleGo
- 遠端辦公如何保證資料安全?
- HTTPS 如何保證資料傳輸安全HTTP
- Redis能保證資料不丟失嗎?Redis
- Google 釋出訊息,宣佈推出具備強一致性資料保證的雲資料庫管理系統(Cloud Spanner)Go資料庫Cloud
- 【大廠面試01期】高併發場景下,如何保證快取與資料庫一致性?面試快取資料庫
- Linux系統:保證資料安全落盤Linux
- 如何用第三層交換保證資料安全