在Apache Flink中,处理数据流并将其分配到不同的分区(partition)是实现并行处理的关键手段之一。Flink提供了灵活的机制来控制数据如何分配到不同的并行任务(subtasks)上。下面是一些使用Flink DataStream API调用8种分区策略的实例,这些策略可以帮助你根据不同的需求来控制数据的分区。
1. 默认分区(Global Partitioning)
默认情况下,当你使用DataStream的keyBy方法但没有指定特定的分区器时,Flink会使用全局(Global)分区策略,即所有的数据都会被发送到同一个并行任务上。
DataStream<Tuple2<String, Integer>> stream = ...;
stream.keyBy(value -> value.f0)
.map(value -> value) // 示例操作
.setParallelism(8); // 设置并行度为8
2. 哈希分区(Hash Partitioning)
通过keyBy方法可以实现哈希分区,这是最常用的分区方式之一。
DataStream<Tuple2<String, Integer>> stream = ...;
stream.keyBy(value -> value.f0)
.map(value -> value) // 示例操作
.setParallelism(8); // 设置并行度为8
3. 重新平衡分区(Rebalance Partitioning)
使用rebalance()方法可以将数据均匀地重新分配到下游的所有并行任务中。
DataStream<Tuple2<String, Integer>> stream = ...;
stream.rebalance()
.map(value -> value) // 示例操作
.setParallelism(8); // 设置并行度为8
4. 重缩放分区(Rescale Partitioning)
与rebalance()类似,但rescale()主要用于上游和下游的并行度相同时。它会尝试最小化网络传输。
DataStream<Tuple2<String, Integer>> stream = ...;
stream.rescale()
.map(value -> value) // 示例操作
.setParallelism(8); // 设置并行度为8
5. 广播分区(Broadcast Partitioning)
使用broadcast()方法可以将数据流广播到所有下游任务。这在某些类型的全局状态更新场景中很有用。
DataStream<Tuple2<String, Integer>> stream = ...;
stream.broadcast()
.map(value -> value) // 示例操作
.setParallelism(8); // 设置并行度为8
6. 自定义分区(Custom Partitioning)
你可以实现自定义的分区逻辑,通过partitionCustom()方法。这需要你提供一个自定义的分区器。
DataStream<Tuple2<String, Integer>> stream = ...;
stream.partitionCustom(new MyCustomPartitioner(), keySelector)
.map(value -> value) // 示例操作
.setParallelism(8); // 设置并行度为8
其中MyCustomPartitioner是一个实现了org.apache.flink.api.common.operators.base.PartitionerDescriptor接口的类。
7. 范围分区(Range Partitioning)
范围分区通常用于有序的数据流,通过keyBy()后跟一个有序的数据类型来实现。例如,使用元组的第一个字段作为键。
ataStream<Tuple2<String, Integer>> stream = ...;
stream.keyBy(0) // 基于元组的第一个字段进行范围分区
.map(value -> value) // 示例操作
.setParallelism(8); // 设置并行度为8,确保下游并行度与上游一致或更大以充分利用范围分区特性。
8. 随机分区(Random Partitioning)
使用shuffle()方法可以将数据随机分配到下游的各个任务中。这通常用于需要随机打乱数据顺序的场景。
DataStream<Tuple2<String, Integer>> stream = ...;
stream.shuffle() // 随机分区
.map(value -> value) // 示例操作
.setParallelism(8); // 设置并行度为8
9. Forward分区(Forward Partitioning)
使用shuffle()方法可以将数据随机分配到下游的各个任务中。这通常用于需要随机打乱数据顺序的场景。
DataStream<Tuple2<String, Integer>> stream = ...;
stream.forward() // Forward分区
.map(value -> value) // 示例操作
.setParallelism(8); // 设置并行度为8
Flink 分区是为了解决并行计算时的数据重分布与负载均衡 问题;分区后的数据合并并非自动发生,需通过Union/Connect/Join/CoGroup 等算子显式组合,或依赖KeyedStream 的状态聚合逻辑 。
为什么要分区
- 适配并行度变化:上下游算子并行度不一致时,必须重分区才能将数据正确路由到所有下游子任务,避免数据丢失或资源浪费 。
- 实现负载均衡:防止数据倾斜,通过 Shuffle/Rebalance 等策略将数据均匀分发,提升集群资源利用率 。
- 满足业务逻辑需求 :如
keyBy按键分组确保相同 Key 进入同一子任务以进行状态计算;广播分区将配置流分发给所有实例;全局分区用于汇总到单点 。 - 跨节点通信基础:分布式环境下,数据需通过网络传输到特定 TaskManager 的特定 Slot,分区器定义了具体的路由规则 。
分区后数据如何"合并"
Flink 中"合并"指逻辑上的流汇聚,不同场景对应不同算子,物理分区本身不自动合并数据:
-
**简单拼接(Union)**
- 场景 :多条数据类型完全相同的流直接合并(如多源日志)。
- 机制 :
stream.union(otherStreams...),数据按 FIFO 混合,水位线取最小值,不进行去重或关联。 - 注意:仅合并流结构,不改变数据内容 。
-
**异构连接(Connect)**
- 场景 :两条数据类型不同的流需关联处理(如订单流 + 用户画像流)。
- 机制 :
stream1.connect(stream2)生成ConnectedStreams,需配合CoProcessFunction自定义逻辑,可共享状态但流内部独立 。 - 关键 :常先对双流执行
keyBy将相同 Key 路由到同一子任务,再在函数内匹配处理 。
-
**时间窗口关联(Join / Interval Join)**
- 场景 :基于时间窗口和 Key 匹配两条流中的事件(如点击流 + 转化流)。
- 机制 :
stream1.join(stream2).where(...).equalTo(...).window(...).apply(...),仅在窗口内且 Key 匹配的数据对才会输出,非匹配数据丢弃(Inner Join 逻辑)。 - 前提:必须定义 Watermark 以处理事件时间 。
-
**分组聚合(KeyedStream + Reduce/Aggregate)**
- 场景 :分区(
keyBy)后对同一 Key 的数据进行统计(如求和、计数)。 - 机制 :
keyBy将相同 Key 强制路由到同一子任务,后续调用reduce/aggregate/sum在该子任务内部逻辑合并状态,输出单条结果。这是最典型的"分区后合并计算"模式 。
- 场景 :分区(
-
**全局汇总(Global + Sink)**
- 场景:将所有数据强制发送到单个子任务进行最终汇总。
- 机制 :使用
.global()分区算子将所有记录路由到下游第一个子任务(并行度实际失效为 1),随后在该子任务内聚合或写入 Sink。易造成单点瓶颈,慎用 。
核心区别 :Union/Connect 是流的物理拼接,数据量不变;keyBy+ 聚合 是逻辑归并,数据量通常减少;Join 是条件匹配,数据量取决于匹配结果。选择取决于业务是需"拼接数据"、"关联分析"还是"统计聚合"。