Flink 合流操作——Union
应用中,我们经常会遇到来源不同的多条流,需要将它们的数据进行联合处理。所以Flink中合流的操作会更加普遍,对应的API也更加丰富。
Union
最简单的合流操作就是直接将多条流合在一起,这种操作称作为流的 “联合”(union) ,如下图所示,进行联合操作的流的数据类型需要保持一致,合并之后的流会包含所有流种的元素,数据类型保持不变,这种合流操作简单除暴。
在代码中,我们只要基于DataStream直接调用.union()方法,传入其他DataStream作为参数,就可以实现流的联合了;得到的依然是一个DataStream:
stream1.union(stream2,stream3...)
注意:union()的参数可以是多个DataStream,所以联合操作可以实现多条流的合并。这里需要考虑一个问题。在事件时间语义下,水位线是时间的进度标志;不同的流中可能水位线的进展快慢完全不同,如果它们合并在一起,水位线又该以哪个为准呢?还以要考虑水位线的本质含义,是“之前的所有数据已经到齐了”;所以对于合流之后的水位线,也是要以最小的那个为准,这样才可以保证所有流都不会再传来之前的数据。换句话说,多流合并时处理的时效性是以最慢的那个流为准的。我们自然可以想到,这与之前介绍的并行任务水位线传递的规则是完全一致的;多条流的合并,某种意义上也可以看作是多个并行任务向同一个下游任务汇合的过程。
参考代码
public class UnionStreamTest { public static void main(String[] args) throws Exception { StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); env.setParallelism(1); SingleOutputStreamOperator<Event> eventStream = env.socketTextStream("hadoop103", 9999) .map(data -> { String[] split = data.split(","); return new Event(split[0].trim(), split[1].trim(), Long.valueOf(split[2])); }) .assignTimestampsAndWatermarks(WatermarkStrategy. <Event>forBoundedOutOfOrderness(Duration.ofSeconds(2)) .withTimestampAssigner(new SerializableTimestampAssigner<Event>() { @Override public long extractTimestamp(Event element, long recordTimestamp) { return element.timestamp * 10000; } })); eventStream.print("eventStream "); SingleOutputStreamOperator<Event> eventStream1 = env.socketTextStream("hadoop103", 8888) .map(data -> { String[] split = data.split(","); return new Event(split[0].trim(), split[1].trim(), Long.valueOf(split[2])); }) .assignTimestampsAndWatermarks(WatermarkStrategy. <Event>forBoundedOutOfOrderness(Duration.ofSeconds(5)) .withTimestampAssigner(new SerializableTimestampAssigner<Event>() { @Override public long extractTimestamp(Event element, long recordTimestamp) { return element.timestamp * 10000; } })); eventStream1.print("eventStream1 "); /** * union 可以合并多个流,数据类型需要一样,合并后水位线 取最小的那个 */ eventStream.union(eventStream1).process(new ProcessFunction<Event, String>() { @Override public void processElement(Event value, Context ctx, Collector<String> out) throws Exception { out.collect("水位线 " + ctx.timerService().currentWatermark()); } }).print(); env.execute(); } }
这里为了更清晰地看到水位线的进展,我们创建了两条流来读取socket文本数据,并从数据中提取时间戳作为生成水位线的依据。用union将两条流合并后,用一个ProcessFunction来进行处理,获取当前的水位线进行输出。我们会发现两条流中每输入一个数据,合并之后的流中都会有数据出现;而水位线只有在两条流中水位线最小值增大的时候,才会真正向前推进。我们可以来分析一下程序的运行:在合流之后的ProcessFunction对应的算子任务中,逻辑时钟的初始状态如下图所示
由于Flink会在流的开始处,插入一个负无穷大(Long.MIN_VALUE)的水位线,所以合流后的ProcessFunction对应的处理任务,会为合并的每条流保存一个“分区水位线”,初始值都是Long.MIN_VALUE;而此时算子任务的水位线是所有分区水位线的最小值,因此也是Long.MIN_VALUE。我们在第一条socket文本流输入数据[Alice, ./home, 1000] 时,水位线不会立即改变,只有到水位线生成周期的时间点(200ms一次)才会推进到1000 -1 = 999毫秒;这与我们在7.3.2小节中对事件时间定时器的测试是一致的。不过即使第一条水位线推进到了999,由于另一条流没有变化,所以合流之后的Process任务水位线仍然是初始值。如下图所示。
如果这时我们在第二条socket文本流输入数据[Alice, ./home, 2000],那么第二条流的水位线会随之推进到2000 –1 = 1999毫秒,Process任务所保存的第二条流分区水位线更新为1999;这样两个分区水位线取最小值,Process任务的水位线也就可以推进到999了。如下图所示。
进而如果我们继续在第一条流中输入数据[Alice, ./home, 3000],Process任务的第一条流分区水位线就会更新为2999,同时将算子任务的时钟推进到1999。状态如下图所示