4.1 DataStream API:Source/Transformation/Sink

Flink DataStream API 核心编程模型:StreamExecutionEnvironment 创建与配置 addSource/addSink、DataStream 转换操作符 map/filter/keyBy/window、类型系统与 TypeInformation、以及并行度设置与 Slot 分配原理。

DataStream API 概览

Flink DataStream API 是 Flink 的底层流处理 API,提供对数据流的细粒度控制。对比 SQL/Table API:

API抽象层级适用场景
DataStream底层(Java/Scala 代码)复杂逻辑、自定义状态、精准控制
Table API/SQL高层(声明式)简单 ETL、分析查询

eventd v3 使用 DataStream API,因为需要自定义窗口逻辑和 CEP 模式匹配。

StreamExecutionEnvironment

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

// 并行度(本地环境默认 = CPU 核数;集群默认看 parallelism.default,通常为 1)
env.setParallelism(4);

// 容错:Checkpoint 每 30 秒(默认 EXACTLY_ONCE)
env.enableCheckpointing(30_000);

时间语义不在 StreamExecutionEnvironment 上设置:Flink 1.12 起每个 Source 用自己的 WatermarkStrategy 决定事件时间,setStreamTimeCharacteristic 已废弃且不改变窗口语义。

getExecutionEnvironment() 自动检测运行环境:

  • 本地 IDE → 创建 LocalStreamEnvironment,同 JVM 多线程执行
  • 提交到 Flink 集群 → 连接集群 JobManager

本地环境不是 MiniCluster:要验证 Slot 分配、作业提交或 Checkpoint 行为,需要在测试里显式启动 MiniCluster(见 8.3 )。

Source:数据源

Kafka Source

import org.apache.flink.connector.kafka.source.KafkaSource;
import org.apache.flink.connector.kafka.source.enumerator.initializer.OffsetsInitializer;

KafkaSource<Event> source = KafkaSource.<Event>builder()
    .setBootstrapServers("kafka:9092")
    .setTopics("eventd-cleaned")
    .setGroupId("eventd-analytics")
    .setStartingOffsets(OffsetsInitializer.earliest())  // 或 latest()
    .setValueOnlyDeserializer(new EventDeserializationSchema())
    .build();

DataStream<Event> stream = env.fromSource(
    source,
    WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ofSeconds(10))
        .withTimestampAssigner((event, ts) -> event.getEventTime()),
    "Kafka Source"
);

withTimestampAssigner 不能省:只给 forBoundedOutOfOrderness 而不分配时间戳,Flink 不会把事件时间切成处理时间,而是沿用 Source 已写入记录的时间戳(KafkaSource 用 Kafka record timestamp;集合源常见为 0)。事件时间窗口仍按这个时间戳推进,只是它未必符合业务语义:集合源的时间戳为 0 时窗口可能永远不触发,Kafka 里 timestamp 类型不对时窗口又会落在错误的区间。只有处理时间窗口才按处理时间触发。

Socket Source(测试用)

DataStream<String> text = env.socketTextStream("localhost", 9999);

Collection Source(测试用)

List<Event> events = List.of(
    new Event("1", "order", 1000, Instant.now().toEpochMilli()),
    new Event("2", "order", 2000, Instant.now().toEpochMilli())
);
DataStream<Event> stream = env.fromCollection(events);

自定义 Source

public class EventSource implements SourceFunction<Event> {
    private volatile boolean running = true;

    @Override
    public void run(SourceContext<Event> ctx) throws Exception {
        while (running) {
            ctx.collect(generateEvent());
            Thread.sleep(100);  // 每 100ms 生成一条
        }
    }

    @Override
    public void cancel() {
        running = false;
    }
}

SourceFunction 在 1.20 已废弃(连同 env.addSource)。新代码应实现 Source 接口并用 env.fromSource(source, watermarkStrategy, name);它支持并行、分区与 Offset 状态。SourceFunction 只在维护旧代码或写最小示例时使用。

Transformation:数据转换

map / filter / flatMap

DataStream<EnrichedEvent> enriched = stream
    .filter(event -> event.getAmount() > 0)                    // 过滤
    .map(event -> new EnrichedEvent(event, lookupUser(event.getUserId())))  // 转换
    .flatMap((FlatMapFunction<EnrichedEvent, Alert>) (value, out) -> {
        if (value.event().getAmount() > 10000) {
            out.collect(new Alert("LARGE_ORDER", value));
        }
    });  // 1 对 N

这里 EnrichedEvent 用 record 定义(record EnrichedEvent(Event event, User user)),它自己的访问器只有 event() 和 user();金额仍从 Event 的 getAmount() 取。Event 是带 getter 的 POJO,两种风格不要混在同一个类上。

keyBy

DataStream<Event> keyed = stream.keyBy(Event::getUserId);

keyBy 按 key 的 hash 值分区,相同 key 的数据发送到同一分区。后续聚合操作(window、reduce)必须紧跟 keyBy。

reduce / aggregate

keyedStream
    .window(TumblingEventTimeWindows.of(Duration.ofMinutes(1)))
    .reduce((e1, e2) -> new Event(
        e1.getEventId(), e1.getType(),
        e1.getAmount() + e2.getAmount(), e1.getEventTime()));

Sink:数据输出

Kafka Sink

import org.apache.flink.connector.kafka.sink.KafkaSink;
import org.apache.flink.connector.kafka.sink.KafkaRecordSerializationSchema;

KafkaSink<String> sink = KafkaSink.<String>builder()
    .setBootstrapServers("kafka:9092")
    .setRecordSerializer(KafkaRecordSerializationSchema.builder()
        .setTopic("eventd-metrics")
        .setValueSerializationSchema(new SimpleStringSchema())
        .build())
    .setDeliveryGuarantee(DeliveryGuarantee.EXACTLY_ONCE)
    // EXACTLY_ONCE 必须配事务前缀,否则 build() 直接抛异常
    .setTransactionalIdPrefix("eventd-analytics")
    .build();

// Sink 的元素类型是 String,先用 map 把 Event 序列化成 JSON
stream.map(JsonMapper::toJson).sinkTo(sink);

打印 / 文件 / Redis Sink

stream.print();  // 打印到控制台(仅测试)

stream.writeAsText("/tmp/output.txt", WriteMode.OVERWRITE);  // 文件,写入方式见下方说明

// Redis:Apache Flink 没有官方 Redis 连接器,
// 也没有 RedisSink / RedisMapper 这两个类
// 自定义 Sink 的泛型必须与流元素类型一致(这里是 DataStream<Metric>)
metrics.addSink(new RedisMetricSink());  // 自己实现的 RichSinkFunction,见 7.2

两点容易踩坑:

  • stream.writeAsText(...) 仍然可用,但 DataStream.write* 这一组方法自 1.19 起已废弃,新代码用 FileSink 或 sinkTo(...)。
  • 不要照抄旧教程里的 RedisSink / RedisMapper。它们来自 Apache Bahir 的 Redis 连接器,该项目早已停止更新,不支持 Flink 1.20;org.apache.flink 组下也没有官方 Redis 连接器。自定义写法见 7.2 侧输出连接器 。

类型系统

Flink 需要知道数据的类型信息(TypeInformation)来选择序列化器和优化算子:

// 自动推断(Java API)
DataStream<Event> stream = env.fromCollection(events);  // 自动推断 Event 类型

// 显式声明(POJO 更精确)
env.getConfig().registerTypeWithKryoSerializer(Event.class, EventSerializer.class);

POJO 规则:

  • 类是 public 的
  • 有无参构造函数
  • 所有字段是 public 或有 getter/setter
  • 字段类型也是 POJO 或基本类型
public class Event {
    public String eventId;
    public String type;
    public String userId;
    public String symbol;
    public double amount;
    public long eventTime;

    public Event() {}  // 必须有:Flink 反序列化时要能实例化

    public Event(String eventId, String type, double amount, long eventTime) {
        this.eventId = eventId;
        this.type = type;
        this.amount = amount;
        this.eventTime = eventTime;
    }

    public String getEventId() { return eventId; }
    public String getType()    { return type; }
    public String getUserId()  { return userId; }
    public String getSymbol()  { return symbol; }
    public double getAmount()  { return amount; }
    public long getEventTime() { return eventTime; }
}

Event 字段类型都是 String/double/long,Flink 会把它当 POJO 用 PojoSerializer,比交给 Kryo 更省空间也更快。registerTypeWithKryoSerializer 只在这类自动推断失效(字段是接口、泛型擦除)时才需要。

并行度与 Slot

并行度 = 算子实例数(同时处理的分区数)
Slot = TaskManager 上的资源槽(CPU + 内存)

env.setParallelism(4):
    Source(4) → Map(4) → KeyBy(4) → Sink(4)
    ↑           ↑          ↑          ↑
  4 个实例    4 个实例   4 个实例   4 个实例

setParallelism 设的是默认值,不是只作用于某一个算子:没有单独覆盖的算子都会用 4。Source 的并行度还受分区数约束——Kafka Topic 只有 2 个分区时,即使默认并行度是 4,Source 实际也只会用到 2 个 subtask,多出来的处于空闲状态。

单个算子可覆盖并行度:

stream.map(new ExpensiveMapper()).setParallelism(8);  // 只给 map 配 8 并行

执行

// 阻塞执行(推荐 main 方法)
env.execute("eventd-analytics-job");

// 异步执行(测试或 Web 应用)
JobClient client = env.executeAsync("eventd-analytics-job");
client.getJobExecutionResult().get();  // 等待完成

本地环境测试

@Test
void testWordCount() throws Exception {
    StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
    env.setParallelism(1);

    DataStream<String> text = env.fromElements("hello world", "hello flink");

    DataStream<Tuple2<String, Integer>> counts = text
        .flatMap((FlatMapFunction<String, Tuple2<String, Integer>>) (value, out) -> {
            for (String word : value.split(" ")) {
                out.collect(Tuple2.of(word, 1));
            }
        })
        .returns(Types.TUPLE(Types.STRING, Types.INT))
        .keyBy(v -> v.f0)
        .sum(1);

    // 用 CollectSink 收集结果(实现见 8.3;集合必须是静态的,
    // 因为算子函数会被序列化后在新对象里执行)
    CollectSink.VALUES.clear();
    counts.addSink(new CollectSink<>());

    env.execute("test");

    List<Tuple2<String, Integer>> result = CollectSink.values();
    assertTrue(result.contains(Tuple2.of("hello", 2)));
    assertTrue(result.contains(Tuple2.of("world", 1)));
}

小结

概念API说明
Sourceenv.fromSource() / env.socketTextStream()数据输入
Transformationmap / filter / flatMap / keyBy数据转换
Sinkstream.sinkTo() / stream.print()数据输出
并行度env.setParallelism()全局或单算子
POJOpublic 类 + 无参构造 + getter类型推断
执行env.execute() / executeAsync()阻塞/异步

下一节 4.2 Flink 时间语义与 Watermark 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 15.3 全书总结与学习路径回顾
  2. 15.2 响应式流处理的演进趋势
  3. 15.1 引擎选型指南