做者:林小鉑(網易遊戲)html
肯定性(Determinism)是計算機科學中十分重要的特性,肯定性的算法保證對於給定相同的輸入老是產生相同的輸出。在分佈式實時計算領域,肯定性是業界一直難以解決的課題,由此致使用離線計算修正實時計算結果的 Lambda 架構成爲大數據領域過去近十年的主流架構。算法
而在最近幾年隨着 Google The Dataflow Model 的提出,實時計算和離線計算的關係逐漸清晰,在實時計算中提供與離線計算一致的肯定性成爲可能。本文將基於流行實時計算引擎 Apache Flink,梳理構建一個肯定性的實時應用要知足什麼條件。數據庫
比起肯定性,準確性(Accuracy)多是咱們接觸更多的近義詞,大多數場景下二者能夠混用,但其實它們稍有不一樣: 準確的東西必定是肯定的,但肯定性的東西未必百分百準確。在大數據領域,很多算法能夠根據需求調整成本和準確性的平衡,好比 HyperLogLog 去重統計算法給出的結果是有必定偏差的(所以不是準確的),但卻同時是肯定性的(重算能夠獲得相同結果)。apache
要分區肯定性和準確性的緣故是,準確性與具體的業務邏輯緊密耦合難以評估,而肯定性則是通用的需求(除去少數場景用戶故意使用非肯定性的算法)。當一個 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 能夠分爲兩大類:分佈式
爲了保證 Flink 應用的肯定性,在選用官方 Connector,特別是 Sink Connector 時,用戶應該留意官方文檔關於 Connector 投遞語義的說明[3]。此外,在實現定製化的 Sink Connector 時也須要明確達到何種投遞語義,能夠參考利用外部系統的事務、寫操做的冪等性或預寫日誌三種方式達到 Exactly-Once 語義。函數
函數反作用是指用戶函數對外界形成了計算框架意料以外的影響。好比典型的是在一個 Map 函數裏將中間結果寫到數據庫,若是 Flink 做業異常自動重啓,那麼數據可能被寫兩遍,致使不肯定性。對於這種狀況,Flink 提供了基於 Checkpoint 的兩階段提交的鉤子(CheckpointedFunction 和 CheckpointListener),用戶能夠用它來實現事務,以消除反作用的不肯定性。另外還有一種常見的狀況是,用戶使用本地文件來保存臨時數據,這些數據在 Task 從新調度的時候極可能丟失。其餘的場景或許還有不少,總而言之,若是須要在用戶函數裏改變外部系統的狀態,請確保 Flink 對這些操做是知情的(好比用 State API 記錄狀態,設置 Checkpoint 鉤子)。
在算法中引入當前時間做爲參數是常見的操做,但在實時計算中引入當前系統時間,即 Processing Time,是形成不肯定性的最多見也最難避免的緣由。對 Processing 的引用能夠是很明顯、有完善文檔標註的,好比 Flink 的 Time Characteristic,但也多是徹底出乎用戶意料的,好比來源於緩存等經常使用的技術。爲此,筆者總結了幾類常見的 Processing Time 引用:
綜合來說,要徹底避免 Processing Time 形成的影響是很是困難的,不太輕微的不肯定性對於業務來講一般是能夠接受的,咱們要作的更可能是提早預料到可能的影響,保證不肯定性在可控範圍內。
Watermark 做爲計算 Event Time 的機制,其中一個很重要的用途是決定實時計算什麼時候要輸出計算結果,相似文件結束標誌符(EOF)在離線批計算中達到的效果。然而,在輸出結果以後可能還會有遲到的數據到達,這稱爲窗口完整性問題(Window Completeness)。
窗口完整性問題沒法避免,應對辦法是要麼更新計算結果,要麼丟棄這部分數據。由於離線場景延遲容忍度較大,離線做業能夠推遲必定時間開始,儘量地將延遲數據歸入計算。而實時場景對延遲有比較高的要求,所以通常是輸出結果後讓狀態保存一段時間,在這段時間內根據遲到數據持續更新結果(即 Allowed Lateness),此後將數據丟棄。由於定位,實時計算自然可能出現更多被丟棄的遲到數據,這將和 Watermark 的生成算法緊密相關。
雖然 Watermark 的生成是流式的,但 Watermark 的下發是斷點式的。Flink 的 Watermark 下發策略有 Periodic 和 Punctuated 兩種,前者基於 Processing Time 定時觸發,後者根據數據流中的特殊消息觸發。
基於 Processing Time 的 Periodic Watermark 具備不肯定。在平時流量平穩的時候 Watermark 的提高多是階梯式的(見圖1(a));然而在重放歷史數據的狀況下,相同長度的系統時間內處理的數據量可能會大不少(見圖1(b)),而且伴有 Event Time 傾斜(即有的 Source 的 Event Time 明顯比其餘要快或慢,致使取最小值的整體 Watermark 被慢 Watermark 拖慢),致使原本丟棄的遲到數據,如今變爲 Allowed Lateness 以內的數據(見圖1中紅色元素)。
相比之下 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 這幾點形成的不肯定性。
參考:
做者介紹:
林小鉑,網易遊戲高級開發工程師,負責遊戲數據中心實時平臺的開發及運維工做,目前專一於 Apache Flink 的開發及應用。探究問題原本就是一種樂趣。