【Flink】Deduplicate 去重運算元原始碼解讀

Mulavar發表於2023-05-08

語法

https://nightlies.apache.org/flink/flink-docs-master/docs/dev...

SELECT [column_list]
FROM (
    SELECT [column_list],
         ROW_NUMBER() OVER ([PARTITIONBY col1[, col2...]]
    ORDER BY time_attr [asc|desc])AS rownum
    FROM table_name
)
WHERE rownum= 1

注意點:

  • ORDER BY 後的欄位必須是時間屬性(process time/row time)

Minibatch 開關

開啟 MiniBatch 時使用 KeyedMapBundleOperator,否則使用 KeyedProcessOperator。

狀態使用

Event TimeProcess Time
開啟 minibatchRowTimeMiniBatchDeduplicateFunctionProcTimeMiniBatchDeduplicateKeepLastRowFunction
ProcTimeMiniBatchDeduplicateKeepFirstRowFunction
沒開啟 minibatchRowTimeDeduplicateFunctionProcTimeDeduplicateKeepLastRowFunction
ProcTimeDeduplicateKeepFirstRowFunction

在 Event Time 場景下,每條資料到來後必須對比其附帶的事件時間和該運算元已儲存的事件時間進行對比,因此只需一個函式統一做邏輯處理。

而處理時間是由當前處理資料的運算元賦予,因此可以直接簡化為兩種場景:保留第一條和保留最後一條:

  • First Row:儲存第一條過來的資料,並丟棄後面來的所有資料即可,只能處理上游是 Append-only 的資料,如果是 Process Time 場景也只會產生 Insert 資料;
  • Last Row:每次到來資料需要根據時間屬性留下最新的一條,如果當前的資料是最新的,則下發回撤老資料。

所有資料的最終處理邏輯最終會落到 DeduplicateFunctionHelper,因此我們可以透過閱讀 DeduplicateFunctionHelper 的原始碼檢視不同場景的處理情況。

DeduplicateFunctionHelper

去重函式的處理都最終呼叫這個工具類的方法

Process Time&Last Row

Process Time 根據能否處理回撤訊息分為兩種:

  • processLastRowOnProcTime:僅支援處理 INSERT 訊息;
  • processLastRowOnChangelog:除 INSERT 外可處理回撤等訊息。

Last Row on Proctime

static void processLastRowOnProcTime(
            RowData currentRow,
            boolean generateUpdateBefore,
            boolean generateInsert,
            ValueState<RowData> state,
            Collector<RowData> out)
            throws Exception {

  checkInsertOnly(currentRow);
  if (generateUpdateBefore || generateInsert) {
    // use state to keep the previous row content if we need to generate UPDATE_BEFORE
    // or use to distinguish the first row, if we need to generate INSERT
    RowData preRow = state.value();
    state.update(currentRow);
    if (preRow == null) {
      // the first row, send INSERT message
      currentRow.setRowKind(RowKind.INSERT);
      out.collect(currentRow);
    } else {
      if (generateUpdateBefore) {
        preRow.setRowKind(RowKind.UPDATE_BEFORE);
        out.collect(preRow);
      }
      currentRow.setRowKind(RowKind.UPDATE_AFTER);
      out.collect(currentRow);
    }
  } else {
    // always send UPDATE_AFTER if INSERT is not needed
    currentRow.setRowKind(RowKind.UPDATE_AFTER);
    out.collect(currentRow);
  }
}
  1. 檢視該訊息是否是 INSERT 格式(只接受 INSERT 格式訊息);
  2. 檢查該節點傳送的訊息型別(generateUpdateBefore 的值由 changelogmode 和下游需要的訊息型別共同決定是否需要下發回撤,generateInsert 由 table.exec.deduplicate.insert-update-after-sensitive-enabled 配置確定)是否支援 UA 和 I;
  3. 如果不支援發回撤且不支援發 INSERT 則直接傳送 UA 訊息到下游;
  4. 如果支援,檢查當前這條資料是否是第一條資料(根據狀態查詢,狀態裡會儲存上一條到來的資料),並更新狀態為當前資料;
  5. 如果是第一條資料,則附上 +I 標識傳送訊息到下游;
  6. 如果不是第一條資料,檢查該節點是否支援傳送 UB 訊息;
  7. 如果需要,則附上 -UB 標識傳送回撤上一條資料;
  8. 傳送 +UA 訊息。

Last Row on Changelog

static void processLastRowOnChangelog(
        RowData currentRow,
        boolean generateUpdateBefore,
        ValueState<RowData> state,
        Collector<RowData> out,
        boolean isStateTtlEnabled,
        RecordEqualiser equaliser)
        throws Exception {
    RowData preRow = state.value();
    RowKind currentKind = currentRow.getRowKind();
    if (currentKind == RowKind.INSERT || currentKind == RowKind.UPDATE_AFTER) {
        if (preRow == null) {
            // the first row, send INSERT message
            currentRow.setRowKind(RowKind.INSERT);
            out.collect(currentRow);
        } else {
            if (!isStateTtlEnabled && equaliser.equals(preRow, currentRow)) {
                // currentRow is the same as preRow and state cleaning is not enabled.
                // We do not emit retraction and update message.
                // If state cleaning is enabled, we have to emit messages to prevent too early
                // state eviction of downstream operators.
                return;
            } else {
                if (generateUpdateBefore) {
                    preRow.setRowKind(RowKind.UPDATE_BEFORE);
                    out.collect(preRow);
                }
                currentRow.setRowKind(RowKind.UPDATE_AFTER);
                out.collect(currentRow);
            }
        }
        // normalize row kind
        currentRow.setRowKind(RowKind.INSERT);
        // save to state
        state.update(currentRow);
    } else {
        // DELETE or UPDATER_BEFORE
        if (preRow != null) {
            // always set to DELETE because this row has been removed
            // even the input is UPDATE_BEFORE, there may no UPDATE_AFTER after it.
            preRow.setRowKind(RowKind.DELETE);
            // output the preRow instead of currentRow,
            // because preRow always contains the full content.
            // currentRow may only contain key parts (e.g. Kafka tombstone records).
            out.collect(preRow);
            // clear state as the row has been removed
            state.clear();
        }
        // nothing to do if removing a non-existed row
    }
}

changelog 場景下,因為需要處理回撤資訊,去重邏輯相對複雜一點:

  1. 檢視當前資料是 accumulate(INSERT 或 UPDATE)還是回撤訊息;
  2. 如果是回撤訊息,且狀態不為空,則將狀態的資料附上 -D 標識下發回撤,注意:這裡回撤用的資料必須是狀態裡的,因為當前的資料可能只包含 key 部分;
  3. 如果不是回撤資料,則檢視狀態資料是否為空(即當前資料是否是第一條資料),如果是,則附上 +I 標識下發;
  4. 如果沒有啟用 TTL 且當前資料和狀態裡的資料一致,則不下發(減少大量重複資料場景下下發的資料量);
  5. 如果支援傳送 UB 訊息(generateUpdateBefore 為 true),則附上 -UB 標識下發回撤;
  6. 為當前訊息附上 +UA 標識下發;
  7. 更新狀態為當前訊息。

Process Time&First Row

static void processFirstRowOnProcTime(
            RowData currentRow, ValueState<Boolean> state, Collector<RowData> out)
            throws Exception {

  checkInsertOnly(currentRow);
  // ignore record if it is not first row
  if (state.value() != null) {
    return;
  }
  state.update(true);
  // emit the first row which is INSERT message
  out.collect(currentRow);
}
  1. 檢視該訊息是否是 INSERT 格式(只接受 INSERT 格式訊息);
  2. 檢查該條資料是否是第一條到達的資料(透過狀態查詢是否來過資料);
  3. 如果是,則直接忽略不下發,否則附上 +I 標識下發該條資料。

Event Time

由於資料到來可能存在亂序,最早到的資料不一定是 Event Time 最老的,最後到的資料也不一定是 Event Time 最新的,因此 Event Time 的去重場景不需要像 Process Time 那樣針對 First Row 和 Last Row 分別實現一套邏輯,只需在檢查當前資料是否需要下發時採取不同的策略即可。

Event Time 去重場景實現可參考 RowTimeDeduplicateFunction:

public static void deduplicateOnRowTime(
        ValueState<RowData> state,
        RowData currentRow,
        Collector<RowData> out,
        boolean generateUpdateBefore,
        boolean generateInsert,
        int rowtimeIndex,
        boolean keepLastRow)
        throws Exception {
    checkInsertOnly(currentRow);
    RowData preRow = state.value();

    if (isDuplicate(preRow, currentRow, rowtimeIndex, keepLastRow)) {
        updateDeduplicateResult(generateUpdateBefore, generateInsert, preRow, currentRow, out);
        state.update(currentRow);
    }
}
  1. 首先檢查訊息是否是 INSERT;
  2. 呼叫 isDuplicate 判斷該訊息是否應該下發,而 Event Time 去重的邏輯精髓就在於此;
  3. 下發訊息並更新狀態。

判斷訊息優先順序

static boolean isDuplicate(
            RowData preRow, RowData currentRow, int rowtimeIndex, boolean keepLastRow) {
  if (keepLastRow) {
    return preRow == null
      || getRowtime(preRow, rowtimeIndex) <= getRowtime(currentRow, rowtimeIndex);
  } else {
    return preRow == null
      || getRowtime(currentRow, rowtimeIndex) < getRowtime(preRow, rowtimeIndex);
  }
}
  1. 如果是儲存最新一條資料(Last Row),則比較當前資料的事件時間是否大於等於先前資料的事件時間;
  2. 如果是儲存最早一條資料(First Row),則比較當前資料的事件時間是否小於先前資料的事件時間。

注:這裡為什麼第一個判斷用大於等於,第二個判斷用小於?因為第一種情況的語義是最新一條資料,因此兩條資料事件時間一樣,取後來的資料,而第二種情況的語義是最早一條資料,兩條資料事件時間一樣時取先來的資料。

更新下發結果

static void updateDeduplicateResult(
            boolean generateUpdateBefore,
            boolean generateInsert,
            RowData preRow,
            RowData currentRow,
            Collector<RowData> out) {

  if (generateUpdateBefore || generateInsert) {
    if (preRow == null) {
      // the first row, send INSERT message
      currentRow.setRowKind(RowKind.INSERT);
      out.collect(currentRow);
    } else {
      if (generateUpdateBefore) {
        final RowKind preRowKind = preRow.getRowKind();
        preRow.setRowKind(RowKind.UPDATE_BEFORE);
        out.collect(preRow);
        preRow.setRowKind(preRowKind);
      }
      currentRow.setRowKind(RowKind.UPDATE_AFTER);
      out.collect(currentRow);
    }
  } else {
    currentRow.setRowKind(RowKind.UPDATE_AFTER);
    out.collect(currentRow);
  }
}

這段邏輯與 Process Time&Last Row 的邏輯非常相似,可直接參考上述的程式碼講解。

相關文章