【Flink】Flink数据源Source详解

Flink数据源Source详解:内置与自定义Connector

前言

上一篇我们学习了 Flink 的编程模型,知道了一个 Flink 程序由 Source → Transformation → Sink 三部分组成。这篇文章我们来深挖 Source------数据从哪来?

Source 是整个数据管道的起点,它决定了你能处理什么数据。Flink 内置了丰富的 Source,也支持自定义。这篇文章会带你全面了解 Flink 的数据源体系。

🏠个人主页:你的主页


目录


一、Source概述

1.1 什么是Source?

Source(数据源) 是 Flink 程序的数据入口,负责从外部系统读取数据并转换为 Flink 内部的 DataStream。

复制代码
┌─────────────────────────────────────────────────────────────────────┐
│                        数据流向                                       │
│                                                                      │
│    外部系统                   Flink                    外部系统       │
│  ┌──────────┐            ┌──────────────┐           ┌──────────┐   │
│  │  Kafka   │ ───────→   │              │ ───────→  │  MySQL   │   │
│  │  File    │            │  DataStream  │           │  Redis   │   │
│  │  MySQL   │  Source    │   处理逻辑    │   Sink    │  ES      │   │
│  │  Socket  │ ───────→   │              │ ───────→  │  Kafka   │   │
│  └──────────┘            └──────────────┘           └──────────┘   │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

1.2 Source的分类

按数据边界分:

类型 说明 示例
有界Source 数据有明确终点 文件、数据库查询结果
无界Source 数据源源不断 Kafka、Socket

按内置/自定义分:

类型 说明 示例
内置Source Flink自带或官方Connector Kafka、File、JDBC
自定义Source 用户自己实现 对接私有协议、特殊系统

1.3 新老API对比

Flink 1.12 开始推出了新版 Source API(Source 接口),老版(SourceFunction)逐步废弃:

java 复制代码
// 老版API(已废弃)
env.addSource(new FlinkKafkaConsumer<>(...));

// 新版API(推荐)
env.fromSource(
    KafkaSource.<String>builder().build(),
    WatermarkStrategy.noWatermarks(),
    "Kafka Source"
);

为什么换新API? 新版 Source 支持:

  • 流批一体
  • 更好的 Watermark 集成
  • 更清晰的架构

二、新版Source架构

2.1 核心组件

新版 Source 采用 SplitEnumerator + Reader 架构:

复制代码
┌─────────────────────────────────────────────────────────────────────┐
│                    新版 Source 架构                                   │
│                                                                      │
│  ┌─────────────────────────────────────────────────────────────┐    │
│  │                    SplitEnumerator                           │    │
│  │                     (运行在 JobManager)                       │    │
│  │                                                               │    │
│  │    • 发现数据分片 (Split)                                     │    │
│  │    • 将分片分配给 Reader                                      │    │
│  │    • 处理 Reader 汇报的已完成分片                              │    │
│  └───────────────────────────┬─────────────────────────────────┘    │
│                               │ 分配 Split                           │
│                               ↓                                      │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐                 │
│  │ SourceReader │  │ SourceReader │  │ SourceReader │                │
│  │   (TM 1)     │  │   (TM 2)     │  │   (TM 3)     │                │
│  │              │  │              │  │              │                │
│  │ 读取分片数据  │  │ 读取分片数据  │  │ 读取分片数据  │                │
│  └─────────────┘  └─────────────┘  └─────────────┘                 │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘
组件 运行位置 职责
Source 入口 工厂,创建Enumerator和Reader
SplitEnumerator JobManager 发现、分配数据分片
SourceReader TaskManager 实际读取数据
Split - 数据分片的抽象(如Kafka分区、文件块)

2.2 用大白话解释

想象你是一个快递公司老板:

  • SplitEnumerator = 调度中心:扫描有哪些快递(Split),分配给快递员

  • SourceReader = 快递员:负责实际取件并送到仓库(Flink)

  • Split = 快递:每个快递就是一个数据分片

    调度中心发现:今天有3个取件点(Split)

    │ 分配任务

    快递员A ← 取件点1
    快递员B ← 取件点2
    快递员C ← 取件点3

    │ 取回快递(数据)

    送到仓库(Flink DataStream)


三、内置Source详解

3.1 常用Source一览

复制代码
┌────────────────────────────────────────────────────────────────────┐
│                    Flink 内置 Source                                │
│                                                                     │
│  ┌───────────────────────┐  ┌───────────────────────┐             │
│  │   消息队列              │  │   文件系统              │             │
│  │   • KafkaSource        │  │   • FileSource         │             │
│  │   • PulsarSource       │  │   • 支持 HDFS/S3/本地   │             │
│  │   • RabbitMQ           │  │                        │             │
│  └───────────────────────┘  └───────────────────────┘             │
│                                                                     │
│  ┌───────────────────────┐  ┌───────────────────────┐             │
│  │   数据库               │  │   开发测试             │             │
│  │   • JdbcSource         │  │   • fromElements      │             │
│  │   • MongoDBSource      │  │   • fromCollection    │             │
│  │   • CDC Source         │  │   • socketTextStream  │             │
│  └───────────────────────┘  └───────────────────────┘             │
└────────────────────────────────────────────────────────────────────┘

3.2 开发测试用Source

开发时快速测试用的简单Source:

java 复制代码
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

// 1. 从集合创建(最简单)
DataStream<String> fromCollection = env.fromElements("hello", "flink", "world");

// 2. 从Java集合创建
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
DataStream<Integer> fromList = env.fromCollection(numbers);

// 3. 从Socket读取(常用于本地调试)
// 先启动: nc -lk 9999
DataStream<String> fromSocket = env.socketTextStream("localhost", 9999);

// 4. 生成序列(有界)
DataStream<Long> sequence = env.fromSequence(1, 100);

使用场景

  • fromElements/fromCollection:单元测试、快速验证逻辑
  • socketTextStream:本地开发调试
  • fromSequence:性能测试、生成测试数据

四、Kafka Source实战

Kafka 是最常用的数据源,重点讲解。

4.1 添加依赖

xml 复制代码
<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-connector-kafka</artifactId>
    <version>3.0.0-1.17</version>
</dependency>

版本对应关系

Flink版本 Connector版本
1.17.x 3.0.0-1.17
1.16.x 1.16.x
1.15.x 1.15.x

4.2 基础用法

java 复制代码
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.serialization.SimpleStringSchema;
import org.apache.flink.connector.kafka.source.KafkaSource;
import org.apache.flink.connector.kafka.source.enumerator.initializer.OffsetsInitializer;

public class KafkaSourceExample {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        
        // 构建 Kafka Source
        KafkaSource<String> kafkaSource = KafkaSource.<String>builder()
            // Kafka 集群地址
            .setBootstrapServers("localhost:9092")
            // 订阅的 Topic
            .setTopics("input-topic")
            // 消费者组ID
            .setGroupId("flink-consumer-group")
            // 起始消费位置
            .setStartingOffsets(OffsetsInitializer.earliest())
            // 反序列化器
            .setValueOnlyDeserializer(new SimpleStringSchema())
            .build();
        
        // 使用 Source
        DataStream<String> stream = env.fromSource(
            kafkaSource,
            WatermarkStrategy.noWatermarks(),
            "Kafka Source"
        );
        
        stream.print();
        env.execute("Kafka Source Demo");
    }
}

4.3 消费起始位置配置

java 复制代码
// 1. 从最早的数据开始消费(适合首次启动)
.setStartingOffsets(OffsetsInitializer.earliest())

// 2. 从最新的数据开始消费(跳过历史数据)
.setStartingOffsets(OffsetsInitializer.latest())

// 3. 从消费者组的已提交位置开始(默认行为)
.setStartingOffsets(OffsetsInitializer.committedOffsets())

// 4. 如果没有已提交的位置,则从最早开始
.setStartingOffsets(OffsetsInitializer.committedOffsets(OffsetResetStrategy.EARLIEST))

// 5. 从指定时间戳开始
.setStartingOffsets(OffsetsInitializer.timestamp(1609459200000L))

// 6. 从指定的 offset 开始
Map<TopicPartition, Long> offsets = new HashMap<>();
offsets.put(new TopicPartition("topic", 0), 100L);
.setStartingOffsets(OffsetsInitializer.offsets(offsets))

4.4 消费者组与Offset管理

java 复制代码
KafkaSource<String> kafkaSource = KafkaSource.<String>builder()
    .setBootstrapServers("localhost:9092")
    .setTopics("input-topic")
    .setGroupId("my-group")
    // 消费语义
    .setProperty("enable.auto.commit", "false")  // Flink自己管理offset
    .setProperty("auto.offset.reset", "earliest")
    .setValueOnlyDeserializer(new SimpleStringSchema())
    .build();

Offset提交策略

模式 说明
无 Checkpoint 周期性提交到 Kafka
有 Checkpoint Checkpoint 完成后提交

4.5 订阅多个Topic

java 复制代码
// 方式1:指定Topic列表
.setTopics("topic1", "topic2", "topic3")

// 方式2:使用正则表达式
.setTopicPattern("order-.*")  // 匹配所有 order- 开头的 Topic

// 方式3:指定Topic和分区
.setPartitions(
    new HashSet<>(Arrays.asList(
        new TopicPartition("topic1", 0),
        new TopicPartition("topic1", 1),
        new TopicPartition("topic2", 0)
    ))
)

4.6 自定义反序列化

处理复杂数据类型(如JSON):

java 复制代码
// 1. 使用 Jackson 反序列化 JSON
public class OrderDeserializer implements DeserializationSchema<Order> {
    
    private final ObjectMapper mapper = new ObjectMapper();
    
    @Override
    public Order deserialize(byte[] bytes) throws IOException {
        return mapper.readValue(bytes, Order.class);
    }
    
    @Override
    public boolean isEndOfStream(Order order) {
        return false;  // 流永不结束
    }
    
    @Override
    public TypeInformation<Order> getProducedType() {
        return TypeInformation.of(Order.class);
    }
}

// 使用
KafkaSource<Order> kafkaSource = KafkaSource.<Order>builder()
    .setBootstrapServers("localhost:9092")
    .setTopics("orders")
    .setGroupId("order-consumer")
    .setValueOnlyDeserializer(new OrderDeserializer())
    .build();

4.7 完整生产配置示例

java 复制代码
KafkaSource<String> kafkaSource = KafkaSource.<String>builder()
    // 基础配置
    .setBootstrapServers("kafka1:9092,kafka2:9092,kafka3:9092")
    .setTopics("input-topic")
    .setGroupId("flink-app-001")
    
    // 起始位置
    .setStartingOffsets(OffsetsInitializer.committedOffsets(OffsetResetStrategy.EARLIEST))
    
    // 有界模式(批处理时使用)
    // .setBounded(OffsetsInitializer.latest())
    
    // 反序列化
    .setValueOnlyDeserializer(new SimpleStringSchema())
    
    // Kafka Consumer 高级配置
    .setProperty("session.timeout.ms", "30000")
    .setProperty("max.poll.records", "500")
    .setProperty("fetch.min.bytes", "1")
    .setProperty("fetch.max.wait.ms", "500")
    
    // 安全认证(如需要)
    // .setProperty("security.protocol", "SASL_PLAINTEXT")
    // .setProperty("sasl.mechanism", "PLAIN")
    
    .build();

// 配置 Watermark(处理事件时间)
WatermarkStrategy<String> watermarkStrategy = WatermarkStrategy
    .<String>forBoundedOutOfOrderness(Duration.ofSeconds(5))
    .withIdleness(Duration.ofMinutes(1));  // 处理空闲分区

DataStream<String> stream = env.fromSource(
    kafkaSource, 
    watermarkStrategy, 
    "Kafka Source"
);

五、File Source实战

5.1 基础用法

java 复制代码
import org.apache.flink.connector.file.src.FileSource;
import org.apache.flink.connector.file.src.reader.TextLineInputFormat;
import org.apache.flink.core.fs.Path;

// 读取单个文件
FileSource<String> fileSource = FileSource
    .forRecordStreamFormat(
        new TextLineInputFormat(),
        new Path("/data/input/data.txt")
    )
    .build();

// 读取目录下所有文件
FileSource<String> dirSource = FileSource
    .forRecordStreamFormat(
        new TextLineInputFormat(),
        new Path("/data/input/")
    )
    .build();

DataStream<String> stream = env.fromSource(
    fileSource,
    WatermarkStrategy.noWatermarks(),
    "File Source"
);

5.2 持续监控目录(流模式)

java 复制代码
FileSource<String> fileSource = FileSource
    .forRecordStreamFormat(
        new TextLineInputFormat(),
        new Path("/data/input/")
    )
    // 持续监控新文件
    .monitorContinuously(Duration.ofSeconds(10))
    .build();

使用场景:日志文件持续追加,需要实时处理新增内容。

5.3 读取CSV文件

java 复制代码
// 使用 CsvReaderFormat 读取 CSV
CsvReaderFormat<Order> csvFormat = CsvReaderFormat.forPojo(Order.class);

FileSource<Order> csvSource = FileSource
    .forRecordStreamFormat(csvFormat, new Path("/data/orders.csv"))
    .build();

5.4 读取不同存储系统

java 复制代码
// 本地文件系统
new Path("file:///data/input/")

// HDFS
new Path("hdfs://namenode:8020/data/input/")

// S3
new Path("s3://bucket-name/data/input/")

// OSS(阿里云)
new Path("oss://bucket-name/data/input/")

六、自定义Source

当内置Source无法满足需求时,可以自定义。

6.1 简单方式:实现SourceFunction(已废弃但简单)

java 复制代码
/**
 * 自定义 Source:生成随机订单
 * 注意:SourceFunction 已废弃,仅用于学习理解
 */
public class RandomOrderSource implements SourceFunction<Order> {
    
    private volatile boolean isRunning = true;
    private final Random random = new Random();
    
    @Override
    public void run(SourceContext<Order> ctx) throws Exception {
        while (isRunning) {
            // 生成随机订单
            Order order = new Order(
                "ORDER_" + System.currentTimeMillis(),
                "USER_" + random.nextInt(100),
                random.nextDouble() * 1000,
                System.currentTimeMillis()
            );
            
            // 发送数据
            ctx.collect(order);
            
            // 控制发送频率
            Thread.sleep(100);
        }
    }
    
    @Override
    public void cancel() {
        isRunning = false;
    }
}

// 使用
env.addSource(new RandomOrderSource());

6.2 新版方式:实现Source接口

新版 Source 需要实现三个组件:

java 复制代码
/**
 * 自定义数据库 Source(简化版)
 */
public class DatabaseSource implements Source<Record, DatabaseSplit, DatabaseEnumeratorState> {
    
    private final String jdbcUrl;
    private final String query;
    
    public DatabaseSource(String jdbcUrl, String query) {
        this.jdbcUrl = jdbcUrl;
        this.query = query;
    }
    
    @Override
    public Boundedness getBoundedness() {
        return Boundedness.BOUNDED;  // 有界数据源
    }
    
    @Override
    public SplitEnumerator<DatabaseSplit, DatabaseEnumeratorState> createEnumerator(
            SplitEnumeratorContext<DatabaseSplit> context) {
        return new DatabaseSplitEnumerator(context, jdbcUrl, query);
    }
    
    @Override
    public SourceReader<Record, DatabaseSplit> createReader(SourceReaderContext context) {
        return new DatabaseSourceReader(context, jdbcUrl);
    }
    
    // ... 其他方法省略(序列化器等)
}

实现复杂度较高,一般情况下建议:

  1. 优先使用官方 Connector
  2. 如需自定义,可以基于现有 Connector 扩展
  3. 简单场景用 RichParallelSourceFunction

6.3 使用RichSourceFunction(可并行)

java 复制代码
/**
 * 可并行的自定义 Source
 */
public class ParallelRandomSource extends RichParallelSourceFunction<Integer> {
    
    private volatile boolean isRunning = true;
    
    @Override
    public void run(SourceContext<Integer> ctx) throws Exception {
        int subtaskIndex = getRuntimeContext().getIndexOfThisSubtask();
        int numSubtasks = getRuntimeContext().getNumberOfParallelSubtasks();
        
        // 每个并行实例生成不同范围的数据
        int start = subtaskIndex * 1000;
        int end = start + 999;
        
        while (isRunning) {
            int value = start + new Random().nextInt(end - start);
            ctx.collect(value);
            Thread.sleep(100);
        }
    }
    
    @Override
    public void cancel() {
        isRunning = false;
    }
}

// 使用时可以设置并行度
env.addSource(new ParallelRandomSource()).setParallelism(4);

七、Source的并行度与分区

7.1 并行读取

Source 的并行度决定了同时有多少个 Reader 在读取数据:

复制代码
┌─────────────────────────────────────────────────────────────────────┐
│                    Source 并行读取                                    │
│                                                                      │
│   Kafka Topic: 6个分区                                               │
│   ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐                   │
│   │ P0  │ │ P1  │ │ P2  │ │ P3  │ │ P4  │ │ P5  │                   │
│   └──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘                   │
│      │       │       │       │       │       │                       │
│      └───────┴───────┼───────┴───────┼───────┘                       │
│                      │               │                               │
│   Source 并行度=3    ↓               ↓                               │
│   ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐       │
│   │  Reader 0       │ │  Reader 1       │ │  Reader 2       │       │
│   │  负责 P0, P1    │ │  负责 P2, P3    │ │  负责 P4, P5    │       │
│   └─────────────────┘ └─────────────────┘ └─────────────────┘       │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

7.2 并行度设置建议

java 复制代码
// 设置 Source 并行度
env.fromSource(kafkaSource, watermarkStrategy, "Kafka Source")
    .setParallelism(6);  // 等于 Kafka 分区数
场景 建议并行度
Kafka Source 等于或小于分区数
File Source 文件数或 CPU 核数
自定义 Source 根据数据源特性决定

注意:Source 并行度大于分区数时,多余的实例会空转浪费资源。

7.3 数据分发策略

Source 读取的数据如何分发给下游?

java 复制代码
// 1. Forward(默认,Source和下游并行度相同时)
source.map(...);  // 数据直接传递,不走网络

// 2. Rebalance(轮询分发)
source.rebalance().map(...);  // 均匀分发到所有并行实例

// 3. Shuffle(随机分发)
source.shuffle().map(...);  // 随机选择下游实例

// 4. Rescale(局部轮询)
source.rescale().map(...);  // 在相邻的并行实例间轮询

八、总结

这篇文章我们深入学习了 Flink 的数据源体系:

核心要点

主题 要点
Source架构 SplitEnumerator(分配分片)+ SourceReader(读取数据)
Kafka Source 最常用,支持精确一次语义,可配置起始位置
File Source 支持多种文件系统,可持续监控新文件
自定义Source 简单用SourceFunction,复杂用Source接口
并行度 Source并行度不超过数据分区数

Kafka Source核心配置

java 复制代码
KafkaSource.<String>builder()
    .setBootstrapServers("localhost:9092")     // 必须
    .setTopics("topic")                        // 必须
    .setGroupId("group")                       // 推荐
    .setStartingOffsets(OffsetsInitializer.earliest())  // 起始位置
    .setValueOnlyDeserializer(new SimpleStringSchema()) // 反序列化
    .build();

Source选择指南

复制代码
需要从 Kafka 读数据? → KafkaSource
需要从文件读数据?   → FileSource  
开发测试?          → fromElements / socketTextStream
对接私有系统?      → 自定义 Source

下一篇文章,我们将学习 Flink 算子大全,深入了解 map、filter、keyBy 等各种 Transformation 操作。


热门专栏推荐

等等等还有许多优秀的合集在主页等着大家的光顾,感谢大家的支持


文章到这里就结束了,如果有什么疑问的地方请指出,诸佬们一起来评论区一起讨论😊

希望能和诸佬们一起努力,今后我们一起观看感谢您的阅读🙏

如果帮助到您不妨3连支持一下,创造不易您们的支持是我的动力🌟

相关推荐
Hello.Reader2 小时前
Flink Materialized Table 完整部署与运维指南:架构、SQL Gateway、Catalog Store、调度刷新一站式落地
运维·架构·flink
PS1232322 小时前
隔爆型防爆压力变送器的多信号输出优势
大数据·人工智能
Jackyzhe2 小时前
Flink源码阅读:Watermark机制
大数据·flink
TG:@yunlaoda360 云老大2 小时前
如何通过华为云国际站代理商CSBS进行跨Region备份与容灾?
大数据·数据库·华为云
Hello.Reader2 小时前
Flink Materialized Table Quickstart本地 10 分钟跑通 CONTINUOUS / FULL
大数据·flink
DolphinDB智臾科技2 小时前
如何用脚本榨出C++级性能?微秒级低延时系统优化深度解析
大数据·c++·时序数据库·低延时·dolphindb
Macbethad3 小时前
网络安全渗透测试技术报告:攻防实践与技术路线分析
大数据
张彦峰ZYF3 小时前
从路径抽象到安全归档 Python 文件组织实战
大数据·python·从路径抽象到安全归档·python 文件组织实战
b***25113 小时前
汽车圆柱电池气动点焊机:串并联组合自动化焊接的核心驱动力
大数据·人工智能