目录
需求
技术点
实现
checkpoint
State Backends
Watermark
Broadcast State 模式
Keyed State
需求
用户自定义电子围栏,记录车辆进出电子围栏的时间。
技术点
- checkpoint
- stateBackend
- watermark
- Broadcast State 模式
- 数据流连接广播流
- state
实现
checkpoint
Flink中的每个方法或算子都能够是有状态的。状态化的方法在处理单个元素/事件的时候存储数据,让状态成为使各个类型的算子更加精细的重要部分。为了让状态容错,Flink需要为状态添加checkpoint(检查点)。Checkpoint 使得Flink能够恢复状态和在流中的位置,从而向应用提供和无故障执行时一样的语义。
默认情况下,checkpoint 是禁用的,通过 StreamExecutionEnvironment 的 enableCheckpointing(n) 来启用 checkpoint,里面的 n 是进行 checkpoint 的间隔,单位毫秒。
val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
// 开启checkpoint 单位毫秒 并设置模式为精准一次 env.enableCheckpointing(60000,
CheckpointingMode.EXACTLY_ONCE)
// Checkpoint 必须在一分钟内完成,否则就会被抛弃
env.getCheckpointConfig.setCheckpointTimeout(60000)
// 以下是高级选项:
// 设置模式为精确一次 (这是默认值)
env.getCheckpointConfig.setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE)
// 确认 checkpoints 之间的时间会进行 500 ms
env.getCheckpointConfig.setMinPauseBetweenCheckpoints(500)
// Checkpoint 必须在一分钟内完成,否则就会被抛弃
env.getCheckpointConfig.setCheckpointTimeout(60000)
// 如果 task 的 checkpoint 发生错误,会阻止 task 失败,checkpoint 仅仅会被抛弃
env.getCheckpointConfig.setFailTasksOnCheckpointingErrors(false)
// 同一时间只允许一个 checkpoint 进行
env.getCheckpointConfig.setMaxConcurrentCheckpoints(1)
State Backends
Flink 内置了以下这些开箱即用的 state backends :
- MemoryStateBackend
- FsStateBackend
- RocksDBStateBackend
如果不设置,默认使用 MemoryStateBackend。
MemoryStateBackend 适用场景:
- 本地开发和调试。
- 状态很小的 Job,例如:由每次只处理一条记录的函数(Map、FlatMap、Filter 等)构成的 Job。Kafka Consumer 仅仅需要非常小的状态。
FsStateBackend 适用场景:
- 状态比较大、窗口比较长、key/value 状态比较大的 Job。
- 所有高可用的场景。
RocksDBStateBackend 的适用场景:
- 状态非常大、窗口非常长、key/value 状态非常大的 Job。
- 所有高可用的场景。
Watermark
策略简介
为了使用事件时间语义,Flink 应用程序需要知道事件时间戳对应的字段,意味着数据流中的每个元素都需要拥有可分配的事件时间戳。其通常通过使用
TimestampAssigner API 从元素中的某个字段去访问/提取时间戳。
时间戳的分配与 watermark 的生成是齐头并进的,其可以告诉 Flink 应用程序事件时间的进度。其可以通过指定
WatermarkGenerator 来配置 watermark 的生成方式。
使用 Flink API 时需要设置一个同时包含 TimestampAssigner 和 WatermarkGenerator 的 WatermarkStrategy。
consumer.assignTimestampsAndWatermarks(WatermarkStrategy.forBoundedOutOfOrderness(Duration.ofSeconds(20))
.withTimestampAssigner(new SerializableTimestampAssigner[ObjectNode] {
override def extractTimestamp(element: ObjectNode, recordTimestamp: Long): Long = element.get("value").get("gps_time").asLong()
}))
Broadcast State 模式
在本例中,我们定义两个流,一个包含车辆位置(KafkaStreamSource),包含车牌号、经纬度、时间戳属性。另一个包含规则,即用户自定义的电子围栏(Rule)
在位置数据流中,需要首先使用车牌号将流进行区分(keybay),这能确保相同车牌的数据会流转到相同的物理机。
val keyedKafkaSourceStream: DataStream[KafkaStreamSource] = env.addSource(consumer)
.filter(_.get("value").get("vno").asText() != "")
.map(obj => {
KafkaStreamSource(obj.get("value").get("vno").asText(), obj.get("value").get("longitude").asDouble(),
obj.get("value").get("latitude").asDouble, obj.get("value").get("gps_time").asLong())
})
.keyBy(_.vno)
对于规则流,它应该被广播到所有的下游task中,下游task应当存储这些规则并根据它寻找满足规则的电子围栏,使用MapStateDescriptor来描述并创建broadcast state 在下游的存储结构
// 一个 map descriptor,它描述了用于存储规则名称与规则本身的 map 存储结构
val ruleStateDescriptor = new MapStateDescriptor[String, Rule](
"RulesBroadcastState",
BasicTypeInfo.STRING_TYPE_INFO,
TypeInformation.of(new TypeHint[Rule] {}));
// 广播流,广播规则并且创建 broadcast state
val ruleBroadcastStream: BroadcastStream[Rule] = env.addSource(new GetRule)
.setParallelism(1)
.broadcast(ruleStateDescriptor)
最终,使用电子围栏规则流来匹配车辆位置,需要:
- 将两个流连接起来
- 完成我们的处理逻辑
为了关联一个非广播流(keyed 或者 non-keyed)与一个广播流(BroadcastStream),我们可以调用非广播流的方法 connect(),并将 BroadcastStream 当做参数传入。
这个方法的返回参数是 broadcastConnectedStream,具有类型方法 process(),传入一个特殊的 CoProcessFunction 来书写我们的模式识别逻辑。 具体传入 process() 的是哪个类型取决于非广播流的类型:
- 如果流是一个keyed流,那就是KeyedBroadcastProcessFunction 类型
- 如果流是non-keyed, 那就是 BroadcastProcessFunction 类型
在本次例子中,位置数据是一个keyed stream,所以代码如下:
// 数据流与规则流连接处理逻辑
keyedKafkaSourceStream
.connect(ruleBroadcastStream)
.process(new EfenceProcess)
EfenceProcess 类如下:
/**
* 数据流连接规则流处理逻辑
*/
class EfenceProcess extends KeyedBroadcastProcessFunction[String, KafkaStreamSource, Rule, KafkaStreamSource] {
private val log = LoggerFactory.getLogger(classOf[JSONKeyValueDeserializationSchema])
/**
* 矩形电子围栏转换
*
* @param coo
* @return
*/
def polygonCoordinateConvert(coo: String): Array[Point] = {
val array: JSONArray = JSON.parseArray(coo)
val ab = new ArrayBuffer[Point]()
for (arr <- array) {
val array1: JSONArray = JSON.parseArray(arr.toString)
ab.append(new Point(array1.getDouble(0), array1.getDouble(1)))
}
ab.toArray
}
/**
* 圆形电子围栏转换
*
* @param coo
* @return
*/
def circleCoordinateConvert(coo: String): Point = {
val array: JSONArray = JSON.parseArray(coo)
new Point(array(0).toString.toDouble, array(1).toString.toDouble)
}
/**
* 数据库插入语句
*
* @param vno
* @param efenceId
* @param inTime
*/
def insertDB(efenceId: Long, vno: String, inTime: Long): Unit = {
val id: Long = DB.localTx(implicit session => {
sql"INSERT INTO fence_log(fence_id, car_vno, in_time) VALUES (?, ?, ?)"
.bind(efenceId, vno, new Timestamp(inTime * 1000l))
.updateAndReturnGeneratedKey()
.apply()
})
dbIdState.update(id)
log.info(s"insert op, id is ${id}")
}
/**
* 数据库更新语句
*
* @param outTime
* @param id
*/
def updateDB(outTime: Long, id: Long): Unit = {
DB.localTx(implicit session => {
sql"UPDATE fence_log SET out_time = ? WHERE id = ?"
.bind(new Timestamp(outTime * 1000l), id)
.update()
.apply()
})
log.info(s"update op, id is ${id}")
}
/**
* 关闭临时电子围栏
* @param fenceId
*/
def closeEFence(fenceId: Long): Unit = {
DB.localTx(implicit session => {
sql"UPDATE fence SET `state` = ? WHERE id = ?"
.bind("关闭", fenceId)
.update()
.apply()
})
log.info(s"close temporary e-fence, current id is ${fenceId}")
}
// 规则流
private var ruleStateDescriptor: MapStateDescriptor[String, Rule] = _
// 电子围栏状态
private var efenceState: ValueState[Boolean] = _
// 数据库 id
private var dbIdState: ValueState[Long] = _
// 数据库连接池
private var dataSource: DruidDataSource = _
// 变量初始化、数据库连接池配置
override def open(parameters: Configuration): Unit = {
ruleStateDescriptor = new MapStateDescriptor[String, Rule]("RulesBroadcastState", BasicTypeInfo.STRING_TYPE_INFO, TypeInformation.of(new TypeHint[Rule]() {}))
efenceState = getRuntimeContext.getState(
new ValueStateDescriptor[Boolean]("efence", createTypeInformation[Boolean])
)
dbIdState = getRuntimeContext.getState(
new ValueStateDescriptor[Long]("id", createTypeInformation[Long])
)
// 数据路连接池
DBs.setup()
}
// 处理数据流的数据
override def processElement(kafkaSource: KafkaStreamSource, readOnlyContext: KeyedBroadcastProcessFunction[String, KafkaStreamSource, Rule, KafkaStreamSource]#ReadOnlyContext, collector: Collector[KafkaStreamSource]): Unit = {
// 车辆是否在电子围栏内的状态 true 为在, false 为不在
val tmpState: Boolean = efenceState.value()
// 记录插入数据后返回的id , 用于更新离开时间操作
val tmpId: Long = dbIdState.value()
// 初始状态 假设不在电子围栏
val currentState = if (tmpState != null) {
tmpState
} else {
false
}
// 初始化 id
val currentId = if (tmpId != null) {
tmpId
} else {
-1L
}
// 获取广播流状态中的电子围栏规则信息
val iterator: util.Iterator[Map.Entry[String, Rule]] = readOnlyContext.getBroadcastState(ruleStateDescriptor).immutableEntries().iterator()
while (iterator.hasNext) {
val rule: Rule = iterator.next().getValue
val efenceType: String = rule.`type`
// 数据流的车牌与规则流的车牌进行对应
if (kafkaSource.vno == rule.car_vno) {
// 电子围栏类型判断
if (efenceType.equals("polygon")) {
val coordinates: String = rule.coordinates
val points: Array[Point] = polygonCoordinateConvert(coordinates)
// 如果点位出现在电子围栏区域并且当前状态为false,即未曾出现在电子围栏中,判定为首次进入,进行数据库插入操作
if (EFence.isPtInPoly(kafkaSource.longitude, kafkaSource.latitude, points) && !currentState) {
collector.collect(kafkaSource)
efenceState.update(true)
val efenceId: Long = rule.`fence_id`
val vno: String = kafkaSource.vno
val inTime: Long = kafkaSource.gps_time
insertDB(efenceId, vno, inTime)
}
// 如果点位不在电子围栏区域并且当前状态为 true,即上一条数据在电子围栏中,判定首次离开,进行数据库更新操作
if (!EFence.isPtInPoly(kafkaSource.longitude, kafkaSource.latitude, points) && currentState) {
collector.collect(kafkaSource)
efenceState.clear()
val outTime: Long = kafkaSource.gps_time
updateDB(outTime, currentId)
// 如果电子围栏的状态为 临时, 一进一出后关闭该电子围栏
if (rule.`state` == "临时") {
closeEFence(rule.`fence_id`)
}
dbIdState.clear()
}
}
if (efenceType.equals("circle")) {
val center: String = rule.circle_center
val radius: Int = rule.circle_radius
val aPoint = new Point(kafkaSource.longitude, kafkaSource.latitude)
val cPoint: Point = circleCoordinateConvert(center)
// 如果点位出现在电子围栏区域并且当前状态为false,即未曾出现在电子围栏中,判定为首次进入,进行数据库插入操作
if (EFence.isPtInCircle(aPoint, cPoint, radius) && !currentState) {
collector.collect(kafkaSource)
efenceState.update(true)
val efenceId: Long = rule.`fence_id`
val vno: String = kafkaSource.vno
val inTime: Long = kafkaSource.gps_time
insertDB(efenceId, vno, inTime)
}
// 如果点位不在电子围栏区域并且当前状态为 true,即上一条数据在电子围栏中,判定首次离开,进行数据库更新操作
if (!EFence.isPtInCircle(aPoint, cPoint, radius) && currentState) {
collector.collect(kafkaSource)
efenceState.clear()
val outTime: Long = kafkaSource.gps_time
updateDB(outTime, currentId)
// 如果电子围栏的状态为 临时, 一进一出后关闭该电子围栏
if (rule.`state` == "临时") {
closeEFence(rule.`fence_id`)
}
dbIdState.clear()
}
}
}
}
}
// 处理广播流的数据
override def processBroadcastElement(rule: Rule, context: KeyedBroadcastProcessFunction[String, KafkaStreamSource, Rule, KafkaStreamSource]#Context, collector: Collector[KafkaStreamSource]): Unit = {
context.getBroadcastState(ruleStateDescriptor).put("broadcast", rule)
}
override def close(): Unit = {
dataSource.close()
}
}
广播流处理如下:
class GetRule extends RichSourceFunction[Rule] {
// 数据库配置初始化
DBs.setup()
var isRunning: Boolean = _
val duration = 1 * 60 * 1000L
override def open(parameters: Configuration): Unit = {
isRunning = true
}
override def run(sourceContext: SourceFunction.SourceContext[Rule]): Unit = {
while (isRunning) {
val rules: List[Rule] = DB readOnly { implicit session =>
sql"select * from fence_car"
.map(rs => Rule(rs.string("car_vno"), rs.long("fence_id"), rs.string("type")
, rs.string("circle_center"), rs.int("circle_radius"), rs.string("state"),
rs.string("coordinates")))
.list()
.apply()
}
for (rule <- rules) {
sourceContext.collect(rule)
}
//线程睡眠
Thread.sleep(duration);
}
}
override def cancel(): Unit = {
isRunning = false
}
}
Keyed State
keyed state 接口提供不同类型状态的访问接口,这些状态都作用于当前输入数据的 key 下。换句话说,这些状态仅可在 KeyedStream 上使用,可以通过 stream.keyBy(...) 得到 KeyedStream.
所有支持的状态类型如下所示:
- ValueState: 保存一个可以更新和检索的值(如上所述,每个值都对应到当前的输入数据的 key,因此算子接收到的每个 key 都可能对应一个值)。 这个值可以通过 update(T) 进行更新,通过 T value() 进行检索。
- ListState: 保存一个元素的列表。可以往这个列表中追加数据,并在当前的列表上进行检索。可以通过 add(T) 或者 addAll(List) 进行添加元素,通过 Iterable get() 获得整个列表。还可以通过 update(List) 覆盖当前的列表。
- ReducingState: 保存一个单值,表示添加到状态的所有值的聚合。接口与 ListState 类似,但使用 add(T) 增加元素,会使用提供的 ReduceFunction 进行聚合。
- AggregatingState: 保留一个单值,表示添加到状态的所有值的聚合。和 ReducingState 相反的是, 聚合类型可能与 添加到状态的元素的类型不同。 接口与 ListState 类似,但使用 add(IN) 添加的元素会用指定的 AggregateFunction 进行聚合。
- MapState: 维护了一个映射列表。 你可以添加键值对到状态中,也可以获得反映当前所有映射的迭代器。使用 put(UK,UV) 或者 putAll(Map) 添加映射。 使用 get(UK) 检索特定 key。 使用 entries(),keys() 和 values() 分别检索映射、键和值的可迭代视图。你还可以通过 isEmpty() 来判断是否包含任何键值对。
所有类型的状态还有一个clear() 方法,清除当前 key 下的状态数据,也就是当前输入元素的 key。
请牢记,这些状态对象仅用于与状态交互。状态本身不一定存储在内存中,还可能在磁盘或其他位置。 另外需要牢记的是从状态中获取的值取决于输入元素所代表的 key。 因此,在不同 key 上调用同一个接口,可能得到不同的值。