Flink Process Table Functions(PTF)实战详解:把 SQL 变成“可编程算子”,状态、时间、定时器一把梭

1. PTF 是什么:UDF 的"超集"

Process Table Functions(PTFs)是 Flink SQL & Table API 中最强的函数类型,可以实现接近内置算子的能力:

  • 输入:零/一/多张表(也可混合 scalar 参数)
  • 输出:零/一/多行(任意 Row 或结构化类型)
  • 能力:Flink 托管状态(managed state)、事件时间(event time)、Timer、底层 changelog(CDC)

一句话:PTF 让你用"函数"写一个可状态化、可计时、可处理更新的表算子。

2. PTF 与 SQL:2016 PTF 的关系

文档里提到 SQL:2016 的 Polymorphic Table Functions(同样简称 PTF)。Flink 的 Process Table Functions 在语义上对齐 SQL 标准的一些调用特征(表参数、row/set 语义、descriptor 参数等),但同时增强了 Flink 的流式能力:

  • 状态管理(Flink state backend)
  • 时间与 watermark
  • Timer 服务
  • 运行时 Changelog 能力

你可以理解为:Flink 在 SQL 标准 PTF 上叠加了流式计算"必须的三件套":state、time、changelog。

3. PTF 最核心的概念:Row 语义 vs Set 语义

PTF 的 eval() 不是"只接受一行",它可以接受一个"表参数",并声明该表如何被理解:

3.1 Row Semantics(行语义)

  • 认为每行彼此独立
  • 系统可自由分发,每个虚拟处理器一次只看到当前行
  • 通常无状态(或者不依赖历史)

示例:给每个 name 加个 greeting(逐行处理)

java 复制代码
public static class Greeting extends ProcessTableFunction<String> {
  public void eval(@ArgumentHint(ArgumentTrait.ROW_SEMANTIC_TABLE) Row input) {
    collect("Hello " + input.getFieldAs("name") + "!");
  }
}

3.2 Set Semantics(集合语义)

  • 认为行之间有关联,需要按 key 聚合成一个"集合"
  • 调用时必须(或可选)指定 PARTITION BY
  • 允许状态:同一个 key 下的历史行可通过 state 记忆

示例:同一个 name 来过几次

java 复制代码
public static class GreetingWithMemory extends ProcessTableFunction<String> {
  public static class CountState { public long counter = 0L; }

  public void eval(
    @StateHint CountState state,
    @ArgumentHint(ArgumentTrait.SET_SEMANTIC_TABLE) Row input
  ) {
    state.counter++;
    collect("Hello " + input.getFieldAs("name") + ", your " + state.counter + " time?");
  }
}

调用(Table API):

java 复制代码
env.fromValues("Bob","Alice","Bob")
  .as("name")
  .partitionBy($("name"))
  .process(GreetingWithMemory.class)
  .execute()
  .print();

4. Virtual Processor:为什么 PTF 既能扩展又能有状态

PTF 会把输入表分布到所谓"虚拟处理器(virtual processor)"上执行。你可以理解为:一个 virtual processor 对应一个 key 的处理上下文(或者 row 语义下随机分发)。

  • Row 语义:processor 只看到当前 row
  • Set 语义:processor 被 PARTITION BY key "圈定",同 key 的数据共定位,state/timer 也都在这个 key 上生效

这就是 PTF 既能 scale-out,又能做到 per-key 状态机的根本原因。

5. 调用语法:隐式参数 on_time 与 uid

PTF 调用时,除了你定义的参数,系统还会"隐式补两类参数":

  • on_time:用于事件时间语义(DESCRIPTOR)
  • uid:用于 stateful query evolution(保证 savepoint 恢复、fan-out 优化等)

推荐 name-based 调用方式,后续演进更稳:

SQL:

sql 复制代码
SELECT * FROM TableFilter(input => TABLE t, threshold => 100, uid => 'my-ptf');

Table API:

java 复制代码
env.from("t")
  .process(TableFilter.class,
    lit(100).asArgument("threshold"),
    lit("my-ptf").asArgument("uid"));

6. 实现规则:eval() 方法签名是"铁律"

PTF 只支持一个 eval()(不支持重载),签名模式:

复制代码
eval( <context>? , <state entry>* , <call argument>* )
  • Context(可选)必须是第一个
  • State entries 必须在用户参数之前
  • eval 必须 public,不能 static

7. State:PTF 的灵魂(含 TTL / 大状态)

7.1 基本 state(Value State)

通过 @StateHint 声明一个可变参数作为 state:

java 复制代码
class CountingFunction extends ProcessTableFunction<String> {
  public static class CountState { public long count = 0L; }

  public void eval(
    @StateHint CountState memory,
    @ArgumentHint(ArgumentTrait.SET_SEMANTIC_TABLE) Row input
  ) {
    memory.count++;
    collect("Seen rows: " + memory.count);
  }
}

7.2 State TTL(建议默认就设计)

java 复制代码
public void eval(
  Context ctx,
  @StateHint(ttl = "1 day") SeenState memory,
  @ArgumentHint(ArgumentTrait.SET_SEMANTIC_TABLE) Row input
) { ... }

TTL 基于 processing time,能有效避免"开 keyspace"导致 state 无限增长。

7.3 大状态:ListView / MapView(避免整块反序列化)

  • ListView:列表 state
  • MapView:map state,按 key 读取更省
java 复制代码
class LargeHistoryFunction extends ProcessTableFunction<String> {
  public void eval(
    @StateHint MapView<String, Integer> largeMemory,
    @ArgumentHint(ArgumentTrait.SET_SEMANTIC_TABLE) Row input
  ) {
    String eventId = input.getFieldAs("eventId");
    Integer count = largeMemory.get(eventId);
    largeMemory.put(eventId, count == null ? 1 : count + 1);
  }
}

8. Time & Timers:让 PTF 变成"事件时间状态机"

8.1 on_time 与 rowtime 输出

声明 on_time 后,PTF 输出会自动带一个 rowtime 列,用于下游继续做时间计算。

SQL:

sql 复制代码
SELECT * FROM PingLaterFunction(
  input => TABLE Events PARTITION BY id,
  on_time => DESCRIPTOR(ts)
);

8.2 定时器使用模式:eval 注册,onTimer 响应

典型例子:最后一次事件后 1 分钟发 ping

java 复制代码
public static class PingLaterFunction extends ProcessTableFunction<String> {
  public void eval(
    Context ctx,
    @ArgumentHint({ArgumentTrait.SET_SEMANTIC_TABLE, ArgumentTrait.REQUIRE_ON_TIME}) Row input
  ) {
    TimeContext<Instant> timeCtx = ctx.timeContext(Instant.class);
    timeCtx.registerOnTime("ping", timeCtx.time().plus(Duration.ofMinutes(1)));
  }

  public void onTimer(OnTimerContext onTimerCtx) {
    collect("ping");
  }
}

设计建议:Timer 也会占 state,尽量减少 timer 数量,及时 clearAllTimers/clearAllState。

9. 多表输入:PTF 可以做"自定义 Join"

PTF 可以同时接收多张表(都必须 set semantics,且 PARTITION BY 结构一致)。一次 eval 只会有一个表参数非空,通过 null 判断来源。

示例:访问表 Visits + 购买表 Purchases,按用户关联,记住 last purchase:

java 复制代码
public static class GreetingWithLastPurchase extends ProcessTableFunction<String> {
  public static class LastItemState { public String lastItem; }

  public void eval(
    @StateHint LastItemState state,
    @ArgumentHint(ArgumentTrait.SET_SEMANTIC_TABLE) Row visit,
    @ArgumentHint(ArgumentTrait.SET_SEMANTIC_TABLE) Row purchase
  ) {
    if (purchase != null) {
      state.lastItem = purchase.getFieldAs("item");
    } else if (visit != null) {
      if (state.lastItem == null) {
        collect("Hello " + visit.getFieldAs("name") + ", let me know if I can help!");
      } else {
        collect("Hello " + visit.getFieldAs("name") + ", here to buy " + state.lastItem + " again?");
      }
    }
  }
}

注意:多输入的到达顺序可能导致非确定性,要么用 watermark 做"时间驱动",要么用条件缓冲来保证逻辑严谨。

10. UID:PTF 独有的"状态化查询演进"能力

PTF 是可持久化状态块,周围 SQL 变了也可能恢复,只要 state schema 不变。为此,Flink 要求 set semantics 的 PTF 有唯一 UID:

  • 未指定 uid:默认用函数名(同一个 statement 中只能出现一次)
  • 多次调用:必须手动指定 uid,确保全局唯一
  • 同 uid:优化器可做 fan-out(共享一个 stateful PTF)

这对"一个状态机输出分流到多个 sink"非常重要。

11. Changelog(更新/撤回)支持:PTF 可以玩 CDC

默认 PTF 假设输入是 append-only(+I),输出也是 append-only,这对 watermark 与时间语义最友好。

若要接更新表,必须声明:

  • SUPPORTS_UPDATES:允许更新进入
  • REQUIRE_UPDATE_BEFORE:强制 retract 模式(-U/+U)
  • REQUIRE_FULL_DELETE:强制 full delete(-D 全字段)

示例:把更新表转成 append-only(把 RowKind 写进 payload,输出始终 +I)

java 复制代码
@DataTypeHint("ROW<flag STRING, sum INT>")
public static class ToChangelogFunction extends ProcessTableFunction<Row> {
  public void eval(@ArgumentHint({ArgumentTrait.SET_SEMANTIC_TABLE, ArgumentTrait.SUPPORTS_UPDATES}) Row input) {
    collect(Row.of(input.getKind().toString(), input.getField("sum")));
  }
}

更高级:实现 ChangelogFunction,自己声明输出模式(retract / upsert / delete 规则)。但要非常谨慎:输出 changelog 声明错了会导致整条 pipeline 行为未定义。

12. 高级案例:购物车状态机(最典型 PTF)

购物车本质就是 per-user 状态机:ADD/REMOVE/CHECKOUT + REMINDER/TIMEOUT。

PTF 用 state 存 cart,用 timer 做 reminder/timeout,CHECKOUT 后 clear state------这就是 PTF 的"正确打开方式"。

这类场景用传统 SQL + UDF 很难优雅实现,但 PTF 非常顺。

13. 当前限制(务必注意)

文档明确提到一些限制(你贴的结尾也有):

  • PTF 不能跑 batch mode
  • 部分能力在早期阶段:例如 broadcast state 等(文档后面还会列更多限制)
  • 如果 PTF 接 updates:很多功能会受限(例如 on_time 不支持等,文档中也强调了)

建议:PTF 目前适合"流式、事件驱动、状态机类"问题。

14. 什么时候该用 PTF

用一句很实际的话总结:

  • 你只是做字段变换 → ScalarFunction
  • 一行拆多行 / 维表 lookup → TableFunction / AsyncTableFunction
  • 多行聚一值 → AggregateFunction(UDAGG)
  • 多行聚多行 → TableAggregateFunction(UDTAGG)
  • 你要状态机 + timer + 复杂 state + 多表协同 + 处理更新 → PTF
相关推荐
2301_773730314 小时前
网络编程—TCP传输控制协议
服务器·网络·tcp/ip
云老大TG:@yunlaoda3604 小时前
华为云国际站代理商DAS的跨境合规适配是如何保障数据合规的?
网络·数据库·华为云
TG:@yunlaoda360 云老大4 小时前
华为云国际站代理商DAS的跨境合规适配在游戏出海场景中的应用
服务器·网络·游戏·华为云
没有bug.的程序员4 小时前
熔断、降级、限流:高可用架构的三道防线
java·网络·jvm·微服务·架构·熔断·服务注册
武昌库里写JAVA5 小时前
在iview中使用upload组件上传文件之前先做其他的处理
java·vue.js·spring boot·后端·sql
知新坊5 小时前
飞牛NAS 没有公网 IP?使用它让 NAS 访问、文件远程像在局域网
网络·网络协议·tcp/ip
emma羊羊5 小时前
【AWVS】漏扫工具
网络·安全·web安全·awvs
山风wind5 小时前
IP地址、子网掩码与网络连通性:从入门到精通
网络·tcp/ip·php
尋有緣6 小时前
力扣614-二级关注者
大数据·数据库·sql·oracle