本文从基础原理到代码层面逐步解释 Flink 的数据通道 StreamEdge
,尽量让初学者也能理解。
主要思路:从概念开始,逐步深入到实现细节,并结合伪代码来逐步推导。
第一步:什么是 StreamEdge?
StreamEdge
是 Flink 中用于表示数据流图(Stream Graph)中两个算子(Operator)之间数据传输关系的对象。简单来说,它是一条"数据通道",定义了数据如何从一个算子流向下游的另一个算子。想象成一条水管,上游的水(数据)通过这条管子流到下游。
- 为什么需要 StreamEdge?
Flink 是一个分布式流处理框架,数据需要在不同的任务(Task)之间传递。StreamEdge
负责描述这种传递的逻辑,包括"从哪里来"(源算子)、"到哪里去"(目标算子)、"数据如何分区"(Partitioning)等。
第二步:数据流图的基础
在 Flink 中,用户定义的程序(比如"读取数据 -> 过滤 -> 聚合")会被转换成一个逻辑执行计划,叫做 StreamGraph
。这个图由以下部分组成:
- StreamNode:表示算子(比如 map、filter 等操作)。
- StreamEdge:表示算子之间的连接。
例如:
bash
输入数据 -> map 操作 -> sink 操作
在 StreamGraph
中:
- 有 3 个
StreamNode
:输入源(source)、map、sink。 - 有 2 个
StreamEdge
:输入源(source)到 map,map 到 sink。
StreamEdge
的作用是连接这些节点,并定义数据传输的规则。
第三步:StreamEdge 的核心属性
StreamEdge
是一个 Java 类,我们来看看它的主要属性(基于 Flink 源代码,比如 org.apache.flink.streaming.api.graph.StreamEdge
):
- 源节点 ID(sourceId)
表示数据从哪个算子发出。 - 目标节点 ID(targetId)
表示数据流向哪个算子。 - 分区策略(Partitioning)
定义数据如何分配到下游。比如:FORWARD
:直接发给下游的同一个任务。HASH
:根据键(key)哈希分配。BROADCAST
:发送到下游所有任务。
- 数据类型(OutputTag)
如果有侧输出(side output),会标记数据的类型。
用生活中的例子比喻:
- 源节点和目标节点像是寄信的"发件人"和"收件人"。
- 分区策略像是"快递分拣规则"(按地址分、全部发、随机发等)。
第四步:StreamEdge 的创建过程
让我们一步步推导 StreamEdge
如何在 Flink 中生成:
1. 用户代码
假设写了一个简单的 Flink 程序:
java
DataStream<String> input = env.fromElements("a", "b", "c");
DataStream<String> mapped = input.map(x -> x.toUpperCase());
mapped.print();
这里有 3 个操作:输入源 -> map -> print。
2. 构建 StreamGraph
Flink 的 StreamGraphGenerator
会把这个程序翻译成图:
- 创建
StreamNode
:为每个算子分配一个 ID。 - 创建
StreamEdge
:连接这些节点。
伪代码(简化的生成过程):
java
StreamNode sourceNode = new StreamNode(1, "Source");
StreamNode mapNode = new StreamNode(2, "Map");
StreamNode sinkNode = new StreamNode(3, "Sink");
StreamEdge edge1 = new StreamEdge(sourceNode, mapNode, Partitioning.FORWARD);
StreamEdge edge2 = new StreamEdge(mapNode, sinkNode, Partitioning.FORWARD);
3. 分区策略的推导
- 从
source
到map
,数据是顺序传递的,所以用FORWARD
。 - 从
map
到sink
,也是直接输出,所以也是FORWARD
。
如果用户加了 keyBy
(比如按某个字段分组),分区策略会变成 HASH
,数据会根据键的哈希值分配。
第五步:底层实现与数据传输
StreamEdge
只是逻辑表示,实际数据传输发生在运行时,由 StreamTask
和 RecordWriter
完成。
-
序列化与发送
- 数据被序列化(比如 "A" 变成字节数组)。
RecordWriter
根据StreamEdge
的分区策略决定发送目标。
-
网络传输
- 如果下游在另一台机器上,数据通过网络发送(基于 Netty)。
- 分区策略决定数据发到哪个子任务(Subtask)。
-
反序列化与处理
- 下游任务收到数据,反序列化后交给目标算子处理。
用伪代码表示:
java
class RecordWriter {
void emit(Record record, StreamEdge edge) {
if (edge.partitioning == FORWARD) {
sendToLocalTask(edge.targetId, record);
} else if (edge.partitioning == HASH) {
int targetSubtask = hash(record.key) % numSubtasks;
sendToRemoteTask(edge.targetId, targetSubtask, record);
}
}
}
第六步:从代码层面看 StreamEdge
以下是 StreamEdge
类的简化版(基于 Flink 1.18 左右的源码):
java
public class StreamEdge {
private final int sourceId; // 源节点 ID
private final int targetId; // 目标节点 ID
private final StreamPartitioner<?> partitioner; // 分区策略
private final OutputTag outputTag; // 侧输出标记(可选)
public StreamEdge(StreamNode source, StreamNode target, StreamPartitioner<?> partitioner) {
this.sourceId = source.getId();
this.targetId = target.getId();
this.partitioner = partitioner;
this.outputTag = null; // 默认无侧输出
}
public int getSourceId() { return sourceId; }
public int getTargetId() { return targetId; }
public StreamPartitioner<?> getPartitioner() { return partitioner; }
}
StreamPartitioner
是一个接口,定义了具体分区逻辑(比如ForwardPartitioner
、KeyGroupStreamPartitioner
)。
第七步:完整推导示例
假设输入是 {("key1", 1), ("key2", 2)}
,经过 keyBy(key)
和 sum(value)
:
-
StreamGraph:
StreamNode1
:SourceStreamNode2
:KeyBy + SumStreamEdge
:Source -> KeyBy,分区策略为HASH
。
-
数据流:
- ("key1", 1) 的哈希值算出目标子任务 0。
- ("key2", 2) 的哈希值算出目标子任务 1。
RecordWriter
根据StreamEdge
的HASH
策略发送。
-
结果:
- 子任务 0 计算 "key1" 的和。
- 子任务 1 计算 "key2" 的和。
总结
StreamEdge
是 Flink 数据流处理的核心桥梁:
- 逻辑层面:定义算子间的数据流向和分区规则。
- 运行时层面:指导数据如何在分布式环境中传输。
- 代码层面:通过属性和分区器实现灵活的分发。
通过上面的逐步推导,从"水管"比喻到代码实现,希望你对 StreamEdge
的原理有了清晰的理解!