Flink流处理高级编程
在上一个章节中,我们已经学习了Flink的基础编程API的使用,接下来,我们来学习Flink编程的高阶部分。所谓的高阶部分内容,其实就是Flink与其他计算框架不相同且占优势的地方,比如Window和Exactly-Once,接下来我们就对这些内容进行详细的学习。
7.1 Flink的window机制
7.1.1 窗口概述
在流处理应用中,数据是连续不断的,因此我们不可能等到所有数据都到了才开始处理。当然我们可以每来一个消息就处理一次,但是有时我们需要做一些聚合类的处理,例如:在过去的1分钟内有多少用户点击了我们的网页。在这种情况下,我们必须定义一个窗口,用来收集最近一分钟内的数据,并对这个窗口内的数据进行计算。
流式计算是一种被设计用于处理无限数据集的数据处理引擎,而无限数据集是指一种不断增长的本质上无限的数据集,而Window窗口是一种切割无限数据为有限块进行处理的手段。
在Flink中, 窗口(window)是处理无界流的核心. 窗口把流切割成有限大小的多个"存储桶"(bucket), 我们在这些桶上进行计算。
7.1.2 窗口的分类
- 基于时间的窗口(时间驱动)
- 基于元素个数的窗口(数据驱动)
7.1.2.1 基于时间的窗口
时间窗口包含一个开始时间戳(包括)和结束时间戳(不包括),这两个时间戳一起限制了窗口的尺寸。时间窗口是左闭右开的。
在代码中,Flink使用TimeView这个类来表示基于时间的窗口。这个类提供了key查询开始时间戳和结束时间戳的方法,还提供了针对给定的窗口混去它允许的最大时间差的方法 maxTimestamp()。
时间窗口又分为四种:
- Tumbling Windows 滚动窗口
- Sliding Windows 滚动窗口
- Session Windows 会话窗口
- Global Windows 全局窗口
7.1.2.1.1 Tumbling Windows 滚动窗口
滚动窗口有固定的大小, 窗口与窗口之间不会重叠也没有缝隙.比如,如果指定一个长度为5分钟的滚动窗口, 当前窗口开始计算, 每5分钟启动一个新的窗口.
滚动窗口能将数据流切分成不重叠的窗口,每一个事件只能属于一个窗口。
package com.atguigu.flink.window;
import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.java.tuple.Tuple;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.datastream.WindowedStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.windowing.ProcessWindowFunction;
import org.apache.flink.streaming.api.windowing.assigners.TumblingProcessingTimeWindows;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;
import org.apache.flink.util.Collector;
public class Test01_Window_Time_Tumbling {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
DataStreamSource<String> streamSource = env.socketTextStream("localhost", 9999);
// 将数据转化为Tuple
SingleOutputStreamOperator<Tuple2<String, Long>> wordToOneDStream = streamSource.flatMap(new FlatMapFunction<String, Tuple2<String, Long>>() {
@Override
public void flatMap(String value, Collector<Tuple2<String, Long>> out) throws Exception {
String[] split = value.split(" ");
for (String s: split) {
out.collect(Tuple2.of(s, 1L));
}
}
});
// 将相同的单词聚合到同一个分区
KeyedStream<Tuple2<String, Long>, Tuple> keyedStream = wordToOneDStream.keyBy(0);
// 开启一个基于时间的滚动窗口
WindowedStream<Tuple2<String, Long>, Tuple, TimeWindow> window = keyedStream.window(TumblingProcessingTimeWindows.of(Time.seconds(10)));
SingleOutputStreamOperator<String> process = window.process(new ProcessWindowFunction<Tuple2<String, Long>, String, Tuple, TimeWindow>() {
@Override
public void process(Tuple tuple, Context context, Iterable<Tuple2<String, Long>> elements, Collector<String> out) throws Exception {
String msg = "窗口: [" + context.window().getStart() / 1000 + "," + context.window().getEnd() / 1000 + " ] 一共有 " + elements.spliterator().estimateSize() + "条数据";
out.collect(msg);
}
});
process.print();
window.sum(1).print();
env.execute();
}
}
说明:
- 时间间隔可以通过: Time.milliseconds(x), Time.seconds(x), Time.minutes(x),等等来指定.
- 我们传递给window函数的对象叫窗口分配器.
7.1.2.1.2 Sliding Windows 滑动窗口
与滚动窗口一样,滑动窗口也是有固定的长度。另外一个参数我们叫滑动步长,用来控制滑动窗口启动的频率。所以,如果滑动步长小于窗口长度,滑动窗口会重叠。这种情况下,一个元素可能会被分配到多个窗口中。例如,滑动窗口长度10分钟, 滑动步长5分钟,则每5分钟会得到一个包含最近10分钟的数据。
package com.atguigu.flink.window;
import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.java.tuple.Tuple;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.datastream.WindowedStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.windowing.ProcessWindowFunction;
import org.apache.flink.streaming.api.windowing.assigners.SlidingProcessingTimeWindows;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;
import org.apache.flink.util.Collector;
public class Test02_Time_Sliding {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
DataStreamSource<String> streamSource = env.socketTextStream("localhost", 9999);
SingleOutputStreamOperator<Tuple2<String, Long>> wordToOneDStream = streamSource.flatMap(new FlatMapFunction<String, Tuple2<String, Long>>() {
@Override
public void flatMap(String value, Collector<Tuple2<String, Long>> out) throws Exception {
String[] split = value.split(" ");
for (String s: split) {
out.collect(Tuple2.of(s, 1L));
}
}
});
KeyedStream<Tuple2<String, Long>, Tuple> keyedStream = wordToOneDStream.keyBy(0);
WindowedStream<Tuple2<String, Long>, Tuple, TimeWindow> window = keyedStream.window(SlidingProcessingTimeWindows.of(Time.seconds(6), Time.seconds(3)));
SingleOutputStreamOperator<String> process = window.process(new ProcessWindowFunction<Tuple2<String, Long>, String, Tuple, TimeWindow>() {
@Override
public void process(Tuple tuple, Context context, Iterable<Tuple2<String, Long>> elements, Collector<String> out) throws Exception {
String msg = "窗口: [" + context.window().getStart() / 1000 + "," + context.window().getEnd() / 1000 + ") 一共有" + elements.spliterator().estimateSize() + "条数据";
out.collect(msg);
}
});
process.print();
window.sum(1).print();
env.execute();
}
}
7.1.2.1.3 Session Windows 会话窗口
会话窗口分配器会根据活动的元素进行分组。会话窗口不会有重叠,与滚动窗口和滑动窗口相比,会话窗口也没有固定的开启和关闭时间。如果会话窗口有一段时间没有收到数据, 会话窗口会自动关闭,这段没有收到数据的时间就是会话窗口的gap(间隔)。我们可以配置静态的gap,也可以通过一个 gap extractor 函数来定义gap的长度。当时间超过了这个gap,当前的会话窗口就会关闭,后序的元素会被分配到一个新的会话窗口。
// 静态gap
.window(ProcessingTimeSessionWindows.withGap(Time.seconds(10)))
// 动态gap
.window(ProcessingTimeSessionWindows.withGap(new SessionWindowTimeGapExtractor<Tuple2<String, Long>> {
@Override
public long extract(Tuple2<String, Long> element) {
return element.f0.length() * 1000;
}
}))
创建原理:
因为会话窗口没有固定的开启和关闭时间,所以会话窗口的创建和关闭与滚动、滑动窗口不同。在Flink内部,每到达一个新的元素都会创建一个新的会话窗口,如果这些窗口彼此相距比定义的gap小,则会对他们进行合并。为了能够合并,会话窗口算子需要合并触发器和合并窗口函数:
ReduceFunction, AggregateFunction, or ProcessWindowFunction
7.1.2.1.4 Global Windows 全局窗口
全局窗口分配器会分配相同key的所有元素进入同一个 Global window。这种窗口机制只有指定自定义的触发器时才有用。否则,不会做任务计算,因为这种窗口没有能够处理聚集在一起元素的结束点。
.window(GolbalWindows.create());
7.1.2.2 基于元素个数的窗口
按指定的数据条数生成一个Window,与时间无关,一共可分为两类:
- 滚动窗口
- 滑动窗口
7.1.2.2.1 滚动窗口
默认的 CountWindow 是一个滚动窗,只需要指定窗口大小即可,当元素数量达到窗口大小时,就会触发窗口的执行。
.countWindow(3)
说明:哪个窗口先达到3个元素,哪个窗口就关闭,不影响其他的窗口。
7.1.2.2.2 滑动窗口
滑动窗口和滚动窗口的函数名是完全一致,只是在传参数时需要传入两个参数,一个是window_size,一个是sliding_size。下面代码中的sliding_size设置为了2,也就是说,每收到两个相同key的数据就计算一次,每一次计算的window范围最多是3个元素。
.countWindow(3,2)
7.1.3 Window Function
前面指定了窗口的分配器,接着我们需要来指定如何计算,这事由 window function 来负责。一旦窗口关闭,window function 去计算处理窗口中的每个元素.
window function 可以是 ReduceFunction,AggregateFunction,or ProcessWindowFunction 中的任意一种。
ReduceFunction,AggregateFunction 更加高效,原因就是Flink可以对到来的元素进行增量聚合。ProcessWindowFunction(全窗口函数)可以得到一个包含这个窗口中所有元素的迭代器,以及这些元素所属窗口的一些元数据信息。ProcessWindowFunction 不能被高效执行的原因是Flink在执行这个函数之前, 需要在内部缓存这个窗口上所有的元素
增量聚合函数是来一条计算一条,而全窗口函数则是等到数据都到了再做计算做一次计算。
7.1.3.1 ReduceFunction(增量聚合函数-不会改变数据的类型)
.window(TumblingProcessingTimeWindows.of(Time.seconds(5)))
.reduce(new ReduceFunction<Tuple2<String, Long>>() {
@Override
public Tuple2<String, Long> reduce(Tuple2<String, Long> value1, Tuple2<String, Long> value2) throws Exception {
System.out.println(value1 + " ----- " + value2);
// value1是上次聚合的结果. 所以遇到每个窗口的第一个元素时, 这个函数不会进来
return Tuple2.of(value1.f0, value1.f1 + value2.f1);
}
})
7.1.3.2 AggregateFunction(增量聚合函数-可以改变数据的类型)
.window(TumblingProcessingTimeWindows.of(Time.seconds(5)))
.aggregate(new AggregateFunction<Tuple2<String, Long>, Long, Long>() {
// 创建累加器: 初始化中间值
@Override
public Long createAccumulator() {
System.out.println("createAccumulator");
return 0L;
}
// 累加器操作
@Override
public Long add(Tuple2<String, Long> value, Long accumulator) {
System.out.println("add");
return accumulator + value.f1;
}
// 获取结果
@Override
public Long getResult(Long accumulator) {
System.out.println("getResult");
return accumulator;
}
// 累加器的合并: 只有会话窗口才会调用
@Override
public Long merge(Long a, Long b) {
System.out.println("merge");
return a + b;
}
})
7.1.3.3 ProcessWindowFunction(全窗口函数)
.process(new ProcessWindowFunction<Tuple2<String, Long>, Tuple2<String, Long>, String, TimeWindow>() {
// 参数1: key 参数2: 上下文对象 参数3: 这个窗口内所有的元素 参数4: 收集器, 用于向下游传递数据
@Override
public void process(String key,
Context context,
Iterable<Tuple2<String, Long>> elements,
Collector<Tuple2<String, Long>> out) throws Exception {
System.out.println(context.window().getStart());
long sum = 0L;
for (Tuple2<String, Long> t : elements) {
sum += t.f1;
}
out.collect(Tuple2.of(key, sum));
}
})
全窗口函数应用场景:可以求百分之多少的数据或者求平均数这种需要把全部数据拿到之后再求的场景。
7.2 Keyed VS Non-Keyed Windows
其实,在用window前首先需要确认应该是在keyBy后的流上用,还是在没有keyBy的流上使用。
在 keyed streams 上使用窗口,窗口计算被并行的运用在多个task上,可以认为每个分组都有自己单独窗口。正如前面的代码所示。
在 non-keyed stream 上使用窗口,无论并行度设置的是几窗口的并行度都是1, 所有的窗口逻辑只能在一个单独的task上执行。
.windowAll(TumblingProcessingTimeWindows.of(Time.seconds(10)))
需要注意的是: 非key分区的流,即使把并行度设置为大于 1 的数,窗口也只能在某个分区上使用。
7.3 Flink中的时间语义与WaterMark
7.3.1 Flink中的时间语义
7.3.1.1 处理时间(Process time)
处理时间是指的执行操作的各个设备的时间
对于运行在处理时间上的流程序,所有的基于时间的操作(比如时间窗口)都是使用的设备时钟。比如,一个长度为1个小时的窗口将会包含设备时钟表示的1个小时内所有的数据。假设应用程序在9:15am启动,第1个小时窗口将会包含9:15am到10:00am所有的数据,然后下个窗口是10:00am-11:00am,等等。
处理时间是最简单时间语义,数据流和设备之间不需要做任何的协调。他提供了最好的性能和最低的延迟。但是,在分布式和异步的环境下,处理时间没有办法保证确定性,容易受到数据传递速度的影响:事件的延迟和乱序
在使用窗口的时候,如果使用处理时间,就指定时间分配器为处理时间分配器。
7.3.1.2 事件时间(event time)
事件时间是指的这个事件发生的时间
在event进入Flink之前,通常被嵌入到了event中,一般作为这个event的时间戳存在。
在事件时间体系中,时间的进度依赖于数据本身,和任何设备的时间无关。事件时间程序必须制定如何产生 Event Time Watermarks(水印) 。在事件时间体系中,水印是表示时间进度的标志(作用就相当于现实时间的时钟)。
在理想情况下,不管事件时间何时到达或者他们的到达的顺序如何,事件时间处理将产生完全一致且确定的结果。事件时间处理会在等待无序事件(迟到事件)时产生一定的延迟。由于只能等待有限的时间,因此这限制了确定性事件时间应用程序的可使用性。
假设所有数据都已到达,事件时间操作将按预期方式运行,即使在处理无序或迟到的事件或重新处理历史数据时,也会产生正确且一致的结果。例如,每小时事件时间窗口将包含带有事件时间戳的所有记录,该记录落入该小时,无论它们到达的顺序或处理时间。
在使用窗口的时候,如果使用事件时间,就指定时间分配器为事件时间分配器。
注意:
在1.12之前默认的时间语义是处理时间,从1.12开始,Flink内部已经把默认的语义改成了事件时间。
7.3.2 哪种时间更重要
7.3.3 Flink中的WaterMark
支持 event time 的流式处理框架需要一种能够测量 event time 进度的方式。比如, 一个窗口算子创建了一个长度为1小时的窗口,那么这个算子需要知道事件时间已经到达了这个窗口的关闭时间,从而在程序中去关闭这个窗口。
事件时间可以不依赖处理时间来表示时间的进度。例如,在程序中,即使处理时间和事件时间有相同的速度,事件时间可能会轻微的落后处理时间。另外一方面,使用事件时间可以在几秒内处理已经缓存在Kafka中多周的数据,这些数据可以照样被正确处理,就像实时发生的一样能够进入正确的窗口。
这种在Flink中去测量事件时间的进度的机制就是watermark(水印)。watermark作为数据流的一部分在流动,并且携带一个时间戳t。
一个Watermark(t)表示在这个流里面事件时间已经到了时间t,意味着此时,流中不应该存在这样的数据:他的时间戳 t2<=t (时间比较旧或者等于时间戳)。
水印的总结:
- 衡量事件时间的进展
- 是一个特殊的时间戳,生成之后随着流的流动而向后传递
- 用来处理数据乱序的问题
- 触发窗口等的计算、关闭
- 单调递增的(时间不能倒退)
- Flink认为,小于Watermark时间戳的数据处理完了,不应该再出现
7.3.3.1 有序流中的水印
在下面的这个图中,事件是有序的(生成数据的时间和被处理的时间顺序是一致的), watermark是流中一个简单的周期性的标记。
有序场景:
- 底层调用的是乱序的Watermark生成器,只是乱序程度传了一个 0 ms
- Watermark = maxTimestamp - outOfOrdernessMills - 1ms
= maxTimestamp - 0ms - 1ms
= 事件时间 - 1ms
7.3.3.2 乱序流中的水印
在下图中,按照他们时间戳来看,这些事件是乱序的,则watermark对于这些乱序的流来说至关重要。
通常情况下,水印是一种标记,是流中的一个点,所有在这个时间戳(水印中的时间戳)前的数据应该已经全部到达。一旦水印到达了算子,则这个算子会提高他内部的时钟的值为这个水印的值。
乱序场景:
- 什么是乱序:时间戳大的比时间戳小的先来
- 乱序程度设置多少比较合适
- 经验值:依据对自身集群和数据的了解,大概估算
- 对数据进行抽样
- 肯定不会设置为几个小时,一般设置为秒或分钟
- Watermark = maxTimestamep -outOfOrdernessMills - 1ms
= 当前最大的事件时间 - 乱序程度(等待时间) -1ms
7.3.4 Flink中如何产生水印
在Flink中,水印由应用程序开发人员生成,这通常需要对相应的领域有一定的了解。完美的水印永远不会错:在特殊情况下(例如非乱序事件流),最近一次事件的时间戳就可能是完美的水印。时间戳小于水印标记时间的事件不会再出现。
启发式水印则相反,它只估计时间,因此有可能出错,即迟到的事件(其时间戳小于水印标记时间)晚于水印出现。针对启发式水印,Flink提供了处理迟到元素的机制。
设定水印通常需要用到领域知识。举例来说,如果知道事件的迟到时间不会超过 5 秒,就可以将水印标记时间设为收到的最大时间戳减去 5 秒。另一种做法是,采用一个Flink作业监控事件流,学习事件的迟到规律,并以此构建水印生成模型。
7.3.5 EventTime和WaterMark的使用
Flink内置了两个WaterMark生成器:
1 Monotonously Increasing Timestamps (时间戳单调增长:其实就是允许的延迟为0)
WatermarkStrategy.forMonotonousTimestamps();
2 Fixed Amount of Lateness (允许固定时间的延迟)
package com.atguigu.flink.watermark;
import com.atguigu.flink.source.WaterSensor;
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.windowing.ProcessWindowFunction;
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 java.time.Duration;
public class Test01_OrderedWaterMark {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment
.getExecutionEnvironment()
.setParallelism(1);
SingleOutputStreamOperator<WaterSensor> stream = env
.socketTextStream("hadoop102", 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]));
}
});
stream.print();
// 创建水印产生策略
WatermarkStrategy<WaterSensor> wms = WatermarkStrategy
// 最大容忍的延迟时间
.<WaterSensor>forBoundedOutOfOrderness(Duration.ofSeconds(3))
// 指定时间戳
.withTimestampAssigner(new SerializableTimestampAssigner<WaterSensor>() {
@Override
public long extractTimestamp(WaterSensor element, long recordTimestamp) {
return element.getTs() * 1000;
}
});
stream
// 指定水印和时间戳
.assignTimestampsAndWatermarks(wms)
.keyBy(WaterSensor::getId)
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
.process(new ProcessWindowFunction<WaterSensor, String, String, TimeWindow>() {
@Override
public void process(String key, Context context, Iterable<WaterSensor> elements, Collector<String> out) throws Exception {
String msg =
"当前key: " + key + "窗口: [" + context.window().getStart() / 1000 + "," + context.window().getEnd() / 1000 + ") 一共有 " + elements.spliterator().estimateSize() + "条数据";
out.collect(msg);
}
})
.print();
env.execute();
}
}
7.3.6 自定义WatermarkStrategy
有2种风格的WaterMark生产方式: periodic(周期性) and punctuated(间歇性),都需要继承接口: WatermarkGenerator
// 周期性
package com.atguigu.flink.watermark;
import com.atguigu.flink.source.WaterSensor;
import org.apache.flink.api.common.eventtime.*;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.datastream.WindowedStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
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.kerby.kerberos.provider.token.JwtAuthToken;
public class Test02_Window_EventTime_tumbling_CustomerPeriod {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
SingleOutputStreamOperator<WaterSensor> waterSensorDS = env.socketTextStream("hadoop102", 9999)
.map(date -> {
String[] words = date.split(",");
return new WaterSensor(words[0], Long.parseLong(words[1]), Integer.parseInt(words[2]));
});
//waterSensorDS.print();
//todo 自定义WatermarkStrategy
SingleOutputStreamOperator<WaterSensor> waterSensorSingleOutputStreamOperator = waterSensorDS.assignTimestampsAndWatermarks(new WatermarkStrategy<WaterSensor>() {
@Override
public WatermarkGenerator<WaterSensor> createWatermarkGenerator(WatermarkGeneratorSupplier.Context context) {
return new Myperiod(2000L);
}
}
.withTimestampAssigner(new SerializableTimestampAssigner<WaterSensor>() {
// 提取数据时间戳
@Override
public long extractTimestamp(WaterSensor element, long recordTimestamp) {
return element.getTs() * 1000L;
}
}));
// 按id分组
KeyedStream<WaterSensor, String> keyedStream = waterSensorSingleOutputStreamOperator.
keyBy(date -> date.getId());
//keyedStream.print();
// 开窗
WindowedStream<WaterSensor, String, TimeWindow> window =
keyedStream.window(TumblingEventTimeWindows.of(Time.seconds(5)));
window.sum("vc").print();
env.execute();
}
public static class Myperiod implements WatermarkGenerator<WaterSensor> {
// 最大时间戳
private Long maxTs;
// 最大延迟时间
private Long maxDelay;
// 构造方法
public Myperiod(Long maxDelay) {
this.maxDelay = maxDelay;
this.maxTs = Long.MIN_VALUE + this.maxDelay + 1;
}
// 当数结局来的时候调用 -- 周期性
@Override
public void onEvent(WaterSensor event, long eventTimestamp, WatermarkOutput output) {
maxTs = Math.max(eventTimestamp, maxTs);
}
@Override
public void onPeriodicEmit(WatermarkOutput output) {
// System.out.println("生成Watermark");
output.emitWatermark(new Watermark(maxTs - maxDelay - 1L));
}
}
}
// 间歇性
package com.atguigu.flink.watermark;
import com.atguigu.flink.source.WaterSensor;
import org.apache.flink.api.common.eventtime.*;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.datastream.WindowedStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
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.kerby.kerberos.provider.token.JwtAuthToken;
public class Test02_Window_EventTime_tumbling_CustomerPeriod {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
SingleOutputStreamOperator<WaterSensor> waterSensorDS = env.socketTextStream("hadoop102", 9999)
.map(date -> {
String[] words = date.split(",");
return new WaterSensor(words[0], Long.parseLong(words[1]), Integer.parseInt(words[2]));
});
//waterSensorDS.print();
//todo 自定义WatermarkStrategy
SingleOutputStreamOperator<WaterSensor> waterSensorSingleOutputStreamOperator = waterSensorDS.assignTimestampsAndWatermarks(new WatermarkStrategy<WaterSensor>() {
@Override
public WatermarkGenerator<WaterSensor> createWatermarkGenerator(WatermarkGeneratorSupplier.Context context) {
return new Myperiod(2000L);
}
}
.withTimestampAssigner(new SerializableTimestampAssigner<WaterSensor>() {
// 提取数据时间戳
@Override
public long extractTimestamp(WaterSensor element, long recordTimestamp) {
return element.getTs() * 1000L;
}
}));
// 按id分组
KeyedStream<WaterSensor, String> keyedStream = waterSensorSingleOutputStreamOperator.
keyBy(date -> date.getId());
//keyedStream.print();
// 开窗
WindowedStream<WaterSensor, String, TimeWindow> window =
keyedStream.window(TumblingEventTimeWindows.of(Time.seconds(5)));
window.sum("vc").print();
env.execute();
}
public static class Myperiod implements WatermarkGenerator<WaterSensor> {
// 最大时间戳
private Long maxTs;
// 最大延迟时间
private Long maxDelay;
// 构造方法
public Myperiod(Long maxDelay) {
this.maxDelay = maxDelay;
this.maxTs = Long.MIN_VALUE + this.maxDelay + 1;
}
// 当数结局来的时候调用 -- 周期性
@Override
public void onEvent(WaterSensor event, long eventTimestamp, WatermarkOutput output) {
// 获取当前数据中最大的时间戳并赋值
maxTs = Math.max(eventTimestamp, maxTs);
System.out.println("生成Watermark");
output.emitWatermark(new Watermark(maxTs - maxDelay - 1L));
}
// 周期性调用
@Override
public void onPeriodicEmit(WatermarkOutput output) {
}
}
}
7.3.7 多并行度下WaterMark的传递
总结:
- 多并行度的条件下, 向下游传递WaterMark的时候是以广播的方式传递的
- 总是以最小的那个WaterMark为准!木桶原理!
- 并且当Watermark值没有增长的时候不会向下游传递,注意:生成不变。
7.4 窗口允许迟到的数据
已经添加了wartemark之后, 仍有数据会迟到怎么办? Flink的窗口, 也允许迟到数据。
当触发了窗口计算后, 会先计算当前的结果, 但是此时并不会关闭窗口。以后每来一条迟到数据, 则触发一次这条数据所在窗口计算(增量计算)。
那么什么时候会真正的关闭窗口呢? wartermark 超过了窗口结束时间 + 等待时间
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
.allowedLateness(Time.seconds(3))
**注意:**允许迟到只能运用在event time上
7.5 侧输出流(sideOutput)
7.5.1 处理窗口关闭之后的迟到数据
允许迟到数据, 窗口也会真正的关闭, 如果还有迟到的数据怎么办? Flink提供了一种叫做侧输出流的来处理关窗之后到达的数据。
package com.atguigu.flink.sideoutput;
import com.atguigu.flink.source.WaterSensor;
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.windowing.ProcessWindowFunction;
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 java.time.Duration;
public class Test01_sideOutput {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
// System.out.println(env.getConfig());
SingleOutputStreamOperator<WaterSensor> stream = env
.socketTextStream("hadoop102", 9999) // 在socket终端只输入毫秒级别的时间戳
.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]));
}
});
// 创建水印生产策略
WatermarkStrategy<WaterSensor> wms = WatermarkStrategy
.<WaterSensor>forBoundedOutOfOrderness(Duration.ofSeconds(3)) // 最大容忍延迟时间
.withTimestampAssigner(new SerializableTimestampAssigner<WaterSensor>() {
@Override
public long extractTimestamp(WaterSensor element, long recordTimestamp) {
return element.getTs() * 1000;
}
});
SingleOutputStreamOperator<String> result = stream
.assignTimestampsAndWatermarks(wms)
.keyBy(WaterSensor::getId)
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
.allowedLateness(Time.seconds(3))
.sideOutputLateData(new OutputTag<WaterSensor>("side_1") {
})
.process(new ProcessWindowFunction<WaterSensor, String, String, TimeWindow>() {
@Override
public void process(String key, Context context, Iterable<WaterSensor> elements,
Collector<String> out) throws Exception {
String msg = "当前key: " + key + " 窗口: [" + context.window().getStart() / 1000 + ", " + context.window().getEnd() / 1000 +
") 一共有 " + elements.spliterator().estimateSize() + " 条数据" + "watermark: " + context.currentWatermark();
out.collect(context.window().toString());
out.collect(msg);
}
});
result.print();
result.getSideOutput(new OutputTag<WaterSensor>("side_1"){}).print();
env.execute();
}
}
允许迟到数据+侧输出流作用:
尽量快速提供一个近似准确结果,为了保证时效性,然后加上允许迟到数据+侧输出流得到最终的数据,这样也不用维护大量的窗口,性能也就会好很多。
7.5.2 使用侧输出流把一个流拆成多个流
split算子可以把一个流分成两个流, 从1.12开始已经被移除了. 官方建议我们用侧输出流来替换split算子的功能.
需求: 采集监控传感器水位值,将水位值高于5cm的值输出到 side output
package com.atguigu.flink.sideoutput;
import com.atguigu.flink.source.WaterSensor;
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
import org.apache.flink.streaming.api.functions.windowing.ProcessWindowFunction;
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 java.time.Duration;
public class Test02_WarmingSideOutput {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
// System.out.println(env.getConfig());
SingleOutputStreamOperator<WaterSensor> result =
env
.socketTextStream("hadoop102", 9999) // 在socket终端只输入毫秒级别的时间戳
.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]));
}
})
.keyBy(ws -> ws.getTs())
.process(new KeyedProcessFunction<Long, WaterSensor, WaterSensor>() {
@Override
public void processElement(WaterSensor value, Context ctx, Collector<WaterSensor> out) throws Exception {
out.collect(value);
if (value.getVc() > 5) { // 水位大于5的写入侧输出流
ctx.output(new OutputTag<WaterSensor>("警告") {
}, value);
}
}
});
result.print("主流");
result.getSideOutput(new OutputTag<WaterSensor>("警告") {
}).print("警告");
env.execute();
}
}
7.6 ProcessFunction API(底层API)
我们之前学习的转换算子是无法访问事件的时间戳信息和水位线信息的。而这在一些应用场景下,极为重要。例如MapFunction这样的map转换算子就无法访问时间戳或者当前事件的事件时间。
基于此,DataStream API提供了一系列的Low-Level转换算子。可以访问时间戳、watermark以及注册定时事件。还可以输出特定的一些事件,例如超时事件等。Process Function用来构建事件驱动的应用以及实现自定义的业务逻辑(使用之前的window函数和转换算子无法实现)。例如,Flink SQL就是使用Process Function实现的。
Flink提供了8个Process Function:
下面分别给一个示例, 有些其实已经学习过了.
7.6.1 Processfunction
env
.socketTextStream("hadoop102", 9999)
.map(line -> {
String[] datas = line.split(",");
return new WaterSensor(datas[0], Long.valueOf(datas[1]), Integer.valueOf(datas[2]));
})
.process(new ProcessFunction<WaterSensor, String>() {
@Override
public void processElement(WaterSensor value, Context ctx, Collector<String> out) throws Exception {
out.collect(value.toString());
}
})
.print();
7.6.2 KeyedProcessFunction
env
.socketTextStream("hadoop102", 9999)
.map(line -> {
String[] datas = line.split(",");
return new WaterSensor(datas[0], Long.valueOf(datas[1]), Integer.valueOf(datas[2]));
})
.keyBy(ws -> ws.getId())
.process(new KeyedProcessFunction<String, WaterSensor, String>() { // 泛型1:key的类型 泛型2:输入类型 泛型3:输出类型
@Override
public void processElement(WaterSensor value, Context ctx, Collector<String> out) throws Exception {
System.out.println(ctx.getCurrentKey());
out.collect(value.toString());
}
})
.print();
7.6.3 CoProcessFunction
DataStreamSource<Integer> intStream = env.fromElements(1, 2, 3, 4, 5);
DataStreamSource<String> stringStream = env.fromElements("a", "b", "c");
ConnectedStreams<Integer, String> cs = intStream.connect(stringStream);
cs
.process(new CoProcessFunction<Integer, String, String>() {
@Override
public void processElement1(Integer value, Context ctx, Collector<String> out) throws Exception {
out.collect(value.toString());
}
@Override
public void processElement2(String value, Context ctx, Collector<String> out) throws Exception {
out.collect(value);
}
})
.print();
7.6.4 ProcessJoinFunction
SingleOutputStreamOperator<WaterSensor> s1 = env
.socketTextStream("hadoop102", 8888) // 在socket终端只输入毫秒级别的时间戳
.map(value -> {
String[] datas = value.split(",");
return new WaterSensor(datas[0], Long.valueOf(datas[1]), Integer.valueOf(datas[2]));
});
SingleOutputStreamOperator<WaterSensor> s2 = env
.socketTextStream("hadoop102", 9999) // 在socket终端只输入毫秒级别的时间戳
.map(value -> {
String[] datas = value.split(",");
return new WaterSensor(datas[0], Long.valueOf(datas[1]), Integer.valueOf(datas[2]));
});
s1.join(s2)
.where(WaterSensor::getId)
.equalTo(WaterSensor::getId)
.window(TumblingProcessingTimeWindows.of(Time.seconds(5))) // 必须使用窗口
.apply(new JoinFunction<WaterSensor, WaterSensor, String>() {
@Override
public String join(WaterSensor first, WaterSensor second) throws Exception {
return "first: " + first + ", second: " + second;
}
})
.print();
7.6.5 BroadcastProcessFunction
7.6.6 KeyedBroadcastProcessFunction
keyBy之后使用
7.6.7 ProcessWindowFunction
添加窗口之后使用
7.6.8 ProcessAllWindowFunction
全窗口函数之后使用
7.7 定时器
基于处理时间或者事件时间处理过一个元素之后, 注册一个定时器, 然后指定的时间执行,定时器只能用于keyedStream中,即keyby之后使用.
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 删除之前注册的事件时间定时器,如果没有此时间戳的定时器,则不执行。
7.7.1 基于处理时间的定时器
SingleOutputStreamOperator<WaterSensor> stream = env
.socketTextStream("hadoop102", 9999) // 在socket终端只输入毫秒级别的时间戳
.map(value -> {
String[] datas = value.split(",");
return new WaterSensor(datas[0], Long.valueOf(datas[1]), Integer.valueOf(datas[2]));
});
stream
.keyBy(WaterSensor::getId)
.process(new KeyedProcessFunction<String, WaterSensor, String>() {
@Override
public void processElement(WaterSensor value, Context ctx, Collector<String> out) throws Exception {
// 处理时间过后5s后触发定时器
ctx.timerService().registerProcessingTimeTimer(ctx.timerService().currentProcessingTime() + 5000);
out.collect(value.toString());
}
// 定时器被触发之后, 回调这个方法
// 参数1: 触发器被触发的时间
@Override
public void onTimer(long timestamp, OnTimerContext ctx, Collector<String> out) throws Exception {
System.out.println(timestamp);
out.collect("我被触发了....");
}
})
.print();
7.7.2 基于事件时间的定时器
在测试的时候, 脑子里面要想着: 时间进展依据的是watermark
SingleOutputStreamOperator<WaterSensor> stream = env
.socketTextStream("hadoop102", 9999) // 在socket终端只输入毫秒级别的时间戳
.map(value -> {
String[] datas = value.split(",");
return new WaterSensor(datas[0], Long.valueOf(datas[1]), Integer.valueOf(datas[2]));
});
WatermarkStrategy<WaterSensor> wms = WatermarkStrategy
.<WaterSensor>forBoundedOutOfOrderness(Duration.ofSeconds(3))
.withTimestampAssigner((element, recordTimestamp) -> element.getTs() * 1000);
stream
.assignTimestampsAndWatermarks(wms)
.keyBy(WaterSensor::getId)
.process(new KeyedProcessFunction<String, WaterSensor, String>() {
@Override
public void processElement(WaterSensor value, Context ctx, Collector<String> out) throws Exception {
System.out.println(ctx.timestamp());
ctx.timerService().registerEventTimeTimer(ctx.timestamp() + 5000);
out.collect(value.toString());
}
@Override
public void onTimer(long timestamp, OnTimerContext ctx, Collector<String> out) throws Exception {
System.out.println("定时器被触发.....");
}
})
.print();
7.7.3 定时器练习
// 监控水位传感器的水位值,如果水位值在五秒钟之内连续上升,则报警,并将报警信息输出到侧输出流。
package com.atguigu.flink.timer;
import com.atguigu.flink.source.WaterSensor;
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
import org.apache.flink.util.Collector;
import org.apache.flink.util.OutputTag;
public class warmingTest {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment()
.setParallelism(1);
SingleOutputStreamOperator<WaterSensor> waterSensorDS =
env.socketTextStream("hadoop102", 9999).map(new MapFunction<String, WaterSensor>() {
@Override
public WaterSensor map(String value) throws Exception {
String[] words = value.split(" ");
return new WaterSensor(words[0], Long.parseLong(words[1]), Integer.parseInt(words[2]));
}
})
.assignTimestampsAndWatermarks(
WatermarkStrategy
.<WaterSensor>forMonotonousTimestamps()
.withTimestampAssigner(new SerializableTimestampAssigner<WaterSensor>() {
@Override
public long extractTimestamp(WaterSensor element, long recordTimestamp) {
return element.getTs() * 1000L;
}
}));
// 按照传感器ID分组
KeyedStream<WaterSensor, String> keyedStream = waterSensorDS.keyBy(WaterSensor::getId);
// 是所有ProcessFunction实现5秒钟水位不下降则报警,且将报警信息输出侧输出流
SingleOutputStreamOperator<WaterSensor> result = keyedStream.process(new KeyedProcessFunction<String, WaterSensor, WaterSensor>() {
// 记录上一次水位高度
private Integer lastVc = Integer.MIN_VALUE;
// 用来记录定时器时间
private Long timerTs = Long.MIN_VALUE;
@Override
public void processElement(WaterSensor value, Context ctx, Collector<WaterSensor> out) throws Exception {
// 判断当前水位线是否高于上一次水位线
if (value.getVc() > lastVc) {
// 定时器是否重置,是否为第一条数据
if (timerTs == Long.MIN_VALUE) {
System.out.println("注册定时器...");
// 注册5秒之后的定时器
System.out.println(ctx.timestamp());
long timerTs = ctx.timestamp() + 5000L;
ctx.timerService().registerEventTimeTimer(timerTs);
}
} else {
// 如果水位线没有上升,则删除定时器
ctx.timerService().deleteEventTimeTimer(timerTs);
System.out.println("删除定时器...");
// 将定时器重置
timerTs = Long.MIN_VALUE;
}
// 最后更新最新的水位线
lastVc = value.getVc();
System.out.println(lastVc);
out.collect(value);
}
@Override
public void onTimer(long timestamp, OnTimerContext ctx, Collector<WaterSensor> out) throws Exception {
ctx.output(new OutputTag<String>("sideOut") {
}, ctx.getCurrentKey() + "报警!!!");
// 重置定时器时间
timerTs = Long.MIN_VALUE;
// 并且,重置lastVc
lastVc = Integer.MIN_VALUE;
}
});
result.print("主流");
result.getSideOutput(new OutputTag<String>("sideOut") {
}).print("报警信息");
env.execute();
}
}
7.8 Flink状态编程
有状态的计算是流处理框架要实现的重要功能,因为稍复杂的流处理场景都需要记录状态,然后在新流入数据的基础上不断更新状态。
SparkStreaming在状态管理这块做的不好, 很多时候需要借助于外部存储(例如Redis)来手动管理状态, 增加了编程的难度。
Flink的状态管理是它的优势之一。
7.8.1 什么是状态
在流式计算中有些操作一次处理一个独立的事件(比如解析一个事件), 有些操作却需要记住多个事件的信息(比如窗口操作)。
那些需要记住多个事件信息的操作就是有状态的。流式计算分为无状态计算和有状态计算两种情况。
- 无状态的计算观察每个独立事件,并根据最后一个事件输出结果。例如,流处理应用程序从传感器接收水位数据,并在水位超过指定高度时发出警告。
- 有状态的计算则会基于多个事件输出结果。以下是一些例子。例如,计算过去一小时的平均水位,就是有状态的计算。所有用于复杂事件处理的状态机。例如,若在一分钟内收到两个相差20cm以上的水位差读数,则发出警告,这是有状态的计算。流与流之间的所有关联操作,以及流与静态表或动态表之间的关联操作,都是有状态的计算。
7.8.2 为什么需要管理状态
下面的几个场景都需要使用流处理的状态功能:
- 去重:数据流中的数据有重复,我们想对重复数据去重,需要记录哪些数据已经流入过应用,当新数据流入时,根据已流入过的数据来判断去重。
- 检测:检查输入流是否符合某个特定的模式,需要将之前流入的元素以状态的形式缓存下来。比如,判断一个水位传感器数据流中的温度是否在持续上升。
- 聚合:对一个时间窗口内的数据进行聚合分析,分析一个小时内水位的情况。
- 更新机器学习模型:在线机器学习场景下,需要根据新流入数据不断更新机器学习的模型参数。
7.8.3 Flink中的状态分类
Flink包括两种基本类型的状态 Managed State 和 Raw State
Managed State | Raw State(了解) | |
状态管理方式 | Flink Runtime托管, 自动存储, 自动恢复, 自动伸缩 | 用户自己管理 |
状态数据结构 | Flink提供多种常用数据结构, 例如: ListState, MapState等 | 字节数组: byte[] |
使用场景 | 绝大数Flink算子 | 所有算子 |
注意:
- 从具体使用场景来说,绝大多数的算子都可以通过继承Rich函数类或其他提供好的接口类,在里面使用Managed State。Raw State一般是在已有算子和Managed State不够用时,用户自定义算子时使用。
- 在我们平时的使用中Managed State已经足够我们使用, 下面重点学习Managed State
7.8.4 Managed State 分类
对Managed State继续细分,它又有两种类型:Keyed State(键控状态) 和 Operator State(算子状态)。
Operator State | Keyed State | |
适用用算子类型 | 可用于所有算子: 常用于source, 例如 FlinkKafkaConsumer | 只适用于KeyedStream上的算子 |
状态分配 | 一个算子的子任务对应一个状态 | 一个Key对应一个State: 一个算子会处理多个Key, 则访问相应的多个State |
创建和访问方式 | 实现CheckpointedFunction或ListCheckpointed(已经过时)接口 | 重写RichFunction, 通过里面的RuntimeContext访问 |
横向扩展 | 并行度改变时有多种重新分配方式可选: 均匀分配和合并后每个得到全量 | 并发改变, State随着Key在实例间迁移 |
支持的数据结构 | ListState和BroadCastState | ValueState, ListState,MapState ReduceState, AggregatingState |
7.8.5 键控状态的使用
键控状态是根据输入数据流中定义的键(key)来维护和访问的。
Flink为每个键值维护一个状态实例,并将具有相同键的所有数据,都分区到同一个算子任务中,这个任务会维护和处理这个key对应的状态。当任务处理一条数据时,它会自动将状态的访问范围限定为当前数据的key。因此,具有相同key的所有数据都会访问相同的状态。
Keyed State很类似于一个分布式的key-value map数据结构,只能用于KeyedStream(keyBy算子处理之后)。
键控状态支持的数据类型
ValueState<T>
保存单个值. 每个key有一个状态值. 设置使用 update(T), 获取使用 T value()
ListState<T>:
保存元素列表.
添加元素: add(T) addAll(List<T>)
获取元素: Iterable<T> get()
覆盖所有元素: update(List<T>)
ReducingState<T>:
存储单个值, 表示把所有元素的聚合结果添加到状态中. 与ListState类似, 但是当使用add(T)的时候ReducingState会使用指定的ReduceFunction进行聚合。
AggregatingState<IN, OUT>:
存储单个值. 与ReducingState类似, 都是进行聚合. 不同的是, AggregatingState的聚合的结果和元素类型可以不一样。
MapState<UK, UV>:
存储键值对列表.
添加键值对: put(UK, UV) or putAll(Map<UK, UV>)
根据key获取值: get(UK)
获取所有: entries(), keys() and values()
检测是否为空: isEmpty()
注意:
- 所有的类型都有clear(), 清空当前key的状态
- 这些状态对象仅用于用户与状态进行交互
- 状态不是必须存储到内存, 也可以存储在磁盘或者任意其他地方
- 从状态获取的值与输入元素的key相关
// Test01 ValueState
检测传感器的水位线值,如果连续的两个水位线差值超过10,就输出报警。
package com.atguigu.flink.state;
import com.atguigu.flink.source.WaterSensor;
import org.apache.flink.api.common.state.ValueState;
import org.apache.flink.api.common.state.ValueStateDescriptor;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
import org.apache.flink.util.Collector;
public class Test01_Keyed_Value {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment
.getExecutionEnvironment()
.setParallelism(3);
env
.socketTextStream("hadoop102", 9999)
.map(value -> {
String[] datas = value.split(",");
return new WaterSensor(datas[0], Long.valueOf(datas[1]), Integer.valueOf(datas[2]));
})
.keyBy(WaterSensor::getId)
.process(new KeyedProcessFunction<String, WaterSensor, String>() {
private ValueState<Integer> state;
@Override
public void processElement(WaterSensor value, Context ctx, Collector<String> out) throws Exception {
Integer lastVc = state.value() == null ? 0 : state.value();
if (Math.abs(value.getVc() - lastVc) >= 10) {
out.collect(value.getId() + "红色警报!!!");
state.update(value.getVc());
}
}
})
.print();
env.execute();
}
}
// Test02 ListState
针对每个传感器输出最高的3个水位值
package com.atguigu.flink.state;
import com.atguigu.flink.source.WaterSensor;
import org.apache.flink.api.common.state.ListState;
import org.apache.flink.api.common.state.ListStateDescriptor;
import org.apache.flink.api.common.state.ValueState;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
import org.apache.flink.util.Collector;
import java.util.ArrayList;
import java.util.List;
public class Test02_Keyed_ListState {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment
.getExecutionEnvironment()
.setParallelism(3);
env
.socketTextStream("hadoop102", 9999)
.map(value -> {
String[] datas = value.split(",");
return new WaterSensor(datas[0], Long.valueOf(datas[1]), Integer.valueOf(datas[2]));
})
.keyBy(WaterSensor::getId)
.process(new KeyedProcessFunction<String, WaterSensor, List<Integer>>() {
private ListState<Integer> vcState;
@Override
public void open(Configuration parameters) throws Exception {
getRuntimeContext().getListState(new ListStateDescriptor<Integer>("vcState", Integer.class));
}
@Override
public void processElement(WaterSensor value, Context ctx, Collector<List<Integer>> out) throws Exception {
vcState.add(value.getVc());
// 1 获取状态中所有水位高度,并排序
List<Integer> vcs = new ArrayList<>();
for (Integer vc: vcState.get()) {
vcs.add(vc);
}
// 2 降序排列
vcs.sort((o1, o2) -> o1 - o2);
// 3 当长度超过3的时候移除最后一个
if (vcs.size() > 3) {
vcs.remove(3);
}
vcState.update(vcs);
out.collect(vcs);
}
})
.print();
env.execute();
}
}
// Test03_ReducingState
计算每个传感器的水位和
package com.atguigu.flink.state;
import com.atguigu.flink.source.WaterSensor;
import org.apache.flink.api.common.functions.ReduceFunction;
import org.apache.flink.api.common.state.ListState;
import org.apache.flink.api.common.state.ListStateDescriptor;
import org.apache.flink.api.common.state.ReducingState;
import org.apache.flink.api.common.state.ReducingStateDescriptor;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
import org.apache.flink.util.Collector;
import java.util.ArrayList;
import java.util.List;
public class Test03_ReducingState {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment
.getExecutionEnvironment()
.setParallelism(3);
env
.socketTextStream("hadoop102", 9999)
.map(value -> {
String[] datas = value.split(",");
return new WaterSensor(datas[0], Long.valueOf(datas[1]), Integer.valueOf(datas[2]));
})
.keyBy(WaterSensor::getId)
.process(new KeyedProcessFunction<String, WaterSensor, WaterSensor>() {
//定义状态
private ReducingState<WaterSensor> reducingState;
//在声明周期open方法中初始化状态
@Override
public void open(Configuration parameters) throws Exception {
reducingState = getRuntimeContext().getReducingState(new ReducingStateDescriptor<WaterSensor>("reducing-state", new ReduceFunction<WaterSensor>() {
@Override
public WaterSensor reduce(WaterSensor value1, WaterSensor value2) throws Exception {
return new WaterSensor(value1.getId(), value2.getTs(), value1.getVc() + value2.getVc());
}
}, WaterSensor.class));
}
@Override
public void processElement(WaterSensor value, Context ctx, Collector<WaterSensor> out) throws Exception {
//将当前数据聚合进状态
reducingState.add(value);
//取出状态中的数据
WaterSensor waterSensor = reducingState.get();
//输出数据
out.collect(waterSensor);
}
})
.print();
env.execute();
}
}
// Test04 AggregatingState
计算每个传感器的平均水位
package com.atguigu.flink.state;
import com.atguigu.flink.source.WaterSensor;
import org.apache.flink.api.common.functions.ReduceFunction;
import org.apache.flink.api.common.state.ListState;
import org.apache.flink.api.common.state.ListStateDescriptor;
import org.apache.flink.api.common.state.ReducingState;
import org.apache.flink.api.common.state.ReducingStateDescriptor;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
import org.apache.flink.util.Collector;
import java.util.ArrayList;
import java.util.List;
public class Test04_ReducingState {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment
.getExecutionEnvironment()
.setParallelism(3);
env
.socketTextStream("hadoop102", 9999)
.map(value -> {
String[] datas = value.split(",");
return new WaterSensor(datas[0], Long.valueOf(datas[1]), Integer.valueOf(datas[2]));
})
.keyBy(WaterSensor::getId)
.process(new KeyedProcessFunction<String, WaterSensor, WaterSensor>() {
//定义状态
private ReducingState<WaterSensor> reducingState;
//在声明周期open方法中初始化状态
@Override
public void open(Configuration parameters) throws Exception {
reducingState = getRuntimeContext().getReducingState(new ReducingStateDescriptor<WaterSensor>("reducing-state", new ReduceFunction<WaterSensor>() {
@Override
public WaterSensor reduce(WaterSensor value1, WaterSensor value2) throws Exception {
return new WaterSensor(value1.getId(), value2.getTs(), value1.getVc() + value2.getVc());
}
}, WaterSensor.class));
}
@Override
public void processElement(WaterSensor value, Context ctx, Collector<WaterSensor> out) throws Exception {
//将当前数据聚合进状态
reducingState.add(value);
//取出状态中的数据
WaterSensor waterSensor = reducingState.get();
//输出数据
out.collect(waterSensor);
}
})
.print();
env.execute();
}
}
// Test05 MapState
去重: 去掉重复的水位值. 思路: 把水位值作为MapState的key来实现去重, value随意
package com.atguigu.flink.state;
import com.atguigu.flink.source.WaterSensor;
import org.apache.flink.api.common.functions.ReduceFunction;
import org.apache.flink.api.common.state.MapState;
import org.apache.flink.api.common.state.MapStateDescriptor;
import org.apache.flink.api.common.state.ReducingState;
import org.apache.flink.api.common.state.ReducingStateDescriptor;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
import org.apache.flink.util.Collector;
public class Test05_MapState {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment
.getExecutionEnvironment()
.setParallelism(3);
env
.socketTextStream("hadoop102", 9999)
.map(value -> {
String[] datas = value.split(",");
return new WaterSensor(datas[0], Long.valueOf(datas[1]), Integer.valueOf(datas[2]));
})
.keyBy(WaterSensor::getId)
.process(new KeyedProcessFunction<String, WaterSensor, WaterSensor>() {
// 定义状态map中的key为水位,因为要对相同的水位做去重,value为具体的数值
private MapState<Integer, WaterSensor> mapState;
// 在声明周期open方法中初始化状态
@Override
public void open(Configuration parameters) throws Exception {
mapState = getRuntimeContext().getMapState(new MapStateDescriptor<Integer, WaterSensor>("map" +
"-state", Integer.class, WaterSensor.class));
}
@Override
public void processElement(WaterSensor value, Context ctx, Collector<WaterSensor> out) throws Exception {
// 判断mapState中不包含相同的水位,则输出
if (!mapState.contains(value.getVc())) {
out.collect(value);
// 更新状态
mapState.put(value.getVc(), value);
}
}
})
.print();
env.execute();
}
}
// Test06 ChangingRequirement
监控水位传感器的水位值,如果水位值在五秒钟之内(processing time)连续上升,则报警
package com.atguigu.flink.state;
import com.atguigu.flink.source.WaterSensor;
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.state.MapState;
import org.apache.flink.api.common.state.MapStateDescriptor;
import org.apache.flink.api.common.state.ValueState;
import org.apache.flink.api.common.state.ValueStateDescriptor;
import org.apache.flink.api.common.typeinfo.Types;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
import org.apache.flink.util.Collector;
import org.apache.flink.util.OutputTag;
public class Test06_Warming {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment
.getExecutionEnvironment()
.setParallelism(3);
SingleOutputStreamOperator<WaterSensor> waterSensorDS = env
.socketTextStream("hadoop102", 9999)
.map(value -> {
String[] datas = value.split(",");
return new WaterSensor(datas[0], Long.valueOf(datas[1]), Integer.valueOf(datas[2]));
})
.assignTimestampsAndWatermarks(
WatermarkStrategy
.<WaterSensor>forMonotonousTimestamps()
.withTimestampAssigner(new SerializableTimestampAssigner<WaterSensor>() {
@Override
public long extractTimestamp(WaterSensor element, long recordTimestamp) {
return element.getTs() * 1000L;
}
}
)
);
// 按照传感器ID分组
KeyedStream<WaterSensor, String> keyedStream = waterSensorDS.keyBy(WaterSensor::getId);
// 使用ProcessFunction实现5秒水位不下降,则报警,且报警信息输出到侧流
SingleOutputStreamOperator<WaterSensor> result = keyedStream.process(new KeyedProcessFunction<String, WaterSensor, WaterSensor>() {
// 定义状态
private ValueState<Integer> vcState;
private ValueState<Long> tsState;
// 初始化状态
@Override
public void open(Configuration parameters) throws Exception {
vcState = getRuntimeContext().getState(new ValueStateDescriptor<Integer>("vcState", Types.INT,
Integer.MIN_VALUE));
tsState = getRuntimeContext().getState(new ValueStateDescriptor<Long>("tsState", Types.LONG));
}
@Override
public void processElement(WaterSensor value, Context ctx, Collector<WaterSensor> out) throws Exception {
// 判断当前水位线是否高于上次水位线
if (value.getVc() > vcState.value()) {
// 判断定时器是否重置,是否为第一条数据
if (tsState.value() == null) {
System.out.println("waterMarks = " + ctx.timerService().currentWatermark());
System.out.println("注册定时器... " + (ctx.timestamp() + 5000L));
// 注册5秒之后的定时器
ctx.timerService().registerEventTimeTimer(ctx.timestamp() + 5000L);
tsState.update(ctx.timestamp() + 5000L);
}
} else {
// 如果水位线没有上升则删除定时器
System.out.println("删除定时器... " + tsState.value());
ctx.timerService().deleteEventTimeTimer(tsState.value());
// 重新注册5秒后的定时器
System.out.println("重新注册定时器... " + (ctx.timestamp() + 5000L));
ctx.timerService().registerEventTimeTimer(ctx.timestamp() + 5000L);
// 将定时器的时间重置
tsState.clear();
}
// 最后更新最新的水位线
vcState.update(value.getVc());
System.out.println("水位状态:= " + vcState.value());
out.collect(value);
}
@Override
public void onTimer(long timestamp, OnTimerContext ctx, Collector<WaterSensor> out) throws Exception {
System.out.println("删除定时器... " + tsState.value());
tsState.clear();
ctx.output(new OutputTag<String>("sideOut") {
}, "传感器 = " + ctx.getCurrentKey() + "在 ts = " + timestamp + "报警!!!");
}
});
result.print("主流>>>");
result.getSideOutput(new OutputTag<String>("sideOut"){}).print("报警信息>>>");
env.execute();
}
}
7.8.6 算子状态的使用
Operator State 可以用在所有算子上,每个算子子任务或者说每个算子实例分别维护一个状态,流入这个算子子任务的数据可以访问和更新这个状态。
注意:算子子任务之间的状态不能互相访问
Operator State 的实际应用场景不如 Keyed State 多,它经常被用在 Source 或 Sink 等算子上,用来保存流入数据的偏移量或对输出数据做缓存,以保证Flink应用的Exactly-Once语义。
Flink为算子状态提供三种基本数据结构:
列表状态(List state)
将状态表示为一组数据的列表
联合列表状态(Union list state)
也将状态表示为数据的列表。它与常规列表状态的区别在于,在发生故障时,或者从保存点(savepoint)启动应用程序时如何恢复。
一种是均匀分配(List state),另外一种是将所有 State 合并为全量 State 再分发给每个实例(Union list state)。
广播状态(Broadcast state)
是一种特殊的算子状态. 如果一个算子有多项任务,而它的每项任务状态又都相同,那么这种特殊情况最适合应用广播状态。
// 列表状态
package com.atguigu.flink.state;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.common.state.ListState;
import org.apache.flink.api.common.state.ListStateDescriptor;
import org.apache.flink.runtime.state.FunctionInitializationContext;
import org.apache.flink.runtime.state.FunctionSnapshotContext;
import org.apache.flink.streaming.api.checkpoint.CheckpointedFunction;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
public class Test07_State_Operator {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment()
.setParallelism(1);
env
.socketTextStream("hadoop102", 9999)
.map(new MyCountMapper())
.print();
env.execute();
}
private static class MyCountMapper implements MapFunction<String, Long>, CheckpointedFunction {
private Long count = 0L;
private ListState<Long> state;
@Override
public Long map(String value) throws Exception {
count++;
return count;
}
// Checkpoint时会调用这个方法,我们要实现具体的snapshot逻辑,比如将那些本地状态持久化
@Override
public void snapshotState(FunctionSnapshotContext context) throws Exception {
System.out.println("snapshotState...");
state.clear();
state.add(count);
}
// 初始化时会调用这个方法,向本地状态中填充数据,每个子任务调用一次
@Override
public void initializeState(FunctionInitializationContext context) throws Exception {
System.out.println("initializaState...");
state = context
.getOperatorStateStore()
.getListState(new ListStateDescriptor<Long>("state", Long.class));
for (Long c: state.get()) {
count += c;
}
}
}
}
广播状态
从版本 1.5.0 开始,Apache Flink具有一种新的状态,称为广播状态*。
广播状态被引入以支持这样的用例:来自一个流的一些数据需要广播到所有下游任务,在那里它被本地存储,并用于处理另一个流上的所有传入元素。作为广播状态自然适合出现的一个例子,我们可以想象一个低吞吐量流,其中包含一组规则,我们希望根据来自另一个流的所有元素对这些规则进行评估。考虑到上述类型的用例,广播状态与其他操作符状态的区别在于:
- 它是一个map格式。
- 它只对输入有广播流和无广播流的特定操作符可用。
- 这样的操作符可以具有不同名称的多个广播状态。
package com.atguigu.flink.state;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.common.state.*;
import org.apache.flink.runtime.state.FunctionInitializationContext;
import org.apache.flink.runtime.state.FunctionSnapshotContext;
import org.apache.flink.streaming.api.checkpoint.CheckpointedFunction;
import org.apache.flink.streaming.api.datastream.BroadcastStream;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.co.BroadcastProcessFunction;
import org.apache.flink.util.Collector;
public class Test08_State_Operator_Broad {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment()
.setParallelism(3);
DataStreamSource<String> dataStream = env.socketTextStream("hadoop102", 9999);
DataStreamSource<String> controalStream = env.socketTextStream("hadoop102", 8888);
// 定义状态并广播
MapStateDescriptor<String, String> stateDescriptor = new MapStateDescriptor<>("state", String.class, String.class);
// 广播流
BroadcastStream<String> broadcastStream = controalStream.broadcast(stateDescriptor);
dataStream
.connect(broadcastStream)
.process(new BroadcastProcessFunction<String, String, String>() {
@Override
public void processElement(String value, ReadOnlyContext ctx, Collector<String> out) throws Exception {
// 从广播状态中取值,不同的值做不同的业务
ReadOnlyBroadcastState<String, String> state = ctx.getBroadcastState(stateDescriptor);
if ("1".equals(state.get("switch"))) {
out.collect("切换到1号配置...");
} else if ("0".equals(state.get("switch"))) {
out.collect("切换到2号配置...");
} else {
out.collect("切换到其他配置...");
}
}
@Override
public void processBroadcastElement(String value, Context ctx, Collector<String> out) throws Exception {
// 提取状态
BroadcastState<String, String> state = ctx.getBroadcastState(stateDescriptor);
// 把值放入广播状态
state.put("switch", value);
}
})
.print();
env.execute();
}
}
7.8.7 状态后端
每传入一条数据,有状态的算子任务都会读取和更新状态。由于有效的状态访问对于处理数据的低延迟至关重要,因此每个并行任务(子任务)都会在本地维护其状态,以确保快速的状态访问。
状态的存储、访问以及维护,由一个可插入的组件决定,这个组件就叫做状态后端(state backend)
状态后端主要负责两件事:
- 本地的状态管理
- 将检查点(checkpoint)状态写入远程存储
状态后端的分类:
状态后端作为一个可插入的组件, 没有固定的配置, 我们可以根据需要选择一个合适的状态后端。Flink提供了3中状态后端:
MemoryStateBackend 内存级别的状态后端,
**存储方式:**本地状态存储在TaskManager的内存中,checkpoint 存储在 JobManager 的内存中。
**特点:**快速,低延迟,但不稳定
使用场景:
- 本地测试
- 几乎无状态的作业(ETL)
- JobManager不容易挂,或者挂了影响不大
- 不推荐在生产环境下使用
FsStateBackend
**存储方式:**本地状态在 TaskManager 内存,Checkpoint存储在文件系统中
**特点:**拥有内存级别的本地访问速度,和更好的容错保证
使用场景:
- 常规使用状态的作业。例如分钟级别窗口聚合,join等
- 需要开启HA的作业
- 可以应用在生产环境中
RocksDBStateBackend
将所有的状态序列化之后,存入本地的 RocksDB 数据库中。(一种 NoSql 数据库,KV 形式存储)
存储方式:
- 本地状态存储在 TaskManager 的 RocksDB 数据库中(实际是内存+磁盘)
- Checkpoint 在外部文件系统中
使用场景:
- 超大状态的作业,例如天级的窗口聚合
- 需要开启HA的作业
- 对读写状态性能要求不高的作业
- 可以使用在生产环境
配置状态后端
# 全局配置状态后端
在flink-conf.yaml文件中设置默认的全局后端
# Optional, Flink will automatically default to FileSystemCheckpointStorage
# when a checkpoint directory is specified.
# Memory
state.backend: hashmap
state.checkpoint-storage: jobmanager
# Fs
state.backend: hashmap
state.checkpoints.dir: file:///checkpoint-dir/
state.checkpoint-storage: filesystem
# RockDB
state.backend: rocksdb
state.checkpoints.dir: file:///checkpoint-dir/
state.checkpoint-storage: filesystem
// 在代码中配置状态后端
可以在代码中单独为这个Job设置状态后端.
// 1.12版本设置方法
env.setStateBackend(new MemoryStateBackend());
env.setStateBackend(new FsStateBackend("hdfs://hadoop102:8020/flink/checkpoints/fs"));
// 如何要使用RocksDBBackend, 需要先引入依赖:
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-statebackend-rocksdb_${scala.binary.version}</artifactId>
<version>${flink.version}</version>
<scope>provided</scope>
</dependency>
env.setStateBackend(new RocksDBStateBackend("hdfs://hadoop102:8020/flink/checkpoints/rocksdb"));
// 1.13 版本设置方法
// Memory
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setStateBackend(new HashMapStateBackend());
env.getCheckpointConfig().setCheckpointStorage(new JobManagerCheckpointStorage
());
// Fs
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setStateBackend(new HashMapStateBackend());
env.getCheckpointConfig().setCheckpointStorage("file:///checkpoint-dir");
// Advanced FsStateBackend configurations, such as write buffer size
// can be set by manually instantiating a FileSystemCheckpointStorage object.
env.getCheckpointConfig().setCheckpointStorage(new FileSystemCheckpointStorage("file:///checkpoint-dir"));
// RocksDB
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setStateBackend(new EmbeddedRocksDBStateBackend());
env.getCheckpointConfig().setCheckpointStorage("file:///checkpoint-dir");
// If you manually passed FsStateBackend into the RocksDBStateBackend constructor
// to specify advanced checkpointing configurations such as write buffer size,
// you can achieve the same results by using manually instantiating a FileSystemCheckpointStorage object.
env.getCheckpointConfig().setCheckpointSt2orage(new FileSystemCheckpointStorage("file:///checkpoint-dir"));
7.9 Flink的容错机制
7.9.1 状态的一致性
当在分布式系统中引入状态做 checkpoint 时,自然也引入了一致性问题。
一致性实际上是 “正确性级别” 的另一种说法,也就是说在成功处理故障并恢复之后得到的结果,与没有发生任何故障时得到的结果相比,前者到底有多正确?举例来说,假设要对最近一小时登录的用户计数。在系统经历故障之后,计数结果是多少?如果有偏差,是有漏掉的计数还是重复计数?
一致性级别
在流处理中,一致性可以分为3个级别:
at-most-once(最多变一次): 这其实是没有正确性保障的委婉说法——故障发生之后,计数结果可能丢失。
at-least-once(至少一次): 这表示计数结果可能大于正确值,但绝不会小于正确值。也就是说,计数程序在发生故障后可能多算,但是绝不会少算。
**exactly-once(严格变一次)😗*这指的是系统保证在发生故障后得到的计数结果与正确值一致.既不多算也不少算。
曾经,at-least-once 非常流行。第一代流处理器(如Storm和Samza)刚问世时只保证at-least-once,原因有二:
- 保证 exactly-once 的系统实现起来更复杂。这在基础架构层(决定什么代表正确,以及 exactly-once 的范围是什么)和实现层都很有挑战性
- 流处理系统的早期用户愿意接受框架的局限性,并在应用层想办法弥补(例如使应用程序具有幂等性,或者用批量计算层再做一遍计算)。
最先保证 exactly-once 的系统(Storm Trident 和 Spark Streaming)在性能和表现力这两个方面付出了很大的代价。为了保证 exactly-once,这些系统无法单独地对每条记录运用应用逻辑,而是同时处理多条(一批)记录,保证对每一批的处理要么全部成功,要么全部失败。这就导致在得到结果前,必须等待一批记录处理结束。因此,用户经常不得不使用两个流处理框架(一个用来保证 exactly-once,另一个用来对每个元素做低延迟处理),结果使基础设施更加复杂。曾经,用户不得不在保证 exactly-once 与获得低延迟和效率之间权衡利弊。Flink 避免了这种权衡。
Flink的一个重大价值在于,它既保证了 exactly-once,又具有低延迟和高吞吐的处理能力。
从根本上说,Flink 通过使自身满足所有需求来避免权衡,它是业界的一次意义重大的技术飞跃。尽管这在外行看来很神奇,但是一旦了解,就会恍然大悟。
端到端的状态一致性
目前我们看到的一致性保证都是由流处理器实现的,也就是说都是在 Flink 流处理器内部保证的;而在真实应用中,流处理应用除了流处理器以外还包含了数据源(例如 Kafka)和输出到持久化系统。
端到端的一致性保证,意味着结果的正确性贯穿了整个流处理应用的始终;每一个组件都保证了它自己的一致性,整个端到端的一致性级别取决于所有组件中一致性最弱的组件。
具体划分如下:
source端:需要外部源可重设数据的读取位置。目前我们使用的 Kafka Source 具有这种特性:读取数据的时候可以指定offset。
flink内部:依赖checkpoint机制
sink端:需要保证从故障恢复时,数据不会重复写入外部系统. 有2种实现形式:
- 幂等(Idempotent)写入:所谓幂等操作,是说一个操作,可以重复执行很多次,但只导致一次结果更改,也就是说,后面再重复执行就不起作用了。
- 事务性(Transactional)写入:需要构建事务来写入外部系统,构建的事务对应着 checkpoint,等到 checkpoint 真正完成的时候,才把所有对应的结果写入 sink 系统中。对于事务性写入,具体又有两种实现方式:预写日志(WAL)和两阶段提交(2PC)
7.9.2 Checkpoint原理
Flink使用一种被称为"检查点"(checkpoint)的特性,在出现故障时将系统重置回正确状态,从而保证exactly-once。下面通过简单的类比来解释检查点的作用。
假设你和两位朋友正在数项链上有多少颗珠子,如下图所示。你捏住珠子,边数边拨,每拨过一颗珠子就给总数加一。你的朋友也这样数他们手中的珠子。当你分神忘记数到哪里时,怎么办呢? 如果项链上有很多珠子,你显然不想从头再数一遍,尤其是当三人的速度不一样却又试图合作的时候,更是如此(比如想记录前一分钟三人一共数了多少颗珠子,回想一下一分钟滚动窗口)。
于是,你想了一个更好的办法:在项链上每隔一段就松松地系上一根有色皮筋,将珠子分隔开;当珠子被拨动的时候,皮筋也可以被拨动;然后,你安排一个助手,让他在你和朋友拨到皮筋时记录总数。用这种方法,当有人数错时,就不必从头开始数。相反,你向其他人发出错误警示,然后你们都从上一根皮筋处开始重数,助手则会告诉每个人重数时的起始数值,例如在粉色皮筋处的数值是多少。
Flink 检查点的作用就类似于皮筋标记。数珠子这个类比的关键点是:对于指定的皮筋而言,珠子的相对位置是确定的;这让皮筋成为重新计数的参考点。总状态(珠子的总数)在每颗珠子被拨动之后更新一次,助手则会保存与每根皮筋对应的检查点状态,如当遇到粉色皮筋时一共数了多少珠子,当遇到橙色皮筋时又是多少。当问题出现时,这种方法使得重新计数变得简单。
7.9.2.1 Flink检查点算法
checkpoint 机制是 Flink 可靠性的基石,可以保证 Flink 集群在某个算子因为某些原因(如异常退出)出现故障时,能够将整个应用流图的状态恢复到故障之前的某一状态,保证应用流图状态的一致性.
快照的实现算法:
- 简单算法 – 暂停应用, 然后开始做检查点, 再重新恢复应用
- Flink 的改进 Checkpoint 算法。Flink 的 checkpoint 机制原理来自 “Chandy-Lamport algorithm” 算法(分布式快照算法)的一种变体:异步 barrier 快照(asynchronous barrier snapshotting)
每个需要 checkpoint 的应用在启动时,Flink 的 JobManager为其创建一个 CheckpointCoordinator,CheckpointCoordinator 全权负责本应用的快照制作。
7.9.2.2 理解Barrier
流的 barrier 是 Flink 的 Checkpoint 中的一个核心概念。多个 barrier 被插入到数据流中,然后作为数据流的一部分随着数据流动(有点类似于 Watermark)。这些 barrier 不会跨越流中的数据。
每个 barrier 会把数据流分成两部分:一部分数据进入当前的快照,另一部分数据进入下一个快照。每个 barrier 携带着快照的 id。barrier 不会暂停数据的流动, 所以非常轻量级。在流中,同一时间可以有来源于多个不同快照的多个 barrier,这个意味着可以并发的出现不同的快照。
7.9.2.3 Flink检查点的制作过程
第一步:Checkpoint Coordinator 向所有 source 节点 trigger Checkpoint。然后 Source Task 会在数据流中安插 CheckPoint barrier
第二步: source 节点向下游广播 barrier,这个 barrier 就是实现 Chandy-Lamport 分布式快照算法的核心,下游的 task 只有收到所有 input 的 barrier 才会执行相应的 Checkpoint
第三步: 当 task 完成 state 备份后,会将备份数据的地址(state handle)通知给 Checkpoint coordinator。
第四步: 下游的 sink 节点收集齐上游两个 input 的 barrier 之后,会执行本地快照,这里特地展示了 RocksDB incremental Checkpoint 的流程,首先 RocksDB 会全量刷数据到磁盘上(红色大三角表示),然后 Flink 框架会从中选择没有上传的文件进行持久化备份(紫色小三角)。
第五步: 同样的,sink 节点在完成自己的 Checkpoint 之后,会将 state handle 返回通知 Coordinator。
第六步: 最后,当 Checkpoint coordinator 收集齐所有 task 的 state handle,就认为这一次的 Checkpoint 全局完成了,向持久化存储中再备份一个 Checkpoint meta 文件。
7.9.2.4 严格一次语义:barrier对齐
在多并行度下, 如果要实现严格一次, 则要执行barrier对齐。
当 job graph 中的每个 operator 接收到 barriers 时,它就会记录下其状态。拥有两个输入流的 Operators(例如 CoProcessFunction)会执行 barrier 对齐(barrier alignment)以便当前快照能够包含消费两个输入流 barrier 之前(但不超过)的所有 events 而产生的状态。
https://ci.apache.org/projects/flink/flink-docs-release-1.12/fig/stream_aligning.svg
- 当 operator 收到数字流的 barrier n 时, 它就**不能处理(但是可以接收)**来自该流的任何数据记录,直到它从字母流所有输入接收到 barrier n 为止。否则,它会混合属于快照 n 的记录和属于快照 n + 1 的记录。
- 接收到 barrier n 的流(数字流)暂时被搁置。从这些流接收的记录入输入缓冲区, 不会被处理。
- 图一中的 Checkpoint barrier n 之后的数据 123 已结到达了算子, 存入到输入缓冲区没有被处理, 只有等到字母流的 Checkpoint barrier n 到达之后才会开始处理。
- 一旦最后所有输入流都接收到 barrier n,Operator 就会把缓冲区中 pending 的输出数据发出去,然后把 CheckPoint barrier n 接着往下游发送。这里还会对自身进行快照。
7.9.2.5 至少一次语义:barrier不对齐
barrier不对齐时,会重复消费, 就是至少一次语义。
假设不对齐, 在字母流的 Checkpoint barrier n 到达前, 已经处理了1 2 3 … 等字母流 Checkpoint barrier n 到达之后,会做 Checkpoint n。假设这个时候程序异常错误了,则重新启动的时候会 Checkpoint n 之后的数据重新计算。1 2 3 会被再次被计算, 所以123出现了重复计算。
Unaligned Checkpointing
未对齐的检查点确保障碍尽可能快地到达接收器。它特别适用于具有至少一个缓慢移动数据路径的应用程序,其中对齐时间可能达到数小时。
// 设置不对齐Checkpoint
env.getCheckpointConfig().enableUnalignedCheckpoints();
7.9.3 Savepoint原理
- Flink 还提供了可以自定义的镜像保存功能,就是保存点(savepoints)
- 原则上,创建保存点使用的算法与检查点完全相同,因此保存点可以认为就是具有一些额外元数据的检查点
- Flink不会自动创建保存点,因此用户(或外部调度程序)必须明确地触发创建操作
- 保存点是一个强大的功能。除了故障恢复外,保存点可以用于:有计划的手动备份,更新应用程序,版本迁移,暂停和重启应用,等等
7.9.4 Checkpoint 和 Savepoint 的区别
Savepoint | Checkpoint |
Savepoint是由命令触发, 由用户创建和删除 | Checkpoint被保存在用户指定的外部路径中。 |
保存点存储在标准格式存储中,并且可以升级作业版本并可以更改其配置。 | 当作业失败或被取消时,将保留外部存储的检查点。 |
用户必须提供用于还原作业状态的保存点的路径。 | 用户必须提供用于还原作业状态的检查点的路径。 |
7.9.5 在代码中测试Checkpoint
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>3.1.3</version>
</dependency>
package com.atguigu.flink.checkpoint;
import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.runtime.state.filesystem.FsStateBackend;
import org.apache.flink.streaming.api.CheckpointingMode;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;
public class Test01_Checkpoint {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 设置用户权限
System.setProperty("HADOOP_USER_NAME", "atguigu");
// 设置状态后端
env.setStateBackend(new FsStateBackend("hdfs://hadoop102:8020/flink/ck"));
// 开启CK
// 每5000ms开始一次checkpoint
env.enableCheckpointing(5000);
// 设置格式为精确一次(这是默认值)
env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);
// 读取端口并转换为元组
SingleOutputStreamOperator<Tuple2<String, Long>> wordToOneDStream = env.socketTextStream("hadoop102", 9999)
.flatMap(new FlatMapFunction<String, Tuple2<String, Long>>() {
@Override
public void flatMap(String value, Collector<Tuple2<String, Long>> out) throws Exception {
String[] words = value.split(" ");
for (String word: words
) {
out.collect(Tuple2.of(word, 1L));
}
}
});
// 按单词分组
KeyedStream<Tuple2<String, Long>, String> keyedStream = wordToOneDStream.keyBy(r -> r.f0);
// 累加计算
SingleOutputStreamOperator<Tuple2<String, Long>> result = keyedStream.sum(1);
result.print();
env.execute();
}
}
# 从SavePoint和CK恢复任务步骤:
# 启动任务
bin/flink run -d -m hadoop102:8081 -c com.atguigu.day06.Flink10_SavePoint ./flink-0108-1.0-SNAPSHOT.jar
# 保存点(只能手动)
bin/flink savepoint -m hadoop102:8081 JobId hdfs://hadoop102:8020/flink/save
# 关闭任务并从保存点恢复任务
bin/flink run -s hdfs://hadoop102:8020/flink/save/... -m hadoop102:8081 -c com.atguigu.WordCount xxx.jar
# 从CK位置恢复数据,在代码中开启cancel的时候不会删除checkpoint信息这样就可以根据checkpoint来回复数据了
env.getCheckpointConfig().enableExternalizedCheckpoints(CheckpointConfig.ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION);
# 指定恢复的地址
bin/flink run -s hdfs://hadoop102:8020/flink/ck/Jobid/chk-960 -m hadoop102:8081 -c com.atguigu.WordCount xxx.jar
7.9.6 Flink + Kafka 实现端到端严格一次
我们知道,端到端的状态一致性的实现,需要每一个组件都实现,对于 Flink + Kafka 的数据管道系统(Kafka进、Kafka出)而言,各组件怎样保证 exactly-once 语义呢?
- 内部 —— 利用 checkpoint 机制,把状态存盘,发生故障的时候可以恢复,保证部的状态一致性
- source —— kafka consumer 作为 source,可以将偏移量保存下来,如果后续任务出现了故障,恢复的时候可以由连接器重置偏移量,重新消费数据,保证一致性
- sink —— kafka producer 作为 sink,采用两阶段提交 sink,需要实现一个 TwoPhaseCommitSinkFunction
内部的 checkpoint 机制我们已经有了了解,那 source 和 sink 具体又是怎样运行的呢?接下来我们逐步做一个分析。
具体的两阶段提交步骤总结如下:
- jobmanager 触发 checkpoint 操作,barrier 从 source 开始向下传递,遇到 barrier 的算子将状态存入状态后端,并通知 jobmanager
- 第一条数据来了之后,开启一个 kafka 的事务(transaction),正常写入 kafka 分区日志但标记为未提交,这就是“预提交”
- sink 连接器收到 barrier,保存当前状态,存入 checkpoint,通知 jobmanager,并开启下一阶段的事务,用于提交下个检查点的数据
- jobmanager 收到所有任务的通知,发出确认信息,表示 checkpoint 完成
- sink 任务收到 jobmanager 的确认信息,正式提交这段时间的数据
- 外部 kafka 关闭事务,提交的数据可以正常消费了