2. Flink 基础回顾:数据流模型、时间语义与容错机制

在构建低延迟特征服务之前,必须重新审视 Flink 最核心的三大支柱:数据流模型决定了“如何描述计算”;时间语义决定了“何时触发计算”;状态后端与容错决定了“计算失败后如何恢复”。这三者共同构成了特征服务实时性与一致性的底层保障。

2.1 Flink 数据流模型:从 DAG 到连续处理

Flink 将任何计算任务抽象为 Dataflow Graph(数据流图),这是一个有向无环图(DAG)。图中的每个节点称为 Operator(算子),每条边代表数据流动的 Partition(分区)策略。

核心要点:特征服务的每个特征计算逻辑(如滑动平均、计数窗口、实时拼接)都可以映射为一个或多个 Flink Operator。理解数据流模型是设计低延迟 pipeline 的第一步。

2.1.1 数据流模型的关键概念

概念 说明 特征服务中的典型场景
Source 数据输入算子,从 Kafka、Pulsar、文件等读取 读取用户行为日志、交易流水
Transformation 数据转换算子,如 map、flatMap、filter、keyBy 特征提取、字段映射、异常过滤
Window 在有限时间/计数范围内聚合计算 最近 5 分钟点击率、过去 10 笔交易均值
Join 将两个或多个流按 key 关联 实时拼接用户画像与行为特征
Sink 结果输出算子,写入 Redis、HBase、Kafka 特征写入在线存储供模型推理调用

2.1.2 数据流模型与并行度

Flink 的 DAG 天然支持并行执行。每个算子可以设置并行度(Parallelism),数据通过 分区器(Partitioner)在并行子任务间路由。特征服务中,通常按 特征 Key(如 user_id、device_id)进行 keyBy 分区,确保同一实体的所有事件路由到同一个处理线程,从而保证状态一致性。

设计建议:在特征服务中,keyBy 的分区策略直接影响延迟和负载均衡。建议使用 keyBy(featureKey) 而非全局随机分区,避免跨分区状态访问带来的网络开销。
Flink 数据流模型与特征服务映射 Flink 数据流模型与特征服务映射 Source Kafka / Pulsar Transformation map / filter / keyBy Window 滑动 / 滚动窗口 Sink Redis / HBase 特征服务 Pipeline 示例 用户点击日志 → 过滤爬虫 → keyBy(user_id) → 滑动窗口(5min) → 计算点击率 → 写入 Redis 并行度与分区 每个算子可设置并行度 N,数据通过 keyBy 分区器路由到对应子任务,保证同一特征 Key 的状态本地性

2.2 时间语义:Event Time 与 Processing Time

时间语义是 Flink 流处理中最容易混淆、也最关键的概念。特征服务中,时间决定了“特征值反映的是哪个时间段的数据”。

2.2.1 两种时间语义对比

维度 Processing Time(处理时间) Event Time(事件时间)
定义 数据到达 Flink 算子的本地时钟时间 数据本身携带的发生时间戳
延迟 最低,无需等待乱序数据 较高,需设置 watermark 容忍乱序
准确性 受系统时钟、背压、重试影响,结果不稳定 准确反映事件发生顺序,结果可复现
特征服务适用场景 对时间精度要求低的简单计数、实时监控 需要精确时间窗口的特征(如最近 N 分钟均值)
容错恢复后行为 恢复后时间重置,可能丢失历史窗口 基于 watermark 和状态,可精确恢复
常见误区:很多特征服务初期使用 Processing Time 实现简单窗口,上线后发现特征值在重跑或故障恢复后与离线不一致。对于需要“可复现”的特征(如模型训练与线上特征对齐),必须使用 Event Time。

2.2.2 Watermark:Event Time 的进度信号

Watermark 是 Flink 中衡量 Event Time 进度的机制。它表示“当前时间戳之前的数据已经全部到达”。特征服务中,Watermark 的生成策略直接影响特征延迟与完整性之间的平衡。

  • 周期性 Watermark:每隔 N 毫秒生成一次,适合数据流稳定的场景。
  • 标点 Watermark:由数据流中的特殊标记触发,适合有明确事件边界的数据源。
  • 乱序容忍度:设置 maxOutOfOrderness 参数,允许事件时间戳在指定范围内乱序到达。
// 特征服务中典型的 Event Time 与 Watermark 配置示例
DataStream<UserEvent> stream = env
    .addSource(kafkaConsumer)
    .assignTimestampsAndWatermarks(
        WatermarkStrategy.<UserEvent>forBoundedOutOfOrderness(Duration.ofSeconds(5))
            .withTimestampAssigner((event, timestamp) -> event.getEventTime())
    );

// 使用 Event Time 定义滑动窗口计算特征
stream
    .keyBy(UserEvent::getUserId)
    .window(SlidingEventTimeWindows.of(Time.minutes(5), Time.minutes(1)))
    .aggregate(new ClickRateAggregate())
    .addSink(redisSink);
延迟与完整性的权衡:乱序容忍度设置越大,特征延迟越高,但窗口数据越完整。建议根据业务容忍度动态调整:在线推理场景容忍度可设为 2-5 秒;离线回填场景可设为 30-60 秒。
Event Time vs Processing Time 与 Watermark 机制 Event Time vs Processing Time 与 Watermark 机制 Processing Time 数据到达时间 = 窗口时间 事件 A (T=10) 到达 T=12 → 窗口 T=10~15 事件 B (T=8) 到达 T=16 → 窗口 T=15~20 结果不可复现,依赖到达顺序 Event Time 事件发生时间 = 窗口时间 事件 A (T=10) 到达 T=12 → 窗口 T=5~10 事件 B (T=8) 到达 T=16 → 窗口 T=5~10 结果可复现,基于事件时间戳 Watermark 机制 Watermark = 已观察到最大事件时间 - 乱序容忍度。当 Watermark 超过窗口结束时间时,触发窗口计算。 时间线示例(乱序容忍度 = 5s) 事件到达: T=12(evt=10), T=14(evt=12), T=18(evt=8) → Watermark = max(10,12,8) - 5 = 7 当 Watermark=7 时,窗口 [0,5) 触发计算;窗口 [5,10) 等待 Watermark ≥ 10

2.3 状态后端与容错机制

特征服务是有状态的:每个窗口的聚合值、每个实体的最新特征、Join 操作的缓存数据都存储在 Flink 的 State 中。状态后端决定了这些数据如何存储、如何访问;容错机制决定了故障时如何恢复。

2.3.1 状态后端对比

状态后端 存储位置 适用场景 延迟特征 容错方式
MemoryStateBackend TaskManager JVM 堆内存 调试、小状态、低延迟原型 极低(纯内存) Checkpoint 到 JobManager 堆
FsStateBackend TaskManager 堆内存 + 文件系统 生产环境,状态量中等 低(内存访问 + 异步快照) Checkpoint 到 HDFS/S3
RocksDBStateBackend 本地磁盘(RocksDB) + 文件系统 超大状态(TB 级),特征服务常用 中等(磁盘 I/O 开销) 增量 Checkpoint 到 HDFS/S3
特征服务选型建议:对于大多数在线特征服务,状态量通常在 GB ~ TB 级别,推荐使用 RocksDBStateBackend 配合增量 Checkpoint。它支持超大状态存储,且增量快照大幅减少容错开销。

2.3.2 Checkpoint 与 Savepoint

  • Checkpoint:自动触发的分布式快照,用于故障恢复。特征服务中建议设置 checkpointInterval 为 10-30 秒,平衡恢复时间与性能开销。
  • Savepoint:手动触发的快照,用于版本升级、作业迁移、A/B 测试。Savepoint 可以跨 Flink 版本恢复,是特征服务灰度发布的关键工具。

2.3.3 端到端 Exactly-Once 语义

特征服务要求“每条数据只影响特征一次”,避免重复计算导致特征值偏差。Flink 通过以下机制实现端到端 Exactly-Once:

  1. Source 端:Kafka 消费者记录偏移量到状态中,故障后从上次提交的偏移量重新消费。
  2. Flink 内部:Checkpoint 保存所有算子状态的一致性快照,故障后全体回滚到最近一次 Checkpoint。
  3. Sink 端:使用幂等写入(如 Redis SET)或两阶段提交(如 Kafka Producer 的事务 API)。
// 特征服务中启用 Exactly-Once 语义的关键配置
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

// 启用 Checkpoint,间隔 15 秒
env.enableCheckpointing(15000, CheckpointingMode.EXACTLY_ONCE);

// 设置状态后端为 RocksDB
env.setStateBackend(new EmbeddedRocksDBStateBackend(true)); // true 表示增量 Checkpoint

// 配置 Checkpoint 存储路径
env.getCheckpointConfig().setCheckpointStorage("hdfs://namenode:8020/flink/checkpoints");

// 设置 Kafka Source 的消费模式
KafkaSource<UserEvent> source = KafkaSource.<UserEvent>builder()
    .setBootstrapServers("kafka:9092")
    .setTopics("user-events")
    .setGroupId("feature-service")
    .setStartingOffsets(OffsetsInitializer.latest())
    .setValueOnlyDeserializer(new UserEventDeserializer())
    .setProperty("enable.auto.commit", "false") // 关闭自动提交,由 Flink 管理偏移量
    .build();
性能优化:RocksDB 的增量 Checkpoint 只上传变更的 SST 文件,大幅减少快照时间。建议同时开启 state.backend.rocksdb.checkpoint.transfer.thread.num 参数,增加上传并行度。

2.4 本章小结