深入浅出 Flink DataStream API:原理与使用

一、背景

Flink 提供了四层 API 抽象,自底向上分别为:

sql 复制代码
┌─────────────────────────────────────────────────────────┐
│                  SQL(最高层声明式)                       │
├─────────────────────────────────────────────────────────┤
│                Table API(关系型操作)                     │
├─────────────────────────────────────────────────────────┤
│           DataStream API(流处理核心 API)  ← 本文重点     │
├─────────────────────────────────────────────────────────┤
│          ProcessFunction(最底层、最灵活)                 │
└─────────────────────────────────────────────────────────┘

DataStream API 的定位:在灵活性与易用性之间取得平衡------比 SQL/Table API 更底层、更灵活,可直接操作事件时间、状态和定时器;比 ProcessFunction 更具结构化,提供了丰富的预定义算子。

尽管 Flink 社区在大力推进 SQL/Table API 的功能覆盖,但在以下场景中 DataStream API 仍不可替代:

  • 复杂的自定义序列化/反序列化逻辑
  • 需要直接操作底层 State 后端(如 MapState 的 TTL 精细控制)
  • 异构多流的非等值关联(超出 SQL JOIN 能力范围)
  • 对延迟有极致要求的低延迟管道(需要精细控制 buffer timeout)

二、核心原理与架构

DataStream 执行架构图:

核心概念:

概念 说明
DataStream 不可变的数据集合抽象,支持有界/无界数据
Transformation 从一个或多个 DataStream 生成新 DataStream 的操作
Operator Transformation 的运行时实现
Operator Chain 相邻算子链接在同一线程内执行,减少序列化/网络开销
StreamGraph 用户代码的逻辑执行计划
JobGraph 优化后的物理执行计划(算子链合并)
ExecutionGraph 并行化后的执行图,每个算子对应多个并行实例

Flink DataStream 程序采用懒执行机制:调用 map()、filter() 等方法时,并不会立即执行计算,而是构建一个 DAG(有向无环图)。只有在显式调用 env.execute() 时,才会将整个 DAG 提交给集群执行。

这种设计的优势:

  • 允许 Flink 对整个 DAG 做全局优化(算子链接、资源分配)
  • 支持在提交前进行计划验证

三、程序骨架与执行模型

每个 Flink DataStream 程序遵循以下五步模式:

scss 复制代码
// 1. 获取执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

// 2. 配置执行参数
env.setParallelism(4);
env.enableCheckpointing(60000); // 60s 一次 checkpoint

// 3. 读取数据源
DataStream<String> source = env.addSource(new FlinkKafkaConsumer<>(...));

// 4. 定义转换逻辑
DataStream<Result> result = source
    .map(...)
    .keyBy(...)
    .window(...)
    .reduce(...);

// 5. 输出结果 & 触发执行
result.addSink(new FlinkKafkaProducer<>(...));
env.execute("My Flink Job");

执行流程图:

四、数据源 Source API

从 Flink 1.12 起推荐使用新的 Source 接口(统一批/流,支持更好的扩展性),旧的 SourceFunction 已被标记为过时。新接口将 Source 拆分为 SplitEnumerator(分片枚举)和 SourceReader(数据读取)两个组件。

scss 复制代码
┌──────────────────────────────────────────────────────┐
│                    Source (FLIP-27)                    │
├──────────────────────┬───────────────────────────────┤
│  SplitEnumerator     │     SourceReader              │
│  (运行在 JobManager) │  (运行在 TaskManager)          │
│                      │                               │
│  • 发现/分配分片     │  • 从分配的分片读取数据        │
│  • 协调多个 Reader   │  • 维护分片级别的状态          │
│  • 支持动态发现新分片│  • 发送数据到下游算子          │
└──────────────────────┴───────────────────────────────┘

常用预定义 Source:

scss 复制代码
// Kafka Source(推荐使用新接口)
KafkaSource<String> kafkaSource = KafkaSource.<String>builder()
    .setBootstrapServers("broker1:9092")
    .setTopics("input-topic")
    .setGroupId("my-group")
    .setStartingOffsets(OffsetsInitializer.earliest())
    .setValueOnlyDeserializer(new SimpleStringSchema())
    .build();

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

// 文件 Source
FileSource<String> fileSource = FileSource
    .forRecordStreamFormat(new TextLineInputFormat(), new Path("/data/input"))
    .monitorContinuously(Duration.ofSeconds(10))
    .build();

// Collection Source(用于测试)
DataStream<Integer> testStream = env.fromElements(1, 2, 3, 4, 5);

自定义Source示例:

typescript 复制代码
// 基于新 Source 接口(简化示意)
public class MySource implements Source<MyEvent, MySplit, MyEnumState> {
    
    @Override
    public Boundedness getBoundedness() {
        return Boundedness.CONTINUOUS_UNBOUNDED; // 无界流
    }
    
    @Override
    public SplitEnumerator<MySplit, MyEnumState> createEnumerator(
            SplitEnumeratorContext<MySplit> context) {
        return new MySplitEnumerator(context);
    }
    
    @Override
    public SourceReader<MyEvent, MySplit> createReader(
            SourceReaderContext context) {
        return new MySourceReader(context);
    }
    // ... 其他方法
}

五、转换算子 Transformation

Filter:条件过滤

scss 复制代码
// 过滤无效订单
DataStream<Order> validOrders = orders.filter(order -> 
    order.getAmount() > 0 && order.getUserId() != null
);

Map:一对一转换(适用场景:数据清洗、格式转换、字段提取等一对一映射操作。)

vbnet 复制代码
// 将JSON字符串解析为POJO
DataStream<Order> orders = rawStream.map(new MapFunction<String, Order>() {
    @Override
    public Order map(String json) throws Exception {
        return objectMapper.readValue(json, Order.class);
    }
});

// Lambda 简写
DataStream<Integer> doubled = intStream.map(value -> value * 2);

FlatMap:一对多转换(适用场景:一条记录展开为多条(如日志解析、订单拆分)、条件过滤+转换合一。)

csharp 复制代码
// 将订单拆分为多个订单项事件
DataStream<OrderItem> items = orders.flatMap(
    new FlatMapFunction<Order, OrderItem>() {
        @Override
        public void flatMap(Order order, Collector<OrderItem> out) {
            for (OrderItem item : order.getItems()) {
                out.collect(item);
            }
        }
    }
);

KeyBy:逻辑分区(key 类型不能是数组;POJO 作为 key 时必须正确重写 hashCode() 方法;KeyBy 会触发网络 Shuffle(hash 分区))

rust 复制代码
// 按用户ID分区
KeyedStream<Order, String> keyedOrders = orders.keyBy(Order::getUserId);

// 使用元组字段
KeyedStream<Tuple2<String, Integer>, String> keyed = tupleStream.keyBy(t -> t.f0);

Operator Chaining:Flink 默认会将可以链接的算子合并在同一个 Task 线程中执行,以减少线程切换和序列化开销。

scss 复制代码
链接前:Source → Map → Filter → KeyBy → Window → Sink
                                    ↓ (network shuffle)
链接后:[Source → Map → Filter] → [KeyBy → Window → Sink]
         (一个 Operator Chain)       (另一个 Operator Chain)

可通过以下方式控制链接行为:

scss 复制代码
// 全局禁用算子链
env.disableOperatorChaining();

// 单个算子断开前向链
stream.map(...).startNewChain();

// 单个算子完全禁用链接
stream.map(...).disableChaining();

六、数据汇 Sink API

与 Source 类似,Sink 也经历了接口重构。从1.14版本开始使用新的Sink接口(新统一接口,支持精确一次语义),旧的SinkFunction接口被标记为过时。

常用 Sink 示例:

scss 复制代码
// Kafka Sink(新接口)
KafkaSink<String> kafkaSink = KafkaSink.<String>builder()
    .setBootstrapServers("broker1:9092")
    .setRecordSerializer(
        KafkaRecordSerializationSchema.builder()
            .setTopic("output-topic")
            .setValueSerializationSchema(new SimpleStringSchema())
            .build()
    )
    .setDeliveryGuarantee(DeliveryGuarantee.EXACTLY_ONCE) // 精确一次
    .setTransactionalIdPrefix("my-app")
    .build();

stream.sinkTo(kafkaSink);

// FileSink(精确一次 + 文件滚动)
FileSink<String> fileSink = FileSink
    .forRowFormat(new Path("/output"), new SimpleStringEncoder<String>("UTF-8"))
    .withRollingPolicy(
        DefaultRollingPolicy.builder()
            .withRolloverInterval(Duration.ofMinutes(15))
            .withInactivityInterval(Duration.ofMinutes(5))
            .withMaxPartSize(MemorySize.ofMebiBytes(1024))
            .build()
    )
    .build();

stream.sinkTo(fileSink);

writeAsText() / print() 等便捷方法不参与 Checkpoint,仅适合调试,不保证 Exactly-Once。生产环境务必使用 sinkTo() 或 addSink() 结合支持事务的 Connector。

相关推荐
大大大大晴天1 天前
聊聊Flink的几种血缘方案
前端·flink
大大大大晴天️2 天前
主流CDC 组件对比选型指南
大数据·flink·cdc
大大大大晴天4 天前
Flink CDC 深度解析:从原理到实践的全链路指南
大数据·flink
数据开发 Yang 同学6 天前
【Flink 时间语义与窗口】Watermark 机制、迟到数据处理与生产踩坑
flink
本旺9 天前
Flink 2.x状态演进:理解解 1.x 与 2.x 状态存储机制
大数据·flink
Apache Flink9 天前
从结构化到多模态:Apache Flink,多模态数据处理的流式底座
大数据·flink·apache
婉然从物9 天前
Flink SQL 元数据持久化实战:从“每次重启表消失”到“Hive Metastore 永久存储”
hive·sql·flink
@SmartSi10 天前
新一代实时数据集成框架 Flink CDC 3.0 架构解析
flink·cdc
TTBIGDATA11 天前
【Ambari Plus】12.Flink 安装
hive·hadoop·flink·ambari·hdp·cdh·bigtop