一、整体架构:一条典型的 Streaming ETL Pipeline
本教程的数据流非常清晰:
-
MySQL
mydb.products:商品维表mydb.orders:订单事实表
-
Postgres
public.shipments:发货/物流表,记录订单发货、到达情况
-
Flink 集群
- 通过
mysql-cdc捕获 MySQL 变更; - 通过
postgres-cdc捕获 Postgres 变更; - 在 Flink SQL 中对三张表进行实时 Left Join;
- 将结果写入 Elasticsearch。
- 通过
-
Elasticsearch + Kibana
- 存储富化后的订单数据;
- 在 Kibana 里通过 Index Pattern 展示实时变化。
用一句话概括就是:
多源 CDC → Flink SQL 实时 Join → 下游 ES 实时同步
二、环境准备
1. 前提条件
-
一台 Linux 或 MacOS 机器;
-
已安装 Docker & Docker Compose;
-
端口没有被占用:
3306(MySQL)、5432(Postgres)、9200/9300(ES)、5601(Kibana)、8081(Flink Web UI)。
2. 使用 docker-compose 启动核心组件
在任意目录创建 docker-compose.yml:
yaml
version: '2.1'
services:
postgres:
image: debezium/example-postgres:1.1
ports:
- "5432:5432"
environment:
- POSTGRES_DB=postgres
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
mysql:
image: debezium/example-mysql:1.1
ports:
- "3306:3306"
environment:
- MYSQL_ROOT_PASSWORD=123456
- MYSQL_USER=mysqluser
- MYSQL_PASSWORD=mysqlpw
elasticsearch:
image: elastic/elasticsearch:7.6.0
environment:
- cluster.name=docker-cluster
- bootstrap.memory_lock=true
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
- discovery.type=single-node
ports:
- "9200:9200"
- "9300:9300"
ulimits:
memlock:
soft: -1
hard: -1
nofile:
soft: 65536
hard: 65536
kibana:
image: elastic/kibana:7.6.0
ports:
- "5601:5601"
说明一下:
- MySQL 和 Postgres 都用的是 Debezium 提供的 Example 镜像,已经开好了 binlog / wal,适合做 CDC。
- Elasticsearch + Kibana 用于结果展示。
在该目录执行:
bash
docker-compose up -d
启动完成后用:
bash
docker ps
确认容器都在运行。
浏览器打开 http://localhost:5601,能看到 Kibana 登陆页就 OK。
三、准备 Flink 与 CDC 相关 JAR
下载 Flink 1.18.0 ,解压到 flink-1.18.0 目录:
bash
tar -xzf flink-1.18.0-bin-scala_2.12.tgz
cd flink-1.18.0
将以下 JAR 放到 flink-1.18.0/lib/ 下:
flink-sql-connector-elasticsearch7-3.0.1-1.17.jarflink-sql-connector-mysql-cdc-3.0-SNAPSHOT.jarflink-sql-connector-postgres-cdc-3.0-SNAPSHOT.jar
⚠️ SNAPSHOT 版本往往需要从对应项目(比如 Flink CDC 仓库)源码自行打包。
四、构造业务数据:MySQL + Postgres
1. 在 MySQL 中创建产品 & 订单表
进入 MySQL 容器:
bash
docker-compose exec mysql mysql -uroot -p123456
创建数据库和表:
sql
-- MySQL
CREATE DATABASE mydb;
USE mydb;
CREATE TABLE products (
id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description VARCHAR(512)
);
ALTER TABLE products AUTO_INCREMENT = 101;
INSERT INTO products
VALUES (default,"scooter","Small 2-wheel scooter"),
(default,"car battery","12V car battery"),
(default,"12-pack drill bits","12-pack of drill bits with sizes ranging from #40 to #3"),
(default,"hammer","12oz carpenter's hammer"),
(default,"hammer","14oz carpenter's hammer"),
(default,"hammer","16oz carpenter's hammer"),
(default,"rocks","box of assorted rocks"),
(default,"jacket","water resistent black wind breaker"),
(default,"spare tire","24 inch spare tire");
CREATE TABLE orders (
order_id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY,
order_date DATETIME NOT NULL,
customer_name VARCHAR(255) NOT NULL,
price DECIMAL(10, 5) NOT NULL,
product_id INTEGER NOT NULL,
order_status BOOLEAN NOT NULL -- Whether order has been placed
) AUTO_INCREMENT = 10001;
INSERT INTO orders
VALUES (default, '2020-07-30 10:08:22', 'Jark', 50.50, 102, false),
(default, '2020-07-30 10:11:09', 'Sally', 15.00, 105, false),
(default, '2020-07-30 12:00:30', 'Edward', 25.25, 106, false);
MySQL 里现在有:
- 商品表
products - 订单表
orders
2. 在 Postgres 中创建 shipments 物流表
进入 Postgres 容器:
bash
docker-compose exec postgres psql -h localhost -U postgres
执行 SQL:
sql
-- PG
CREATE TABLE shipments (
shipment_id SERIAL NOT NULL PRIMARY KEY,
order_id SERIAL NOT NULL,
origin VARCHAR(255) NOT NULL,
destination VARCHAR(255) NOT NULL,
is_arrived BOOLEAN NOT NULL
);
ALTER SEQUENCE public.shipments_shipment_id_seq RESTART WITH 1001;
-- 为了 CDC 能拿到完整变更,打开 REPLICA IDENTITY FULL
ALTER TABLE public.shipments REPLICA IDENTITY FULL;
INSERT INTO shipments
VALUES (default,10001,'Beijing', 'Shanghai', false),
(default,10002,'Hangzhou', 'Shanghai', false),
(default,10003,'Shanghai', 'Hangzhou', false);
这里的设计逻辑:
shipments.order_id对应 MySQL 的orders.order_id;origin/destination/is_arrived提供物流维度信息;- 打开
REPLICA IDENTITY FULL是为了保证更新/删除时 CDC 能获取旧值。
五、启动 Flink 集群 & SQL Client
进入 flink-1.18.0 目录:
bash
cd flink-1.18.0
启动 Flink 集群:
bash
./bin/start-cluster.sh
打开浏览器访问:http://localhost:8081,确认 Flink 正常运行。
启动 Flink SQL CLI:
bash
./bin/sql-client.sh
如果进入了交互式 SQL 客户端,说明一切就绪。
六、在 Flink SQL 里创建 CDC 源表 & ES Sink 表
1. 开启 checkpoint(Streaming ETL 的"心跳")
sql
-- Flink SQL
SET execution.checkpointing.interval = 3s;
checkpoint 用来:
- 保证 Flink 任务的容错;
- 对 sink(比如 Elasticsearch)起到"提交点"的作用;
- 对 CDC 源,在全量 → 增量阶段的切换上,保证顺序性。
2. MySQL CDC - products 源表
sql
CREATE TABLE products (
id INT,
name STRING,
description STRING,
PRIMARY KEY (id) NOT ENFORCED
) WITH (
'connector' = 'mysql-cdc',
'hostname' = 'localhost',
'port' = '3306',
'username' = 'root',
'password' = '123456',
'database-name' = 'mydb',
'table-name' = 'products'
);
3. MySQL CDC - orders 源表
sql
CREATE TABLE orders (
order_id INT,
order_date TIMESTAMP(0),
customer_name STRING,
price DECIMAL(10, 5),
product_id INT,
order_status BOOLEAN,
PRIMARY KEY (order_id) NOT ENFORCED
) WITH (
'connector' = 'mysql-cdc',
'hostname' = 'localhost',
'port' = '3306',
'username' = 'root',
'password' = '123456',
'database-name' = 'mydb',
'table-name' = 'orders'
);
两张 MySQL 源表都是通过 mysql-cdc connector 捕获 binlog,实现快照+实时变更同步。
4. Postgres CDC - shipments 源表
sql
CREATE TABLE shipments (
shipment_id INT,
order_id INT,
origin STRING,
destination STRING,
is_arrived BOOLEAN,
PRIMARY KEY (shipment_id) NOT ENFORCED
) WITH (
'connector' = 'postgres-cdc',
'hostname' = 'localhost',
'port' = '5432',
'username' = 'postgres',
'password' = 'postgres',
'database-name' = 'postgres',
'schema-name' = 'public',
'table-name' = 'shipments',
'slot.name' = 'flink'
);
这里有几个点值得注意:
postgres-cdc会使用逻辑复制槽(logical replication slot)读取 WAL;slot.name = 'flink'指定复制槽名,方便管理;- 前面我们在 PG 里已经设置了
REPLICA IDENTITY FULL,保证更新/删除事件内容完整。
5. Elasticsearch Sink - enriched_orders 目标表
这张表用于存储富化后的订单数据。
sql
CREATE TABLE enriched_orders (
order_id INT,
order_date TIMESTAMP(0),
customer_name STRING,
price DECIMAL(10, 5),
product_id INT,
order_status BOOLEAN,
product_name STRING,
product_description STRING,
shipment_id INT,
origin STRING,
destination STRING,
is_arrived BOOLEAN,
PRIMARY KEY (order_id) NOT ENFORCED
) WITH (
'connector' = 'elasticsearch-7',
'hosts' = 'http://localhost:9200',
'index' = 'enriched_orders'
);
connector = 'elasticsearch-7':对应 ES 7.x;index = 'enriched_orders':ES 中的索引名;- 以
order_id作为主键,是因为我们希望"每个订单一行",更新时覆盖原数据。
七、用一条 SQL 完成 Streaming ETL:多源 Join → ES
真正酷的地方来了:
整条 Streaming ETL pipeline,实际上就一条
INSERT INTO ... SELECT ...。
sql
INSERT INTO enriched_orders
SELECT
o.*,
p.name,
p.description,
s.shipment_id,
s.origin,
s.destination,
s.is_arrived
FROM orders AS o
LEFT JOIN products AS p ON o.product_id = p.id
LEFT JOIN shipments AS s ON o.order_id = s.order_id;
这条 SQL 做了几件事:
- 从
orders流中读取订单变更; - 左连接
products,拿到商品名和描述; - 再左连接
shipments,拿到物流信息; - 把所有字段合在一起写入 Elasticsearch 的
enriched_orders索引。
此时:
- Flink 会启动一个持续运行的流任务;
- 任务会自动拉取 MySQL + Postgres 的数据变更并实时 Join;
- 结果持续推送到 ES。
八、在 Kibana 里查看富化后的订单
-
打开 Kibana:http://localhost:5601
-
进入 Index Pattern 创建页面:
- 地址类似:
http://localhost:5601/app/kibana#/management/kibana/index_pattern - 创建一个 index pattern:
enriched_orders*
- 地址类似:
-
进入 Discover 页面:
- 地址类似:
http://localhost:5601/app/kibana#/discover - 选择刚才创建的 index pattern,就能看到富化后的订单记录了:
每条记录会包含:
- 订单基础信息:
order_id,order_date,customer_name,price,order_status... - 商品信息:
product_name,product_description - 物流信息:
shipment_id,origin,destination,is_arrived
- 地址类似:
这就是你要的实时富化订单视图。
九、验证"实时性":动一动上游,看 ES 怎么跟着跑
下面我们通过一组操作,来观察 Streaming ETL 链路的实时效果。
1. 在 MySQL 中插入新订单
sql
-- MySQL
INSERT INTO orders
VALUES (default, '2020-07-30 15:22:00', 'Jark', 29.71, 104, false);
这会生成一个新订单(假设自动生成的 order_id = 10004)。
2. 在 Postgres 中为新订单插入一条 shipment
sql
-- Postgres
INSERT INTO shipments
VALUES (default,10004,'Shanghai','Beijing',false);
此时你在 Kibana 的 enriched_orders 里应该能看到一条新记录:
order_id = 10004is_arrived = false- 起始地/目的地为
Shanghai→Beijing。
3. 更新订单状态(已下单 → 已完成)
sql
-- MySQL
UPDATE orders
SET order_status = true
WHERE order_id = 10004;
4. 更新物流状态(未到达 → 已到达)
sql
-- Postgres
UPDATE shipments
SET is_arrived = true
WHERE shipment_id = 1004;
回到 Kibana,你会看到同一条 order_id = 10004 的记录:
order_status变为true;is_arrived也变为true。
这其实就是:
跨库更新 在 Flink CDC + Flink SQL Streaming ETL 下被正确处理、合流、并实时写入 ES。
5. 删除订单,看下游如何同步删除
sql
-- MySQL
DELETE FROM orders
WHERE order_id = 10004;
稍后在 Kibana 中刷新,你会发现:
order_id = 10004的记录从enriched_orders中消失。
说明 Flink SQL + ES sink 正确处理了删除事件(通常会变成一个 delete-by-id 请求)。
十、清理环境
实验结束后,可以用以下命令关闭环境,避免资源占用:
bash
# 在 docker-compose.yml 所在目录
docker-compose down
停止 Flink 集群,在 flink-1.18.0 目录下执行:
bash
./bin/stop-cluster.sh
十一、总结:Flink CDC Streaming ETL 的几个关键价值点
这个 Demo 其实展示的是一类 非常有代表性 的场景:
上游多库多源(MySQL + Postgres)
通过 Flink CDC 统一变更流
在 Flink SQL 中做实时 Join / 富化
最终写入 ES 做搜索 / 看板。
相比传统"导数 + 定时任务 + 批处理"的模式,它有几个明显优势:
-
真正的 Streaming ETL
- 数据一写入 MySQL/Postgres,
变更几乎就会在秒级到达 Elasticsearch。
- 数据一写入 MySQL/Postgres,
-
多源数据统一建模
- 在 Flink SQL 里用标准 SQL 语法做 Join/清洗/投影;
- 逻辑清晰、上线快、不依赖 Java/Scala 开发。
-
对上游入侵极小
- MySQL / Postgres 只需开启 binlog / wal;
- 不需要改业务代码、也不需要额外同步程序。
-
下游灵活可拓展
- 本文示例是 Elasticsearch;
- 实际中可以拓展为 Kafka、Iceberg/Hudi/Paimon、Doris/StarRocks 等。