一、窗口 Window
窗口分类:
┌─────────────────────────────────────────────────────────────┐
│ Window 分类 │
├──────────────────┬────────────────────┬─────────────────────┤
│ Tumbling Window │ Sliding Window │ Session Window │
│ (滚动窗口) │ (滑动窗口) │ (会话窗口) │
│ │ │ │
│ ┌──┐┌──┐┌──┐ │ ┌────┐ │ ┌──┐ ┌────┐ ┌─┐ │
│ │ ││ ││ │ │ │┌────┐ │ │ │ │ │ │ │ │
│ │ ││ ││ │ │ ││┌────┐ │ │ │ │ │ │ │ │
│ └──┘└──┘└──┘ │ └┘└────┘ │ └──┘ └────┘ └─┘ │
│ 无重叠,首尾相连 │ 固定大小,可重叠 │ 活动间隙切分 │
├──────────────────┴────────────────────┴─────────────────────┤
│ Global Window (全局窗口 + 自定义 Trigger) │
└─────────────────────────────────────────────────────────────┘
窗口使用示例:
// 滚动事件时间窗口 - 每5秒统计一次
keyedStream
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
.reduce((a, b) -> new Result(a.key, a.count + b.count));
// 滑动处理时间窗口 - 窗口10秒,步长5秒
keyedStream
.window(SlidingProcessingTimeWindows.of(Time.seconds(10), Time.seconds(5)))
.aggregate(new MyAggregateFunction());
// 会话窗口 - 30秒无活动则关闭窗口
keyedStream
.window(EventTimeSessionWindows.withGap(Time.seconds(30)))
.process(new MyProcessWindowFunction());
// 全局窗口 + 自定义触发器(如每100条触发一次)
keyedStream
.window(GlobalWindows.create())
.trigger(CountTrigger.of(100))
.apply(new MyWindowFunction());
窗口函数选择:
|----------------------------|----------------------|-----------------|
| 窗口函数 | 特点 | 适用场景 |
| ReduceFunction | 增量聚合,内存占用小 | 简单累加/求最值 |
| AggregateFunction | 增量聚合,支持不同输入/累加器/输出类型 | 复杂聚合(如均值) |
| ProcessWindowFunction | 全量计算,可访问窗口元信息 | 需要排序/Top-N/窗口时间 |
| Reduce/Aggregate + Process | 增量+全量结合 | 既要高效又要窗口元信息 |
推荐模式:增量聚合 + ProcessWindowFunction 结合
// 先增量聚合,最后附加窗口信息
keyedStream
.window(TumblingEventTimeWindows.of(Time.minutes(1)))
.aggregate(
new MyAggregateFunction(), // 增量聚合逻辑
new MyProcessWindowFunction() // 附加窗口 start/end 时间
);
// ProcessWindowFunction 获取窗口元信息
public class MyProcessWindowFunction
extends ProcessWindowFunction<AggResult, OutputResult, String, TimeWindow> {
@Override
public void process(String key, Context context,
Iterable<AggResult> elements, Collector<OutputResult> out) {
AggResult aggResult = elements.iterator().next();
TimeWindow window = context.window();
out.collect(new OutputResult(
key, aggResult, window.getStart(), window.getEnd()
));
}
}
迟到数据处理:
DataStream<Order> result = orderStream
.keyBy(Order::getUserId)
.window(TumblingEventTimeWindows.of(Time.minutes(5)))
.allowedLateness(Time.minutes(1)) // 允许迟到1分钟
.sideOutputLateData(lateOutputTag) // 超过容忍度的数据输出到侧流
.reduce(new OrderReduceFunction());
// 获取迟到数据侧流
DataStream<Order> lateOrders = result.getSideOutput(lateOutputTag);
lateOrders.addSink(new LateDateAlertSink()); // 可写入告警或延迟表
二、水位线 Watermark
Watermark 是 Flink 事件时间处理的核心机制,用于衡量事件时间的推进进度。
语义定义:Watermark(t) 表示------不会再有时间戳 ≤ t 的事件到来。
事件流(按到达顺序):
时间 → ──[e1:t=1]──[e3:t=3]──[e2:t=2]──[W(3)]──[e5:t=5]──[e4:t=4]──[W(5)]──→
含义:
• W(3) 到达时:Flink 认为 t≤3 的数据已全部到齐,触发 [0,3) 的窗口计算
• W(5) 到达时:Flink 认为 t≤5 的数据已全部到齐,触发 [3,5) 的窗口计算
• e4(t=4) 在 W(5) 之前到达 → 正常处理
• 如果 e4 在 W(5) 之后到达 → 迟到数据(由 allowedLateness 决定处理方式)
Watermark 生成策略:
// 策略1:有序流(Watermark = 当前最大时间戳)
WatermarkStrategy.<Event>forMonotonousTimestamps()
.withTimestampAssigner((event, timestamp) -> event.getTimestamp());
// 策略2:乱序流 - 固定延迟(最常用)
WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ofSeconds(5))
.withTimestampAssigner((event, timestamp) -> event.getTimestamp());
// 策略3:自定义 Watermark 生成器
WatermarkStrategy.<Event>forGenerator(ctx -> new WatermarkGenerator<Event>() {
private long maxTimestamp = Long.MIN_VALUE;
@Override
public void onEvent(Event event, long eventTimestamp, WatermarkOutput output) {
maxTimestamp = Math.max(maxTimestamp, eventTimestamp);
}
@Override
public void onPeriodicEmit(WatermarkOutput output) {
// 每隔 autoWatermarkInterval 调用一次
output.emitWatermark(new Watermark(maxTimestamp - 5000)); // 5s 容忍度
}
});
Watermark 在多并行度下的传播:
Source(p=2) Map(p=2) Window(p=2)
┌──────────┐ ┌──────────┐ ┌──────────────┐
│ Source-0 │─W(3)→│ Map-0 │─W(3)→ │ │
│ │ │ │ │ Window-0 │
│ Source-1 │─W(5)→│ Map-1 │─W(5)→ │ 取 min(3,5) │
└──────────┘ └──────────┘ │ = W(3) │
└──────────────┘
多输入算子取所有输入通道 Watermark 的最小值作为自己的 Watermark。
Watermark 与 Window 的协作流程:
┌──────────┐ 事件(带时间戳) ┌─────────────────┐ Watermark ┌──────────┐
│ Source │ ───────────────→ │ WatermarkStrategy│ ──────────→ │ Window │
│ │ │ (提取时间戳 + │ │ Operator │
│ │ │ 生成 Watermark) │ │ │
└──────────┘ └─────────────────┘ └────┬─────┘
│
▼
Watermark >= window.end
│
▼
┌──────────────┐
│ 触发窗口计算 │
│ (Fire Window)│
└──────────────┘
三、状态管理 State
State 分类:
┌─────────────────────────────────────────────────────────┐
│ Flink State │
├────────────────────────┬────────────────────────────────┤
│ Keyed State │ Operator State │
│ (键控状态) │ (算子状态) │
│ │ │
│ • ValueState<T> │ • ListState<T> │
│ • ListState<T> │ • UnionListState<T> │
│ • MapState<K,V> │ • BroadcastState<K,V> │
│ • ReducingState<T> │ │
│ • AggregatingState<I,O>│ │
│ │ │
│ 作用域:per-key │ 作用域:per-operator-subtask │
│ 使用前提:KeyedStream │ 使用场景:Source/Sink Connector│
└────────────────────────┴────────────────────────────────┘
Keyed State 使用示例:
public class DeduplicateFunction extends KeyedProcessFunction<String, Event, Event> {
// 声明 ValueState:记录是否已出现过
private ValueState<Boolean> seenState;
@Override
public void open(OpenContext openContext) throws Exception {
// 配置 State TTL(状态过期清理,防止状态无限增长)
StateTtlConfig ttlConfig = StateTtlConfig.newBuilder(Duration.ofHours(24))
.setUpdateType(StateTtlConfig.UpdateType.OnCreateAndWrite)
.setStateVisibility(StateTtlConfig.StateVisibility.NeverReturnExpired)
.cleanupFullSnapshot() // 全量快照时清理
.cleanupInRocksdbCompactFilter(1000) // RocksDB compaction 时清理
.build();
ValueStateDescriptor<Boolean> descriptor =
new ValueStateDescriptor<>("seen", Boolean.class);
descriptor.enableTimeToLive(ttlConfig);
seenState = getRuntimeContext().getState(descriptor);
}
@Override
public void processElement(Event event, Context ctx, Collector<Event> out) throws Exception {
if (seenState.value() == null) {
// 首次出现,输出并标记
seenState.update(true);
out.collect(event);
}
// 重复数据直接丢弃
}
}
MapState 使用示例:
public class OrderJoinFunction extends KeyedProcessFunction<String, Event, JoinedResult> {
private MapState<String, Order> orderState;
private MapState<String, Payment> paymentState;
@Override
public void open(OpenContext openContext) throws Exception {
orderState = getRuntimeContext().getMapState(
new MapStateDescriptor<>("orders", String.class, Order.class)
);
paymentState = getRuntimeContext().getMapState(
new MapStateDescriptor<>("payments", String.class, Payment.class)
);
}
@Override
public void processElement(Event event, Context ctx, Collector<JoinedResult> out)
throws Exception {
if (event instanceof Order) {
Order order = (Order) event;
// 检查是否已有匹配的支付
Payment payment = paymentState.get(order.getOrderId());
if (payment != null) {
out.collect(new JoinedResult(order, payment));
paymentState.remove(order.getOrderId());
} else {
orderState.put(order.getOrderId(), order);
// 注册定时器,超时清理
ctx.timerService().registerEventTimeTimer(
ctx.timestamp() + 60_000L // 60s 超时
);
}
}
// ... 处理 Payment 事件类似
}
@Override
public void onTimer(long timestamp, OnTimerContext ctx, Collector<JoinedResult> out) {
// 超时清理未匹配的状态
// ...
}
}
State Backend 选择:
|-----------------------------|-----------------------------------|----------------------|
| State Backend | 特点 | 适用场景 |
| HashMapStateBackend | 状态存储在 JVM 堆上,访问速度快 | 状态量小(< 几 GB)、低延迟要求高 |
| EmbeddedRocksDBStateBackend | 状态存储在 RocksDB(磁盘),支持增量 Checkpoint | 状态量大(TB级)、生产环境首选 |
// 配置 RocksDB State Backend
env.setStateBackend(new EmbeddedRocksDBStateBackend(true)); // true = 增量checkpoint
env.getCheckpointConfig().setCheckpointStorage("hdfs:///flink/checkpoints");
四、常见踩坑点
- Watermark 不推进(空闲分区问题)
现象:Kafka Topic 有多个分区,部分分区长时间无数据,导致全局 Watermark 无法推进,窗口不触发。
原因:多输入通道取 min(所有通道 Watermark),空闲通道 Watermark 停留在初始值。
解决方案:
WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ofSeconds(5))
.withIdleness(Duration.ofMinutes(2)) // 2分钟无数据则标记为 idle
.withTimestampAssigner((event, ts) -> event.getTimestamp());
- State 无限膨胀导致 OOM
现象:作业运行一段时间后 TaskManager OOM,Checkpoint 越来越大。
原因:Keyed State 没有设置 TTL,Key 空间持续增长但从不清理过期数据。
解决方案:
- 为所有 State 配置合理的 TTL
- 使用定时器(Timer)主动清理不再需要的状态
- 生产环境使用 RocksDB State Backend(不受堆内存限制)
- KeyBy 数据倾斜
现象:部分 TaskManager 负载极高,其他空闲;Checkpoint 因个别 Task 慢导致超时。
原因:Key 分布不均(如某个用户 ID 产生大量事件、使用了大量 null key)。
解决方案:
-
预聚合 + 二次聚合(local pre-aggregation)
-
在 Key 后附加随机后缀打散(需配合二次合并)
-
过滤或旁路输出热点 Key
// 二次聚合打散热点
DataStreampreAgg = stream
.map(e -> new KeyedEvent(e.key + "_" + ThreadLocalRandom.current().nextInt(10), e))
.keyBy(KeyedEvent::getKey)
.window(...)
.reduce(...);DataStream
finalResult = preAgg
.map(e -> new KeyedEvent(e.key.split("_")[0], e))
.keyBy(KeyedEvent::getKey)
.window(...)
.reduce(...);
- Checkpoint 超时/失败
现象:Checkpoint 频繁超时或失败,作业不稳定。
常见原因及解决:
|------------------|---------------------------------------------------------------------------------|
| 原因 | 解决方案 |
| 状态太大 | 使用 RocksDB + 增量 Checkpoint |
| 反压导致 Barrier 对齐慢 | 启用 Unaligned Checkpoint(env.getCheckpointConfig().enableUnalignedCheckpoints()) |
| Source 消费慢 | 提高并行度或优化数据源 |
| GC 频繁 | 优化堆外内存、调整 JVM 参数 |
- 序列化问题
现象:运行时抛出 org.apache.flink.api.common.InvalidProgramException 或序列化相关异常。
原因:
- 匿名内部类/Lambda 捕获了不可序列化的外部对象
- POJO 缺少无参构造函数或 public 字段
解决方案:
-
使用 RichFunction + open() 方法延迟初始化不可序列化的对象
-
POJO 确保有无参构造函数 + 所有字段为 public 或有 getter/setter
-
优先使用 Flink 的 TypeInformation 体系(Tuple、Row、POJO)
// 错误写法:ObjectMapper 不可序列化
stream.map(json -> new ObjectMapper().readValue(json, Order.class)); // ❌ 每条都创建// 正确写法:在 RichFunction 中初始化
stream.map(new RichMapFunction<String, Order>() {
private transient ObjectMapper mapper;@Override public void open(OpenContext openContext) { mapper = new ObjectMapper(); } @Override public Order map(String json) throws Exception { return mapper.readValue(json, Order.class); }}); // ✅
- Window Join 丢数据
现象:Window Join 结果比预期少很多。
原因:Window Join 默认是 Inner Join 语义------只有两侧在同一窗口内都有匹配 key 的数据才会输出。
解决方案:
- 如需 Left/Right/Full Outer Join 语义,使用 coGroup 替代
- 或使用 connect + State 实现自定义匹配逻辑