数据连接与集成构建管道Streaming pipelines流式有状态变换

注意:以下翻译的准确性尚未经过验证。这是使用 AIP ↗ 从原始英文文本进行的机器翻译。

流式有状态变换

Foundry管道提供有状态的数据流变换,以快速流式传输速度实现复杂的数据变换行为。

什么是状态?

在数据流中,当每个输出行可能依赖于先前处理的行中包含的信息时,数据变换是有状态的。行间持久存在的信息称为状态。每行可以访问和改变状态。

有状态变换的一个例子是求和聚合变换。考虑下表,其中可以使用求和聚合变换来计算每天具有hot值的传感器读数的数量。最新流入的行在表中较高。

DaySensorReadingTimestamp
Mondayhot3
Mondaycold2
Mondayhot1

一个有状态的求和聚合变换计算每天hot读数的实时运行数量,其输出可能如下所示:

DaySensorReadingTimestampState
Mondayhot32
Mondaycold21
Mondayhot11

有状态变换可以存储任何可序列化类型的状态,包括整个行,这使得它们能够实现复杂的行为。Pipeline Builder中的状态变换是预构建的,并自动处理状态类型及其演变方式。

状态的内容并不总是用户可访问的。状态可以用于支持后端处理行为。例如,在数据管道Flink任务中读取流输入源将存储每个分区的最后偏移量状态;这使得任务能够在失败或重启后从上次成功点恢复。

为什么状态在数据流中如此强大?

Foundry数据流使用Flink架构提供低延迟的数据管道;每行在处理后立即计算并传递到下一个操作。与批处理变换不同,流变换逐行确定下一个输出,而不是查看完整的数据。

对于非有状态(也称为无状态)流变换,这种架构意味着变换逻辑只能依赖于一行。例如,一个无状态变换总是可以将整数列加5。

相比之下,有状态流变换可以访问以前行的持久数据,同时仍然能够逐行处理,并在它们到达时立即处理。

Foundry有状态流提供精确一次保证选项,这是默认的管道配置。当选择时,导致状态变更的行被保证每个键精确且按顺序地变更状态。这使得精确和复杂的数据流行为成为可能。

例如,如果您打算求和,即使流重启或出现故障,您的求和结果也将始终准确。如果使用排序,即使在任务中排序过程中重启任务,任务已产生部分排序输出不再实时,排序也将始终对每个输入只生成一个输出。

键控状态

所有Pipeline Builder有状态流变换都使用键控状态,并要求用户指定分区键列。有状态变换对于具有不同键列值的行分别处理。这允许后端并行化处理并扩展到大数据量。

例如,考虑有状态求和聚合示例,计算每天hot读数的实时运行计数。请注意,在此示例中,Day列被用作分区键。

DaySensorReadingTimestampState
Tuesdayhot51
Mondayhot42
Tuesdaycold30
Mondaycold21
Mondayhot11

注意,状态独立于Day值为Monday的行和值为Tuesday的行计算。Tuesday键值行的出现不影响Monday的存储内容,并且如果有更多行到达Day列中具有不同值(如Wednesday),这些键的状态将不受影响。

键应仔细选择,因为导致记录分布不均的键可能人为增加负载并限制吞吐量。参见流键最佳实践

事件时间和水印

有状态流变换通常依赖于时间信息。因为流是持续的,并且可能随时接收到新的实时行,所以将时间上接近的行分组在一起通常是有意义的。例如,外部缓存合并变换仅在它们共享合并列的值并且行的时间戳在过期限制内时,将两个输入流的行合并在一起。Pipeline Builder流使用Flink事件时间以接近确定性的方式实现有状态变换,这将在重放时产生相同或非常相似的输出。

Pipeline Builder中执行基于时间的操作的有状态变换需要在上游进行指派时间戳和水印变换,并且如果在您的管道图中缺少它,将产生验证错误。指派时间戳和水印为每行分配一个"事件时间",通常是行中包含的时间戳列。水印是每个变换操作的近似确定性"当前时间"的概念,是一个单调递增的值,紧跟在任何输入行中看到的最大事件时间值之后。例如,当外部缓存合并确定缓存中的条目是否已过期时,它检查水印是否大于或等于过期时间,这只有在合并已接收到至少具有该事件时间的输入行时才为true。

对于单个流输入的变换操作符,水印是每个并行实例看到的最大事件时间行的最小值。对于具有多个流输入的变换,水印是输入的水印的最小值。

重放将产生相似但有时略有不同的输出。这是因为在重放时,不同的分区键可能被分配给不同的Flink并行实例,并且具有多个输入的操作符可能在上游以不同的速度处理。Flink处理时间不被支持且不被推荐,因为它在重放时可能产生显著不同且可能不直观的结果。

状态过期

存储大型状态可能导致性能瓶颈,负面影响吞吐量和延迟,因此Pipeline Builder要求用户限制状态大小。

通常,状态通过用户提供的缓存时间过期来限制。对于需要缓存时间参数的有状态变换,状态通常存储在每个键的状态缓存中,直到水印超过该键所见的最后事件时间加上过期。

窗口和触发器

聚合窗口变换允许用户设置*窗口,这是将行及其状态分组在一起的策略,以及触发器*,这是聚合应何时产生输出的策略。

窗口

当前支持的窗口有:

  • **滚动事件时间:**将时间划分为固定长度、无重叠、连续的区间。具有相同键且事件时间在同一区间内的行被分组在一起。例如,您可以将同一日期所有在同一小时内事件时间的行分组在一起。
  • **计数:**给定用户指定的计数n,对于每个键,将最新的n行与该键分组在一起。
  • **会话:**将属于同一会话的行分组在一起。如果它们共享一个键,并且在事件时间中没有超过用户指定的会话间隔的行中断,则行属于同一会话。例如,在包含流平台用户操作数据的数据集中,您可以将一个用户工作流的所有行分组在一起,直到用户休息。

依赖时间的窗口(例如滚动事件时间窗口和会话窗口)将在水印足够推进时最终关闭。

  • 如果未设置或为零允许延迟,则窗口保持打开状态,直到水印通过窗口的结束时间,此时窗口关闭,可能产生输出,并删除其状态。
  • 如果指定了允许延迟,窗口将保持打开状态,直到水印通过窗口结束加上允许延迟。这允许晚到或无序记录仍然是窗口的一部分,即使水印已经过了窗口的结束。
  • 当水印超过窗口结束加上允许延迟时到达的行将始终被丢弃,因为窗口已经关闭并删除了其状态。

依赖时间的窗口还允许指定自定义触发器。

触发器

当前支持的触发器有:

  • **水印后触发器:**当水印通过窗口的结束时触发窗口输出。允许为水印在窗口结束之前以及水印在窗口之后(当窗口仍然活着因其允许延迟)时指定其他自定义触发器。例如,用户可能希望在窗口关闭之前没有输出,但希望在允许延迟期间看到每个晚到记录的输出。
  • **计数触发器:**给定用户指定的计数n,当接收到n行的倍数时,为每个键触发窗口输出。
  • **窗口关闭触发器:**仅在窗口关闭并删除状态时触发输出。每个窗口仅触发一次输出,并且仅在窗口结束时触发。

有状态流的最佳实践

大型状态可能对性能产生负面影响,因此在设计有状态管道时,建议使用尽可能紧凑的状态过期策略。这通常意味着不要将缓存时间过期设置得比必要的大,也不要为计数窗口设置比必要大的计数。

对于需要大型状态的管道,性能(包括吞吐量、检查点持续时间和延迟)随着Flink任务的并行性而扩展。并行性可以在流管道设置中编辑,较大的并行性允许增加的数据处理能力以及状态读写速度。

适当的键应为有状态变换选择,因为键列的值过多或行分布不均衡可能导致瓶颈或扩展问题。