Debezium Engine 使用入门

3,604 阅读7分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

Debezium Engine

通常将Debezium connectors 部署到 Kafka Connect 服务中,并配置一个或多个连接器来监控上游数据库 ,这些连接器会捕获上游数据库中所有更改后生成数据更改事件。这些数据更改事件被写入 Kafka,在那里它们可以被许多不同的应用程序独立使用。Kafka Connect 提供出色的容错性和可扩展性,因为它作为分布式服务运行,并确保所有已注册和配置的连接器始终运行。例如,即使集群中的一个 Kafka Connect 端点出现故障,其余的 Kafka Connect 端点也会重新启动之前在现在终止的端点上运行的任何连接器,从而最大限度地减少停机时间并消除管理活动。

并非每个应用程序都需要这种级别的容错和可靠性,他们可能不想依赖外部的 Kafka 代理集群和 Kafka Connect 服务。相反,一些应用程序更愿意将Debezium 连接器直接嵌入到应用程序空间中。他们仍然想要相同的数据更改事件,但更愿意让连接器将它们直接发送到应用程序,而不是在 Kafka 中持久化它们。

debezium-api模块定义了一个小型 API,允许应用程序使用 Debezium Engine 轻松配置和运行 Debezium 连接器。

1. 依赖项

要使用 Debezium Engine 模块,请将debezium-api模块添加到应用程序的依赖项中。模块中有一个开箱即用的 API 实现,debezium-embedded也应该添加到依赖项中。对于 Maven,这需要将以下内容添加到应用程序的 POM:

<dependency>
    <groupId>io.debezium</groupId>
    <artifactId>debezium-api</artifactId>
    <version>${version.debezium}</version>
</dependency>
<dependency>
    <groupId>io.debezium</groupId>
    <artifactId>debezium-embedded</artifactId>
    <version>${version.debezium}</version>
</dependency>

其中${version.debezium}是您正在使用的 Debezium 版本,或者是其值包含 Debezium 版本字符串的 Maven 属性。

同样,为您的应用程序将使用的每个 Debezium 连接器添加依赖项。例如,可以将以下内容添加到应用程序的 Maven POM 文件中,以便应用程序可以使用 MySQL 连接器:

<dependency>
    <groupId>io.debezium</groupId>
    <artifactId>debezium-connector-mysql</artifactId>
    <version>${version.debezium}</version>
</dependency>

或者对于 MongoDB 连接器:

<dependency>
    <groupId>io.debezium</groupId>
    <artifactId>debezium-connector-mongodb</artifactId>
    <version>${version.debezium}</version>
</dependency>

本文档的其余部分描述了在您的应用程序中嵌入 MySQL 连接器。除了特定于连接器的配置、主题和事件之外,其他连接器的使用方式类似。

2. 代码样例

您的应用程序需要为您要运行的每个连接器实例设置一个嵌入式引擎。该类io.debezium.engine.DebeziumEngine<R>用作任何 Debezium 连接器的易于使用的包装器,并完全管理连接器的生命周期。您DebeziumEngine使用其构建器 API 创建实例,提供以下内容:

  • 您希望接收消息的格式,例如 JSON、Avro 或 Kafka Connect SourceRecord (请参阅输出消息格式

  • 定义引擎和连接器环境的配置属性(可能从属性文件加载)

  • 为连接器产生的每个数据更改事件调用的方法

这是配置和运行嵌入式MySQL 连接器的代码示例:

// Define the configuration for the Debezium Engine with MySQL connector...
final Properties props = config.asProperties();
props.setProperty("name", "engine");
props.setProperty("offset.storage", "org.apache.kafka.connect.storage.FileOffsetBackingStore");
props.setProperty("offset.storage.file.filename", "/tmp/offsets.dat");
props.setProperty("offset.flush.interval.ms", "60000");
/* begin connector properties */
props.setProperty("database.hostname", "localhost");
props.setProperty("database.port", "3306");
props.setProperty("database.user", "mysqluser");
props.setProperty("database.password", "mysqlpw");
props.setProperty("database.server.id", "85744");
props.setProperty("database.server.name", "my-app-connector");
props.setProperty("database.history",
      "io.debezium.relational.history.FileDatabaseHistory");
props.setProperty("database.history.file.filename",
      "/path/to/storage/dbhistory.dat");

// Create the engine with this configuration ...
try (DebeziumEngine<ChangeEvent<String, String>> engine = DebeziumEngine.create(Json.class)
        .using(props)
        .notifying(record -> {
            System.out.println(record);
        }).build()
    ) {
    // Run the engine asynchronously ...
    ExecutorService executor = Executors.newSingleThreadExecutor();
    executor.execute(engine);

    // Do something else or wait for a signal or an event
}
// Engine is stopped when the main code is finished

让我们更详细地看一下这段代码,从前几行开始:

// Define the configuration for the Debezium Engine with MySQL connector...
final Properties props = config.asProperties();
props.setProperty("name", "engine");
props.setProperty("connector.class", "io.debezium.connector.mysql.MySqlConnector");
props.setProperty("offset.storage", "org.apache.kafka.connect.storage.FileOffsetBackingStore");
props.setProperty("offset.storage.file.filename", "/tmp/offsets.dat");
props.setProperty("offset.flush.interval.ms", 60000);

这将创建一个新的Properties对象来设置引擎所需的多个字段。第一个是引擎的名称,将在连接器生成的源记录及其内部状态中使用。该connector.class字段定义了扩展Kafka Connectorg.apache.kafka.connect.source.SourceConnector抽象类的类的名称;在这个例子中,我们指定了 Debezium 的MySqlConnector类。

当 Kafka Connect 连接器运行时,它会从源数据库读取信息并定期记录“偏移量”,这些“偏移量”定义了它处理了多少信息。如果连接器重新启动,它将使用最后记录的偏移量来了解它应该从源信息中的哪个位置继续读取。由于连接器不知道也不关心偏移量是如何存储的,因此引擎需要提供一种方法来存储和恢复这些偏移量。我们配置的接下来的几个字段指定我们的引擎应该使用FileOffsetBackingStore该类将偏移量存储在/path/to/storage/offset.dat本地文件系统上的文件(该文件可以任意命名并存储在任何地方)。此外,虽然连接器使用它生成的每个源记录来记录偏移量,但引擎会定期将偏移量刷新到后备存储(在我们的例子中,每分钟一次)。这些字段可以根据您的应用程序的需要进行定制。

接下来的几行定义特定于连接器的字段,在我们的示例中是MySqlConnector连接器:

    /* begin connector properties */
    props.setProperty("database.hostname", "localhost")
    props.setProperty("database.port", "3306")
    props.setProperty("database.user", "mysqluser")
    props.setProperty("database.password", "mysqlpw")
    props.setProperty("database.server.id", "85744")
    props.setProperty("database.server.name", "my-app-connector")
    props.setProperty("database.history",
          "io.debezium.relational.history.FileDatabaseHistory")
    props.setProperty("database.history.file.filename",
          "/path/to/storage/dbhistory.dat")

在这里,我们设置了运行 MySQL 数据库服务器的主机名称和端口号,并定义了用于连接 MySQL 数据库的用户名和密码。请注意,对于 MySQL,用户名和密码应对应于已被授予以下 MySQL 权限的 MySQL 数据库用户:

  • SELECT

  • RELOAD

  • SHOW DATABASES

  • REPLICATION SLAVE

  • REPLICATION CLIENT

读取数据库的一致快照时需要前三个权限。最后两个权限允许数据库读取通常用于 MySQL 复制的服务器的 binlog。

该配置还包括server.id. 由于 MySQL 的 binlog 是 MySQL 复制机制的一部分,为了读取 binlog,MySqlConnector实例必须加入 MySQL 服务器组,这意味着该服务器 ID在组成 MySQL 服务器组的所有进程中必须是唯一的,并且是介于两者之间的任何整数1 和 2的32方 -1。在我们的代码中,我们将它设置为一个相当大但有点随机的值,我们将只用于我们的应用程序。

该配置还指定 MySQL 服务器的逻辑名称。连接器在它生成的每个源记录的主题字段中包含这个逻辑名称,使您的应用程序能够识别这些记录的来源。我们的示例使用“products”的服务名称,大概是因为数据库包含产品信息。当然,您可以将其命名为对您的应用程序有意义的任何名称。

MySqlConnector该类运行时,它会读取 MySQL 服务器的 binlog,其中包括对服务器托管的数据库所做的所有数据更改和schema更改。由于对数据的所有更改都是根据记录更改时所属表的schema来构建的,因此连接器需要跟踪所有schema更改,以便它可以正确解码更改事件。连接器记录模式信息,以便连接器重新启动并从上次记录的偏移量继续读取,它确切地知道数据库模式在该偏移量处的状态。连接器如何记录数据库模式历史记录在我们配置的最后两个字段中定义,即我们的连接器应该使用FileDatabaseHistory该类来存储数据库模式历史更改在/path/to/storage/dbhistory.dat本地文件系统上的文件(同样,这个文件可以任意命名并存储在任何地方)。

最后使用该build()方法构建配置。(顺便说一句,我们可以使用其中一种方法从属性文件中读取配置,而不是以编程方式构建它Configuration.read(…​)。)

现在我们有了配置,我们可以创建我们的引擎。这里再次是相关的代码行:

// Create the engine with this configuration ...
try (DebeziumEngine<ChangeEvent<String, String>> engine = DebeziumEngine.create(Json.class)
        .using(props)
        .notifying(record -> {
            System.out.println(record);
        })
        .build()) {
}

所有更改事件都将传递给给定的处理方法,该方法必须与java.util.function.Consumer<R>功能接口的签名相匹配,其中<R>必须与调用时指定的格式的类型相匹配create()。请注意,您的应用程序的处理函数不应抛出任何异常;如果是这样,引擎将记录该方法抛出的任何异常,并将继续对下一个源记录进行操作,但您的应用程序将没有机会处理导致异常的特定源记录,这意味着您的应用程序可能会变得与数据库不一致。

此时,我们有一个已DebeziumEngine配置并准备好运行的现有对象,但它不执行任何操作。被DebeziumEngine设计为由Executoror异步执行ExecutorService

// Run the engine asynchronously ...
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(engine);

// Do something else or wait for a signal or an event

close()您的应用程序可以通过调用其方法来安全、优雅地停止引擎:

// At some later time ...
engine.close();

或者由于引擎支持Closeable接口,它会在try离开块时自动调用。

引擎的连接器将停止从源系统读取信息,将所有剩余的更改事件转发到您的处理函数,并将最新的offets 刷新到偏移存储。只有在所有这些完成之后,引擎的run()方法才会返回。如果您的应用程序需要在退出之前等待引擎完全停止,您可以使用ExcecutorService shutdownandawaitTermination方法执行此操作:

try {
    executor.shutdown();
    while (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
        logger.info("Waiting another 5 seconds for the embedded engine to shut down");
    }
}
catch ( InterruptedException e ) {
    Thread.currentThread().interrupt();
}

或者,您可以CompletionCallback在创建时注册DebeziumEngine为回调,以便在引擎终止时得到通知。

回想一下,当 JVM 关闭时,它只等待守护线程。因此,如果您的应用程序退出,请务必等待引擎完成,或者在守护线程上运行引擎。

您的应用程序应始终正确停止引擎,以确保正常和完全关闭,并且每个源记录都准确地发送到应用程序一次。例如,不要依赖关闭ExecutorService,因为这会中断正在运行的线程。虽然DebeziumEngine它的线程被中断时确实会终止,但引擎可能不会完全终止,并且当您的应用程序重新启动时,它可能会看到一些与它在关闭之前处理过的相同的源记录。

3. 输出消息格式

DebeziumEngine#create()可以接受多个不同的参数,这些参数会影响消费者接收消息的格式。允许的值为:

  • Connect.class- 输出值是包装 Kafka Connect 的更改事件SourceRecord

  • Json.class- 输出值是一对编码为JSON字符串的键和值

  • Avro.class- 输出值是一对编码为 Avro 序列化记录的键和值(有关详细信息,请参阅Avro 序列化)

  • CloudEvents.class- 输出值是一对编码为云事件消息的键和值

在内部,引擎使用委托转换的适当 Kafka Connect 转换器实现。可以使用引擎属性对转换器进行参数化以修改其行为。

JSON输出格式的一个例子是

final Properties props = new Properties();
...
props.setProperty("converter.schemas.enable", "false"); // don't include schema in message
...
final DebeziumEngine<ChangeEvent<String, String>> engine = DebeziumEngine.create(Json.class)
    .using(props)
    .notifying((records, committer) -> {

        for (ChangeEvent<String, String> r : records) {
            System.out.println("Key = '" + r.key() + "' value = '" + r.value() + "'");
            committer.markProcessed(r);
        }
...

其中ChangeEvent数据类型是键/值对。

4. 消息转换

在将消息传递给处理程序之前,可以通过 Kafka Connect 简单消息转换(SMT) 的管道运行它们。每个 SMT 都可以将消息原封不动地传递、修改或过滤掉。链是使用 property 配置的transforms。该属性包含要应用的转换的逻辑名称的逗号分隔列表。然后,属性transforms.<logical_name>.type定义每个转换的实现类的名称以及transforms.<logical_name>.*传递给转换的配置选项。

配置的一个例子是

final Properties props = new Properties();
...
props.setProperty("transforms", "filter, router");                                               // (1)
props.setProperty("transforms.router.type", "org.apache.kafka.connect.transforms.RegexRouter");  // (2)
props.setProperty("transforms.router.regex", "(.*)");                                            // (3)
props.setProperty("transforms.router.replacement", "trf$1");                                     // (3)
props.setProperty("transforms.filter.type", "io.debezium.embedded.ExampleFilterTransform");      // (4)
  1. 定义了两个转换 -filterrouter

  2. router转型的实施是org.apache.kafka.connect.transforms.RegexRouter

  3. 转换有router两个配置选项 -regexreplacement

  4. filter转型的实施是io.debezium.embedded.ExampleFilterTransform

5. 高级记录消费

对于某些用例,例如尝试批量写入记录或针对异步 API 时,上述功能接口可能具有挑战性。在这些情况下,使用该io.debezium.engine.DebeziumEngine.ChangeConsumer<R>.界面可能会更容易。

该接口具有单一功能,具有以下签名:

/**  * Handles a batch of records, calling the {@link RecordCommitter#markProcessed(Object)}  * for each record and {@link RecordCommitter#markBatchFinished()} when this batch is finished.  * @param records the records to be processed  * @param committer the committer that indicates to the system that we are finished  */
 void handleBatch(List<R> records, RecordCommitter<R> committer) throws InterruptedException;

如 Javadoc 中所述,RecordCommitter将为每条记录调用该对象,并且在每批完成后调用该对象。该RecordCommitter接口是线程安全的,允许灵活处理记录。

您可以选择覆盖已处理记录的偏移量。这是通过首先 Offsets调用构建一个新对象来完成的RecordCommitter#buildOffsets(),用 更新偏移量Offsets#set(String key, Object value),然后RecordCommitter#markProcessed(SourceRecord record, Offsets sourceOffsets)用更新的 调用Offsets

要使用ChangeConsumerAPI,您必须将接口的实现传递给notifyingAPI,如下所示:

class MyChangeConsumer implements DebeziumEngine.ChangeConsumer<RecordChangeEvent<SourceRecord>> {
  public void handleBatch(List<RecordChangeEvent<SourceRecord>> records, RecordCommitter<RecordChangeEvent<SourceRecord>> committer) throws InterruptedException {
    ...
  }
}
// Create the engine with this configuration ...
DebeziumEngine<RecordChangeEvent<SourceRecord>> engine = DebeziumEngine.create(ChangeEventFormat.of(Connect.class))
        .using(props)
        .notifying(new MyChangeConsumer())
        .build();

如果使用 JSON 格式(等效格式也适用于其他格式),则代码如下所示:

class JsonChangeConsumer implements DebeziumEngine.ChangeConsumer<ChangeEvent<String, String>> {
  public void handleBatch(List<ChangeEvent<String, String>> records,    RecordCommitter<ChangeEvent<String, String>> committer) throws InterruptedException {
    ...
  }
}
// Create the engine with this configuration ...
DebeziumEngine<ChangeEvent<String, String>> engine = DebeziumEngine.create(Json.class)
        .using(props)
        .notifying(new MyChangeConsumer())
        .build();

6. 引擎属性

除非有默认值可用,否则需要以下配置属性(为了兼容文本格式,Java 类的包名称被替换为<…​>)。

属性默认值描述
name连接器实例的唯一名称。
connector.class连接器的 Java 类的名称,例如 <…​>.MySqlConnectorMySQL 连接器。
offset.storage<…​>.FileOffsetBackingStore负责连接器偏移持久性的 Java 类的名称。它必须实现<…​>.OffsetBackingStore接口。
offset.storage.file.filename""要存储偏移量的文件的路径。需要时设置offset.storage<…​>.FileOffsetBackingStore
offset.storage.topic""要存储偏移量的 Kafka 主题的名称。需要时设置offset.storage<…​>.KafkaOffsetBackingStore
offset.storage.partitions""创建偏移存储主题时使用的分区数。需要时设置offset.storage<…​>.KafkaOffsetBackingStore
offset.storage.replication.factor""创建偏移存储主题时使用的复制因子。需要时设置offset.storage<…​>.KafkaOffsetBackingStore
offset.commit.policy<…​>.PeriodicCommitOffsetPolicy提交策略的 Java 类的名称。它根据处理的事件数量和自上次提交以来经过的时间定义何时必须触发偏移提交。这个类必须实现接口<…​>.OffsetCommitPolicy。默认是基于时间间隔的定期提交策略。
offset.flush.interval.ms60000尝试提交偏移量的时间间隔。默认值为 1 分钟。
offset.flush.timeout.ms5000在取消进程并在未来尝试中恢复要提交的偏移数据之前,等待记录刷新和分区偏移数据提交到偏移存储的最大毫秒数。默认值为 5 秒。
internal.key.converter<…​>.JsonConverter应该用于序列化和反序列化偏移量的关键数据的 Converter 类。默认为 JSON 转换器。
internal.value.converter<…​>.JsonConverter应该用于序列化和反序列化偏移值数据的 Converter 类。默认为 JSON 转换器。

Database history properties 数据库历史属性

一些连接器还需要额外的一组属性来配置数据库历史记录:

  • MySQL

  • SQL Server

  • Oracle

  • Db2

如果没有正确配置数据库历史,连接器将拒绝启动。默认配置要求 Kafka 集群可用。对于其他部署,可以使用基于文件的数据库历史存储实现。(为了兼容文本格式,Java 类的包名称被替换为<…>

属性默认值描述
database.history<…​>.KafkaDatabaseHistory负责数据库历史记录持久性的 Java 类的名称。
它必须实现<…​>.DatabaseHistory接口。
database.history.file.filename""存储数据库历史记录的文件的路径。需要时设置database.history
<…​>.FileDatabaseHistory
database.history.kafka.topic""存储数据库历史记录的 Kafka 主题。需要时设置database.history
<…​>.KafkaDatabaseHistory
database.history.kafka.bootstrap.servers""要连接的 Kafka 集群服务器的初始列表。集群提供存储数据库历史的主题。需要时设置database.history
为``<…​>.KafkaDatabaseHistory`

7. 处理故障

当引擎执行时,连接器会主动记录每个源记录中的源偏移量,并且引擎会定期将这些偏移量刷新到持久存储中。当应用程序和引擎正常关闭或崩溃时,或当它们重新启动时,引擎及其连接器将从上次记录的偏移量处恢复读取源信息。

那么,当您的应用程序在嵌入式引擎运行时失败时会发生什么?最终结果是应用程序可能会在重新启动后收到一些在崩溃前已经处理过的源记录。多少取决于引擎将偏移量刷新到其存储(通过offset.flush.interval.ms属性)的频率以及特定连接器在一批中返回的源记录的数量。最好的情况是每次都刷新偏移量(例如,offset.flush.interval.ms设置为 0),但即使这样,嵌入式引擎仍将仅在从连接器接收到每批源记录后才刷新偏移量。

例如,MySQL 连接器使用max.batch.size指定可以在批处理中出现的最大源记录数。即使offset.flush.interval.ms设置为 0,当应用程序在崩溃后重新启动时,它可能会看到多达n 个重复项,其中n是批次的大小。如果该offset.flush.interval.ms属性设置得更高,那么应用程序可能会看到最多n * m重复项,其中n是批次的最大大小,m是在单个偏移刷新间隔期间可能累积的批次数。(显然,可以将嵌入式连接器配置为不使用批处理并始终刷新偏移量,从而导致应用程序永远不会收到任何重复的源记录。但是,这会大大增加开销并降低连接器的吞吐量。)

底线是,当使用嵌入式连接器时,应用程序将在正常操作期间仅接收每个源记录一次(包括正常关闭后重新启动),但确实需要容忍在崩溃或不正确关闭后重新启动后立即接收重复事件. 如果应用程序需要更严格的精确一次行为,那么他们应该使用可以提供精确一次保证的完整 Debezium 平台(即使在崩溃和重启之后)。

点击查看原文: Debezium Engine