这是【实时数仓】系列第3篇。上篇讲CDC到Doris乱序覆盖,这篇讲Flink多表JOIN状态爆炸。
周日下午接到电话,出库包裹的实时任务告警了。
打开Flink Web UI一看:Checkpoint size 12GB,状态还在涨。RocksDB的write stall已经触发,任务处理延迟从秒级飙到分钟级。
这个任务关联了10张MySQL CDC源表------shipment_package、package_trajectory、warehouse、owner、carrier...最后写一张Doris宽表。跑了两个月,状态从几百MB涨到12GB。
一上来我先给所有表加了TTL 10天,结果第二天产品就投诉物流状态没更新。排查发现轨迹类数据15天才到,10天TTL早就把状态清了。
改成30天,轨迹数据是保住了,但状态开始疯涨------从几百MB一路飙到12GB。
TTL太小丢数据,太大爆内存。两头堵。
两头堵:TTL太小丢数据,太大爆内存
TTL = 10天:轨迹丢了
业务反馈一个包裹的物流状态不对------MySQL里已经是"签收",Doris里还是"已发货"。
排查发现:这条轨迹的update_time距离包裹创建已经15天。TTL设的10天,状态早就被清理了。Flink收到这条轨迹时,找不到之前的状态上下文,当成新数据处理。
TTL = 30天:状态爆了
改成30天后,轨迹数据是保住了,但Checkpoint size开始疯涨。两个月没重启,状态从几百MB涨到12GB。RocksDB的write stall触发,任务处理延迟从秒级飙到分钟级。
问题本质:
| TTL设置 | 轨迹数据 | 内存 |
|---|---|---|
| 10天 | 15天到的轨迹丢失 | 正常 |
| 30天 | 保住 | 爆炸 |
TTL不是解法,是两头堵。
根因:Regular JOIN的状态爆炸
TTL两头堵的根因,是Regular JOIN的状态管理机制。
官方文档原文:"For streaming queries, the grammar of regular joins is the most flexible...However, this operation has important operational implications: it requires to keep both sides of the join input in Flink state forever."
官方文档原文:"You can provide a query configuration with an appropriate state time-to-live (TTL) to prevent excessive state size."
Regular JOIN的工作方式:
- 左表来一条 → 查右表状态 → 关联 → 输出
- 右表来一条 → 查左表状态 → 关联 → 输出
- 两边的数据都要永久保存在状态里,直到设置TTL
- 10表JOIN = 10个中间状态,状态可能比原始数据大很多倍
我的任务:10张表JOIN,状态从几百MB涨到12GB,两个月没重启过。TTL是补救措施,不是根本解法。
分层治理方案
Flink有两种JOIN方式,适合不同场景:
| JOIN类型 | 适用场景 | 状态大小 | 我的表 |
|---|---|---|---|
| Regular JOIN | 通用,两边都要存状态 | 大(无界) | 大部分表 |
| Event Time Temporal JOIN | 维度表,取watermark时刻的版本(存since watermark的所有版本) | 小(有界) | warehouse, owner |
我的10张表,按JOIN方式分两层:
第一层:Event Time Temporal JOIN(2张维度表,状态由watermark控制)
LEFT JOIN warehouse FOR SYSTEM_TIME AS OF a.update_time d
LEFT JOIN owner FOR SYSTEM_TIME AS OF a.update_time e
第二层:Regular JOIN + TTL(8张表,靠TTL控制状态上限)
LEFT JOIN package_trajectory_ttl_view b ON b.package_id = a.id
LEFT JOIN shipment_package_carrier c ON c.package_id = a.id
LEFT JOIN data_dict f ON a.tenant_id = cast(f.tenant_id as bigint) AND d.code = f.dict_code
LEFT JOIN package_trajectory_detail_ttl_view g ON b.id = g.package_trajectory_id
LEFT JOIN shipment_package_statistics h ON a.id = h.package_id
LEFT JOIN abnormal_package i ON a.id = i.package_id
LEFT JOIN shipment_package_consignee j ON a.id = j.package_id
第一层:Event Time Temporal JOIN解决维度表状态
warehouse和owner是维度表,变化频率低。用Regular JOIN会存所有历史版本,用Event Time Temporal JOIN取watermark时刻的版本。
官方文档原文:"Event Time temporal joins allow joining against a versioned table. This means a table can be enriched with changing metadata and retrieve its value at a certain point in the time."
官方文档原文:"The versioned table will store all versions - identified by time - since the last watermark."
官方文档原文:"As time passes, no longer needed versions of the record will be removed from the state."
Event Time Temporal JOIN与Regular JOIN的本质区别:
| Regular JOIN | Event Time Temporal JOIN | |
|---|---|---|
| 状态范围 | 两边永久保存全部历史(无界) | 存since watermark的所有版本(有界) |
| 状态清理 | 全靠TTL | Flink按watermark自动清理 |
| 极端情况 | TTL设大仍会爆 | watermark停滞时仍会积累,建议TTL兜底 |
sql
-- 之前:Regular JOIN,两边永久保存状态
LEFT JOIN warehouse d
ON a.tenant_id = cast(d.tenant_id as bigint) AND a.warehouse_id = d.id
-- 之后:Event Time Temporal JOIN,取watermark时刻的版本
LEFT JOIN warehouse FOR SYSTEM_TIME AS OF a.update_time d
ON a.warehouse_id = d.id AND a.tenant_id = cast(d.tenant_id as bigint)
官方文档原文:"Flink uses the SQL syntax of FOR SYSTEM_TIME AS OF to perform this operation from the SQL:2011 standard."
官方文档:https://nightlies.apache.org/flink/flink-docs-release-1.19/docs/dev/table/sql/queries/joins/
前提条件(官方要求):
- 维度表是Versioned Table = PRIMARY KEY + WATERMARK + changelog source(CDC天然支持)✅
- 主表有Event Time属性(WATERMARK)✅
- join条件包含右表的PRIMARY KEY ✅
sql
-- 主表加WATERMARK(Event Time属性)
CREATE TABLE shipment_package(
...
update_time timestamp(3),
WATERMARK FOR update_time AS update_time - INTERVAL '10' SECOND,
PRIMARY KEY (id) NOT ENFORCED
)
-- 维度表加WATERMARK(Versioned Table)
CREATE TABLE warehouse(
...
update_time timestamp(3),
WATERMARK FOR update_time AS update_time - INTERVAL '10' SECOND,
PRIMARY KEY (id) NOT ENFORCED
)
Event Time vs Processing Time:
| Event Time Temporal Join | Processing Time Temporal Join | |
|---|---|---|
| 语法 | FOR SYSTEM_TIME AS OF table1.rowtime |
FOR SYSTEM_TIME AS OF table1.proctime |
| Flink 1.19 | ✅ 支持 | ❌ 不支持("not support yet") |
| 取什么版本 | 数据变更时刻的版本(历史版本) | Flink处理时刻的最新版本 |
| 状态存储 | 存since watermark的所有版本 | --- |
| 替代方案 | 无(不需要) | LATERAL TABLE(Rates(o_proctime)) |
官方文档原文(Processing Time章节):
"Currently, the FOR SYSTEM_TIME AS OF syntax used in temporal join with latest version of any view/table is not support yet ."
"The reason is only the semantic consideration, because the join processing for left stream doesn't wait for the complete snapshot of temporal table, this may mislead users in production environment."
所以我们用Event Time------基于CDC的update_time,join的是"数据变更时刻"的维度版本,不是"Flink处理时刻"的版本。
第二层:Regular JOIN + TTL
其他8张表是事实表或传递关联表,不能用Temporal JOIN,只能用Regular JOIN + TTL。
sql
-- 用STATE_TTL hint为每张表单独设TTL(表名或别名均可)
SELECT /*+ STATE_TTL('b'='30d', 'c'='30d') */ *
FROM shipment_package a
LEFT JOIN package_trajectory b ON b.package_id = a.id AND b.deleted = 0
LEFT JOIN carrier c ON c.package_id = a.id
...
官方文档原文:"For stateful computation Regular Join and Group Aggregation, users can use
STATE_TTLhint to specify operator-level Idle State Retention Time, which enables the aforementioned operators to have a different TTL against the pipeline level configurationtable.exec.state.ttl."
STATE_TTL hint语法要点(官方):
- key是表名或别名,如
'b'='30d'或'package_trajectory'='30d' - value是时间单位:
1d/3d/30d等 - 比全局配置
table.exec.state.ttl更精细,可以每张表单独设 - 只对Regular JOIN和Group Aggregation生效
TTL配置: 事实表必须设TTL(用STATE_TTL hint或全局配置)。维度表由Flink按watermark自动清理版本,但建议设置TTL作为兜底(防止watermark停滞)。
优化效果对比
| 对比项 | 优化前 | 优化后 |
|---|---|---|
| JOIN方式 | 全部Regular JOIN | Event Time Temporal JOIN(2张)+ Regular JOIN(8张) |
| 维度表状态 | 存两边全部历史(无界) | 存since watermark的所有版本(Flink自动清理) |
| TTL依赖 | 全靠TTL补救 | 维度表Flink自动清理+TTL兜底,事实表靠TTL |
Event Time Temporal JOIN的限制:
- 维度表必须有PRIMARY KEY和WATERMARK(Versioned Table)
- 必须用Event Time(WATERMARK),Processing Time的
FOR SYSTEM_TIME AS OF在Flink 1.19不支持 - join条件必须包含右表的PRIMARY KEY
- watermark停滞时状态仍会积累,建议设置TTL兜底
Regular JOIN + TTL的限制:
- TTL太短丢数据,太长爆内存
- 需要根据业务数据的实际生命周期来设TTL
- TTL是补救措施,不是根本解法
写在最后
Flink多表JOIN状态爆炸,不是"加TTL"就能解决的问题。
TTL是补救措施,不是根本解法。根本解法是分层治理:
- 维度表用Event Time Temporal JOIN,状态由watermark控制,Flink自动清理不需要的版本
- 事实表用Regular JOIN + TTL,靠TTL控制状态上限
什么时候该用哪种JOIN:
| 场景 | 推荐JOIN类型 |
|---|---|
| 维度表关联(warehouse、owner) | Event Time Temporal JOIN |
| 事实表关联(package_trajectory等) | Regular JOIN + TTL |
| 传递关联(data_dict等) | Regular JOIN + TTL |
| 不确定 | 先用Regular JOIN + TTL,观察状态增长 |
Event Time Temporal JOIN的限制:
- 维度表必须有PRIMARY KEY和WATERMARK(Versioned Table)
- 必须用Event Time(WATERMARK),Processing Time的
FOR SYSTEM_TIME AS OF在Flink 1.19不支持 - join条件必须包含右表的PRIMARY KEY
- watermark停滞时状态仍会积累,建议设置TTL兜底
TTL配置经验:
- 维度表:Temporal JOIN由Flink按watermark自动清理版本,建议设置TTL防止极端情况
- 事实表:TTL根据业务数据的实际生命周期来设,不能一刀切
官方文档:
下一篇讲数据消重------实时数据里的重复问题怎么处理。