之前研究了SparkSQL中Join的原理,这次来研究下Flink中的双流Join的原理。
Flink中的Join分为Window Join 和 Interval join两种。前者是将数据缓存在Window中,然后再进行Join,所以感觉本质上其实和SparkSQL中的Join一样,算是个离线数据Join;Interval join不太一样,是一条流中的每个元素和另外一条流某个时间范围区间的所有元素进行Join;详细的分析我们后续马上就会讲到。Flink双流Join详细完整的demo可见参考中的链接,那位博主的Demo写的非常好。
Window Join:
Flink中的Window Join支持Tumbling Window Join、Sliding Window Join和Session Window Join这三种方式。以Tumbling Window Join为例,Join时如下图所示:
其他Join图示可见官网,参考中已给出链接。Window Join具体有两种实现的API:Join 和 coGroup。Join API只能实现Inner Join,如果要实现 Left Join、Right Join 、Full Outer Join等操作,得使用coGroup API,后续可以看到Join底层也是基于coGroup实现的,Join只会输出Key能够匹配上的两侧数据,coGroup如果没有找到匹配的Key,单侧数据也会输出。Join代码示例如下:
stream.join(otherStream) .where(<KeySelector>) .equalTo(<KeySelector>) .window(<WindowAssigner>) .apply(<JoinFunction>)
Join和coGroup操作其实是将两条流中的数据进行Shuffle,将相同key的数据划分到一起,然后调用用户编写的Function对数据进行操作。我们来看下Join的底层实现,可以看到,Join底层最终还是调用coGroup API来实现的:
JoinCoGroupFunction内部会对两条流中Join上的Key的数据进行遍历,使用二重for循环调用用户编写的函数对数据进行处理,然后输出到下游:
coGroup底层的具体实现是给两条流分别打上标记,然后将两条流union成一条流,接一个keyBy将相同Key的数据划分到一起,接着调用CoGroupWindowFunction对数据进行处理:
CoGroupWindowFunction内部会对数据进行分类,讲相同Key,不同流的数据拆分到两个不同的集合里面,然后调用coGroup()方法,这个方法会调用到外部用户自己实现的方法,从而实现Left Join等功能:
Interval Join:
该种Join方式是流种的一条数据和另外一条流种一段时间范围内的数据进行Join(inner ,left outer, right outer , full outer 等皆可),如下图所示:
其实这张图画的不全或者说意思没有解释到位,黄色的整条流种如果来了一条数据,那么就去绿色的那条流中找[ timestamp - lower bound , timestamp + upper bound ] 这段时间范围内去找对应的数据,找到了就进行Join。如果是绿色流中来了一条数据,对应的会去黄色这条流中找 [ timestamp - upper bound , timestamp + lower bound ] 这段时间范围内的数据,找到了就进行Join。
代码示例如下:
stream.keyBy(<KeySelector>)
.intervalJoin(otherStream.keyBy(<KeySelector>))
.between(Time.seconds(-10), Time.seconds(10)) // 上下20s之内的数据
.upperBoundExclusive() // 排除上界临界点那一批数据
.process(new ProcessJoinFunction<>())
底层实现时是将两条流先connect合并成一条,然后调用IntervalJoinOperator方法开始处理数据:
IntervalJoinOperator方法初始化的时候会初始化两个MapState,分别存放左右流中的数据,而Key就是timestamp。迟到的数据会被丢弃,不迟到的数据则会被缓存,缓存好了之后,接下来就是Join的操作了:
左右流的数据到达的时候,会分别调用不同的processElement()方法来处理数据,左流调用的是processElement1(),右流调用的是processElement2(),注意倒数第二和第三个参数,就是左右流来了一条数据之后,怎么去对方的流中找可以对应的元素:
@Override
public void processElement1(StreamRecord<T1> record) throws Exception {
processElement(record, leftBuffer, rightBuffer, lowerBound, upperBound, true);
}
@Override
public void processElement2(StreamRecord<T2> record) throws Exception {
processElement(record, rightBuffer, leftBuffer, -upperBound, -lowerBound, false);
}
processElement()内部具体实现如下:
private <THIS, OTHER> void processElement(
final StreamRecord<THIS> record,
final MapState<Long, List<IntervalJoinOperator.BufferEntry<THIS>>> ourBuffer,
final MapState<Long, List<IntervalJoinOperator.BufferEntry<OTHER>>> otherBuffer,
final long relativeLowerBound,
final long relativeUpperBound,
final boolean isLeft) throws Exception {
final THIS ourValue = record.getValue();
final long ourTimestamp = record.getTimestamp();
if (ourTimestamp == Long.MIN_VALUE) {
throw new FlinkException("Long.MIN_VALUE timestamp: Elements used in " +
"interval stream joins need to have timestamps meaningful timestamps.");
}
// 丢弃迟到的数据
if (isLate(ourTimestamp)) {
return;
}
// 数据加入缓存
addToBuffer(ourBuffer, ourValue, ourTimestamp);
// 从对方流中获取相应时间范围内的数据,然后进行Join
for (Map.Entry<Long, List<BufferEntry<OTHER>>> bucket: otherBuffer.entries()) {
final long timestamp = bucket.getKey();
// 不是对应时间范围内的数据就不进行Join了
if (timestamp < ourTimestamp + relativeLowerBound ||
timestamp > ourTimestamp + relativeUpperBound) {
continue;
}
for (BufferEntry<OTHER> entry: bucket.getValue()) {
if (isLeft) {
// collect()方法中会调用用户自定义的userFunction(),真正进行Join操作
collect((T1) ourValue, (T2) entry.element, ourTimestamp, timestamp);
} else {
collect((T1) entry.element, (T2) ourValue, timestamp, ourTimestamp);
}
}
}
// 时间已致,清理无效的缓存数据。 以上面的图为例,如果水印时间完全超过那片黄色覆盖的范围,那么黄色时间范围内的数据就可以清理掉了
long cleanupTime = (relativeUpperBound > 0L) ? ourTimestamp + relativeUpperBound : ourTimestamp;
if (isLeft) {
internalTimerService.registerEventTimeTimer(CLEANUP_NAMESPACE_LEFT, cleanupTime);
} else {
internalTimerService.registerEventTimeTimer(CLEANUP_NAMESPACE_RIGHT, cleanupTime);
}
}
至此,IntervalJoin就完成了。 哈哈哈,以上理论上的分析,生产环境还没有真正实践过,如果有不对的地方,欢迎各位大佬帮忙指正哈。
参考:
https://ci.apache.org/projects/flink/flink-docs-release-1.13/zh/docs/dev/datastream/operators/joining/(Flink Join 官方文档介绍)
https://zhuanlan.zhihu.com/p/340560908(Flink Join Demo)
(Flink中的allowedLateness)
https://www.jianshu.com/p/11b482394c73(Flink Interval Join说明)