深入浅出 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。

相关推荐
楠楠子呀3 天前
从零构建高性能 WhatsApp 链接生成器与重定向服务:架构与路由解析
redis·python·容器·架构·flink·散列表·flume
头茬韭菜3 天前
功能点 9:Flink Connector
大数据·flink·fluss
Faith_xzc4 天前
一条 SQL 顶一条 Flink 链路?Doris Streaming Job 持续导入全景解析
大数据·数据库·sql·flink
何升曜6 天前
Flink Slot 分配机制深度解析:从申请到部署的完整链路
flink
我变秃了也没变强7 天前
standalone模式部署flink集群
flink·apache
渣渣盟7 天前
Flink 流式写入文件终极指南:从 Row Format 到 Bucket 分区的深度剖析
大数据·flink
CIO_Alliance7 天前
AI算法系列(3)| 实时数据管道:CDC+Flink实现库存异动秒级响应
大数据·人工智能·flink
渣渣盟8 天前
Flink 写入 Redis 实战:数据模型选型、连接池调优与常见坑
大数据·redis·flink
渣渣盟9 天前
Flink + Kafka 数据写入实战:从API调用到端到端一致性精讲
flink·kafka·linq
TDengine (老段)13 天前
TDengine 第三方工具 — Telegraf、Kafka Connect、Flink、Spark
大数据·数据库·物联网·flink·kafka·时序数据库·tdengine