文章目录
- 六、高阶编程
- 开窗一般在分组之后
- 6.1 Window
- 6.2 时间语义与Watermark
- 6.3 ProcessFunction
- 6.4 状态编程和容错机制
六、高阶编程
开窗一般在分组之后
6.1 Window
6.1.1 窗口概述及类型
6.1.2 窗口使用API
6.1.2.1 ReduceFunctioin
1.窗口的增量聚合函数
2.窗口内,同一组数据,来一条,算一条,窗口关闭时才会输出一次结果
--<输入数据类型,输出数据类型>
new ReduceFunction<Tuple2<String, Integer>>()
socketWS.reduce(
new REduceFunction<Tuple2<String,Integer>>(){
@Override
public Tuple2<String, Integer> reduce(Tuple2<String, Integer> value1, Tuple2<String, Integer> value2) throws Exception {
System.out.println(value1 + "<----------------->" + value2);
return Tuple2.of(value1.f0, value1.f1 + value2.f1);
}
}
)
6.1.2.2 AggregateFuncation --> 灵活
1.窗口内 同一组的数据, 来一条,算一次,最后窗口关闭的时候,输出一次
2.输入的类型、输出的类型、累加器的类型,"可以不一样,更灵活一点"
--<输入的类型,输出的类型,累加器的类型>
new AggregateFunction<Tuple2<String, Integer>, Long, Long>
"重点:"
--该函数还可以传两个参数
/*
* @param aggFunction The aggregate function that is used for incremental aggregation.
用于增量聚合的聚合函数。
* @param windowFunction The window function.
窗口函数
* @return The data stream that is the result of applying the window function to the window.
将窗口功能应用于窗口的结果 是数据流。
* @param <ACC> The type of the AggregateFunction's accumulator
AggregateFunction的累加器的类型
* @param <V> The type of AggregateFunction's result, and the WindowFunction's input
AggregateFunction的结果类型和WindowFunction的输入
* @param <R> The type of the elements in the resulting stream, equal to the
WindowFunction's result type
结果流中元素的类型,等于
WindowFunction的结果类型
*/
@PublicEvolving
public <ACC, V, R> SingleOutputStreamOperator<R> aggregate(
AggregateFunction<T, ACC, V> aggFunction,
ProcessWindowFunction<V, R, K, W> windowFunction)
.aggregate(new AggregateFunction<Tuple2<String, Integer>, Long, Long>()
@Override
createAccumulator //第一条数据调用
add //输入的时候来一条计算一条
getResult //最后只输出一次,窗口关闭时才会输出结果
merge
6.1.2.3 全窗口函数(full window functions)
先将窗口内同一组数先存着,窗口触发(窗口关闭)的时候一次性计算
四个泛型:
--<分组,上下文,窗口内 同一分组的所有数据,采集器>
new ProcessWindowFunction<Tuple2<String, Integer>, Long, String, TimeWindow>
.process(
new ProcessWindowFunction<Tuple2<String, Integer>, Long, String, TimeWindow>() {
/**
* 窗口内 同一分组 的 所有数据
* @param key 分组
* @param context 上下文
* @param elements 窗口内 同一分组 所有数据
* @param out 采集器
* @throws Exception
*/
@Override
public void process(String key, Context context, Iterable<Tuple2<String, Integer>> elements, Collector<Long> out) throws Exception {
System.out.println("process...");
long count = elements.spliterator().estimateSize();
out.collect(count);
}
}
)
6.1.2.4 其它可选API
.trigger() —— 触发器:定义 window 什么时候关闭,触发计算并输出结果
.evitor() —— 移除器: 定义移除某些数据的逻辑
.allowedLateness() —— 允许处理迟到的数据
.sideOutputLateData() —— 将迟到的数据放入侧输出流
.getSideOutput() —— 获取侧输出流
6.2 时间语义与Watermark
6.2.0 与spark的比较
--Spark:
1.spark就是什么时候拿到这个数据就什么时候算,就相当于时间语义是处理时间
2.没有数据来的时候spark会输出空
3.spark的窗口长度必须是批次的整数倍
--Flink:
1.flink的窗口长度可以随便设置
2.没数据来就不会输出空,没数据来就一直不动
3.是一个事件驱动框架 kafka也是一个事件驱动框架
6.2.1 时间语义
①Event Time (事件时间)
事件创建时间,一般数据采集时,前端埋点日志会有一个时间戳,这个时间戳就是事件时间
②Ingestion Time(数据进入Flink的时间)
数据注入时间
③Processing Time(处理时间) Flink框架默认情况下时间语义是处理时间
进入flink后被处理的时候的处理时间
④代码实现:
思路
在设置完并行度之后
--设置执行环境,指定为事件时间语义
读取数据之后
--指定如何从数据里提取事件时间,就是数据里的时间戳
import org.apache.flink.streaming.api.TimeCharacteristic
// 0 执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 1.env指定时间语义
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
//2.指定如何获取数据里的事件时间
.withTimestampAssigner((sensor, recordTs) -> sensor.getTs() * 1000L)
代码
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); env.setParallelism(1);
//TODO 设置执行环境,指定为 事件时间 语义
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
//读取数据
SingleOutputStreamOperator<WaterSensor> socketDS = env
.socketTextStream("localhost", 9999)
.map(new MapFunction<String, WaterSensor>() {
@Override
public WaterSensor map(String value) throws Exception {
String[] datas = value.split(",");
return new WaterSensor(datas[0], Long.valueOf(datas[1]), Integer.valueOf(datas[2]));
}
})
// TODO 2.指定 如何从 数据里 提取 事件时间(注意:单位为 ms)
.assignTimestampsAndWatermarks( WatermarkStrategy
.<WaterSensor>forMonotonousTimestamps()
.withTimestampAssigner(
new SerializableTimestampAssigner<WaterSensor>() {
@Override
public long extractTimestamp(WaterSensor element, long recordTimestamp) {
return element.getTs() * 1000L;
}
}
)
);
//业务代码
socketDS
.keyBy(r -> r.getId())
.timeWindow(Time.seconds(5))
.sum("vc")
.print();
//执行
env.execute();
6.2.2 Window源码解析
- 以滚动窗口为例子
- 滚动的事件时间的窗口
事件来了立马计算这个数据在哪个窗口,计算出来之后立马new一个window,这是一个窗口对象
问题一:窗口是如何划分的?
start = timestamp(时间戳) - (timestamp - offset + windowSize) % windowSize;
这个offset可以省略,它是用来求时差的,flink时间以伦敦时间为准
end = start + windowSize(窗口滚动时间)
问题二:窗口什么时候创建?
属于该窗口的第一条数据来的时候,创建一个Window实例(new TimeWindow)
问题三: 窗口怎么触发计算和关闭?
当窗口的事件的最大时间戳(事件时间) <= 上下文的事件时间的watrmark时候 触发计算和关闭
window.maxTimestamp() <= ctx.getCurrentWatermark()
当有一个东西达到窗口的最大时间时触发,这个东西就是watermark
触发和关闭是两个动作
问题四:为什么是左闭右开
源码:maxTimestamp = end - 1ms
窗口的时间的最大时间戳 = 窗口的结束时间-1ms
所以 :
窗口是左闭右开
问题五:Flink的窗口是什么时候创建的?
首先数据来了,根据数据的时间,算出属于哪个窗口,如果这条数据是这个窗口的第一条数据,就通过new的方式,创建一个Windows对象,然后放到一个单例集合里(.singleton)。这样后续属于这个窗口的其它数据再来的时候就不用再去创建一个对象。
触发计算的时候,这个窗口就关闭了,关闭了这个窗口对象就清空了
6.2.3 watermark
当watermark达到窗口的最大时间时触发窗口的计算
6.2.3.1 概念
**有一个东西衡量时间推进的进展**
1.用来解决时间乱序
(能包括最大乱序是场景,就是进入flink的乱序数据的最大时间差,设置的时间,能够包括所有乱序的数据处理完的时间)
2.用来衡量事件时间的进度
3.包含 特殊的 时间戳 源码有一个属性'timestamp'
4.单调递增的(不减少的)
5.认为在它之前的数据都处理完了
6.触发一些窗口的计算
--当ctx.getCurrentWatermark() >= window.maxTimestamp()时候=触发计算
时间语义为事件时间的前提下;
当watermark的时间大于等于当前窗口的最大事件时间的时候
6.2.3.2 使用WatermarkStrategy
根据不同的数据处理场景watermark会有不同的策略:
① 升序的:
WatermarkStrategy.forMonotonousTimestamps()
watermark = maxTimestamp(最大事件时间) - 1ms
升序代码:
//1.创建执行环境,设置并行度
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
"2.设置执行环境,指定为事件时间语义"
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
//3.读数据
SingleOutputStreamOperator<WaterSensor> socketDS = env
.socketTextStream("localhost", 9999)
.map(new MapFunction<String, WaterSensor>() {
@Override
public WaterSensor map(String value) throws Exception {
String[] datas = value.split(",");
return new WaterSensor(datas[0], Long.valueOf(datas[1]), Integer.valueOf(datas[2]));
}
})
//4.指定如何从数据里提取事件时间(注意:单位为ms)
.assignTimestampsAndWatermarks(
"4.1指定升序场景下watermark如何生成"
WatermarkStrategy.<WaterSensor>forMonotonousTimestamps()
//4.2 指定事件时间
.withTimestampAssigner((sensor,recordTs) -> sensor.getTs()*1000L)
);
② 乱序的:
WatermarkStrategy.forBoundedOutOfOrderness(Duration . ofSeconds ( 2 ) )
watermark = maxTimestamp(最大事件时间) - outOfOrdernessMillis(乱序等待时间) - 1ms
乱序代码:
//1.创建执行环境,设置并行度
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
"2.设置执行环境,指定为事件时间语义"
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
//3.读数据
SingleOutputStreamOperator<WaterSensor> socketDS = env
.socketTextStream("localhost", 9999)
.map(new MapFunction<String, WaterSensor>() {
@Override
public WaterSensor map(String value) throws Exception {
String[] datas = value.split(",");
return new WaterSensor(datas[0], Long.valueOf(datas[1]), Integer.valueOf(datas[2]));
}
})
//4.指定如何从数据里提取事件时间(注意:单位为ms)
.assignTimestampsAndWatermarks(
"4.1指定乱序场景下watermark如何生成"//.ofSeconds(4)这个值是经验值
WatermarkStrategy.<WaterSensor>forBoundedOutOfOrderness(Duration.ofSeconds(4))
//4.2 指定事件时间
.withTimestampAssigner((sensor,recordTs) -> sensor.getTs()*1000L)
);
升序是乱序的程度设置为0了,设置了时间语义就必须要用watermark
6.2.3.3 Watermark的两大生成方式
① periodic :在onPeriodicEmit( )中生成watermark
-- 一个周期执行一次该方法
代码:
public class PeriodicWatermarkGenerator<T> implements WatermarkGenerator<WaterSensor> {
private long maxTimestamp = Long.MIN_VALUE;
/**
* 来一条数据,执行一次这个方法
* @param event
* @param eventTimestamp
* @param output
*/
@Override
public void onEvent(WaterSensor event, long eventTimestamp, WatermarkOutput output) {
System.out.println("on Event ...... ");
maxTimestamp = Math.max(maxTimestamp,eventTimestamp);
}
/**
* 每隔一个周期,执行这个方法
* @param output
*/
@Override
public void onPeriodicEmit(WatermarkOutput output) {
System.out.println("on Periodic ... ");
output.emitWatermark(new Watermark(maxTimestamp));
}
}
//--main
.assignTimestampsAndWatermarks(
WatermarkStrategy.forGenerator(
new WatermarkGeneratorSupplier<WaterSensor>() {
@Override
public WatermarkGenerator<WaterSensor> createWatermarkGenerator(Context context) {
return new PeriodicWatermarkGenerator<WaterSensor>();
}
}
)
);
② punctuated :在onEvent()中生成watermark
-- 一条数据执行一次
代码:
public class PunctuateWatermarkGenerator<T> implements WatermarkGenerator<WaterSensor> {
/**
* 来一条数据,执行一次这个方法
* @param event
* @param eventTimestamp
* @param output
*/
@Override
public void onEvent(WaterSensor event, long eventTimestamp, WatermarkOutput output) {
System.out.println("on Event ... ");
output.emitWatermark(new Watermark(eventTimestamp));
}
/**
* 每隔一个周期,执行这个方法
* @param output
*/
@Override
public void onPeriodicEmit(WatermarkOutput output) {
}
}
//--main
.assignTimestampsAndWatermarks(
WatermarkStrategy.forGenerator(
new WatermarkGeneratorSupplier<WaterSensor>() {
@Override
public WatermarkGenerator<WaterSensor> createWatermarkGenerator(Context context) {
return new PunctuatedWatermarkGenerator<WaterSensor>();
}
}
)
);
6.2.3.4 多并行度
轮循:
两个并行度进了一条数据,"watermark以最小的时间为准"
类比木桶原理
6.2.3.5 一般写在哪
在哪写,就在哪生成,建议设在source
因为里面一般要设置事件时间
以特殊的时间戳的形式,插入到流里面
6.2.3.6 watermark的传递
场景一:少对多
广播传递
场景二:多对少
下游收到多个watermark以小的为准
场景三:多对多
上游每一个分组都广播发送数据到下游,下游每一个都接收了上游的所有watermark,然后每个单独分析取'最小'的最为自己的
6.2.3.7 问题:读文件的时候,因为文件是一种有界流,这种有界流,最后一个无法触发,怎么解决?
-
这里flink会在文件读取结束之后,把watermark设置成long的最大值,为什么?
为了保证所有的窗口都被触发,所有的数据都能够被计算
-
为什么所有窗口都被触发?
因为watermark的周期是200ms生成一次,默认值是long的最小值, 读完文件,还不够200ms,没有窗口触发,结束的时候,watermark被设置为Long的最大值,一下子所有的窗口都触发
6.2.3.8 窗口允许迟到
(1)原理
开窗之后调用 allowedLateness(Time)
1.当 watermark >= 窗口最大时间戳时,
"触发窗口计算,但是不会关窗"
2.当窗口最大时间戳 < watermark < 窗口最大时间戳 + 允许迟到时间
"每来一条迟到的数据(属于本窗口的数据) 都会触发一次计算"
3.当 watermark >= 窗口最大时间戳 + 允许迟到时间,
"会关闭窗口,再有迟到的数据,也不会被计算"
-- 到等待时间结束时触发关窗,来一条数据触发计算
(2)代码:
//设置窗口允许迟到时间
.allowedLateness(Time.seconds(2))
socketDS
.keyBy(r -> r.getId())
.timeWwindow(Time.seconds(5))
.allowedLateness(Time.seconds(2))
.process(
new ProcessWindowFunction<WaterSeneor,String,String,TimeWindow>(){
@Override
public void process(String s,Context context,Integer<WaterSensor> elements,Collector<String> out) throwsException {
out.collect(
"======================================"
+ "\n key=" + s
+ "\n 当前窗口是[" + context.window().getStart() + "," + context.window().getEnd() + ")"
+ "\n 一共有" + element.spliterator().estimateSize() + "条数据"
+ "\n watermark=" + context.currentWatermark()
+ "\n ================================"
);
}
}
)
6.2.3.9 侧输出流
(1)原理
最后的保证
那么对于迟到太久的数据怎么处理呢?
引入一个侧输出流
在从主流里获取侧输出流
--怎么把迟到的数据 跟原来的结果合并
union\connect
1.判断迟到的数据属于哪个窗口
2.取出主流里的对应窗口的结果
3.计算并更新结果
(2)代码:
//TODO 迟到数据处理 - 侧输出流
OutputTag<WaterSensor> outputTag = new OutputTag<WaterSensor> (id:"wuyanzu") {};
//分组后,将侧输出流标签设置侧输出流
.sideOutputLateData(outputTag)
//TODO 从主流里 获取 侧输出流
DataStream<WaterSensor> sideOutput = resultDS.getSideOutout(outputTag);
//创建环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
//设置并行度
env.setParallelism(1);
//设置时间语义
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
//读数据
SingleOutputStreamOperator<WaterSensor> socketDS = env.socketTextStream("localhost", 9999)
//将数据封装样例类
.map(new MapFunction<String, WaterSensor>() {
@Override
public WaterSensor map(String value) throws Exception {
String[] datas = value.split(",");
return new WaterSensor(datas[0],
Long.valueOf(datas[1]),
Integer.valueOf(datas[2]));
}
})
//指定事件时间
.assignTimestampsAndWatermarks(
WatermarkStrategy
.<WaterSensor>forMonotonousTimestamps()
.withTimestampAssigner((sensor, recordTs) -> sensor.getTs() * 1000L)
);
"======================================="
//TODO 迟到数据处理 - 侧输出流
OutputTag<WaterSensor> outputTag = new OutputTag<WaterSensor>("wuyanzu"){
"======================================="
};
SingleOutputStreamOperator<String> resultDS = socketDS
//分组
.keyBy(r -> r.getId())
//开窗
.timeWindow(Time.seconds(5))
//设置数据允许迟到时间
.allowedLateness(Time.seconds(2))
//传一个侧输出流标签,这里不用传入迟到的数据,因为迟到的数据已经固定了,只要提供一个支流给它放就可以了
"======================================="
.sideOutputLateData(outputTag)
"======================================="
//处理
.process(
new ProcessWindowFunction<WaterSensor, String, String, TimeWindow>() {
@Override
public void process(String s, Context context, Iterable<WaterSensor> elements, Collector<String> out) throws Exception {
out.collect("======================================="
+ "\nkey=" + s
+ "\n当前窗口是[" + context.window().getStart() + "," + context.window().getEnd() + ")"
+ "\n一共有" + elements.spliterator().estimateSize() + "条数据"
+ "\nwatermark=" + context.currentWatermark()
+ "\n======================================\n\n");
}
}
);
"======================================="
//TODO 从主流中获取 侧输出流
DataStream<WaterSensor> sideOutput = resultDS.getSideOutput(outputTag);
"======================================="
resultDS.print();
sideOutput.print();
/*
怎么把迟到的数据 跟原来的结果进行合并
union、connect
1.判断迟到的数据属于哪个窗口
2.取出主流里对应窗口的结果
3.计算并更新结果
*/
env.execute();
}
6.2.3.10 总结
//TODO 1.对watermark概念的理解
为了解决乱序的问题
用来表示事件时间的进展
是一个特殊的事件时间 的数据 类里面就有一个时间戳属性
单调递增的,(不减)
用来触发窗口的计算,关窗
认为 在它时间之前的数据都处理过了
//TODO 2.watermark的生成
升序的: watermark = maxTs -1ms
乱序的: watermark = maxTs -乱序等待时间 -1ms
//TODO 3.watermark的生成方式
periodic(周期性) 默认是这种方式,默认周期是200ms
=> WatermarkStrategy.<WaterSensor>forMonotonousTimestamps()
=> 乱序:
WatermarkStrategy .<WaterSensor>forBoundedOutOfOrderness(Duration.ofSeconds(4))
punctuated(打点式、间歇式)
//TODO 4.多并行度下的确定
以最小的为准
//TODO 5.watermark的传递
一般在source指定watermark的生成
watermark作为一个特殊的时间的数据,插入流里,随着流而向下有传递
多对一:选最小
一对多:广播
多对多:结合 上面 两种
//TODO 6.迟到数据的处理
1.窗口允许迟到 - 关窗之前迟到
当watermark >= 窗口最大时间戳 时,触发窗口计算 但是不会关窗
当窗口最大时间戳 < watermark < 窗口最大时间戳 + 允许迟到时间
当watermark >= 窗口最大时间戳 + 允许迟到时间 ,会关闭窗口,再有迟到的数据,也不会被计算
2.侧输出流 - 关窗之后的迟到
开窗之后,调用sideoutputlatedata
参数是一个OutputTag,必须使用匿名内部类,new 的时候指定泛型、后面加 {}
从主流里,调用getsideoutput 获取侧输出流
关联两条流,更新结果
随笔
1、 怎么知道是乱序?怎么知道是迟到的数据?
因为是基于 事件时间
2、 基于时间事件,数据乱序,怎么表示时间的进展?怎么处理?
Watermark
=> 用来 衡量 时间的进展
=> 处理乱序
=> 触发 窗口的 计算和关系
=> 时间是 单调递增的(不减少)
=> 表示(认为)之前的数据都处理完了
=> 是一个 时间戳
3、 怎么知道当前来的数据,属于哪个窗口?也就是,窗口是怎么划分的?
窗口划分:TumblingEventTimeWindows类 assignWindows方法
=》 窗口开始时间:timestamp - (timestamp + windowSize) % windowSize
=》 窗口结束时间:new TimeWindow(start, start + size) =》 start + size
=》 窗口是左闭右开: 属于窗口的最大时间戳为 maxTimestamp = end - 1
窗口触发条件:window.maxTimestamp() <= ctx.getCurrentWatermark()
=》 由watermark触发窗口的计算,当 watermark 大于等于 窗口数据的最大时间
4、 watermark设定了等待的时间,如果超过了等待的时间,还有数据没到齐,怎么办?
窗口允许迟到
5、 如果窗口设置了延迟时间,但是到了真正关窗的时间,后面还有属于这个窗口的数据来,怎么办?
放到 侧输出流
6、 在多并行度的时候,怎么确定 watermark 的取值?
以最小的为准
-----------------------------------------------------------------------------------------------------
定时器源码分析
=> 注册 eventTimeTimersQueue.add(new TimerHeapInternalTimer<>(time, (K) keyContext.getCurrentKey(), namespace));
=》 为了避免重复注册、重复创建对象,注册定时器的时候,判断一下是否已经注册过了
=> 触发 timer.getTimestamp() <= time ==========> 定时的时间 <= watermark
6.2.4 Flink对乱序、迟到数据的处理
6.2.4.1 watermark => 乱序程度 (等待时间)
//设置执行环境,指定为事件时间语义
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
//4.指定如何从数据里提取事件时间(注意:单位为ms)
.assignTimestampsAndWatermarks(
"指定升序场景下watermark如何生成"
WatermarkStrategy.<WaterSensor>forMonotonousTimestamps()
"指定乱序场景下watermark如何生成"//.ofSeconds(4)这个值是经验值
WatermarkStrategy.<WaterSensor>forBoundedOutOfOrderness(Duration.ofSeconds(4))
//4.2 指定事件时间
.withTimestampAssigner((sensor,recordTs) -> sensor.getTs()*1000L)
6.2.4.2 窗口允许迟到的时间
//读取数据
//分组
//设置窗口
.timeWindow(Time.seconds(5))
//设置窗口允许迟到时间
.allowedLateness(Time.seconds(2))
6.2.4.3 侧输出流
//TODO 迟到数据处理 - 侧输出流
OutputTag<WaterSensor> outputTag = new OutputTag<WaterSensor> (id:"wuyanzu"){
};
.sideOutputLateData(outputTag)
//TODO 从主流里 获取 侧输出流
DataStream<WaterSensor> sideOutput = resultDS.getSideOutout(outputTag);
6.2.5 知识点代码总结
//创建环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
//设置并行度
env.setParallelism(1);
//设置时间语义
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
//调整watermark生成周期
env.getConfig().setAutoWatermarkInterval(2000L);
//读取数据
SingleOutputStreamOperator<WaterSensor> socketDS = env
.socketTextStream("localhost", 9999)
//将数据封装成样例类
.map(new MapFunction<String, WaterSensor>() {
@Override
public WaterSensor map(String value) throws Exception {
String[] datas = value.split(",");
return new WaterSensor(datas[0], Long.valueOf(datas[1]), Integer.valueOf(datas[2]));
}
})
//指定如何从数据里提取事件时间,即指定watermark如何生成【分乱序,升序场景】
.assignTimestampsAndWatermarks(
//升序
WatermarkStrategy.WaterSensor>forMonotonousTimestamps()
//降序
WatermarkStrategy.<WaterSensor>forBoundedOutOfOrderness(Duration.ofSeconds(4))
//指定提取哪个时间戳作为事件时间
.withTimestampAssigner((sensor, recordTs) -> sensor.getTs() * 1000L)
);
socketDS
//迟到数据处理 - 侧输出流
OutputTag<WaterSensor> outputTag = new OutputTag<WaterSensor>("wuyanzu") {
};
//分组
.keyBy(r -> r.getId())
//开窗
.timeWindow(Time.seconds(5))
//允许迟到时间
.allowedLateness(Time.seconds(2))
//将迟到数据加入主流
//注意:【如果处理函数是ProcessWindowFunction时,要加入,是ProcessFunction时,可在上下文环境中获取】
.sideOutputLateData(outputTag)
//处理
.process(
new ...ProcessFunction<>(){
"后续内容"
//获取侧输出流 上下文.output(测输出流属性名)
ctx.output(侧输出流 属性名,要输出的值)
}
)
// TODO 从 主流里 获取 侧输出流
.getSideOutput(outputTag);
//执行环境
.execute()
6.3 ProcessFunction
引言
既然flink处理的数据可以有时间语义,我们需要获取这些时间戳,就需要一些方法:
之前的转换算子和窗口函数是无法访问时间的时间戳信息的
基于此,Flink提供了8个ProcessFunction
ProcessFunction
KeyedProcessFunction
CoProcessFunction
ProcessJoinFunction
BroadcastProcessFunction
KeyedBroadcastProcessFunction
ProcessWindowFunction
ProcessAllWindowFunction
这里我们以KeyedProcessFunction为例:
6.3.1 KeyedProcessFunction
用来操作KeyedStream
**所有的Process Function都继承自RichFunction接口,**
**所以都有open()、close()和getRuntimeContext()等方法。**
而KeyedProcessFunction[KEY(分组), IN(输入类型), OUT(输出类型)]还额外提供了两个方法:
① processElement (v: IN, ctx: Context, out: Collector[OUT])
1.流中的每一个元素都会调用这个方法,调用结果将会放在Collector数据类型中输出
2.Context可以访问
元素的时间戳,
元素的key,
以及TimerService时间服务。
3.Context还可以将结果输出到别的流(side outputs)。
"Context"
② onTimer (timestamp: Long, ctx: OnTimerContext, out: Collector[OUT])
1.是一个"回调函数"。
2.当之前注册的定时器触发时调用。
3.参数timestamp为定时器所设定的触发的时间戳。
4.Collector为输出结果的集合。
OnTimerContext和processElement的Context参数一样,提供了上下文的一些信息,
例如定时器触发的时间信息(事件时间或者处理时间)。
"OnTimerContext"
对比以上两个方法的上下文参数
6.3.2 TimerService和定时器 (Timers)
(1)TimerService对象的一些方法
Context和OnTimerContext所持有的TimerService对象拥有以下方法:
-
currentProcessingTime(): Long
返回当前处理时间
-
currentWatermark(): Long
返回当前watermark的时间戳
-
registerProcessingTimeTimer(timestamp: Long): Unit
会注册当前key的processing time的定时器。当processing time到达定时时间时,触发timer。
-
registerEventTimeTimer(timestamp: Long): Unit
会注册当前key的event time 定时器。当水位线大于等于定时器注册的时间时,触发定时器执行回调函数。
-
deleteProcessingTimeTimer(timestamp: Long): Unit
删除之前注册处理时间定时器。如果没有这个时间戳的定时器,则不执行。
-
deleteEventTimeTimer(timestamp: Long): Unit
删除之前注册的事件时间定时器,如果没有此时间戳的定时器,则不执行。
-
output ( outputTag ,value ) 获取侧输出流 ( 这里需要传入侧输出流的标签名,还要将数据放进去)
-
timestamp
事件时间语义时,获取的时间戳是数据的事件时间
处理时间语义时,没有watermark和没指定数据里哪个时间戳,获取的是一个null值
依旧处理时间语义时,但我指定了从数据里面获取一个时间戳,获取的是事件时间
总结:
timestamp这个方法,不管是什么时间语义,只要指定了从数据中获取一个时间戳,就获取的是事件时间
-
getCurrentKey 获取当前的Key
(2)定时器原理
当定时器timer"触发时",会执行"回调函数onTimer()"。注意定时器timer只能在keyed streams上面使用。
(3)定时器实现代码
① 注册定时器
ctx.timerService().registerEventTimeTimer(ctx.timestamp() + 5000L);
new KeyedProcessFunction<String, WaterSensor, String>() {
@Override
public void processElement(WaterSensor value, Context ctx, Collector<String> out) throws Exception {
// TODO 1.注册定时器
/*
long time = System.currentTimeMillis() + 5000;
ctx.timerService().registerProcessingTimeTimer(time);
out.collect("注册了定时器,ts=" + new Timestamp(time));
*/
ctx.timerService().registerEventTimeTimer(ctx.timestamp() + 5000L);
}
② 源码分析
定时器 注册 and 触发(事件时间的定时器)
=> 1、注册 eventTimeTimersQueue.add(new TimerHeapInternalTimer<>(time, (K) keyContext.getCurrentKey(), namespace));
=》 队列添加的时候会进行去重,如果同一组,重复注册了相同的时间的定时器,只会添加一个到队列里
=》 每个分组是个隔离的,即是注册的时间是相同的,但是每个分组照样有一个
=> 2、什么时候触发?
当定时的时间 <= watermark,"注意" //因为watermark会减1秒,所以一般要往后延一条数据
timer.getTimestamp() <= time 时触发。
③ 触发定时器
定义 onTimer 方法 - 定时器触发 之后 的处理逻辑
/*
TODO 2.定义 onTimer 方法 - 定时器触发 之后 的处理逻辑
@param timestamp 定时器触发的时间,也就是 定的那个时间
@param ctx 上下文
@param out 采集器
@throws Exception
*/
@Override
public void onTimer(long timestamp, OnTimerContext ctx, Collector<String> out) throws Exception {
// System.out.println("定时器触发了, onTimer ts = " + new Timestamp(timestamp));
System.out.println("定时器触发了, onTimer ts = " + timestamp);
}
}
(4)小练习
监控水位传感器的水位值,如果水位值在五分钟之内(processing time)连续上升,则报警。
//按id分组
.keyBy(r -> r.getId())
//处理
.process(
new KeyProcessFunction<String,WaterSensor,String>(){
private long timerTs = 0L;
private int lastVC = 0;
//流中每一个元素都会调用该方法
@Override
public void processElement(WaterSensor value,Context ctx,Collextor<String> out) throws Exception {
if(timerTs == 0){
//第一条数据来,注册一个 5s 后的定时器
ctx.timerService().registerEventTimeTimer(ctx.timestamp() + 5000L);
timerTs = ctx.timestamp() + 5000L;
}
//判断是否上升
if (value.getVc() < lastVC) {
//如果出现下降 ,删除 原来的定时器
ctx.timerService().deleteEventTimeTimer(timeTs);
timerTs = 0L; //或者直接重新注册,更新timerTs的值
}
//不管上升还是下降,都要把当前水位值 更新到 变量 ,为了 下一条数据进行判断
lastVC = vlaue.getVc();
}
/*
定时器触发,触发告警
@param timestamp 定时器触发的时间,也就是 定的那个时间
@param ctx 上下文
@param out 采集器
@throws Exception
*/
@Override
public void onTimer(long timestamp,OnTimerContext ctx , Collector<String> out) throws Exception {
out.collect("warning :监测到5s内 水位连续上涨 !!!")
//重置保存的时间,后面才可以接着告警
timeTs = 0L;
}
}
)
6.3.3 侧输出流(SideOutput)
(1)小练习
采集监控传感器水位值,将水位值高于5cm的值输出到side output。
//执行环境
//并行度
//时间语义
//读数据
//封装样例类
//指定watermark如何生成
//设置侧输出流
//new一个匿名内部类
OutputTag<String> alarmTag = new OutputTag<String>("alarm") {
};
//处理
//分组
//处理
SingleOutputStreamOperator<WaterSensor> resultDS = socketDS
.process(
new KeyedProcessFunction<String,WaterSensor,WaterSensor>(){
/**
* 当水位高于 5的时候,告警输出到 侧输出流
* @param value
* @param ctx
* @param out
* @throws Exception
*/
@Override
public void processElement(WaterSensor value , Context ctx , Collector<WaterSensor> out) throw Exception {
//判断水位值
if(value.getVc() > 5) {
//告警输出到侧面输出流
ctx.output(alarmTag,value:"监控到水位值超过 5cm !!!")
}
}
}
);
//输出主流结果
resultDS.print("result");
//从主流中获取侧输出流结果
resultDS.getSideOutput(alarmTag).print("alarm");
//执行环境
env.execute();
6.3.4 CoProcessFunction
--见核心编程案例 双流连接后的处理
6.4 状态编程和容错机制
6.4.1 概述
流式计算分为无状态和有状态两种情况。
- 无状态的计算观察每个独立事件
- 有状态的计算则会基于多个事件输出结果
- 流与流之间的所有关联操作,以及流与静态或动态表之间的关联操作,都是有状态的计算
6.4.2 有状态的算子
Flink内置的很多算子,数据源source,数据存储sink都是有状态的,
流中的数据都是buffer records,会保存一定的元素或者元数据。
例如:
- ProcessWindowFunction会缓存输入流的数据,
- ProcessFunction会保存设置的定时器信息
6.4.2.1 算子状态
(1)**原理 **
算子状态的作用范围限定为算子任务。这意味着由同一并行任务所处理的所有数据都可以访问到相同的状态,状态对于同一任务而言是共享的。算子状态不能由相同或不同算子的另一个任务访问
1.一个算子的子任务(或者说实例、subtask)体现一种状态
2.状态对于同一任务而言是共享的
3.算子状态不能由相同或不同算子的另一个任务访问
checkpoint两个方法一个做快照的一个做恢复的
(2)三种基本数据结构
-
①列表状态( List state)
将状态表示为一组数据列表
-
②联合列表状态(Union list state)
也将状态表示为数据的列表,它与常规列表状态的区别在于: 在发生故障时,或者从保存点启动应用程序时如何恢复
-
以上两者区别:
1.常规列表状态是均匀分配, 2.联合列表状态是将所有State合并为全量State再分发给每个实例
-
③广播状态(Broadcast state)
如果一个算子有多项任务,而它的每项任务又都相同,那么这种特殊情况最适合应用广播状态。
广播状态代码:
//1.创建执行环境 StreanExecutionEnvironment env = StreamExecutionEnvironment.getExecutorEnvironment(); env.setParallelism(1); env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime); //一条流A DataStreamSource<String> inputDS = env.socketTextStream("localhost",9999); //另一条流B DataStreamSource<String> controlDS = env.socketTextStream("localhost",8888); //TODO 应用场景 (1.5 版才有的) //1.动态配置更新 //2.类似开关的功能,切换处理逻辑 //TODO 限制 //1.要广播出去的流B,最好是 数据量小、更新不频繁 // TODO 1.将 其中一条流 B 广播出去 MapStateDescriptor<String,String> broadcastMapStateDesc = new MapStateDescriptor<>("broadcast-state",String.class,String.class); BroadcastStream<String> controlBS = controlDS.broadcast(broadcaseMapStateDesc) // TODO 2.连接 流 A 和 广播B BroadcastConnectedStream<String,String> inputControlBCS = inputDS.connect(controlBS); // TODO 3.使用 Process inputControlBCS .process( new BroadcastProcessFunction<String,String,String>(){ /* 处理流A的数据 */ @Override public void processElement(String value,ReadOnlyContext ctx,Collector<String> out) throw Exception { //主流A 获取广播状态,但是 只读的,不能修改,要在流B去更新 ReadOnlyBroadcastState<String,String> broadcastState = ctx.getBroadcastState(broadcastMapStateDesc); if("1".equals(broadcastState.get("switch"))){ out.collect("打开。。。"); }else { out.collect("不打开。。。"); } } /* 处理广播流B 的数据 */ @Override public void processBroadcastElement(String value,Context ctx,Collector<String> out) throws Exception{ BroadcastState<String,String> broadcastState = ctx.getBroadcastState(broadcastMapStateDesc); //把数据写入广播状态 broadcastState.put("switch",value); } } ) .print(); env.execute(); } }
6.4.2.2 键控状态
(1)**原理 **
keyBy之后的相关算子:
- 同一子任务的每个组都有自己的状态,这些状态是隔离的
- 是哪个组就用的哪个组的状态
- 状态都在process中定义
键控状态是根据输入数据流中定义的键来维护键(key)和访问的,
Flink为每一个键值维护一个状态实例,
并将具有相同键的所有数据,都分区到同一个算子任务中,
这个任务会维护和处理这个key对应的状态,
当任务处理一条数据时,它会自动将状态的访问范围限定为当前数据的key。
因此,具有相同key的所有数据都会访问相同的状态。
KeyedState很类似于一个分布式的key-vlaue map数据结构,只能用于KeyedStream (keyby算子处理之后的流)
(2)支持的类型代码实现 :
定义状态
在open里通过运行时上下文创建状态
在processElement里使用状态
-
①值的类型
get操作: ValueState.value() set操作: ValueState.update(value: T) --定义状态 ValueState<Integer> valueState ; --在open里通过运行时上下文创建状态 valueState = getRuntimeContext().getState(new ValueStateDescriptor<Integer>("value-state",Integer.class)); --在processElement里使用状态 valueState.value(); vaueState.update(); valueState.clear();
代码实现
.keyBy(r -> r.getId) .process( new KeyedProcessFunction<String,WaterSensor,WaterSensor>(){ //TODO 1.定义状态 //注意: 不能在这里使用 RuntimeContext // => The runtime context has not been initialized // 【 运行时上下文尚未初始化。】 ValueState<Integer> valueState ; @Override public void open(Configuration parameters) throws Exception { //TODO 2.在open 里面创建状态 --> 通过运行时上下文 valueState = getRuntimeContext().getState(new ValueStateDescriptor<Integer>("value-state",Integer.class)); } @Override public void processElement(WaterSensor value,Context ctx,Collector<WaterSensor> out) throws Exception { // TODO 3.使用状态 //获取状态的值 valueState.value(); //注意要.value //更新状态的值 vaueState.update(); //清空当前key 对应的状态 valueState.clear(); } } )
-
②列表类型
--保存一个列表,列表里的元素的数据类型为T。基本操作如下: ListState.add(value: T) ListState.addAll(values: java.util.List[T]) ListState.get()返回Iterable[T] ListState.update(values: java.util.List[T] --定义状态 ListState<Integer> listState ; --在open里通过运行时上下文创建状态 --通过RuntimeContext注册StateDescriptor。 --StateDescriptor以状态state的名字和存储的数据类型为参数。 listState = getRuntimeContext().getListState(new ListStateDescriptor<String>( "list-state", --state的名字 String.class --存储的数据类型 )); --在processElement里使用状态 listState.get(); listState.add(); listState.addAll(); listState.update(); listState.clear();
代码实现
.keyBy(r -> r.getId) .process( new KeyedProcessFunction<String,WaterSensor,WaterSensor>(){ //TODO 1.定义状态 //注意: 不能在这里使用 RuntimeContext // => The runtime context has not been initialized // 【 运行时上下文尚未初始化。】 ValueState<Integer> valueState ; @Override public void open(Configuration parameters) throws Exception { //TODO 2.在open 里面创建状态 --> 通过运行时上下文 listState = getRuntimeContext().getListState(new ListStateDescriptor<String>("list-state", String.class)); } @Override public void processElement(WaterSensor value,Context ctx,Collector<WaterSensor> out) throws Exception { // TODO 3.使用状态 // 获取 List状态的值 listState.get(); // 添加 单个值 listState.add(); // 添加 整个 List listState.addAll(); // 更新 整个 List listState.update(); // 清空 当前key 对应的 状态 listState.clear(); } } )
-
③K-V类型
--保存Key-Value对。 MapState.get(key: K) MapState.put(key: K, value: V) MapState.contains(key: K) MapState.remove(key: K) --定义状态 ValueState<Integer> valueState ; --在open里通过运行时上下文创建状态 mapState = getRuntimeContext().getMapState(new MapStateDescriptor<String, Long>("map-state", String.class, Long.class)); --在processElement里使用状态 mapState.get(); mapState.clear();
代码实现
.keyBy(r -> r.getId) .process( new KeyedProcessFunction<String,WaterSensor,WaterSensor>(){ //TODO 1.定义状态 //注意: 不能在这里使用 RuntimeContext //=> The runtime context has not been initialized // 【 运行时上下文尚未初始化。】 ValueState<Integer> valueState ; @Override public void open(Configuration parameters) throws Exception { //TODO 2.在open 里面创建状态 --> 通过运行时上下文 mapState = getRuntimeContext().getMapState(new MapStateDescriptor<String, Long>("map-state", String.class, Long.class)); } @Override public void processElement(WaterSensor value,Context ctx,Collector<WaterSensor> out) throws Exception { // TODO 3.使用状态 // 根据 key 获取 value mapState.get(); // 清空 当前key 对应的 状态 mapState.clear(); } } )
-
④ReducingState
-
⑤AggregatingState
注意的点:
1.State.clear()是清空操作 2.通过RuntimeContext注册StateDescriptor。 3.StateDescriptor以状态state的名字和存储的数据类型为参数。 4.在open()方法中创建state变量。
(3)小练习:
如果连续两次水位差超过40cm,发生预警信息。
.process(
new KeyedProcessFunction<String,WaterSensor,String>(){
//1.定义任务
ValueState<Integer> valueState;
/*
如果使用变量来接收状态
Integer lastvc = 0;
*/
@Override
public void open(Configuration parameters) throws Exception {
//2.在open 里面创建状态
valueState = getRuntimeContext().getState(new ValueStateDescriptor<Integer>(name:"value-state",Integer.class))
}
@Override
public void processElement(WaterSensor value, Context ctx,Collector<String> out) throws Exception {
//使用键控状态 保存上一次的水位值 => 分组之间是隔离
out.collect("key=" + ctx.getCurrentKey() + ",上一次的水位值=" + valueState.value)
//把 水位值 更新到 状态里
valueState.update(value.getVc());
/*
// TODO 如果使用的是变量 来 保存上一次水位值
out.collect("key="+ctx.getCurrentKey()+",上一次的水位值="+lastvc);
lastvc = value.getVc();
*/
}
}
)
6.4.2.3 状态后端
// 要使用状态后端,一定要开启checkpoint
env.enableCheckpointing(111111L);
(1) 概述——状态的备份
主要负责两件事:
- 本地状态管理
- 将检查点(checkpoint)状态写入远程存储
(2) 状态后端分类
① MemoryStateBackend
"内存级"的状态后端,
会将键控状态作为内存中的对象进行管理,将它们存储在TaskManager的JVM堆上;
1. "键控状态" -> "存在TaskManager的JVM堆上"
而将checkpoint存储在JobManager的内存中
2. "checkpoint" -> "JobManager的内存中"
何时使用MemoryStateBackend?
建议使用MemoryStateBackend进行本地开发或调试,因为它的状态有限
MemoryStateBackend最适合具有小状态大小的用例和有状态流处理应用程序,
例如:
仅包含一次记录功能(Map,FlatMap或Filter)的作业或使用Kafkaconsumer。
② FsStateBackend
将checkpoint存到远程的持久化文件系统上,
1."checkpoint" -> "持久化到文件系统上"
而对于本地状态,跟MemoryStateBackend一样,也会存在TaskManager的JVM堆上。
2."本地状态" -> "存在TaskManager的JVM堆上 "
何时使用FsStateBackend?
FsStateBackend最适合处理大状态,长窗口或大键值状态的Flink有状态流处理作业
FsStateBackend最适合每个高可用性设置
③ RocksDBStateBackend
flink内置的
将所有状态序列化后,存入本地的RocksDB中存储
1."本地状态" -> "存入本地的RocksDB中 "
"注意:" RocksDB的支持并不直接包含在flink中,需要引入依赖:
2."checkpoint" -> "持久化到文件系统上"
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-statebackend-rocksdb_${scala.binary.version}</artifactId>
<version>${flink.version}</version>
</dependency>
何时使用RockDBStateBackend?
唯一可用于支持有状态流处理应用程序的增量检查点的状态后端
RocksDBStateBackend最适合处理大状态,长窗口或大键值状态的Flink有状态流处理作业
RocksDBCtateBackend最适合每个高可用性设置
RockDBStateBackend是目前唯一可用于支持有状态流处理应用程序的"增量检查点的状态后端【只保存变化的机制】"
(3) 代码实现
选择一个状态后端(state backend)
设置状态后端WieRocksDBStateBackend:
StateBackend rocksDBStateBackend = new RocksDBStateBackend("hdfs://xxxx:xx/flink/statebackend/rocksdb/");
env.setStateBackend(rocksDBStateBackend);
env.enableCheckpointing(300L);
// 1.创建执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
"==================================================================================================="
// TODO 状态后端的使用
// 1. MemoryStateBackend的使用
StateBackend memoryStateBackend = new MemoryStateBackend();
env.setStateBackend(memoryStateBackend);
// 2. FsStateBackend的使用
StateBackend fsStateBackend = new FsStateBackend("hdfs://host:port/xxx/xxx/xxx");
env.setStateBackend(fsStateBackend);
// 3. RocksDBStateBackend的使用 (需要 导入 依赖)
StateBackend rocksDBStateBackend = new RocksDBStateBackend("hdfs://{fs.defaultFS}/xxx/xxx/xxx");
env.setStateBackend(rocksDBStateBackend);
"==================================================================================================="
// TODO 要使用状态后端,一定要开启checkpoint
env.enableCheckpointing(111111L);
// 一条流A
DataStreamSource<String> inputDS = env.socketTextStream("localhost", 9999);
// 另一条流 B
DataStreamSource<String> controlDS = env.socketTextStream("localhost", 8888);
// TODO 应用场景(1.5版本才有的)
// 1.动态配置更新
// 2.类似开关的功能, 切换处理逻辑
// TODO 限制
// 1. 要广播出去的流B,最好是 数据量小、 更新不频繁
// TODO 1.将 其中一条流 B 广播出去
MapStateDescriptor<String, String> broadcastMapStateDesc = new MapStateDescriptor<>("broadcast-state", String.class, String.class);
BroadcastStream<String> controlBS = controlDS.broadcast(broadcastMapStateDesc);
// TODO 2.连接 流 A 和 广播B
BroadcastConnectedStream<String, String> inputControlBCS = inputDS.connect(controlBS);
// TODO 3.使用 Process
inputControlBCS
.process(
new BroadcastProcessFunction<String, String, String>() {
/**
* 处理 流 A的数据
* @param value
* @param ctx
* @param out
* @throws Exception
*/
@Override
public void processElement(String value, ReadOnlyContext ctx, Collector<String> out) throws Exception {
// 主流 A 获取 广播状态,但是 只读的,不能修改,要在 流 B去更新
ReadOnlyBroadcastState<String, String> broadcastState = ctx.getBroadcastState(broadcastMapStateDesc);
if ("1".equals(broadcastState.get("switch"))) {
out.collect("打开....");
} else {
out.collect("不打开...");
}
}
/**
* 处理 广播流B 的数据
* @param value
* @param ctx
* @param out
* @throws Exception
*/
@Override
public void processBroadcastElement(String value, Context ctx, Collector<String> out) throws Exception {
BroadcastState<String, String> broadcastState = ctx.getBroadcastState(broadcastMapStateDesc);
// 把 数据 写入 广播状态
broadcastState.put("switch", value);
}
}
)
.print();
env.execute();
6.4.3 代码总结
//创建环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
//设置并行度
env.setParallelism(1);
//设置时间语义
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
//调整watermark生成周期
env.getConfig().setAutoWatermarkInterval(2000L);
"====================================设置状态后端===================================================="
// TODO 状态后端的使用
// 1. MemoryStateBackend的使用
StateBackend memoryStateBackend = new MemoryStateBackend();
env.setStateBackend(memoryStateBackend);
// 2. FsStateBackend的使用
StateBackend fsStateBackend = new FsStateBackend("hdfs://host:port/xxx/xxx/xxx");
env.setStateBackend(fsStateBackend);
// 3. RocksDBStateBackend的使用 (需要 导入 依赖)
StateBackend rocksDBStateBackend = new RocksDBStateBackend("hdfs://{fs.defaultFS}/xxx/xxx/xxx");
env.setStateBackend(rocksDBStateBackend);
"===========================================^======================================================="
//读取数据
SingleOutputStreamOperator<WaterSensor> socketDS = env
//一条流A
.socketTextStream("localhost", 9999)
"====================================状态后端代码===================================================="
//TODO 0.另一条流B ——>后续设为广播流
DataStreamSource<String> controlDS = env.socketTextStream("localhost", 8888);
// TODO 应用场景(1.5版本才有的)
// 1.动态配置更新
// 2.类似开关的功能, 切换处理逻辑
// TODO 限制
// 1. 要广播出去的流B,最好是 数据量小、 更新不频繁
// TODO 1.将 其中一条流 B 广播出去
MapStateDescriptor<String, String> broadcastMapStateDesc = new MapStateDescriptor<>("broadcast-state", String.class, String.class);
BroadcastStream<String> controlBS = controlDS.broadcast(broadcastMapStateDesc);
// TODO 2.连接 流 A 和 广播B
BroadcastConnectedStream<String, String> inputControlBCS = inputDS.connect(controlBS);
"===========================================^======================================================="
//将数据封装成样例类
.map(new MapFunction<String, WaterSensor>() {
@Override
public WaterSensor map(String value) throws Exception {
String[] datas = value.split(",");
return new WaterSensor(datas[0], Long.valueOf(datas[1]), Integer.valueOf(datas[2]));
}
})
//指定如何从数据里提取事件时间,即指定watermark如何生成【分乱序,升序场景】
.assignTimestampsAndWatermarks(
//升序
WatermarkStrategy.WaterSensor>forMonotonousTimestamps()
//降序
WatermarkStrategy.<WaterSensor>forBoundedOutOfOrderness(Duration.ofSeconds(4))
.withTimestampAssigner((sensor, recordTs) -> sensor.getTs() * 1000L)
);
socketDS
//迟到数据处理 - 侧输出流
OutputTag<WaterSensor> outputTag = new OutputTag<WaterSensor>("wuyanzu") {
};
//分组
.keyBy(r -> r.getId())
//开窗
.timeWindow(Time.seconds(5))
//允许迟到时间
.allowedLateness(Time.seconds(2))
//将迟到数据加入主流
//注意:【如果处理函数是ProcessWindowFunction时,要加入,是ProcessFunction时,可在上下文环境中获取】
.sideOutputLateData(outputTag)
//处理
.process(
"===========================侧输出流=========================================="
new ...ProcessFunction<>(){
"后续内容"
//获取侧输出流 上下文.output(测输出流属性名)
ctx.output(侧输出流 属性名,要输出的值)
}
"============================================================================="
"==================================广播状态===================================="
new BroadcastProcessFunction<String, String, String>() {
/**
* 处理 流 A的数据
* @param value
* @param ctx
* @param out
* @throws Exception
*/
@Override
public void processElement(String value, ReadOnlyContext ctx, Collector<String> out) throws Exception {
// 主流 A 获取 广播状态,但是 只读的,不能修改,要在 流 B去更新
ReadOnlyBroadcastState<String, String> broadcastState = ctx.getBroadcastState(broadcastMapStateDesc);
if ("1".equals(broadcastState.get("switch"))) {
out.collect("打开....");
} else {
out.collect("不打开...");
}
}
/**
* 处理 广播流B 的数据
* @param value
* @param ctx
* @param out
* @throws Exception
*/
@Override
public void processBroadcastElement(String value, Context ctx, Collector<String> out) throws Exception {
BroadcastState<String, String> broadcastState = ctx.getBroadcastState(broadcastMapStateDesc);
// 把 数据 写入 广播状态
broadcastState.put("switch", value);
}
}
"===================================^========================================="
)
// TODO 从 主流里 获取 侧输出流
.getSideOutput(outputTag);
//执行环境
.execute()
容错机制
6.4.4 状态一致性
6.4.4.1一致性级别
在流处理中,一致性可以分为3个级别
1.at-most-once:
这其实是没有正确性保障的委婉说法—故障发生后,计数结果可能丢失,同样的还有udp
2.at-least-once:("最少一次")
这表示计数结果可能大于正确值,但绝不会小于正确值,也就是说,计数程序在发生故障后可能多算,但是绝不会少算。
3.exactly-once:("精准一次")
这指的是系统保证在发生故障后得到的计数结果与正确值一致。
--Flink的一个重大价值在于,它既保证了exactly-once,也具有低延迟和高吞吐的处理能力。
--从根本上说,Flink通过使自身满足所有需求来避免权衡。
6.4.4.2 端到端的状态一致性——如何保证精准一次新消费
整个端到端的一致性级别取决于所有组件中一致性最弱的组件
具体可以划分如下:
source端 ———— 需要外部源可重设数据的读取位置
flink内部 ———— 依赖checkpoint
sink端 ———— 需要保证从故障恢复时,数据不会重复写入外部系统
而对于sink端,又有两种具体实现方式:
'幂等写入'和'事务写入'
事务写入有两种方式:预写日志和两阶段提交
6.4.4.2.1 幂等写入
幂等操作,是说一个操作,可以重复执行很多次,但只导致一次结果更改,也就是说,后面再重复执行就不起作用了。
6.4.4.2.2 事务写入
需要构建事务来写入外部系统,构建的事务对应着checkpoint,等到checkpoint真正完的时候,才把所有对应的结果写入sink系统中,对于事务性写入,具体又有两种实现方式:预写日志(WAL)和两阶段提交(2PC)
①预写日志(WAL)
预写日志(Write-Ahead-Log)
1)把结果数据先当成状态保存,然后收到checkpoint完成的通知时,一次性写入sink系统。
2)由于数据提前在状态后端(state backend)中做了缓存,所以无论什么 sink 系统,都能用这种方式一批搞定
3)DataStream API提供了一个模板类:GenericWriteAheadSink来实现这种事务性sink
②两阶段提交(2PC)
两阶段提交two-parse commit
1)对于每个checkpoint,sink任务会启动一个事务,并将接下来所有接收的数据添加到事务里。
2)将这些数据写入外部sink系统,但是不提交它们,只是“预提交”。
3)当它收到checkpoint完成的通知时,才正式提交事务,实现结果的真正写入。
4)这种方式真正实现了exactly-once。
5)这种方式真正实现了exactly-once,它需要一个提供事务支持的外部sink系统,Flink提供了TwoPhaseCommitSinkFunction接口。
TwoPhaseCommitSink对外部sink系统的要求
1)外部 sink 系统必须提供事务支持,或者 sink 任务必须能够模拟外部系统上的事务
2)在 checkpoint 的间隔期间里,必须能够开启一个事务并接受数据写入
3)在收到 checkpoint 完成的通知之前,事务必须是“等待提交”的状态。
在故障恢复的情况下,这可能需要一些时间。如果这个时候sink系统关闭事务(例如超时了),那么未提交的数据就会丢失
4)sink 任务必须能够在进程失败后恢复事务
5)提交事务必须是幂等操作
总结
DataStream API 提供了GenericWriteAheadSink模板类和TwoPhaseCommitSinkFunction 接口,可以方便地实现这两种方式的事务性写入。 不同 Source 和 Sink 的一致性保证可以用下表说明:
'幂等会出现暂时不一致:'
是指一批数据回滚后,在发生故障前这批数据已经有写入sink的了,回滚会重新重播这部分数据,但是它是幂等操作,所以还是保证了Exactly-once。
6.4.5 检查点 —— checkpoint
①flink检查点算法——Chandy-Lamport 算法的分布式快照
作用: 为了保证精准一次性消费
Flink检查点的核心作用是确保状态正确,即使遇到程序中断,也要正确。记住这一基本点之后,Flink为用户提供了用来定义状态的工具。
1.Checkpoint Coordinator 向所有的source节点trigger Checkpoint;
2.source节点向下游广播barrier,这个barrier就是实现Chandy-Lamport分布式快照算法的核心,下游的task只有收到所有input的barrier才会执行相应的checkpoint;
3.当task完成state备份后,会将备份数据的地址(state handle)通知给checkpoint coordinator;
4.下游的sink节点收集齐上游两个input的barrier之后,会执行本地快照,这里特地展示了RocksDB incremental Checkpoint的流程,首先RocksDB会全量刷数据到磁盘上,然后Flink框架会从中选择没有上传的文件进行持久化备份
5.同样的,sink节点在完成自己的checkpoint之后,会将state handle返回通知Coordinator
6.最后,当checkpoint coordinator 收集齐所有的state handle,就认为这一次的checkpoint全局完成了,向持久化存储中再备份一个checkpoint meta文件
②barrier对齐
1.一旦 Operator 从输入流接收到 CheckPointbarrier n,它就不能处理来自该流的任何数据记录,直到它从其他所有输入接收到 barrier n 为止。否则,它会混合属于快照 n 的记录和属于快照 n + 1 的记录。
2.接收到 barrier n 的流暂时被搁置。从这些流接收的记录不会被处理,而是放入输入缓冲区。
3.上图中第 2 个图,虽然数字流对应的 barrier 已经到达了,但是 barrier 之后的 1、2、3 这些数据只能放到 buffer 中,等待字母流的 barrier 到达。
4.一旦最后所有输入流都接收到 barrier n,Operator 就会把缓冲区中 pending 的输出数据发出去,然后把 CheckPoint barrier n 接着往下游发送。这里还会对自身进行快照。
5.之后,Operator 将继续处理来自所有输入流的记录,在处理来自流的记录之前先处理来自输入缓冲区的记录。
③barrier不对齐
1.上述图 2 中,当还有其他输入流的 barrier 还没有到达时,会把已到达的 barrier 之后的数据 1、2、3 搁置在缓冲区,等待其他流的 barrier 到达后才能处理。
2.barrier 不对齐就是指当还有其他流的 barrier 还没到达时,为了不影响性能,也不用理会,直接处理 barrier 之后的数据。等到所有流的 barrier 的都到达后,就可以对该 Operator 做 CheckPoint 了。
Exactly Once 时必须 barrier 对齐,如果 barrier 不对齐就变成了 At Least Once。
④Flink+Kafka 如何实现端到端的Exactly-Once语义
1.第一条数据来了之后,开启一个kafka的事务(transaction),正常写入kafka分区日志,单标记为未提交,这就是“预提交”
2.jobmanager触发checkpoint操作,barrier从source开始向下传递,遇到barrier的算子将状态存入状态后端,并通知jobmanager
3.sink连接器收到barrier,保存当前状态,存入checkpoint,通知jobmanager,并开启下一阶段事务,用于提交下一个检查点的数据
4.jobmanager收到所有任务的通知,发出确认小子,表示checkpoint完成
5.sink收到jobmanager的确认信息,正式提交这段时间的数据
6.外部kafka关闭事务,提交的数据就可以正常消费了。
代码
public static void main(String[] args) throws Exception {
// 1.创建执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
// TODO Checkpoint设置
// env.enableCheckpointing(5000L,CheckpointingMode.EXACTLY_ONCE);
env.enableCheckpointing(5000L); //多久生成一次checkpoint(就是多久生成一次barrier)
env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);
env.getCheckpointConfig().setMaxConcurrentCheckpoints(2); // 异步ck,同时有几个ck在执行
env.getCheckpointConfig().setMinPauseBetweenCheckpoints(500L); // 上一个ck结束后,到下一个ck开启,最小间隔多久
env.getCheckpointConfig().setPreferCheckpointForRecovery(false); // 默认为 false,表示从 ck恢复;true,从savepoint恢复
env.getCheckpointConfig().setTolerableCheckpointFailureNumber(3); // 允许当前checkpoint失败的次数
// 一条流A
DataStreamSource<String> inputDS = env.socketTextStream("localhost", 9999);
// 另一条流 B
DataStreamSource<String> controlDS = env.socketTextStream("localhost", 8888);
// TODO 应用场景(1.5版本才有的)
// 1.动态配置更新
// 2.类似开关的功能, 切换处理逻辑
// TODO 限制
// 1. 要广播出去的流B,最好是 数据量小、 更新不频繁
// TODO 1.将 其中一条流 B 广播出去
MapStateDescriptor<String, String> broadcastMapStateDesc = new MapStateDescriptor<>("broadcast-state", String.class, String.class);
BroadcastStream<String> controlBS = controlDS.broadcast(broadcastMapStateDesc);
// TODO 2.连接 流 A 和 广播B
BroadcastConnectedStream<String, String> inputControlBCS = inputDS.connect(controlBS);
// TODO 3.使用 Process
inputControlBCS
.process(
new BroadcastProcessFunction<String, String, String>() {
/**
* 处理 流 A的数据
* @param value
* @param ctx
* @param out
* @throws Exception
*/
@Override
public void processElement(String value, ReadOnlyContext ctx, Collector<String> out) throws Exception {
// 主流 A 获取 广播状态,但是 只读的,不能修改,要在 流 B去更新
ReadOnlyBroadcastState<String, String> broadcastState = ctx.getBroadcastState(broadcastMapStateDesc);
if ("1".equals(broadcastState.get("switch"))) {
out.collect("打开....");
} else {
out.collect("不打开...");
}
}
/**
* 处理 广播流B 的数据
* @param value
* @param ctx
* @param out
* @throws Exception
*/
@Override
public void processBroadcastElement(String value, Context ctx, Collector<String> out) throws Exception {
BroadcastState<String, String> broadcastState = ctx.getBroadcastState(broadcastMapStateDesc);
// 把 数据 写入 广播状态
broadcastState.put("switch", value);
}
}
)
.print();
env.execute();
}
随笔
--------------------------------------------------------------------------------------------------------------------------------
checkpoint配置
env.getCheckpointConfig.setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE)
env.getCheckpointConfig.setCheckpointTimeout(300000L) // ck执行多久超时
env.getCheckpointConfig.setMaxConcurrentCheckpoints(2) // 异步ck,同时有几个ck在执行
env.getCheckpointConfig.setMinPauseBetweenCheckpoints(500L) // 上一个ck结束后,到下一个ck开启,最小间隔多久
env.getCheckpointConfig.setPreferCheckpointForRecovery(false) // 默认为 false,表示从 ck恢复;true,从savepoint恢复
env.getCheckpointConfig.setTolerableCheckpointFailureNumber(3) // 允许当前checkpoint失败的次数
--------------------------------------------------------------------------------------------------------------------------------
1.flink中的检查点,保存的是所有任务状态的快照
这个状态要求是所有任务都处理同一个数据之后的状态
2. flink checkpoint算法
基于 Chandy-Lamport 算法的分布式快照
3. flink checkpoint中重要的概念
barrier用于分隔不同的checkpoint,对于每个任务而言,收到barrier就意味着要开始做state的保存
算子中需要对不同上游分区发来的barrier,进行对齐
4. checkpoint存储位置,由state backend决定
一般是放在远程持久化存储空间
jobmanager触发一个checkpoint操作,会把checkpoint中所有任务状态的拓扑结构保存下来
5. barrier和watermark类似,都可以看作一个插入数据流中的特殊数据结构
barrier在数据处理上跟watermark是两套机制,完全没有关系
自我总结
--自我总结
分布式快照算法,它是分布式的快照算法,异步的,处理数据和执行ck是异步的,不需要把整个流程序停止再去做备份
--大概过程
jobmanager内部有一个协调器,固定周期会生成一个barrier,barrier插到数据流里,随着数据流的流动而流动。
首先经过一个source,barrier每到一个算子的task里,就会触发当前算子实例的ck,它会把本地状态保存到状态后端,害的向协调器汇报,备份的地址
barrier会不断的向下游传递,以此经过每一个算子,每个算子都做一个备份,每次备份完会向协调器汇报
最后到一个sink,sink会做一次预提交,预提交之后,备份自己的状态,然后会向协调器汇报自己的状态,
jobmanager收到sink的通知之后,会向所有的算子实例发送checkpoint完成的通知,sink接到通知之后会执行第二阶段的提交
--barrier分为对齐和不对齐
如果上游并行度大一点,就是多对一,同一个编号的barrier向下游传递的时候,下游需要等到同一编号的barrier都到齐了之后才会触发自己的备份
这中间涉及到barrier到的时间有快有慢,
如果barrier先到的那个任务,barrier后面的那个数据要不要接着处理的问题?如果接着处理就会造成结果不准备,造成一个重复的计算
如果不处理,先到的barrier放哪里呢,放在下游算子的缓存里,
如果是一对多,下游的并行度比下游多,就用广播
--状态后端的知识点
除了rockdb,本地状态后存在taskmanager的jvm堆里
checkpoint什么时候用呢?出故障恢复数据的时候用
再做计算的时候,要把状态取出来用,并把状态更新,因为状态备份在ck里难到每次都要去ck里读取吗,那太慢了
本地状态的作用是:执行的时候用,一个算子对本地状态,就是内存里读取会快一点,然后做定期的做checkpoint,做一个备份,备份是挂了再起起来,把备份从checkpoint那端恢复到本地状态
--和保存点的区别:
checkpoint是自动的,保存点是手动的
--ck的产生的周期,就是barrier产生的间隔:
如果太小,性能要求比较高
如果太大,1.要重跑的时间间隔太慢了
2.sink的时候,如果是量阶段提交,间隔一个小时的话,正式提交要一个小时之后才提交,这样对实时有影响
--如果要求实时响应在毫秒级
1.性能特别特别好可以选barrier对齐没问题,大概率选不对齐,就是无法保证精准一次性了
2.间隔周期不宜过长,没法毫秒级输出展示,可能处理是毫秒级
6.4.6 保存点
1、Flink 还提供了可以自定义的镜像保存功能,就是保存点(savepoints)
2、原则上,创建保存点使用的算法与检查点完全相同,因此保存点可以认为就是具有一些额外元数据的检查点
3、Flink不会自动创建保存点,因此用户(或外部调度程序)必须明确地触发创建操作
4、保存点是一个强大的功能。除了故障恢复外,保存点可以用于:有计划的手动备份,更新应用程序,版本迁移,暂停和重启应用,等等