技术原理
-
数据shuffle:双流join场景,flink会根据ON 中的联接key进行partition,确保双流相同key的数据在同一个节点进行处理
-
数据保存:由于左右两边流速不同,join过程中会开辟leftState和rightState,保存数据。
- LeftEvent到来存储到LState,RightEvent到来的时候存储到RState;
- LeftEvent会去RightState进行JOIN,并发出所有JOIN之后的Event到下游;
- RightEvent会去LeftState进行JOIN,并发出所有JOIN之后的Event到下游。
-
inner join: 双流JOIN两边事件都会存储到State里面,直到符合join条件才会输出。
-
left outer join:无论右流是否有符合join的事件,左流都会流入下流节点。当右流没有可以join的事件,右边的事件信息补NULL,直到右流有可以join的事件时,撤回NULL的事件,并下发JOIN完整(带有右边事件列)的事件到下游。
-
right outer join 同理
种类
UnBounded JOIN
1. Regular join
SELECT * FROM Orders
INNER JOIN Product
ON Orders.productId = Product.id
特性:
-
Unbounded JOIN
-
将两边流的数据持久化到state,state的大小随时间 持续增长
-
JOIN结果是一个Retract Stream(即计算结果会被insert/delete),一边流的数据将和另一边流的 所有以前和将来的数据 进行关联。
Bounded JOIN
1. Window Join
面向 KeyedStream 基于 windowTime 的 join,对拥有相同key且位于相同时间窗口的元素进行 join。
根据窗口类型(固定/滑动/会话)实现不同的window join,窗口时间范围左闭右开,例如[5, 10);
stream.join(otherStream)
.where(<KeySelector>)
.equalTo(<KeySelector>)
.window(<WindowAssigner>)
.apply(<JoinFunction>)
join逻辑通过 JoinFunction / FlatJoinFunction 实现。
EventTimeWindow Join WaterMark定义
双流join的EventTimeWindow中,双流都指定watermark,此时join window的watermark取双流中较慢的为准。
2. Interval Join
面向 KeyedStream 基于 eventTime 的 join,对拥有相同key且 事件时间处于 lowerBoundTime 和 upperBoundTime之间的元素进行join。
默认为闭合时间区间,即
orangeElem.ts + lowerBound <= greenElem.ts <= orangeElem.ts + upperBound
也可通过 .lowerBoundExclusive()
/ .upperBoundExclusive
修改时间区间闭合范围。
由于interval join 只支持eventTime,因此使用watermark来驱动事件流的前进。
Watermark的定义
currentWatermark = Min(greenElem.ts, orangeElem.ts) - upperBound
事件时间小于当前watermark的元素将视为过期数据,会被清除。
import org.apache.flink.api.java.functions.KeySelector;
import org.apache.flink.streaming.api.functions.co.ProcessJoinFunction;
import org.apache.flink.streaming.api.windowing.time.Time;
...
DataStream<Integer> orangeStream = ...
DataStream<Integer> greenStream = ...
orangeStream
.keyBy(<KeySelector>)
.intervalJoin(greenStream.keyBy(<KeySelector>))
// 定义 时间区间
.between(Time.milliseconds(-2), Time.milliseconds(1))
.process (new ProcessJoinFunction<Integer, Integer, String(){
@Override
public void processElement(Integer left, Integer right, Context ctx, Collector<String> out) {
out.collect(first + "," + second);
}
});
SQL:
SELECT *
FROM
Orders o,
Shipments s
WHERE o.id = s.orderId AND
o.ordertime BETWEEN s.shiptime - INTERVAL '4' HOUR AND s.shiptime
JOIN LATERAL
以上都是双流驱动的join,JOIN LATERAL 是单流驱动的join,根据左表逐条数据动态和右表进行JOIN
LATERAL和 CROSS APPLY的语义相同
示例:
# CROSS APPLAY
SELECT
c.customerid, c.city, o.orderid
FROM Customers c, CROSS APPLAY(
SELECT
o.orderid, o.customerid
FROM Orders o
WHERE o.customerid = c.customerid
) as o
# LATERAL
SELECT
e.NAME, e.DEPTNO, d.NAME
FROM EMPS e, LATERAL (
SELECT *
FORM DEPTS d
WHERE e.DEPTNO=d.DEPTNO
) as d;
# inner join
SELECT users, tag
FROM Orders, LATERAL TABLE(unnest_udtf(tags)) t AS tag;
# left outer join,on谓词只支持 TRUE
SELECT users, tag
FROM Orders LEFT JOIN LATERAL TABLE(unnest_udtf(tags)) t AS tag ON TRUE
特性:
- 不是一张物理表,而是一个VIEW或者Table-valued Funciton。即可以join一个function
- 单例驱动,以主流(左流)驱动
- 解决FROM Clause里面的subquery是无法引用左边表信息的问题。
Flink 中join lateral
的使用场景
- UDTF(TVF) - User-defined Table Funciton
- Temporal Table
JOIN Temporal Table
Temporal Tables(时态表):历史中某个特定时间点上表内容的视图,以key=更新时间,value=版本快照 的 Map 形式存储。
Processing-time Temporal Joins——处理时间时态关联
只保存当前最新版本的时态表。JOIN 行为始终发生在 基于系统时间 当前最新版本的基础表 上。不可能在历史时间版本的基础表上进行关联。基础表的更新不会影响先前已经发出的关联结果。
Event-time Temporal Joins——事件时间时态关联
保存当前watermark到当前系统时间所有版本的时态表。JOIN 行为发生在 指定事件时间 最新版本的基础表 上。即可以根据事件时间,关联历史时间版本的基础表。
例如,指定事件时间为12:00,即使当前系统时间已经为13:00,join 时只会对截止12:00时最新版本的基础表进行关联。
Demo
-
根据主键/时间属性字段,创建 temporal table function 并注册
调用
createTemporalTableFunction
方法,方法定义如下:def createTemporalTableFunction( timeAttribute: String, primaryKey: String): TemporalTableFunction = { createTemporalTableFunction( ExpressionParser.parseExpression(timeAttribute), ExpressionParser.parseExpression(primaryKey)) }
-
在SQL中使用 Temporal Table 进行join
temporal table function 即一种特殊的 table function,evel函数的参数为时间字段
示例:
import org.apache.flink.table.functions.TemporalTableFunction;
(...)
// Get the stream and table environments.
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
StreamTableEnvironment tEnv = StreamTableEnvironment.create(env);
// Provide a static data set of the rates history table.
List<Tuple2<String, Long>> ratesHistoryData = new ArrayList<>();
ratesHistoryData.add(Tuple2.of("US Dollar", 102L));
ratesHistoryData.add(Tuple2.of("Euro", 114L));
ratesHistoryData.add(Tuple2.of("Yen", 1L));
ratesHistoryData.add(Tuple2.of("Euro", 116L));
ratesHistoryData.add(Tuple2.of("Euro", 119L));
// Create and register an example table using above data set.
// In the real setup, you should replace this with your own table.
DataStream<Tuple2<String, Long>> ratesHistoryStream = env.fromCollection(ratesHistoryData);
Table ratesHistory = tEnv.fromDataStream(ratesHistoryStream, "r_currency, r_rate, r_proctime.proctime");
tEnv.registerTable("RatesHistory", ratesHistory);
// Create and register a temporal table function.
// Define "r_proctime" as the time attribute and "r_currency" as the primary key.
// 创建 TemporalTableFunction
TemporalTableFunction rates = ratesHistory.createTemporalTableFunction("r_proctime", "r_currency");
// 在环境中注册 TemporalTableFunction
tEnv.registerFunction("Rates", rates);
使用:
其中Rates()
是一个已经注册好的 temporalTableFunction
SELECT o.currency, o.amount, r.rate
o.amount * r.rate AS yen_amount
FROM
Orders AS o,
LATERAL TABLE (Rates(o.rowtime)) AS r
WHERE r.currency = o.currency
应用
维表JOIN
静态维表JOIN
异步IO查询+缓存;根据维表大小选择 维表缓存策略:全量缓存/LRU缓存。
动态维表JOIN
监听维表变化,动态更新维表
是否需要版本快照:若需要版本快照,则需要设置事件时间属性特征,利用temporalTable
实现join
比较
- 双流join: 双流驱动,双边每条数据流入都可以进行JOIN计算。
- LATERAL JOIN:stream JOIN Table Function(单流和UDTF的join)。单流驱动,根据stream逐条数据动态生成table view。不具备状态管理功能
- Tamporal Table JOIN:sream JOIN Temporal Table(单流和版本表的join)。基于 LATERAL JOIN 实现,单流驱动。具有历史版本状态管理功能
优化
-
构造PK Source:Source Connector上面无法定义PK,在join的时候会产生历史数据的冗余join数据
- 解决方案:join时只取事件流同一PK的最后一行数据。Blink支持 LAST_VALUE 语句。参考Apache Flink 漫谈系列(07) - 持续查询(Continuous Queries)
-
NULL造成的热点:left outer join时,往往一边的流数据还没有可以join的事件,导致A LEFT JOIN B 会产生大量的 (A, NULL),同时再 left join C 时,由于B中的cCol为null,所以最终数据shuffle会在同一个节点出现大量cCol为null的数据,造成热点。
- 解决方案:JOIN ReOrder 改变join的顺序,逆向join
参考
Apache Flink 漫谈系列(07) - 持续查询(Continuous Queries)
Apache Flink 漫谈系列(09) - JOIN 算子
Apache Flink 漫谈系列(10) - JOIN LATERAL
Apache Flink 漫谈系列(11) - Temporal Table JOIN
Apache Flink 漫谈系列(12) - Time Interval(Time-windowed) JOIN