07.flink source数据源的处理

image

 

既然你提到了数据源,结合你之前一直在搭建的实时日志/链路数据处理系统,数据源的选择和开发直接决定了整个系统的吞吐量和稳定性。

下面为你深度拆解这三种接口的区别、内置连接器的应用场景,并提供一个贴合你业务场景的完整实战 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 的,这意味着:

  1. 如果你的 Job 因为机器宕机重启,Flink 会从上一个成功的 Checkpoint 时间点恢复。
  2. 但是,你的自定义 Source 不知道该从哪里重新开始读取数据(比如 Kafka 的 Offset 是由 FlinkKafkaConsumer 自动维护的,但 TCP Socket 没有这个概念)。
  3. 结果就是:重启后,你可能会丢失宕机期间的日志数据,或者造成数据重复处理(取决于你的业务逻辑)。

解决方案:

  1. 使用官方连接器:如果是 Kafka,直接使用 FlinkKafkaConsumer,它会自动帮你做 Offset 的提交和恢复。
  2. 手动实现 Checkpoint 兼容:如果你必须写自定义 Source,需要实现 CheckpointedFunctionListCheckpointed 接口,在 snapshotState 方法中保存当前的读取进度(比如 TCP 读取的字节数、或者一个递增的 ID),在 initializeState 方法中恢复进度。

五、 下一步建议

既然你已经掌握了数据源的开发,接下来可以重点研究:

  1. 水位线 (Watermark):如果你的日志中包含时间戳(比如日志打印的时间),你需要学习如何生成 Watermark,以便进行窗口聚合(如计算每 5 分钟的错误日志数)。
  2. ProcessFunction:这是 Flink 中最底层的 API,可以让你在处理数据流时,注册定时器、查询和管理 Keyed State,非常适合做复杂的会话窗口迟到数据处理

你现在是在做纯学习,还是有具体的业务数据源(比如 MySQL binlog、自定义 TCP 日志)需要接入 Flink?如果有具体场景,我可以给你更针对性的代码和优化建议。

posted on 2026-08-19 14:43  luzhouxiaoshuai  阅读(10)  评论(0)    收藏  举报

导航