对于持续生成新数据的场景,采用流计算显然是有利的。数据源源不断的产生,流计算系统理论上就要不间断的提供数据计算(可以停机维护的场景不在本文的讨论范围)。那么假如遇到下面的几种情况,流计算是如何保证数据的一致性的呢?
1、应用程序bug修复,即功能的修改
2、应用程序增加、删除新的功能
3、流计算框架版本的升级
4、突发的大量数据的到来
以上列出的几种情况,我相信在大多数的流数据场景下,都可能遇到。对于以上的这些情况,我们在流计算系统中,分别会产生什么问题呢?
1、bug修复--》停止job,重新发布新的jar包后,之前每个operator的状态数据,在重启job时还能继续用么?
2、功能修复--》停止job,重新发布新的jar包后,新的或已删除的operator的状态,怎么办呢?
3、框架版本升级--》相同的jar包,在升级前后,状态能兼容么?
4、突发大量数据--》势必会导致严重的背压(backpressure),临时增加集群规模(扩容),状态还能正确恢复么?
上述这几种情况所面临的问题,相信大多数的流计算程序员或者架构人员都要考虑。幸好,Apache Flink的savepoint机制,让这一切变得简单而高效!
1、Flink中的savepoint(保存点)
savepoint是做什么的,有什么作用?简单而言,它是检查点的一个指针,提供了让“时间倒流”的功能,可以让Flink流计算程序重新处理过去的数据。
这种能力需要设置几个条件:
1、激活检查点
2、使用可重发的数据源
3、状态要被Flink管理
4、合适的state backend
具体的内容,可以参考blogSavepoints: Turning Back Time以及视频Savepoints in Apache Flink Stream Processing
2、reprocessing的思考
Flink通过savepoint机制,可以让流处理程序比较优雅的处理bug修复,程序升级、集群扩容等需求。实际上,我们只需要获取以下3种数据即可:
1、应用程序的jar包
2、保存点对应的快照(实际是检查点产生的)
3、可访问的保存点和检查点路径
其余的组件,都可以认为是临时性的。
3、Flink管理的状态
有人问过我流计算中,状态是指什么?
这个问题很好回答,我举个例子:
假如输入数据为 e = {event_id:int, event_value:int}。
如果输出仅仅是event_value,那就用个map即可,这就是无状态的流计算。
如果输出是最大的event_value,那就需要在map函数中,记住之前最大的event_value,然后再与当前数据的event_value比较,去输出最终最大的event_value。
然后有人就问,这个用一个HashMap不也行么?或者把更复杂的临时数据,存到redis等也可以达到相同的目的啊。
考虑一下,你如果存到HashMap中,程序一旦失败,自动恢复后,此时HashMap中的数据还有么?显然是无法拿到的。
Flink本身就是一个有状态的分布式流计算系统。在提交job时,Flink逻辑上将所有的operator分解成job graph,物理上分解为并行的execution graph。每个并行的slot都是一个独立的task或subtask,同一个operator的不同的并行slot之间,并不共享数据。整个的DAG图中,数据只是从上游的operator流向下游的operator。
就数据本地性而言,Flink中的状态数据总是绑定到特定的task上。基于这种设计,一个task的状态总是本地的,在tasks之间没有通信。
Flink中有两种类型的状态:
1、operator state
2、keyed state
每个operator state一定会绑定到一个特定的operator,其是属于一个operator的,例如实现了ListCheckpointed接口的operator。比如kafka就是一个很好的operator的例子。
keyed state就相当于分组后的operator state。其通常要基于keyedStream。
参见官方文档
4、扩容时的思考
考虑下假如有非常大的数据到来,我们想要扩容来应对这些增加的数据,例如下图:
当map的并行读由2变为3后,我们可以将之前的map_1和map_2的状态对应到新的map_1和map_2,留给map_3一个空的状态。
依赖于状态的类型以及具体的操作,这种方法很低