将外部系统与 Kafka Connect 集成

166 阅读16分钟

本章涵盖

  • 将 Kafka 与数据源和接收端集成
  • 为最佳数据流配置连接器和 worker
  • 使用 REST API 管理 Kafka Connect
  • 创建与修改连接器
  • 使用 JDBC 源和 Debezium 连接器

在大多数情况下,我们并不是孤立地引入 Kafka,而是希望将公司的其他系统(例如数据库和消息系统)与 Kafka 连接起来。大多数此类用例非常相似:我们希望将预定义数据库表的数据传到指定的 topic,或将某些 topic 的数据写入文件。当然,我们始终可以手动编写自己的 producer 和 consumer 来在系统之间移动数据。

然而,这样做既耗时又容易出错,且难以扩展。即便是很简单的需求(比如把数据从一个系统搬到另一个系统),也有很多特殊情况需要考虑。作为替代方案,我们可以使用 Kafka Connect。

11.1 什么是 Kafka Connect?

Kafka Connect 是一个框架,用于把第三方系统的数据写入 Kafka,也可将 Kafka 的数据传出到外部系统(见图 11.1)。它区分两类连接器:source 连接器(本质上是向 Kafka 写入的 producer,先从外部系统读取数据)和 sink 连接器(作为从 Kafka 读取并写入外部系统的 consumer)。作为一个框架,Kafka Connect 是 Apache Kafka 的一部分,与 Kafka 一样以 Apache 2.0 开源许可证发布。

image.png

Kafka Connect 的目标是提供一个用于数据迁移的标准化工具。像 Kafka 本身一样,Kafka Connect 可扩展且容错,并非常重视正确性与性能。现有的连接器覆盖了广泛的系统类型,包括 PostgreSQL、SQLite、MySQL 等数据库;Amazon S3、Azure Blob Storage 等云环境下的对象存储;MQTT(Message Queuing Telemetry Transport)、JMS(Java Message Service)等消息协议;以及 Snowflake、Amazon Redshift 等数据仓库系统。撰写本文时,仅 Confluent Hub 上就列出大约 200 个连接器,而且还有许多来自其他来源的连接器。

Kafka Connect 的核心思想是实现与外部数据源与接收端的无缝集成,尤其是那些可以表示为分区流(partitioned streams)的系统。也就是说,把数据划分为不同的流,从而能够横向扩展并高效处理大量数据。每个流包含相关联且不应再被进一步拆分的数据。在 Kafka 中,topic 会被分区以实现这一点,从而确保数据被合理地分发到多个消费者以供处理(见图 11.2)。

image.png

在数据库系统中,我们可以将单个表视为数据流——这些数据流不能轻易拆分,否则会丧失诸如顺序性等重要保证(如图 11.3 所示)。

首先,在文件系统中,这些数据流例如可以对应到各个独立的文件。其次,对于我们的分区化数据流,需要一种方式来跟踪哪些数据已经被读取、哪些尚未被读取。由于这些数据流可能非常长,我们无法简单地记住已经见过的每条消息。相反,像 Kafka 那样,我们使用偏移量(即数据流中的位置)来确定已经读取到哪个位置。在数据库中,我们可能使用自增 ID 或时间戳来跟踪某行的最后修改时间;在文件中,则可以记录上次读取的行号或字节位置。

image.png

11.2 Kafka Connect 集群:分布式模式

为了充分利用 Kafka Connect,通常会以所谓的 Distributed Mode(分布式模式) 运行它。在这种模式下,Kafka Connect 会将所有配置数据和内部偏移量存储在 Kafka 主题中。然后我们可以启动任意数量的 Kafka Connect 实例。所有具有相同 group.id 的 Kafka Connect 实例会组成一个 Kafka Connect 集群,它们使用我们在第 9 章 9.4 节讨论的相同 Kafka Rebalance 协议来协调并相互分配负载。

在开发环境以及某些特殊场景(例如需要访问本地文件)下,放弃这些分布式优势、改为以 Standalone Mode(独立模式) 运行 Kafka Connect 可能更为合适。在独立模式下,Kafka Connect 不会把内部数据(例如偏移量)存储到 Kafka,也不保留任何配置。我们只是把 Kafka Connect 当作本地机器上的一个程序运行,完成后直接停止即可。该模式适合在开发机器上测试 Kafka Connect,但不具备可伸缩性,也不能在 Kafka 中存储 Kafka Connect 的偏移量。

11.2.1 配置 Kafka Connect 集群

首先,我们要为分布式模式配置 Kafka Connect worker。Kafka Connect 的 worker 是单个进程,作为 Kafka Connect 集群的一部分运行,负责管理 connector 及其 task。这里的 task 是一个工作单元,例如从某个源分区读取数据或写入到目的地。每个 task 独立运行,并可以分布到多个 worker 上以实现并行处理。Kafka Connect worker 彼此协调,以确保数据高效、容错且可扩展地被处理。

要开始配置,我们创建一个名为 worker.properties 的文件,其内容如下:

bootstrap.servers=localhost:9092
group.id=connect
config.storage.topic=connect-config
offset.storage.topic=connect-offset
status.storage.topic=connect-status
key.converter=org.apache.kafka.connect.storage.StringConverter
value.converter=org.apache.kafka.connect.storage.StringConverter   
plugin.path=<PATH-TO-YOUR-USER-DIRECTORY>/kafka/libs/

注意:我们需要把 plugin.path 中的占位符替换为实际路径。

第 11.4 节会详细解释这些参数,但目前只需用下面的命令启动 Kafka Connect 即可:

$ connect-distributed.sh worker.properties

Kafka Connect 提供一个默认监听在 8083 端口的 REST API 用于管理。如果这些内部主题尚不存在,Kafka Connect 会为我们创建它们。请注意,推荐这些主题的复制因子为 3。如果我们的 broker 少于 3 个,Kafka Connect 无法启动。在测试环境中,我们可以通过调整 config.storage.replication.factor(以及其他主题对应的设置)来降低复制因子。

11.2.2 创建 connector

当 Kafka Connect 运行起来后,它本身并不会做任何事情。我们首先需要配置一个 connector。connector 的创建和管理是通过 REST API 完成的。例如,假设我们有一个名为 customers.txt 的文件,其中存放客户姓名。我们希望把这些数据导入到 Kafka。为此可以使用随 Kafka 一起提供的 FileStreamSource connector。注意:这个 connector 很基础、错误处理较弱,不推荐在生产环境使用;对于类似需求,我们更倾向于使用 FilePulse 之类的 connector(开源地址:mng.bz/MDAQ)。不过这里我们仍用 FileStreamSource 举例,创建一个名为 source-connector.json 的文件:

{
  "name": "customers-source",
  "config": {
    "connector.class": "FileStreamSource",
    "tasks.max": "1",
    "file": "/tmp/customers.txt",
    "topic": "customers"
  }
}

这里我们配置了名为 customers-source 的 connector,使用 FileStreamSource 类。因为只读取一个文件,无法扩展,所以将 tasks.max 设为 1。我们希望把 /tmp/customers.txt 中的数据写入到 customers 主题。

提示:在生产或真实环境中,建议在 connector 配置中显式指定 converter 类,而不要依赖 worker 的默认配置。

用下面的 curl 命令通过 POST 请求将配置文件发送给 Kafka Connect:

$ curl -X POST -H "Content-Type: application/json" \
    --data @source-connector.json \
    http://localhost:8083/connectors

11.2.3 测试 connector

我们可以在 http://localhost:8083/connectors 查询集群中所有的 connector:

$ curl http://localhost:8083/connectors
["customers-source"]

要检查某个 connector 的当前状态,使用:

$ curl http://localhost:8083/connectors/customers-source/status | jq

可能得到如下输出:

{
  "name": "customers-source",
  "connector": {
    "state": "RUNNING",
    "worker_id": "127.0.0.1:8083"
  },
  "tasks": [
    {
      "id": 0,
      "state": "RUNNING",
      "worker_id": "127.0.0.1:8083"
    }
  ],
  "type": "source"
}

提示:我们使用 jq 来美化输出。

从上面可以看到 connector 正在运行,并在 127.0.0.1:8083 的 worker 上运行。我们还可以看到所有的 tasks。因为只定义了一个 task,所以它在与 connector 相同的 worker 上运行。然而,如果查看 Kafka Connect 的日志或 /status 输出,可能会发现并非一切顺利,例如:

WARN [customers-source|task-0] Couldn't find file /tmp/customers.txt for
FileStreamSourceTask, sleeping to wait for it to be created
(org.apache.kafka.connect.file.FileStreamSourceTask:119)

所以,我们在另一个终端向文件写入一些数据:

$ echo "Linus Torvalds" >> /tmp/customers.txt
$ echo "Steve Jobs" >> /tmp/customers.txt

然后并行启动一个 consumer 来观察 Kafka Connect 写入 customers 主题的内容:

$ kafka-console-consumer.sh \
    --topic customers \
    --from-beginning \
    --bootstrap-server localhost:9092

如果一切正常,我们应该能看到两条客户记录:

Linus Torvalds
Steve Jobs

如果看不到,可以检查 /status 端点或 Kafka Connect 日志以获取错误信息并定位问题。随着我们继续向文件追加数据,相应的条目也会出现在 consumer 中。如果重启 Kafka Connect,connector 不会从文件的开头重新读取,而是会记住上次在文件中的读取位置,仅读取新增的内容。

警告:由于该 connector 记录了它上次读取的行位置,对已存在文件中已修改(非追加)的数据并不会产生新消息。这个 connector 仅能检测追加数据。

11.3 Kafka Connect 的可扩展性与容错性

Kafka Connect 如何使我们能够扩展 connector(连接器)并提高容错性?首先要注意,用简单的 FileStreamSource 连接器很难做到这一点,因为它只从本地磁盘读取文件,而这些文件很可能不会出现在其他 Kafka Connect worker 上。

警告:我们不建议在生产环境中使用 FileStreamSource 连接器,因为它更像是概念验证,而非成熟的生产级连接器。

一个 Kafka Connect 集群由在分布式模式下使用相同 group.id 运行的一个或多个 worker 组成,如图 11.4 所示。这些 worker 相互协调,类似于消费者组中的消费者,使用 Kafka Rebalancing 协议。如果某个 worker 崩溃或我们添加新的 worker,其余 worker 会在它们之间重新分配任务。在同一个 Kafka Connect 集群中,我们可以同时运行多个连接器。例如,我们可以一边从外部数据库读取数据,一边将其它数据写入第三方系统。

image.png

如果我们想扩展 Kafka Connect 集群,只需添加更多使用相同 group.id 的 worker 即可。连接器本身会将工作划分为各个独立运行的任务(tasks)。例如,如果要从 12 个数据库表导入数据,并且将 tasks.max 设置为 4,则每个任务负责三个表。Kafka Connect 会把这些任务分配到不同的 worker 上,从而实现均衡的负载分配,使我们能够更快速地在 Kafka 与外部系统之间移动大量数据。

提示:已经存在大量不同的连接器可用,我们强烈建议优先使用这些现成连接器。如果没有满足某个特定用例的连接器,但数据结构可以被有效地表示为分区流(partitioned streams),我们通常建议编写自己的 Kafka Connect 连接器,而不是试图用生产者和消费者重新实现相同功能。

11.4 Worker 配置

在分布式模式的示例中,我们已经见过一个最小化的 Kafka Connect worker 配置。本节将更深入地探讨用于自定义 Kafka Connect 集群的可用选项。注意 Kafka Connect 有两类配置:一类是 worker 配置(对运行在该 worker 上的所有连接器生效),另一类是通过 REST API 提供的 connect 配置(用于配置实际在 worker 上运行的连接器)。Kafka Connect 的配置通过内部配置 topic 在各个 worker 之间分发。下面回顾我们之前创建的 worker.properties 文件示例:

bootstrap.servers=localhost:9092
group.id=connect
config.storage.topic=connect-config
offset.storage.topic=connect-offset
status.storage.topic=connect-status
key.converter=org.apache.kafka.connect.storage.StringConverter
value.converter=org.apache.kafka.connect.storage.StringConverter
plugin.path=<PATH-TO-YOUR-USER-DIRECTORY>/kafka/libs/

bootstrap.servers 属性(如前章所述)定义了 Kafka 集群地址。一个非常重要的参数是 group.id。这个参数有点容易让人误解,因为它并不是传统意义上的 consumer group,而是为 Kafka Connect 集群提供一个名称或唯一标识(用 connect.id 可能会更合适)。所有使用相同 group.id 的 Kafka Connect worker 都属于同一个 Kafka Connect 集群。

各种存储主题用于管理 Kafka Connect 集群。config.storage.topic 存储各个连接器的配置,status.storage.topic 用于监控连接器并包含任务信息,offset.storage.topic 存储源连接器(source connector)的位置(例如数据库记录的 ID)等偏移信息。

消息的编码/解码由 key.convertervalue.converter 指定。除了 StringConverter,还有针对 Avro、Protobuf、JSON Schema、简单 JSON、byte[] 等的 converter。Avro/Protobuf/JSON Schema 的 converter 需要 Schema Registry(我们将在后面详细讨论)。提示:我们建议在 connector 配置中显式指定 converter 类,但这些参数仍然需要在 worker 配置中存在。在生产环境中,推荐使用 JsonConverterAvroConverter 作为默认的 value converter,而不是 StringConverter

某些 converter(例如 JsonConverter)提供额外属性,例如 key/value.converter.schemas.enable 用来开启或关闭在每条消息中写入 schema。这样会产生非常冗长的消息,因此我们建议使用支持 Schema Registry 的 converter(例如 io.confluent.connect.avro.AvroConverter),此类 converter 还需要指定 key/value.converter.schema.registry.url 等设置。

最后一个示例参数是 plugin.path。Kafka Connect 使用该参数定位 connector、converter、transformation 等插件,也可指定多个路径(用逗号分隔)。

接下来看一些其他重要参数。Kafka Connect 可以在所需主题不存在时自动创建主题。视集群管理策略,这一行为可能并不理想(例如希望通过 IaC 完全控制主题)。可以通过 topic.creation.enable 控制自动创建主题(默认启用)。

在第 5 章我们讨论了 exactly-once 保证。Kafka Connect 支持在 source connector 端实现 exactly-once,但必须通过 exactly.once.source.support 开启(默认禁用)。若要为已有集群更改此行为,需要先将该参数置为 preparing,然后再置为 enabled(这是一个中间步骤)。

Kafka Connect 的 consumer 行为也可通过若干参数影响。session.timeout.ms 指定在认定 worker 不活跃之前的等待时间。较低值可减少故障恢复时间,但也可能导致不必要的 rebalance(重新均衡)。request.timeout.ms 定义等待响应的最大时间,超时后 Kafka Connect 会重试该请求。较低的超时时间有助于更快发现错误,但如果网络不稳定或 broker 正在高负载传输大量数据,将可能导致不必要的重试并最终导致请求无法被标记为成功,从而使连接器无法处理数据。

类似地,offset.flush.interval.msoffset.flush.timeout.ms 控制 Kafka Connect 刷新偏移的频率和超时时间。提示:如果有很多小消息,刷写可能需要更长时间,因此建议更频繁地 flush 或增大超时时间。否则可能出现“Failed to flush, timed out while waiting for producer to flush outstanding x messages.” 的日志并陷入循环。我们也可以为 worker 的 producer/consumer 设置通用配置(如 fetch.max.bytesfetch.max.wait.ms)。

连接器配置还能覆盖一些集群级的通用参数,这在上面提到的参数上尤其有用。通过 connector.client.config.override.policy 指定哪些参数允许被覆盖,然后在具体连接器配置中用 producer.override.consumer.override. 前缀来调整,例如 consumer.override.fetch.max.bytes

还可以使用 config providers,在个别参数中使用变量,这对安全性大有裨益。在 config.providers 中定义别名列表,在 config.providers.<name>.class 中引用相应类,并通过 config.providers.<name>.param.<param> 设置其参数。这样可以同一类多次使用不同设置,并在配置中引用这些 provider。

连接器特有的认证与授权参数将在第 13 章讨论。

最后,listeners 参数可以配置 REST API 监听的端口、主机和协议;若不指定,Kafka Connect 默认在 HTTP 的 8083 端口监听。

11.5 Kafka Connect 的 REST API

了解完 Kafka Connect 集群的配置选项后,我们来看看如何管理集群与连接器。Kafka Connect 提供了一个 REST API(我们在本章开头已略见一二)。

必须指出:Kafka Connect 的 REST API 默认不提供认证和授权。可以配置基本认证,但那样一旦有人能访问该 API 就能执行所有操作。因此我们建议在生产环境不要让人工直接访问该 API,而应通过 CI/CD 管道来管理连接器。

11.5.1 查看 Kafka Connect 集群状态

在根 URL(/)处,我们可以看到 Kafka Connect 集群使用的版本信息、Git commit ID 以及所连接的 Kafka 集群 ID,例如:

$ curl http://localhost:8083 | jq
{
  "version": "3.9.0",
  "commit": "a60e31147e6b01ee",
  "kafka_cluster_id": "y9uk3gYxSu6sGKfGRDgjGQ"
}

注意:REST API 完全基于 JSON,Kafka Connect 在请求和响应中都使用 JSON 对象。kafka_cluster_id 指的是用于管理 Kafka Connect 集群的 Kafka 集群 ID,通常也是用于生产/消费数据的集群。

我们可以通过 /connector-plugins 检查 Kafka Connect 是否成功加载了在 plugin.path 下的插件:

$ curl http://localhost:8083/connector-plugins | jq
[
  {
    "class": "org.apache.kafka.connect.file.FileStreamSinkConnector",
    "type": "sink",
    "version": "3.9.0"
  },
  {
    "class": "org.apache.kafka.connect.file.FileStreamSourceConnector",
    "type": "source",
    "version": "3.9.0"
  },
 …
]

如果为某个连接器缺少所需插件,创建连接器时会报错并列出已安装的插件。可以通过 /connectors 获取当前运行的连接器列表:

$ curl http://localhost:8083/connectors | jq
[
  "customers-source"
]

若要查看某个连接器的详细信息,可访问 /connectors/<connector>

$ curl http://localhost:8083/connectors/customers-source | jq
{
  "name": "customers-source",
  "config": {
    "connector.class": "FileStreamSource",
    "file": "/tmp/customers.txt",
    "tasks.max": "1",
    "name": "customers-source",
    "topic": "customers"
  },
  "tasks": [
    {
      "connector": "customers-source",
      "task": 0
    }
  ],
  "type": "source"
}

若只想获取当前配置,可以调用 /connectors/<name>/config。单个任务与 worker 的详细状态可在 /connectors/<connector>/status 查看:

$ curl http://localhost:8083/connectors/customers-source/status | jq
{
  "name": "customers-source",
  "connector": {
    "state": "RUNNING",
    "worker_id": "127.0.0.1:8083"
  },
  "tasks": [
    {
      "id": 0,
      "state": "RUNNING",
      "worker_id": "127.0.0.1:8083"
    }
  ],
  "type": "source"
}

/connectors?expand=info?expand=status 可一次获取所有连接器的更详尽信息。某个连接器正在使用的主题可通过 /connectors/<connector>/topics 查询:

$ curl http://localhost:8083/connectors/customers-source/topics | jq
{
  "customers-source": {
    "topics": [
      "customers"
    ]
  }
}

我们也可以通过 PUT /connectors/<connector>/topics/reset 来重置该状态——这在维护后可能有用,但不会产生实际的运行效果(即不改变已经存在的数据处理流程)。

11.5.2 创建、修改和删除连接器

在详细了解了通过 REST API 可以查询到哪些连接器信息之后,我们现在来看如何创建、修改和删除连接器,从而配置与外部数据源的连接。首先,删除我们之前创建的连接器 customers-source

$ curl -X DELETE http://localhost:8083/connectors/customers-source

如果一切正常,响应会返回 HTTP 204 状态码。如果连接器不存在,则会返回 HTTP 404。现在,我们重新创建该连接器。为此调用 POST /connectors,并以 JSON 对象传递连接器配置,该 JSON 只包含连接器名称和连接器特定配置:

$ cat source-connector.json
{
  "name": "customers-source",
  "config": {
    "connector.class": "FileStreamSource",
    "tasks.max": "1",
    "file": "/tmp/customers.txt",
    "topic": "customers"
  }
}
$ curl -X POST -H "Content-Type: application/json" \
    --data @source-connector.json \
    http://localhost:8083/connectors

如果连接器创建成功,我们会收到与调用 GET /connectors/<connector> 时相同的响应。如果尝试创建已存在的连接器,会收到相应的错误信息。

我们也可以事后修改连接器的配置。例如,如果连接器的源文件从 /tmp/customers.txt 改为 /tmp/new_customers.txt,只需修改 file 参数。为此我们创建 source-connector-config.json

{
    "connector.class": "FileStreamSource",
    "tasks.max": "1",
    "file": "/tmp/new_customers.txt",
    "topic": "customers"
}

然后通过 PUT /connectors/<connector>/config 更新配置:

$ curl -X PUT -H "Content-Type: application/json" \
    --data @source-connector-config.json \
    http://localhost:8083/connectors/customers-source/config

我们也可以暂停连接器。在对第三方系统维护时这很有用,以避免连接器在维护期间失败或因为不一致而意外写入数据到 Kafka:

$ curl -X PUT http://localhost:8083/connectors/customers-source/pause
$ curl http://localhost:8083/connectors/customers-source/status | jq
{
  "name": "customers-source",
  "connector": {
    "state": "PAUSED",
    "worker_id": "127.0.0.1:8083"
  },
  "tasks": [
    {
      "id": 0,
      "state": "PAUSED ",
      "worker_id": "127.0.0.1:8083"
    }
  ],
  "type": "source"
}

随后可恢复连接器:

$ curl -X PUT http://localhost:8083/connectors/customers-source/resume

连接器或其任务也可能失败。虽然 Kafka Connect 对大多数瞬时错误可以通过重试机制进行补偿,但有时这仍不足以恢复——例如长时间的网络故障或消息数据结构问题。在这种情况下,解决问题后需要重启连接器及其任务(通过 POST):

$ curl -X POST http://localhost:8083/connectors/customers-source/\
    restart?includeTasks=true&onlyFailed=true

使用 includeTasksonlyFailed 标志可以确保不仅重启连接器本身,还重启所有处于失败状态的任务。或者也可以只重启单个任务:

POST /connectors/<connector>/tasks/<taskId>/restart

11.6 连接器配置

在学习完如何配置 Kafka Connect 集群并管理连接器之后,本节将探讨连接器的配置选项。首先再看一遍 customers-source 连接器示例:

{
  "name": "customers-source",
  "config": {
    "connector.class": "FileStreamSource",
    "tasks.max": "1",
    "file": "/tmp/customers.txt",
    "topic": "customers"
  }
}

连接器最重要且常常最难配置的参数可能是它的名称(name)。名称用于识别连接器,也是 Kafka Connect REST API 路径的一部分。因为名称是连接器唯一的元数据参数,当 Kafka Connect 管理大量连接器时,使用可靠的命名规范非常关键。

11.6.1 通用连接器配置

connector.class 决定连接器类型。不同的连接器类通常有其特定参数,例如 FileStreamSource 有 file 参数。其他示例类包括:

  • org.apache.kafka.connect.mirror.MirrorSourceConnector
  • io.confluent.connect.jdbc.JdbcSourceConnector
  • io.confluent.connect.jdbc.JdbcSinkConnector
  • io.aiven.connect.jdbc.JdbcSourceConnector
  • io.aiven.connect.jdbc.JdbcSinkConnector
  • io.debezium.connector.postgresql.PostgresConnector

通常有两类连接器:source(从外部系统读数据并写入 Kafka)和 sink(从 Kafka 消费并写入外部系统)。MirrorMaker 是例外,它在集群间复制数据。

tasks.max 定义连接器最多可以拆分为多少个任务以实现并行化。但实际任务数常受连接器本身或数据结构的限制(例如源端表/分区数)。将 tasks.max 设得高于数据或连接器能支持的并行度并不会带来额外并行化。

对于 sink 连接器,还有 topics 参数(要消费的主题列表),或者 topic.regex 可以用正则匹配主题名,适用于想消费大量主题或主题名随时间变化的场景——这要求有一致的主题命名规范。

对于由 Kafka 消费的 sink 连接器,默认会创建以 connect 加连接器名为前缀的 consumer group,当然可以用 consumer.group.id 覆盖此值(前提是 worker 配置允许覆盖)。

在连接器配置中,我们可以通过 exactly.once.support 指定是否要求 Kafka Connect 集群支持 exactly-once 语义(required)或只是请求(requested)。默认是请求。如果将 exactly-once 设置为必需,但集群(worker)不支持(即 exactly.once.source.support 未启用),连接器将失败并停止执行。

Kafka Connect 还允许在将数据读入/写出时对单条消息做转换(single message transformations),一些示例会在 11.7 节中介绍。

11.6.2 Kafka Connect 中的错误处理

和任何应用一样,Kafka Connect 在处理数据时也可能遇到问题。最常见的原因是临时网络问题或无法处理的数据。为了避免每遇到小错误就让整个连接器停止,我们可以根据用例配置不同的错误处理策略。

默认情况下,Kafka Connect 在遇到错误时会让连接器失效(即容错为零)。这一行为可由 errors.tolerance 参数控制。如果将其设置为 all(而不是 none),不合格的记录会被跳过。启用此配置时,建议同时设置 errors.log.enable=true,否则只有不被容忍的错误才会被记录。这样我们仍能在事后查看有问题的操作并手动修正。

提示:如果将容错设置为跳过错误记录,建议为此配置建立告警与通知机制。

errors.log.include.messages 允许指定是否在错误日志中附带出错的记录本身。默认不启用,因为日志中可能包含敏感数据,不应随意写入日志。如果使用 sink 连接器,也可以将出错的消息发送到死信队列(dead-letter queue)主题,需要先配置 errors.deadletterqueue.topic.name

除了无法处理的记录外,如果访问第三方系统失败,连接器也会失败。对此可以用 errors.retry.timeouterrors.retry.delay.max.ms 配置重试策略。errors.retry.timeout 指定重试失败操作的最大毫秒数(默认 0,表示不重试)。errors.retry.delay.max.ms 指定重试之间的最大间隔。

某些连接器还有各自的重试参数,例如 JDBC 连接器可以通过 connection.attempts(尝试次数)和 connection.backoff.ms(重试间隔)来配置连接数据库的重试。默认情况下,JDBC 连接器会尝试 3 次,每次间隔 10 秒。

提示:建议为 JDBC 连接器在数据库维护或临时网络故障期间预留更长的恢复时间,默认 30 秒可能不足。延长等待可以减少因连接器失败而需要人工重启导致的更长停机时间。

此外,JDBC sink 连接器在写入失败时也可以重试。可通过 max.retriesretry.backoff.ms 配置在放弃或跳过记录前的重试次数和重试间隔。这在对应数据库记录被短期锁定时非常有用。默认情况下,JDBC sink 连接器在放弃前会重试写入 10 次,每次间隔 3 秒。

11.7 单条消息转换

仅仅把数据从 A 移到 B 往往不够,尤其当数据需要被转换、处理或路由到不同目标时。虽然 Kafka Connect 并不是一个完整的 ETL(抽取、转换、加载)工具,但它允许我们使用单条消息转换(SMT,Single Message Transformations)来完成一些较简单的转换。SMT 可用于源连接器和汇(sink)连接器,能用于重命名字段、对数据做脱敏(掩码),甚至将消息的值移动到键中。

图 11.5 可视化展示了转换发生的位置。在源连接器中,实际的连接器从外部数据源抓取数据并将其转换为 Kafka Connect 的内部数据结构。此后,我们可以在数据被转换为目标数据格式(如 JSON)并写入 Kafka 主题之前,应用任意多个转换。对于汇连接器,流程则相反:首先将主题中的数据从其格式转换为 Kafka Connect 的内部数据结构,接着 Kafka Connect 应用 SMT,最后由具体的连接器类将数据写入目标系统。

image.png

警告:不要滥用 SMT(单条消息转换)。它并不是一个完整的 ETL 工具。如果你需要做复杂的转换,请使用 Kafka Streams 或其它流处理框架。

下面看几个 SMT 的实际例子。要使用 SMT,需要在 Connect 配置中声明转换步骤,也就是你发送到 Kafka Connect REST API 的 JSON 配置中,例如:

{
  "config": {
    […],
    "transforms": "mySMT1,mySMT2,…",
    "transforms.mySMT1.type": "java.class.name",
    "transforms.mySMT1.…": "some-config",
    "transforms.mySMT2.type": "java.class.name2",
    "transforms.mySMT2.…": "some-config2",
    [… ]
  }
}

首先,我们要设置要执行的转换列表(transforms)。转换的名字可以随意取(如 mySMT1, mySMT2)。然后,对于每个转换,至少要设置其 Java 类名(type: java.class.name),通常还会有额外的配置。转换会按逗号分隔列表中的顺序依次执行。

下面看几种常见的 SMT。ReplaceField SMT 可用于重命名或删除字段。例如,下列转换会删除字段 dropme,并把字段 foo 重命名为 bar

    […],
    "transforms": "rename",
    "transforms.rename.type":
        "org.apache.kafka.connect.transforms.ReplaceField$Value",
    "transforms.rename.exclude": "dropme",
    "transforms.rename.renames": "foo:bar",
    [… ]

许多源连接器只设置消息的 value 而不设置 key,这时可以使用 ValueToKey SMT 从 value 中提取某些字段到 key。问题是,提取后的 key 常常成为一个只有一对键值的 JSON 对象。例如如果我们用 product_id 字段作为 key,ValueToKey 生成的 key 可能是 {product_id: <产品ID>}。通过 ExtractField SMT 可以把某个字段提取为 key,使得最终的 key 变成一个简单的字符串(例如产品 ID):

    […],
    "transforms": "moveKey,extractKey",
    "transforms.moveKey.type":
        "org.apache.kafka.connect.transforms.ValueToKey",
    "transforms.moveKey.fields": "product_id",
    "transforms.extractKey.type":
        "org.apache.kafka.connect.transforms.ExtractField$Key",
    "transforms.extractKey.field": "product_id",
    [… ]

MaskField SMT 也很有用,它可以将某些字段设为 null,以避免把敏感信息发送到下游系统。Kafka Connect 自带了若干 SMT,有些连接器(例如 Debezium)还额外提供更多 SMT。更多 SMT 的细节请参阅 Apache Kafka 文档: mng.bz/rK7B

11.8 Kafka Connect 示例:JDBC 源连接器

在前一节讨论了连接器的一般配置以及 JDBC 连接器的特定配置后,我们将通过一个 JDBC 源连接器的实际示例来加深对 Kafka Connect 的理解。仍以“客户”示例为背景:假设到目前为止,所有客户数据都保存在数据库中。将来我们希望也能在 Kafka 中访问这些客户数据。正如图 11.6 所示,我们将使用 JDBC 源连接器把 customers 表的数据移入 sqlite_customers 主题(topic)。

image.png

11.8.1 准备 JDBC 源连接器

首先,我们需要一个简单的 SQLite 数据库作为前提。可以直接从官网下载安装 SQLite 二进制文件,或使用包管理器安装。

首先,初始化一个名为 customers.db 的 SQLite 数据库并在其中创建一张表:

$ sqlite3 customers.db
CREATE TABLE customers(    #1
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  firstname VARCHAR,
  lastname VARCHAR,
  birthdate DATE,
  lastupdated TIMESTAMP DEFAULT current_timestamp
);
#1 在 SQLite 中

customers 表存储所有与客户相关的数据。除了主键外,还有客户的名字、姓氏和出生日期列。现在我们插入一些记录,以便之后把数据写入 Kafka 时有数据可用:

INSERT INTO customers(id, firstname, lastname, birthdate) VALUES
  (1, 'Steve', 'Jobs', '1955-02-24'),    #1
  (2, 'Bill', 'Gates', '1955-10-28');
#1 在 SQLite 中

接下来,需要安装我们的 JDBC 连接器,可以从 Confluent Hub 下载。必须确保把它放到 worker.propertiesplugin.path 指定的目录下。然后重启 Kafka Connect 集群。

11.8.2 配置 JDBC 源连接器

现在,创建连接器的配置并保存为 customers-jdbc-source-connector.json

{
  "name": "customers-jdbc-source-connector",
  "config": {
    "connector.class": "io.confluent.connect.jdbc.JdbcSourceConnector",
    "connection.url": "jdbc:sqlite:/path/to/customers.db",
    "value.converter": "org.apache.kafka.connect.json.JsonConverter",
    "value.converter.schemas.enable": false,
    "table.types": "TABLE",
    "table.whitelist": "customers",
    "topic.prefix": "sqlite_",
    "mode": "incrementing",
    "incrementing.column.name": "id",
    "batch.size": 1000
  }
}

除了我们熟悉的字段(如 nameconnector.classbatch.size)外,还有一些针对 JDBC 源连接器的新参数。connection.url 指定连接类型(jdbc)、数据库类型(sqlite)以及数据库位置(/path/to/customers.db),在本例中就是 SQLite 文件的路径。

我们还在此覆盖了 worker.properties 中的 value.converter,使用 org.apache.kafka.connect.json.JsonConverter,并且禁用了 schema(value.converter.schemas.enable=false),以便使用简单的 JSON。

table.types 用来指定要查询的数据库对象类型,除了 TABLE,另一个常见选项是 VIEW
提示:尽管 TABLEtable.types 的默认值,最好显式设置这些选项以避免版本升级后出现意外。

table.whitelist 指定要读取的表,或者可以用 table.blacklist 指定不读取的表。本例中我们只想把 customers 表的内容写入 Kafka。

Kafka 主题名由 topic.prefix 与数据库表名拼接而成。本例中,前缀 sqlite_ 与表名 customers 组合成主题 sqlite_customers。这个参数有两个重要功能:一是便于将主题和连接器关联,二是防止误把数据写入同名的已存在主题(当使用 table.blacklist 时,这一点尤其重要)。剩下的参数 modeincrementing.column.name 稍后会详细讨论。

11.8.3 测试 JDBC 源连接器

在创建连接器之前,可以另开一个窗口,用 kafka-console-consumer.sh 从连接器的目标主题 sqlite_customers 开始消费:

$ kafka-console-consumer.sh --bootstrap-server localhost:9092 \
    --topic sqlite_customers --from-beginning
[..]sqlite_customers=UNKNOWN_TOPIC_OR_PARTITION[..]

正如预期,主题尚不存在,但默认情况下在开始消费时会自动创建。现在创建连接器:

$ curl -X POST -H "Content-Type: application/json" \
    --data @customers-jdbc-source-connector.json \
    http://localhost:8083/connectors

一旦连接器配置成功,我们会看到 kafka-console-consumer.sh 开始消费消息:

{"id":1,"firstname":"Steve","lastname":"Jobs", "birthdate":"1955-02-24"}
{"id":2,"firstname":"Bill","lastname":"Gates", "birthdate":"1955-10-28"}

如上所示,Kafka Connect 自动将数据库记录转换为 JSON,列名成为对象的键。现在向数据库表插入一条新行(此处故意写错了出生日期列名):

INSERT INTO customers(id, firstname, lastname, brithdate) VALUES
  (3, "Linus", "Torvalds", "1869-12-28");    #1
#1 在 SQLite 中

可以在 kafka-console-consumer.sh 中观察到,新数据库条目在几秒后出现在 Kafka 主题中:

{"id":3,"firstname":"Linus","lastname":"Torvalds", "birthdate":"1868-12-28"}

但如果我们更新已有的数据库条目会发生什么?例如:

UPDATE customers SET birthdate="1969-12-28" where id=3;
#1 在 SQLite 中

这次我们没有消费到新的消息,这是因为所选的连接器模式是 mode=incrementing 并且增量列是 incrementing.column.name=id。在该配置下,只有当出现新的 ID(即全新的记录)时,连接器才会向 Kafka 发送新消息。Kafka Connect 通过基于已存储偏移量查询数据库表以查找新记录。例如,如果最后处理的记录 ID 是 3,连接器会执行如下查询:

SELECT * FROM customers WHERE id > 3;

另外一种选择是使用 mode=timestamp 并设置 timestamp.column.name=lastupdated,这种配置会在记录的时间戳列变更时检测到变更并将更新写入 Kafka。也可以使用 timestamp+incrementing 的组合模式。务必确保相关列是单调递增的。

警告:ID 必须严格递增,因为 Kafka 将其作为增加的偏移量。如果某情形下新条目被分配了更小的 ID(例如某记录被删除后重用了 ID),连接器不会把它识别为新条目,因此不会将其写入 Kafka。同样,删除记录也不会被连接器识别(因为删除不会产生新的 ID)。

不过,这种模式也有局限性,并不是每张表都有可用的时间戳列。例如,如果只是存储客户地址并且客户搬家了,表中可能没有单独的 timestamp 列,这种变更就不会被写入 Kafka。即便有时间戳列,也不能保证能捕获所有变更:如果两个变更在相同时间戳下发生,连接器可能读取第一个变更,但错过第二个,因为它认为已经读到了那个时间戳之前的所有更改。时间戳的精度越低,这类问题越容易出现,但这种情况永远不能完全排除。

提示:要保证使用 JDBC 源连接器捕获所有变化,唯一可行的方法是使用 bulk 模式 —— 每次都把整张表重新加载到 Kafka。该方式极其低效,不推荐使用。这个问题可以通过使用像 Debezium 这样的变更数据捕获(CDC)连接器来解决,下一节将介绍 Debezium。

11.9 Kafka Connect 示例:变更数据捕获(CDC)连接器

本节将以实操方式说明如何配置变更数据捕获(CDC)连接器,具体以针对 PostgreSQL 的 Debezium 连接器为例。CDC 是一种监控数据源(如数据库表)中所有变更的概念——不是周期性读取整个表的数据,而是捕捉发生的变更。技术实现方式取决于第三方系统本身,可能基于触发器或日志。对于关系型数据库,可以监控事务日志(transaction log),因为所有变更都会被记录到那里。

11.9.1 为 PostgreSQL 准备 Debezium 连接器

在配置连接器之前,需要准备 PostgreSQL 数据库。PostgreSQL 在各大操作系统上的下载与安装说明可在其官网找到。安装 PostgreSQL 后,必须在 postgresql.conf 中将 wal_level 设置为 logical,否则无法监控所需的变更:

  • Linux 路径示例:/etc/postgresql/<version>/main/postgresql.conf
  • Windows 路径示例:C://Programs/PostgreSQL/<version>/data/postgresql.conf
wal_level = logical

然后重启 PostgreSQL 服务:

$ sudo service postgresql restart

注意:像 AWS、Azure、Google Cloud 这样的公有云供应商通常也支持这些功能。

PostgreSQL 启动并运行后,创建名为 customers 的数据库,并创建一个数据库用户 customers_user,把该用户设为 customers 数据库的所有者。首先以 postgres 用户启动交互式 psql

sudo -u postgres psql    #1
CREATE DATABASE customers;    #2
CREATE ROLE customers_user REPLICATION LOGIN 
    PASSWORD 'supersecret';    #3
ALTER DATABASE customers OWNER TO customers_user;    #4
\q    #5
#1 在 Linux 上以 postgres 身份启动 psql CLI
#2 创建 customers 数据库
#3 创建具有 REPLICATION 权限和登录权限的 customers_user,密码为 supersecret
#4 将 customers 数据库的所有权授予 customers_user
#5 退出 psql CLI

务必授予 customers_user REPLICATION 权限,这样该用户才具有 CDC 连接器所需的权限。

接下来用新用户 customers_user 连接到 customers 数据库并创建 customers 表,并插入一些示例数据:

psql -h localhost -U customers_user -W -d customers    #1
CREATE TABLE customers (
  id SERIAL PRIMARY KEY,
  firstname VARCHAR,
  lastname VARCHAR,
  birthdate VARCHAR
);    #2
INSERT INTO customers (id, firstname, lastname, birthdate) VALUES
  (1, 'Steve', 'Jobs', '1955-02-24'),
  (2, 'Bill', 'Gates', '1955-10-28');    #3
\q    #4
#1 以 customers_user 连接到 customers 数据库
#2 创建 customers 表
#3 向 customers 表插入数据
#4 退出 psql CLI

最重要的部分是连接器本身。Debezium 的下载与部署说明见其文档的 Deployment 部分,安装过程类似于安装 JDBC 连接器:把 Debezium 插件放到 Kafka Connect 的 plugin.path,重启 Connect worker 等。

11.9.2 为 PostgreSQL 配置 Debezium 连接器

准备就绪后,创建连接器配置文件 customers_debezium_connector.json

{  
  "name": "customers_debezium_connector",
  "config": {
    "connector.class": "io.debezium.connector.postgresql.PostgresConnector",
    "database.hostname": "localhost",
    "database.port": "5432",
    "database.user": "customers_user",
    "database.password": "supersecret",
    "database.dbname" : "customers",
    "value.converter": "org.apache.kafka.connect.json.JsonConverter",
    "value.converter.schemas.enable": false,
    "plugin.name": "pgoutput",
    "publication.autocreate.mode": "filtered",
    "topic.prefix": "debezium",
    "table.include.list": "public.customers"
  }
}

大多数参数与 JDBC 源连接器类似(或名称相近)。数据库连接相关的配置都在 database.* 下。我们同样用 topic.prefix 指定 Kafka 主题前缀。Debezium 的表白名单参数名为 table.include.listplugin.namepublication.autocreate.mode 是新的参数,用于控制 Debezium 从 PostgreSQL 提取变更的方式(例如使用哪种输出插件以及是否自动创建 publication/publication 的范围等)。

11.9.3 测试 PostgreSQL 的 Debezium 连接器

先创建连接器:

$ curl -X POST -H "Content-Type: application/json" \
    --data @customers_debezium_connector.json \
    http://localhost:8083/connectors

然后启动消费者监听 Debezium 产出的主题:

$ kafka-console-consumer.sh --bootstrap-server localhost:9092 \
    --topic debezium.public.customers --from-beginning

你会看到类似如下的消息(初始快照时的条目):

{
    "before":null,
    "after":{"id":1,"firstname":"Steve","lastname":"Jobs","birthdate":"1955-02-24"},
    "source":{"version":"2.1.2.Final","connector":"postgresql","name":"debezium",
              "ts_ms":1738493972052,"snapshot":"first","db":"customers",
              "sequence":"[null,"24287336"]","schema":"public","table":"customers",
              "txId":739,"lsn":24287336,"xmin":null},
    "op":"r",
    "ts_ms":1738493972146,
    "transaction":null
}

注意:生成的主题名 debezium.public.customerstopic.prefixdebezium)加上表名 public.customers 组成的。

输出里包含了比预期更多的信息。Debezium 在每条消息中添加额外字段来说明数据来源与元信息,主要字段说明如下:

  • after:包含变更后行的实际数据(这是通常关心的部分)。
  • before:若配置与数据库支持,会包含变更前的行(用于比较旧值)。
  • source:说明数据来自何处及技术性元数据(版本、connector 名称、LSN、事务 id 等)。
  • op:表示操作类型,c = create(插入),u = update(更新),d = delete(删除),r = snapshot(初始快照导入)。
  • ts_ms:事件时间戳(毫秒)。
  • transaction:若变更包含在事务中,会有事务信息。

现在在数据库中插入一条新纪录:

INSERT INTO customers (id, firstname, lastname, birthdate) VALUES
  (3, 'Linus', 'Torvalds', '1869-12-28');

消费者会看到类似下面的消息(op: "c" 表示创建):

{
    "before":null,
    "after":{"id":3,"firstname":"Linus","lastname":"Torvalds","birthdate":"1869-12-28"},
    "source":{},
    "op":"c",
    "ts_ms":1738494107344,
    "transaction":null
}

如果更新该行,例如:

UPDATE customers SET birthdate='1968-12-28' where id=3;

与 JDBC 源连接器不同,Debezium 会把这次更新捕获并发送到 Kafka(op: "u" 表示 update):

{
    "before":{...},        // 如果开启包含 before,则这里会有更新前的内容
    "after":{"id":3,"firstname":"Linus","lastname":"Torvalds","birthdate":"1969-12-28"},
    "source":{},
    "op":"u",
    "ts_ms":1738494350114,
    "transaction":null
}

提示:大多数情况下,我们不希望把完整的 Debezium 变更事件写入下游系统。可以使用 ExtractNewRecordState 这个 SMT,使 Kafka 中只写入 after 字段(即仅写出变更后的新记录状态)。

总结

  • Kafka Connect 是一个用于在 Kafka 与其他系统之间高效、可扩展地移动数据的框架。
  • 连接器分为 source(将外部数据导入 Kafka)和 sink(将 Kafka 数据写出到外部系统)。
  • 每个连接器可通过丰富的参数进行配置以满足特定需求。
  • Kafka Connect 提供 REST API 用于管理 connectors 与 worker 配置,也可通过启动时的属性文件配置 worker。
  • REST API 可用于创建、更新、删除连接器以及查询其状态。
  • worker 配置决定 Kafka Connect 的运行方式,包括资源分配、任务分发与整体容错能力。
  • 可以通过 errors.tolerance 等参数配置错误处理策略(例如跳过坏记录并将其记录日志或送入死信队列)。
  • Single Message Transformations(SMT)支持简单的数据转换(字段重命名、掩码、将 value 中字段移动到 key 等),但不宜用于复杂 ETL。
  • JDBC 连接器与 Debezium 等 CDC 连接器提供了将数据库变化实时同步到 Kafka 的能力;Debezium 能监控事务日志并发送完整变更事件(包含 before/after、元数据等)。
  • PostgreSQL 使用 Debezium 时需要启用 wal_level = logical 并给予对应用户 REPLICATION 权限。
  • topic.prefixtable.include.list 等参数可控制 Debezium 产生的主题名称与筛选哪些表被捕获。