Flink基石----State and Checkpoint
目录
- Flink基石----State and Checkpoint
- Flink State and Checkpoint
- State
- ValueState
- Checkpoint
- 通过checkpoint来恢复之前的状态
- 1、从 Flink web 页面
- 2、通过flink命令
- State
- ListState
Flink State and Checkpoint
Flink 通过 State 和 Checkpoint 来实现容错和数据处理的唯一一次
State
丰富的State API。
ValueState<T>
: 保存一个可以更新和检索的值(如上所述,每个值都对应到当前的输入数据的 key,因此算子接收到的每个 key 都可能对应一个值)。 这个值可以通过update(T)
进行更新,通过T value()
进行检索。ListState<T>
: 保存一个元素的列表。可以往这个列表中追加数据,并在当前的列表上进行检索。可以通过add(T)
或者addAll(List<T>)
进行添加元素,通过Iterable<T> get()
获得整个列表。还可以通过update(List<T>)
覆盖当前的列表。ReducingState<T>
: 保存一个单值,表示添加到状态的所有值的聚合。接口与ListState
类似,但使用add(T)
增加元素,会使用提供的ReduceFunction
进行聚合。AggregatingState<IN, OUT>
: 保留一个单值,表示添加到状态的所有值的聚合。和ReducingState
相反的是, 聚合类型可能与 添加到状态的元素的类型不同。 接口与ListState
类似,但使用add(IN)
添加的元素会用指定的AggregateFunction
进行聚合。MapState<UK, UV>
: 维护了一个映射列表。 你可以添加键值对到状态中,也可以获得反映当前所有映射的迭代器。使用put(UK,UV)
或者putAll(Map<UK,UV>)
添加映射。 使用get(UK)
检索特定 key。 使用entries()
,keys()
和values()
分别检索映射、键和值的可迭代视图。你还可以通过isEmpty()
来判断是否包含任何键值对。所有类型的状态还有一个
clear()
方法,清除当前 key 下的状态数据,也就是当前输入元素的 key。
在之前我们的示例代码中统计单词的数量是通过 keyBy 之后做 sum 求和得出结果,而 Flink 的数据是一条一条处理的,那么能不能不用sum来求单词的数量呢??
package com.shujia.flink.state
import org.apache.flink.api.common.functions.MapFunction
import org.apache.flink.streaming.api.scala._
import scala.collection.mutable
object Demo1NoStateWC {
def main(args: Array[String]): Unit = {
val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
val linesDS: DataStream[String] = env.socketTextStream("master", 8888)
val kvDS: DataStream[(String, Int)] = linesDS.flatMap(_.split(",")).map((_, 1))
val keyByDS: KeyedStream[(String, Int), String] = kvDS.keyBy(_._1)
val countDS: DataStream[(String, Int)] = keyByDS.map(new MapFunction[(String, Int), (String, Int)] {
//统计单词的数量
var count = 0
override def map(value: (String, Int)): (String, Int) = {
count += 1
(value._1, count)
}
})
countDS.print()
env.execute()
}
}
但是这样会有问题
这是为什么呢?
因为每一个并行度中的 task 都会定义一个 var count = 0 来统计流过这个task的单词数量
java 和 jva 被分给一个task进行处理
所以同一个task中流过的数据,都会对该task中定义的 count 进行修改
那怎么办呢??
package com.shujia.flink.state
import org.apache.flink.api.common.functions.MapFunction
import org.apache.flink.streaming.api.scala._
import scala.collection.mutable
object Demo1NoStateWC {
def main(args: Array[String]): Unit = {
val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
val linesDS: DataStream[String] = env.socketTextStream("master", 8888)
val kvDS: DataStream[(String, Int)] = linesDS.flatMap(_.split(",")).map((_, 1))
val keyByDS: KeyedStream[(String, Int), String] = kvDS.keyBy(_._1)
val countDS: DataStream[(String, Int)] = keyByDS.map(new MapFunction[(String, Int), (String, Int)] {
/**
* 下面的方式会产生一个问题
*
* map集合的数据保存在内存中
* 任务失败重启之后 map 集合中的数据会丢失
* 可以使用MySQL或者Redis来存数据,但是这样会降低Flink的性能(增加了网络开销)
*
* flink的状态(之前的计算结果)
* 状态和普通的集合变量的区别
*
* flink的状态会被checkpoint持久化到hdfs中,如果任务失败了
* 重新启动,可以从hdfs中恢复任务,保证之前的计算结果不丢失
*
*/
//key 是单词,value 是单词的数量
//使用 HashMap 保存之前的计算结果
val map = new mutable.HashMap[String, Int]()
override def map(value: (String, Int)): (String, Int) = {
val word: String = value._1
//从map集合中获取单词的数量,如果有就返回,如果没有就返回0
val count: Int = map.getOrElse(word, 0)
//统计新的单词的数量
val newCount: Int = count + 1
//覆盖map集合
map.put(word, newCount)
(word, newCount)
}
})
countDS.print()
env.execute()
}
}
* flink的状态(之前的计算结果) * 状态和普通的集合变量的区别 * * flink的状态会被checkpoint持久化到hdfs中,如果任务失败了 * 重新启动,可以从hdfs中恢复任务,保证之前的计算结果不丢失
ValueState
单值状态
classOf[] -- Scala中获取类对象
package com.shujia.flink.state
import org.apache.flink.api.common.functions.{RichMapFunction, RuntimeContext}
import org.apache.flink.api.common.state.{ValueState, ValueStateDescriptor}
import org.apache.flink.configuration.Configuration
import org.apache.flink.streaming.api.scala._
object Demo2ValueState {
def main(args: Array[String]): Unit = {
val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
val linesDS: DataStream[String] = env.socketTextStream("master", 8888)
val kvDS: DataStream[(String, Int)] = linesDS.flatMap(_.split(",")).map((_, 1))
val keyByDS: KeyedStream[(String, Int), String] = kvDS.keyBy(_._1)
val countDS: DataStream[(String, Int)] = keyByDS.map(new RichMapFunction[(String, Int), (String, Int)] {
/**
* ValueState:单值状态,为每一个key保存一个值
* 保证在状态中的数据不会因为flink程序挂断而丢失
* checkpoint不能在本地执行,需要将代码提交到集群中,状态的数据需要保存在hdfs中
*
*/
var valueState: ValueState[Int] = _
/**
* open:在map之前执行,一般用于建立网络链接
* 还用于初始化状态
*
*/
override def open(parameters: Configuration): Unit = {
//1、获取flink的环境对象,通过环境初始化状态
val context: RuntimeContext = getRuntimeContext
//2、创建状态的描述对象,需要指定 状态名 和 状态保存的数据类型的类对象
//classOf[] -- Scala中获取类对象
val valueStateDesc = new ValueStateDescriptor[Int]("count", classOf[Int])
//3、获取状态
valueState = context.getState(valueStateDesc)
}
override def map(value: (String, Int)): (String, Int) = {
/**
* 使用状态统计单词的数量
*
*/
//获取之前的单词数量
val count: Int = valueState.value()
//计算新的数量
val newCount: Int = count + 1
//更新状态
valueState.update(newCount)
//返回数据
(value._1, newCount)
}
})
countDS.print()
env.execute()
}
}
为了观察 ValueState 的效果,需要开启 Checkpoint 然后在集群中运行
Checkpoint
开启 Checkpoint 比较麻烦,东西有点多,可以去官网找一下开启时需要指定的参数
package com.shujia.flink.state
import org.apache.flink.api.common.functions.{RichMapFunction, RuntimeContext}
import org.apache.flink.api.common.state.{ValueState, ValueStateDescriptor}
import org.apache.flink.configuration.Configuration
import org.apache.flink.runtime.state.filesystem.FsStateBackend
import org.apache.flink.streaming.api.CheckpointingMode
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.environment.CheckpointConfig.ExternalizedCheckpointCleanup
object Demo3Checkpoint {
def main(args: Array[String]): Unit = {
val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
/**
* checkpoint : 定时将flink执行过程中的状态保存到状态后端
* 只保存最新一次成功的checkpoint
*
*
* 任务执行失败之后可以从最新的checkpoint恢复任务
* flink run -c com.shujia.flink.state.Demo3Checkpoint -s hdfs://master:9000/flink/checkpoint/a306d8690ca7718aec6a425b0d8ee171/chk-645 flink-1.0.jar
*
* -s : 指定恢复任务的位置,需要指定到chk-***
*
*/
// 每 1000ms 开始一次 checkpoint
env.enableCheckpointing(1000)
// 高级选项:
// 设置模式为精确一次 (这是默认值)
env.getCheckpointConfig.setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE)
// 确认 checkpoints 之间的时间会进行 500 ms
env.getCheckpointConfig.setMinPauseBetweenCheckpoints(500)
// Checkpoint 必须在一分钟内完成,否则就会被抛弃
env.getCheckpointConfig.setCheckpointTimeout(60000)
// 同一时间只允许一个 checkpoint 进行
env.getCheckpointConfig.setMaxConcurrentCheckpoints(1)
// 当作业取消时,保留作业的 checkpoint。注意,这种情况下,需要手动清除该作业保留的 checkpoint。默认是不保留
//ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION:在取消作业时保留checkpoint。请注意,在这种情况下,您必须在取消后手动清理检查点状态。
//ExternalizedCheckpointCleanup.DELETE_ON_CANCELLATION:取消作业时删除checkpoint。仅当作业失败时,检查点状态才可用。
env.getCheckpointConfig.enableExternalizedCheckpoints(ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION)
//将状态保存到hdfs的状态后端
//在这里指定状态保存到HDFS中的路径
val stateBackend = new FsStateBackend("hdfs://master:9000/flink/checkpoint")
//设置状态后端
env.setStateBackend(stateBackend)
val linesDS: DataStream[String] = env.socketTextStream("master", 8888)
val kvDS: DataStream[(String, Int)] = linesDS.flatMap(_.split(",")).map((_, 1))
val keyByDS: KeyedStream[(String, Int), String] = kvDS.keyBy(_._1)
val countDS: DataStream[(String, Int)] = keyByDS.map(new RichMapFunction[(String, Int), (String, Int)] {
/**
* ValueState: 单值状态,为每一个key保存一个值
* 保证在状态中的数据不会因为flink程序挂断而丢失
* checkpoint不能再本地执行,需要将代码提交到服务器中,状态的数据需要保存再hdfs中
*
*/
var valueState: ValueState[Int] = _
/**
* open:方法在map之前执行,一般用于建立网络链接
* 还用于初始化状态
*
*/
override def open(parameters: Configuration): Unit = {
//1、获取flink的环境对象,通过环境初始化状态
val context: RuntimeContext = getRuntimeContext
//创建状态的描述对象,需要指定状态保存的数据类型和状体名
val valueStateDesc = new ValueStateDescriptor[Int]("count", classOf[Int])
//获取状态
valueState = context.getState(valueStateDesc)
}
override def map(value: (String, Int)): (String, Int) = {
/**
* 使用状态统计单词的数量
*
*/
//获取之前的单词数量
val count: Int = valueState.value()
//计算新的数量
val newCount: Int = count + 1
//更新状态
valueState.update(newCount)
//返回数据
(value._1, newCount)
}
})
countDS.print()
env.execute()
}
}
通过checkpoint来恢复之前的状态
将代码打成jar包,提交到集群中运行
在job执行的过程中,将job取消,如何通过checkpoint来恢复之前的状态??
1、从 Flink web 页面
2、通过flink命令
flink run -c 主类名 -s 指定恢复任务的位置(hdfs上的路径需要加上前缀 hdfs://master:9000 ,需要指定到chk-***) jar包名
State
ListState
集合状态
package com.shujia.flink.state
import java.lang
import org.apache.flink.api.common.functions.RuntimeContext
import org.apache.flink.api.common.state.{ListState, ListStateDescriptor}
import org.apache.flink.configuration.Configuration
import org.apache.flink.streaming.api.functions.KeyedProcessFunction
import org.apache.flink.streaming.api.scala._
import org.apache.flink.util.Collector
object Demo4ListState {
def main(args: Array[String]): Unit = {
val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
val studentDS: DataStream[String] = env.socketTextStream("master", 8888)
/**
* 实时统计每个班级的平均年龄
*
*/
val kvDS: DataStream[(String, Int)] = studentDS.map(stu => {
val split: Array[String] = stu.split(",")
val clazz: String = split(4)
val age: Int = split(2).toInt
(clazz, age)
})
//按照班级分组
val keyByDS: KeyedStream[(String, Int), String] = kvDS.keyBy(_._1)
/**
* process : flink底层的api,一条一条的将数据传到后面的方法中
* 可以在process中操作flink的时间、状态和事件
*
* KeyedProcessFunction[K,I,O]
* K -- key的类型
* I -- 输入数据类型
* O -- 输出数据类型
*/
val avgAgeDS: DataStream[(String, Double)] = keyByDS.process(new KeyedProcessFunction[String, (String, Int), (String, Double)] {
/**
* open : 在processElementz之前执行
* 有open方法,当然也有close方法
*/
/**
* 集合状态 : 为每一个key保存一个集合
*
*/
var listState: ListState[Int] = _
override def open(parameters: Configuration): Unit = {
//获取flink环境
val context: RuntimeContext = getRuntimeContext
//创建集合状态的描述对象
val listStateDesc = new ListStateDescriptor[Int]("ages", classOf[Int])
//通过 context 也能获取其他的状态
listState = context.getListState(listStateDesc)
}
/**
* processElement : 每一条数据执行一次
*
* @param kv :一条数据
* @param ctx : 上下文对象
* @param out : 用于将数据发送到下游
*/
override def processElement(kv: (String, Int),
ctx: KeyedProcessFunction[String, (String, Int), (String, Double)]#Context,
out: Collector[(String, Double)]): Unit = {
//接收元组中元素
val (clazz, age) = kv
//将每一条数据的年龄保存到集合状态中
listState.add(age)
//从状态中获取所有的年龄计算平均值
val iterable: lang.Iterable[Int] = listState.get()
//导入一个隐式转换将java的迭代器转换成scala的集合
import scala.collection.JavaConversions._
val ages: List[Int] = iterable.iterator().toList
val avgAge: Double = ages.sum.toDouble / ages.length
//将数据发送到下游
out.collect(clazz, avgAge)
}
})
avgAgeDS.print()
env.execute()
}
}