FLINK入门——JOIN整理

10,877 阅读6分钟

技术原理

  • 数据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

  1. 根据主键/时间属性字段,创建 temporal table function 并注册

    调用 createTemporalTableFunction方法,方法定义如下:

    def createTemporalTableFunction( timeAttribute: String, primaryKey: String): TemporalTableFunction = { createTemporalTableFunction( ExpressionParser.parseExpression(timeAttribute), ExpressionParser.parseExpression(primaryKey)) }

  2. 在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数据

  • 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

Flink 1.7 新特性:Temporal Tables和MATCH_RECOGNIZE

Flink SQL 功能解密系列 —— 维表 JOIN 与异步优化