简介
在流处理中,数据是连续不断到来和处理的。每个任务进行计算处理时,可以基于当前数
据直接转换得到输出结果;也可以依赖一些其他数据。这些由一个任务维护,并且用来计算输
出结果的所有数据,就叫作这个任务的状态。
![](https://i-blog.csdnimg.cn/blog_migrate/ef8d9fb0241fe44634f27c56bf472ddc.png)
按键分区状态(Keyed State)
值状态(ValueState)
对于keyBy以后才能够使用
public class FlinkApp {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env =
StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
DataStreamSource<String> initDStream = env.fromElements("a", "b", "a");
//装换成key,value类型的数据才能使用keyBy来使用键值状态
SingleOutputStreamOperator<Tuple2<String, Integer>> map = initDStream.map(new MapFunction<String, Tuple2<String, Integer>>() {
@Override
public Tuple2<String, Integer> map(String value) throws Exception {
return Tuple2.of(value, 1);
}
});
map.keyBy(data -> data.f0)
.process(new ProcessFunction<Tuple2<String, Integer>, Tuple2<String, Integer>>() {
// 声明状态
private transient ValueState<String> state;
@Override
public void open(Configuration parameters) throws Exception {
//初始化状态
ValueStateDescriptor<String> descriptor = new ValueStateDescriptor<>(
"mystate", // 状态名称
Types.STRING // 状态类型
);
state = getRuntimeContext().getState(descriptor);
}
@Override
public void processElement(Tuple2<String, Integer> value, ProcessFunction<Tuple2<String, Integer>, Tuple2<String, Integer>>.Context ctx, Collector<Tuple2<String, Integer>> out) throws Exception {
String initState = state.value();
System.out.println("现在的状态值为: " + initState);
// 下面更新状态
state.update(value.f0);
System.out.println("myvalue: "+state.value());
out.collect(value);
}
}).print();
env.execute();
}
}
输出(可以看到开始的时候就是null,后面就是自己update的状态值,每一个Key的状态值是相互不影响的)
现在的状态值为: null
myvalue: a
(a,1)
现在的状态值为: null
myvalue: b
(b,1)
现在的状态值为: a
myvalue: a
(a,1)
列表状态(ListState)
public class FlinkApp {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env =
StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
DataStreamSource<String> initDStream = env.fromElements("a", "b", "a");
//装换成key,value类型的数据才能使用keyBy来使用键值状态
SingleOutputStreamOperator<Tuple2<String, Integer>> map = initDStream.map(new MapFunction<String, Tuple2<String, Integer>>() {
@Override
public Tuple2<String, Integer> map(String value) throws Exception {
return Tuple2.of(value, 1);
}
});
map.keyBy(data -> data.f0)
.process(new ProcessFunction<Tuple2<String, Integer>, Tuple2<String, Integer>>() {
private ListState<Tuple2<String, Integer>> listState;
@Override
public void open(Configuration parameters) throws Exception {
//初始化状态
listState = getRuntimeContext().getListState(
new ListStateDescriptor<Tuple2<String, Integer>>
("stream1-list", Types.TUPLE(Types.STRING, Types.INT)));
}
@Override
public void processElement(Tuple2<String, Integer> value, ProcessFunction<Tuple2<String, Integer>, Tuple2<String, Integer>>.Context ctx, Collector<Tuple2<String, Integer>> out) throws Exception {
listState.add(value);
System.out.println("现在的值");
Iterable<Tuple2<String, Integer>> tuple2s = listState.get();
System.out.println("=============");
for (Tuple2<String, Integer> tuple2 : tuple2s) {
System.out.println(tuple2);
}
out.collect(value);
}
}).print("initData: ");
env.execute();
}
}
结果,可以看到key一样的数据保存到了不同的list里面
现在的值
=============
(a,1)
initData: > (a,1)
现在的值
=============
(b,1)
initData: > (b,1)
现在的值
=============
(a,1)
(a,1)
initData: > (a,1)
映射状态(MapState)
public class FlinkApp {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env =
StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
DataStreamSource<String> initDStream = env.fromElements("a", "b", "a");
//装换成key,value类型的数据才能使用keyBy来使用键值状态
SingleOutputStreamOperator<Tuple2<String, Integer>> map = initDStream.map(new MapFunction<String, Tuple2<String, Integer>>() {
@Override
public Tuple2<String, Integer> map(String value) throws Exception {
return Tuple2.of(value, 1);
}
});
map.keyBy(data -> data.f0)
.process(new ProcessFunction<Tuple2<String, Integer>, Tuple2<String, Integer>>() {
// 声明状态,用 map 保存值(key,count)
MapState<String, Integer> mapState;
@Override
public void open(Configuration parameters) throws Exception {
//初始化状态
mapState = getRuntimeContext().getMapState(new
MapStateDescriptor<String, Integer>("window-pv", String.class, Integer.class));
}
@Override
public void processElement(Tuple2<String, Integer> value, ProcessFunction<Tuple2<String, Integer>, Tuple2<String, Integer>>.Context ctx, Collector<Tuple2<String, Integer>> out) throws Exception {
Integer oldData = mapState.get(value.f0);
if(oldData==null){
mapState.put(value.f0, value.f1);
}else{
Integer newValue = value.f1;
Integer res=newValue+oldData;
mapState.put(value.f0, res);
}
Iterator<Map.Entry<String, Integer>> iterator = mapState.iterator();
System.out.println("得到的数据为: ");
while (iterator.hasNext()) {
Map.Entry<String, Integer> next = iterator.next();
System.out.println(next.getKey()+" value: "+next.getValue());
}
}
}).print("initData: ");
env.execute();
}
}
输出的结果为
得到的数据为:
a value: 1
得到的数据为:
b value: 1
得到的数据为:
a value: 2
状态的生存时间
public class FlinkApp {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env =
StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
DataStreamSource<String> initDStream = env.fromElements("a", "b", "a");
//装换成key,value类型的数据才能使用keyBy来使用键值状态
SingleOutputStreamOperator<Tuple2<String, Integer>> map = initDStream.map(new MapFunction<String, Tuple2<String, Integer>>() {
@Override
public Tuple2<String, Integer> map(String value) throws Exception {
return Tuple2.of(value, 1);
}
});
map.keyBy(data -> data.f0)
.process(new ProcessFunction<Tuple2<String, Integer>, Tuple2<String, Integer>>() {
// 声明状态,用 map 保存值(key,count)
MapState<String, Integer> mapState;
@Override
public void open(Configuration parameters) throws Exception {
//初始化状态
MapStateDescriptor<String, Integer> stringIntegerMapStateDescriptor = new MapStateDescriptor<>("window-pv", String.class, Integer.class);
//设置状态的ttl
StateTtlConfig ttlConfig = StateTtlConfig
.newBuilder(Time.seconds(10))
//如果是创建还有写的时候更新状态
.setUpdateType(StateTtlConfig.UpdateType.OnCreateAndWrite)
//如果过期了那么就不用返回了
.setStateVisibility(StateTtlConfig.StateVisibility.NeverReturnExpired)
.build();
//根据状态描述器来设置状态的TTL
stringIntegerMapStateDescriptor.enableTimeToLive(ttlConfig);
//由于上面设置了那么这个状态就有了TTL的功能
mapState = getRuntimeContext().getMapState(stringIntegerMapStateDescriptor);
}
@Override
public void processElement(Tuple2<String, Integer> value, ProcessFunction<Tuple2<String, Integer>, Tuple2<String, Integer>>.Context ctx, Collector<Tuple2<String, Integer>> out) throws Exception {
Integer oldData = mapState.get(value.f0);
if(oldData==null){
mapState.put(value.f0, value.f1);
}else{
Integer newValue = value.f1;
Integer res=newValue+oldData;
mapState.put(value.f0, res);
}
Iterator<Map.Entry<String, Integer>> iterator = mapState.iterator();
System.out.println("得到的数据为: ");
while (iterator.hasNext()) {
Map.Entry<String, Integer> next = iterator.next();
System.out.println(next.getKey()+" value: "+next.getValue());
}
}
}).print("initData: ");
env.execute();
}
}
状态持久化和状态后端
检查点(Checkpoint)
有状态流应用中的检查点(
checkpoint
),其实就是所有任务的状态在某个时间点的一个快
照(一份拷贝)。简单来讲,就是一次“存盘”,让我们之前处理数据的进度不要丢掉。在一个
流应用程序运行时,
Flink
会定期保存检查点,在检查点中会记录每个算子的
id
和状态;如果
发生故障,
Flink
就会用最近一次成功保存的检查点来恢复应用的状态,重新启动处理流程,
就如同“读档”一样。
状态后端(State Backends)
检查点的保存离不开
JobManager
和
TaskManager
,以及外部存储系统的协调。在应用进
行检查点保存时,首先会由
JobManager
向所有
TaskManager
发出触发检查点的命令;
TaskManger
收到之后,将当前任务的所有状态进行快照保存,持久化到远程的存储介质中;
完成之后向
JobManager
返回确认信息。这个过程是分布式的,当
JobManger
收到所有
TaskManager
的返回信息后,就会确认当前检查点成功保存。
而这一切工作的
协调,就需要一个“专职人员”来完成。
![](https://i-blog.csdnimg.cn/blog_migrate/f9c63f31aef1628a33cca6d589b8b2a6.png)
哈希表状态后端(HashMapStateBackend)
这种方式就是我们之前所说的,把状态存放在内存里。具体实现上,哈希表状态后端在内
部会直接把状态当作对象(
objects
),保存在
Taskmanager
的
JVM
堆(
heap
)上。普通的状态,
以及窗口中收集的数据和触发器(
triggers
),都会以键值对(
key-value
)的形式存储起来,所
以底层是一个哈希表(
HashMap
),这种状态后端也因此得名。
对于检查点的保存,一般是放在持久化的分布式文件系统(
file system
)中,也可以通过
配置“检查点存储”(
CheckpointStorage
)来另外指定。
HashMapStateBackend
是将本地状态全部放入内存的,这样可以获得最快的读写速度,使
计算性能达到最佳;代价则是内存的占用。它适用于具有大状态、长窗口、大键值状态的作业,
对所有高可用性设置也是有效的。
内嵌 RocksDB 状态后端(EmbeddedRocksDBStateBackend)
RocksDB
是一种内嵌的
key-value
存储介质,可以把数据持久化到本地硬盘。配置
EmbeddedRocksDBStateBackend
后,会将处理中的数据全部放入
RocksDB
数据库中,
RocksDB
默认存储在
TaskManager
的本地数据目录里。
与
HashMapStateBackend
直接在堆内存中存储对象不同,这种方式下状态主要是放在
RocksDB
中的。数据被存储为序列化的字节数组(
Byte Arrays
),读写操作需要序列化
/
反序列
化,因此状态的访问性能要差一些。另外,因为做了序列化,
key
的比较也会按照字节进行,
而不是直接调用
.hashCode()
和
.equals()
方法。
对于检查点,同样会写入到远程的持久化文件系统中。
EmbeddedRocksDBStateBackend
始终执行的是异步快照,也就是不会因为保存检查点而阻
塞数据的处理;而且它还提供了增量式保存检查点的机制,这在很多情况下可以大大提升保存
效率。
由于它会把状态数据落盘,而且支持增量化的检查点,所以在状态非常大、窗口非常长、
键
/
值状态很大的应用场景中是一个好选择,同样对所有高可用性设置有效。
如何选择正确的状态后端
HashMap
和
RocksDB
两种状态后端最大的区别,就在于本地状态存放在哪里:前者是内
存,后者是
RocksDB
。在实际应用中,选择那种状态后端,主要是需要根据业务需求在处理
性能和应用的扩展性上做一个选择。
HashMapStateBackend
是内存计算,读写速度非常快;但是,状态的大小会受到集群可用
内存的限制,如果应用的状态随着时间不停地增长,就会耗尽内存资源。
而
RocksDB
是硬盘存储,所以可以根据可用的磁盘空间进行扩展,而且是唯一支持增量
检查点的状态后端,所以它非常适合于超级海量状态的存储。不过由于每个状态的读写都需要
做序列化
/
反序列化,而且可能需要直接从磁盘读取数据,这就会导致性能的降低,平均读写
性能要比
HashMapStateBackend
慢一个数量级。
我们可以发现,实际应用就是权衡利弊后的取舍。最理想的当然是处理速度快且内存不受
限制可以处理海量状态,那就需要非常大的内存资源了,这会导致成本超出项目预算。比起花
更多的钱,稍慢的处理速度或者稍小的处理规模,老板可能更容易接受一点。
配置默认的状态后端
在
flink-conf.yaml
中,可以使用
state.backend
来配置默认状态后端。
配置项的可能值为
hashmap
,这样配置的就是
HashMapStateBackend
;也可以是
rocksdb
,
276
这样配置的就是
EmbeddedRocksDBStateBackend
。另外,也可以是一个实现了状态后端工厂
StateBackendFactory
的类的完全限定类名。
下面是一个配置
HashMapStateBackend
的例子:
# 默认状态后端
state.backend: hashmap
# 存放检查点的文件路径
state.checkpoints.dir: hdfs://namenode:40010/flink/checkpoints
为每个作业(
Per-job
)单独配置状态后端
StreamExecutionEnvironment env =
StreamExecutionEnvironment.getExecutionEnvironment();
env.setStateBackend(new HashMapStateBackend());
需要注意,如果想在
IDE
中使用
EmbeddedRocksDBStateBackend
,需要为
Flink
项目添加
依赖:
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-statebackend-rocksdb_${scala.binary.version}</artifactId>
<version>1.13.0</version>
</dependency>
而由于
Flink
发行版中默认就包含了
RocksDB
,所以只要我们的代码中没有使用
RocksDB
的相关内容,就不需要引入这个依赖。即使我们在
flink-conf.yaml
配置文件中设定了
state.backend
为
rocksdb
,也可以直接正常运行,并且使用
RocksDB
作为状态后端。