文章目录

  • 将应用程序状态映射到数据集
  • 读取状态
  • 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 是无状态的。

flink metrics 监控 flink监控api_flink metrics 监控


MyApp 的保存点或检查点由所有状态的数据组成,以可以恢复每个任务的状态的方式组织。当使用批处理作业处理保存点(或检查点)的数据时,我们需要一个心智模型,将各个任务状态的数据映射到数据集或表中。实际上,我们可以将保存点视为数据库。每个算子(由其 UID 标识)代表一个命名空间。算子的每个算子状态都映射到命名空间中的一个专用表,其中有一列保存所有任务的状态数据。算子的所有键状态都映射到一个表,该表由键的一列和每个键状态的一列组成。下图显示了 MyApp 的保存点如何映射到数据库。

flink metrics 监控 flink监控api_ide_02


该图显示了 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);