07.flink source数据源的处理

既然你提到了数据源,结合你之前一直在搭建的实时日志/链路数据处理系统,数据源的选择和开发直接决定了整个系统的吞吐量和稳定性。
下面为你深度拆解这三种接口的区别、内置连接器的应用场景,并提供一个贴合你业务场景的完整实战 Demo。
一、 深度解析:三种自定义 Source 接口的抉择
当现有的连接器无法满足需求(比如你需要从一个特殊的 TCP Socket、共享内存或自研的日志收集客户端拉取数据)时,就需要通过 addSource(...) 来实现自定义数据源。这三种接口的核心区别在于并行度和功能丰富度:
1. SourceFunction (非并行)
- 特点:它是所有 Source 的基础,但不支持并行度设置(即
setParallelism(n)会失效,始终只有 1 个 Task 在处理)。 - 适用场景:数据本身必须严格保证全局有序,或者底层的读取 API 本身就不是线程安全的。
- 缺点:无法利用集群的多核优势,容易成为性能瓶颈。在现代 Flink 开发中,除非有特殊顺序要求,否则极少直接使用。
2. ParallelSourceFunction (并行)
- 特点:实现了
Runnable接口,支持设置大于 1 的并行度。 - 适用场景:基础的并行读取,不需要感知作业级别的全局生命周期。
3. RichParallelSourceFunction (强烈推荐 ✨)
- 特点:这是 Flink 开发中最常用的自定义并行 Source 基类。它不仅继承了
ParallelSourceFunction的并行能力,还增加了 Flink 著名的 “Rich” 系列功能:open(Configuration parameters):在每个 SubTask 启动之前执行,通常用于初始化昂贵的外部连接(如数据库连接池、Socket 连接、加载本地缓存文件等)。close():在 SubTask 停止之后执行,用于优雅地释放资源。- 可以获取运行时上下文
getRuntimeContext(),获取当前 Task 的索引、并行度等信息。
- 适用场景:几乎所有需要自定义读取逻辑的生产级开发。
二、 内置 Connectors 的应用场景
内置的连接器(Connectors),在实际项目中承担着“数据高速公路”的角色。结合你的日志处理模块,最常用的几个及其适用场景如下:
|
连接器 (Connector) |
读写支持 |
典型业务场景 |
|---|---|---|
|
Apache Kafka |
Source / Sink |
实时日志收集的首选。微服务产生的日志或事件通常先打入 Kafka,Flink 作为核心消费者进行实时计算。 |
|
Elasticsearch |
Sink |
计算结果(如异常告警日志、聚合后的报表数据)通常需要实时写入 ES 供 Kibana/Grafana 可视化查询。 |
|
Hadoop FileSystem |
Source / Sink |
离线/准实时兜底。处理历史积压数据(Backfill)时,会从 HDFS 读取大量静态日志文件;或者将处理结果落地为 Parquet/ORC 文件。 |
|
RabbitMQ / ActiveMQ |
Source / Sink |
传统的企业级异步解耦场景,部分遗留系统可能仍在使用。 |
|
Redis |
Sink |
常用于维表关联(Lookup Join),例如根据日志中的 UserID 去 Redis 查取用户的实时画像信息。 |
注意:使用这些内置连接器,必须在项目的
pom.xml中引入对应的依赖(例如flink-connector-kafka)。
三、 实战代码:实现一个自定义的 TCP 日志 Source
为了让你直观感受 RichParallelSourceFunction 的魅力,假设你的微服务日志是通过 Filebeat 统一收集后,通过一个 TCP Socket 推送到 Flink 服务器的特定端口(比如 9999 端口)。我们需要编写一个自定义的 TCP Source 来接收这些原始日志字符串。
1. Maven 依赖准备
确保你的 pom.xml 引入了 Flink 的核心依赖和 Web UI 所需的依赖:
xml
<dependencies>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-streaming-java_${scala.binary.version}</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-clients_${scala.binary.version}</artifactId>
<version>${flink.version}</version>
</dependency>
</dependencies>
2. 完整的 TCP Source 实现
java
import org.apache.flink.streaming.api.functions.source.RichParallelSourceFunction;
import org.apache.flink.streaming.api.functions.source.SourceFunction;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
public class TcpLogSource extends RichParallelSourceFunction<String> {
private volatile boolean isRunning = true;
private transient ServerSocket serverSocket;
private int port;
public TcpLogSource(int port) {
this.port = port;
}
@Override
public void open(Configuration parameters) throws Exception {
super.open(parameters);
// Rich 函数的优势:可以在这里初始化昂贵的资源
// 例如为每个 SubTask 打开一个独立的 ServerSocket 监听不同端口,或者初始化连接池
System.out.println("Opening TCP Socket on port: " + port + ", Task Name: " + getRuntimeContext().getTaskName());
serverSocket = new ServerSocket(port);
}
@Override
public void run(SourceContext<String> ctx) throws Exception {
System.out.println("TCP Source started on port: " + port);
while (isRunning) {
try {
// 阻塞等待客户端连接
Socket clientSocket = serverSocket.accept();
BufferedReader reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
String line;
// 读取客户端发送的数据(例如 Filebeat 推送的一行日志)
while (isRunning && (line = reader.readLine()) != null) {
// 收集数据,发送到下游
ctx.collect(line);
}
} catch (Exception e) {
if (isRunning) {
e.printStackTrace();
}
}
}
}
@Override
public void cancel() {
isRunning = false;
try {
if (serverSocket != null) {
serverSocket.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void close() throws Exception {
super.close();
// Rich 函数的优势:优雅关闭资源
System.out.println("Closing TCP Socket resources.");
if (serverSocket != null && !serverSocket.isClosed()) {
serverSocket.close();
}
}
}
3. 如何在 Job 中使用
java
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
public class TcpLogProcessingJob {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 如果你的 Flink 版本较新(1.15+),建议使用新的 Source API
// env.fromSource(new TcpLogSource(9999), WatermarkStrategy.noWatermarks(), "TCP Log Source");
// 兼容旧版本的写法(你之前用的 Flink 1.11+ 依然支持)
env.addSource(new TcpLogSource(9999))
.setParallelism(2) // 设置并行度为 2,因为有 2 个 TaskManager Slot
.map(log -> "Processed: " + log)
.print();
env.execute("TCP Log Processing Demo");
}
}
四、 生产环境避坑指南(Checkpoint 与状态管理)
在实际生产环境中,如果你使用自定义的 SourceFunction,有一个极其重要的点必须注意:Checkpoint(检查点)机制。
Flink 的核心优势是容错(Exactly-Once)。默认情况下,自定义的 SourceFunction 是不感知 Checkpoint 的,这意味着:
- 如果你的 Job 因为机器宕机重启,Flink 会从上一个成功的 Checkpoint 时间点恢复。
- 但是,你的自定义 Source 不知道该从哪里重新开始读取数据(比如 Kafka 的 Offset 是由 FlinkKafkaConsumer 自动维护的,但 TCP Socket 没有这个概念)。
- 结果就是:重启后,你可能会丢失宕机期间的日志数据,或者造成数据重复处理(取决于你的业务逻辑)。
解决方案:
- 使用官方连接器:如果是 Kafka,直接使用
FlinkKafkaConsumer,它会自动帮你做 Offset 的提交和恢复。 - 手动实现 Checkpoint 兼容:如果你必须写自定义 Source,需要实现
CheckpointedFunction或ListCheckpointed接口,在snapshotState方法中保存当前的读取进度(比如 TCP 读取的字节数、或者一个递增的 ID),在initializeState方法中恢复进度。
五、 下一步建议
既然你已经掌握了数据源的开发,接下来可以重点研究:
- 水位线 (Watermark):如果你的日志中包含时间戳(比如日志打印的时间),你需要学习如何生成 Watermark,以便进行窗口聚合(如计算每 5 分钟的错误日志数)。
- ProcessFunction:这是 Flink 中最底层的 API,可以让你在处理数据流时,注册定时器、查询和管理 Keyed State,非常适合做复杂的会话窗口或迟到数据处理。
你现在是在做纯学习,还是有具体的业务数据源(比如 MySQL binlog、自定义 TCP 日志)需要接入 Flink?如果有具体场景,我可以给你更针对性的代码和优化建议。
posted on 2026-08-19 14:43 luzhouxiaoshuai 阅读(10) 评论(0) 收藏 举报
浙公网安备 33010602011771号