Flink watermark

Flink中watermark主要解決保序問題. 而保序問題的根本原因是多個任務同時從流中並行處理數據,順序無法保證.

上游: 生成watermark
一般在WINDOW 操作之前生成WATERMARK, WATERMARK 有兩種:
AssignWithPeriodicWatermarks:
每隔N秒自動向流裏注入一個WATERMARK 時間間隔由ExecutionConfig.setAutoWatermarkInterval 決定. 每次調用getCurrentWatermark 方法, 如果得到的WATERMARK 不爲空並且比之前的大就注入流中 (emitWatermark)
參考 TimestampsAndPeriodicWatermarksOperator.processElement

AssignWithPunctuatedWatermarks:
基於事件向流裏注入一個WATERMARK,每一個元素都有機會判斷是否生成一個WATERMARK. 如果得到的WATERMARK 不爲空並且比之前的大就注入流中 (emitWatermark)
參考 TimestampsAndPunctuatedWatermarksOperator.processElement

每次生成WATERMARK將覆蓋流中已有的WATERMARK

下游: 處理watermark
StatusWatermarkValve 負責將不同Channel 的Watermark 對齊,再傳給pipeline 下游,對齊的概念是當前Channel的Watermark時間大於所有Channel最小的Watermark時間
Flink watermark

WindowOperator 的處理:
WindowOperator.processElement

  1. WindowAssigner.assignWindows 爲當前的消息分配滑動窗口
    常用的有: TumblingEventTimeWindows: 按照消息的 EventTime 分配窗口 (每次生成單個窗口)
    TumblingProcessingTimeWindows 按照當前的時間分配窗口 (每次生成單個窗口)
    需要配合StreamExecutionEnvironment.setStreamTimeCharacteristic 使用 (默認是TimeCharacteristic.ProcessingTime), 這個必須匹配
    否則無法正常觸發滑動窗口

實際觀察結果:

  • 如果使用ProcessingTimeWindows 即使Event 本身的時間落後於窗口時間很多也會被觸發
  • 無論是否使用WATERMARK,窗口中的數據會有亂序,即後到窗口中的數據早於先到窗口中的數據
  • 如果使用EventTimeWindow, 數據和窗口時間對齊不會亂序,同一窗口中的數據不能嚴格保證順序,需要SORT.
  • 最後一批數據有缺失,缺失的數據取決於WATERMARK的MAXOUTOFORDERNESS
  • 默認的WATERMARK算法是根據元素的最大時間決定的,當沒有新的元素進入流中的時候,水位不再上漲,再減去MAXOUTOFORDERNESS, 則最後一批數據無法落在水位之下,導致WINDOW無法觸發
  1. 將當前的滑動窗口和對象加入WindowState, 根據不同的應用場景會使用不同的WindowState. WindowState 的類型由WindowedStream的具體操作決定, 生成對應的StateDescriptor, 不同的WindowState 的 add/get 行爲會不同. 比如HeapListWindowState 會把當前的對象追加到currentNamespace (即Timewindow) 對應的List 下. 比如HeapAggregateState 會對當前的對象應用Aggregate function 並更新結果

Window 觸發的條件
在 WindowOperator 中有兩個點會檢查窗口是否觸發,兩者的檢查條件有所不同

  1. processElement 這是在新的流數據進入時觸發
    檢查條件: watermark時間 >= 窗口最大時間 參見 EventTimeTrigger.onElement
    如果窗口不能被觸發則調用InteralTimeService.registerEventTimeTimer 註冊一個定時器,以KEY+窗口最大時間爲條件觸發, 到一定時間後定時器會被自動銷燬. 時間爲窗口最大時間+WindowOperator.allowedLateness WindowOperator.allowedLateness 可以通過 Stream.window(...).allowedLateness(...) 設置. 一般應該略大於WatermarkGenerator 的 maxOutOfOrderness

  2. onEventTime 或者 onProcessingTime 取決於Watermark的類型, 這是在Watermark更新的時候觸發 (InteralTimeService.advanceWatermark). 這時會把當前Watermark 的時間和之前註冊的定時器的時間做比較, 如果定時器還存在並且Watermark的時間大於定時器時間則可以觸發窗口. 參見 EventTimeTrigger.onEventTime
    Flink watermark
    參考 http://blog.csdn.net/lmalds/article/details/52704170

WATERMARK和普通數據分開處理
如果一個元素來的過晚 element.getTimestamp + allowedLateness < currentWatermark
會有一個特殊的OutputTag 和正常的流數據區分開
參考 https://ci.apache.org/projects/flink/flink-docs-release-1.3/dev/stream/side_output.html

如果窗口來的過晚, window.maxTimestamp + allowedLateness < currentWatermark, 則窗口會被直接丟棄

Watermark 的問題:
默認的Watermark機制是數據驅動的,新的數據進入纔會觸發水位上升, 而由於maxOutOfOrderness 的存在, watermark < 最大流數據時間 < 當前窗口結束時間
根據之前的分析,最新的時間窗口總是不會被觸發,除非更新的數據進入再次提高水位到當前窗口結束時間以後, 如果數據進入的頻率低或者沒有新的數據進入流,那最新的時間窗口被處理的延時會非常高甚至永遠不會被觸發,這在實時性要求高的流式系統是很致命的. 比如一個銀行系統,要做客戶賬號層面的保序,每個賬號的交易可能一天只有幾筆甚至一筆,如果我們在Window 處理的時候KEY BY 賬號就會引起上述問題. 我們可以考慮KEY BY的條件改爲 HASH(賬號) 再取模,然後在窗口處理中再次根據賬號分組,這樣雖然處理複雜一些,但是保證了窗口中數據的頻次

另外一種方案是優化WATERMARK生成的機制,如果一段時間後WATERMARK仍然沒有變化,那就將WATERMARK自動上漲一次到當前窗口的結束時間,這樣保證窗口處理的延時有個上限

public abstract class AbstractWatermarkGenerator<T> implements AssignerWithPeriodicWatermarks<T> {
    private static final long serialVersionUID = -2006930231735705083L;
    private static final Logger logger = LoggerFactory.getLogger(AbstractWatermarkGenerator.class);
    private final long maxOutOfOrderness; // 10 seconds
    private long windowSize;
    private long currentMaxTimestamp;
    private long lastTimestamp = 0;
    private long lastWatermarkChangeTime = 0;
    private long windowPurgeTime = 0;

    public AbstractWatermarkGenerator(long maxOutOfOrderness, long windowSize) {
        this.maxOutOfOrderness = maxOutOfOrderness;
        this.windowSize = windowSize;
    }

    public AbstractWatermarkGenerator() {
        this(10000, 10000);
    }

    protected abstract long extractCurTimestamp(T element) throws Exception;

    public long extractTimestamp(T element,
            long previousElementTimestamp) {
        try {
            long curTimestamp = extractCurTimestamp(element);
            lastWatermarkChangeTime = new Date().getTime();
            currentMaxTimestamp = Math.max(curTimestamp, currentMaxTimestamp);
            windowPurgeTime = Math.max(windowPurgeTime, getWindowExpireTime(currentMaxTimestamp));
            if (logger.isDebugEnabled()) {
                logger.debug("Extracting timestamp: {}", currentMaxTimestamp);
            }
            return curTimestamp;
        } catch (Exception e) {
            logger.error("Error extracting timestamp", e);      
        }

        return 0;
    }

    protected long getWindowExpireTime(long currentMaxTimestamp) {
        long windowStart = TimeWindow.getWindowStartWithOffset(currentMaxTimestamp, 0, windowSize);
        long windowEnd = windowStart + windowSize;
        return windowEnd + maxOutOfOrderness;
    }

    public Watermark getCurrentWatermark() {
        long curTime = new Date().getTime();
        if (currentMaxTimestamp > lastTimestamp) {
            if (logger.isDebugEnabled()) {
                logger.debug("Current max timestamp has been increased since last");
            }
            lastTimestamp = currentMaxTimestamp;
            lastWatermarkChangeTime = curTime;
        }
        else {
            long diff = windowPurgeTime - currentMaxTimestamp;
            if (diff > 0 && curTime - lastWatermarkChangeTime > diff) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Increase current MaxTimestamp once");
                }
                currentMaxTimestamp = windowPurgeTime;
                lastTimestamp = currentMaxTimestamp;
                lastWatermarkChangeTime = curTime;
            }
        }

        return new Watermark(currentMaxTimestamp - maxOutOfOrderness);
    }
}

實際測試中發現 WATERMARK是否觸發和算子的併發度和WATERMARK生成的位置有關
測試結果如下:

  • Env default parallism 10: Source parallism 20, window parallism 6, watermark 生成定義在keyby 之前
    Source 爲單獨的SUBTASK 併發度爲20, 之後到WINDOW算子之前合成一個SUBTASK,併發度爲10, WINDOW SUBTASK 併發度爲6, 窗口可以正常觸發
  • Env default parallism 20, Source parallism 20, window parallism 6, watermark 生成定義在keyby 之前
    Source 到 WINDOW 算子之前 合成一個SUBTASK,併發度爲20, WINDOW SUBTASK 併發度爲6, 窗口可以正常觸發
  • Env default parallism 60, Source parallism 20, window parallism 10, watermark 生成定義在keyby 之前
    Source 爲單獨的SUBTASK 併發度爲20, 之後到WINDOW算子之前合成一個SUBTASK,併發度爲60,WINDOW SUBTASK 併發度爲10, 窗口不能正常觸發 (個人理解原因是算子併發度擴大,導致一些CHANNEL處理線程沒有數據,根據上文的解釋,WATERMARK對齊會取所有CHANNEL最小的WATERMARK,導致水位無法上漲
    可以從FLINK CONSOLE的WATERMARKS看出)
  • Env default parallism 60, Source parallism 20, window parallism 10, watermark 生成定義在Source之後
    Source 爲單獨的SUBTASK 併發度爲20, 之後到WINDOW算子之前合成一個SUBTASK,併發度爲60,WINDOW SUBTASK 併發度爲10, 窗口可以正常觸發
  • Env default parallism 10, Source parallism 20, window parallism 20, watermark 生成定義在keyby 之前
    Source 爲單獨的SUBTASK 併發度爲20, 之後到WINDOW算子之前合成一個SUBTASK,併發度爲10, WINDOW SUBTASK 併發度爲20, 窗口可以正常觸發
  • Env default parallism 30, Source parallism 20, window parallism 20, watermark 生成定義在keyby 之前
    Source 爲單獨的SUBTASK 併發度爲20, 之後到WINDOW算子之前合成一個SUBTASK,併發度爲30, WINDOW SUBTASK 併發度爲20, 窗口不能正常觸發
  • Env default parallism 30, Source parallism 20, window parallism 20, watermark 生成定義在keyby 之前
    Source 爲單獨的SUBTASK 併發度爲20, 之後到WINDOW算子之前合成一個SUBTASK,併發度爲30, WINDOW SUBTASK 併發度爲20, 窗口不能正常觸發
  • Env default parallism 30, Source parallism 20, window parallism 20, watermark 生成定義在Source 之後
    Source 爲單獨的SUBTASK 併發度爲20, 之後到WINDOW算子之前合成一個SUBTASK,併發度爲30, WINDOW SUBTASK 併發度爲20, 窗口可以正常觸發

所以注意WINDOW算子之前最好避免讓下游算子的併發度超過上游算子,否則就把WATERMARK的生成儘量放到DAG的前端,這樣WATERMARK可以被傳遞到下游算子

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