Flink使用指南: 面试必问内存管理模型,进大厂一定要知道!
Flink使用指南: Kafka流表关联HBase维度表
Flink使用指南: Watermark新版本使用
Flink使用指南: Flink SQL自定义函数
目录
系列文章目录
前言
一、Checkpoint机制
如何开启Checkpoint
二、Keyed State 和 Operator State
原始状态和托管状态
如何使用Managed Keyed State
状态的生命周期(TTL)
如何使用Managed Operator State
三. checkpoint算法
总结
什么是状态与容错?
在使用Flink做实时计算时,计算中间结果如何存储,这叫状态;当想升级某个程序代码或者某个程序异常退出等事故情况,Flink如果能保证数据准备性,这叫容错。
Flink的状态与容错主要分为一下几个知识点:
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 每 1000ms 开始一次 checkpoint
env.enableCheckpointing(1000);
// 高级选项:
// 设置模式为精确一次 (这是默认值)
env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);
// 确认 checkpoints 之间的时间会进行 500 ms
env.getCheckpointConfig().setMinPauseBetweenCheckpoints(500);
// Checkpoint 必须在一分钟内完成,否则就会被抛弃
env.getCheckpointConfig().setCheckpointTimeout(60000);
// 同一时间只允许一个 checkpoint 进行
env.getCheckpointConfig().setMaxConcurrentCheckpoints(1);
// 开启在 job 中止后仍然保留的 externalized checkpoints
env.getCheckpointConfig().enableExternalizedCheckpoints(ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION);
更多参数配置请参考Flink配置文档:
https://ci.apache.org/projects/flink/flink-docs-release-1.12/zh/deployment/config.html
Flink的Checkpoint机制会将operator操作进行快照存储到State Backends中,默认情况,状态是保持在 TaskManagers 的内存中,checkpoint 保存在 JobManager 的内存中。为了合适地持久化大体量状态, Flink 支持各种各样的途径去存储 checkpoint 状态到其他的 state backends 上。Flink 现在为没有迭代(iterations)的作业提供一致性的处理保证。在迭代作业上开启 checkpoint 会导致异常。为了在迭代程序中强制进行 checkpoint,用户需要在开启 checkpoint 时设置一个特殊的标志:env.enableCheckpointing(interval, CheckpointingMode.EXACTLY_ONCE, force = true)。
首先,flink中的state分为两种:Keyed State 和 Operator State。
Keyed State:keyed state始终与key相关,所以只能在KeyedStream的函数和算子中使用keyed state。可以理解为,KeyedStream的算子或者函数按照key将数据流进行分区,每个key就是一个分区,而每个分区都保存着一个的keyed state。
以后版本中,可能会将keyed state改为Key Groups,Key Groups就是一个flink实例被分配到的所有key的组合。所以Key Groups的数量等于设置的并行度。
Operator State:Operator State即non-keyed state。算子操作或者非键控函数的每个并行任务都会绑定一个Operator State。如kafka连接器就是一个很好的例子:kafka消费者的每个分区都会维护一个map类型的数据,作为状态保存topic、分区和offset。当并行度发生变化时,Operator State支持重新分配状态。
keyed state和Operator State可以有两种形式存在:managed (托管)和raw(原始)。
Managed State:Managed State在运行时由flink控制,保存在哈希表、RocksDB等结构化数据中。如ValueState、ListState。flink会对Managed State编码,并写入checkpoint。
Raw State:Raw State是以自定义的数据类型保存的状态信息。在写入checkpoint时,作为二进制序列写入checkpoint中。所以flink不知道Operator State的数据结构,只能获得原始的二进制序列。通常情况下,使用managed state居多。
所有的flink函数都可以使用Managed State,但是如果需要使用Raw State,则需要在函数内实现相应的接口。相较于Raw State,官方更推荐使用Managed State,使用Managed State时,支持修改并行度后自动重新分配状态,且具备更完善的内存管理。
注意:如果使用Managed State时需要自定义序列化器
https://ci.apache.org/projects/flink/flink-docs-release-1.12/zh/dev/stream/state/custom_serialization.html
上面说到,flink的状态可以保存所有元素、聚合结果、历史数据等,flink提供了相应的接口以实现这些功能。另外,keyed state顾名思义,必须在stream.keyBy(…)之后使用,否则会报错。
下面是flink提供的状态数据类型:
ValueState
: 仅保存一个可更新、可检索的值。作用域为输入元素的键,即每个key保存一个
ListState
: 保存一个列表的状态。可以对这个列表进行追加写或者检索。使用add(T)或者addAll(List
ReducingState
: 保存一个唯一值,这个值是当前所有元素的预聚合结果。这个接口与ListState
相似,区别在于ReducingState
的add()方法是调用ReduceFunction方法,将当前元素与之前的预聚合结果进行计算,再保存新的预聚合结果。
AggregatingState
:保存一个唯一值,这个值是当前所有元素的预聚合结果。与ReducingState
的区别在于AggregatingState
的输入类型和输出类型可以不一致,AggregatingState
分别定义输入、输出两个参数的数据类型。add(IN)内部调用的是AggregateFunction方法。
MapState
: 保存一个列表的map类型的状态。可以使用put方法向其添加k-v类型的键值对,也可以用于检索。使用 put(UK, UV)
或者putAll(Map
方法添加数据;使用entries()
, keys()
和values()
来检索key和value。使用 isEmpty()
判断是否存在数据。
所有的状态类型都有一个clear()方法,用于清空当前key的状态中所有的数据。
重点提示一:以上的状态类型对象仅仅只是作为一个状态的接口而已,状态不一定是存储在以上对象里面的,还可以存在本地磁盘或者其他地方。
重点提示二:你从状态中获取的value取决于当前输入元素的key,所以,你调用的同一个函数会根据不同的key返回不同的value。
获取状态时,必须创建一个StateDescriptor对象用于描绘状态的名称和数据类型,还可能包含自定义的函数,如ReduceFunction。状态通过RuntimeContext调用getState方法来获取,所以必须是富函数才能获取状态。
获取不同的状态对应方式如下:
ValueState getState(ValueStateDescriptor)
ReducingState getReducingState(ReducingStateDescriptor)
ListState getListState(ListStateDescriptor)
AggregatingState getAggregatingState(AggregatingStateDescriptor)
FoldingState getFoldingState(FoldingStateDescriptor)
MapState getMapState(MapStateDescriptor)
以FlatMapFunction为例,使用状态代码如下:
class CountWindowAverage extends RichFlatMapFunction[(Long, Long), (Long, Long)] {
private var sum: ValueState[(Long, Long)] = _
override def flatMap(input: (Long, Long), out: Collector[(Long, Long)]): Unit = {
// 获取状态的值
val tmpCurrentSum = sum.value
// 如果状态不为空,则将其值赋给currentSum;否则初始化currentSum为(0L,0L)
val currentSum = if (tmpCurrentSum != null) {
tmpCurrentSum
} else {
(0L, 0L)
}
// 计算sum值
val newSum = (currentSum._1 + 1, currentSum._2 + input._2)
// 更新状态
sum.update(newSum)
// 当元素个数达到2, 发出平均值并清空状态。
if (newSum._1 >= 2) {
out.collect((input._1, newSum._2 / newSum._1))
sum.clear()
}
}
override def open(parameters: Configuration): Unit = {
//在open函数中初始化状态,以免过早地获取状态导致数据错误。也可以在外部使用lazy修饰,效果与在open中初始化一样。
sum = getRuntimeContext.getState(
new ValueStateDescriptor[(Long, Long)]("average", createTypeInformation[(Long, Long)])
)
}
}
object ExampleCountWindowAverage extends App {
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.fromCollection(List(
(1L, 3L),
(1L, 5L),
(1L, 7L),
(1L, 4L),
(1L, 2L)
)).keyBy(_._1)
.flatMap(new CountWindowAverage())
.print()
// the printed output will be (1,4) and (1,5)
env.execute("ExampleManagedState")
这个例子中,以输入元组的第一个元素为key(例子中所有key都是1),函数将元素个数和value的sum值保存在状态中。当元素个数达到2时,返回value的平均值并清除状态。
注意,如果元组列表中的元组的第一个元素不相同(即key不同),则这为每个不同的key保留不同的状态。
任何类型的keyed state都可以分配一个生命周期时间(TTL)。如果配置了TTL,且一个状态过期了,那么就清空这个状态。每个key都有其对应的状态,状态收集器对每个状态独立判断TTL,这意味着如果某个key的状态过期,那么只会情况该key的状态,而不会影响其他key的状态。
判断过期的逻辑为:上一个时间戳+TTL<=当前时间,则视为过期。以下是判断是否过期的源码:
public class TtlUtils {
static boolean expired(@Nullable TtlValue ttlValue, long ttl, TtlTimeProvider timeProvider) {
return expired(ttlValue, ttl, timeProvider.currentTimestamp());
}
static boolean expired(@Nullable TtlValue ttlValue, long ttl, long currentTimestamp) {
return ttlValue != null && expired(ttlValue.getLastAccessTimestamp(), ttl, currentTimestamp);
}
static boolean expired(long ts, long ttl, TtlTimeProvider timeProvider) {
return expired(ts, ttl, timeProvider.currentTimestamp());
}
//上一个时间戳+TTL<=当前时间,则视为过期
public static boolean expired(long ts, long ttl, long currentTimestamp) {
return getExpirationTimestamp(ts, ttl) <= currentTimestamp;
}
private static long getExpirationTimestamp(long ts, long ttl) {
long ttlWithoutOverflow = ts > 0 ? Math.min(Long.MAX_VALUE - ts, ttl) : ttl;
return ts + ttlWithoutOverflow;
}
static TtlValue wrapWithTs(V value, long ts) {
return new TtlValue<>(value, ts);
}
}
配置TTL
配置TTL首先需要创建一个StateTtlConfig
的对象,用于配置TTL相关信息。然后调用状态描述器的enableTimeToLive方法开启TTL,之后再通过描述器在RumTimeContext中获取状态。示例如下:
import org.apache.flink.api.common.state.StateTtlConfig;
import org.apache.flink.api.common.state.ValueStateDescriptor;
import org.apache.flink.api.common.time.Time;
StateTtlConfig ttlConfig = StateTtlConfig
.newBuilder(Time.seconds(1))
.setUpdateType(StateTtlConfig.UpdateType.OnCreateAndWrite)
.setStateVisibility(StateTtlConfig.StateVisibility.NeverReturnExpired)
.build();
ValueStateDescriptor stateDescriptor = new ValueStateDescriptor<>("text state", String.class);
stateDescriptor.enableTimeToLive(ttlConfig);
其中,setUpdateType方法用于设置TTL刷新方式,有两种刷新机制:
StateTtlConfig.UpdateType.OnCreateAndWrite
- 仅在创建和写入时。StateTtlConfig.UpdateType.OnReadAndWrite
- 创建、写入、读取时。setStateVisibility方法用于设置对已过期但还未被清理掉的状态如何处理,也有两种机制:
StateTtlConfig.StateVisibility.NeverReturnExpired
- 过期数据不可见,即使未被清除,也不可见。StateTtlConfig.StateVisibility.ReturnExpiredIfNotCleanedUp
- 过期但未被清除的可见。清除过期状态
默认情况下,过期数据会在读取时自动删除,然后后台会定期进行垃圾回收。也可以选择关闭后台垃圾回收,代码如下:
import org.apache.flink.api.common.state.StateTtlConfig;
StateTtlConfig ttlConfig = StateTtlConfig
.newBuilder(Time.seconds(1))
.disableCleanupInBackground()
.build();
也可以设置成在创建全状态镜像时清除过期状态,这样可以减小快照大小。在这种模式下,本地状态不会被清理,但是如果从快照中恢复状态时,也不会包含过期数据。注意:此选项不适用于使用RocksDB做增量checkpoint。设置方式如下:
import org.apache.flink.api.common.state.StateTtlConfig
import org.apache.flink.api.common.time.Time
val ttlConfig = StateTtlConfig
.newBuilder(Time.seconds(1))
.cleanupFullSnapshot
.build
也可以在访问状态或者处理数据时触发状态删除操作。如果使用这种策略,则状态存储后端会保存一个懒加载的全局迭代器用于存储所有的state。只有在触发清理操作时,才会激活这个迭代器,遍历所有状态并清理过期状态。配置代码如下:
import org.apache.flink.api.common.state.StateTtlConfig
val ttlConfig = StateTtlConfig
.newBuilder(Time.seconds(1))
.cleanupIncrementally(10, true)
.build
配置这种方式需要传入两个参数,第一个参数是当访问状态时(访问状态必定触发清理),每次检查的状态的数目;第二个参数是是否在处理每条数据是触发清理操作。默认是每次检查5个状态数据,不基于处理数据触发清理。
Scala DataStream API中特有的状态接口
除了上述接口外,在scala API中,对map()
或者 flatMap()函数在操作
KeyedStream时,还提供了一种快捷方式来访问一个ValueState
。如:
val stream: DataStream[(String, Int)] = ...
val counts: DataStream[(String, Int)] = stream
.keyBy(_._1)
.mapWithState((in: (String, Int), count: Option[Int]) =>
count match {
case Some(c) => ( (in._1, c), Some(c + in._2) )
case None => ( (in._1, 0), Some(in._2) )
})
因为生产环境中大部分使用的都是keyed state,很少使用Operator State,所以这里只展示几个简单的例子,不做赘述。
下面例子中使用了有状态的SinkFunction ,使用了CheckpointedFunction在输出元素前先进行缓冲,然后将事件切分并更新状态。
class BufferingSink(threshold: Int = 0)
extends SinkFunction[(String, Int)]
with CheckpointedFunction {
@transient
private var checkpointedState: ListState[(String, Int)] = _
private val bufferedElements = ListBuffer[(String, Int)]()
override def invoke(value: (String, Int), context: Context): Unit = {
bufferedElements += value
if (bufferedElements.size == threshold) {
for (element <- bufferedElements) {
// send it to the sink
}
bufferedElements.clear()
}
}
override def snapshotState(context: FunctionSnapshotContext): Unit = {
checkpointedState.clear()
for (element <- bufferedElements) {
checkpointedState.add(element)
}
}
override def initializeState(context: FunctionInitializationContext): Unit = {
val descriptor = new ListStateDescriptor[(String, Int)](
"buffered-elements",
TypeInformation.of(new TypeHint[(String, Int)]() {})
)
checkpointedState = context.getOperatorStateStore.getListState(descriptor)
if(context.isRestored) {
for(element <- checkpointedState.get()) {
bufferedElements += element
}
}
}
}
基于分布式快照Chandy-Lamport,具体可查看这篇博客https://www.cnblogs.com/yuanyifei1/p/10360465.html
Chandy-Lamport算法写起来篇幅有点长,有空单独出一篇博客讲下Flink源码里的Chandy-Lamport算法,Flink的这个算法和原生的算法还是有点区别的。
实时计算的一大有点是可以在资源空闲时一直在做计算,这样可以让数据达到实时计算目的,计算就会使用上一时刻数据或者下一时刻数据,此时状态计算的作用就显现出来了,状态计算可以充分的保留某个时间段的数据,并且根据规则做TTL清楚保证了存储不会很大的问题。
上车了兄弟们!!!