为了展示所提供的 API,我们将从一个示例开始,然后再展示它们的全部功能。作为我们的运行示例,我们将使用这样的情况:我们有不同颜色和形状的对象流,并且我们想要找到遵循特定模式的相同颜色的对象对,例如矩形后面跟着三角形。我们假设这组有趣的模式会随着时间而演变。
在此示例中,第一个流将包含Item具有一个Color和一个Shape属性的类型元素。另一个流将包含Rules.
从 Items流开始,我们只需要用 来键入它,Color因为我们想要相同颜色的对。这将确保相同颜色的元素最终出现在同一台物理机器上。
// key the items by color
KeyedStream<Item, Color> colorPartitionedStream = itemStream
.keyBy(new KeySelector<Item, Color>(){...});
继续前进Rules,包含它们的流应该被广播到所有下游任务,并且这些任务应该将它们存储在本地,以便它们可以根据所有传入Items的 . 下面的代码片段将 i) 广播规则流和 ii) 使用提供的MapStateDescriptor,它将创建将存储规则的广播状态。
// a map descriptor to store the name of the rule (string) and the rule itself.
MapStateDescriptor<String, Rule> ruleStateDescriptor = new MapStateDescriptor<>(
"RulesBroadcastState",
BasicTypeInfo.STRING_TYPE_INFO,
TypeInformation.of(new TypeHint<Rule>() {}));
// broadcast the rules and create the broadcast state
BroadcastStream<Rule> ruleBroadcastStream = ruleStream
.broadcast(ruleStateDescriptor);
最后,为了评估Rules来自流的传入元素Item,我们需要:
将流(键控或非键控)与 一个连接BroadcastStream可以通过调用connect()非广播流来完成,并将 BroadcastStream作为参数。这将返回一个BroadcastConnectedStream,我们可以在其上调用process()一个特殊类型的CoProcessFunction。该函数将包含我们的匹配逻辑。函数的确切类型取决于非广播流的类型:
鉴于我们的非广播流是键控的,以下代码段包含上述调用:
应在非广播流上调用连接,并将 BroadcastStream 作为参数。
DataStream<String> output = colorPartitionedStream
.connect(ruleBroadcastStream)
.process(
// type arguments in our KeyedBroadcastProcessFunction represent:
// 1. the key of the keyed stream
// 2. the type of elements in the non-broadcast side
// 3. the type of elements in the broadcast side
// 4. the type of the result, here a string
new KeyedBroadcastProcessFunction<Color, Item, Rule, String>() {
// my matching logic
}
);
与 CoProcessFunction情况一样,这些函数有两种处理方法要实现;哪个processBroadcastElement() 负责处理广播流中的传入元素,processElement()用于非广播流。方法的完整签名如下所示:
public abstract class BroadcastProcessFunction<IN1, IN2, OUT> extends BaseBroadcastProcessFunction {
public abstract void processElement(IN1 value, ReadOnlyContext ctx, Collector<OUT> out) throws Exception;
public abstract void processBroadcastElement(IN2 value, Context ctx, Collector<OUT> out) throws Exception;
}
public abstract class KeyedBroadcastProcessFunction<KS, IN1, IN2, OUT> {
public abstract void processElement(IN1 value, ReadOnlyContext ctx, Collector<OUT> out) throws Exception;
public abstract void processBroadcastElement(IN2 value, Context ctx, Collector<OUT> out) throws Exception;
public void onTimer(long timestamp, OnTimerContext ctx, Collector<OUT> out) throws Exception;
}
首先要注意的是,这两个函数都需要实现广播端的元素processBroadcastElement()处理方法和processElement()非广播端的元素处理方法。
这两种方法在所提供的上下文中有所不同。非广播方有ReadOnlyContext,而广播方有Context。
这两个上下文(ctx在以下枚举中):
stateDescriptor中的应该getBroadcastState()与.broadcast(ruleStateDescriptor) 上面的相同。
不同之处在于两端对广播状态的访问类型。广播端对其具有 读写访问权限,而非广播端具有只读访问权限(因此是名称)。原因是在 Flink 中没有跨任务通信。因此,为了保证广播状态中的内容在我们的操作符的所有并行实例中是相同的,我们只给广播端提供读写访问权限,广播端在所有任务中看到相同的元素,我们需要在每个任务上进行计算该侧的传入元素在所有任务中都是相同的。忽略此规则会破坏状态的一致性保证,导致结果不一致且通常难以调试。
processBroadcastElement()中实现的逻辑必须在所有并行实例中具有相同的确定性行为!
最后,由于KeyedBroadcastProcessFunction在键控流上运行,它公开了一些BroadcastProcessFunction. 那是:
注册计时器只能在processElement()且KeyedBroadcastProcessFunction 仅在 那里才有可能。在该方法中这是不可能的processBroadcastElement(),因为没有与广播元素关联的键。
回到我们原来的例子,我们KeyedBroadcastProcessFunction可能看起来像下面这样:
new KeyedBroadcastProcessFunction<Color, Item, Rule, String>() {
// store partial matches, i.e. first elements of the pair waiting for their second element
// we keep a list as we may have many first elements waiting
private final MapStateDescriptor<String, List<Item>> mapStateDesc =
new MapStateDescriptor<>(
"items",
BasicTypeInfo.STRING_TYPE_INFO,
new ListTypeInfo<>(Item.class));
// identical to our ruleStateDescriptor above
private final MapStateDescriptor<String, Rule> ruleStateDescriptor =
new MapStateDescriptor<>(
"RulesBroadcastState",
BasicTypeInfo.STRING_TYPE_INFO,
TypeInformation.of(new TypeHint<Rule>() {}));
@Override
public void processBroadcastElement(Rule value,
Context ctx,
Collector<String> out) throws Exception {
ctx.getBroadcastState(ruleStateDescriptor).put(value.name, value);
}
@Override
public void processElement(Item value,
ReadOnlyContext ctx,
Collector<String> out) throws Exception {
final MapState<String, List<Item>> state = getRuntimeContext().getMapState(mapStateDesc);
final Shape shape = value.getShape();
for (Map.Entry<String, Rule> entry :
ctx.getBroadcastState(ruleStateDescriptor).immutableEntries()) {
final String ruleName = entry.getKey();
final Rule rule = entry.getValue();
List<Item> stored = state.get(ruleName);
if (stored == null) {
stored = new ArrayList<>();
}
if (shape == rule.second && !stored.isEmpty()) {
for (Item i : stored) {
out.collect("MATCH: " + i + " - " + value);
}
stored.clear();
}
// there is no else{} to cover if rule.first == rule.second
if (shape.equals(rule.first)) {
stored.add(value);
}
if (stored.isEmpty()) {
state.remove(ruleName);
} else {
state.put(ruleName, stored);
}
}
}
}
在描述了所提供的 API 之后,本节将重点介绍使用广播状态时要记住的重要事项。这些是:
不存在跨任务通信:如前所述,这就是为什么只有广播方 (Keyed)-BroadcastProcessFunction可以修改广播状态的内容的原因。此外,用户必须确保所有任务对每个传入元素都以相同的方式修改广播状态的内容。否则,不同的任务可能会有不同的内容,导致结果不一致。
广播状态中的事件顺序可能因任务而异:尽管广播流的元素可以保证所有元素(最终)都会到达所有下游任务,但元素可能以不同的顺序到达每个任务。因此,每个传入元素的状态更新不得依赖于传入事件的顺序。
所有任务都检查其广播状态:虽然所有任务在发生检查点时在其广播状态中具有相同的元素(检查点屏障不会越过元素),但所有任务都会检查其广播状态,而不仅仅是其中一个。这是一个设计决策,以避免在还原期间从同一文件读取所有任务(从而避免热点),尽管它的代价是将检查点状态的大小增加了 p 倍(= 并行度)。Flink 保证在恢复/重新缩放时不会有重复和丢失数据。在以相同或更小的并行度进行恢复的情况下,每个任务都会读取其检查点状态。扩大规模后,每个任务读取自己的状态,其余任务(p_new-p_old) 以循环方式读取先前任务的检查点。
没有 RocksDB 状态后端:广播状态在运行时保存在内存中,并且应该相应地进行内存配置。这适用于所有算子状态。
Flink 中的每个函数和运算符都可以是有状态的(有关详细信息,请参阅使用状态)。有状态的函数在单个元素/事件的处理过程中存储数据,使状态成为任何类型的更精细操作的关键构建块。
为了使状态容错,Flink 需要检查点状态。检查点允许 Flink 恢复流中的状态和位置,从而为应用程序提供与无故障执行相同的语义。
关于流式容错的文档详细描述了 Flink 流式容错机制背后的技术。
Flink 的检查点机制与流和状态的持久存储交互。一般来说,它需要:
默认情况下,检查点是禁用的。要启用检查点,请调用enableCheckpointing(n),StreamExecutionEnvironment其中n是检查点间隔,以毫秒为单位。
检查点的其他参数包括:
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// start a checkpoint every 1000 ms
env.enableCheckpointing(1000);
// advanced options:
// set mode to exactly-once (this is the default)
env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);
// make sure 500 ms of progress happen between checkpoints
env.getCheckpointConfig().setMinPauseBetweenCheckpoints(500);
// checkpoints have to complete within one minute, or are discarded
env.getCheckpointConfig().setCheckpointTimeout(60000);
// only two consecutive checkpoint failures are tolerated
env.getCheckpointConfig().setTolerableCheckpointFailureNumber(2);
// allow only one checkpoint to be in progress at the same time
env.getCheckpointConfig().setMaxConcurrentCheckpoints(1);
// enable externalized checkpoints which are retained
// after job cancellation
env.getCheckpointConfig().setExternalizedCheckpointCleanup(
ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION);
// enables the unaligned checkpoints
env.getCheckpointConfig().enableUnalignedCheckpoints();
// sets the checkpoint storage where checkpoint snapshots will be written
env.getCheckpointConfig().setCheckpointStorage("hdfs:///my/checkpoint/dir");
// enable checkpointing with finished tasks
Configuration config = new Configuration();
config.set(ExecutionCheckpointingOptions.ENABLE_CHECKPOINTS_AFTER_TASKS_FINISH, true);
env.configure(config);
可以通过以下方式设置更多参数和/或默认值conf/flink-conf.yaml(请参阅配置以获取完整指南):
Flink 的检查点机制将所有状态的一致快照存储在计时器和有状态操作符中,包括连接器、窗口和任何用户定义的状态。检查点的存储位置(例如,JobManager 内存、文件系统、数据库)取决于配置的 Checkpoint Storage。
默认情况下,检查点存储在 JobManager 的内存中。为了正确持久化大状态,Flink 支持在其他位置检查点状态的各种方法。检查点存储的选择可以通过配置StreamExecutionEnvironment.getCheckpointConfig().setCheckpointStorage(…)。强烈建议将检查点存储在用于生产部署的高可用性文件系统中。
有关作业范围和集群范围配置的可用选项的更多详细信息,请参阅检查点存储。
Flink 目前只为没有迭代的作业提供处理保证。在迭代作业上启用检查点会导致异常。为了在迭代程序上强制检查点,用户需要在启用检查点时设置一个特殊标志:env.enableCheckpointing(interval, CheckpointingMode.EXACTLY_ONCE, force = true).
请注意,循环边缘中的飞行记录(以及与之相关的状态更改)将在失败期间丢失。
从 Flink 1.14 开始,即使部分作业图已完成所有数据的处理,也可以继续执行检查点,如果它包含有界源,则可能会发生这种情况。从 1.15 开始默认启用此功能,并且可以通过功能标志禁用它:
Configuration config = new Configuration();
config.set(ExecutionCheckpointingOptions.ENABLE_CHECKPOINTS_AFTER_TASKS_FINISH, false);
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(config);
一旦任务/子任务完成,它们就不再对检查点做出贡献。在实现任何自定义运算符或 UDF(用户定义函数)时,这是一个重要的考虑因素。
为了支持对已完成任务进行检查点,我们调整了任务生命周期 并引入了 StreamOperator#finish 方法。此方法有望成为刷新任何剩余缓冲状态的明确截止点。在调用完成方法之后采取的所有检查点都应该是空的(在大多数情况下)并且不应该包含任何缓冲数据,因为没有办法发出这些数据。一个值得注意的例外是,如果您的operator有一些指向外部系统中事务的指针(即,为了实现精确一次语义)。在这种情况下,在调用finish()方法应该保留一个指针,指向将在操作员关闭之前在最终检查点提交的最后一个事务。一个很好的内置示例是exact-once sinks 和TwoPhaseCommitSinkFunction.
有一个特殊的处理方法UnionListState,它经常被用来在外部系统中实现对偏移量的全局视图(即存储 Kafka 分区的当前偏移量)。如果我们丢弃了调用了它的方法的单个子任务的状态close,我们将丢失分配给它的分区的偏移量。为了解决这个问题,我们让检查点只有在没有或所有使用UnionListState的子任务完成时才成功。
我们还没有看到ListState以类似的方式使用过,但是您应该知道,在该close方法之后设置的任何状态检查点都将被丢弃并且在还原后不可用。
任何准备重新调整的算子都应该很好地处理部分完成的任务。从只完成一部分任务的检查点恢复,相当于恢复这样一个任务,其中新子任务的数量等于正在运行的任务的数量。
为了确保使用两阶段提交的算子可以提交所有记录,任务将在所有操作员完成后等待最终检查点成功完成。需要注意的是,这种行为会延长任务的执行时间。如果检查点间隔较长,执行时间也会大大延长。在最坏的情况下,如果检查点间隔设置为Long.MAX_VALUE,则任务实际上将永远被阻塞,因为最终的检查点永远不会发生。
可查询状态的客户端 API 当前处于不断发展的状态,并且无法保证所提供接口的稳定性。在即将到来的 Flink 版本中,客户端很可能会出现重大的 API 更改。
简而言之,此功能将 Flink 的托管键控(分区)状态(请参阅使用状态)暴露给外部世界,并允许用户从 Flink 外部查询作业的状态。对于某些场景,可查询状态消除了与外部系统(例如键值存储)的分布式操作/事务的需要,这在实践中通常是瓶颈。此外,此功能对于调试目的可能特别有用。
查询状态对象时,该对象是从并发线程访问的,无需任何同步或复制。这是一种设计选择,因为上述任何一种情况都会导致作业延迟增加,而我们希望避免这种情况。由于任何使用 Java 堆空间的状态后端, 例如 HashMapStateBackend,在检索值时不使用副本,而是直接引用存储的值,所以读-修改-写模式是不安全的,并且可能导致可查询状态服务器由于并发修改而失败。从这些EmbeddedRocksDBStateBackend问题中是安全的。
在展示如何使用可查询状态之前,简要描述组成它的实体很有用。可查询状态功能由三个主要实体组成:
客户端连接到其中一个代理并发送与特定密钥关联的状态的请求k。正如使用 State中所述,键控状态是按 Key Groups组织的,并且每个TaskManager都分配有许多这样的键组。要发现哪个TaskManager负责关键组持有k,代理会询问JobManager。根据答案,代理将查询与相关的状态的QueryableStateServer运行,并将响应转发回客户端。
要在 Flink 集群上启用可查询状态,您需要执行以下操作:
将Flink 发行版文件夹中的复制到该flink-queryable-state-runtime-1.15.0.jar 文件夹中。opt/lib/
将属性设置queryable-state.enable为true。有关详细信息和其他参数,请参阅配置文档。
要验证您的集群是否在启用可查询状态的情况下运行,请检查任何任务管理器的日志中的以下行:“Started the Queryable State Proxy Server @ …”。
现在您已经在集群上激活了可查询状态,是时候看看如何使用它了。为了使状态对外界可见,需要使用以下命令显式地使其可查询:
以下部分解释了这两种方法的使用。
调用.asQueryableState(stateName, stateDescriptor) 作用到KeyedStream返回 QueryableStateStream,它提供其值作为可查询状态。根据状态的类型,该asQueryableState() 方法有以下变体:
// ValueState
QueryableStateStream asQueryableState(
String queryableStateName,
ValueStateDescriptor stateDescriptor)
// Shortcut for explicit ValueStateDescriptor variant
QueryableStateStream asQueryableState(String queryableStateName)
// ReducingState
QueryableStateStream asQueryableState(
String queryableStateName,
ReducingStateDescriptor stateDescriptor)
注意:没有可查询的接收ListState器,因为它会导致一个不断增长的列表,可能不会被清理,因此最终会消耗太多的内存。
返回的QueryableStateStream可以看作是一个接收器,不能进一步转换。在内部,a QueryableStateStream被转换为使用所有传入记录来更新可查询状态实例的运算符。调用中StateDescriptor提供的类型暗示了更新逻辑。asQueryableState在像下面这样的程序中,键控流的所有记录将用于通过以下方式更新状态实例 ValueState.update(value):
stream.keyBy(value -> value.f0).asQueryableState("query-name");
这就像 Scala API 的flatMapWithState.
operator的托管键控状态(请参阅使用托管键控状态)可以通过使适当的状态描述符可查询来实现查询 StateDescriptor.setQueryable(String queryableStateName),如下例所示:
ValueStateDescriptor<Tuple2<Long, Long>> descriptor =
new ValueStateDescriptor<>(
"average", // the state name
TypeInformation.of(new TypeHint<Tuple2<Long, Long>>() {})); // type information
descriptor.setQueryable("query-name"); // queryable state name
注意:参数queryableStateName可以任意选择,仅用于查询。它不必与state自己的名称相同。
这个变体对于可以查询哪种类型的状态没有限制。这意味着这可以用于任何ValueState, ReduceState, ListState,MapState和AggregatingState.
到目前为止,您已将集群设置为以可查询状态运行,并且您已将(部分)状态声明为可查询。现在是时候看看如何查询这个状态了。
为此,您可以使用QueryableStateClient帮助程序类。这在flink-queryable-state-client.jar 中可用,它必须作为依赖项显式包含在pom.xml项目的 中flink-core,如下所示:
<dependency>
<groupId>org.apache.flinkgroupId>
<artifactId>flink-coreartifactId>
<version>1.15.0version>
dependency>
<dependency>
<groupId>org.apache.flinkgroupId>
<artifactId>flink-queryable-state-client-javaartifactId>
<version>1.15.0version>
dependency>
有关这方面的更多信息,您可以查看如何设置 Flink 程序。
会将您的QueryableStateClient查询提交给内部代理,然后由内部代理处理您的查询并返回最终结果。初始化客户端的唯一要求是提供有效的TaskManager主机名(请记住,每个任务管理器上都有一个可查询的状态代理)和代理侦听的端口。更多关于如何在配置部分配置代理和状态服务器端口。
QueryableStateClient client = new QueryableStateClient(tmHostname, proxyPort);
客户端准备好后,要查询与 type V键关联的 type状态K,可以使用以下方法:
CompletableFuture<S> getKvState(
JobID jobId,
String queryableStateName,
K key,
TypeInformation<K> keyTypeInfo,
StateDescriptor<S, V> stateDescriptor)
上面返回一个CompletableFuture,最终保存可查询状态实例的状态值,该状态实例由ID为jobID的作业的queryableStateName标识。键是您感兴趣的键的状态,keyTypeInfo将告诉Flink如何序列化/反序列化它。最后,状态描述符包含关于请求状态的必要信息,即它的类型(Value、Reduce等)和关于如何序列化/反序列化它的必要信息。
细心的读者会注意到,返回的未来包含一个类型的值S,即一个State包含实际值的对象。这可以是 Flink 支持的任何状态类型:ValueState、ReduceState、ListState、MapState和AggregatingState。
注意:这些状态对象不允许修改包含的状态。您可以使用它们来获取状态的实际值,例如使用valueState.get(),或迭代包含的
条目,例如使用mapState.entries(),但您不能修改它们。例如,add()在返回的列表状态上调用该方法将抛出一个 UnsupportedOperationException.
注意:客户端是异步的,可以被多个线程共享。它需要QueryableStateClient.shutdown()在未使用时关闭以释放资源。
以下示例通过使其可查询来扩展CountWindowAverage示例(请参阅使用托管键控状态)并显示如何查询此值:
public class CountWindowAverage extends RichFlatMapFunction<Tuple2<Long, Long>, Tuple2<Long, Long>> {
private transient ValueState<Tuple2<Long, Long>> sum; // a tuple containing the count and the sum
@Override
public void flatMap(Tuple2<Long, Long> input, Collector<Tuple2<Long, Long>> out) throws Exception {
Tuple2<Long, Long> currentSum = sum.value();
currentSum.f0 += 1;
currentSum.f1 += input.f1;
sum.update(currentSum);
if (currentSum.f0 >= 2) {
out.collect(new Tuple2<>(input.f0, currentSum.f1 / currentSum.f0));
sum.clear();
}
}
@Override
public void open(Configuration config) {
ValueStateDescriptor<Tuple2<Long, Long>> descriptor =
new ValueStateDescriptor<>(
"average", // the state name
TypeInformation.of(new TypeHint<Tuple2<Long, Long>>() {})); // type information
descriptor.setQueryable("query-name");
sum = getRuntimeContext().getState(descriptor);
}
}
在作业中使用后,您可以检索作业 ID,然后从此运算符查询任何键的当前状态:
QueryableStateClient client = new QueryableStateClient(tmHostname, proxyPort);
// the state descriptor of the state to be fetched.
ValueStateDescriptor<Tuple2<Long, Long>> descriptor =
new ValueStateDescriptor<>(
"average",
TypeInformation.of(new TypeHint<Tuple2<Long, Long>>() {}));
CompletableFuture<ValueState<Tuple2<Long, Long>>> resultFuture =
client.getKvState(jobId, "query-name", key, BasicTypeInfo.LONG_TYPE_INFO, descriptor);
// now handle the returned value
resultFuture.thenAccept(response -> {
try {
Tuple2<Long, Long> res = response.get();
} catch (Exception e) {
e.printStackTrace();
}
});
Flink 提供了不同的状态后端来指定状态的存储方式和位置。
状态可以位于 Java 的堆或堆外。根据您的状态后端,Flink 还可以管理应用程序的状态,这意味着 Flink 处理内存管理(如果需要,可能会溢出到磁盘)以允许应用程序保持非常大的状态。默认情况下,配置文件flink-conf.yaml决定了所有 Flink 作业的状态后端。
但是,可以基于每个作业覆盖默认状态后端,如下所示。
有关可用状态后端、它们的优势、限制和配置参数的更多信息,请参阅部署和操作中的相应部分。
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setStateBackend(...);