「Flink实时数据分析系列」7.有状态算子和应用(上)

来源 | 「Stream Processing with Apache Flink」

作者 | Fabian Hueske and Vasiliki Kalavri

翻译 | 吴邪 大数据4年从业经验,目前就职于广州一家互联网公司,负责大数据基础平台自研、离线计算&实时计算研究

校对 | gongyouliu

编辑 | auroral-L

全文共8574字,预计阅读时间40分钟。

目录

一、实现有状态函数

       1.在RuntimeContext 中声明Keyed State

       2.使用ListCheckpointed 接口实现算子List State

      3.接收检查点完成通知 

二、为有状态的应用开启故障恢复 

       1.确保有状态应用的可维护性 

       2.指定算子唯一标识 

       3.定义keyed state算子的最大并行度

有状态算子和用户函数是流处理应用程序的常见组成部分,实际上,大多数重要的操作都需要对数据和部分中间结果进行记录,因为数据是一直流动的状态,并且会随着时间的推移到达接收端。Flink很多内置的DataStream算子、sources和sinks都是有状态的,可以用作数据缓存或者维护部分中间计算结果以及记录元数据,例如,窗口算子使用ProcessWindowFunction去收集输入数据,或者使用ReduceFunction输出结果,ProcessFunction还可以使用定时器和一些sink函数用于维护事务的状态,以保证精确的一致性功能。除了内置的算子和提供的source及sink之外,Flink的Datastream API还提供了用户自定义函数(UDF)注册、维护和状态访问的接口。

有状态流处理涉及了流处理的方方面面,比如故障恢复和内存管理,以及流应用程序的维护等等。第2章「流处理基本概念」和第3章「Apache Flink的体系架构」中我们分别讨论有状态流处理的基础以及Flink架构方面的细节。第9章阐述了如何设置和配置Flink从而实现可靠地处理有状态应用程序,第10章给出了如何操作有状态应用的引导--从应用程序保存点获取和恢复、重新扩展应用程序和升级应用程序。

本章重点聚焦在有状态UDF的实现,并讨论有状态应用程序的性能和健壮性。具体而言,我们将介绍如何在UDF中进行不同类型的状态交互和定义,我们还会讨论性能方面的问题以及如何控制函数状态的大小,最后,我们将说明如何将key状态配置为可查询状态,以及如何通过外部应用程序进行访问。

一、实现有状态函数

在“状态管理”一节中,我们介绍了函数主要有两种状态:keyed state 和operator state,Flink提供了多个接口来定义有状态函数,在本节中,我们将阐述如何实现keyed state和operator state的函数。

1.在RuntimeContext 中声明Keyed State

用户函数可以使用keyed state在key属性上下文中存储和访问状态。对于key属性的每个不同值,Flink维护一个状态实例。函数的keyed state实例分布在函数算子的所有并行任务中。这意味着函数的每个并行实例负责key的一个子集并维护相应的状态实例。因此,keyed sate非常类似于分布式键值映射。有关keyed state的更多细节,请参见“状态管理”。

keyed state只能通过应用在KeyedStream上的函数使用。KeyedStream是通过调用DataStream.keyBy()方法构造出来的,该方法定义了一个流上的key。KeyedStream在指定的key上分区并记住key定义。应用于KeyedStream上的算子同时也会应用于其key定义的上下文中。

Flink为key状态提供了多个原语。状态原语定义了单个键的状态结构。正确的状态原语的选择取决于函数如何与状态交互。这种选择还会影响函数的性能,因为每个状态,底层都为这些原语提供了自己的实现。Flink支持以下状态原语:

  • ValueState[T]保存一个类型为T的值。可以使用ValueState.value()获取值,并使用ValueState.update(value: T)更新值。

  • ListState[T]包含T类型元素的列表。可以通过调用ListState.add(value: T) 或 ListState.addAll(values: java.util.List[T])将新元素追加到列表中。状态元素可以通过调用ListState.get()来访问,它返回所有状态元素上的一个Iterable[T]。不能从ListState中删除单个元素,但是可以通过调用ListState.update(values: java.util.List[T])更新列表。对该方法的调用将使用给定的值列表替换现有的值。

  • MapState[K, V]包含key和value的映射。state原语提供了常规Java Map的方法,比如get(key: K), put(key: K, value: V), contains(key: K), remove(key: K),以及遍历entries, keys, 和values的迭代器。

  • ReducingState[T]提供了与ListState[T]相同的方法(addAll()和update()方法除外),但是ReducingState.add(value: T)不向列表添加值,而是使用ReduceFunction立即聚合值。get()方法返回的迭代器,带有单个entry的Iterable,该entry是reduce后的值。

  • AggregatingState[I, O]的行为类似于ReducingState。但是,它使用更通用的AggregateFunction来聚合值。AggregatingState.get()计算最终结果,返回一个包含单个元素的Iterable。

所有的状态原语都可以通过调用State.clear()来清除。

示例7-1展示了如何将带有键值ValueState的FlatMapFunction应用于传感器测量流。如果传感器测量的温度自上次测量以来变化超过阈值,示例应用程序将发出报警事件。

val sensorData: DataStream[SensorReading] = ???
// partition and key the stream on the sensor ID
val keyedData: KeyedStream[SensorReading, String] = sensorData
.keyBy(_.id)
// apply a stateful FlatMapFunction on the keyed stream which
// compares the temperature readings and raises alerts
val alerts: DataStream[(String, Double, Double)] = keyedData.flatMap(new TemperatureAlertFunction(1.7))

具有keyed state的函数必须应用于KeyedStream。在应用该函数之前,需要通过调用输入流上的keyBy()方法来指定key。当调用具有key输入的函数处理方法时,Flink的runtime环境自动将该函数的所有keyed state对象放入key的上下文中。因此,一个函数只能访问它当前处理的记录的状态。

例7-2显示了带有key-value ValueState的FlatMapFunction的实现,该函数检查测量的温度变化是否大于配置的阈值。 

class TemperatureAlertFunction(val threshold: Double)
extends RichFlatMapFunction[SensorReading, (String, Double, Double)]
{
// the state handle object
private var lastTempState: ValueState[Double] = _
override def open(parameters: Configuration): Unit = {
// create state descriptor
val lastTempDescriptor =
new ValueStateDescriptor[Double]("lastTemp", classOf[Double])
// obtain the state handle
lastTempState = getRuntimeContext.getState[Double]
(lastTempDescriptor)
}


override def flatMap(
reading: SensorReading,
out: Collector[(String, Double, Double)]): Unit = {
// fetch the last temperature from state
val lastTemp = lastTempState.value()
// check if we need to emit an alert
val tempDiff = (reading.temperature - lastTemp).abs
if (tempDiff > threshold) {
// temperature changed by more than the threshold
out.collect((reading.id, reading.temperature, tempDiff))
}
// update lastTemp state
this.lastTempState.update(reading.temperature)
}
}

要创建一个状态对象,我们必须通过RuntimeContext向Flink的runtime注册一个StateDescriptor,它是由RichFunction提供的(有关RichFunction接口的讨论,请参阅“实现函数”)。StateDescriptor特定于状态原语,包括状态的名称和状态的数据类型。ReducingState和AggregatingState的描述符也需要ReduceFunction或AggregateFunction对象来聚合添加的值。状态名限定在算子的作用域内,因此一个函数可以通过注册多个状态描述符来拥有多个状态对象。由状态处理的数据类型被指定为类或类型信息对象(有关Flink的类型处理的讨论,请参阅“types”)。必须指定数据类型,因为Flink需要创建合适的序列化器。另外,还可以显式地指定类型序列化器来控制如何将状态写入状态后端、检查点和保存点。

通常,状态句柄对象是在RichFunction的open()方法中创建的。open()方法在调用任何处理方法(如flatMap函数中的flatMap())之前被调用。状态句柄对象(例7-2中的lastTempState)是函数类的常规成员变量。

状态句柄对象仅提供对状态的访问,该状态存储在状态后端维护中。句柄不包状态本身。

当一个函数注册了一个StateDescriptor时,Flink会检查状态后端是否有该函数的数据以及给定的名称和类型的状态。如果重新启动有状态函数以从故障中恢复,或者从保存点启动应用程序,可能会发生这种情况。在这两种情况下,Flink都将新注册的状态句柄对象链接到现有状态。如果状态后端不包含给定描述符的状态,则链接到句柄的状态初始化为空。

Scala DataStream API提供了一些语法快捷方式,可以使用单个ValueState定义map 和flatMap函数。示例7-3展示了如何使用快捷方式实现前面的示例。

val alerts: DataStream[(String, Double, Double)] = keyedData
.flatMapWithState[(String, Double, Double), Double] {
case (in: SensorReading, None) =>
// no previous temperature defined; just update the last
temperature
(List.empty, Some(in.temperature))
case (r: SensorReading, lastTemp: Some[Double]) =>
// compare temperature difference with threshold
val tempDiff = (r.temperature - lastTemp.get).abs
if (tempDiff > 1.7) {
// threshold exceeded; emit an alert and update the last
temperature
(List((r.id, r.temperature, tempDiff)), Some(r.temperature))
} else {
// threshold not exceeded; just update the last temperature
(List.empty, Some(r.temperature))
}
}

flatMapWithState()方法需要一个接受Tuple2的函数。tuple的第一个字段保存输入记录到flatMap,第二个字段保存已处理记录的key的检索状态的Option。如果状态尚未初始化,则不定义Option。该函数还返回一个Tuple2。第一个字段是flatMap结果的列表,第二个字段是状态的新值。

 

2.使用ListCheckpointed 接口实现算子List State

算子状态由每个算子的并行实例管理,在算子的同一并行任务中处理的所有时间都可以获取相同的状态。在“状态管理”中,我们讨论了Flink支持的三种算子状态:

  • list state

  • list union state

  • broadcast state

函数可以通过实现ListCheckpointed接口来处理算子的列表状态(list state)。ListCheckpointed接口不能处理状态句柄,比如在状态后端(state backend)注册的ValueState或ListState。相反,函数将算子状态作为常规成员变量实现,并通过ListCheckpointed接口的回调函数与状态后端(state backend)交互。该接口提供了两种方法:

// returns a snapshot the state of the function as a list
snapshotState(checkpointId: Long, timestamp: Long): java.util.List[T]
// restores the state of the function from the provided list
restoreState(java.util.List[T] state): Unit 

当Flink触发有状态函数的检查点时,将调用snapshotState()方法。该方法有两个参数,checkpointId和timestamp,前者是检查点唯一的、单调递增的标识符,后者是master初始化检查点的时间戳。该方法必须以可序列化的状态对象列表的形式返回算子状态。

restoreState()方法总是在需要初始化函数的状态时调用——在启动作业时(无论是否从保存点启动),或者在失败的情况下。使用状态对象列表调用该方法,并必须基于这些对象恢复算子的状态。

示例7-4展示了如何为一个函数实现ListCheckpointed接口,为该函数的每个并行实例计算每个分区超过阈值的温度测量值。

class HighTempCounter(val threshold: Double)
extends RichFlatMapFunction[SensorReading, (Int, Long)]
with ListCheckpointed[java.lang.Long] {
// index of the subtask
private lazy val subtaskIdx = getRuntimeContext
.getIndexOfThisSubtask
// local count variable
private var highTempCnt = 0L
override def flatMap(
in: SensorReading,
out: Collector[(Int, Long)]): Unit = {
if (in.temperature > threshold) {
// increment counter if threshold is exceeded
highTempCnt += 1
// emit update with subtask index and counter
out.collect((subtaskIdx, highTempCnt))
}
}
override def restoreState(
state: util.List[java.lang.Long]): Unit = {
highTempCnt = 0
// restore state by adding all longs of the list
for (cnt <- state.asScala) {
highTempCnt += cnt
}
}
override def snapshotState(
chkpntId: Long,
ts: Long): java.util.List[java.lang.Long] = {


// snapshot state as list with a single count
java.util.Collections.singletonList(highTempCnt)
}
}

上面示例中的函数为每个并行实例计算超过配置阈值的温度测量值。该函数使用算子状态,并为每个被检查点和使用ListCheckpointed接口的方法恢复的并行算子实例提供一个状态变量。注意,ListCheckpointed接口是在Java中实现的,并且需要java.util.List而不是Scala原生list。

查看这个示例,你可能想知道为什么算子state被处理为一个状态对象列表。正如在“Scaling Stateful Operators”中讨论的,列表结构支持使用算子状态来改变函数的并行性。为了增加或减少具有算子状态的函数的并行性,算子状态需能够被重新分布到更多或更少的任务实例中。这需要分离或合并状态对象。由于分离和合并状态的逻辑是为每个有状态函数定制的,因此不能为任意类型的状态自动执行此操作。

通过提供状态对象的列表,具有算子state的函数可以使用snapshotState()和restoreState()方法实现此逻辑。snapshotState()方法将算子状态拆分为多个部分,而restoreState()方法将算子状态组装为多个部分。当函数的状态被恢复时,状态的各个部分分布在函数的所有并行实例中,并传递给restoreState()方法。如果并行子任务比状态对象多,则一些子任务在没有状态的情况下启动,并使用空列表调用restoreState()方法。

再次查看示例7-4中的HighTempCounter函数,我们可以看到算子的每个并行实例都将其状态公开为带有单个entry的列表。如果增加这个算子的并行度,一些新的子任务将以空状态初始化,并从0开始计数。为了在HighTempCounter函数重新计算时获得更好的状态分配行为,我们可以实现snapshotState()方法,以便将其计数分割为多个部分计数,如示例7-5所示。

override def snapshotState(
   chkpntId: Long,
   ts: Long): java.util.List[java.lang.Long] = {
   // split count into ten partial counts
   val div = highTempCnt / 10
   val mod = (highTempCnt % 10).toInt
   // return count as ten parts
  (List.fill(mod)(new java.lang.Long(div + 1)) ++
   List.fill(10 - mod)(new java.lang.Long(div))).asJava
}

ListCheckpointed接口使用Java序列化对状态对象列表进行序列化和反序列化。如果你需要更新应用程序,这可能是个问题,因为Java序列化不允许迁移或配置自定义序列化程序,如果需要确保一个函数的算子状态支持应用程序更新,可以实现CheckpointedFunction接口,而不是ListCheckpointed接口。

使用连接广播状态

流应用程序中的一个常见需求是将相同的信息分发给一个函数的所有并行实例,并将其维护为可恢复状态。例如,规则流和应用规则的事件流。应用规则的函数接收两个输入流,事件流和规则流。它用算子状态存储规则,以便将它们应用于事件流的所有事件。由于函数的每个并行实例必须将所有规则保存在其算子状态中,因此需要广播规则流,以确保函数的每个实例都接收到所有规则。

在Flink中,这种状态称为广播状态。广播状态可以与常规的DataStream或KeyedStream相结合。示例7-6展示了如何实现一个温度警报应用程序,它具有可以通过广播流,动态配置阈值。

val sensorData: DataStream[SensorReading] = ???
val thresholds: DataStream[ThresholdUpdate] = ???
val keyedSensorData: KeyedStream[SensorReading, String] =sensorData.keyBy(_.id)
// the descriptor of the broadcast state
val broadcastStateDescriptor =
new MapStateDescriptor[String, Double]("thresholds", classOf[String], classOf[Double])
val broadcastThresholds: BroadcastStream[ThresholdUpdate] =
thresholds.broadcast(broadcastStateDescriptor)
// connect keyed sensor stream and broadcasted rules stream
val alerts: DataStream[(String, Double, Double)] =
keyedSensorData.connect(broadcastThresholds)
   .process(new UpdatableTemperatureAlertFunction())

一个带有广播状态的函数作用于两个流,分三步:

  1. 可以通过调用DataStream.broadcast()来创建BroadcastStream,并提供一个或多个MapStateDescriptor对象。每个描述符定义函数的单独广播状态,稍后将其应用于BroadcastStream。

  2. 将BroadcastStream连接到DataStream或KeyedStream。BroadcastStream必须放在connect()方法中作为参数。

  3. 对连接的流应用一个函数。根据流是否key类型流,可以应用KeyedBroadcastProcessFunction或BroadcastProcessFunction。

示例7-7展示了KeyedBroadcastProcessFunction的实现,该函数支持在runtime动态配置传感器阈值。

class UpdatableTemperatureAlertFunction() extends KeyedBroadcastProcessFunction
[String, SensorReading, ThresholdUpdate, (String, Double,Double)] {
// the descriptor of the broadcast state
private lazy val thresholdStateDescriptor =
new MapStateDescriptor[String, Double]("thresholds", classOf[String], classOf[Double])
// the keyed state handle
private var lastTempState: ValueState[Double] = _
override def open(parameters: Configuration): Unit = {
   // create keyed state descriptor
  val lastTempDescriptor = new ValueStateDescriptor[Double]("lastTemp", classOf[Double])
   // obtain the keyed state handle
   lastTempState = getRuntimeContext.getState[Double]
  (lastTempDescriptor)
}
override def processBroadcastElement(
   update: ThresholdUpdate,
   ctx: KeyedBroadcastProcessFunction
  [String, SensorReading, ThresholdUpdate, (String,Double, Double)]#Context,
   out: Collector[(String, Double, Double)]): Unit = {
   // get broadcasted state handle
   val thresholds =ctx.getBroadcastState(thresholdStateDescriptor)
   if (update.threshold != 0.0d) {
       // configure a new threshold for the sensor
       thresholds.put(update.id, update.threshold)
  } else {
       // remove threshold for the sensor
       thresholds.remove(update.id)
  }
}
override def processElement(
   reading: SensorReading,
   readOnlyCtx: KeyedBroadcastProcessFunction
      [String, SensorReading, ThresholdUpdate,(String, Double, Double)]#ReadOnlyContext,
   out: Collector[(String, Double, Double)]): Unit = {
   // get read-only broadcast state
   val thresholds =readOnlyCtx.getBroadcastState(thresholdStateDescriptor)
   // check if we have a threshold
   if (thresholds.contains(reading.id)) {
       // get threshold for sensor
       val sensorThreshold: Double = thresholds.get(reading.id)
       // fetch the last temperature from state
       val lastTemp = lastTempState.value()
       // check if we need to emit an alert
       val tempDiff = (reading.temperature - lastTemp).abs
       if (tempDiff > sensorThreshold) {
           // temperature increased by more than the threshold
           out.collect((reading.id, reading.temperature,tempDiff))
      }
  }
   // update lastTemp state
   this.lastTempState.update(reading.temperature)
}
}

BroadcastProcessFunction和KeyedBroadcastProcessFunction与常规的CoProcessFunction不同,因为元素处理方法是不对称的。使用不同的上下文对象调用方法processElement()和processBroadcastElement()。这两个上下文对象都提供了getBroadcastState(MapStateDescriptor)方法,该方法提供对广播状态句柄的访问。但是,processElement()方法中返回的广播状态句柄提供了对广播状态的只读访问。这是一种安全机制,用于确保广播状态在所有并行实例中保持相同的信息。此外,这两个上下文对象还提供对事件时间戳、当前水印、当前处理时间和侧输出的访问,类似于其他流程函数的上下文对象。

注意:broadcastProcessFunction和KeyedBroadcastProcessFunction也有所不同。BroadcastProcessFunction不公开定时器服务来注册定时器,因此不提onTimer()方法。注意,你不应该从KeyedBroadcastProcessFunction的processBroadcastElement()方法中访问key状态。由于广播输入没有指定key,状态后端无法访问键值并将抛出异常。相反,keyedBroadcastProcessFunction.processbroadcastelement()方法的上下文

提供了一个方法applyToKeyedState(StateDescriptor,KeyedStateFunction)来将

KeyedStateFunction应用于状态描述符引用的键态中的每个key的值。

广播事件可能无法以确定的顺序到达

如果发出广播消息的算子以大于1的并行度运行,广播事件到达广播状态算子的不同并行任务的顺序可能不同。因此,你应该确保广播状态的值不依赖于接受广播消息的顺序,或者确保广播算子的并行度设置为1。

使用CheckpointedFunction接口

CheckpointedFunction接口是指定有状态函数的最低级别接口。它提供了注册和维护keyed state和操作状态的挂钩,是唯一一个允许访问操作列表联合状态的接口——在恢复或保存点重新启动时完全复制的操作状态。

CheckpointedFunction接口定义了两个方法,initializeState()和snapshotState(),它们的工作方式类似于算子列表状态的listcheckpoint接口的方法。在创建CheckpointedFunction的并行实例时调用initializeState()方法。当应用程序启动或任务由于失败而重新启动时,就会发生这种情况。使用FunctionInitializationContext对象调用该方法,该对象提供对OperatorStateStore和KeyedStateStore对象的访问。状态存储负责向Flink的runtime注册函数状态并返回状态对象,如ValueState、ListState或BroadcastState。每个状态都用一个必须是唯一的函数名注册。当函数注册状态时,状态存储尝试通过检查状态后端是否保存在给定名称下注册的函数的状态来初始化状态。如果由于失败或从保存点重新启动任务,则将从保存的数据初始化状态。如果应用程序不是从检查点或保存点启动的,则状态最初将为空。

在采取检查点之前立即调用snapshotState()方法,并接收FunctionSnapshotContext对象作为参数。FunctionSnapshotContext允许访问检查点的唯一标识符和JobManager启动检查点时的时间戳。snapshotState()方法的目的是确保在完成检查点之前更新所有状态对象。此外,结合CheckpointListener接口,可以使用snapshotState()方法通过与Flink的检查点同步来一致地将数据写入外部数据存储。

例7-8显示了如何使用CheckpointedFunction接口来创建一个带有键控和算子状态的函数,该函数计算每个键和算子实例中有多少传感器读数超过了指定的阈值。

class HighTempCounter(val threshold: Double)
extends FlatMapFunction[SensorReading, (String, Long,Long)] with CheckpointedFunction {
// local variable for the operator high temperature cnt
var opHighTempCnt: Long = 0
var keyedCntState: ValueState[Long] = _
var opCntState: ListState[Long] = _
override def flatMap(
   v: SensorReading,
   out: Collector[(String, Long, Long)]): Unit = {
   // check if temperature is high
   if (v.temperature > threshold) {
       // update local operator high temp counter
       opHighTempCnt += 1
       // update keyed high temp counter
       val keyHighTempCnt = keyedCntState.value() + 1
       keyedCntState.update(keyHighTempCnt)
       // emit new counters
       out.collect((v.id, keyHighTempCnt, opHighTempCnt))
  }
}
override def initializeState(initContext:FunctionInitializationContext): Unit = {
   // initialize keyed state
   val keyCntDescriptor = new ValueStateDescriptor[Long]("keyedCnt", classOf[Long])
   keyedCntState =initContext.getKeyedStateStore.getState(keyCntDescriptor)
   // initialize operator state
   val opCntDescriptor = new ListStateDescriptor[Long]("opCnt", classOf[Long])
   opCntState =initContext.getOperatorStateStore.getListState(opCntDescriptor)
   // initialize local variable with state
   opHighTempCnt = opCntState.get().asScala.sum
}
override def snapshotState(snapshotContext: FunctionSnapshotContext): Unit = {
   // update operator state with local state
   opCntState.clear()
   opCntState.add(opHighTempCnt)
}
}

3.接收检查点完成通知

频繁的同步是分布式系统性能受限的主要原因。Flink的设计旨在减少同步点。检查点是基于与数据一起流动的barriers实现的,因此避免了应用程序中所有算子之间的全局同步。

基于检查点机制,Flink可以实现非常好的性能。然而,另一个含义是,应用程序的状态永远不会处于一致的状态,除了在采取检查点时的逻辑时间点。对于一些算子来说,知道检查点是否完成是很重要的。例如,目标是精确地将数据写入外部系统的接收器函数(有且只有一次的保证)必须只输出在成功的检查点之前接收到的记录,以确保在发生故障时不会重新计算接收到的数据。

正如在“检查点、保存点和状态恢复”中讨论的,只有当所有算子任务都成功地将其状态检查点存储时,检查点才会成功。因此,只有JobManager才能确定检查点是否成功。需要通知完成检查点的算子可以实现CheckpointListener接口。这个接口提供了notifyCheckpointComplete(long chkpntId)方法,当JobManager注册一个已完成的检查点时(当所有算子成功地将它们的状态复制到远程存储时)可以调用该方法。

注意:Flink不保证为每个完成的检查点调用notifyCheckpointComplete()
方法。任务可能会错过通知。在实现接口时需要考虑这一点。


 

二、为有状态的应用开启故障恢复

流式应用程序应该持续运行,并且必须从故障(如故障机器或进程)中恢复。大多数流应用程序要求故障不影响计算结果的正确性。

在“检查点、保存点和状态恢复”章节中,我们解释了Flink创建有状态应用程序的一致检查点的机制,在某时间点,算子处理完应用程序输入流的一个特定的位置所有事件,所有内置状态和用户定义的状态函数的快照形成。为了为应用程序提供容错,JobManager定期启动检查点。

应用程序需要通过StreamExecutionEnvironment显式地启用定期检查点机制,如示例7-9所示。

val env = StreamExecutionEnvironment.getExecutionEnvironment
// set checkpointing interval to 10 seconds (10000 milliseconds)
env.enableCheckpointing(10000L)

检查点间隔是一个重要的参数,它影响常规处理期间检查点机制的开销和从故障恢复所需的时间。更短的检查点间隔在常规处理期间会导致更高的开销,但可以实现更快的恢复,因为需要重新处理的数据更少。Flink提供了更多的调优方式来配置检查点行为,比如一致性保证(有且一次或至少一次)的选择、并发检查点的数量、取消长时间运行的检查点的超时以及几个特定于状态后端的选项。我们将在“检查点调优和恢复”中更详细地讨论这些选项。

1.确保有状态应用的可维护性

运行了几周的应用程序的状态可能很庞大,甚至无法重新计算。同时,需要维护长时间运行的应用程序。bug需要修复,功能需要调整、增加或删除,或者算子的并行性需要调整以适应更高或更低的数据速率。因此,重要的是可以将应用程序状态迁移到应用程序的新版本,或者将其重新分发到更多或更少的算子任务。

Flink提供保存点来维护应用程序及其状态。但是,它要求应用程序初始版本的所有有状态算子指定两个参数,以确保将来可以适当地维护应用程序。这些参数是唯一的算子标识符和最大的并行度(对于具有key状态的算子)。下面我们将描述如何设置这些参数。

算子唯一标识符和最大并行度被写入保存点
算子的唯一标识符和最大并行度被放入下一个保存点,并且不能更改。一旦更改算子
标识符或最大并行度,就不能从保存点启动应用程序,而必须在没有仍和状态初始化的情况下从头开始。
 

2.指定算子唯一标识

应该为应用程序的每个算子指定唯一标识符。标识符被写入保存点,作为具有算子的实际状态数据的元数据。当从保存点启动应用程序时,将使用标识符将保存点中的状态映射到启动应用程序的相应算子。只有当启动的应用程序的算子的标识符相同时,才能将保存点状态恢复到该算子。

如果不显式地为有状态应用程序的算子设置唯一标识符,则在更新应用程序时将面临很多限制。我们将在“保存点”中更详细地讨论唯一算子标识符的重要性和保存点状态的映射。

我们强烈建议为应用程序的每个算子分配唯一标识符。可以使用uid()方法设置标识符,如示例7-10所示。

val alerts: DataStream[(String, Double, Double)] =
keyedSensorData.flatMap(new TemperatureAlertFunction(1.1)).uid("TempAlert")

3.定义keyed state算子的最大并行度

算子的最大并行度参数定义了算子的key被分割成的key组的数量。key组的数量限制了可缩放keyed state的并行任务的最大数量。“有状态算子的缩放”讨论了key组以及如何缩放key状态。可以通过StreamExecutionEnvironment为应用程序的所有算子设置最大并行度,也可以使用setMaxParallelism()方法为每个算子设置最大并行度,如示例7-11所示。

val env = StreamExecutionEnvironment.getExecutionEnvironment
// set the maximum parallelism for this application
env.setMaxParallelism(512)
val alerts: DataStream[(String, Double, Double)] =
keyedSensorData.flatMap(new TemperatureAlertFunction(1.1))
// set the maximum parallelism for this operator and
// override the application-wide value
.setMaxParallelism(1024)

算子的默认最大并行度取决于应用程序第一个版本中算子的并行度:

  • 如果并行度小于等于128,那么最大并行度就是128。

  • 如果算子的并行度大于128,则计算最大并行度为nextPowerOfTwo(parallelism + (parallelism / 2))和2^15的最小值。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

数据与智能

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值