在 Flink SQL 中玩转实时模型推理 —— ML_PREDICT 实战解析

传统的实时推理架构一般是这样的链路:

  1. Flink 做特征聚合,把结果写入 Kafka / Redis;
  2. 线上服务(Java / Python 微服务)从 Kafka 或缓存拉特征;
  3. 调用模型服务(TensorFlow Serving、PyTorch Server 或自研 gRPC 接口);
  4. 把预测结果再写回 Kafka / DB。

这条链路的问题是:

  • 组件多、链路长,延迟和稳定性都不太友好;
  • 业务逻辑被拆散:一部分在 Flink SQL,一部分在后端服务里,不易维护;
  • 实时数仓 / 实时决策希望"写一条 SQL 就搞定",却不得不再开整套服务化工程。

Flink 在 1.20 之后引入的 ML_PREDICT 表值函数,正是为了解决这件事:

直接在 SQL 里写:
SELECT * FROM ML_PREDICT(TABLE xxx, MODEL my_model, ...)

把模型推理变成"一个 TVF + 几行 SQL"。

你可以在流式计算任务中:

  • 上游是 Kafka / Pulsar / CDC 源;
  • 中间用 SQL 做清洗、特征构造;
  • 最后一层用 ML_PREDICT 打上模型预测结果;
  • 整个过程只需要一条 Flink SQL Job,就能跑完整的在线特征 + 实时推理

2. ML_PREDICT 的整体语义

ML_PREDICT 是一个 表值函数(TVF)

  • 输入 :一张表(input_table)+ 一个已注册的模型(model_name);
  • 输出:原表所有列 + 模型预测产生的列。

简单可以理解为:

text 复制代码
input_table  --(ML_PREDICT with model)-->  input_columns + prediction_columns

从 SQL 写法上看,它和前面你已经用过的 TUMBLE / HOP / CUMULATE / SESSION 非常像,都是:

sql 复制代码
SELECT ...
FROM ML_PREDICT(TABLE ..., MODEL ..., DESCRIPTOR(...), CONFIG => ...)

只不过,这次 TVF 做的事情是"调用模型推理"。

3. 语法拆解:把每个参数讲清楚

完整语法:

sql 复制代码
SELECT * FROM
ML_PREDICT(
  TABLE input_table,
  MODEL model_name,
  DESCRIPTOR(feature_columns),
  [CONFIG => MAP['key', 'value']]
);
3.1 input_table:要推理的数据源

可以是:

  • 物理表(比如 Kafka Source 定义出来的表);
  • 视图 / 子查询结果(做完过滤、特征工程之后再喂给模型);

比如:

sql 复制代码
WITH features AS (
  SELECT
    user_id,
    device_id,
    age,
    log_ts,
    score1,
    score2
  FROM RawEvents
  WHERE event_type = 'click'
)

SELECT *
FROM ML_PREDICT(
  TABLE features,
  MODEL ctr_model,
  DESCRIPTOR(age, score1, score2)
);
3.2 MODEL model_name:Catalog 中的模型

model_name 必须是已经 在 Catalog 中注册好的模型

模型的输入 / 输出 Schema 都是在 Model Creation 阶段定义的。

一旦注册完成,就可以在 SQL 里像引用一张表一样引用这个模型。

3.3 DESCRIPTOR(feature_columns):特征列映射

DESCRIPTOR 告诉 Flink:输入表里的哪几列是用来做特征的

  • 列的顺序、数量必须与模型输入 Schema 对齐;
  • 否则会直接抛异常(很重要,防止线上悄悄算错)。

举例:

sql 复制代码
DESCRIPTOR(age, score1, score2)

就表示模型的输入是 3 个特征,对应当前表里的这三列。

3.4 CONFIG:推理配置(重点是异步)

CONFIG 是一个 MAP['key','value'],常用配置包括:

  • async:是否使用异步推理('true' / 'false');

  • max-concurrent-operations:异步并发度(异步 I/O 的 buffer 大小);

  • output-mode

    • "ORDERED":按输入顺序输出(严格有序);
    • "ALLOW_UNORDERED":允许无序输出(性能更好);
  • timeout:一次异步推理从发起到完成的超时时间。

例如:

sql 复制代码
SELECT * FROM ML_PREDICT(
  TABLE input_table,
  MODEL ctr_model,
  DESCRIPTOR(age, score1, score2),
  MAP[
    'async', 'true',
    'max-concurrent-operations', '1000',
    'output-mode', 'ALLOW_UNORDERED',
    'timeout', '3s'
  ]
);

4. 同步 vs 异步推理:什么时候该 async?

在高吞吐实时场景下,模型推理通常是下游瓶颈

  • 如果每条记录都同步调用一次模型(RPC / HTTP),并等待返回;
  • 吞吐、延迟都比较吃力。

Flink 提供的异步 I/O 能力,在 ML_PREDICT 里也被用上了:

  1. async = false → PredictRuntimeProvider(同步)

    • Provider 实现 createPredictFunction
    • Flink 在算子内同步调用预测方法;
    • 写法简单,但受单个请求延迟限制明显。
  2. async = true → AsyncPredictRuntimeProvider(异步)

    • Provider 实现 createAsyncPredictFunction
    • 算子一次可以挂起多个请求,通过回调 / future 收结果;
    • max-concurrent-operations 控制并发度;
    • output-mode 控制结果是否必须按输入顺序发射。

如果你不在配置里显式写 async

  • 系统会自己在 Provider 中选择同步或异步;
  • 如果两种能力都存在,会优先选择异步 Provider

实践经验:

  • 高 QPS + 外部模型服务延迟明显 → 首选异步;
  • 简单本地模型(如轻量 Embedding、树模型) → 同步就够,可简化实现;
  • 如果结果顺序不会影响下游逻辑,可以考虑 ALLOW_UNORDERED 进一步提升吞吐。

5. 输出 Schema 与列名冲突

输出表 = 输入表所有列 + 模型输出列。

注意两个细节:

  1. 模型输出的列结构由模型本身定义(输出 Schema);

  2. 如果输出列名与输入表已有列名冲突,会自动加索引后缀

    • 模型定义的输出列叫 prediction
    • 输入表中已经有一个 prediction 列;
    • 最终输出列会自动改名为 prediction0

这可以避免因为列名冲突导致 SQL 难以解析,但在写下游逻辑(比如 Sink 到某个表)时,记得用新名字。

ML_PREDICT 本身只是个 SQL 壳子,真正执行推理的是 ModelProvider

一个 Provider 的职责,可以简单理解为:

  • 告诉 Flink:如何创建一个可被算子调用的预测函数

按同步 / 异步能力不同,分为两类:

6.1 PredictRuntimeProvider(同步)
  • 需要实现 createPredictFunction 方法;
  • 方法返回的是一个普通的预测函数,算子会逐条调用它;
  • 用于 async = false 的情况。
6.2 AsyncPredictRuntimeProvider(异步)
  • 实现 createAsyncPredictFunction

  • 预测函数返回的是 Future / 回调形式结果;

  • 对应 Flink 的 AsyncDataStream 模式,需要配置:

    • timeout:单条记录异步推理的超时时间;
    • max-concurrent-operations:最多挂起多少未完成的预测请求。

只要你在 Model Creation 阶段把模型和对应 Provider 注册进 Catalog,

在 SQL 里就只需要写一个 MODEL my_model 即可,不用关心具体是 TensorFlow、PyTorch 还是自研服务。

7. 约束与错误处理:踩坑之前要知道的事

7.1 只支持 append-only 表

文档里有一句容易被忽视:

ML_PREDICT 只支持追加流(append-only tables),不支持 CDC 表。

原因是模型推理往往是非确定性的

  • 比如模型版本变动、外部依赖的特征变化等等;
  • 如果上游是 CDC(带 update / delete),很难对"旧结果"做正确补偿。

所以:

  • 更推荐在已经完成 CDC 归并、只保留追加写的表上调用 ML_PREDICT
  • 或者把 CDC 先聚合成"快照流 / 事实表",再作为 append-only 源输入。
7.2 常见报错场景

会抛异常的情况包括:

  • 模型在 Catalog 中不存在;
  • DESCRIPTOR 中特征列数量与模型输入 Schema 不一致;
  • 缺少 MODEL 参数;
  • 参数个数错了(太多或太少);
  • async=true 但使用的 Provider 不支持异步。

这些错误基本都发生在 SQL 校验阶段,能尽早暴露问题。

8. 性能调优小贴士

在实际项目里,如果你希望 ML_PREDICT 既好用又高效,可以从这几方面入手:

  1. 优先异步:

    • 高 QPS、外部服务延迟 > 5--10ms 时,强烈建议 async = 'true'
    • 同时配合 max-concurrent-operations(比如几百到几千)。
  2. 合理设置 timeout:

    • 太小 → 频繁超时、重试,浪费资源;
    • 太大 → 延迟尾部很高,backpressure 难以定位;
    • 一般可以设置为"外部模型 P99 * 2~3"。
  3. 评估有序 / 无序输出:

    • 如果只是把预测结果写回 Kafka / OLAP,结果顺序通常没那么重要,可以用 "ALLOW_UNORDERED"
    • 如果下游逻辑依赖严格顺序(比如和原日志按 offset 合并),就老老实实保持 "ORDERED"
  4. 尽量在 Flink 内完成特征工程:

    • 把原始事件在 SQL 层做完清洗 / 特征聚合后再喂给模型;
    • 上游特征越干净简单,模型推理稳定性越好。

9. 一个完整的小例子:实时 CTR 预测

最后给一个稍完整一点的示意:

sql 复制代码
-- 1. 原始点击流,来自 Kafka
CREATE TABLE raw_clicks (
  user_id   STRING,
  item_id   STRING,
  age       INT,
  gender    STRING,
  log_ts    TIMESTAMP_LTZ(3),
  WATERMARK FOR log_ts AS log_ts - INTERVAL '5' SECOND
) WITH (...);

-- 2. 特征工程:做简单编码与组合
CREATE VIEW ctr_features AS
SELECT
  user_id,
  item_id,
  CAST(age AS DOUBLE) AS age_f,
  CASE gender WHEN 'M' THEN 1.0 ELSE 0.0 END AS gender_f,
  log_ts
FROM raw_clicks;

-- 3. 调用 ML_PREDICT 做实时 CTR 推理
CREATE TABLE ctr_with_pred (
  user_id   STRING,
  item_id   STRING,
  age_f     DOUBLE,
  gender_f  DOUBLE,
  log_ts    TIMESTAMP_LTZ(3),
  ctr       DOUBLE   -- 预测结果
) WITH (...);

INSERT INTO ctr_with_pred
SELECT
  user_id,
  item_id,
  age_f,
  gender_f,
  log_ts,
  prediction AS ctr     -- 假设模型输出列名为 prediction
FROM ML_PREDICT(
       TABLE ctr_features,
       MODEL ctr_model,
       DESCRIPTOR(age_f, gender_f),
       MAP['async', 'true',
           'max-concurrent-operations', '1000',
           'output-mode', 'ALLOW_UNORDERED',
           'timeout', '2s']
     );

在这条 SQL Job 中:

  • 上游 Kafka → Flink 负责特征加工;
  • ML_PREDICT 直接把模型推理嵌入到 SQL 里;
  • 下游 ctr_with_pred 就是一条"带预测结果的实时流",可以继续写进 Kafka / ClickHouse / Doris 等任意 Sink。

10. 小结

ML_PREDICT 把"模型推理"变成了 Flink SQL 世界里的一个普通 TVF:

  • 使用方式与 TUMBLE / HOP / CUMULATE / SESSION 非常一致;
  • 上手成本比自研一整套模型服务低得多;
  • 又能通过 ModelProvider 挂接任意底层模型框架。
相关推荐
Mxsoft6192 小时前
Flink CEP预警连锁故障,提前30分钟救场!
大数据·flink
Jackyzhe2 小时前
Flink学习笔记:窗口
大数据·flink
Hello.Reader3 小时前
Flink SQL 窗口表值函数TUMBLE / HOP / CUMULATE / SESSION
java·sql·flink
Franciz小测测4 小时前
Python APScheduler 定时任务 独立调度系统设计与实现
java·数据库·sql
梦里不知身是客114 小时前
flink从kafka读取数据
flink·kafka·linq
少年攻城狮20 小时前
Mybatis-Plus系列---【自定义拦截器实现sql完整拼接及耗时打印】
数据库·sql·mybatis
迷茫的21世纪的新轻年21 小时前
PostgreSQL——SQL优化
数据库·sql·postgresql
2301_800256111 天前
8.3 查询优化 核心知识点总结
大数据·数据库·人工智能·sql·postgresql
梦里不知身是客111 天前
flink的CDC 的种类
大数据·flink