Flink 状态分哪几类
状态是流计算的记忆:跨事件累计、去重、会话切分都靠它。键控状态按 key 分区存放,同一个 key 一定落到同一个并行子任务,因此可以放心做累计;算子状态挂在算子实例上,与 key 无关。状态放在哪里由状态后端决定,内存快但容量小,磁盘慢但能扛住大状态。
基本信息
| 键控状态 | ValueState / ListState / MapState / ReducingState |
|---|---|
| 算子状态 | 算子实例级共享,与 key 无关 |
| 状态后端 | HashMapStateBackend(内存)/ EmbeddedRocksDBStateBackend(磁盘) |
| 过期清理 | StateTtlConfig 配置 TTL |
| 影响面 | 并行度调整会改变 key 的分组归属 |
典型使用场景
- 实时累计:当天各门店的支付金额按 key 累加
- 去重:用状态记录已处理的订单号,避免重复计数
- 会话切分:同一用户超过一定时间没有事件则切分新会话
- 维表缓存:把不常变的映射放进键控状态,减少外部查询
- 告警抑制:短时间内重复告警只在首次触发
状态与 TTL 示例
状态与 TTL 示例
ValueStateDescriptor<Long> desc =
new ValueStateDescriptor<>("pay_sum", Types.LONG);
// 给状态设置过期时间,避免无界增长
StateTtlConfig ttl = StateTtlConfig
.newBuilder(Time.days(7))
.setUpdateType(StateTtlConfig.UpdateType.OnCreateAndWrite)
.setStateVisibility(StateTtlConfig.StateVisibility.NeverReturnExpired)
.build();
desc.enableTimeToLive(ttl); // 配置完成后注册到 RuntimeContext状态后端对照
| 后端 | 状态存放 | 适用判断 |
|---|---|---|
| HashMapStateBackend | JVM 堆内存 | 状态小、追求低延迟 |
| EmbeddedRocksDBStateBackend | 本地磁盘 + 内存缓存 | 状态大、键数量多 |
| 堆内存 + 大状态 | 不推荐 | 易触发内存溢出 |
| 磁盘后端 + 高频读写 | 需评估 | 读写放大影响吞吐 |
状态为什么会失控
状态体积增长通常不是数据量增长造成的,而是没有过期策略:用户维度、会话维度的 key 只增不减,每条记录都往状态里写一笔,几个月后单个子任务的内存占用就能顶上早期整个作业的量。检查点的快照体积同步增长,做一次检查点的时间越来越长,最后超过超时阈值频繁失败。
选择状态后端不能只看内存:RocksDB 后端能容纳远超堆内存的状态,代价是每次读写都要序列化,对延迟敏感的场景要实测。已知状态上限很小的情况,内存后端反而更稳。
没有 TTL 的大状态会让检查点耗时持续上升直至超时,作业反复重启并重新加载状态,形成恶性循环。上线前就把 TTL 定下来,比事后补救容易得多。