状态管理
状态后端
1.状态的存储
状态存在哪?存在状态后端。Flink提供了3种不同的StateBackend
1.MemoryStateBackend
- MemoryStateBackend 内部将状态作为对象保存在taskManager进程的堆内存,通过checkpoint机制,MemoryStateBackend将状态(state)进行快照并保存Jobmanager(master)的堆内存中。
- MemoryStateBackend 可以通过配置来使用异步快照(asynchronous snapshots)。通过异步快照可以避免阻塞管道(blocking pipelines),目前是默认开启,当然也可以通过MemoryStateBackend的构造函数配置进行关闭:
//new MemoryStateBackend(MAX_MEM_STATE_SIZE, false);
env.setStateBackend(new MemoryStateBackend(5242880,false));
2.FsStateBackend
写到TaskManager的磁盘文件中;也可以是hdfs;文件路径由master保管
//参数是checkpoint路径
env.setStateBackend(new FsStateBackend("hdfs:hadoop102:8020/flink/ck"));
3.RocksDBBackend
- RocksDB是一种集成在flink内部的key-value型数据库;
- RocksDBStateBackend将工作状态保存在RocksDB数据库(位置在taskManagerd的数据目录)。通过checkpoint, 整个RocksDB数据库被复制到配置的文件系统或目录中,并且元数据保存jobManager的内存中。
导入依赖
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-statebackend-rocksdb_2.12</artifactId>
<version>1.10.1</version>
</dependency>
//参数是checkpoint路径
env.setStateBackend(new RicksDBStateBackend("hdfs:hadoop102:8020/flink/ck"));
FsStateBackend和RocksDB需要结合Checkpoint一起使用;
4.配置方式
4.1 全局配置
flink可以通过flink-conf.yaml
全局配置state backend
# The backend that will be used to store operator state checkpoints
state.backend: filesystem
# Directory for storing checkpoints
state.checkpoints.dir: hdfs://namenode:40010/flink/checkpoints
state backend可选值包括:
- jobmanager (MemoryStateBackend)
- filesystem (FsStateBackend)
- rocksdb (RocksDBStateBackend)
state.checkpoints.dir设置checkpoints数据和元数据文件
4.2 单任务配置
通过在单个flink任务中通过env.setStateBackend(…)单独调整state backend配置,这种方式会覆盖全局配置。例如:
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setStateBackend(new FsStateBackend("hdfs://namenode:40010/flink/checkpoints"));
2.状态的备份(checkpoint 和 savepoint)
用户可以根据的程序里面的配置将checkpoint打开,给定一个时间间隔后,框架会按照时间间隔给程序的状态进行备份。当发生故障时,Flink会将所有Task的状态一起恢复到Checkpoint的状态。从哪个位置开始重新执行。
- 增量异步备份:不会因为备份影响流计算
- 本地恢复:从本地存储的状态数据恢复
4.3 状态一致性
什么是状态一致性?
Flink中算子都是有状态的,在遇到故障宕机的时候可以恢复状态,恢复完之后还能重新计算,结果也是正确的就是状态一致性。
当在分布式系统中引入状态时,自然也引入了一致性问题。
一致性实际上是"正确性级别"的另一种说法,也就是说在成功处理故障并恢复之后得到的结果,与没有发生任何故障时得到的结果相比,前者到底有多正确?举例来说,假设要对最近一小时登录的用户计数。在系统经历故障之后,计数结果是多少?如果有偏差,是有漏掉的计数还是重复计数?所以根据实际情况就可以对一致性设定不同的级别
4.3.1 flink状态的一致性级别
flink在流处理中,其状态一致性可以分为3个级别:
at-most-once: 这其实是没有正确性保障的委婉说法——故障发生之后,计数结果可能丢失。
at-least-once: 这表示计数结果可能大于正确值,但绝不会小于正确值。也就是说, 计数程序在发生故障后可能多算,但是绝不会少算。
exactly-once: 这指的是系统保证在发生故障后得到的计数结果与正确值一致。
曾经,at-least-once非常流行。第一代流处理器(如Storm和Samza)刚问世时只保证at-least-once,原因有二。
- 保证exactly-once的系统实现起来更复杂。这在基础架构层(决定什么代表正确,以及exactly-once的范围是什么)和实现层都很有挑战性。
- 流处理系统的早期用户愿意接受框架的局限性,并在应用层想办法弥补(例如使应用程序具有幂等性,或者用批量计算层再做一遍计算)。
最先保证exactly-once的系统(Storm Trident和Spark Streaming)在性能和表现力这两个方面付出了很大的代价。为了保证exactly-once,这些系统无法单独地对每条记录运用应用逻辑,而是同时处理多条(一批)记录,保证对每一批的处理要么全部成功,要么全部失败。这就导致在得到结果前,必须等待一批记录处理结束。因此,用户经常不得不使用两个流处理框架(一个用来保证exactly-once,另一个用来对每个元素做低延迟处理),结果使基础设施更加复杂。曾经,用户不得不在保证exactly-once与获得低延迟和效率之间权衡利弊。Flink避免了这种权衡。
Flink的一个重大价值在于,它既保证了exactly-once,也具有低延迟和高吞吐的处理能力。
从根本上说,Flink通过使自身满足所有需求来避免权衡,它是业界的一次意义重大的技术飞跃。尽管这在外行看来很神奇,但是一旦了解,就会恍然大悟。
4.3.2 端到端(end-to-end)状态一致性
上面所描述的一致性保证
都是由流处理器实现的,也就是说都是在 Flink 流处理器内部保证的;而在真实应用中,流处理应用除了流处理器以外还包含了数据源(例如 Kafka)和输出到持久化系统。
端到端的一致性保证,意味着结果的正确性贯穿了整个流处理应用的始终;每一个组件都保证了它自己的一致性,整个端到端的一致性级别取决于所有组件中一致性最弱的组件。 具体可以划分如下:
- source端 —— 需要外部源可重设数据的读取位置
- flink内部 —— 依赖checkpoint
- sink端 —— 需要保证从故障恢复时,数据不会重复写入外部系统
而对于sink端,又有两种具体的实现方式:幂等(Idempotent)写入和事务性(Transactional)写入。
不同 Source 和 Sink 的一致性保证可以用下表说明:
1.Sink端幂等写入
所谓幂等操作,是说一个操作,可以重复执行很多次,但只导致一次结果更改,也就是说,后面再重复执行就不起作用了。
额外说明: sink的幂等写入出现故障恢复的时候,flink应用程序中算子状态和写出的数据库中数据不一致问题,因为假设在A状态flink做了CheckPoint,然后程序继续运行到B状态,此时数据库中存储的是B状态,然后故障重启,flink从A状态开始恢复,但是数据库中是B状态,这就是短暂的不一致,当再次写入到数据库中,就一致了。
2.Sink端事务写入
事务写入原理: 构建事务来写入外部系统,构建的事务对应着 checkpoint,等到 checkpoint 真正完成的时候,才把所有对应的结果写入 sink 系统中。
事务性写入两种实现方式: 预写日志(WAL)和两阶段提交(2PC)。
2.1 预写日志
原理:
- 在JobManager发出Checkpoint任务的时候,Sink启动一个事务,将所接收到的数据放进状态后台进行缓存,而不是直接写出去。等CheckPoint完成之后,提交事务,一次性写出。
特点:
- 简单容易实现;由于数据在状态后端做了缓存,所以无论什么sink系统,这种方式都能一批次搞定
实现:
- DataStream API提供了一个模板类:GenericWriteAheadSink,来实现这种预写日志的事务型sink;
实现举例:
- 搞一个list状态,再搞个时间状态,第一条数据来了,时间状态为null,定义一个定时器十秒后触发,第一条数据直接往List中扔,每条数据都往里面扔,扔完了后判断一下List中个数>=10了,写出并清空时间状态;如果<10,继续写;或者等到定时器触发,再清空;[10s或者10条数据来写到数据库中]
Qps: query per second 滴滴5-7w qps 所以一般都是批量提交
预写日志存在的问题:
- 一次性写出为批量写出,牺牲了一定的实时性。
- 如果想通过预写日志的方式实现Sink的精准一次性写出,这就要求自定义的Sink拥有事务功能,如果没有事务,在往外写出的过程中失败,source端重复消费重写,由于不能撤回,那么会导致数据重复。[注意:预写日志是事务写出的Sink方案,所以不考虑幂等性]
2.2 两阶段提交
原理:
- 预提交: 在JobManager发出checkpoint任务的时候,sink会启动一个事务,将接下来所有接收的数据添加到事务中;将这些数据直接写到外部Sink系统,但是不提交事务,此时是“预提交”;
- 提交: 当JobManager真正完成CheckPoint,sink接收到checkPoint完成的通知时,才会正式的提交事务,此时结果真正的写入外部系统。
特点:
- 每一个barrier都会开启一个独立的事务
2阶段提交对外部Sink系统的要求
事务写入的两种方式对比总结:
共同点:预提交和预写日志都是当Checkpoint完成,才完成对外部系统的写出。
不同点:预写日志是在sink做缓存,等到CK完成后,再将缓存内容批量写出;这个过程中事务从开启到关闭的时间很短暂,开启事务,写出缓存,关闭事务;
2阶段提交是不缓存,而是当sink收集到 barrier之后就开启当前barrier的事务,然后sink往外部系统写出,并等待JobManager通知sink CheckPoint完成,等到CK完成只是提交个事务,数据已经写出去了,预提交效率高一点。
预写日志的事务开启时间短暂,而且和barrier没有关系,两阶段提交和barrier有关系;
5.Flink + Kafka实现端到端的一致性
1.预提交
Kafka的producer就是两阶段提交,生产者预提交的数据生产者读取不到;
- JobManager往Source中注入Barrier
- Source将 消费的偏移量存到状态后端,并且barrier往下流(二者是并行)
- 以此类推,直到sink算子存储状态+预提交
2.真正的提交
- 当sink收集到一个barrier之后,会向JobManager汇报可以做CheckPoint了,JobManager再向Sink通知CheckPoint完成,在Sink等待收到CK完成的通知的过程中,Sink仍然会不断地接收barrier后面的数据,但是不是做缓存,而是开启一个新的事务,因此barrier后面的数据会进入新的事务。
- 所以一个barrier对应一个检查点对应一个事务,当JobManager反馈检查点完成,事务才提交。[一个barrier对应一个事务]