一、Flink的时间语义
流式数据处理,最大的特点是数据上具有时间的属性特征,Flink 根据时间产生的位置不同,将时间分为三种时间语义
Event Time:事件产生的时间,它通常由事件中的时间戳描述
Ingestion Time:事件进入 Flink 的时间
Processing Time:事件被处理时当前系统的时间
在Flink中默认情况下使用是ProcessTime时间语义,如果用户选择使用EventTime或者IngestionTime语义,则需要在创建的StreamExecutionEnvironment中调用setStreamTimeCharacteristic()方法设定系统的时间概念
StreamExecutionEnvironment streamEnv=StreamExecutionEnvironment.getExecutionEnvironment();
//设置使用EventTime
streamEnv.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
//设置使用IngestionTime
streamEnv.setStreamTimeCharacteristic(TimeCharacteristic.IngestionTime);
二、Flink的WaterMark
1、watermark的作用
在使用EventTime处理Stream数据的时候会遇到数据乱序的问题,流处理从Event(事件)产生,流经Source,再到Operator,这中间需要一定的时间。虽然大部分情况下,传输到Operator的数据都是按照事件产生的时间顺序来的,但是也不排除由于网络延迟等原因而导致乱序的产生,特别是使用Kafka的时候,多个分区之间的数据无法保证有序。因此,在进行Window计算的时候,不能无限期地等下去,必须要有个机制来保证在特定的时间后触发Window进行计算,这个特别的机制就是Watermark(水位线)。Watermark主要是用于处理乱序事件的。
2、watermark的原理
在Flink的窗口处理过程中,如果确定全部数据到达,就可以对Window的所有数据做窗口计算操作(如汇总、分组等),如果数据没有全部到达,则继续等待该窗口中的数据全部到达才开始处理。这种情况下就需要用到水位线(WaterMarks)机制,它能够衡量数据处理进度(表达数据到达的完整性),保证事件数据(全部)到达Flink系统,或者在乱序及延迟到达时,也能够像预期一样计算出正确并且连续的结果。当任何Event进入到Flink系统时,会根据当前最大事件时间产生Watermarks时间戳。
watermark的计算:
watermark = 进入Flink的最大的事件时间(mxtEventTime)— 指定的延迟时间(t)
watermark的窗口函数的触发条件:
窗口的结束时间 <= maxEventTime — watermark(当时的warkmark )
3、watermark的使用
(1)本来有序的 Stream 中的 Watermark
如果数据元素的事件时间是有序的,Watermark时间戳会随着数据元素的事件时间按顺序生成,此时水位线的变化和事件时间保持一直(因为既然是有序的时间,就不需要设置延迟了,那么t就是0。所以watermark=maxtime-0=maxtime),也就是理想状态下的水位线。当Watermark时间大于Windows结束时间就会触发对Windows的数据计算,以此类推,下一个Window也是一样
(2)乱序事件中的Watermark
现实情况下数据元素往往并不是按照其产生顺序接入到Flink系统中进行处理,而频繁出现乱序或迟到的情况,这种情况就需要使用Watermarks来应对。比如下图,设置延迟时间t为2,过了窗口时间后2秒内的数据一样会按对应窗口内的数据计算,而不会丢弃
(3)并行数据流中的Watermark
在多并行度的情况下,Watermark 会有一个对齐机制,这个对齐机制会取所有Channel中最小的Watermark,如图1、2、3、4最终task都会取最小并行度watermark最终watermark
4、watermark与allowedLateness结合
基于Event-Time的窗口处理流式数据,虽然提供了Watermark机制,却只能在一定程度上解决了数据乱序的问题。但在某些情况下数据可能延时会非常严重,即使通过Watermark机制也无法等到数据全部进入窗口再进行处理。Flink中默认会将这些迟到的数据做丢弃处理,但是对于我们而言,希望即使数据延迟到达的情况下也能够正常按照流程处理并输出结果,此时就需要使AllowedLateness机制来对迟到的数据进行额外的处理。
使用AllwedLateness设定延迟时间,通过使用sideOutputLateData(OutputTag)来标记迟到数据计算的结果,然后使用getSideOutput(lateOutputTag)从窗口结果中获取lateOutputTag标签对应的数据,之后转成独立的DataStream数据集进行处理,最后将延时数据和结果存储到数据库中,便于后期对延时数据进行分析。
5、综合使用示例
按1分钟粒度,根据skuid统计商品销量排名
下单流日志orderLog.txt
{"orderId":"20201011231245423","skuId":"1226354","priceType":"new","requestTime":"1599931959000"}
{"orderId":"20201011231254678","skuId":"1226322","priceType":"normal","requestTime":"1599931359024"}
{"orderId":"20201011231212768","skuId":"1226324","priceType":"back","requestTime":"1599931359011"}
{"orderId":"20201011231234567","skuId":"1226351","priceType":"normal","requestTime":"1599932029000"}
{"orderId":"20201011231245424","skuId":"1226354","priceType":"new","requestTime":"1599931959000"}
下单流实体如下:
@Data
class OrderLog {
private String orderId;
private String skuId;
private String priceType;
private Long requestTime;
private Long sum = 1L;
}
完整代码如下:
import java.util.Iterator;
import org.apache.flink.api.common.functions.AggregateFunction;
import org.apache.flink.api.common.functions.RichFlatMapFunction;
import org.apache.flink.api.common.typeinfo.TypeInformation;
import org.apache.flink.streaming.api.TimeCharacteristic;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.sink.RichSinkFunction;
import org.apache.flink.streaming.api.functions.timestamps.BoundedOutOfOrdernessTimestampExtractor;
import org.apache.flink.streaming.api.functions.windowing.ProcessWindowFunction;
import org.apache.flink.streaming.api.functions.windowing.WindowFunction;
import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;
import org.apache.flink.util.Collector;
import org.apache.flink.util.OutputTag;
import com.alibaba.fastjson.JSON;
import lombok.Data;
public class TestFlinkTime {
public static final OutputTag<OrderLog> LATE_OUTPUT_TAG = new OutputTag<>("LATE_OUTPUT_TAG", TypeInformation.of(OrderLog.class));
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);//设置事件时间
DataStreamSource<String> textDataSteam = env.readTextFile("C:\\Users\\admin\\Desktop\\orderLog.txt");
SingleOutputStreamOperator<OrderLog> dayPvDataStream = textDataSteam
.flatMap(new OutPutMapFunction())
.assignTimestampsAndWatermarks(new AssignedWaterMarks(Time.seconds(3))) //添加watermark,设置3秒延迟
.keyBy(OrderLog::getSkuId)
.window(TumblingEventTimeWindows.of(Time.minutes(1), Time.seconds(5))) //5秒统计一次最近1分钟商品下单排名
.allowedLateness(Time.minutes(30)) //允许数据迟到30分钟
.sideOutputLateData(TestSideOutputStream.LATE_OUTPUT_TAG) //标记迟到数据
.aggregate(new CountAggregateFunction(), new OutResultWindowFunction());//聚合累加,并输出
dayPvDataStream.addSink(new SideOutPutSinkFunction());//sink持久化操作
dayPvDataStream.getSideOutput(TestSideOutputStream.LATE_OUTPUT_TAG) //通过标签获取延迟数据
.keyBy(OrderLog::getSkuId)
.window(TumblingEventTimeWindows.of(Time.seconds(3))) //对迟到数据3秒计算一次
.process(new OutPutWindowProcessFunction())
.addSink(new SideOutPutSinkFunction2());//对迟到的的数据,增量计算并sink持久化操作
env.execute();
}
}
/**
*聚合累加
*/
class CountAggregateFunction implements AggregateFunction<OrderLog,Long, Long> {
private static final long serialVersionUID = 3409883268443447549L;
@Override
public Long createAccumulator() {
return 0L;
}
@Override
public Long add(OrderLog value, Long accumulator) {
return value.getSum() + accumulator;
}
@Override
public Long getResult(Long accumulator) {
return accumulator;
}
@Override
public Long merge(Long a, Long b) {
return a + b;
}
}
/**
* 输出结果
*/
class OutResultWindowFunction implements WindowFunction<Long,OrderLog,String,TimeWindow> {
private static final long serialVersionUID = -3864495373009800962L;
@Override
public void apply(String key, TimeWindow window, Iterable<Long> input, Collector<OrderLog> out) throws Exception {
Iterator<Long> iterator = input.iterator();
while(iterator.hasNext()) {
OrderLog orderLog = new OrderLog();
orderLog.setSkuId(key);
orderLog.setSum(iterator.next());
orderLog.setRequestTime(window.getEnd());
out.collect(orderLog);
}
}
}
/**
* 水位线,保证按事件时间处理
*/
class AssignedWaterMarks extends BoundedOutOfOrdernessTimestampExtractor<OrderLog> {
private static final long serialVersionUID = 2021421640499388219L;
public AssignedWaterMarks(Time maxOutOfOrderness) {
super(maxOutOfOrderness);
}
@Override
public long extractTimestamp(OrderLog orderLog) {
return orderLog.getRequestTime();
}
}
/**
* map转换输出
*/
class OutPutMapFunction extends RichFlatMapFunction<String, OrderLog> {
private static final long serialVersionUID = -6478853684295335571L;
@Override
public void flatMap(String value, Collector<OrderLog> out) throws Exception {
OrderLog orderLog = JSON.parseObject(value, OrderLog.class);
out.collect(orderLog);
}
}
/**
* 窗口函数
*/
class OutPutWindowProcessFunction extends ProcessWindowFunction <OrderLog, OrderLog, String, TimeWindow> {
private static final long serialVersionUID = -6632888020403733197L;
@Override
public void process(String arg0, ProcessWindowFunction<OrderLog, OrderLog, String, TimeWindow>.Context ctx,
Iterable<OrderLog> it, Collector<OrderLog> collect) throws Exception {
Iterator<OrderLog> iterator = it.iterator();
while (iterator.hasNext()) {
OrderLog orderLog = iterator.next();
collect.collect(orderLog);
}
}
}
/**
* sink函数
*/
class SideOutPutSinkFunction extends RichSinkFunction<OrderLog> {
private static final long serialVersionUID = -6632888020403733197L;
@Override
public void invoke(OrderLog orderLog, Context context) throws Exception {
//做自己的存储计算逻辑,存到redis或者hbase等存储系统
System.out.println(orderLog.getSkuId() +"="+ orderLog.getSum());
}
}
/**
* sink函数
*/
class SideOutPutSinkFunction2 extends RichSinkFunction<OrderLog> {
private static final long serialVersionUID = -6632888020403733197L;
@Override
public void invoke(OrderLog orderLog, Context context) throws Exception {
//做自己的存储计算逻辑,存到redis或者hbase等存储系统
System.out.println(orderLog.getSkuId() +"===="+ orderLog.getSum());
}
}
输出如下:
1226324=1
1226322=1
1226354=2
1226351=1