文章目录
- 将应用程序状态映射到数据集
- 读取状态
- operator状态
- 算子列表状态
- Operator Union List State
- 广播状态
- 使用自定义序列化程序
- 键控状态
- 窗口状态
- 编写新的保存点
- operator state
- 广播状态
- 键控状态
- 窗口状态
- 修改保存点
Apache Flink 的状态处理器 API 提供了强大的功能来使用 Flink 的 DataStream API 读取、写入和修改保存点和检查点BATCH。由于DataStream 和 Table API 的互操作性,您甚至可以使用关系 Table API 或 SQL 查询来分析和处理状态数据。
例如,您可以获取正在运行的流处理应用程序的保存点,并使用 DataStream 批处理程序对其进行分析,以验证应用程序的行为是否正确。或者,您可以从任何存储读取一批数据,对其进行预处理,然后将结果写入用于引导流应用程序状态的保存点。也可以修复不一致的状态条目。最后,状态处理器 API 开辟了许多方法来发展一个有状态的应用程序,该应用程序以前被参数和设计选择所阻止,这些参数和设计选择无法更改,而不会丢失应用程序启动后的所有状态。例如,您现在可以任意修改状态的数据类型、调整算子的最大并行度、拆分或合并算子状态、重新分配算子 UID 等等。
要开始使用状态处理器 api,请在您的应用程序中包含以下库。
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-state-processor-api</artifactId>
<version>1.15.0</version>
</dependency>
将应用程序状态映射到数据集
状态处理器 API 将流应用程序的状态映射到一个或多个可以单独处理的数据集。为了能够使用 API,您需要了解此映射的工作原理。
但是让我们先来看看有状态的 Flink 作业是什么样的。一个 Flink 作业是由算子组成的;通常是一个或多个源算子,几个用于实际处理的算子,以及一个或多个汇算子。每个算子在一个或多个任务中并行运行,并且可以处理不同类型的状态。一个算子可以有零个、一个或多个“算子状态”,这些“算子状态”被组织成列表,这些列表的范围仅限于算子的任务。如果将算子应用于键控流,则它还可以具有零个、一个或多个“键控状态”,其范围限定为从每个已处理记录中提取的键。您可以将键控状态视为分布式键值映射。
下图显示了应用程序“MyApp”,它由三个运算符“Src”、“Proc”和“Snk”组成。Src 有一个算子状态(os1),Proc 有一个算子状态(os2)和两个键控状态(ks1,ks2),Snk 是无状态的。
MyApp 的保存点或检查点由所有状态的数据组成,以可以恢复每个任务的状态的方式组织。当使用批处理作业处理保存点(或检查点)的数据时,我们需要一个心智模型,将各个任务状态的数据映射到数据集或表中。实际上,我们可以将保存点视为数据库。每个算子(由其 UID 标识)代表一个命名空间。算子的每个算子状态都映射到命名空间中的一个专用表,其中有一列保存所有任务的状态数据。算子的所有键状态都映射到一个表,该表由键的一列和每个键状态的一列组成。下图显示了 MyApp 的保存点如何映射到数据库。
该图显示了 Src 的 operator state 的值如何映射到具有一列和五行的表,其中一行用于 Src 的所有并行任务中的每个列表条目。“Proc”的算子状态 os2 类似地映射到单个表。键控状态 ks1 和 ks2 组合成一个包含三列的表,一列用于键,一列用于 ks1,一列用于 ks2。键控表为两个键控状态的每个不同键保存一行。由于算子“Snk”没有任何状态,它的命名空间是空的。
读取状态
读取状态首先指定有效保存点或检查点的路径以及StateBackend用于恢复数据的路径。恢复状态的兼容性保证与恢复应用程序时的兼容性保证相同DataStream。
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
SavepointReader savepoint = SavepointReader.read(env, "hdfs://path/", new HashMapStateBackend());
operator状态
算子状态是 Flink 中的任何非键状态。这包括但不限于对应用程序的任何使用CheckpointedFunction或BroadcastState在应用程序内的任何使用。读取算子状态时,用户指定算子 uid、状态名称和类型信息。
算子列表状态
CheckpointedFunction使用 usinggetListState可以读取存储在 using 中的算子状态ExistingSavepoint#readListState。状态名称和类型信息应与用于定义ListStateDescriptor在 DataStream 应用程序中声明此状态的那些信息相匹配。
DataStream<Integer> listState = savepoint.readListState<>(
"my-uid",
"list-state",
Types.INT);
Operator Union List State
CheckpointedFunction使用 usinggetUnionListState可以读取存储在 using 中的算子状态ExistingSavepoint#readUnionState。状态名称和类型信息应与用于定义ListStateDescriptor在 DataStream 应用程序中声明此状态的那些信息相匹配。框架将返回状态的单个副本,相当于恢复并行度为 1 的 DataStream。
DataStream<Integer> listState = savepoint.readUnionState<>(
"my-uid",
"union-state",
Types.INT);
广播状态
BroadcastState可以使用ExistingSavepoint#readBroadcastState. 状态名称和类型信息应与用于定义MapStateDescriptor在 DataStream 应用程序中声明此状态的那些信息相匹配。框架将返回状态的单个副本,相当于恢复并行度为 1 的 DataStream。
DataStream<Tuple2<Integer, Integer>> broadcastState = savepoint.readBroadcastState<>(
"my-uid",
"broadcast-state",
Types.INT,
Types.INT);
使用自定义序列化程序
TypeSerializers如果用于定义StateDescriptor写出状态的算子状态阅读器,则每个算子状态阅读器都支持使用自定义。
DataStream<Integer> listState = savepoint.readListState<>(
"uid",
"list-state",
Types.INT,
new MyCustomIntSerializer());
键控状态
键控状态或分区状态是相对于键进行分区的任何状态。读取键控状态时,用户指定operator id 和 KeyedStateReaderFunction<KeyType, OutputType>。
允许用户读取任意列和复杂的KeyedStateReaderFunction状态类型,例如 ListState、MapState 和 AggregatingState。这意味着如果算子包含有状态的过程功能,例如:
public class StatefulFunctionWithTime extends KeyedProcessFunction<Integer, Integer, Void> {
ValueState<Integer> state;
ListState<Long> updateTimes;
@Override
public void open(Configuration parameters) {
ValueStateDescriptor<Integer> stateDescriptor = new ValueStateDescriptor<>("state", Types.INT);
state = getRuntimeContext().getState(stateDescriptor);
ListStateDescriptor<Long> updateDescriptor = new ListStateDescriptor<>("times", Types.LONG);
updateTimes = getRuntimeContext().getListState(updateDescriptor);
}
@Override
public void processElement(Integer value, Context ctx, Collector<Void> out) throws Exception {
state.update(value + 1);
updateTimes.add(System.currentTimeMillis());
}
}
然后它可以通过定义一个输出类型和对应的KeyedStateReaderFunction.
DataStream<KeyedState> keyedState = savepoint.readKeyedState("my-uid", new ReaderFunction());
public class KeyedState {
public int key;
public int value;
public List<Long> times;
}
public class ReaderFunction extends KeyedStateReaderFunction<Integer, KeyedState> {
ValueState<Integer> state;
ListState<Long> updateTimes;
@Override
public void open(Configuration parameters) {
ValueStateDescriptor<Integer> stateDescriptor = new ValueStateDescriptor<>("state", Types.INT);
state = getRuntimeContext().getState(stateDescriptor);
ListStateDescriptor<Long> updateDescriptor = new ListStateDescriptor<>("times", Types.LONG);
updateTimes = getRuntimeContext().getListState(updateDescriptor);
}
@Override
public void readKey(
Integer key,
Context ctx,
Collector<KeyedState> out) throws Exception {
KeyedState data = new KeyedState();
data.key = key;
data.value = state.value();
data.times = StreamSupport
.stream(updateTimes.get().spliterator(), false)
.collect(Collectors.toList());
out.collect(data);
}
}
除了读取已注册的状态值外,每个键还可以访问Context带有元数据的元数据,例如已注册的事件时间和处理时间计时器。
注意:当使用 KeyedStateReaderFunction时,所有状态描述符都必须在 open 中先注册。任何尝试调用 RuntimeContext#get*State都会导致 a RuntimeException。
窗口状态
状态处理器 api 支持从窗口算子读取状态。读取窗口状态时,用户指定算子 ID、窗口分配器和聚合类型。
此外,WindowReaderFunction可以指定 一个 以使用类似于 WindowFunction或的附加信息来丰富每个读取ProcessWindowFunction。
假设一个 DataStream 应用程序计算每个用户每分钟的点击次数。
class Click {
public String userId;
public LocalDateTime time;
}
class ClickCounter implements AggregateFunction<Click, Integer, Integer> {
@Override
public Integer createAccumulator() {
return 0;
}
@Override
public Integer add(Click value, Integer accumulator) {
return 1 + accumulator;
}
@Override
public Integer getResult(Integer accumulator) {
return accumulator;
}
@Override
public Integer merge(Integer a, Integer b) {
return a + b;
}
}
DataStream<Click> clicks = ...;
clicks
.keyBy(click -> click.userId)
.window(TumblingEventTimeWindows.of(Time.minutes(1)))
.aggregate(new ClickCounter())
.uid("click-window")
.addSink(new Sink());
可以使用下面的代码读取此状态。
class ClickState {
public String userId;
public int count;
public TimeWindow window;
public Set<Long> triggerTimers;
}
class ClickReader extends WindowReaderFunction<Integer, ClickState, String, TimeWindow> {
@Override
public void readWindow(
String key,
Context<TimeWindow> context,
Iterable<Integer> elements,
Collector<ClickState> out) {
ClickState state = new ClickState();
state.userId = key;
state.count = elements.iterator().next();
state.window = context.window();
state.triggerTimers = context.registeredEventTimeTimers();
out.collect(state);
}
}
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
SavepointReader savepoint = SavepointReader.read(env, "hdfs://checkpoint-dir", new HashMapStateBackend());
savepoint
.window(TumblingEventTimeWindows.of(Time.minutes(1)))
.aggregate("click-window", new ClickCounter(), new ClickReader(), Types.String, Types.INT, Types.INT)
.print();
此外,触发器状态(来自CountTriggers 或自定义触发器)可以 Context#triggerState使用WindowReaderFunction.
编写新的保存点
Savepoint也可以编写,这允许诸如基于历史数据的引导状态之类的用例。每个保存点由一个或多个StateBootstrapTransformation’s 组成(解释如下),每个保存点都定义了单个算子的状态。
使用时SavepointWriter,您的应用程序必须在BATCH执行下执行。
注意状态处理器 api 目前不提供 Scala API。因此,它将始终使用 Java 类型堆栈自动派生序列化程序。要为 Scala DataStream API 引导保存点,请手动传入所有类型信息。
int maxParallelism = 128;
SavepointWriter
.newSavepoint(new HashMapStateBackend(), maxParallelism)
.withOperator("uid1", transformation1)
.withOperator("uid2", transformation2)
.write(savepointPath);
与每个算子关联的UID必须与分配给应用程序中的算子的 UID 一对一匹配DataStream;这些就是 Flink 如何知道什么状态映射到哪个算子。
operator state
CheckpointedFunction可以使用 . 创建简单的算子状态using StateBootstrapFunction。
public class SimpleBootstrapFunction extends StateBootstrapFunction<Integer> {
private ListState<Integer> state;
@Override
public void processElement(Integer value, Context ctx) throws Exception {
state.add(value);
}
@Override
public void snapshotState(FunctionSnapshotContext context) throws Exception {
}
@Override
public void initializeState(FunctionInitializationContext context) throws Exception {
state = context.getOperatorState().getListState(new ListStateDescriptor<>("state", Types.INT));
}
}
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStream<Integer> data = env.fromElements(1, 2, 3);
StateBootstrapTransformation transformation = OperatorTransformation
.bootstrapWith(data)
.transform(new SimpleBootstrapFunction());
广播状态
BroadcastState可以使用BroadcastStateBootstrapFunction. DataStream与API中的广播状态类似,完整状态必须适合内存。
public class CurrencyRate {
public String currency;
public Double rate;
}
public class CurrencyBootstrapFunction extends BroadcastStateBootstrapFunction<CurrencyRate> {
public static final MapStateDescriptor<String, Double> descriptor =
new MapStateDescriptor<>("currency-rates", Types.STRING, Types.DOUBLE);
@Override
public void processElement(CurrencyRate value, Context ctx) throws Exception {
ctx.getBroadcastState(descriptor).put(value.currency, value.rate);
}
}
DataStream<CurrencyRate> currencyDataSet = env.fromCollection(
new CurrencyRate("USD", 1.0), new CurrencyRate("EUR", 1.3));
StateBootstrapTransformation<CurrencyRate> broadcastTransformation = OperatorTransformation
.bootstrapWith(currencyDataSet)
.transform(new CurrencyBootstrapFunction());
键控状态
ProcessFunction’s 和其他类型的键控状态RichFunction可以使用KeyedStateBootstrapFunction.
public class Account {
public int id;
public double amount;
public long timestamp;
}
public class AccountBootstrapper extends KeyedStateBootstrapFunction<Integer, Account> {
ValueState<Double> state;
@Override
public void open(Configuration parameters) {
ValueStateDescriptor<Double> descriptor = new ValueStateDescriptor<>("total",Types.DOUBLE);
state = getRuntimeContext().getState(descriptor);
}
@Override
public void processElement(Account value, Context ctx) throws Exception {
state.update(value.amount);
}
}
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStream<Account> accountDataSet = env.fromCollection(accounts);
StateBootstrapTransformation<Account> transformation = OperatorTransformation
.bootstrapWith(accountDataSet)
.keyBy(acc -> acc.id)
.transform(new AccountBootstrapper());
支持设置事件KeyedStateBootstrapFunction时间和处理时间计时器。定时器不会在引导函数内部触发,只有在DataStream应用程序中恢复后才会激活。如果设置了处理时间计时器,但直到该时间过去后状态才恢复,则计时器将在启动时立即触发。
注意如果您的引导函数创建计时器,则只能使用其中一种进程类型函数来恢复状态。
窗口状态
状态处理器 api 支持窗口operator的写入状态。在编写窗口状态时,用户指定operator id、窗口分配者、剔除器、可选触发器和聚合类型。引导转换上的配置与 DataStream 窗口上的配置相匹配是很重要的。
public class Account {
public int id;
public double amount;
public long timestamp;
}
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStream<Account> accountDataSet = env.fromCollection(accounts);
StateBootstrapTransformation<Account> transformation = OperatorTransformation
.bootstrapWith(accountDataSet)
.keyBy(acc -> acc.id)
.window(TumblingEventTimeWindows.of(Time.minutes(5)))
.reduce((left, right) -> left + right);
修改保存点
除了从头开始创建保存点之外,您还可以基于现有保存点创建一个保存点,例如在为现有作业引导单个新算子时。
SavepointWriter
.fromExistingSavepoint(oldPath, new HashMapStateBackend())
.withOperator("uid", transformation)
.write(newPath);