Watermark
Watermark,水位线机制,用于保证数据安全不丢失,是Flink认定完整数据的边界,即Flink认为水位线以前的数据都已经接收到了。
例如:当接收数据中最大时间戳为8S时,在默认情况下(水位线为0,允许延迟时间为0)Flink就认为8S以前的数据全部接收到了,如果有在8S关闭的窗口就会关闭并触发计算,但如果6S的数据还未完成数据传输的话,就无法进入窗口中了。
但如果设置了水位线(假设水位线延迟3S),当8S数据到达时,Flink只会认为5S以前的数据全部接收到了,并不会关闭窗口。直到11S及以后的数据到达时,Flink才会认为8S及以前的数据全部接收到了,并关闭窗口触发计算。
Watermark的设定:
val kafkaSource: DataStream[SensorReading] = source
.filter(_.matches("^sensor_\\d{1},\\d{13},\\d{2}\\.\\d{2}$"))
.map(line => {
val fields: Array[String] = line.split(",")
SensorReading(fields(0), fields(1).toLong, fields(2).toDouble)
})
// 这里的Time.seconds就是水位线的设置
.assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor[SensorReading](Time.seconds(0)) {
override def extractTimestamp(element: SensorReading): Long = element.timeStamp * 1000L
})
Watermark于allowlateness设置有些类似,两者都可以拖延窗口彻底关闭的时间,而区别在于对于窗口关闭时间的认定,allowlateness不会影响窗口触发计算的时间,而watermark会改变计算触发的时间。
指定和选择Watermark
在Flink中指定Watermark的操作通常会与指定Timestamp的操作一同进行,这两个操作通过WatermarkStrategy
接口实现
public interface WatermarkStrategy<T>
extends TimestampAssignerSupplier<T>,
WatermarkGeneratorSupplier<T>{
/**
* Instantiates a {@link TimestampAssigner} for assigning timestamps according to this
* strategy.
*/
@Override
TimestampAssigner<T> createTimestampAssigner(TimestampAssignerSupplier.Context context);
/**
* Instantiates a WatermarkGenerator that generates watermarks according to this strategy.
*/
@Override
WatermarkGenerator<T> createWatermarkGenerator(WatermarkGeneratorSupplier.Context context);
}
可以看到接口中的两个方法分别用于生成Watermark和Timestamp,Timestamp的生成比较简单(processing time自动取当前时间,event time从数据中取出时间即可),重点观察Watermark的生成机制
Watermark的指定有两种方式
- 在数据源source指定
- 在其他任意位置通过
assignTimestampsAndWatermarks
方法(这里传入WatermarkStrategy接口的实现类)指定
官方推荐使用数据源中指定的方式,原因在于通过第一种方式指定生成的watermark更加精确;
上述的WatermarkStrategy
接口中生成watermark返回的对象是WatermarkGenerator
接口的实现类,下面看下WatermarkGenerator
接口源码:
@Public
public interface WatermarkGenerator<T> {
/**
* Called for every event, allows the watermark generator to examine and remember the
* event timestamps, or to emit a watermark based on the event itself.
*/
void onEvent(T event, long eventTimestamp, WatermarkOutput output);
/**
* Called periodically, and might emit a new watermark, or not.
*
* <p>The interval in which this method is called and Watermarks are generated
* depends on {@link ExecutionConfig#getAutoWatermarkInterval()}.
*/
void onPeriodicEmit(WatermarkOutput output);
}
里面只有两个方法,onEvent
方法每条数据执行一次,而onPeriodicEmit
每隔一段时间执行一次,用于提交watermark,这里涉及了watermark提交的两种策略:
- periodic:即每隔一段时间提交一次watermark,这种策略在每条数据到来时调用
onEvent
方法用于更新watermark,在flink周期性调用onPeriodicEmit
方法时将watermark提交出去 - punctuated:这种策略不会固定周期提交watermark,而是更加灵活地根据数据中的时间来判断是否提交,因此选用这种策略时不需要实现
onPeriodicEmit
方法(不需要在该方法中提交watermark),理论上选用该策略时可以每条数据都提交一次watermark,但是过多次提交watermark只会拖累系统性能
下面是两种策略下onEvent
和onPeriodicEmit
方法地实现实例:
// periodic策略
/**
* This generator generates watermarks assuming that elements arrive out of order,
* but only to a certain degree. The latest elements for a certain timestamp t will arrive
* at most n milliseconds after the earliest elements for timestamp t.
*/
public class BoundedOutOfOrdernessGenerator implements WatermarkGenerator<MyEvent> {
private final long maxOutOfOrderness = 3500; // 3.5 seconds
private long currentMaxTimestamp;
@Override
public void onEvent(MyEvent event, long eventTimestamp, WatermarkOutput output) {
// 根据新到数据更新timestamp,如果是processing time这里就不需要实现什么逻辑了
currentMaxTimestamp = Math.max(currentMaxTimestamp, eventTimestamp);
}
@Override
public void onPeriodicEmit(WatermarkOutput output) {
// 当flink调用是提交watermark,通过当前timestamp减去一个固定时间(可以自行修改这里的策略)
output.emitWatermark(new Watermark(currentMaxTimestamp - maxOutOfOrderness - 1));
}
}
// punctuated
public class PunctuatedAssigner implements WatermarkGenerator<MyEvent> {
@Override
public void onEvent(MyEvent event, long eventTimestamp, WatermarkOutput output) {
// 直接在这里提交watermark,注意要判断什么时候提交watermark
if (event.hasWatermarkMarker()) {
output.emitWatermark(new Watermark(event.getWatermarkTimestamp()));
}
}
@Override
public void onPeriodicEmit(WatermarkOutput output) {
// don't need to do anything because we emit in reaction to events above
}
}
watermark处理机制
每个算子都要在处理过watermark(completely process,一个算子需要接收到全部上游算子发送的watermark)之后才能将数据发往下游算子,当处在窗口中时,watermark要在全部数据处理完成后才能发送。
对于关联流TwoInputStreamOperator
,两条流共同的watermark会是两条流各自watermark中较小的一个
Idle Source
当数据源的一个分区(splits/partitions/shards)长时间不发送数据时,这个数据源就被称为Idle Source
,而由于flink在多个并行度读取数据时,watermark会按照最小的一个保存,所以Idle Source
会拖累计算性能
要解决这个问题可以在WatermarkStrategy
中指定超时时间:
WatermarkStrategy
.forBoundedOutOfOrderness[(Long, String)](Duration.ofSeconds(20)) // 指定最大乱序时间
.withIdleness(Duration.ofMinutes(1)) // 指定超时时间,超过该事件未发送数据,对应数据源会被标记为idle source
State
Flink状态特点:
- 由一个任务维护,并且用来计算某个结果的所有数据,都属于这个任务的状态
- 可以认为状态就是一个本地变量,可以被任务的业务逻辑访问
- Flink会进行状态管理,包括状态一致性、故障处理以及高效存储的访问,以便开发人员可以专注于应用程序的逻辑
- 状态始终与特定算子相关联
- 为了使运行时的Flink了解算子的状态,算子需要预先注册其状态
Flink状态分为两种:算子状态和键控状态
算子状态(Operator State)
- 算子状态的作用范围限定为算子任务,由同一并行任务处理的所有数据都可以访问到相同的状态
- 状态对于同一子任务而言是共享的
- 算子状态不能由相同或不同算子的零一子任务访问
DataStream
调用算子的状态- 算子状态
- 列表状态(List State)
- 将状态表示为一组数据的列表
- 联合列表状态(Union list state)
- 与常规列表状态的区别在于,故障恢复时,可以从保存点(savepoint)启动应用程序
- 广播状态(Broadcast state)
- 如果一个算子有多项任务,而它的每项任务状态又都相同,那么这种特殊情况最适合应用广播状态
- 列表状态(List State)
键控状态(Keyed State)
- 根据数据流中定义的键来维护和访问的
- 为每个key维护一个状态实例,并将具有相同键的所有数据,都分区到同一个算子任务中,这个任务会维护和处理这个key对应的状态
- 当任务处理一条数据时,它会自动将状态的访问范围限定为当前数据的key
KeyedStream
调用的算子的状态- 算子状态
- 值状态(value state)
- 将状态表示为单个的值
- 列表状态(List state)
- 将状态表示为一组数据的列表
- 映射状态(Map state)
- 将状态表示为一组Key-Value对
- 聚合状态(Reducing state & Aggregating state)
- 将状态表示为一个用于聚合操作的列表
- 值状态(value state)
使用案例:如果温度在10S内温度是连续上升状态,就执行报警操作
import org.apache.flink.api.common.state.{ValueState, ValueStateDescriptor}
import org.apache.flink.api.scala._
import org.apache.flink.streaming.api.functions.KeyedProcessFunction
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.util.Collector
import source.SensorReading
object ProcessFunctionTest {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(4)
val dataStream = env.socketTextStream("localhost", 7777).map(
sentence => {
val fields = sentence.split(",").map(_.trim)
SensorReading(fields(0), fields(1).toLong, fields(2).toDouble)
}
)
dataStream.keyBy(_.id)
.process(new TempIncreWarning(10000L)).print()
env.execute("ProcessFunction")
}
}
class TempIncreWarning(interval: Long) extends KeyedProcessFunction[String, SensorReading, String] {
// 定义状态,保存上一个温度值进行比较,保存注册定时间的时间戳用于删除
lazy val lastTempState: ValueState[Double] = getRuntimeContext.getState(new ValueStateDescriptor[Double]("last tempreature", classOf[Double]))
lazy val timerTsState: ValueState[Long] = getRuntimeContext.getState(new ValueStateDescriptor[Long]("last timestamp", classOf[Long]))
override def processElement(value: SensorReading, ctx: KeyedProcessFunction[String, SensorReading, String]#Context, out: Collector[String]): Unit = {
// 先取出状态
val lastTemp = lastTempState.value()
val timerTs = timerTsState.value()
lastTempState.update(value.temperature)
// 判断当前温度值和上次温度比较
if (value.temperature > lastTemp && timerTs == 0) {
// 温度上升,且没有定时器,那么注册当前数据时间戳10S后的定时器
val ts = ctx.timerService().currentProcessingTime() + interval
ctx.timerService().registerEventTimeTimer(ts)
timerTsState.update(ts)
} else if (value.temperature < lastTemp) {
// 如果温度下降,那么删除定时器
ctx.timerService().deleteEventTimeTimer(timerTsState.value())
timerTsState.clear()
}
}
override def onTimer(timestamp: Long, ctx: KeyedProcessFunction[String, SensorReading, String]#OnTimerContext, out: Collector[String]): Unit = {
out.collect("传感器"+ctx.getCurrentKey+"的温度连续"+(interval/1000)+"秒连续上升")
timerTsState.clear()
}
}
状态后端(State Backends)
- 每传入一条数据,有状态的算子任务都会读取和更新状态
- 由于有效的状态访问对于处理数据的低延迟至关重要,因此每个并行任务都会在本地维护其状态,以确保快速的状态访问
- 状态的存储、访问以及维护,由一个可插入的组件决定,这个组件就是状态后端(state backend)
- 状态后端主要负责两件事:本地的状态管理,以及将检查点(checkpoint)状态写入远程存储
- 状态后端可选保存位置
- MemoryStateBackend
- 内存级的状态后端,会将键控状态作为内存中的对象进行管理,将他们存储在TaskManager的JVM堆上,而将ckeckpoint存储在JobManager的内存中
- 特点:快速低延迟、不稳定
- FsStateBackend
- 将checkpoint存到远程的持久化文件系统中,而对于本地状态,跟MemoryStateBackend一样,也会存在TaskManager的JVM堆上
- 同时拥有内存级的本地访问速度,和更好的容错保证
- RocksDBStateBackend
- 将所有状态序列化后,存入本地的RocksDB中存储
- MemoryStateBackend
- 状态后端配置:
env.setStateBackend()
参数传入上述三种类型的对象即可,RocksDBStateBackend需要额外导入依赖
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-statebackend-rocksdb_2.11</artifactId>
<version>1.10.3</version>
</dependency>
一些常见的状态后端配置方法及配置项:
// 配置状态后端为HDFS,指定存储路径
env.setStateBackend(new FsStateBackend("hdfs://num04:9000/gmall_flink/checkpoint"));
// 配置检查点保存的时间间隔,注意这里的间隔指的是从一个检查点开始保存到下个检查点开始保存的时间间隔
env.enableCheckpointing(5000L);
// 配置检查点的保存策略,表示当失败重试时,系统提供的数据一致性保证级别,只有EXACTLY_ONCE和AT_LEAST_ONCE两个选项
// EXACTLY_ONCE操作比较复杂,但是会保证从检查点恢复时数据仅读取一次
// AT_LEAST_ONCE操作相对简单(延迟较低),但是恢复数据时,数据可能会被多次读取
// 当数据流较为简单或对延迟要求不太严格时,可以选择EXACTLY_ONCE
// 当要求较低延迟,且重复数据无影响时,选择AT_LEAST_ONCE
env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);
// 设置检查点保存的超时时间
env.getCheckpointConfig().setCheckpointTimeout(10000L);
// 配置同一时间可同时有多少个检查点保存操作
env.getCheckpointConfig().setMaxConcurrentCheckpoints(2);
// 配置检查点保存的最小时间间隔,这里的时间间隔是指上一个检查点保存成功后,需要间隔多久才可以开始保存下一个检查点
env.getCheckpointConfig().setMinPauseBetweenCheckpoints(3000L);