2. Flink 基础回顾:架构、DataStream API、时间语义与水位线

在搭建特征服务之前,必须牢固掌握 Flink 的核心基础。本章将系统回顾 Flink 的运行时架构、DataStream API 编程模型、时间语义以及水位线(Watermark)机制,为后续的因子计算与特征管道设计打下坚实的技术地基。

2.1 Flink 运行时架构

Flink 采用经典的 主从(Master-Worker) 架构,核心组件包括 JobManager 和 TaskManager。理解各角色的职责与协作方式,是进行集群部署与作业调优的前提。

组件 角色 核心职责
JobManager Master 接收作业、生成执行计划、协调 Checkpoint、故障恢复
TaskManager Worker 执行具体的 Task Slot,管理本地内存与网络缓冲
Dispatcher REST 入口 提供 Web UI 与 REST API,提交与管理作业
ResourceManager 资源调度 管理 TaskManager 的申请与释放(YARN/K8s)
关键设计:Task Slot 是 TaskManager 中的最小资源单元,每个 Slot 可以运行一个 SubTask。Slot 数量决定了作业的并行度上限。
Flink 运行时架构图 Flink 运行时架构 JobManager Dispatcher ResourceManager TaskManager Slot | Slot | Slot TaskManager Slot | Slot | Slot TaskManager Slot | Slot | Slot 数据源 (Kafka) 数据输出 (DB) 说明:JobManager 负责任务调度与容错,TaskManager 负责实际数据处理,Slot 是资源隔离的基本单位。

2.2 DataStream API 编程模型

DataStream API 是 Flink 流处理的核心抽象。一个典型的 Flink 流作业包含三个步骤:Source(数据源)、Transformation(转换算子)、Sink(数据输出)。

2.2.1 核心算子速览

算子类别 算子名称 用途说明
Source fromSource / addSource 从 Kafka、文件、Socket 等读取数据流
单流转换 map / flatMap / filter 数据映射、扁平化、过滤
分组聚合 keyBy / window / reduce 按 Key 分组、开窗、增量聚合
多流合并 union / connect / coMap 合并多条流或连接不同类型流
Sink toSink / addSink 输出到 Kafka、JDBC、HDFS 等
实战提示:在特征服务中,最常用的算子组合是 keyBy → window → aggregate,用于按股票代码或用户 ID 分组后计算滑动窗口内的统计特征。

2.2.2 典型作业骨架

// 1. 创建执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

// 2. 定义数据源(Kafka)
DataStream<String> sourceStream = env.fromSource(
    KafkaSource.<String>builder()
        .setBootstrapServers("localhost:9092")
        .setTopics("stock-tick")
        .setGroupId("flink-feature")
        .build(),
    WatermarkStrategy.noWatermarks(),
    "Kafka Source"
);

// 3. 转换逻辑:解析 JSON 并计算因子
DataStream<StockFeature> featureStream = sourceStream
    .map(json -> parseStockTick(json))
    .keyBy(StockTick::getSymbol)
    .window(SlidingEventTimeWindows.of(Time.minutes(5), Time.minutes(1)))
    .aggregate(new PriceFeatureAggregator());

// 4. 输出到特征存储
featureStream.sinkTo(
    JdbcSink.sink(
        "INSERT INTO stock_features VALUES (?, ?, ?, ?)",
        new FeatureJdbcWriter()
    )
);

// 5. 提交执行
env.execute("Stock Feature Pipeline");

2.3 时间语义

Flink 支持三种时间语义,在因子计算中必须根据业务场景正确选择:

时间语义 定义 特征服务典型场景
Event Time 事件实际发生的时间(嵌入在数据中) 交易行情因子、用户行为序列特征
Ingestion Time 数据进入 Flink 算子的时间 对时间精度要求不高的监控指标
Processing Time 算子本地系统时间 实时报警、当前状态快照
避坑指南:在特征服务中,必须优先使用 Event Time。Processing Time 无法处理乱序数据,会导致因子计算结果在回测与实盘之间不一致。使用 Event Time 时,必须配合水位线机制处理延迟数据。

2.4 水位线(Watermark)机制

水位线是 Flink 中衡量 Event Time 进度的核心机制,用于触发窗口计算并处理乱序数据。其本质是一个带时间戳的流式信号,表示“当前时间之前的数据已经全部到达”。

2.4.1 水位线的工作原理

  • 单调递增:水位线时间戳必须单调递增,不能回退。
  • 触发窗口:当水位线超过窗口的结束时间时,触发该窗口的计算。
  • 允许延迟:通过 allowedLateness 参数,可以控制窗口在触发后继续等待迟到数据的时间。
水位线机制与窗口触发示意图 水位线机制与窗口触发 0 5 10 15 20 窗口 [0, 10) 窗口 [10, 20) e1 e2 e3 e4 e5 Watermark = 12 当 Watermark = 12 时,窗口 [0,10) 被触发计算(因为 12 >= 10),窗口 [10,20) 继续等待。 迟到事件 e3(时间戳 9)若在 Watermark 12 之后到达,将被丢弃或进入侧输出流。

2.4.2 水位线生成策略

Flink 提供了两种内置的水位线生成策略,开发者也可以自定义:

策略 类名 适用场景
周期性 WatermarkStrategy.forBoundedOutOfOrderness 已知最大乱序时间(如 5 秒),周期性生成水位线
标点式 WatermarkStrategy.forGenerator 数据流中自带标记事件,用于精确控制水位线
特征服务最佳实践:对于股票行情数据,推荐使用 forBoundedOutOfOrderness(Duration.ofSeconds(5)),并配合 allowedLateness(Duration.ofSeconds(10)) 处理网络抖动导致的少量迟到数据。对于迟到超过 15 秒的数据,建议通过侧输出流(Side Output)单独处理,避免影响主窗口的因子准确性。

2.4.3 代码示例:设置水位线

// 定义水位线策略:允许最大乱序 5 秒
WatermarkStrategy<StockTick> watermarkStrategy =
    WatermarkStrategy.<StockTick>forBoundedOutOfOrderness(Duration.ofSeconds(5))
        .withTimestampAssigner((event, timestamp) -> event.getEventTime());

// 应用到数据流
DataStream<StockTick> streamWithWatermark = sourceStream
    .assignTimestampsAndWatermarks(watermarkStrategy);

通过以上回顾,我们为后续章节的因子计算管道搭建了坚实的 Flink 基础。下一章将正式进入特征服务的需求分析与架构设计。