Flink1.11 intervalJoin watermark生成,状态清理机制源码理解&Demo分析

参考博客 https://cloud.tencent.com/developer/article/1738836 

数据类型为左流 FlinkClick(userid=gk01, click=Pay, ctime=2020-12-14 09:55:00.000) ; 右流为 FlinkPay(userid=gk01, payway=alipy, ptime=2020-12-14 09:58:00.000)

join的这段代码如下

  
    clickOut.keyBy(t->t.getUserid())
                .intervalJoin(payOunt.keyBy(t->t.getUserid()))
                .between(Time.minutes(1),Time.minutes(5))
                .lowerBoundExclusive()    //默认是闭区间,这样就变成了开区间
                .upperBoundExclusive()
                .process(new ProcessJoinFunction<FlinkClick, FlinkPay, String>() {
                    @Override
                    public void processElement(FlinkClick left, FlinkPay right, Context ctx, Collector<String> out) throws Exception {

                        out.collect(StringUtils.join(Arrays.asList(
                                left.getUserid(),
                                left.getClick(),
                                right.getPayway()

                        ),'\t'));
                    }
                }).print().setParallelism(1);

 

一:watermark生成规则:

     watermark的计算为 min(ctime,ptime)-watermark (watermark为左右流定义的乱序时间,我这里设置的0),贴出其中一个流的demo,注意watermark

env
                .addSource(payConsumer).map(new MapFunction<String, FlinkPay>() {
                    @Override
                    public FlinkPay map(String pv) throws Exception {
                        JSONObject clickObject = JSONObject.parseObject(pv);
                        String userid = clickObject.getString("userid");
                        String payway = clickObject.getString("payway");
                        String ptime = clickObject.getString("ptime");
                        FlinkPay payO = new FlinkPay(userid, payway, ptime);
                        return payO;
                    }
                }).assignTimestampsAndWatermarks(
                        WatermarkStrategy.<FlinkPay>forBoundedOutOfOrderness(Duration.ZERO)   //watermark时间
                                .withTimestampAssigner(new SerializableTimestampAssigner<FlinkPay>() {
                                                           @Override
                                                           public long extractTimestamp(FlinkPay element, long recordTimestamp) {
                                                               Date dateP = new Date();
                                                               try {
                                                                   System.out.println(element);
                                                                   dateP = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").parse(element.getPtime());
                                                               } catch (ParseException e) {
                                                                   e.printStackTrace();
                                                               }
//                                                        System.out.println(dateP.getTime());
                                                               return dateP.getTime();
                                                           }
                                                       }

                                ));

二:状态清理机制

贴上几段源码,均在  IntervalJoinOperator 类中

 

private transient MapState<Long, List<BufferEntry<T1>>> leftBuffer;
private transient MapState<Long, List<BufferEntry<T2>>> rightBuffer;

@Override
public void initializeState(StateInitializationContext context) throws Exception {
    super.initializeState(context);
    this.leftBuffer = context.getKeyedStateStore().getMapState(new MapStateDescriptor<>(
        LEFT_BUFFER,
        LongSerializer.INSTANCE,
        new ListSerializer<>(new BufferEntrySerializer<>(leftTypeSerializer))
    ));
    this.rightBuffer = context.getKeyedStateStore().getMapState(new MapStateDescriptor<>(
        RIGHT_BUFFER,
        LongSerializer.INSTANCE,
        new ListSerializer<>(new BufferEntrySerializer<>(rightTypeSerializer))
    ));
}

在IntervalJoinOperator中,会利用两个MapState分别缓存左流和右流的数据。其中,Long表示时间时间戳,List<BufferEntry<T>>表示该时刻到来的数据记录,当左流和右流有数据到达时,会分别调用processElement1()和processElement2()方法,它们都调用了processElement()方法

 

 

@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);
	}


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);

		for (Map.Entry<Long, List<BufferEntry<OTHER>>> bucket: otherBuffer.entries()) {
			final long timestamp  = bucket.getKey();

			if (timestamp < ourTimestamp + relativeLowerBound ||
					timestamp > ourTimestamp + relativeUpperBound) {
				continue;
			}

			for (BufferEntry<OTHER> entry: bucket.getValue()) {
				if (isLeft) {
					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);
		}
	}

代码最后调用TimerService.registerEventTimeTimer(),注册时间戳为timestamp+relativeUpperBound的定时器,该定时器负责在水印超过区间的上界时执行状态的清理逻辑,防止数据堆积。注意左右流的定时器所属的namespace是不同的,具体逻辑位于onEventTime()方法中

	@Override
	public void onEventTime(InternalTimer<K, String> timer) throws Exception {

		long timerTimestamp = timer.getTimestamp();
		String namespace = timer.getNamespace();

		logger.trace("onEventTime @ {}", timerTimestamp);

		switch (namespace) {
			case CLEANUP_NAMESPACE_LEFT: {
				long timestamp = (upperBound <= 0L) ? timerTimestamp : timerTimestamp - upperBound;
				logger.trace("Removing from left buffer @ {}", timestamp);
				leftBuffer.remove(timestamp);
				break;
			}
			case CLEANUP_NAMESPACE_RIGHT: {
				long timestamp = (lowerBound <= 0L) ? timerTimestamp + lowerBound : timerTimestamp;
				logger.trace("Removing from right buffer @ {}", timestamp);
				rightBuffer.remove(timestamp);
				break;
			}
			default:
				throw new RuntimeException("Invalid namespace " + namespace);
		}
	}

先把测试数据及结果贴在这里

id左流数据时间戳(ctime)右流数据时间戳(ptime)左流清理时间右侧清理时间
12020-12-14 01:55:00.0002020-12-14 02:00:00.000 
22020-12-14 01:55:00.000 2020-12-14 01:55:00.000

对这个结果说明一下:

我们在自己的代码里设置了:.between(Time.minutes(1),Time.minutes(5)) 

上述源码中有这一行

long cleanupTime = (relativeUpperBound > 0L) ? ourTimestamp + relativeUpperBound : ourTimestamp;

从这里我们就可以计算左右流的清理时间了:

当左流数据进来时,(lowerBound, upperBound) 为 (1 ,5) ,当右流数据进来时,(lowerBound, upperBound) 为 (-5 ,-1),其实就是   left+1min <  right  <left+5min ,反过来就是 right  -5min <  left <right -1min

2020-12-14 01:55:00.000 的左侧数据进来,upperBound大于0,cleanupTime = 时间戳+5min 即等于2020-12-14 02:00:00.000;这是因为,当右侧流在2020-12-14 02:00:00.000需要查找左侧流的数据时间为 [2020-12-14 01:55:00.000,2020-12-14 01:59:00.000],所以watermark> 2020-12-14 02:00:00.000 时可以清除2020-12-14 01:55:00.000的数据

2020-12-14 01:55:00.000的右侧数据进来,upperBound小于0,clearnupTime = 时间戳,即等于 2020-12-14 01:55:00.000;这是因为,左侧数据流在 2020-12-14 01:55:00.000时,需要查找的右侧流时间戳范围 [2020-12-14 01:56:00.000, 2020-12-14 02:00:00.000],所以当watermark达到2020-12-14 01:55:00.000时 可以清除 2020-12-14 01:55:00.000 的数据

在 https://cloud.tencent.com/developer/article/1417447 这篇博客中,博主说watermark讲到 WaterMark是根据实际最小值减去UpperBound生成,即:Min(左,右)-upperBound,个人觉得不太对,如果有小伙伴对我这篇博客有疑问,欢迎留言,会积极改正!!

 

 

 

 

  • 5
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
Flink中的Interval Join是一种流处理中常用的操作,用于在两个流之间基于时间窗口进行连接。下面是对FlinkInterval Join源码分析: 1. 首先,用户需要指定两个输入流以及连接条件和时间窗口大小等参数来创建一个Interval Join操作。 2. 在Flink中,Interval Join是通过CoProcessFunction来实现的。CoProcessFunction是一个可以处理两个输入流的函数,可以用于实现各种复杂的操作。 3. 在Interval Join的实现中,通过继承RichCoProcessFunction类,重写processElement1和processElement2方法来处理两个输入流的元素。 4. 在processElement1和processElement2方法中,可以访问输入元素以及当前的时间信息,并根据时间窗口进行连接操作。 5. 对于每个输入元素,Interval Join会根据连接条件和时间窗口对两个输入流进行连接。连接的结果可以通过OutputTag发送到侧输出流,或者直接通过Collector发送到主输出流。 6. Interval Join的关键部分是如何处理时间窗口的匹配。在Flink中,可以使用KeyedState来存储和管理窗口状态。KeyedStateFlink提供的一种键值对状态,可以在函数中进行读写操作。 7. 在processElement1和processElement2方法中,可以使用KeyedState来存储和检索窗口状态。可以根据窗口大小和时间戳等信息将元素放入合适的窗口中,并在后续处理中进行匹配和连接操作。 8. 除了processElement1和processElement2方法外,还可以重写其他方法来处理定时器和清理状态等操作。这些方法可以用于管理时间窗口的生命周期和释放资源。 总体而言,Flink中的Interval Join是通过CoProcessFunction和KeyedState来实现的。它可以在流处理中进行灵活的连接操作,并根据时间窗口来控制连接的精度和时效性。通过理解Interval Join源码实现,可以更好地使用和优化这个功能,以满足不同业务场景下的需求。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值