Flink CDC 系列 - Flink MongoDB CDC 在 XTransfer 的生產實踐

ApacheFlink 發表於 2022-01-21
MongoDB Flink

本文作者孫家寶,分享如何在 Flink CDC 基礎上通過 MongoDB Change Streams 特性實現了 Flink MongoDB CDC Connector。主要內容包括:

  1. Flink CDC
  2. MongoDB 複製機制
  3. Flink MongoDB CDC

Flink 中文學習網站
https://flink-learning.org.cn

前言

XTransfer 專注為跨境 B2B 電商中小企業提供跨境金融和風控服務,通過建立資料化、自動化、網際網路化和智慧化的風控基礎設施,搭建通達全球的財資管理平臺,提供開立全球和本地收款賬戶、外匯兌換、海外外匯管制國家申報等多種跨境金融服務的綜合解決方案。

在業務發展早期,我們選擇了傳統的離線數倉架構,採用全量採集、批量處理、覆蓋寫入的資料整合方式,資料時效性較差。隨著業務的發展,離線數倉越來越不能滿足對資料時效性的要求,我們決定從離線數倉向實時數倉進行演進。而建設實時數倉的關鍵點在於變更資料採集工具和實時計算引擎的選擇。

經過了一系列的調研,在 2021 年 2 月份,我們關注到了 Flink CDC 專案,Flink CDC 內嵌了 Debezium,使 Flink 本身具有了變更資料捕獲的能力,很大程度上降低了開發門檻,簡化了部署複雜度。加上 Flink 強大的實時計算能力和豐富的外部系統接入能力,成為了我們構建實時數倉的關鍵工具。

另外,我們在生產中也大量使用到了 MongoDB,所以我們在 Flink CDC 基礎上通過 MongoDB Change Streams 特性實現了 Flink MongoDB CDC Connector,並貢獻給了 Flink CDC 社群,目前已在 2.1 版本中釋出。很榮幸在這裡能夠在這裡和大家分享一下實現細節和生產實踐。

一、Flink CDC

Dynamic Table (動態表) 是 Flink 的支援流資料的 Table API 和 SQL 的核心概念。流和表具有對偶性,可以將錶轉換成一個變更流 (changelog stream),也可以回放變更流還原成一張表。

變更流有兩種形式:Append Mode 和 Update Mode。Append Mode 只會新增,不會變更和刪除,常見的如事件流。Update Mode 可能新增,也可能發生變更和刪除,常見的如資料庫操作日誌。在 Flink 1.11之前,只支援在 Append Mode 上定義動態表。

Flink 1.11 在 FLIP-95 引入了新的 TableSource 和 TableSink,實現了對 Update Mode changelog 的支援。並且在 FLIP-105 中,引入了對 Debezium 和 Canal CDC format 的直接支援。通過實現 ScanTableSource,接收外部系統變更日誌 (如資料庫的變更日誌),將其解釋為 Flink 的能夠識別的 changlog 並向下流轉,便可以支援從變更日誌定義動態表。

img

在 Flink 內部,changelog 記錄由 RowData 表示,RowData 包括 4 種型別:+I (INSERT), -U (UPDATE_BEFORE),+U (UPDATE_AFTER), -D (DELETE)。根據 changelog 產生記錄型別的不同,又可以分為 3 種 changelog mode。

  • INSERT_ONLY:只包含 +I,適用於批處理和事件流。
  • ALL:包含 +I, -U, +U, -D 全部的 RowKind,如 MySQL binlog。
  • UPSERT:只包含 +I, +U, -D 三種型別的 RowKind,不包含 -U,但必須按唯一鍵的冪等更新 , 如 MongoDB Change Streams。

二、MongoDB 複製機制

如上節所述,實現 Flink CDC MongoDB 的關鍵點在於:如何將 MongoDB 的操作日誌轉換為 Flink 支援的 changelog。要解決這個問題,首先需要了解一下 MongoDB 的叢集部署和複製機制。

2.1 副本集和分片叢集

副本集是 MongoDB 提供的一種高可用的部署模式,副本整合員之間通過 oplog (操作日誌) 的複製,來完成副本整合員之間的資料同步。

分片叢集是 MongoDB 支援大規模資料集和高吞吐量操作的部署模式,每個分片由一個副本集組成。

img

2.2 Replica Set Oplog

操作日誌 oplog,在 MongoDB 中是一個特殊的 capped collection (固定容量的集合),用來記錄資料的操作日誌,用於副本整合員之間的同步。oplog 記錄的資料結構如下所示。

{
    "ts" : Timestamp(1640190995, 3),
    "t" : NumberLong(434),
    "h" : NumberLong(3953156019015894279),
    "v" : 2,
    "op" : "u",
    "ns" : "db.firm",
    "ui" : UUID("19c72da0-2fa0-40a4-b000-83e038cd2c01"),
    "o2" : {
        "_id" : ObjectId("61c35441418152715fc3fcbc")
    },
    "wall" : ISODate("2021-12-22T16:36:35.165Z"),
    "o" : {
        "$v" : 1,
        "$set" : {
            "address" : "Shanghai China"
        }
    }
}
欄位是否可空描述
tsN操作時間,BsonTimestamp
tY對應raft協議裡面的term,每次發生節點down掉,新節點加入,主從切換,term都會自增。
hY操作的全域性唯一id的hash結果
vNoplog版本
opN操作型別:"i" insert, "u" update, "d" delete, "c" db cmd, "n" no op
nsN名稱空間,表示操作對應的集合全稱
uiNsession id
o2Y在更新操作中記錄_id和sharding key
wallN操作時間,精確到毫秒
oN變更資料描述

從示例中可以看出,MongoDB oplog 的更新記錄即不包含更新前的資訊,也不包含更新後的完整記錄,所以即不能轉換成 Flink 支援的 ALL 型別的 changelog,也難以轉換成 UPSERT 型別的 changelog。

另外,在分片叢集中,資料的寫入可能發生在不同的分片副本集中,因此每個分片的 oplog 中僅會記錄發生在該分片上的資料變更。因此需要獲取完整的資料變更,需要將每個分片的 oplog 按照操作時間排序合併到一起,加大了捕獲變更記錄的難度和風險。

Debezium MongoDB Connector 在 1.7 版本之前是通過遍歷 oplog 來實現變更資料捕獲,由於上述原因,我們沒有采用 Debezium MongoDB Connector 而選擇了 MongoDB 官方的基於 Change Streams 的 MongoDB Kafka Connector。

2.3 Change Streams

Change Streams 是 MongoDB 3.6 推出的一個新特性,遮蔽了遍歷 oplog 的複雜度,使使用者通過簡單的 API 就能訂閱叢集、資料庫、集合級別的資料變更。

2.3.1 使用條件

  • WiredTiger 儲存引擎
  • 副本集 (測試環境下,也可以使用單節點的副本集) 或分片叢集部署
  • 副本集協議版本:pv1 (預設)
  • 4.0 版本之前允許 Majority Read Concern: replication.enableMajorityReadConcern = true (預設允許)
  • MongoDB 使用者擁有 findchangeStream 許可權

2.3.2 Change Events

Change Events 是 Change Streams 返回的變更記錄,其資料結構如下所示:

{
   _id : { <BSON Object> },
   "operationType" : "<operation>",
   "fullDocument" : { <document> },
   "ns" : {
      "db" : "<database>",
      "coll" : "<collection>"
   },
   "to" : {
      "db" : "<database>",
      "coll" : "<collection>"
   },
   "documentKey" : { "_id" : <value> },
   "updateDescription" : {
      "updatedFields" : { <document> },
      "removedFields" : [ "<field>", ... ],
      "truncatedArrays" : [
         { "field" : <field>, "newSize" : <integer> },
         ...
      ]
   },
   "clusterTime" : <Timestamp>,
   "txnNumber" : <NumberLong>,
   "lsid" : {
      "id" : <UUID>,
      "uid" : <BinData>
   }
}
欄位型別描述
_iddocument表示resumeToken
operationTypestring操作型別,包括:insert, delete, replace, update, drop, rename, dropDatabase, invalidate
fullDocumentdocument完整文件記錄,insert, replace預設包含,update需要開啟updateLookup,delete和其他操作型別不包含
nsdocument操作記錄對應集合的完全名稱
todocument當操作型別為rename時,to表示重新命名後的完全名稱
documentKeydocument包含變更文件的主鍵 _id,如果該集合是一個分片集合,documentKey中也會包含分片建
updateDescriptiondocument當操作型別為update時,描述有變更的欄位和值
clusterTimeTimestamp操作時間
txnNumberNumberLong事務號
lsidDocumentsession id

2.3.3 Update Lookup

由於 oplog 的更新操作僅包含了有變更後的欄位,變更後完整的文件無法從 oplog 直接獲取,但是在轉換為 UPSERT 模式的 changelog 時,UPDATE_AFTER RowData 必須擁有完整行記錄。Change Streams 通過設定 fullDocument = updateLookup,可以在獲取變更記錄時返回該文件的最新狀態。另外,Change Event 的每條記錄都包含 documentKey (_id 以及 shard key),標識發生變更記錄的主鍵資訊,即滿足冪等更新的條件。所以通過 Update Lookup 特性,可以將 MongoDB 的變更記錄轉換成 Flink 的 UPSERT changelog。

三、Flink MongoDB CDC

在具體實現上,我們整合了 MongoDB 官方基於 Change Streams 實現的 MongoDB Kafka Connector。通過 Debezium EmbeddedEngine,可以很容易地在 Flink 中驅動 MongoDB Kafka Connector 執行。通過將 Change Stream 轉換成 Flink UPSERT changelog,實現了 MongoDB CDC TableSource。配合 Change Streams 的 resume 機制,實現了從 checkpoint、savepoint 恢復的功能。

如 FLIP-149 所述,一些運算 (如聚合) 在缺失 -U 訊息時難以正確處理。對於 UPSERT 型別的 changelog,Flink Planner 會引入額外的計算節點 (Changelog Normalize) 來將其標準化為 ALL 型別的 changelog。

img

支援特性

  • 支援 Exactly-Once 語義
  • 支援全量、增量訂閱
  • 支援 Snapshot 資料過濾
  • 支援從檢查點、儲存點恢復
  • 支援後設資料提取

四、生產實踐

4.1 使用 RocksDB State Backend

Changelog Normalize 為了補齊 -U 的前置映象值,會帶來額外的狀態開銷,在生產環境中推薦使用 RocksDB State Backend。

4.2 合適的 oplog 容量和過期時間

MongoDB oplog.rs 是一個特殊的有容量集合,當 oplog.rs 容量達到最大值時,會丟棄歷史的資料。Change Streams 通過 resume token 進行恢復,太小的 oplog 容量可能導致 resume token 對應的 oplog 記錄不再存在,因而導致恢復失敗。

在沒有顯示指定 oplog 容量時,WiredTiger 引擎的 oplog 預設容量為磁碟大小的 5%,下限為 990MB,上限為 50GB。在 MongoDB 4.4 之後,支援設定 oplog 最短保留時間,在 oplog 已滿並且 oplog 記錄超過最短保留時間時,才會對該 oplog 記錄進行回收。

可以使用 replSetResizeOplog 命令重新設定 oplog 容量和最短保留時間。在生產環境下,建議設定 oplog 容量不小於 20GB,oplog 保留時間不少於 7 天。

db.adminCommand(
  {
    replSetResizeOplog: 1, // 固定值1
    size: 20480,           // 單位為MB,範圍在990MB到1PB
    minRetentionHours: 168 // 可選項,單位為小時
  }
)

4.3 變更慢的表開啟心跳事件

Flink MongoDB CDC 會定期將 resume token 寫入 checkpoint 對 Change Stream 進行恢復,MongoDB 變更事件或者心跳事件都能觸發 resume token 的更新。如果訂閱的集合變更緩慢,可能造成最後一條變更記錄對應的 resume token 過期,從而無法從 checkpoint 進行恢復。因此對於變更緩慢的集合,建議開啟心跳事件 (設定 heartbeat.interval.ms > 0),來維持 resume token 的更新。

WITH (
    'connector' = 'mongodb-cdc',
    'heartbeat.interval.ms' = '60000'
)

4.4 自定義 MongoDB 連線引數

當預設連線無法滿足使用要求時,可以通過 connection.options 配置項傳遞 MongoDB 支援的連線引數

https://docs.mongodb.com/manu...

WITH (
   'connector' = 'mongodb-cdc',
   'connection.options' = 'authSource=authDB&maxPoolSize=3'
)

4.5 Change Stream 引數調優

可以在 Flink DDL 中通過 poll.await.time.ms 和 poll.max.batch.size 精細化配置變更事件的拉取。

  • poll.await.time.ms

變更事件拉取時間間隔,預設為 1500ms。對於變更頻繁的集合,可以適當調小拉取間隔,提升處理時效;對於變更緩慢的集合,可以適當調大拉取時間間隔,減輕資料庫壓力。

  • poll.max.batch.size

每一批次拉取變更事件的最大條數,預設為 1000 條。調大改引數會加快從 Cursor 中拉取變更事件的速度,但會提升記憶體的開銷。

4.6 訂閱整庫、叢集變更

database = "db",collection = "",可以訂閱 db 整庫的變更;database = "",collection = "",可以訂閱整個叢集的變更。

DataStream API 可以使用 pipeline 可以過濾需要訂閱的 db 和 collection,對於 Snapshot 集合的過濾目前還不支援。

MongoDBSource.<String>builder()
    .hosts("127.0.0.1:27017")
    .database("")
    .collection("")
    .pipeline("[{'$match': {'ns.db': {'$regex': '/^(sandbox|firewall)$/'}}}]")
    .deserializer(new JsonDebeziumDeserializationSchema())
    .build();

4.7 許可權控制

MongoDB 支援對使用者、角色、許可權進行細粒度的管控,開啟 Change Stream 的使用者需要擁有 find 和 changeStream 兩個許可權。

  • 單集合
{ resource: { db: <dbname>, collection: <collection> }, actions: [ "find", "changeStream" ] }
  • 單庫
{ resource: { db: <dbname>, collection: "" }, actions: [ "find", "changeStream" ] }
  • 叢集
{ resource: { db: "", collection: "" }, actions: [ "find", "changeStream" ] }

在生產環境下,建議建立 Flink 使用者和角色,並對該角色進行細粒度的授權。需要注意的是,MongoDB 可以在任何 database 下建立使用者和角色,如果使用者不是建立在 admin 下,需要在連線引數中指定 authSource =< 使用者所在的 database>。

use admin;
// 建立使用者
db.createUser(
 {
   user: "flink",
   pwd: "flinkpw",
   roles: []
 }
);

// 建立角色
db.createRole(
   {
     role: "flink_role", 
     privileges: [
       { resource: { db: "inventory", collection: "products" }, actions: [ "find", "changeStream" ] }
     ],
     roles: []
   }
);

// 給使用者授予角色
db.grantRolesToUser(
    "flink",
    [
      // 注意:這裡的db指角色建立時的db,在admin下建立的角色可以包含不同database的訪問許可權
      { role: "flink_role", db: "admin" }
    ]
);

// 給角色追加許可權
db.grantPrivilegesToRole(
    "flink_role",
     [
       { resource: { db: "inventory", collection: "orders" }, actions: [ "find", "changeStream" ] }
     ]
);

在開發環境和測試環境下,可以授予 readreadAnyDatabase 兩個內建角色給 Flink 使用者,即可對任意集合開啟 change stream。

use admin;
db.createUser({
  user: "flink",
  pwd: "flinkpw",
  roles: [
    { role: "read", db: "admin" },
    { role: "readAnyDatabase", db: "admin" }
  ]
});

五、後續規劃

  • 支援增量 Snapshot

目前,MongoDB CDC Connector 還不支援增量 Snapshot,對於資料量較大的表還不能很好發揮 Flink 平行計算的優勢。後續將實現 MongoDB 的增量 Snapshot 功能,使其支援 Snapshot 階段的 checkpoint,和併發度設定。

  • 支援從指定時間進行變更訂閱

目前,MongoDB CDC Connector 僅支援從當前時間開始 Change Stream 的訂閱,後續將提供從指定時間點的 Change Stream 訂閱。

  • 支援庫和集合的篩選

目前,MongoDB CDC Connector 支援叢集、整庫的變更訂閱和篩選,但對於是否需要進行 Snapshot 的集合的篩選還不支援,後續將完善這個功能。

參考文件

[1] Duality of Streams and Tables

[2] FLIP-95: New TableSource and TableSink interfaces

[3] FLIP-105: Support to Interpret Changelog in Flink SQL (Introducing Debezium and Canal Format)

[4] FLIP-149: Introduce the upsert-kafka Connector

[5] Apache Flink 1.11.0 Release Announcement

[6] Introduction to SQL in Flink 1.11

[7] MongoDB Manual

[8] MongoDB Connection String Options

[9] MongoDB Kafka Connector


更多 Flink 相關技術問題,可掃碼加入社群釘釘交流群
第一時間獲取最新技術文章和社群動態,請關注公眾號~

image.png