您好,登錄后才能下訂單哦!
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時間
WindowOperator 的處理:
WindowOperator.processElement
實際觀察結果:
Window 觸發的條件
在 WindowOperator 中有兩個點會檢查窗口是否觸發,兩者的檢查條件有所不同
processElement 這是在新的流數據進入時觸發
檢查條件: watermark時間 >= 窗口最大時間 參見 EventTimeTrigger.onElement
如果窗口不能被觸發則調用InteralTimeService.registerEventTimeTimer 注冊一個定時器,以KEY+窗口最大時間為條件觸發, 到一定時間后定時器會被自動銷毀. 時間為窗口最大時間+WindowOperator.allowedLateness WindowOperator.allowedLateness 可以通過 Stream.window(...).allowedLateness(...) 設置. 一般應該略大于WatermarkGenerator 的 maxOutOfOrderness
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生成的位置有關
測試結果如下:
所以注意WINDOW算子之前最好避免讓下游算子的并發度超過上游算子,否則就把WATERMARK的生成盡量放到DAG的前端,這樣WATERMARK可以被傳遞到下游算子
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。