Debezium引擎
通常,通过将Debezium连接器部署到Kafka Connect服务并配置一个或多个连接器来监视上游数据库并为它们在上游数据库中看到的所有更改生成数据更改事件来进行操作。这些数据更改事件被写入Kafka,在那里它们可以被许多不同的应用程序独立使用。Kafka Connect具有出色的容错性和可扩展性,因为它作为分布式服务运行,并确保所有已注册和配置的连接器始终处于运行状态。例如,即使集群中的一个Kafka Connect端点发生故障,其余的Kafka Connect端点也将重新启动以前在现在终止的端点上运行的所有连接器,从而最大程度地减少了停机时间并消除了管理活动。
并非每个应用程序都需要这种级别的容错性和可靠性,并且他们可能不希望依赖于Kafka代理和Kafka Connect服务的外部集群。相反,某些应用程序希望将 Debezium连接器直接嵌入应用程序空间内。他们仍然想要相同的数据更改事件,但是更喜欢让连接器将它们直接发送到应用程序,而不是将其持久保存在Kafka中。
该debezium-api模块定义了一个小的API,该API允许应用程序使用Debezium Engine轻松配置和运行Debezium连接器。
依赖关系
要使用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版本或Maven属性(其值包含Debezium版本字符串)。
同样,为应用程序将使用的每个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连接器嵌入应用程序中。除了特定于连接器的配置,主题和事件以外,其他连接器的使用方式也相似。
编码
您的应用程序需要为要运行的每个连接器实例设置一个嵌入式引擎。该io.debezium.engine.DebeziumEngine班作为各地的任何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<SourceRecord> engine = DebeziumEngine.create(Connect.class)
.using(props)
.notifying(record -> {
System.out.println(record);
}).build()
) {
Executors.newSingleThreadExecutor().execute(engine);
// 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 Connect org.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和232-1。在我们的代码中,我们将其设置为一个较大但有些随机的值,仅用于我们的应用程序。
该配置还指定了MySQL服务器的逻辑名称。连接器在其产生的每个源记录的主题字段中都包含此逻辑名,使您的应用程序可以识别这些记录的来源。我们的示例使用服务器名称“ products”,大概是因为数据库包含产品信息。当然,您可以为应用程序命名任何有意义的名称。
当MySqlConnector类运行时,它将读取MySQL服务器的binlog,其中包括对服务器托管的数据库的所有数据更改和架构更改。由于记录数据时所有数据更改都是根据拥有表的架构进行结构化的,因此连接器需要跟踪所有架构更改,以便它可以正确解码更改事件。连接器记录模式信息,以便在重新启动连接器并从最后记录的偏移开始恢复读取时,连接器将确切知道该偏移处的数据库模式。连接器如何记录数据库架构历史记录在配置的最后两个字段中定义,即我们的连接器应使用FileDatabaseHistory该类将数据库架构历史记录更改存储在配置文件中。/path/to/storage/dbhistory.dat 本地文件系统上的文件(同样,该文件可以命名为任何文件并存储在任何地方)。
最终,使用该build()方法构建了不可变的配置。(顺便说一句,我们可以使用一种方法从属性文件中读取配置,而不是通过编程方式构建它Configuration.read(…)。)
现在我们有了配置,我们可以创建引擎了。这又是相关的代码行:
// Create the engine with this configuration ...
try (DebeziumEngine<SourceRecord> engine = DebeziumEngine.create(Connect.class)
.using(props)
.notifying(record -> {
System.out.println(record);
}).build()) {
}
流利风格的构建器API用于创建使用我们的Properties对象的引擎,并将所有数据更改记录发送到该handleEvent(SourceRecord)方法,该方法可以是与java.util.function.Consumer功能接口的签名相匹配的任何方法,该类SourceRecord为org.apache.kafka.connect.source.SourceRecord类。这个例子专门讲,SourceRecord但是引擎的实现是参数化的,将来还会有不同的类型,例如JSON或Avro可能由引擎退回。注意,应用程序的处理程序函数不应引发任何异常。如果是这样,引擎将记录该方法引发的任何异常并将继续对下一个源记录进行操作,但是您的应用程序将没有其他机会处理导致异常的特定源记录,这意味着您的应用程序可能变得不一致与数据库。
至此,我们有一个已DebeziumEngine配置并可以运行的现有对象,但是它什么也没做。将DebeziumEngine被设计成由一个异步执行Executor或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离开该块时,它将被自动调用。
引擎的连接器将停止从源系统中读取信息,将所有剩余的SourceRecord对象转发到处理程序功能,并将最新的信息刷新到偏移量存储中。只有完成所有这些操作,引擎的run()方法才会返回。如果您的应用程序需要等待引擎完全停止才能退出,则可以使用ExcecutorService shutdown和awaitTermination方法:
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,因为那样会中断正在运行的线程。尽管DebeziumEnginewill确实在其线程中断时终止,但是引擎可能无法干净终止,并且在重新启动您的应用程序时,它可能会看到一些与关闭之前处理过的相同的源记录。
更高级的记录消费
对于某些用例,例如,当尝试批量写入记录或针对异步API编写记录时,上述功能接口可能具有挑战性。在这些情况下,使用该io.debezium.engine.DebeziumEngine.ChangeConsumer.界面可能会更容易。
此接口具有单个功能,带有以下签名:
/**
* Handles a batch of records, calling the {@link RecordCommitter#markProcessed(SourceRecord)}
* 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<SourceRecord> records, RecordCommitter committer) throws InterruptedException;
如Javadoc中所述,RecordCommitter将为每个记录调用对象,并在每个批处理完成后调用该对象。该RecordCommitter接口是线程安全的,它允许灵活地处理记录。
要使用ChangeConsumerAPI,您必须将接口的实现传递给notifyingAPI,如下所示:
class MyChangeConsumer implements EmbeddedEngine.ChangeConsumer {
public void handleBatch(List<SourceRecord> records, RecordCommitter committer) throws InterruptedException {
...
}
}
// Create the engine with this configuration ...
DebeziumEngine<SourceRecord> engine = DebeziumEngine.create(Connect.class)
.using(props)
.notifying(new MyChangeConsumer())
.build();
引擎参数
除非提供默认值,否则以下配置属性是必需的(为了文本格式化,Java类的包名称被替换为<…>)。
属性 | 默认 | 描述 |
---|---|---|
name | 连接器实例的唯一名称。 | |
connector.class | 连接器(例如<…>.MySqlConnectorMySQL连接器)的Java类的名称 。 | |
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.ms | 60000 | 尝试提交偏移量的时间间隔。默认值为1分钟。 |
offset.flush.timeout.ms | 5000 | 在取消进程并恢复要在以后尝试中提交的偏移数据之前,等待记录刷新并将分区数据提交给偏移存储的最大毫秒数。默认值为5秒。 |
internal.key.converter | <…>.JsonConverter | 应该用于序列化和反序列化偏移量的关键数据的Converter类。默认值为JSON转换器。 |
internal.value.converter | <…>.JsonConverter | 应该用于对偏移量的值数据进行序列化和反序列化的Converter类。默认值为JSON转换器。 |
失败的处理
引擎执行时,其连接器正在将源偏移量主动记录在每个源记录内,并且引擎会定期将这些偏移量刷新到持久性存储中。当应用程序和引擎正常关闭或崩溃时,当它们重新启动时,引擎及其连接器将恢复从最后记录的offset中读取源信息。
那么,当嵌入式引擎运行时您的应用程序失败时会发生什么呢?最终结果是,应用程序可能会在重启后收到一些源记录,这些记录是崩溃之前就已经处理过的。有多少取决于引擎(通过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平台,该平台可以提供一次准确的保证(即使在崩溃并重新启动之后)。