Flink源码分析系列文档目录
请点击:Flink 源码分析系列文档目录
前言
Flink流处理作业支持并行操作。每一个并行度可以理解为一个数据管道。上游和下游的并行度也可能是不同的。为了解决数据从上游到下游的分发策略问题,Flink提供了一系列分区(partition)算子。下面为大家介绍分区算子以及他们对应的Partitioner(分区器)。
5种分区算子
Flink共有5种类型的分区算子:
- 随机分区:随机挑选一个下游管道,将数据发送给它。代表算子为shuffle。
- 轮询分区:轮流将数据发送给各个下游管道。代表算子为rebalance(上游和所有下游建立连接,轮询发送)和 rescale(下游和上游对应关系是确定的,无论扩容还是缩容)。
- 广播:发送数据到所有下游管道。代表算子为broadcast。
- 按键值分区:需要用户指定key抽取逻辑。根据数据流中的元素,提取出这条数据的key,然后将key做hash运算,决定发送给下游哪个管道,确保key相同的数据一定被发送到同一个下游管道。代表算子为keyBy。
- 发送到下游算子第一个并行任务:上游所有管道的数据合在一起。代表算子为global。
- 自定义规则:自定义数据分发策略。代表算子为partitionCustom。
这些算子在底层对应了不同的Partitioner(分区器)。分区器为分区策略的具体实现方式。下面为大家列出这些算子创建分区器的逻辑。分区器的具体实现即将在下一个章节详细介绍。
shuffle
shuffle
算子对应的分区器为ShufflePartitioner
。
@PublicEvolving
public DataStream shuffle() {
return setConnectionType(new ShufflePartitioner());
}
rebalance
rebalance
算子对应的分区器为RebalancePartitioner
。
public DataStream rebalance() {
return setConnectionType(new RebalancePartitioner());
}
rescale
rescale
算子对应的分区器为RescalePartitioner
。
@PublicEvolving
public DataStream rescale() {
return setConnectionType(new RescalePartitioner());
}
broadcast
broadcast
算子对应的分区器为BroadcastPartitioner
。
public DataStream broadcast() {
return setConnectionType(new BroadcastPartitioner());
}
keyBy
keyBy
算子对应的分区器为KeyGroupStreamPartitioner
。
DataStream
的keyBy
方法代码如下:
public KeyedStream keyBy(KeySelector key) {
Preconditions.checkNotNull(key);
return new KeyedStream<>(this, clean(key));
}
发现这里面没有使用到Partitioner,而是创建了一个KeyedStream
。我们继续跟踪它的构造函数:
KeyedStream.java
public KeyedStream(DataStream dataStream, KeySelector keySelector) {
this(
dataStream,
keySelector,
TypeExtractor.getKeySelectorTypes(keySelector, dataStream.getType()));
}
public KeyedStream(
DataStream dataStream,
KeySelector keySelector,
TypeInformation keyType) {
this(
dataStream,
// 在这里创建出了一个`PartitionTransformation`,专用于分区操作
// 它内部封装了`KeyGroupStreamPartitioner`
new PartitionTransformation<>(
dataStream.getTransformation(),
new KeyGroupStreamPartitioner<>(
keySelector,
StreamGraphGenerator.DEFAULT_LOWER_BOUND_MAX_PARALLELISM)),
keySelector,
keyType);
}
global
global
算子对应的分区器为GlobalPartitioner
。
@PublicEvolving
public DataStream global() {
return setConnectionType(new GlobalPartitioner());
}
partitionCustom
partitionCustom
算子对应的分区器为CustomPartitionerWrapper
。
public DataStream partitionCustom(
Partitioner partitioner, KeySelector keySelector) {
return setConnectionType(
new CustomPartitionerWrapper<>(clean(partitioner), clean(keySelector)));
}
8种分区器
前面章节讲解了Flink的5种分区算子,接下来为大家带来分区器的讲解。
Flink共有8种类型的分区器:
- BroadcastPartitioner
- CustomPartitionerWrapper
- ForwardPartitioner
- GlobalPartitioner
- KeyGroupStreamPartitioner
- RebalancePartitioner
- RescalePartitioner
- ShufflePartitioner
其中除了ForwardPartitioner
,其余的在上面都已经提到过。
这些分区器的父类为StreamPartitioner
。它本质上是一种ChannelSelector
,用来根据规则决定数据被发往下游哪一个channel。
接下来我们父类StreamPartitioner
开始,逐个介绍下分区器的实现。
StreamPartitioner和ChannelSelector
将StreamPartitioner
和ChannelSelector
放在第一位介绍的原因是我们需要提前了解它内部各个方法的作用。它的8个子类仅仅是覆盖或重写了这些方法而已。
ChannelSelector
接口的各个方法定义如下所示:
public interface ChannelSelector {
// 初始化ChannelSelector,传入的参数为下游channel(output channel)的数量
void setup(int numberOfChannels);
// 返回选择的channel索引编号,这个方法决定的上游的数据需要写入到哪个channel中
// 这个方法的Partitioner子类重点需要实现的方法
// 对于broadcast广播类型算子,不需要实现这个方法
// 传入的参数为记录数据流中的元素,该方法需要根据元素来推断出需要发送到的下游channel
int selectChannel(T record);
// 返回是否为广播类型
// 广播类型指的是上游数据发送给所有下游channel
boolean isBroadcast();
}
StreamPartitioner
抽象类实现了StreamPartitioner
接口,它的代码如下所示:
@Internal
public abstract class StreamPartitioner
implements ChannelSelector>>, Serializable {
private static final long serialVersionUID = 1L;
// 持有output channel数量
protected int numberOfChannels;
// 初始化的时候设置numberOfChannels的值
@Override
public void setup(int numberOfChannels) {
this.numberOfChannels = numberOfChannels;
}
@Override
public boolean isBroadcast() {
return false;
}
public abstract StreamPartitioner copy();
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final StreamPartitioner> that = (StreamPartitioner>) o;
return numberOfChannels == that.numberOfChannels;
}
@Override
public int hashCode() {
return Objects.hash(numberOfChannels);
}
// 决定了作业恢复时候上游遇到扩缩容的话,需要处理哪些上游状态保存的数据
public SubtaskStateMapper getUpstreamSubtaskStateMapper() {
return SubtaskStateMapper.ARBITRARY;
}
/**
* Defines the behavior of this partitioner, when downstream rescaled during recovery of
* in-flight data.
*/
// 同上,但关注的是下游扩缩容
public abstract SubtaskStateMapper getDownstreamSubtaskStateMapper();
// 重要
// isPointwise方法决定了上游和下游的对应关系。
// false表示没有指向性,上游和下游没有明确的对应关系
// true表示上游和下游存在对应关系
public abstract boolean isPointwise();
}
接下来我们开始介绍它的子类。
ForwardPartitioner
ForwardPartitioner
直接将数据发送给本地下游(一对一)。要求上游的并行度和下游必须相同。
@Override
public int selectChannel(SerializationDelegate> record) {
return 0;
}
@Override
public boolean isPointwise() {
return true;
}
ForwardPartitioner
在StreamGraph
的addEdgeInternal
方法中自动创建(生成StreamGraph的过程),代码片段如下所示:
// ...
if (partitioner == null
&& upstreamNode.getParallelism() == downstreamNode.getParallelism()) {
// 只有在上游和下游的并行度相同的时候,才会使用ForwardPartitioner
partitioner = new ForwardPartitioner
除此之外还可以通过DataStream
的forward
算子创建,但是这个算子不常用。
public DataStream forward() {
return setConnectionType(new ForwardPartitioner());
}
KeyGroupStreamPartitioner
KeyGroupStreamPartitioner
根据StreamRecord
计算出它的key,然后根据key决定发往下游哪个channel。它包含一个KeySelector
,用于从StreamRecord
中抽取出key。
@Override
public int selectChannel(SerializationDelegate> record) {
K key;
try {
// 用户的KeyBy算子参数被转换为keySelector,抽取出数据对应的key
key = keySelector.getKey(record.getInstance().getValue());
} catch (Exception e) {
throw new RuntimeException(
"Could not extract key from " + record.getInstance().getValue(), e);
}
// 计算出key值对应的channel,通过hash算法确保同样的key计算出channel是相同的
// hash使用murmurhash,详细过程这里不再介绍
return KeyGroupRangeAssignment.assignKeyToParallelOperator(
key, maxParallelism, numberOfChannels);
}
RebalancePartitioner
轮询分区器。上游将数据轮询发往下游。
@Override
public void setup(int numberOfChannels) {
super.setup(numberOfChannels);
nextChannelToSendTo = ThreadLocalRandom.current().nextInt(numberOfChannels);
}
@Override
public int selectChannel(SerializationDelegate> record) {
nextChannelToSendTo = (nextChannelToSendTo + 1) % numberOfChannels;
return nextChannelToSendTo;
}
// 它的上游和下游没有明确对应关系,只依赖轮询结果
@Override
public boolean isPointwise() {
return false;
}
// 故障恢复时,将保存的出站数据轮询发给下游
@Override
public SubtaskStateMapper getDownstreamSubtaskStateMapper() {
return SubtaskStateMapper.ROUND_ROBIN;
}
RescalePartitioner
分区缩放,上游一个分区可以对应下游固定的多个分区(扩容),或者多个固定的上游分区对应一个下游分区(缩容)。
@Override
public int selectChannel(SerializationDelegate> record) {
if (++nextChannelToSendTo >= numberOfChannels) {
nextChannelToSendTo = 0;
}
return nextChannelToSendTo;
}
// 它的上游和下游明确对应
@Override
public boolean isPointwise() {
return true;
}
// 恢复时候不支持扩缩容,因为原先的对应关系被破坏了
@Override
public SubtaskStateMapper getDownstreamSubtaskStateMapper() {
return SubtaskStateMapper.UNSUPPORTED;
}
@Override
public SubtaskStateMapper getUpstreamSubtaskStateMapper() {
return SubtaskStateMapper.UNSUPPORTED;
}
ShufflePartitioner
将上游数据随机派发给下游channel。
@Override
public int selectChannel(SerializationDelegate> record) {
return random.nextInt(numberOfChannels);
}
GlobalPartitioner
将所有上游的数据发往第一个下游。
// 这里和ForwardPartitioner相同
@Override
public int selectChannel(SerializationDelegate> record) {
return 0;
}
// 任务恢复的时候,将所有任务恢复到第一个子任务
@Override
public SubtaskStateMapper getDownstreamSubtaskStateMapper() {
return SubtaskStateMapper.FIRST;
}
// 非点对点,上下游没有对应关系
@Override
public boolean isPointwise() {
return false;
}
BroadcastPartitioner
将上游的数据广播到所有的下游。它重写了isBroadcast
方法。
@Override
public boolean isBroadcast() {
return true;
}
CustomPartitionerWrapper
自定义Partitioner。用户使用的时候需要传入两个参数:
- Partitioner: 分区逻辑,根据key计算出分区id索引。
- KeySelector: key抽取逻辑,从流数据中抽取出key。
selectChannel
方法如下所示:
@Override
public int selectChannel(SerializationDelegate> record) {
K key;
try {
// 这里和KeyGroupStreamPartitioner相同
key = keySelector.getKey(record.getInstance().getValue());
} catch (Exception e) {
throw new RuntimeException("Could not extract key from " + record.getInstance(), e);
}
// 调用用户自定义的分区逻辑
return partitioner.partition(key, numberOfChannels);
}
Partitioner是怎么被调用的
Flink通过RecordWriter
向下游写入输入。RecordWriter
通过RecordWriterBuilder
创建。
public RecordWriter build(ResultPartitionWriter writer) {
if (selector.isBroadcast()) {
return new BroadcastRecordWriter<>(writer, timeout, taskName);
} else {
return new ChannelSelectorRecordWriter<>(writer, selector, timeout, taskName);
}
}
该Builder返回RecordWriter
的时候会检查ChannelSelector
的类型。如果isBroadcast
返回true,创建BroadcastRecordWriter
,否则就创建ChannelSelectorRecordWriter
。
接下来我们分别查看他们的emit
方法。
BroadcastRecordWriter
的emit
方法:
@Override
public void emit(T record) throws IOException {
// 调用broadcastEmit方法
broadcastEmit(record);
}
@Override
public void broadcastEmit(T record) throws IOException {
checkErroneous();
// 调用目标partition的broadcastRecord方法,广播发送数据
// 发送前先将数据序列化
targetPartition.broadcastRecord(serializeRecord(serializer, record));
if (flushAlways) {
flushAll();
}
}
ChannelSelectorRecordWriter
的emit
方法调用了父类的emit(T record, int targetSubpartition)
方法,第一个参数是需要发送到下游的数据,第二个参数为目标子分区索引值。其中targetSubpartition
通过channelSelector.selectChannel(record)
计算出。这里的ChannelSelector
的实现类为上面介绍的8种分区器之一。代码如下所示:
@Override
public void emit(T record) throws IOException {
emit(record, channelSelector.selectChannel(record));
}
本博客为作者原创,欢迎大家参与讨论和批评指正。如需转载请注明出处。