您的位置:首页 > 编程语言

Flink流计算编程--状态与检查点

2016-07-21 18:21 801 查看

1、Exactly_once简介

Exactly_once语义是Flink的特性之一,那么Flink到底提供了什么层次的Excactly_once?有人说是是每个算子保证只处理一次,有人说是每条数据保证只处理一次。其实理解这个语义并不难,直接在官方文档中就可以看出:



从图中可以看出:Exactly_once是为有状态的计算准备的!

换句话说,没有状态的算子操作(operator),Flink无法也无需保证其只被处理Exactly_once!为什么无需呢?因为即使失败的情况下,无状态的operator(map、filter等)只需要数据重新计算一遍即可。例如:

dataStream.filter(_.isInNYC)


当机器、节点等失败时,只需从最近的一份快照开始,利用可重发的数据源重发一次数据即可,当数据经过filter算子时,全部重新算一次即可,根本不需要区分哪个数据被计算过,哪个数据没有被计算过,因为没有状态的算子只有输入和输出,没有状态可以保存。

2、Flink的恢复机制

Flink的失败恢复依赖于“检查点机制+可部分重发的数据源”。

2.1、检查点机制:检查点定期触发,产生快照,快照中记录了(1)当前检查点开始时数据源(例如Kafka)中消息的offset,(2)记录了所有有状态的operator当前的状态信息(例如sum中的数值)。

2.2、可部分重发的数据源:Flink选择最近完成的检查点K。然后系统重放整个分布式的数据流,然后给予每个operator他们在检查点k快照中的状态。数据源被设置为从位置Sk开始重新读取流。例如在Apache Kafka中,那意味着告诉消费者从偏移量Sk开始重新消费。

容错特性:



3、检查点与保存点

3.1、检查点

Flink的检查点机制实现了标准的Chandy-Lamport算法,并用来实现分布式快照。在分布式快照当中,有一个核心的元素:Barrier。

屏障作为数据流的一部分随着记录被注入到数据流中。屏障永远不会赶超通常的流记录,它会严格遵循顺序。屏障将数据流中的记录隔离成一系列的记录集合,并将一些集合中的数据加入到当前的快照中,而另一些数据加入到下一个快照中。每一个屏障携带着快照的ID,快照记录着ID并且将其放在快照数据的前面。屏障不会中断流处理,因此非常轻量级。来自不同快照的多个屏障可能同时出现在流中,这意味着多个快照可能并发地发生。

单流的barrier:



多流的barrier:



不止一个输入流的的operator需要在快照屏障上对齐(align)输入流。

在stream source中,流屏障被注入到并发数据流中。快照n被注入屏障的点(简称为Sn),是在source stream中的数据已被纳入该快照后的位置。例如,在Apache Kafka中,该位置将会是partition中最后一条记录的offset。这个Sn的位置将被报告给检查点协调器(Flink JobManager)。

屏障接下来会流向下游。当一个中间的operator从所有它的输入流中接收到一个来自快照n的屏障,它自身发射一个针对快照n的屏障到所有它的输出流。一旦一个sink operator(流DAG的终点)从它所有的输入流中接收到屏障n,它将会像检查点协调器应答快照n。在所有的sink应答该快照后,它才被认为是完成了。

程序中如何设置检查点?

val 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)

// checkpoints have to complete within one minute, or are discarded
env.getCheckpointConfig.setCheckpointTimeout(60000)

// allow only one checkpoint to be in progress at the same time
env.getCheckpointConfig.setMaxConcurrentCheckpoints(1)


3.2、保存点

保存点本质上就是一次检查点,但它与检查点的不同在于:

(1)手动触发

(2)不会过期,除非用户明确的处理

先来看一张图:



保存点仅仅是一个指向检查点的指针;同时,其默认保存在JobManager的memory中,但为了高可用,建议保存到hdfs上。通入如下参数调整:

savepoints.state.backend: filesystem
savepoints.state.backend.fs.dir: hdfs:///flink/savepoints


保存点在什么时候使用?

(1)应用程序升级

(2)Flink版本升级

(3)系统升级或系统迁移

(4)程序的模拟仿真情况

(5)A/B测试

如何手动触发及恢复保存点?

CLI方式:

触发:

flink savepoint <JobID>


恢复:

flink run -s <pathToSavepoint> <jobJar> ...


4、状态简介

Flink流处理中的算子操作,是可以有状态的,这也是区别于其他流计算引擎的显著标志之一。

Flink提供了Exactly_once特性,是依赖于带有barrier的分布式快照+可部分重发的数据源功能实现的。而分布式快照中,就保存了operator的状态信息。

4.1、如何定义快照?

(1)使用window操作,基于EventTime、ProcessingTime、基于Count的窗口以及自定义的窗口。

(2)使用检查点接口,可以注册任何类型的java/scala对象。

(3)使用key/value状态接口,通过key来分区使用state。

4.2、重点说说如何使用基于key/value状态接口来定义state

既然是基于key/value的状态接口,那么这些状态只能用于keyedStream之上。keyedStream上的operator操作可以包含window或者map等算子操作。

key/value下可用的状态接口:

(1)ValueState : 状态保存的是一个值,可以通过update(T)来更新,T.value()获取。

(2)ListState : 状态保存的是一个列表,通过add(T)添加数据,Iterable.get获取。

(3)ReducingState : 状态保存的是一个经过聚合之后的值的列表,通过add(T)添加数据,通过指定的聚合方法获取。

通过创建一个StateDescriptor,可以得到一个包含特定名称的状态句柄,可以分别创建ValueStateDescriptor、 ListStateDescriptor或ReducingStateDescriptor状态句柄。

注意:状态是通过RuntimeContext来访问的,因此只能在RichFunction中访问状态。这就要求UDF时要继承Rich函数,例如RichMapFunction、RichFlatMapFunction等。

无状态的流与有状态的流的对比:



4.3、状态保存在哪里

状态终端用来对状态进行持久化存储,Flink支持多个状态终端:

(1)MemoryStateBackend

(2)FsStateBackend

(3)RocksDBStateBackend(第三方开发者实现)

五、带状态的operator例子

这里以flink-training上的例子作为样例:

keyBy之后是一个keyedStream,然后进行flatMap操作,转换为dataStream。定义状态就是在flatMap中实现。

.keyBy("rideId")
// compute the average speed of a ride
.flatMap(new SpeedComputer)


继承RichFlatMapFunction而非FlatMapFunction,此例中state是一个基于key/value接口的ValueState方法。而RichFlatMapFunction又继承了AbstractRichFunction,其中要覆写open方法;同时覆写RichFlatMapFunction中的flatMap方法。

class SpeedComputer extends RichFlatMapFunction[TaxiRide, (Long, Float)] {

var state: ValueState[TaxiRide] = null

override def open(config: Configuration): Unit = {
state = getRuntimeContext.getState(new ValueStateDescriptor("ride", classOf[TaxiRide], null))
}

override def flatMap(ride: TaxiRide, out: Collector[(Long, Float)]): Unit = {

if(state.value() == null) {
// first ride
state.update(ride)
}
else {
// second ride
val startEvent = if (ride.isStart) ride else state.value()
val endEvent = if (ride.isStart) state.value() else ride

val timeDiff = endEvent.time.getMillis - startEvent.time.getMillis
val speed = if (timeDiff != 0) {
(endEvent.travelDistance / timeDiff) * 60 * 60 * 1000
} else {
-1
}
// emit average speed
out.collect( (startEvent.rideId, speed) )

// clear state to free memory
state.update(null)
}
}
}


通过这个例子,可以知道如何在operator中实现state。

六、总结

最后说一下我对Flink中有状态的算子在恢复时是如何进行的:

假设场景Job:1个Source(Kafka)+1个不带state的operator+1个带state的operator+1个sink。

如果失败,则Flink选择最近的一份检查点开始恢复,检查点中记录了这次检查点开始时数据源(kafka)中对应的topic的offset,从offset开始重新发送数据,当数据流到1个不带operator的算子时,数据全部应用在这个算子上;接着数据流向1个带有operator的算子,由于快照中记录着这个operator的状态的值,因此,数据重新计算时只从记录着状态的值的地方开始计算,而不会从头开始计算,例如key0=2,那么只从key0=2开始计算。随后进行sink。由于失败时可能有些数据已经sink了,那么根据幂等性原则,即使中间输出的结果存在异常,但是重发之后再次sink是正确的,最终的结果还是正确的。

由于sink一般都是外围系统,因此sink的设计一般都没有状态,但是如果保证幂等性,最终的结果也没问题。



对Flink的快照以及状态的理解也许有不准确的地方,欢迎大家指出!

参考

https://ci.apache.org/projects/flink/flink-docs-master/apis/streaming/state.html

https://ci.apache.org/projects/flink/flink-docs-master/apis/streaming/state_backends.html

https://ci.apache.org/projects/flink/flink-docs-master/apis/streaming/savepoints.html

http://flink.apache.org/features.html

http://blog.csdn.net/yanghua_kobe/article/details/51714388

http://data-artisans.com/how-apache-flink-enables-new-streaming-applications/#more-608

https://yq.aliyun.com/articles/57828?spm=5176.100239.blogcont57826.5.noEJP1

http://mp.weixin.qq.com/s?__biz=MzI2MjE0MDUzNg==&mid=2652914398&idx=1&sn=9a72035a1ea208b096299684fe637dda&scene=1&srcid=0711fENhBenSH5wu7aOyWHPL&from=groupmessage&isappinstalled=0#wechat_redirect%20%E6%9D%8E%E5%91%88%E7%A5%A5
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  Flink 流计算 状态