[資料整合/資料同步] 基於資料庫增量日誌的資料同步方案 : Flink CDC/Debezium/DataX/Canal/Oracle Goldengate/Kettle/Sqoop

千千寰宇發表於2024-10-16

1 概述

簡述:CDC/增量資料同步

  • CDC 的全稱是 Change Data Capture(增量資料捕獲)
  • 在廣義的概念上,只要能捕獲資料變更的技術,我們都可以稱為 CDC
  • 我們目前通常描述的 CDC 技術主要面向資料庫的變更,是一種用於捕獲資料庫中資料變更的技術。
  • CDC 的技術實現方案
  • 基於查詢的 CDC:
  • 優點 : 實現簡單,是透過批處理實現的
  • 缺點 : 需要依賴離線排程,不能保證資料強一致性和實時性;
  • 基於日誌的 CDC:
  • 缺點 : 實現比較複雜
  • 優點 : 但是可以實時消費日誌,流式處理,可保證資料一致性和實時性;

CDC 的優勢

  • 如今,大多數公司仍然使用批處理在系統之間同步資料。使用批處理:
  • 資料未立即同步
  • 更多分配的資源用於同步資料庫
  • 資料複製僅在指定的批次期間發生
  • 然而,變更資料捕獲具有一些優勢:
  • 不斷跟蹤源資料庫的變化
  • 即時更新目標資料庫
  • 使用流處理來保證即時更改

有了CDC, 不同的資料庫就會持續同步 ,批次任務已經成為過去。此外,由於 CDC 僅傳輸增量更改————因此,降低了傳輸資料的成本。

方案對比

目前市面上的CDC技術比較多,我們選取了幾種主要的開源CDC方案做了對比,總體如下圖:

從CDC機制、增量同步、斷電續傳、全量同步、全量+增量、架構、資料計算、生態這八個方面做了對比。可以看出其中的佼佼者主要是Flink CDC和Oracle OGG以及Debezium;

由於基於查詢的CDC方案缺陷明顯,這裡不作討論,下面我們對基於日誌的CDC方案的優劣來做詳細的介紹。

  • Flink CDCFlink CDC是最近幾年的新貴,Flink CDC 底層封裝了 Debezium,功能比較全面,目前已經迭代到了2.4版本,社群活躍度在幾個方案中是最高的;

  • 優點
  • 全、增量一體的分散式資料整合框架;
  • 同步時無需加鎖;
  • 吞吐量大,適合海量資料實時同步;
  • 操作簡單,SQL即可完成;
  • 具有強大的 transformation 能力,透過 Flink SQL 即可完成ETL 中的資料轉換;
  • 有豐富的 Connector,除關係型資料庫外,HBase、ClickHouse、TiDB等也支援,而且支援自定義 connector;
  • 缺點:依賴Flink叢集,資料量較大時對伺服器要求較高;

Oracle OGG

  • Oracle OGG:Oracle OGG 歷史比較悠久,最初是設計用來從Oracle遷移資料到其它資料庫,或者從其它平臺遷移資料到Oracle,隨著發展,目前已支援 Mysql、Hadoop、Hive、Kafka 等資料來源;

  • 優點:
  • 支援增量和全量同步
  • 支援分散式
  • 高效能
  • 支援資料過濾和轉化,是目前主流的實時同步方案之一;
  • 缺點:支援的資料庫比較少,像一些MongoDB、TiDB等不支援;

Debezium

  • Debezium
  • Debezium最初設計成一個Kafka ConnectSource Plugin
  • 目前開發者雖致力於將其與Kafka Connect解耦,但當前的程式碼實現還未變動。

下圖引自Debeizum官方文件,可以看到一個Debezium在一個完整CDC系統中的位置。

  • 優點:
  • 支援全量+增量同步;
  • 缺點:
  • 全量同步時會加鎖,而且加鎖時間不確定,會嚴重影響業務;
  • 最重要的是跟Kafka等訊息中介軟體強耦合,下游資料要經過Kafka;

Canal

  • Canal:主要用途是基於 MySQL 資料庫增量日誌解析,提供增量資料訂閱和消費。

  • 優點:用於單一的MySQL環境做資料同步還不錯;

  • 缺點:

  • 缺點較為明顯,只支援MySQL的CDC,只支援增量同步,全量需要用DataX或者Sqoop,全量和增量同步割裂;
  • 不支援分散式;

Debezium 平臺

什麼是 Debezium ?

  • 官網
  • https://debezium.io
  • https://github.com/debezium
  • https://github.com/debezium/debezium
  • https://github.com/debezium/debezium-ui
  • https://github.com/debezium/debezium-examples
  • https://github.com/debezium/debezium-examples/tree/main/tutorial
  • 官方的口號與定位

從資料庫流式傳輸更改。
Debezium 是一個用於變更資料捕獲的開源分散式平臺。
啟動它,將它指向你的資料庫,你的應用程式就可以開始響應其他應用程式提交給你的資料庫的所有插入、更新和刪除操作。
Debezium 耐用且快速,因此您的應用程式可以快速響應,即使出現問題也不會錯過事件。

  • Debezium 是一個構建在Apache Kafka之上的 CDC 開源平臺。

它的主要用途是在事務日誌中,記錄提交給每個源資料庫表的所有行級更改
偵聽這些事件的每個應用程式都可以根據增量資料更改執行所需的操作。

  • Debezium 提供了一個聯結器庫支援多種資料庫

例如 MySQL、MongoDB、PostgreSQL 等。

  • 這些聯結器可以監視記錄資料庫更改、並將其釋出到 Kafka 等流服務。

  • 此外, 即使我們的應用程式出現故障,Debezium 也會進行監控 。

重新啟動後,它將開始消耗上次停止的事件,因此不會丟失任何內容。

Debezium架構

  • 部署 Debezium 取決於我們擁有的基礎設施,但更常見的是,我們經常使用 Apache Kafka Connect

  • Kafka Connect 是一個框架,與 Kafka 代理一起作為單獨的服務執行。我們用它在 Apache Kafka 和其他系統之間傳輸資料。

  • 我們還可以定義聯結器來將資料傳入和傳出 Kafka。

下圖顯示了基於 Debezium 的變更資料捕獲管道的不同部分:

  • 首先,在左側,我們有一個 MySQL 源資料庫,我們希望將其資料複製並在目標資料庫(如 PostgreSQL 或任何分析資料庫)中使用。
  • 其次, Kafka Connect 聯結器解析並解釋事務日誌並將其寫入 Kafka 主題。
  • 接下來,Kafka 充當訊息代理,將變更集可靠地傳輸到目標系統。
  • 然後,在右側,我們有 Kafka 聯結器輪詢 Kafka 並將更改推送到目標資料庫。
  • **Debezium 在其架構中使用 Kafka **,但它還提供其他部署方法來滿足我們的基礎設施需求。

我們可以將其用作 Debezium 伺服器的獨立伺服器,也可以將其作為庫嵌入到我們的應用程式程式碼中。

我們將在以下部分中看到這些方法。

Debezium 伺服器

  • Debezium 提供了一個獨立的伺服器 來捕獲源資料庫的更改。它配置為使用 Debezium 源聯結器之一。

此外,這些聯結器將更改事件傳送到各種訊息基礎設施,例如 Amazon KinesisGoogle Cloud Pub/Sub

嵌入式 Debezium

  • Kafka Connect 在用於部署 Debezium 時提供容錯能力和可擴充套件性。

然而,有時我們的應用程式不需要這種級別的可靠性,並且我們希望最大限度地降低基礎設施的成本。

值得慶幸的是, 我們可以透過將 Debezium 引擎嵌入到我們的應用程式中來做到這一點。
完成此操作後,我們必須配置聯結器。

案例:基於 Debezium 的 Spring Boot CDC 應用程式

需求及架構

  • 為了使我們的應用程式保持簡單,我們將建立一個用於客戶管理的 Spring Boot 應用程式。

customer表模型有 ID 、 全名 和 電子郵件 欄位。

對於資料訪問層,使用 Spring Data JPA

最重要的是,應用程式將執行 Debezium 的嵌入式版本。

想象一下這個應用程式的架構:

首先,Debezium 引擎將跟蹤源 MySQL 資料庫(來自另一個系統或應用程式)上的 customer 表的事務日誌。

其次,每當我們對 customer 表執行插入/更新/刪除等資料庫操作時,Debezium 聯結器都會呼叫一個服務方法。

最後,根據這些事件,該方法會將 customer 表的資料同步到目標 MySQL 資料庫(我們應用程式的主資料庫)。

Maven 依賴項

  • 讓我們首先將 所需的依賴項 新增到 pom.xml 中:
<dependency>
    <groupId>io.debezium</groupId>
    <artifactId>debezium-api</artifactId>
    <version>1.4.2.Final</version>
</dependency>
<dependency>
    <groupId>io.debezium</groupId>
    <artifactId>debezium-embedded</artifactId>
    <version>1.4.2.Final</version>
</dependency>
  • 同樣,我們為應用程式將使用的每個 Debezium 聯結器新增依賴項。

  • 在我們的例子中,我們將使用 MySQL 聯結器:

<dependency>
    <groupId>io.debezium</groupId>
    <artifactId>debezium-connector-mysql</artifactId>
    <version>1.4.2.Final</version>
</dependency>

安裝資料庫

  • 我們可以手動安裝和配置我們的資料庫。但是,為了加快速度,我們將使用 docker-compose 檔案:
version: "3.9"
services:
  # Install Source MySQL DB and setup the Customer database
  mysql-1:
    container_name: source-database
    image: mysql
    ports:
      - 3305:3306
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_USER: user
      MYSQL_PASSWORD: password
      MYSQL_DATABASE: customerdb

  # Install Target MySQL DB and setup the Customer database
  mysql-2:
    container_name: target-database
    image: mysql
    ports:
      - 3306:3306
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_USER: user
      MYSQL_PASSWORD: password
      MYSQL_DATABASE: customerdb

  • 該檔案將在不同埠上執行兩個資料庫例項。

  • 我們可以使用命令 docker-compose up -d 執行此檔案。

建表 : customer

  • 現在,讓我們透過執行 SQL 指令碼來建立 customer 表:
CREATE TABLE customer
(
    id integer NOT NULL,
    fullname character varying(255),
    email character varying(255),
    CONSTRAINT customer_pkey PRIMARY KEY (id)
);
  • 在本節中,我們將配置 Debezium MySQL 聯結器、並瞭解如何執行嵌入式 Debezium 引擎

配置 Debezium 聯結器

  • 為了配置 Debezium MySQL 聯結器,我們將建立一個 Debezium 配置 bean:
@Bean
public io.debezium.config.Configuration customerConnector() {
    return io.debezium.config.Configuration.create()
        .with("name", "customer-mysql-connector")
        .with("connector.class", "io.debezium.connector.mysql.MySqlConnector")
        .with("offset.storage", "org.apache.kafka.connect.storage.FileOffsetBackingStore")
        .with("offset.storage.file.filename", "/tmp/offsets.dat")
        .with("offset.flush.interval.ms", "60000")
        .with("database.hostname", customerDbHost)
        .with("database.port", customerDbPort)
        .with("database.user", customerDbUsername)
        .with("database.password", customerDbPassword)
        .with("database.dbname", customerDbName)
        .with("database.include.list", customerDbName)
        .with("include.schema.changes", "false")
        .with("database.server.id", "10181")
        .with("database.server.name", "customer-mysql-db-server")
        .with("database.history", "io.debezium.relational.history.FileDatabaseHistory")
        .with("database.history.file.filename", "/tmp/dbhistory.dat")
        .build();
}

讓我們更詳細地檢查此配置。

該 bean 中的 create 方法 使用構建器來建立 Properties 物件 。

無論首選聯結器如何,此構建器都會設定引擎所需的多個屬性。為了跟蹤源 MySQL 資料庫,我們使用 MySqlConnector 類。

當此聯結器執行時,它開始跟蹤源中的更改並記錄“偏移量”以確定 它從事務日誌中處理了多少資料 。

有多種方法可以儲存這些偏移量,但在本例中,我們將使用類 FileOffsetBackingStore 在本地檔案系統上儲存偏移量。

聯結器的最後幾個引數是 MySQL 資料庫屬性。

現在我們已經有了配置,我們可以建立我們的引擎了。

配置、執行 Debezium 引擎

  • DebeziumEngine 充當我們的 MySQL 聯結器的包裝器。讓我們使用聯結器配置建立引擎:
private DebeziumEngine<RecordChangeEvent<SourceRecord>> debeziumEngine;

public DebeziumListener(Configuration customerConnectorConfiguration, CustomerService customerService) {

    this.debeziumEngine = DebeziumEngine.create(ChangeEventFormat.of(Connect.class))
      .using(customerConnectorConfiguration.asProperties())
      .notifying(this::handleEvent)
      .build();

    this.customerService = customerService;
}

更重要的是,引擎將為每個資料更改呼叫一個方法 - 在我們的示例中為 handleChangeEvent

在此方法中,首先, 我們將根據 呼叫 create() 時指定的格式解析每個事件。

然後,我們找到我們進行的操作並呼叫 CustomerService 在目標資料庫上執行建立/更新/刪除功能:

private void handleChangeEvent(RecordChangeEvent<SourceRecord> sourceRecordRecordChangeEvent) {
    SourceRecord sourceRecord = sourceRecordRecordChangeEvent.record();
    Struct sourceRecordChangeValue= (Struct) sourceRecord.value();

    if (sourceRecordChangeValue != null) {
        Operation operation = Operation.forCode((String) sourceRecordChangeValue.get(OPERATION));

        if(operation != Operation.READ) {
            String record = operation == Operation.DELETE ? BEFORE : AFTER;
            Struct struct = (Struct) sourceRecordChangeValue.get(record);
            Map<String, Object> payload = struct.schema().fields().stream()
              .map(Field::name)
              .filter(fieldName -> struct.get(fieldName) != null)
              .map(fieldName -> Pair.of(fieldName, struct.get(fieldName)))
              .collect(toMap(Pair::getKey, Pair::getValue));

            this.customerService.replicateData(payload, operation);
        }
    }
}

現在我們已經配置了 DebeziumEngine 物件,讓我們使用服務執行器非同步啟動它:

private final Executor executor = Executors.newSingleThreadExecutor();

@PostConstruct
private void start() {
    this.executor.execute(debeziumEngine);
}

@PreDestroy
private void stop() throws IOException {
    if (this.debeziumEngine != null) {
        this.debeziumEngine.close();
    }
}

執行

要檢視我們的程式碼的實際效果,讓我們對源資料庫的 customer 表進行一些資料更改。

Step1 插入記錄

  • 要將新記錄新增到 customer 表中,我們將進入 MySQL shell 並執行:
INSERT INTO customerdb.customer (id, fullname, email) VALUES (1, 'John Doe', '[email protected]')
  • 執行此查詢後,我們將看到應用程式的相應輸出:
23:57:57.897 [pool-1-thread-1] INFO  c.b.l.d.listener.DebeziumListener - Key = 'Struct{id=1}' value = 'Struct{after=Struct{id=1,fullname=John Doe,[email protected]},source=Struct{version=1.4.2.Final,connector=mysql,name=customer-mysql-db-server,ts_ms=1617746277000,db=customerdb,table=customer,server_id=1,file=binlog.000007,pos=703,row=0,thread=19},op=c,ts_ms=1617746277422}'
Hibernate: insert into customer (email, fullname, id) values (?, ?, ?)
23:57:58.095 [pool-1-thread-1] INFO  c.b.l.d.listener.DebeziumListener - Updated Data: {fullname=John Doe, id=1, [email protected]} with Operation: CREATE
  • 最後,我們檢查一條新記錄是否已插入到我們的目標資料庫中:
id  fullname   email
1  John Doe   [email protected]

Step2 更新記錄

  • 現在,讓我們嘗試更新最後插入的客戶並檢查會發生什麼:
UPDATE customerdb.customer t SET t.email = '[email protected]' WHERE t.id = 1
  • 之後,我們將得到與插入相同的輸出,除了操作型別更改為“UPDATE”,當然,Hibernate 使用的查詢是“更新”查詢:
00:08:57.893 [pool-1-thread-1] INFO  c.b.l.d.listener.DebeziumListener - Key = 'Struct{id=1}' value = 'Struct{before=Struct{id=1,fullname=John Doe,[email protected]},after=Struct{id=1,fullname=John Doe,[email protected]},source=Struct{version=1.4.2.Final,connector=mysql,name=customer-mysql-db-server,ts_ms=1617746937000,db=customerdb,table=customer,server_id=1,file=binlog.000007,pos=1040,row=0,thread=19},op=u,ts_ms=1617746937703}'
Hibernate: update customer set email=?, fullname=? where id=?
00:08:57.938 [pool-1-thread-1] INFO  c.b.l.d.listener.DebeziumListener - Updated Data: {fullname=John Doe, id=1, [email protected]} with Operation: UPDATE
  • 我們可以驗證目標資料庫中約翰的電子郵件已更改:
id  fullname   email
1  John Doe   [email protected]

Step3 刪除記錄

現在,我們可以透過執行以下命令刪除 客戶 表中的條目:

DELETE FROM customerdb.customer WHERE id = 1

同樣,這裡我們操作發生變化,再次查詢:

00:12:16.892 [pool-1-thread-1] INFO  c.b.l.d.listener.DebeziumListener - Key = 'Struct{id=1}' value = 'Struct{before=Struct{id=1,fullname=John Doe,[email protected]},source=Struct{version=1.4.2.Final,connector=mysql,name=customer-mysql-db-server,ts_ms=1617747136000,db=customerdb,table=customer,server_id=1,file=binlog.000007,pos=1406,row=0,thread=19},op=d,ts_ms=1617747136640}'
Hibernate: delete from customer where id=?
00:12:16.951 [pool-1-thread-1] INFO  c.b.l.d.listener.DebeziumListener - Updated Data: {fullname=John Doe, id=1, [email protected]} with Operation: DELETE

我們可以驗證目標資料庫上的資料已被刪除:

select * from customerdb.customer where id= 1
0 rows retrieved

Y 推薦文獻

  • [大資料] ETL之增量資料抽取(CDC) - 部落格園/千千寰宇
  • [資料庫] 淺談mysql的serverId/serverUuid - 部落格園/千千寰宇
  • [資料庫] MYSQL之binlog概述 - 部落格園/千千寰宇
  • CDC問題 - 常見問題 - 實時計算Flink版 - Aliyun

X 參考文獻

  • Flink CDC、OGG、Debezium等基於日誌開源CDC方案對比 - CSDN
  • Debezium入門介紹 - baeldung-cn.com

相關文章