ETL 代表提取-转换-加载,是将数据从一个源系统移动到另一个源系统的过程。下面将描述如何使用 Apache Kafka、Kafka Connect、Debezium 和 ksqlDB 构建实时流 ETL 流程。
构建业务应用程序时,会先根据应用程序的功能需求来设计数据模型。为了重塑我们的数据,需要将其移动到另一个数据库。
在行业中,人们大多从源系统中批量提取数据,在合理的时间段内,主要是每天一次,但也可以是每小时一次,也可以是两三天一次。保持较短的周期可能会导致源系统的资源使用率更高,目标系统的中断频繁;但是,保持较长时间可能会导致目标系统出现最新问题。因此,我们需要一些对源系统性能造成最小影响的东西,并且可以在更短的时间内或实时更新目标系统。
Debezium 不使用 SQL 提取数据。它使用数据库日志文件来跟踪数据库中的更改,因此它对源系统的影响最小。
提取数据后,需要 Kafka Connect 将其流式传输到 Apache Kafka 中,以便根据需要使用它并对其进行重塑。可以使用 ksqlDB 来以目标系统所需的方式重塑原始数据。下面考虑一个简单的订购系统数据库,其中有一个客户表、一个产品表和一个订单表,如下所示。
现在需要提交一份关于订单的报告,可以看到购买者的电子邮件,并且在同一行中显示了产品的名称。如下图:
客户列将包含位于客户表的电子邮件字段中的客户的电子邮件,而产品列将包含位于产品表的名称字段中的产品名称。
首先,创建一个源连接器来从源数据库中提取数据。源数据库是 MySQL 数据库,因此使用 Debezium MySQL Source Connector,如下所示:
CREATE SOURCE CONNECTOR `mysql-connector` WITH(
"connector.class"= 'io.debezium.connector.mysql.MySqlConnector',
"tasks.max"= '1',
"database.hostname"= 'mysql',
"database.port"= '3306',
"database.user"= 'root',
"database.password"= 'debezium',
"database.server.id"= '184054',
"database.server.name"= 'dbserver1',
"database.whitelist"= 'inventory',
"table.whitelist"= 'inventory.customers,inventory.products,inventory.orders',
"database.history.kafka.bootstrap.servers"= 'kafka:9092',
"database.history.kafka.topic"= 'schema-changes.inventory',
"transforms"= 'unwrap',
"transforms.unwrap.type"= 'io.debezium.transforms.ExtractNewRecordState',
"key.converter"= 'org.apache.kafka.connect.json.JsonConverter',
"key.converter.schemas.enable"= 'false',
"value.converter"= 'org.apache.kafka.connect.json.JsonConverter',
"value.converter.schemas.enable"= 'false');
现在拥有了来自源系统的表、客户、产品和订单的 Kafka 主题。
ksql> show topics;
Kafka Topic | Partitions | Partition Replicas
-----------------------------------------------------------------
dbserver1 | 1 | 1
dbserver1.inventory.customers | 1 | 1
dbserver1.inventory.orders | 1 | 1
dbserver1.inventory.products | 1 | 1
default_ksql_processing_log | 1 | 1
my_connect_configs | 1 | 1
my_connect_offsets | 25 | 1
my_connect_statuses | 5 | 1
schema-changes.inventory | 1 | 1
使用以下脚本,为订单创建一个 ksqlDB 流,该流在订单数据旁边连接客户和产品数据。
CREATE STREAM S_CUSTOMER (ID INT,
FIRST_NAME string,
LAST_NAME string,
EMAIL string)
WITH (KAFKA_TOPIC='dbserver1.inventory.customers',
VALUE_FORMAT='json');
CREATE TABLE T_CUSTOMER
AS
SELECT id,
latest_by_offset(first_name) as fist_name,
latest_by_offset(last_name) as last_name,
latest_by_offset(email) as email
FROM s_customer
GROUP BY id
EMIT CHANGES;
CREATE STREAM S_PRODUCT (ID INT,
NAME string,
description string,
weight DOUBLE)
WITH (KAFKA_TOPIC='dbserver1.inventory.products',
VALUE_FORMAT='json');
CREATE TABLE T_PRODUCT
AS
SELECT id,
latest_by_offset(name) as name,
latest_by_offset(description) as description,
latest_by_offset(weight) as weight
FROM s_product
GROUP BY id
EMIT CHANGES;
CREATE STREAM s_order (
order_number integer,
order_date timestamp,
purchaser integer,
quantity integer,
product_id integer)
WITH (KAFKA_TOPIC='dbserver1.inventory.orders',VALUE_FORMAT='json');
CREATE STREAM SA_ENRICHED_ORDER WITH (VALUE_FORMAT='AVRO') AS
select o.order_number, o.quantity, p.name as product, c.email as customer, p.id as product_id, c.id as customer_id
from s_order as o
left join t_product as p on o.product_id = p.id
left join t_customer as c on o.purchaser = c.id
partition by o.order_number
emit changes;
最后,在 JDBC 接收器连接器的帮助下,我们会将丰富的订单表推送到 PostgreSQL 数据库中。
CREATE SINK CONNECTOR `postgres-sink` WITH(
"connector.class"= 'io.confluent.connect.jdbc.JdbcSinkConnector',
"tasks.max"= '1',
"dialect.name"= 'PostgreSqlDatabaseDialect',
"table.name.format"= 'ENRICHED_ORDER',
"topics"= 'SA_ENRICHED_ORDER',
"connection.url"= 'jdbc:postgresql://postgres:5432/inventory?user=postgresuser&password=postgrespw',
"auto.create"= 'true',
"insert.mode"= 'upsert',
"pk.fields"= 'ORDER_NUMBER',
"pk.mode"= 'record_key',
"key.converter"= 'org.apache.kafka.connect.converters.IntegerConverter',
"key.converter.schemas.enable" = 'false',
"value.converter"= 'io.confluent.connect.avro.AvroConverter',
"value.converter.schemas.enable" = 'true',
"value.converter.schema.registry.url"= 'http://schema-registry:8081'
);