相对于其他流计算框架,Flink 一个比较重要的特性就是其支持有状态计算。即你可以将中间的计算结果进行保存,并提供给后续的计算使用:
state一般指一个具体的task/operator的状态,state数据默认保存在java堆内存中
Flink 又将状态 (State) 分为 Keyed State 与 Operator State:
算子状态 (Operator State):顾名思义,状态是和算子进行绑定的,一个算子的状态不能被其他算子所访问到。官方文档上对 Operator State 的解释是:each operator state is bound to one parallel operator instance,所以更为确切的说一个算子状态是与一个并发的算子实例所绑定的,即假设算子的并行度是 2,那么其应有两个对应的算子状态:
算子状态的作用范围限定为算子任务,由同一并行子任务所处理的所有数据都可以访问到相同的状态;
状态对于同一个任务而言是共享的(每一个并行的子任务共享一个状态);
算子状态不能由相同或不同算子的另一个任务访问(相同算子的不同任务之间也不能访问);
算子状态数据结构
键控状态 (Keyed State) :是基于keyStream上的状态,对keyStream流上的每个key都对应一个state,即状态是根据输入数据流中定义的 key 值进行区分的,Flink 会为每类键值维护一个状态实例,并将具有相同键的所有数据都分区到同一个算子任务中,这个任务会维护和处理这个key对应的状态 如下图所示,每个颜色代表不同 key 值,对应四个不同的状态实例。需要注意的是键控状态只能在 KeyedStream 上进行使用,我们可以通过 stream.keyBy(…) 来得到 KeyedStream 。
1.键控状态是针对无界流中的keyStream而设计的
2.将原来的DataStream通过keyBy算子指定的字段进行分组,将分组后结果进行有状态的处理,需要
使用富函数,通过富函数相应方法从父类中继承getRuntimeContext,据此注册一个状态
高铁G66抵达了北京西站,旅客依次通过出口,在出口处安放有红外体温测量仪,针对于每个旅客的体温进行监测,xxx旅客的体温偏高,为了蒙蔽体温测量仪,采取措施人为降温,正常通过了红外测温仪的探测,过后该旅客上地铁,也要经过体温测量较之于上次体温升高了0.8度,被地铁的红外测温仪探测到了,发出了警告 ...
package com.jd.unbounded.sample_state
import com.jd.unbounded.Raytek
import org.apache.flink.api.common.functions.RichFlatMapFunction
import org.apache.flink.api.common.state.{ValueState, ValueStateDescriptor}
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.api.scala._
import org.apache.flink.configuration.Configuration
import org.apache.flink.util.Collector
/**
* Description 键控状态演示
*
* @author lijun
* @create 2020-03-31
*/
object KeyedStateTest {
def main(args: Array[String]): Unit = {
//1.环境
val env = StreamExecutionEnvironment.getExecutionEnvironment
//2.获取两个无界流
env.socketTextStream("localhost",6666)
.filter(_.trim.nonEmpty)
.map(perTraveller=>{
val arr = perTraveller.split(",")
val id = arr(0).trim
val temperature = arr(1).trim.toDouble
val name = arr(2).trim
val timestamp = arr(3).trim.toLong
val location = arr(4).trim
Raytek(id, temperature, name, timestamp, location)
}).keyBy("name")
.flatMap(new MyRichFlatMapFunction(0.8))
.print("累加处理后结果是-->")
//4.启动
env.execute()
}
/**
* 自定义的富函数
* @param threshold 体温变化的阈值
*/
class MyRichFlatMapFunction(threshold:Double) extends RichFlatMapFunction[Raytek,(Raytek,String)]{
//通过valueState来存储当前旅客上一次的体温信息
var tempValueState:ValueState[Double] = _
//初始化
override def open(parameters: Configuration): Unit = {
//步骤
//1. ValueStateDescriptor,封装了ValueState中元素的类型信息
val desc:ValueStateDescriptor[Double] = new ValueStateDescriptor("temperature",classOf[Double])
//2.注册一个ValueState
tempValueState = getRuntimeContext.getState[Double](desc)
}
/**
* 每次处理 DataStream中实时产生的元素
* @param value
* @param out
*/
override def flatMap(value: Raytek, out: Collector[(Raytek, String)]): Unit = {
//获得状态中保存的旅客上一次的体温信息
val lastTemperature = tempValueState.value()
val nowTemperature = value.temperature
val normal = nowTemperature >= 36.3 && nowTemperature <= 37.2
if(normal){ //1.若体温正常的话,将旅客本次的体温和上次的体温进行比对 若体温差> 0.8, 此时直接发往目标DataStream,进行后续处理,体温差在正常范围内,不予干预
if(lastTemperature > 0){
val difTemperature = (nowTemperature - lastTemperature).abs
if(difTemperature > threshold){
out.collect((value,s"旅客${value.name},你好,你本次测得的体温是${value.temperature},上次测得的体温是${lastTemperature} 体温差为${difTemperature},不在临界值${threshold}之内,请接受处理"))
}
}
}else{//2.判断旅客的体温是否在正常范围内,若不正常直接发往目标DataStream,进行后续处理
out.collect((value,s"旅客${value.name},你好,你的体温是${value.temperature},不在正常范围之内36.3-37.2,请接受工作人员的处理..."))
}
//更新状态值为该旅客最新的体温信息
tempValueState.update(nowTemperature)
}
}
}
为了使 Flink 的状态具有良好的容错性,Flink 提供了检查点机制 (CheckPoints) 。通过检查点机制,Flink 定期在数据流上生成 checkpoint barrier ,当某个算子收到 barrier 时,即会基于当前状态生成一份快照,然后再将该 barrier 传递到下游算子,下游算子接收到该 barrier 后,也基于当前状态生成一份快照,依次传递直至到最后的 Sink 算子上。当出现异常后,Flink 就可以根据最近的一次的快照数据将所有算子恢复到先前的状态。
默认情况下,检查点机制是关闭的,需要在程序中进行开启:
checkpoint开启之后,默认的checkPointMode是Exactly-once
checkpoint的checkPointMode有两种,Exactly-once和At-least-once
// 开启检查点机制,并指定状态检查点之间的时间间隔
env.enableCheckpointing(1000);
// 其他可选配置如下:
// 设置语义
env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);
// 设置两个检查点之间的最小时间间隔
env.getCheckpointConfig().setMinPauseBetweenCheckpoints(500);
// 设置执行Checkpoint操作时的超时时间
env.getCheckpointConfig().setCheckpointTimeout(60000);
// 设置最大并发执行的检查点的数量
env.getCheckpointConfig().setMaxConcurrentCheckpoints(1);
// 将检查点持久化到外部存储
env.getCheckpointConfig().enableExternalizedCheckpoints(
ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION);
// 如果有更近的保存点时,是否将作业回退到该检查点
env.getCheckpointConfig().setPreferCheckpointForRecovery(true);
如上图sum_even (2+4),sum_odd(1 + 3 + 5),5这个数据之前的都处理完了,就出保存一个checkpoint;Source任务保存状态5,sum_event任务保存状态6,sum_odd保存状态是9;这三个保存到状态后端中就构成了CheckPoint;
Flink故障恢复机制的核心,就是应用状态的一致性检查点;
有状态流应用的一致性检查点(checkpoint),其实就是所有任务的状态,在某个时间点的一份拷贝(一份快照);这个时间点,应该是所有任务都恰好处理完一个相同的输入数据的时候 。(这个同一时间点并不是物理上的在同一时刻)
sum_even(2 + 4 + 6);sum_odd(1 + 3 + 5);
在执行应用程序期间,Flink会定期保存状态的一致性检查点;
如果发生故障,Flink将会使用最近的检查点来一致恢复应用程序的状态,并重新启动处理流程;
遇到故障之后,第一步就是重启应用;
第二步是从checkpoint中读取状态,将状态重置;
从检查点重新启动应用程序后,其内部状态与检查点完成时的状态完全相同;
这种检查点的保存和恢复机制可以为应用程序状态提供“精确一次”(exactly-once)的一致性,因为所有算子都会保存检查点并恢复其所有状态,这样一来所有的输入流就都会被重置到检查点完成时的位置。
简单:暂停应用,保存状态到检查点,再重新恢复应用;
Flink的改进:基于Chandy-Lamport算法的分布式快照;将检查点的保存和数据处理分离开,不暂停整个应用;
检查点分界线(CheckPoint Barrier)
Flink的检查点算法用到了一种称为分界线(barrier)的特殊数据形式,用来把一条流上数据按照不同的检查点分开;
分界线之前到来的数据导致的状态更改,都会被包含在当前分界线所属的检查点中;而基于分界线之后的数据导致的所有更改,就会被包含在之后的检查点中;
现在是一个有两个输入流的应用程序,用并行的两个Source任务来读取:
两个并行输入源按奇偶数来做sum,类似keyBy重分区map为二元组再做奇偶keyBy,Sum odd(1 + 1 + 3),Sum even(2)
JobManager会向每个source任务发送一条带有新检查点ID的消息,通过这种方式来启动检查点;
数据源将它们的状态写入检查点,并发出一个检查点barrier;
状态后端在状态存入检查点之后,会返回通知给source任务,source任务就会向JobManager确认检查点完成。
source1和source2收到检查点ID = 2时,分别存入自己的偏移量蓝3和黄4,存完之后返回一个ID2通知JobManager快照已保存好;(在保存快照时它会暂停发送和处理数据,同事它也会向下游发送带有检查点ID的barrier,发送的方式直接广播;这个过程中Sum和sink任务也没闲着都在处理数据)
分界线对齐(barrier对齐):barrier向下游传递,sum任务会等待所有输入分区的的barrier到达;
对于barrier已经到达的分区,继续到达的数据会被缓存;
而barrier尚未到达的分区,数据会被正常处理;
(比如蓝2通知给了Sum even,它会等黄2的barrier到达,这时处理的数据4来了,会先被缓存因为它数据下一个checkpoint的数据; 黄2的checkpoint还没来这时它如果来数据还会正常处理更改状态,如上图的在黄2的barrier还没来之前,source2的数据来了条4,它会正常处理Sum event(2 + 2 + 4))
当收到所有输入分区的barrier时,任务就将其状态保存到状态后端的检查点中,然后将barrier继续向下游转发。
barrier对齐之后(Sum even和Sum odd都接收到了两个source发来的barrier),将它们各自的8状态存入checkpoint中;接下来继续向下游Sink广播barrier;
向下游转发检查点的barrier后,任务继续正常的数据处理;
先处理缓存的数据,蓝4加载进来Sum event 12,黄6进来Sum event 18。
Sink任务向JobManager确认状态保存到checkpoint完毕;(Sink接收到barrier后先保存状态到checkpoint,然后向JobManager汇报)
当所有任务都确认已成功将状态保存到检查点时,检查点就真正完成了。
检查点算法总结
使用到的核心技术是: ABS(Asynchronous Barrier snapshot)异步分割线快照
barrier
1.直译为"分割线"
2.是一个DataStream中的数据标识
3.在pipleLine中流动着的,但是不参与计算的,与其维护的待计算的数据一起在pipleline中流动
4.每次计算的数据是barrier之前的数据
5.source中同一个时点产生的待计算的源数据会划分到一个barrier中
JobManager与Barrier的关系
根据source汇报的情况,自动在相应的待处理的数据之前添加barrier
barrier与checkpoint的关系
sink之前的barrier的状态信息存储在Memory,RocksDB中的barrier抵达了sink之后,sink确认后,将迄今为止计算的最新的结果落地到checkpoint目的地(JobManager的内存, HDFS等)
Flink还提供了可以自定义的镜像保存功能,就是保存点(savepoints);
原则上,创建保存点使用的算法与检查点完全相同,因此保存点可以认为就是具有一些额外元数据的检查点;
Flink不会自动创建保存点,因此用户(或者外部调度程序)必须明确地触发创建操作;
保存点是一个强大的功能,除了故障恢复外,保存点可以用于:有计划的手动备份,更新应用程序,版本迁移,暂停或重启应用,等等
checkpoint vs savepoint的区别?
checkpoint 应用定时触发,用于保存状态,会过期,内部应用失败重启的时候使用
savepoint 用户手动执行,是指向checkpoint的指针,不会过期,在升级的情况下使用
触发savepoint
bin/flink savepoint jobId [targetDirectory] [-yid yarnAppId](针对on yarn模式需要指定 -yid参数)
bin/flink run -s hdfs://node01:9000/flink/state/fs/733d4f71f675f6174079196b1d5ef49c/chk-14/_metadata flink-job.jar
默认情况下,所有的状态都存储在 JVM 的堆内存中,在状态数据过多的情况下,这种方式很有可能导致内存溢出,因此 Flink 该提供了其它方式来存储状态数据,这些存储方式统一称为状态后端 (或状态管理器)
状态后端主要负责两件事:
状态后端主要有以下三种:
1.MemoryStateBackend
2.FsStateBackend
基于文件系统进行存储,可以是本地文件系统,也可以是 HDFS 等分布式文件系统。 需要注意而是虽然选择使用了 FsStateBackend ,但正在进行的state数据仍是存储在 TaskManager 的内存中的,只有在 checkpoint 时,才会将state的快照数据写入到指定文件系统(hdfs)上。
特点:同时拥有内存级的访问速度和更好的容错保证可使用hdfs等分布式文件系统
3.RocksDBStateBackend
RocksDBStateBackend 在本地文件系统中维护状态,state会直接写入本地rocksdb中。同时RocksDB需要配置一个远端的filesystem。
uri(一般是HDFS),在做checkpoint的时候,会把本地的数据直接复制到filesystem中。fail over的时候从filesystem中恢复到本地。
RocksDB克服了state受内存限制的缺点,同时又能够持久化到远端文件系统中,比较适合在生产中使用
状态(state):包含算子状态、监控状态,就是task在执行时产生的一些结果数据需要存储起来 (以状态这种形式存储)
检查点(checkpoint): 存储的是应用迄今为止计算后的结果
State Backend(状态的后端存储):
Restart Strategies(重启策略)
常用的重启策略
Flink 支持使用两种方式来配置后端管理器:
第一种方式:基于代码方式进行配置,只对当前作业生效:
// 配置 FsStateBackend
env.setStateBackend(new FsStateBackend("hdfs://namenode:40010/flink/checkpoints"));
// 配置 RocksDBStateBackend
env.setStateBackend(new RocksDBStateBackend("hdfs://namenode:port/flink/checkpoints"));
第二种方式:基于 flink-conf.yaml
配置文件的方式进行配置,对所有部署在该集群上的作业都生效:
state.backend: filesystem
state.checkpoints.dir: hdfs://namenode:port/flink/checkpoints
#检查点中保存的数据是否采用增量的方式
state.backend.incremental: false
#flink应用失败后的重启策略
jobmanager.execution.failover-strategy: region
特点:
FsStateBackend 状态存储在TaskManager的内存,checkpoint存储在HDFS
package com.jd.unbounded.sample_statebackend.a_fs
import java.util.concurrent.TimeUnit
import org.apache.flink.api.common.restartstrategy.RestartStrategies
import org.apache.flink.api.common.time.Time
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.api.scala._
import org.apache.flink.runtime.state.filesystem.FsStateBackend
/**
* Description 状态后端之FsBackendState验证
*
* @author lijun
* @create 2020-03-31
*/
object FsStateBackendTest {
def main(args: Array[String]): Unit = {
//执行环境
val env = StreamExecutionEnvironment.getExecutionEnvironment
//设置状态后端(下述提示api过时的原因,官方推荐配置文件的方式,不建议使用硬编码的方式)
env.setStateBackend(new FsStateBackend("hdfs://node01:9000/flink/state/fs"))
//启用checkpoint
env.enableCheckpointing(10000)
//计算
env.socketTextStream("node01",8888)
.flatMap(_.split("\\s+"))
.filter(_.nonEmpty).map((_,1))
.keyBy(0)
.sum(1)
.print("状态后端之FsBackend")
// 启动
env.execute(this.getClass.getSimpleName)
//设置应用的重启策略(一般在配置文件设定)
env.setRestartStrategy(RestartStrategies.fixedDelayRestart(60,Time.of(10,TimeUnit.SECONDS)))
}
}
RocksDBStateBackend状态存储在RocksDB中,checkpoint存储在HDFS
RocksDB是嵌入式的KV对的DB,由facebook开发的,存储的数据庞大且高效
第一步:导入依赖
org.rocksdb
rocksdbjni
5.11.3
第二步:通过程序验证数据的写入和读取
package com.jd.unbounded.sample_statebackend.b_rocksdb;
import org.rocksdb.Options;
import org.rocksdb.RocksDB;
import org.rocksdb.RocksDBException;
/**
* Description
* @author lijun
* @create 2020-03-31
*/
public class RocksDBTest {
//因为RocksDB是由C++编写的,在Java中使用首先需要加载Native库
static{
RocksDB.loadLibrary();
}
public static void main(String[] args) throws RocksDBException {
//1.打开数据库
//1.1 创建数据库配置
Options dbOpt = new Options();
//1.2 配置当数据库不存在时自动创建
dbOpt.setCreateIfMissing(true);
//1.3 打开数据库,因为RocksDB默认是保存在本地磁盘,所以需要指定位置
RocksDB rdb = RocksDB.open("/Users/lijun/Downloads/flink_input/rocksdb");
//2.写入数据
//2.1 RocksDB是以字节流的方式写入数据库中,所以我们需要将字符串转换为字节流再写入
byte[] key = "张五".getBytes();
byte[] value = "20".getBytes();
//2.2 调用put方法写入数据
rdb.put(key,value);
System.out.println("写入数据到RocksDB完成");
//3.调用get方法读取数据
System.out.println("从RocksDB读取key="+new String(key)+"的value为"+new String(rdb.get(key)));
//4.移除数据
rdb.delete(key);
//关闭资源
rdb.close();
dbOpt.close();
}
}
添加依赖
org.apache.flink
flink-statebackend-rocksdb_2.11
1.9.1
源码
package com.jd.unbounded.sample_statebackend.b_rocksdb
import java.util.concurrent.TimeUnit
import org.apache.flink.api.scala._
import org.apache.flink.api.common.restartstrategy.RestartStrategies
import org.apache.flink.api.common.time.Time
import org.apache.flink.contrib.streaming.state.RocksDBStateBackend
import org.apache.flink.runtime.state.filesystem.FsStateBackend
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
/**
* Description
*
* @author lijun
* @create 2020-03-31
*/
object RocksDBStateBackendTest {
def main(args: Array[String]): Unit = {
//执行环境
val env = StreamExecutionEnvironment.getExecutionEnvironment
//设置状态后端(下述提示api过时的原因,官方推荐配置文件的方式,不建议使用硬编码的方式)
val rocks = new RocksDBStateBackend("hdfs://node01:9000/flink/state/fs",true)
//单独设置RocksDB存储的目录,若是不单独设置,目录在以java.io.tmpdir为key对应的值 (System.getProperty("java.io.tmpdir"))
rocks.setDbStoragePath("/Users/lijun/Downloads/flink_input/statebackend")
env.setStateBackend(rocks)
//启用checkpoint
env.enableCheckpointing(10000)
//计算
env.socketTextStream("node01",8888)
.flatMap(_.split("\\s+"))
.filter(_.nonEmpty).map((_,1))
.keyBy(0)
.sum(1)
.print("状态后端之RocksDBStateBackend")
// 启动
env.execute(this.getClass.getSimpleName)
//设置应用的重启策略(一般在配置文件设定)
env.setRestartStrategy(RestartStrategies.fixedDelayRestart(60,Time.of(10,TimeUnit.SECONDS)))
}
}