Flink学习入门(5)——状态管理与容错

参考:
Flink v1.8-State & Fault Tolerance
flink-china/flink-training-course
本参考来源暂未申请授权,如原作者看见有需要可联系(学生一枚,只为学习记录,望谅解)

0. 前言

虽然学了flink有一段时间了,关于time、window等基础概念比较好理解,就不做过多的记录和总结了,但这个state这一块儿还是可以记录一下的。

1. 什么是状态?

先说一句,状态其实就是中间计算结果。

1.1 没有状态的场景

比如在消息队列中有下面这样一个场景
在这里插入图片描述
现在要实时计算每个消费者consumer的消息延迟情况,在上图中:

1. 流输入记录
{	timestamp:1367347324
	producer:15
	consumer:{
				consumer1:7,
				consumer2:10,
				consumer3:12
			}
}

2. 延迟计算结果
consumer1:8,
consumer2:5,
consumer3:3

这个很简单,那么在下一个时间戳,再来一条流记录,如果其值是一样的,那么得到的结果也是一样的,这个没问题的对吧,因为就是时间差相减就行了啊。

这样的过程有两个特点:

  • 单条输入包含所有需要的信息,即不念过往、不惧将来(=。=)
  • 相同的输入,得到相同的输出。

那么问题来了,假如我要实时统计一个网站点击量呢?按照当前的计算方法,那岂不是永远都只有最新的一条点击记录了?这时候就要统计一部分输入了对吧,flink只是流计算框架,又没有存储部分,外接数据库进行存储?

在以前,我们用spark进行微批处理的时候,就是这样的,将当前结果存在mysql中,等过一段时间(例如5s),就再拿5s内的数据进行sum一下再放回去,所以我把这个叫【微批】,而不是【实时】,可以看出来,这其实是pull类型的,是spark主动拉取数据。

很自然的一个想法就是,我能不能把这个临时计算结果记下来,来一条数据,就计算一次,这就是状态!要注意区分的一点是,这是push类型的,并不是说flink拉取一条记录然后计算一下,是数据驱动。状态一直存在那,来数据了,就计算一下。

1.2 有状态计算

还是刚才计算点击量的场景。

1. 流输入记录
"timestamp": "18/Apr/2019:00:00:00",
"url": "/api/a"
}, {
"@timestamp": "18/Apr/2019:00:00:01",
"url": "/api/b"
}, {
"@timestamp": "18/Apr/2019:00:00:02",
"url": "/api/a"

2. 结果
"url": "/api/a"."count": 1
}, {
"url": "/api/b","count": 1
}, {
"url": "/api/a","count": 2

与上面相比,可以看出两个不同的特点:

  • 单条记录仅包含部分信息,也就是当前信息,不能代表最终结果
  • 相同的输入,得到的输出不同
1.3 有状态计算场景

其实按照上面来看,可以想象,大部分聚合函数,都是要有状态计算的,但是可不仅仅是SQL的聚合还有这些:

  • 去重,要知道数据有没有重复,那就要知道以前有没有过这个数据;
  • 窗口计算,和聚合计算一样的道理;
  • 数据对比,同比/环比,这些都是要和历史数据进行对比的;
  • 机器学习/深度学习,包括迭代计算中,每次运行模型的参数,这都是一个状态

简单来说,就是无法光靠自身一条记录,要依赖历史数据的计算,都可以是有状态计算。

2. 两种状态类型(Operator/keyed)

分为两类:Keyed State 和 Operator State。

我理解其实这就和在第一节基础学习说的 Keyed Stream 和 Nonkeyed Stream对应的,想一想第一节中的keyed图,如果不KeyBy的话,整个流就是一个大粗水管,在这上面做窗口计算,就是对所有数据进行计算,但是KeyBy以后,就像分成了很多并行的细水管了,就可以对单个key的流做窗口计算了。

2.1 Keyed State
  1. 只能用于keyed stream的算子上
  2. 每个Key对应一个State
    在一个Operator实例上可能同时运行多个Key,所以可能访问相应的多个State
  3. (Operator)并行度改变时,Key会被重新分布,此时State跟随Key在实例间迁移
    比如开始只有一个实例,两个key及其state都在这个实例上面,后面我要扩容,成了两个实例,那么就将两个key划分到不同的实例上,这时,相应的state就会跟着key一起迁移。
  4. 通过RuntimeContext访问,其RichFunction我也还没很清楚
  5. 支持丰富的数据结果
    ValueState、ListState、AggregatingState、MapState、ReducingState,其义如其名,
2.2 Operator State
  1. 可用于所有Operator,主要用于Source,例如FlinkKafkaConsumer
    这个结合实际应用场景和上面key state来不难理解,原始数据肯定是各个key的流都混在一起进来的,个人理解这个operator在source阶段,就有点像在Data Source阶段,如果要对数据流进行最简单的预处理和记录,就可以用这个了。
    咋说呢,比如我在食堂吃饭,对于食堂阿姨来说,窗口是一直不断来人的,而且每个人要的菜是不一样的,她只能根据同学们点的菜来打,这是根据不同的key来进行不同的操作,但是有一点,无论窗口下一个来的是谁,她都要先做的一件事是先盛饭,盛多少饭这个步骤我就可以看作在source阶段的state。后面再keyby后,进行不同的state管理,也不知道这个理解对不对,还请指教呀。
  2. 一个operator对应一个state,和并行的算子实例绑定,和数元素中的key无关,每个算子实例中持有所有数据元素中的一部分状态数据。
  3. 并行度改变时,有多种重新分配方式
    operator没有key,所以在并发度改变的时候,需要自己来选择分配方式。
    均匀分配:比如本来有3个实例,消费6个partition,这就是每个实例消费2个partition,扩容到6个实例后,均匀分配,就是每个实例分一个partition;
    合并后全量分配:有时候,你并不知道新加进来的实例会处理哪一个state,这时候,就把所有的state合并成全量分发给每一个实例,让它们自己选择state
  4. 实现CheckpointedFunction或ListCheckpointed接口
  5. 支持的数据结构:ListState

3. KeyState

3.1几种keyed state的关系

在这里插入图片描述

这里再放一下几种keyed state之间的差异

在这里插入图片描述

  • ValueState里面的单个值不代表只能是数值,也可以是字符串、list、对象等,就是单个的存在,它的访问接口类似于get和set,update操作就设置该值,value操作就得到该值;
  • MapState/ListState操作就类似于java中的map/array数据结构及其操作,与上面value不同的是,如果value里面存了一个list,这时候你要修改其中的一个值(list[0]),在valuestate中就要更新整个list,而在liststate中就可以对这个值单独操作;
  • ReducingState和listState的访问接口看起来很像,但是不同的是,比如你要进行聚合计算,累加2分钟内的值,listState中的add就会把这些值都存着,然后一起加,但是reducingState中的add就直接加上去,这样减少内存;
  • AggragtingState与上面的reduce区别在哪呢,上面说的都是累加操作,但是如果求平均值呢?在reduce中就只能保留一个数值,也就是保留了总和,却没法保留个数,最后的结果肯定不对。

煮个栗子,进来的数值为[10,20,30],在reduce中的操作就是:

  • (10+20)/2 = 15
  • (15+30)/2 = 22.5
    看到了吧,这样就不对了,在aggregating中的操作就可以保留两个参数,这就是in:
  • m = (10+20) = 30, n = 2
  • m = (30+30) = 60, n = 3
  • out = m/n = 60/3 = 20
3.2 keyedstate使用实例

煮一个简单状态机的栗子

啥叫状态机(State Machine):就是状态及其转换条件的组合
比如淘宝买东西,你下单以后,处于【待付款】状态,等你付款以后,就变成了待发货状态,这就是一个状态及其转换条件的组合——状态机

static class StateMachineMapper extends RichFlatMapFunction<Event, Alert> {
	private ValueState<State> currentState;
	public void open(Configuration conf) {
    // 4. 得到当前状态
		currentState = getRuntimeContext().getState(new ValueStateDescriptor<>("state", State.class));
	}
	public void flatMap(Event evt, Collector<Alert> out) throws Exception {
		State state = currentState.value();
        // 5. 如果原先无这个状态,则先进行初始化
		if (state == null) {
			state = State.Initial;
		}
        // 6. 根据事件类型,转换,得到下一个状态
		State nextState = state.transition(evt.type());
        // 7. 判断下一个状态是否有效
		if (nextState == State.InvalidTransition) {
			out.collect(new Alert(evt.sourceAddress(), state, evt.type()));
		} 
        // 8. 判断其是否终止
        else if (nextState.isTerminal()) {
			currentState.clear();
		} 
        // 9. 正常,则将当前状态更新为下一个状态
        else {
			currentState.update(nextState);
		}
	}
}

// 1. 事件源
DataStream<Event> events = env.addSource(source);
DataStream<Alert> alerts = events
// 2. 根据sourceAddress划分key
.keyBy(Event::sourceAddress)
// 3. 对每个key执行flatmap操作,也就是上面的操作
.flatMap(new StateMachineMapper());

还是按照淘宝买东西的流程,来讲(复述)一下上面的代码逻辑

  1. 你第一次点购买,产生了一个下单事件,此时的sourceAddress是空的,所以在第5步,会进行初始化,状态置为【待付款】;
  2. 第6步,根据你产生的事件类型,得到下一个状态,如果此时来了一个订单取消事件,但是你的订单是已经签收完成了,也就是说该事件和状态非法,7会处理该情况;
  3. 在【待付款】状态后,你执行了订单取消事件,后续的事件操作也就没有了,此时就是第8步,下一步终止了,就会清除状态;
  4. 都无异常操作的话,就在第9步,按照你的操作,正常更新订单状态了。

4. 如何管理状态?

上面说了,状态是个好东西,但是要管理,因为计算是在内存中进行的,特别是在分布式环境中,硬件故障是常态,万一宕机了,这个状态不就没了,基于该状态的计算也就错了。

按照flink流计算的场景要求:

  • 7*24工作,要保证高可靠性
  • 数据不重、不丢,保证数据准确性
  • 实时产出,不延迟

既然是在内存中计算,那直接丢在内存中管理呢?是有几个问题的:

  • 内存容量有限
  • 备份和恢复困难
  • 无法横向扩展(内存咋分布式)

按照参考学习资料的说法,理想的状态管理应该有以下几个方面:

  • 易用。总不能为了管理个状态,花的时间和精力比计算本身还费劲;
  • 高效。这个就是和场景要求对应的了,读写快,恢复快,备份和恢复不影响作业运行,还要方便的横向扩展;
  • 可靠。就是保证数据准确性,还要有容错性;

上面说了两种state类型,是按照是否划分为某个key来分类的,按照其state管理方式还可以分为两种:

在这里插入图片描述

可以看出来,大部分场景,主要用的就是managed state。

5. 容错机制与故障恢复

5.1 状态如何保存及恢复
  • Checkpoint机制
  • 定时制作分布式快照,对程序中的状态进行备份
  • 发生故障时,将整个作业的checkpoint回滚到最后一次成功checkpoint的状态,然后从那个点开始继续处理
    • 是作业粒度,而不是整个集群粒度;
    • 最后一次成功checkpoint的状态,失败的不行;
  • 必要条件:数据源支持重发
    • 不然前面发的消息被作业遗弃了重新开始,数据源却不重发该消息,那不就错了;
  • 一致性语义
    • 根据barrier是否对齐来判断是哪一种:
    • 恰好一次(exactly once)
    • 至少一次(at-least once)

关于一致性语义,要前面checkpoint的知识来理解,barrier是来隔开各个checkpoint的,打个比方,如果并发度只有1,那么只要碰到一个barrier,就知道该checkpoint已经完整了,这时肯定是恰好一次。但多并发的情况下,各个snapshot不一定对齐的啊,就根据barrier来对齐。这样才能保证数据不丢,也不重复。

看一段示例代码:


StreamExecutionEnvironment env = 
// 获取环境
StreamExecutionEnvironment.getExecutionEnvironment();
// 1000ms制作一个checkpoint
env.enableCheckpointing(1000);
// 恰好一次的模式,也就是要数据对齐
env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);
// 每次checkpoint间隔至少500ms
env.getCheckpointConfig().setMinPauseBetweenCheckpoints(500);
// 超时时间60s
env.getCheckpointConfig().setCheckpointTimeout(60000);
// 同时有多少个checkpoint在做
env.getCheckpointConfig().setMaxConcurrentCheckpoints(1);
// 在作业cancel的时候,是否在外部介质保留checkpoint
env.getCheckpointConfig().enableExternalizedCheckpoints(ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION);

因为checkpoint是作业级别的保存点,默认在作业取消的时候会清除。会存在的问题就是,有时候会手动调整并发度,一经调整,就会重启作业,此时如果没有了checkpoint,就无法恢复数据,当然,这种情况还有一个方法——savepoint

在这里插入图片描述

5.2 状态存储方式
  1. MemoryStateBackend
  • 构造方法:MemoryStateBackend(int maxStateSize, boolean Asy…)
  • 存储方式:
    • state:task manage内存
    • checkpoint:jobManage内存
  • 容量限制(虽然参数可调):
    • 单个state的容量:5M,总state < akka.framesize
    • checkpoint < job manage 的内存容量
  • 使用场景:
    • 本地测试:几乎不需要状态的场景,例如etl,或者job manage不容易/无所谓挂的情况,因为checkpoint是存在job manage的内存,一旦挂了就没了
  1. FsStateBackend
  • 构造方法:FsStateBackend(URI XXX, boolean Asy…)
  • 存储方式:
    • state:task manage内存
    • checkpoint:外部文件系统(本地或者HDFS)
  • 容量限制(虽然参数可调):
    • state:不超过task managed的内存总容量
    • checkpoint:不超过文件系统大小
  • 使用场景:
    • 常规用法,分钟级窗口聚合;HA
  1. RocksDBStateBackend
  • 构造方法:RocksDBStateBackend(URI XXX, boolean enableIncremental…)

  • 存储方式:

    • state:task manage上的KV数据库(内存+磁盘,内存溢写到磁盘)
    • checkpoint:外部文件系统(本地或者HDFS)
  • 容量限制(虽然参数可调):

    • 单task manage上的state总量不超过其内存+磁盘(也就是单个节点的所有存储量不能撑爆就行)
    • 单个key的state大小不超过2G
    • 总大小不超过文件系统大小
  • 使用场景:

    • 超大状态的作业,天级窗口聚合;HA;对状态读写性能不高的场合(毕竟可能经过磁盘)
  • 有一点是,前面两个的2参数都是是否异步,RocksDBStateBackend不支持该选项,但是是目前唯一支持增量保存state的方式!

x. Q&A

  1. 实时展示最近7日的均值
    实时展示的黑科技:queriable state,可以在流计算之外,查看流数据的状态。目前在beta版

  2. 如果使用了savepoint重启了,checkpoint是否还会重启?
    这可以理解为两个不同的应用场景,一般savepoint是人工操作,重启整个环境之类的,需要人为选择从哪一个savepoint启动;
    而checkpoint更倾向于job环境还在,其中某个组件出故障了,类似于常态的硬件故障,这时自动恢复,不需要人为参与的。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值