流式聚合不慢才怪?窗口、触发器和内存这三板斧你真用对了吗
做流处理这些年,我发现一个特别有意思的现象:
👉大家都在写聚合,真正把“聚合性能”当回事的人并不多。
很多同学一上来就是:
- keyBy
- window
- sum / reduce
- 跑起来能出数
- 线上一慢:加机器
直到有一天老板问一句:
“你这作业,为啥 1 分钟窗口,内存能涨到 20G?”
这时候,才意识到——
流式聚合这件事,真不是“会写 API 就行”。
今天我就从一个老流批(是的我自己 😂)的视角,聊聊:
流式聚合怎么做,才不至于把窗口、触发器和内存一起玩炸。
一、先说句大实话:90% 的流式 OOM,死在窗口上
我们先把一个误区说清楚:
窗口 ≠ 时间范围这么简单
窗口背后是状态(State),而状态 = 内存 / RocksDB / Checkpoint 成本。
1️⃣ 最常见的“作死写法”
stream.keyBy(Order::getUserId).timeWindow(Time.minutes(10)).sum("amount");看着很干净,对吧?
但你想过三个问题没有:
- 10 分钟内有多少 key?
- 每个 key 状态多久才能被清?
- 下游真的需要 10 分钟后的最终结果吗?
很多业务其实只是假装需要大窗口。
二、窗口不是越大越高级,而是越大越危险
我常说一句话(很多人不爱听):
窗口越大,说明你对业务越不自信。
举个真实的例子
风控同学说:
“我们要统计用户 30 分钟内的下单金额”
我一般会追问一句:
“你是要最终值,还是过程趋势?”
十有八九,答案是:
“其实 1 分钟一次也行,只要别太晚。”
这时候,大窗口 + 默认触发,就是浪费资源。
三、Trigger:真正被严重低估的性能武器
很多人写 Flink,一辈子没写过 Trigger。
但我想说:
👉Trigger 才是流式聚合的“节流阀”。
1️⃣ 默认 Trigger 的问题
- 只在窗口结束时触发
- 状态一直攒着
- 对内存极不友好
2️⃣ 自定义触发器,边算边吐
.window(TumblingEventTimeWindows.of(Time.minutes(10))).trigger(ContinuousEventTimeTrigger.of(Time.minutes(1))).reduce(newReduceFunction<Order>(){@OverridepublicOrderreduce(Ordero1,Ordero2){o1.setAmount(o1.getAmount()+o2.getAmount());returno1;}});这段代码背后的思路很重要:
- 窗口 10 分钟(业务口径)
- 每 1 分钟就输出一次中间结果
- 状态不会无限膨胀
- 下游能提前看到趋势
窗口是口径,触发器是节奏。
很多人把这俩混成一个东西,结果性能就开始玄学了。
四、允许迟到 ≠ 无限留后门
再聊一个特别容易被滥用的东西:allowedLateness
.allowedLateness(Time.minutes(5))这玩意本意是兜底迟到数据,
结果被不少人当成:
“数据乱就多给点时间”
真相是:
- allowedLateness = 窗口状态延寿
- 延得越久,State 清得越慢
- RocksDB 越来越大
- Checkpoint 越来越慢
我的个人建议(很主观,但很实用):
迟到数据,不要全靠窗口兜。
可以考虑:
- 主流:严格窗口
- 迟到:侧输出流补偿
- 或异步修正下游结果
OutputTag<Order>lateTag=newOutputTag<>("late-data");.window(...).allowedLateness(Time.minutes(1)).sideOutputLateData(lateTag);窗口是算账的,不是擦屁股的。
五、State TTL:别指望系统帮你记一辈子
这是我见过最容易被忽略,却最救命的配置之一。
StateTtlConfigttlConfig=StateTtlConfig.newBuilder(Time.hours(1)).setUpdateType(StateTtlConfig.UpdateType.OnCreateAndWrite).cleanupFullSnapshot().build();什么时候一定要配 TTL?
- 会话类统计
- 用户画像
- 长 key 生命周期
- 维度多但访问稀疏
一句人话总结:
你不告诉 Flink 什么时候忘记,它就会帮你记到世界尽头。
六、别迷信增量聚合,也别滥用全量聚合
1️⃣ Reduce / Aggregate:内存友好型
.reduce((a,b)->a+b);优点:
- 状态小
- 边来边算
- 极其省内存
缺点:
- 逻辑有限
- 不适合复杂统计
2️⃣ ProcessWindowFunction:灵活但危险
process(key,context,elements,out)它会:
- 把窗口内所有数据攒齐
- 再一次性处理
适合:
- TopN
- 排序
- 复杂规则
但我一般建议:
能 Aggregate + Process 的,别直接 Process。
七、我这些年踩坑总结的一句话版本
如果你现在已经有点晕了,我帮你浓缩成几句“人话”:
- 窗口是业务口径,不是性能保障
- Trigger 决定了你多久喘一口气
- State TTL 决定了系统会不会老年痴呆
- allowedLateness 用多了,迟早要还
- 能增量算,别全量攒
八、写在最后:流式系统,拼的是“节制”
做流处理越久,我越有一种感觉:
真正牛的流式系统,都很克制。
- 不贪窗口
- 不留过多状态
- 不指望一次算完一切
它更像一个懂分寸的老会计:
边记账、边出报表、边丢旧账。