4、实时数据管道搭建实战:从数据源采集到Kafka、使用Flink进行简单的ETL处理、将结果写入Redis/MySQL

这一章,我们直接动手。

前面聊了那么多概念和架构,现在终于要落地了。实时数据管道,说白了就是把数据从源头捞出来,经过清洗和转换,最后送到目标存储里。整个过程要快,要稳,还不能丢数据。

我刚开始做实时管道的时候,踩过不少坑。有一次线上数据延迟了十几分钟,排查了半天才发现是Kafka的消费者配置有问题。嗯,从那以后我对每个环节的参数都格外小心。

本章核心目标:搭建一条完整的实时数据管道,涵盖数据采集、消息队列、流处理和结果存储四个环节。

4.1 整体架构概览

先看一张图,把整个流程串起来。你想想看,数据从产生到落地,到底经历了什么?

实时数据管道整体架构 数据源 MySQL Binlog / 日志 数据采集 Canal / Filebeat 消息队列 Kafka 流处理 Flink ETL 结果存储 Redis / MySQL 💡 关键点: 数据源 → 采集 → Kafka → Flink → 存储,每个环节都要保证低延迟和高可用。

整个管道分四步走:

  • 数据源采集:从业务数据库或日志文件中捕获变更数据
  • 消息队列:用Kafka做缓冲和解耦,削峰填谷
  • 流处理:Flink负责清洗、转换和聚合
  • 结果存储:写入Redis做缓存,或写入MySQL做持久化

4.2 数据源采集:从MySQL Binlog说起

实时管道的第一步,是拿到数据。最常见的场景是监听MySQL的变更。

我个人习惯用Canal来做这件事。Canal会伪装成MySQL的从库,实时接收Binlog的变更事件。你想想看,每次有数据插入、更新或删除,Canal都能第一时间感知到。

小提示:生产环境中记得开启MySQL的Binlog,并且设置格式为ROW。否则Canal拿不到完整的变更数据。

配置示例:

# canal.properties
canal.instance.master.address=127.0.0.1:3306
canal.instance.dbUsername=canal
canal.instance.dbPassword=canal123
canal.instance.connectionCharset=UTF-8
canal.instance.filter.regex=test\\..*
# 监听test库下的所有表

如果是日志类的数据源,比如Nginx访问日志,我会用Filebeat。它轻量、资源占用低,适合部署在每台服务器上。

4.3 Kafka:管道的核心枢纽

Kafka在这里扮演的角色,说白了就是一个超级缓冲区。采集层把数据扔进来,Flink再从里面拉走。两边互不干扰,各自按自己的节奏跑。

我记得有一次业务流量突然暴涨,Flink处理不过来。但因为Kafka扛住了,数据一点没丢。等Flink恢复后,自动从断点继续消费。这就是消息队列的价值。

Kafka的关键配置:

参数 推荐值 说明
num.partitions 3~6 分区数,影响并行度
replication.factor 2~3 副本数,保证高可用
retention.ms 604800000 数据保留7天
acks all 确保数据不丢失

⚠️ 注意:分区数不是越多越好。分区太多会导致文件句柄暴增,反而影响性能。我一般建议按业务流量预估,留20%的余量。

4.4 Flink ETL:清洗与转换

数据到了Flink,就该干活了。ETL说白了就三件事:过滤掉脏数据、转换字段格式、做简单的聚合。

我曾经接手过一个项目,上游数据里混入了大量空值和乱码。如果不做过滤,下游的Redis和MySQL会直接报错。所以,第一件事永远是数据清洗。

一个典型的Flink ETL作业:

// 1. 从Kafka读取数据
DataStream<String> sourceStream = env
    .addSource(new FlinkKafkaConsumer<>("input-topic",
        new SimpleStringSchema(), kafkaProps));

// 2. 数据清洗:过滤空值和格式异常
DataStream<UserEvent> cleanStream = sourceStream
    .filter(line -> line != null && line.contains("user_id"))
    .map(line -> parseToUserEvent(line));

// 3. 数据转换:字段补齐、格式统一
DataStream<UserEvent> transformedStream = cleanStream
    .map(event -> {
        event.setTimestamp(System.currentTimeMillis());
        event.setStatus("processed");
        return event;
    });

// 4. 简单聚合:按用户ID统计事件次数
DataStream<UserCount> aggregatedStream = transformedStream
    .keyBy(event -> event.getUserId())
    .timeWindow(Time.seconds(10))
    .apply(new CountWindowFunction());

核心原则:Flink的ETL尽量保持无状态。如果必须做状态管理,记得设置合理的TTL,避免状态无限膨胀。

4.5 结果写入:Redis和MySQL

处理完的数据,最终要落到存储里。这里分两种情况:

  • Redis:适合做实时缓存、排行榜、计数器。写入速度快,但数据量不宜过大。
  • MySQL:适合做持久化存储,方便后续查询和分析。写入速度相对慢,需要控制写入频率。

Flink写入Redis的示例:

// 使用Flink Redis Connector
DataStream<Tuple2<String, String>> redisStream = transformedStream
    .map(event -> new Tuple2<>(event.getUserId(), event.getEventType()));

redisStream.addSink(new RedisSink<>(redisConfig, new RedisCommandDescription(RedisCommand.SET)));

Flink写入MySQL的示例:

// 使用JDBC Sink,批量写入
aggregatedStream.addSink(JdbcSink.sink(
    "INSERT INTO user_counts(user_id, count, window_end) VALUES (?, ?, ?)",
    (statement, count) -> {
        statement.setString(1, count.getUserId());
        statement.setLong(2, count.getCount());
        statement.setLong(3, count.getWindowEnd());
    },
    JdbcExecutionOptions.builder()
        .withBatchSize(1000)
        .withBatchIntervalMs(5000)
        .build(),
    new JdbcConnectionOptions.JdbcConnectionOptionsBuilder()
        .withUrl("jdbc:mysql://localhost:3306/test")
        .withDriverName("com.mysql.cj.jdbc.Driver")
        .withUsername("root")
        .withPassword("root123")
        .build()
));

经验之谈:写入MySQL时,一定要用批量写入,不要一条一条插。我见过有人逐条插入,结果Flink的吞吐量直接掉到几百条每秒。改成批量后,轻松上万。

4.6 避坑指南

做实时管道,坑不少。我挑几个最常见的说说:

  • 数据重复:Flink的Exactly-Once语义需要配合Kafka的事务和检查点。如果配置不当,可能会收到重复数据。我建议在目标存储里做幂等处理。
  • 背压问题:当Flink处理速度跟不上Kafka的写入速度时,会出现背压。这时候要么加资源,要么优化算子逻辑。
  • 连接超时:Redis和MySQL的连接池要设置合理。我曾经因为连接池太小,导致Flink频繁重连,性能骤降。

⚠️ 重要提醒:生产环境一定要开启Flink的Checkpoint。否则一旦作业失败,数据会从Kafka的最早偏移量重新消费,造成大量重复甚至数据错乱。

好了,这一章的内容就到这里。管道搭起来了,数据也跑通了。接下来,你可以根据自己的业务场景,调整ETL逻辑和存储策略。记住,实时管道没有银弹,适合自己业务的才是最好的。