flink-流上的join (1.12.2)

flink-流上的join (1.12.2)

一、简介

本篇主要围绕flink table & sql进行实践。

其中文章中大部分内容官方文档有详细记载。

由于环境和代码编写问题,官方示例并不能完全用于代码实践,所以此篇文章通过kafka+mysql进行简单实践。

环境:

  • kafka 2.4
  • flink 1.12.2
  • mysql57
  • java8

二、时间属性

首先对于实时join来说,第一个要讨论的就是时间属性。

在flink sql中,给出了两种时间属性,一个是系统处理时间,一个是数据本身的自带的时间属性。

对于处理时间来说,大部分针对普通的join,对数据顺序先后没有太大要求。

但是在数据自带的时间戳上,哪条数据是在哪个时间段处理便比较重要。

2.1 处理时间

处理时间是基于机器的本地时间来处理数据,它是最简单的一种时间概念,但是它不能提供确定性。它既不需要从数据里获取时间,也不需要生成 watermark。

bsTableEnv.executeSql("create table kafka_table( " +
                "`name` STRING, " +
                "`id` INT," +
                "user_action_time AS PROCTIME() " +
                ") WITH ( " +
                "'connector.type'='kafka', " +
                "'connector.version'='universal', " +
                "'connector.topic'='test', " +
                "'connector.properties.bootstrap.servers'='localhost:9092', " +
                "'connector.properties.group.id'='flink', " +
                "'connector.startup-mode'='earliest-offset', " +
                "'format.type'='json' " +
                ")");
2.2 时间事件

事件时间允许程序按照数据中包含的时间来处理,这样可以在有乱序或者晚到的数据的情况下产生一致的处理结果。它可以保证从外部存储读取数据后产生可以复现(replayable)的结果。

除此之外,事件时间可以让程序在流式和批式作业中使用同样的语法。在流式程序中的事件时间属性,在批式程序中就是一个正常的时间字段。

为了能够处理乱序的事件,并且区分正常到达和晚到的事件,Flink 需要从事件中获取事件时间并且产生 watermark(watermarks)。

bsTableEnv.executeSql("create table kafka_table( " +
                "`receive_time` BIGINT, " +
                "`id` STRING," +
                "`name` STRING," +
                "`ts` AS TO_TIMESTAMP(FROM_UNIXTIME(`receive_time` / 1000, 'yyyy-MM-dd HH:mm:ss'))," +
                "WATERMARK FOR ts AS ts - INTERVAL '5' SECOND " +
                ") WITH ( " +
                "'connector.type'='kafka', " +
                "'connector.version'='universal', " +
                "'connector.topic'='watermarkjoin', " +
                "'connector.properties.bootstrap.servers'='localhost:9092', " +
                "'connector.properties.group.id'='flink', " +
                "'connector.startup-mode'='earliest-offset', " +
                "'format.type'='json' " +
                ")");

注意

在 Table 编程环境中,基于 SQL 的类型系统与程序指定的数据类型之间需要物理提示。该提示指出了实现预期的数据格式。

例如,Data Source 能够使用类 java.sql.Timestamp 来表达逻辑上的 TIMESTAMP 产生的值,而不是使用缺省的 java.time.LocalDateTime。有了这些信息,运行时就能够将产生的类转换为其内部数据格式。反过来,Data Sink 可以声明它从运行时消费的数据格式。

当使用时间事件时,取值为数据自带的时间,一般为xxxx-xx-xx xx:xx:xx,但是在flink sql中进行转换时:

`receive_time` TIMESTAMP(3)

会出现以下错误(时间戳13位):

Caused by: java.time.format.DateTimeParseException: Text '1618987432830' could not be parsed at index 0

或者(yyyy-MM-dd HH:mm:ss时间格式)

Caused by: java.time.format.DateTimeParseException: Text '2021-04-21 11:11:23' could not be parsed at index 10

所以此处用

`receive_time` BIGINT,
`ts` AS TO_TIMESTAMP(FROM_UNIXTIME(`receive_time` / 1000, 'yyyy-MM-dd HH:mm:ss'))

进行了转换,并进行了查询发现其时间类型为2021-04-22T00:12:30才能进行转换。

Kafka Producer生产者生成消息:

Integer i = random.nextInt(10);
            Timestamp timestamp = new Timestamp(System.currentTimeMillis());
            JSONObject jsonObject = new JSONObject();
            jsonObject.put("id", i.toString());
            jsonObject.put("name", "test" + i);
            jsonObject.put("receive_time", timestamp.getTime());

ProducerRecord<String, String> record = new ProducerRecord<>("watermarkjoin", i.toString(),
                                                             jsonObject.toString());

以30s内滚动窗口进行查询,30s进行一次统计:

TableResult result = bsTableEnv.executeSql("select TUMBLE_START(ts, INTERVAL '30' SECOND), COUNT(DISTINCT id) from kafka_table group by " +
                "TUMBLE(ts, INTERVAL '30' SECOND)");

result.print();

结果为:

±—±------------------------±---------------------+
| op | EXPR$0 | EXPR$1 |
±—±------------------------±---------------------+
| +I | 2021-04-21T14:43:30 | 7 |
| +I | 2021-04-21T14:44 | 10 |
| +I | 2021-04-21T14:44:30 | 10 |
| +I | 2021-04-21T14:45 | 10 |
| +I | 2021-04-21T14:45:30 | 10 |
| +I | 2021-04-21T14:46 | 10 |
| +I | 2021-04-21T14:46:30 | 10 |

三、流上join

JOIN 当前维表

Flink SQL 支持 LEFT JOIN 和 INNER JOIN 的维表关联。语法所示,维表 JOIN 语法与传统的 JOIN 语法并无二异。只是 Products 维表后面需要跟上 FOR SYSTEM_TIME AS OF PROCTIME() 的关键字,其含义是每条到达的数据所关联上的是到达时刻的维表快照,也就是说,当数据到达时,我们会根据数据上的 key 去查询远程数据库,拿到匹配的结果后关联输出。这里的 PROCTIME 即 processing time。使用 JOIN 当前维表功能需要注意的是,如果维表插入了一条数据能匹配上之前左表的数据时,JOIN的结果流,不会发出更新的数据以弥补之前的未匹配。JOIN行为只发生在处理时间(processing time),即使维表中的数据都被删了,之前JOIN流已经发出的关联上的数据也不会被撤回或改变。

SELECT *
FROM Orders AS o
[LEFT] JOIN Products FOR SYSTEM_TIME AS OF PROCTIME() AS p
ON o.productId = p.productId
JOIN 历史维表

有时候想关联上的维度数据,并不是当前时刻的值,而是某个历史时刻的值。比如,产品的价格一直在发生变化,订单流希望补全的是下单时的价格,而不是当前的价格,那就是 JOIN 历史维表。语法上只需要将上文的 PROCTIME() 改成 o.orderTime 即可。含义是关联上的是下单时刻的 Products 维表。

SELECT *
FROM Orders AS o
[LEFT] JOIN Products FOR SYSTEM_TIME AS OF o.orderTime AS p
ON o.productId = p.productId

Flink 在获取维度数据时,会根据左流的时间去查对应时刻的快照数据。因此 JOIN 历史维表需要外部存储支持多版本存储,如 HBase,或者存储的数据中带有多版本信息。

多表JOIN
INSERT INTO sink_print
SELECT assure_orders.order_id,assure_orders.order_name,dim_base_province.name FROM assure_orders
LEFT JOIN dim_base_province FOR SYSTEM_TIME AS OF assure_orders.proctime
ON assure_orders.order_id = dim_base_province.id
INSERT INTO sink_print
 select d.order_id,d.company_name,d.address_name,d.order_name,dim_base_province.name from
 (
 SELECT a.*,b.*,c.*
 FROM assure_orders a
 JOIN
 order_company b
  ON a.order_id = b.company_id
 JOIN
 order_address c
 ON a.order_id = c.address_id
  ) d
 LEFT JOIN
 dim_base_province FOR SYSTEM_TIME AS OF d.proctime
 ON d.order_id = dim_base_province.id

在多表join的时候,如果都是全量join,那必定会出现state的增长,最终导致OOM。

所以一般我们想到的就是设置state的过期时间

bsTableEnv.getConfig.setIdleStateRetentionTime(Time.seconds(30), Time.minutes(6))
维表优化

针对维表关联的情况,为了降低 IO 请求次数,降低维表数据库读压力,从而降低延迟,提高吞吐,有以下几种措施:

  • 当维表数据量不大时,通过全量维表数据缓存在本地,同时 ttl 控制缓存刷新的时候,这可以极大的降低 IO 请求次数,但会要求更多的内存空间。

  • 当维表数据量很大时,通过 async 和 LRU cache 策略,同时 ttl 和 size 来控制缓存数据的失效时间和缓存大小,可以提高吞吐率并降低数据库的读压力。

  • 当维表数据量很大同时主流 qps 很高时,可以开启把维表 join 的 key 作为 hash 的条件,将数据进行分区,即在 calc 节点的分区策略是 hash,这样下游算子的 subtask 的维表数据是独立的,不仅可以提高命中率,也可降低内存使用空间。

来自唯品会的flink实践

weipinghui flink sql

维表关联延迟 join

维表关联中,有很多业务场景,在维表数据新增数据之前,主流数据已经发生 join 操作,会出现关联不上的情况。因此,为了保证数据的正确,将关联不上的数据进行缓存,进行延迟 join。

  • 最简单的做法是,在维表关联的 function 里设置重试次数和重试间隔,这个方法会增大整个流的延迟,但主流 qps 不高的情况下,可以解决问题。

  • 增加延迟 join 的算子,当 join 维表未关联时,先缓存起来,根据设置重试次数和重试间隔从而进行延迟的 join。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值