为了处理事件时间,Flink需要知道事件的时间戳,这意味着流中的每个元素都需要分配其事件时间戳。这通常通过从元素中的某个字段访问/提取时间戳来完成。
时间戳分配与生成水印密切相关,水印告诉系统事件时间的进展。
1.1 在数据源(source)中生成WaterMark
public class WaterMarkStream {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
env.setParallelism(1);
Properties p = new Properties();
p.setProperty("bootstrap.servers", "localhost:9092");
p.setProperty("group.id", "test");
String type = "{\"type\":\"object\",\"properties\":{\"Msg\":{\"type\":\"object\",\"properties\":{\"MemberId\":{\"type\":\"string\"},\"LogActionName__Special__\":{\"type\":\"string\"},\"LogControllerName__Special__\":{\"type\":\"string\"},\"LogTime__Special__\":{\"type\":\"string\"}}}}}";
FlinkKafkaConsumer010 consumer010 = new FlinkKafkaConsumer010<>("app_scene_roominfo", new JsonRowDeserializationSchema(type), p);
consumer010.assignTimestampsAndWatermarks(new AscendingTimestampExtractor() {
@Override
public long extractAscendingTimestamp(Row element) {
// 因为定义的mapping json是属于object的嵌套格式,element对象是最外一层的数据结构,需要自己通过getField取出,再获取里面的元素
Row field = (Row) element.getField(0);
int arity = field.getArity();
String time = (String) field.getField(arity - 1);
Date date = null;
try {
date = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss").parse(time);
} catch (ParseException e) {
e.printStackTrace();
}
return date == null ? 0 : date.getTime();
}
});
DataStreamSource source = env.addSource(consumer010);
source.print();
env.execute("WaterMarkStream");
}
}
1.2 通过时间戳分配器生成水印
public class WatermarkStream {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
Properties p = new Properties();
p.setProperty("bootstrap.servers", "localhost:9092");
p.setProperty("group.id", "test");
FlinkKafkaConsumer010 consumer010 = new FlinkKafkaConsumer010<>("app_scene_roominfo", new SimpleStringSchema(), p);
DataStreamSource ds = env.addSource(consumer010);
SingleOutputStreamOperator operator = ds
.map(new RichMapFunction() {
@Override
public Value map(String value) throws Exception {
return new Gson().fromJson(value, Value.class);
}
})
.assignTimestampsAndWatermarks(new AscendingTimestampExtractor() {
@Override
public long extractAscendingTimestamp(Value element) {
long time = 0;
try {
Date date = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss").parse(element.getMsg().getLogTime());
time = date.getTime();
} catch (ParseException e) {
e.printStackTrace();
}
return time;
}
});
operator.print();
env.execute("WatermarkStream");
}
}
Flink提供了2种水印,一种是周期性水印(AssignerWithPeriodicWatermarks),一种是间断性水印(AssignerWithPeriodicWatermarks)。
2.1 周期性水印(AssignerWithPeriodicWatermarks)
默认周期性水印是200ms触发一次。可以通过env对象进行修改,如:
# 改为1s触发一次水印
env.getConfig().setAutoWatermarkInterval(1000);
周期性水印的2种常用的实现:
AscendingTimestampExtractor:具有递增时间戳的分发者
这种是把Stream想成理想状态,事件是以升序的时间过来,并生成水印。在这种情况下,当前时间戳始终可以充当水印,因为没有更早的时间戳会到达。
如果数据真的有延迟,怎么办,AscendingTimestampExtractor的提供3种策略:
.assignTimestampsAndWatermarks(new AscendingTimestampExtractor() {
@Override
public long extractAscendingTimestamp(Value element) {
long time = 0;
try {
Date date = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss").parse(element.getMsg().getLogTime());
time = date.getTime();
} catch (ParseException e) {
e.printStackTrace();
}
return time;
}
});
如果需要修改策略,可以调用AscendingTimestampExtractor
的withViolationHandler(newAscendingTimestampExtractor.IgnoringHandler())
方法。
BoundedOutOfOrdernessTimestampExtractor:允许固定数量的迟到分配者
这个分配器可以设置最大延迟时间,来挽回一些迟到的数据进入窗口。
.assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor(Time.seconds(30)) {
@Override
public long extractTimestamp(Value element) {
long time = 0;
try {
Date date = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss").parse(element.getMsg().getLogTime());
time = date.getTime();
} catch (ParseException e) {
e.printStackTrace();
}
return time;
}
});
2.2 间断性水印(AssignerWithPunctuatedWatermarks)
public class PunctuatedWatermark {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment sEnv = StreamExecutionEnvironment.getExecutionEnvironment();
sEnv.setParallelism(1);
Properties p = new Properties();
p.setProperty("bootstrap.servers", "localhost:9092");
SingleOutputStreamOperator op = sEnv
.addSource(new FlinkKafkaConsumer010("app_scene_roominfo", new SimpleStringSchema(), p))
.map(new MapFunction() {
@Override
public Value map(String value) throws Exception {
return new Gson().fromJson(value, Value.class);
}
})
.assignTimestampsAndWatermarks(new AssignerWithPunctuatedWatermarks() {
/**
* 只有当返回的水印不为空且其时间戳大于以前发送的水印时,才会发出返回的水印(以保留递增水印的契约)。如果返回空值,或者返回的水印的时间戳小于上次发出的水印的时间戳,则不会生成新的水印。
* @param lastElement 上一条记录
* @param extractedTimestamp extractTimestamp()返回的时间戳
* @return 水印对象
*/
@Override
public Watermark checkAndGetNextWatermark(Value lastElement, long extractedTimestamp) {
// 2.创建水印
return new Watermark(extractedTimestamp);
}
@Override
public long extractTimestamp(Value element, long previousElementTimestamp) {
// 1.生成水印时间戳
Date date = null;
try {
date = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss").parse(element.getMsg().getLogTime());
} catch (ParseException e) {
e.printStackTrace();
}
return date == null ? 0 : date.getTime();
}
});
op.print();
sEnv.execute("PunctuatedWatermark");
}
}
间断性水印会给每个事件都生成一个水印,会导致下游计算开销变大,降低程序性能。一般使用较少。
当使用Apache Kafka作为数据源时,每个Kafka分区可能具有简单的事件时间模式(升序时间戳或有界无序)。但是,当从Kafka消费流时,多个分区通常并行消耗,交错来自分区的事件并破坏每个分区模式(这是Kafka的消费者客户端工作的固有方式)。
在这种情况下,您可以使用Flink的Kafka分区感知水印生成。使用该功能,根据Kafka分区在Kafka使用者内部生成水印,并且每个分区水印的合并方式与在流shuffle上合并水印的方式相同。
例如,如果事件时间戳严格按每个Kafka分区升序,则使用升序时间戳水印生成器生成每分区水印 将产生完美的整体水印。
下图显示了如何使用per-Kafka分区水印生成,以及在这种情况下水印如何通过流数据流传播。