位元組跳動流式資料整合基於Flink Checkpoint兩階段提交的實踐和優化

位元組跳動資料平臺發表於2022-03-21

背景

位元組跳動開發套件資料整合團隊(DTS ,Data Transmission Service)在位元組跳動內基於 Flink 實現了流批一體的資料整合服務。其中一個典型場景是 Kafka/ByteMQ/RocketMQ -> HDFS/Hive 。Kafka/ByteMQ/RocketMQ -> HDFS/Hive(下面均稱之為 MQ dump,具體介紹可見 位元組跳動基於Flink的MQ-Hive實時資料整合 ) 在數倉建設第一層,對資料的準確性和實時性要求比較高。​

目前位元組跳動中國區 MQ dump 例行任務數巨大,日均處理流量在 PB 量級。巨大的任務量和資料量對 MQ dump 的穩定性以及準確性帶來了極大的挑戰。​

本文主要介紹 DTS MQ dump 在極端場景中遇到的資料丟失問題的排查與優化,最後介紹了上線效果。

線上問題

HDFS 叢集某個後設資料節點由於硬體故障當機。在該後設資料節點終止半小時後,HDFS 手動運維操作將 HDFS 切主到 backup 節點後,HDFS 恢復服務。故障恢復後使用者反饋 MQ dump 在故障期間有資料丟失,產出的資料與 MQ 中的資料不一致。

收到反饋後我們立即進行故障的排查。下面先簡要介紹一下 Flink Checkpoint 以及 MQ dump 寫入流程,然後再介紹一下故障的排查過程以及解決方案,最後是上線效果以及總結。​

Flink 基於 Chandy-Lamport 分散式快照演算法實現了 Checkpoint 機制,能夠提供 Exactly Once 或者 At Least Once 語義。​

Flink 通過在資料流中注入 barriers 將資料拆分為一段一段的資料,在不終止資料流處理的前提下,讓每個節點可以獨立建立 Checkpoint 儲存自己的快照。每個 barrier 都有一個快照 ID ,在該快照 ID 之前的資料都會進入這個快照,而之後的資料會進入下一個快照。​

Checkpoint 對 Operator state 進行快照的流程可分為兩個階段:

  • Snapshot state 階段:對應 2PC 準備階段。Checkpoint Coordinator 將 barries 注入到 Source Operator 中。Operator 接收到輸入 Operator 所有併發的 barries 後將當前的狀態寫入到 state 中,並將 barries 傳遞到下一個 Operator。​
  • Notify Checkpoint 完成階段:對應 2PC 的 commit 階段。Checkpoint Coordinator 收到 Sink Operator 的所有 Checkpoint 的完成訊號後,會給 Operator 傳送 Notify 訊號。Operator 收到訊號以後會呼叫相應的函式進行 Notify 的操作。​

    而在任務失敗後,任務會從上一個 Checkpoint state 中進行恢復,進而實現 Exactly Once 或者 At Least Once 語義。​

MQ dump 寫入流程梳理​

MQ dump 利用 Flink Checkpoint 機制和 2PC(Two-phase Commit) 機制實現了 Exactly Once 語義,資料可以做到不重不丟。​

根據 Flink Checkpoint 的流程,MQ dump 整個寫入過程可以分為四個不同的流程:​

  • 資料寫入階段​
  • SnapshotState 階段​
  • Notify Checkpoint 完成階段​
  • Checkpoint 恢復階段​

整個流程可以用下面的流程圖表示:

下面詳細介紹上面各個階段的主要操作。假設 Flink 任務當前 Checkpoint id 為 n,當前任務的 task id 為x。​

資料寫入階段​

寫入階段就主要有以下兩個操作:​

  • 如果是當前 Checkpoint 第一次寫入(transaction),先清理要寫入臨時資料夾 /tmp/cp-n/task-x​
  • 在臨時資料夾中建立檔案並寫入資料​

注意在寫入資料之前我們會先清理臨時目錄。執行這個操作的原因是我們需要保證最終資料的準確性:​

假設任務 x 在 Checkpoint n 寫入階段失敗了(將部分資料寫入到臨時資料夾 /tmp/cp-n/task-x),那麼任務會從上一個 Checkpoint n-1 恢復,下一個寫入的 Checkpoint id 仍然為 n。如果寫入前不清理臨時目錄,失敗前遺留的部分髒檔案就會保留,在 Checkpoint 階段就會將髒檔案移到正式目錄中。​

SnapshotState 階段​

SnapshotState 階段對應 2PC 的兩個階段中的第一個階段。主要操作是關閉正在寫入的檔案,並將任務的 state (主要是當前的 Checkpoint id 和 task id)儲存起來。​

Notify Checkpoint 完成階段​

該階段對應 2PC 兩個階段中的第二個階段。主要操作如下:​

  • List 臨時目錄資料夾 /tmp/cp-n/task-x​
  • 將臨時目錄資料夾下的所有檔案 rename 到正式目錄​
  • 刪除臨時目錄資料夾 /tmp/cp-n/task-x​

Checkpoint 恢復階段​

Checkpoint 恢復階段是任務在異常場景下,從輕量級的分散式快照恢復階段。主要操作如下:​

  • 從 Flink state 中恢復出任務的 Checkpoint id n 和 任務的 task id x​
  • 根據 Checkpoint id 和 任務的 task id x 獲取到臨時目錄資料夾 /tmp/cp-n/task-x​
  • 將臨時目錄資料夾下的所有檔案 rename 到正式目錄​
  • 刪除臨時目錄資料夾 /tmp/cp-n/task-x​

故障排查過程​

瞭解完相關寫入流程後,我們回到故障的排查。使用者任務配置的併發為 8,也就是說執行過程中有 8 個task在同時執行。​

排查過程中,我們首先檢視 Flink Job manager 和 Task manager 在 HDFS 故障期間的日誌,發現在 Checkpoint id 為 4608 時, task 2/3/6/7 都產出了若干個檔案。而 task 0/1/4/5 在 Checkpoint id 為 4608 時,都由於某個檔案被刪除造成寫入資料或者關閉檔案時失敗,如 task 0 失敗是由於檔案 /xx/_DUMP_TEMPORARY/cp-4608/task-0/date=20211031/18_xx_0_4608.1635674819911.zstd 被刪除而失敗。​

但是檢視正式目錄下相關檔案的資訊,我們發現 task 2、3 兩個 task 並沒有 Checkpoint 4608 的檔案(檔名含有 task id 和 Checkpoint id 資訊,所以可以根據正式目錄下的檔名知道其是哪個 task 在哪個 Checkpoint 期間建立的)。故初步確定的原因是某些檔案被誤刪造成資料丟失。 Task 2/3/6/7 在檔案刪除後由於沒有檔案的寫入和關閉操作,task 正常執行;而 task 0/1/4/5 在檔案刪除後還有檔案的寫入和關閉操作,造成 task 失敗。​

HDFS 後設資料檢視​

下一步就要去排查檔案丟失的原因。我們通過 HDFS trace 記錄表( HDFS trace記錄表記錄著使用者和系統呼叫行為,以達到分析和運維的目的)檢視 task 2 Checkpoint 4608 臨時目錄操作記錄,對應的路徑為 /xx/_DUMP_TEMPORARY/cp-4608/task-2。​

從 HDFS trace 操作記錄中可以發現資料夾的刪除操作執行了很多次。​

然後再查詢 task 2 Checkpoint 4608 臨時目錄下的檔案操作記錄。可以看出在 2021-10-31 18:08:58 左右實際有建立兩個檔案,但是由於刪除操作的重複執行造成建立的兩個檔案被刪除。​

問題的初步原因已經找到:刪除操作的重複執行造成資料丟失。​

根本原因​

我們對以下兩點感覺比較困惑:一是為啥刪除操作會重複執行;二是在寫入流程中,刪除操作要不是發生在資料寫入之前,要不發生在資料已經移動到正式目錄之後,怎麼會造成資料丟失。帶著疑惑,我們進一步分析。​

忽略 Flink Checkpoint 的恢復流程以及 Flink 狀態的操作流程,只保留與 HDFS 互動的相關步驟,DTS MQ dump 與 HDFS 的操作流程可以簡化為如下流程圖:​

在整個寫入流程中涉及到 delete 的操作有兩個地方:一個是在寫入檔案之前;一個是在將臨時檔案重新命名到正式目錄之後。在第二個刪除操作中,即使刪除操作重複執行,也不影響最終資料的準確性。因為在之前的重新命名過程中已經將所有資料從臨時資料夾移動到正式目錄。​

所以我們可以確定是在寫入檔案之前的刪除操作的重複執行造成最終的資料丟失。​

在 task-2 的日誌中我們發現 HDFS client 在 18:03:37-18:08:58 一直在嘗試呼叫 HDFS 刪除介面刪除臨時目錄,但是由於java.net.SocketTimeoutException 一直刪除失敗。在時間點18:08:58 刪除操作執行成功。而這個時間點也基本與我們在 HDFS trace 資料中發現刪除操作的執行記錄時間是對應的。通過日誌我們發現建立檔案以及關閉檔案操作基本都是在 18:08:58 這個時間點完成的,這個時間點與 HDFS trace 中的記錄也是對應上的。​

諮詢 HDFS 後,HDFS 表示 HDFS 刪除操作不會保證冪等性。進而我們判斷問題發生的根源為:在故障期間,寫入資料前的刪除操作的多次重試在 HDFS NameNode 上重複執行,將我們寫入的資料刪除造成最終資料的丟失。如果重複執行的刪除操作發生在檔案關閉之前,那麼 task 會由於寫入的檔案不存在而失敗;如果重複刪除命令是在關閉檔案之後,那麼就會造成資料的丟失。​

解決方案​

MQ dump 在異常場景中丟失資料的本質原因是我們依賴刪除操作和寫入操作的順序性。但是 HDFS NameNode 在異常場景下是無法保證兩個操作的順序性。​

方案一:HDFS 保證操作的冪等性​

為了解決這個問題,我們首先想到的是 HDFS 保證刪除操作的冪等性,這樣即使刪除操作重複執行也不會影響後續寫入的問題,進而可以保證資料的準確性。但是諮詢 HDFS 後,HDFS 表示 HDFS在現有架構下無法保證刪除的冪等性。​

參考 DDIA (Designing Data-Intensive Applications) 第 9 章中關於因果關係的定義:因果關係對事件施加了一種順序——因在果之前。對應於MQ dump 流程中刪除操作是因,發生在寫入資料之前。我們需要保證這兩個關係的因果關係。而根據其解決因果問題的方法,一種解決思路是 HDFS 在每個client 請求中都帶上序列號順序,進而在HDFS NameNode 上可以保證單個client的請求因果性。跟HDFS 討論後發現這個方案的實現成本會比較大。​

方案二:使用檔案 state​

瞭解 HDFS 難以保證操作的冪等性後,我們想是否可以將寫入前的刪除操作去除,也就是說在寫入 HDFS 之前不清理資料夾而是直接寫入資料到檔案,這樣就不需要有因果性的保證。​

如果我們知道臨時資料夾中哪些檔案是我們需要的,在重新命名階段就可以直接將需要的檔案重新命名到正式目錄而忽略臨時資料夾中的髒檔案,這樣在寫入之前就不需要刪除資料夾。故我們的解決方案是將寫入的檔案路徑儲存到 Flink state 中,從而確保在 commit 階段以及恢復階段可以將需要的檔案移動到正式目錄。​

最終,我們選擇了方案二解決該問題,使用檔案 state 前後處理流程對比如下圖所示:​

目前檔案 state 已經線上上使用了,下面先介紹一下實現中碰到的相關問題,然後再描述一下上線後效果。​

檔案 state 實現細節​

檔案移動冪等性​
通過檔案 state 我們可以解析出當前檔案所在的臨時目錄以及將要寫入的正式目錄。通過以下流程我們保證了移動的冪等性。​

通過以上的流程即使檔案移動失敗,再次重試時也能夠保證檔案移動的冪等性。​

可觀測性​

實現檔案 state 後,我們增加了 metric 記錄建立的檔案數量以及成功移動到正式目錄的檔案數量,提高了系統可觀測性。如果檔案在臨時目錄和正式目錄都不存在時,我們增加了移動失敗的 metric ,並增加了報警,在檔案移動失敗後可以及時感知到,而不是等使用者報告資料丟失後再排查。​

上線後線上 metric 效果如下:

總共有四個指標,分別為建立檔案的數量、重新命名成功檔案的數量、忽略重新命名檔案的數量、重新命名失敗的檔案數量,分別代表的意義如下:​

  • 建立檔案的數量:state 中所有檔案的數量,也就是當前 Checkpoint 處理資料階段建立的所有檔案數量。​
  • 重新命名成功檔案的數量:NotifyCheckpointComplete 階段將臨時檔案成功移動到正式目錄下的檔案數量。
  • 忽略重新命名檔案的數量:NotifyCheckpointComplete 階段忽略移動到正式目錄下下的檔案數量。也就是臨時資料夾中不存在但是正式目錄存在的檔案。這種情況通常發生在任務有 Failover 的情況。 Failover 後任務從 Checkpoint 中恢復,失敗前已經重新命名成功的檔案在當前階段會忽略重新命名。
  • 重新命名失敗的檔案數量:臨時目錄以及正式目錄下都不存在檔案的數量。這種情況通常是由於任務發生了異常造成資料的丟失。目前線上比較常見的一個 case 是任務在關閉一段時間後再開啟。由於 HDFS TTL 的設定小於任務關閉的時長,臨時目錄中寫入的檔案被 HDFS TTL 策略清除。這個結果實際是符合預期的。

前向相容性

預期中上線檔案 state 後寫入資料前不需要刪除要寫入的臨時檔案,但是為了保證升級後的前向相容性,我們分兩期上線了檔案 state :

  • 第一期寫入資料前保留了刪除操作
  • 第二期刪除了寫入資料前的刪除操作

第一期保留刪除操作的原因如果檔案 state 上線後有異常的話,回滾到之前的版本需要保證資料的準確性。而只有保留刪除操作才能保證回滾後資料的準確性。否則如果之前的 Checkpoint 資料夾中有髒檔案存在,回滾到檔案 state 之前的版本的話,由於沒有檔案 state 存在,會將髒檔案也移動到正式目錄中,影響最終資料的準確性。

上線效果

切主演練

上線後與 HDFS 進行了 HDFS 叢集切主演練。演練了以下兩個場景:

  • HDFS 叢集正常切主
  • HDFS 叢集主節點失敗超過10分鐘
    而測試過程是建立兩組不同的任務消費相同的 Kafka topic,寫入不同的 Hive 表。然後建立資料校驗任務校驗兩組任務資料的一致性。一組任務使用 HDFS 測試叢集,另一組任務使用正常叢集。

將測試叢集進行多次 HDFS 正常切主和異常切主,校驗任務顯示演練結束前後兩組任務寫入資料的一致性。結果驗證了該方案可有效解決 HDFS 操作非冪等的丟數問題。

效能效果

使用檔案 state 後,在 Notify Checkpoint 完成階段不需要呼叫 HDFS list 介面,可以減少一次 HDFS 呼叫,理論上可以減少 Notify Checkpoint 階段與 HDFS 互動時間。下圖展示了上線(18:26 左右)前後 Notify 階段與 HDFS 互動的 metrics。可以看出上線前的平均處理時間在 300ms 左右,而上線後平均處理時間在 150 ms 左右,減少了一半的處理時間。

總結

隨著位元組跳動產品業務的快速發展,位元組跳動一站式大資料開發平臺功能也越來越豐富了,提供了離線、實時、增量等場景下全域資料整合解決方案。而業務資料量的增大以及業務的多樣化給資料整合帶來了很大的挑戰。比如我們擴充套件了新增 Hive 分割槽的策略,以支援實時數倉近實時 append 場景,使資料的使用延遲下降了 75% 。

位元組跳動流式資料整合仍在不斷髮展中,未來主要關注以下幾方面:

  1. 功能增強,增加簡單的資料轉換邏輯,縮短流式資料處理鏈路,進而減少處理時延
  2. 架構升級,離線整合和實時資料整合架構統一
  3. 支援 auto scaling 功能,在業務高峰和低峰自動擴縮容,提高資源利用率,減少資源浪費

本文中介紹的《位元組跳動流式資料整合基於Flink Checkpoint兩階段提交的實踐和優化》,目前已通過火山引擎資料產品大資料研發治理套件 DataLeap 向外部企業輸出。

大資料研發治理套件 DataLeap 作為一站式大資料中臺解決方案,可以實現全場景資料整合、全鏈路資料研發、全週期資料治理、全方位資料安全。

參考文獻

  • 位元組跳動基於Flink的MQ-Hive實時資料整合
  • 位元組跳動單點恢復功能及 Regional CheckPoint 優化實踐
  • Designing Data-Intensive Applications
  • Stateful Stream Processing

歡迎關注位元組跳動資料平臺同名公眾號

相關文章