詳解 Flink 實時應用的確定性

確定性(Determinism)是計算機科學中十分重要的特性,確定性的算法保證對於給定相同的輸入總是產生相同的輸出。在分佈式實時計算領域,確定性是業界一直難以解決的課題,由此導致用離線計算修正實時計算結果的 Lambda 架構成爲大數據領域過去近十年的主流架構。

而在最近幾年隨着 Google The Dataflow Model 的提出,實時計算和離線計算的關係逐漸清晰,在實時計算中提供與離線計算一致的確定性成爲可能。本文將基於流行實時計算引擎 Apache Flink,梳理構建一個確定性的實時應用要滿足什麼條件。

確定性與準確性

比起確定性,準確性(Accuracy)可能是我們接觸更多的近義詞,大多數場景下兩者可以混用,但其實它們稍有不同: 準確的東西一定是確定的,但確定性的東西未必百分百準確。在大數據領域,不少算法可以根據需求調整成本和準確性的平衡,比如 HyperLogLog 去重統計算法給出的結果是有一定誤差的(因此不是準確的),但卻同時是確定性的(重算可以得到相同結果)。

要分區確定性和準確性的緣故是,準確性與具體的業務邏輯緊密耦合難以評估,而確定性則是通用的需求(除去少數場景用戶故意使用非確定性的算法)。當一個 Flink 實時應用提供確定性,意味着它在異常場景的自動重試或者手動重流數據的情況下,都能像離線作業一般產出相同的結果,這將很大程度上提高用戶的信任度。

影響 Flink 應用確定性的因素

投遞語義

常見的投遞語義有 At-Most-Once、At-Least-Once 和 Exactly-Once 三種。嚴格來說只有 Exactly-Once滿足確定性的要求,但如果整個業務邏輯是冪等的, 基於At-Least-Once 也可以達到結果的確定性。

實時計算的 Exactly-Once 通常指端到端的 Exactly-Once,保證輸出到下游系統的數據和上游的數據是一致的,沒有重複計算或者數據丟失。要達到這點,需要分別實現讀取數據源(Source 端)的 Exactly-Once、計算的 Exactly-Once 和輸出到下游系統(Sink 端)的 Exactly-Once。

其中前面兩個都比較好保證,因爲 Flink 應用出現異常會自動恢復至最近一個成功 checkpoint,Pull-Based 的 Source 的狀態和 Flink 內部計算的狀態都會自動回滾到快照時間點,而問題在於 Push-Based 的 Sink 端。Sink 端是否能順利回滾依賴於外部系統的特性,通常來說需要外部系統支持事務,然而不少大數據組件對事務的支持並不是很好,即使是實時計算最常用的 Kafka 也直到 2017 年的 0.11 版本才支持事務,更多的組件需要依賴各種 trick 來達到某種場景下的 Exactly-Once。

總體來說這些 trick 可以分爲兩大類:

  • 依賴寫操作的冪等性。比如 HBase 等 KV 存儲雖然沒有提供跨行事務,但可以通過冪等寫操作配合基於主鍵的 Upsert 操作達到 Exactly-Once。不過由於 Upsert 不能表達 Delete 操作,這種模式不適合有 Delete 的業務場景。

  • 預寫日誌(WAL,Write-Ahead-Log)。預寫日誌是廣泛應用於事物機制的技術,包括 MySQL、PostgreSQL 等成熟關係型數據庫的事物都基於預寫日誌。預寫日誌的基本原理先將變更寫入緩存區,等事務提交的時候再一次全部應用。比如 HDFS/S3 等文件系統本身並不提供事務,因此實現預寫日誌的重擔落到了它們的用戶(比如 Flink)身上。通過先寫臨時的文件/對象,等 Flink Checkpoint 成功後再提交,Flink 的 FileSystem Connector 實現了 Exactly-Once。然而,預寫日誌只能保證事務的原子性和持久性,不能保證一致性和隔離性。爲此 FileSystem Connector 通過將預寫日誌設爲隱藏文件的方式提供了隔離性,至於一致性(比如臨時文件的清理)則無法保證。

爲了保證 Flink 應用的確定性,在選用官方 Connector,特別是 Sink Connector 時,用戶應該留意官方文檔關於 Connector 投遞語義的說明[3]。此外,在實現定製化的 Sink Connector 時也需要明確達到何種投遞語義,可以參考利用外部系統的事務、寫操作的冪等性或預寫日誌三種方式達到 Exactly-Once 語義。

函數副作用

函數副作用是指用戶函數對外界造成了計算框架意料之外的影響。比如典型的是在一個 Map 函數裏將中間結果寫到數據庫,如果 Flink 作業異常自動重啓,那麼數據可能被寫兩遍,導致不確定性。對於這種情況,Flink 提供了基於 Checkpoint 的兩階段提交的鉤子(CheckpointedFunction 和 CheckpointListener),用戶可以用它來實現事務,以消除副作用的不確定性。另外還有一種常見的情況是,用戶使用本地文件來保存臨時數據,這些數據在 Task 重新調度的時候很可能丟失。其他的場景或許還有很多,總而言之,如果需要在用戶函數裏改變外部系統的狀態,請確保 Flink 對這些操作是知情的(比如用 State API 記錄狀態,設置 Checkpoint 鉤子)。

Processing Time

在算法中引入當前時間作爲參數是常見的操作,但在實時計算中引入當前系統時間,即 Processing Time,是造成不確定性的最常見也最難避免的原因。對 Processing 的引用可以是很明顯、有完善文檔標註的,比如 Flink 的 Time Characteristic,但也可能是完全出乎用戶意料的,比如來源於緩存等常用的技術。爲此,筆者總結了幾類常見的 Processing Time 引用:

  • Flink 提供的 Time Characteristic。Time Characteristic 會影響所有使用與時間相關的算子,比如 Processing Time 會讓窗口聚合使用當前系統時間來分配窗口和觸發計算,造成不確定性。另外,Processing Timer 也有類似的影響。

  • 直接在函數裏訪問外部存儲。因爲這種訪問是基於外部存儲某個 Processing Time 時間點的狀態,這個狀態很可能在下次訪問時就發生了變化,導致不確定性。要獲得確定性的結果,比起簡單查詢外部存儲的某個時間點的狀態,我們應該獲取它狀態變更的歷史,然後根據當前 Event Time 去查詢對應的狀態。這也是 Flink SQL 中 Temporary Table Join 的實現原理[1]。

  • 對外部數據的緩存。在計算流量很大的數據時,很多情況下用戶會選擇用緩存來減輕外部存儲的負載,但這可能會造成查詢結果的不一致,而且這種不一致是不確定的。無論是使用超時閾值、LRU(Least Recently Used)等直接和系統時間相關的緩存剔除策略,還是 FIFO(First In First Out)、LFU(Less Frequently Used)等沒有直接關聯時間的剔除策略,訪問緩存得到的結果通常和消息的到達順序相關,而在上游經過 shuffle 的算子裏面這是難以保證的(沒有 shuffle 的 Embarrassingly Parallel 作業是例外)。

  • Flink 的 StateTTL。StateTTL 是 Flink 內置的根據時間自動清理 State 的機制,而這裏的時間目前只提供 Processing Time,無論 Flink 本身使用的是 Processing Time 還是 Event Time 作爲 Time Characteristic。BTW,StateTTL 對 Event Time 的支持可以關注 FLINK-12005[2]。

綜合來講,要完全避免 Processing Time 造成的影響是非常困難的,不過輕微的不確定性對於業務來說通常是可以接受的,我們要做的更多是提前預料到可能的影響,保證不確定性在可控範圍內。

Watermark

Watermark 作爲計算 Event Time 的機制,其中一個很重要的用途是決定實時計算何時要輸出計算結果,類似文件結束標誌符(EOF)在離線批計算中達到的效果。然而,在輸出結果之後可能還會有遲到的數據到達,這稱爲窗口完整性問題(Window Completeness)。

窗口完整性問題無法避免,應對辦法是要麼更新計算結果,要麼丟棄這部分數據。因爲離線場景延遲容忍度較大,離線作業可以推遲一定時間開始,儘可能地將延遲數據納入計算。而實時場景對延遲有比較高的要求,因此一般是輸出結果後讓狀態保存一段時間,在這段時間內根據遲到數據持續更新結果(即 Allowed Lateness),此後將數據丟棄。因爲定位,實時計算天然可能出現更多被丟棄的遲到數據,這將和 Watermark 的生成算法緊密相關。

雖然 Watermark 的生成是流式的,但 Watermark 的下發是斷點式的。Flink 的 Watermark 下發策略有 Periodic 和 Punctuated 兩種,前者基於 Processing Time 定時觸發,後者根據數據流中的特殊消息觸發。

圖1. Periodic Watermark 正常狀態與重放追數據狀態

基於 Processing Time 的 Periodic Watermark 具有不確定。在平時流量平穩的時候 Watermark 的提升可能是階梯式的(見圖1(a));然而在重放歷史數據的情況下,相同長度的系統時間內處理的數據量可能會大很多(見圖1(b)),並且伴有 Event Time 傾斜(即有的 Source 的 Event Time 明顯比其他要快或慢,導致取最小值的總體 Watermark 被慢 Watermark 拖慢),導致本來丟棄的遲到數據,現在變爲 Allowed Lateness 之內的數據(見圖1中紅色元素)。

圖2. Punctuated Watermark 正常狀態與重放追數據狀態

相比之下 Punctuated Watermark 更爲穩定,無論在正常情況(見圖2(a))還是在重放數據的情況(見圖2(b))下,下發的 Watermark 都是一致的,不過依然有 Event Time 傾斜的風險。對於這點,Flink 社區起草了 FLIP-27 來處理[4]。基本原理是 Source 節點會選擇性地消費或阻塞某個 partition/shard,讓總體的 Event Time 保持接近。

除了 Watermark 的下發有不確定之外,還有個問題是現在 Watermark 並沒有被納入 Checkpoint 快照中。這意味着在作業從 Checkpoint 恢復之後,Watermark 會重新開始算,導致 Watermark 的不確定。這個問題在 FLINK-5601[5] 有記錄,但目前只體現了 Window 算子的 Watermark,而在 StateTTL 支持 Event Time 後,或許每個算子都要記錄自己的 Watermark。

綜上所述,Watermark 目前是很難做到非常確定的,但因爲 Watermark 的不確定性是通過丟棄遲到數據導致計算結果的不確定性的,只要沒有丟棄遲到數據,無論中間 Watermark 的變化如何,最終的結果都是相同的。

總結

確定性不足是阻礙實時計算在關鍵業務應用的主要因素,不過當前業界已經具備瞭解決問題的理論基礎,剩下的更多是計算框架後續迭代和工程實踐上的問題。就目前開發 Flink 實時應用而言,需要注意投遞語義、函數副作用、Processing Time 和 Watermark 這幾點造成的不確定性。

參考:

  1. Flux capacitor, huh? Temporal Tables and Joins in Streaming SQL

    https://flink.apache.org/2019/05/14/temporal-tables.html

  2. [FLINK-12005][State TTL] Event time support

    https://issues.apache.org/jira/browse/FLINK-12005

  3. Fault Tolerance Guarantees of Data Sources and Sinks

    https://ci.apache.org/projects/flink/flink-docs-release-1.10/dev/connectors/guarantees.html

  4. FLIP-27: Refactor Source Interface

    https://cwiki.apache.org/confluence/display/FLINK/FLIP-27%3A+Refactor+Source+Interface

  5. [FLINK-5601] Window operator does not checkpoint watermarks

    https://issues.apache.org/jira/browse/FLINK-5601

點擊「閱讀原文」可查看作者原版文章~

作者介紹:

林小鉑,網易遊戲高級開發工程師,負責遊戲數據中心實時平臺的開發及運維工作,目前專注於 Apache Flink 的開發及應用。探究問題本來就是一種樂趣。


關注 Flink 中文社區,獲取更多技術乾貨

你也「在看」嗎?????

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