Flink学习(二)

Window

Flink处理的流式数据是指一种不断增长的本质上无限的数据集,而 window 是一种切割无限数据为有限块进行处理的手段。Window 是无限数据流处理的核心,Window 将一个无限的 stream 拆分成有限大小的”buckets”桶,我们可以在这些桶上做计算操作。
切割拆分的时候有两种拆分方法,一种是按数据集大小拆分,即按数量拆分CountWindow。还有一种是按照时间划分区间TimeWindow。
对于 TimeWindow,可以根据窗口实现原理的不同分成三类:滚动窗口(Tumbling Window)、滑动窗口(Sliding Window)和会话窗口(Session Window)。

  • 滚动窗口:时间对齐,窗口长度固定,没有重叠。可以理解为一个正方体向前滚动,每一次移动都是一个正方形的大小,而且不会和上一个正方形有重复
  • 滑动窗口:时间对齐,窗口长度固定,可以有重叠。可以理解为一个正方体向前华东,是一直在移动。
  • 会话窗口:由一系列事件组合一个指定时间长度的 timeout 间隙组成,类似于 web 应用的
    session,也就是一段时间没有接收到新数据就会生成新的窗口。
CountWindow

CountWindow 根据窗口中相同 key 元素的数量来触发执行,执行时只计算元素数量达到窗口大小的 key 对应的结果。
默认的 CountWindow 是一个滚动窗口,只需要指定窗口大小即可,当元素数量
达到窗口大小时,就会触发窗口的执行。

val minTempPerWindow: DataStream[(String, Double)] = dataStream
.map(r => (r.id, r.temperature))
.keyBy(_._1)
.countWindow(5)
.reduce((r1, r2) => (r1._1, r1._2.max(r2._2)))

滑动窗口和滚动窗口的函数名是完全一致的,只是在传参数时需要传入两个参数,一个是 window_size(窗口大小),一个是 sliding_size(滑动步长)。

val keyedStream: KeyedStream[(String, Int), Tuple] = dataStream.map(r =>
(r.id, r.temperature)).keyBy(0)
//每当某一个 key 的个数达到 2 的时候,触发计算,计算最近该 key 最近 10 个元素的内容
val windowedStream: WindowedStream[(String, Int), Tuple, GlobalWindow] =
keyedStream.countWindow(10,2)
val sumDstream: DataStream[(String, Int)] = windowedStream.sum(1)

TimeWindow

Flink 默认的时间窗口根据 Processing Time 进行窗口的划分,将 Flink 获取到的数据根据进入 Flink 的时间划分到不同的窗口中。

val minTempPerWindow = dataStream
.map(r => (r.id, r.temperature))
.keyBy(_._1)
.timeWindow(Time.seconds(15))
.reduce((r1, r2) => (r1._1, r1._2.min(r2._2)))

滑动窗口和滚动窗口的函数名是完全一致的,只是在传参数时需要传入两个参数,一个是 window_size,一个是 sliding_size。

val minTempPerWindow: DataStream[(String, Double)] = dataStream
.map(r => (r.id, r.temperature))
.keyBy(_._1)
.timeWindow(Time.seconds(15), Time.seconds(5))
.reduce((r1, r2) => (r1._1, r1._2.min(r2._2)))
// .window(SlidingEventTimeWindows.of(Time.seconds(15),Time.seconds(5))

window function 定义了要对窗口中收集的数据做的计算操作,主要可以分为两类:

  • 增量聚合函数(incremental aggregation functions):每条数据到来就进行计算,保持一个简单的状态
  • 全窗口函数(full window functions):先把窗口所有数据收集起来,等到计算的时候会遍历所有数据。

时间语义

  • Event Time:是事件创建的时间。它通常由事件中的时间戳描述,例如采集的日志数据中,每一条日志都会记录自己的生成时间,Flink 通过时间戳分配器访问事件时间戳。
  • Ingestion Time:是数据进入 Flink 的时间。
  • Processing Time:是每一个执行基于时间操作的算子的本地系统时间,与机器 相关,默认的时间属性就是 Processing Time。
    在 Flink 的流式处理中,绝大部分的业务都会使用 eventTime
val env = StreamExecutionEnvironment.getExecutionEnvironment
// 从调用时刻开始给 env 创建的每一个 stream 追加时间特征
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)

Watermark

虽然数据产生的时候是按顺序产生的,但是在发送到服务端的过程中,可能因为网络延迟等各种原因导致乱序(以Event Time为标准)
一旦出现乱序,如果只根据 eventTime 决定 window 的运行,我们不能明确数据是否全部到位,但又不能无限期的等下去,此时必须要有个机制来保证一个特定的时间后,必须触发 window 去进行计算了,这个特别的机制,就是 Watermark。

  • Watermark 是一种衡量 Event Time 进展的机制。
  • Watermark 是用于处理乱序事件的,而正确的处理乱序事件,通常用Watermark 机制结合 window 来实现。
  • 数据流中的 Watermark 用于表示 timestamp 小于 Watermark 的数据,都已经到达了,因此,window 的执行也是由 Watermark 触发的。
  • Watermark 可以理解成一个延迟触发机制,我们可以设置 Watermark 的延时时长 t,每次系统会校验已经到达的数据中最大的 maxEventTime,然后认定 eventTime小于 maxEventTime - t 的所有数据都已经到达,如果有窗口的停止时间等于maxEventTime – t,那么这个窗口被触发执行。

当 Flink 接收到数据时,会按照一定的规则去生成 Watermark,这条 Watermark就等于当前所有到达数据中的 maxEventTime - 延迟时长,也就是说,Watermark 是基于数据携带的时间戳生成的,一旦 Watermark 比当前未触发的窗口的停止时间要晚,那么就会触发相应窗口的执行。由于 event time 是由数据携带的,因此,如果运行过程中无法获取新的数据,那么没有被触发的窗口将永远都不被触发。
Watermark 就是触发前一窗口的“关窗时间”,一旦触发关门那么以当前时刻为准在窗口范围内的所有所有数据都会收入窗中。只要没有达到Watermark那么不管现实中的时间推进了多久都不会触发关窗。
在代码中,如果确定数据是有序的,可以使用assignAscendingTimestamps,直接用数据时间生成Watermark。

val orderEventStream = env.readTextFile("D:\\编程\\BigData\\UserBehaviorAnalySis\\OrderTimeoutDetect\\src\\main\\resources\\OrderLog.csv")
      .map( data => {
        val dataArray = data.split(",")
        TxOrderEvent(dataArray(0).toLong, dataArray(1), dataArray(2), dataArray(3).toLong)
      }).filter(_.txId != null)
      .assignAscendingTimestamps(_.eventTime * 1000L)
      .keyBy(_.txId)

如果而对于乱序数据流,如果我们能大致估算出数据流中的事件的最大延迟时间,就可以使用如下代码:

val stream: DataStream[SensorReading] = ...
val withTimestampsAndWatermarks = stream.assignTimestampsAndWatermarks(
new SensorTimeAssigner
)
class SensorTimeAssigner extends
BoundedOutOfOrdernessTimestampExtractor[SensorReading](Time.seconds(5)) {
// 抽取时间戳
override def extractTimestamp(r: SensorReading): Long = r.timestamp
}

ProcessFunction API(底层 API)

DataStream API 提供了一系列的 Low-Level 转换算子。可以访问时间戳、watermark 以及注册定时事件。还可以输出特定的一些事件,例如超时事件等。Flink 提供了 8 个 Process Function:

  • ProcessFunction
  • KeyedProcessFunction
  • CoProcessFunction
  • ProcessJoinFunction
  • BroadcastProcessFunction
  • KeyedBroadcastProcessFunction
  • ProcessWindowFunction
  • ProcessAllWindowFunction
KeyedProcessFunction

KeyedProcessFunction 用来操作 KeyedStream。KeyedProcessFunction 会处理流的每一个元素,输出为 0 个、1 个或者多个元素。所有的 Process Function 都继承自RichFunction 接口,所以都有 open()、close()和 getRuntimeContext()等方法。而KeyedProcessFunction[KEY, IN, OUT]还额外提供了两个方法:

  • processElement(v: IN, ctx: Context, out: Collector[OUT]), 流中的每一个元素都会调用这个方法,调用结果将会放在 Collector 数据类型中输出。Context可以访问元素的时间戳,元素的 key,以及 TimerService 时间服务。Context还可以将结果输出到别的流(side outputs)。
  • onTimer(timestamp: Long, ctx: OnTimerContext, out: Collector[OUT])是一个回调函数。当之前注册的定时器触发时调用。参数 timestamp 为定时器所设定的触发的时间戳。Collector 为输出结果的集合。OnTimerContext 和processElement 的 Context 参数一样,提供了上下文的一些信息,例如定时器触发的时间信息(事件时间或者处理时间)
TimerService 和 定时器(Timers)

Context 和 OnTimerContext 所持有的 TimerService 对象拥有以下方法:

  • currentProcessingTime(): Long 返回当前处理时间
  • currentWatermark(): Long 返回当前 watermark 的时间戳
  • registerProcessingTimeTimer(timestamp: Long): Unit 会注册当前 key 的processing time 的定时器。当 processing time 到达定时时间时,触发 timer。
  • registerEventTimeTimer(timestamp: Long): Unit 会注册当前 key 的 event time 定时器。当水位线大于等于定时器注册的时间时,触发定时器执行回调函数。
  • deleteProcessingTimeTimer(timestamp: Long): Unit 删除之前注册处理时间定时器。如果没有这个时间戳的定时器,则不执行
  • deleteEventTimeTimer(timestamp: Long): Unit 删除之前注册的事件时间定时
    器,如果没有此时间戳的定时器,则不执行。

当定时器 timer 触发时,会执行回调函数 onTimer()。注意定时器 timer 只能在
keyed streams 上面使用。

侧输出流(SideOutput)

大部分的 DataStream API 的算子的输出是单一输出,也就是某种数据类型的流。除了 split 算子,可以将一条流分成多条流,这些流的数据类型也都相同。processfunction 的 side outputs 功能可以产生多条流,并且这些流的数据类型可以不一样。一个 side output 可以定义为 OutputTag[X]对象,X 是输出流的数据类型。process function 可以通过 Context 对象发射一个事件到一个或者多个 side outputs。

val unmatchedPays = new OutputTag[TxOrderEvent]("unmatchedPays")
override def onTimer(timestamp: Long, ctx: CoProcessFunction[TxOrderEvent, ReceiptEvent, OrderResult]#OnTimerContext, out: Collector[OrderResult]): Unit = {
    if (payState.value() != null) {
      ctx.output(out1,payState.value())
      payState.clear()
    }
}
    processedStream.getSideOutput(unmatchedPays).print("没有支付")
CoProcessFunction

对于两条输入流,DataStream API 提供了 CoProcessFunction 这样的 low-level操作。CoProcessFunction 提供了操作每一个输入流的方法: processElement1()和processElement2()。
类似于 ProcessFunction,这两种方法都通过 Context 对象来调用。这个 Context对象可以访问事件数据,定时器时间戳,TimerService,以及 side outputs。CoProcessFunction 也提供了 onTimer()回调函数。

class TxMatchDetection(out1: OutputTag[TxOrderEvent], out2: OutputTag[ReceiptEvent]) extends CoProcessFunction[TxOrderEvent, ReceiptEvent, OrderResult] {
  lazy val payState: ValueState[TxOrderEvent] = getRuntimeContext.getState(new ValueStateDescriptor[TxOrderEvent]("payState",classOf[TxOrderEvent]))
  lazy val receiptState: ValueState[ReceiptEvent] = getRuntimeContext.getState(new ValueStateDescriptor[ReceiptEvent]("receiptState",classOf[ReceiptEvent]))

  override def processElement1(in1: TxOrderEvent, context: CoProcessFunction[TxOrderEvent, ReceiptEvent, OrderResult]#Context, collector: Collector[OrderResult]): Unit = {
    val receiptEvent = receiptState.value()
    if (receiptEvent != null) {
      receiptState.clear()
      collector.collect(OrderResult(in1.orderId,receiptEvent.payChannel))
    }else {
      payState.update(in1)
      context.timerService().registerEventTimeTimer(in1.eventTime * 1000L)
    }
  }

  override def processElement2(in2: ReceiptEvent, context: CoProcessFunction[TxOrderEvent, ReceiptEvent, OrderResult]#Context, collector: Collector[OrderResult]): Unit = {
    val pay = payState.value()
    if (pay != null) {
      payState.clear()
      collector.collect(OrderResult(pay.orderId,in2.payChannel))
    }else {
      receiptState.update(in2)
      context.timerService().registerEventTimeTimer(in2.eventTime * 1000L)
    }
  }

Flink状态

ValueState[T]保存单个的值,值的类型为 T。
  • get 操作: ValueState.value()
  • set 操作: ValueState.update(value: T)
ListState[T]保存一个列表,列表里的元素的数据类型为 T。基本操作如下:
  • ListState.add(value: T)
  • ListState.addAll(values: java.util.List[T])
  • ListState.get()返回 Iterable[T]
  • ListState.update(values: java.util.List[T])
MapState[K, V]保存 Key-Value 对。
  • MapState.get(key: K)
  • MapState.put(key: K, value: V)
  • MapState.contains(key: K)
  • MapState.remove(key: K)

ReducingState[T]和AggregatingState[I, O]用的不多,不过多介绍

//ValueState
lazy val payState: ValueState[TxOrderEvent] = getRuntimeContext.getState(new ValueStateDescriptor[TxOrderEvent]("payState",classOf[TxOrderEvent]))
//ListState
lazy val loginCount:ListState[LoginEvent] = getRuntimeContext.getListState(new ListStateDescriptor[LoginEvent]("loginCount",classOf[LoginEvent]))

容错机制

一致性级别:

  1. at-most-once: 这其实是没有正确性保障的委婉说法——故障发生之后,计数结果可能丢失。同样的还有 udp。
  2. at-least-once: 这表示计数结果可能大于正确值,但绝不会小于正确值。也就是说,计数程序在发生故障后可能多算,但是绝不会少算。
  3. exactly-once: 这指的是系统保证在发生故障后得到的计数结果与正确值一 致。

Flink 的一个重大价值在于,它既保证了 exactly-once,也具有低延迟和高吞吐的处理能力。
flink采取的方式是Watermark和状态。将Watermark加入到数据流后,Source遇到Watermark时会使用状态保存自己当前的值记作检查点,Transform和Sink不需要停留等待。在保存到状态后,Source继续处理流数据,Watermark随着数据流向Transform,在Transform处理到Watermark时,也是将当前的值保存到状态中,source和sink不受影响继续工作,Watermark到sink时也一样。如果遇到错误,就可以恢复到上一个检查点,从新处理一次数据。

CEP

CEP就是一个或多个由简单事件构成的事件流通过一定的规则匹配,然后输出用户想得到的数据,满足规则的复杂事件。
CEP 用于分析低延迟、频繁产生的不同来源的事件流。CEP 可以帮助在复杂的、不相关的事件流中找出有意义的模式和复杂的关系,以接近实时或准实时的获得通知并阻止一些行为。
CEP 支持在流上进行模式匹配,根据模式的条件不同,分为连续的条件或不连续的条件;模式的条件允许有时间的限制,当在条件范围内没有达到满足的条件时,会导致模式匹配超时。
使用Cep时往往分为三步:

  1. Pattern API:每个 Pattern 都应该包含几个步骤,或者叫做 state。从一个 state 到另一个 state,通常我们需要定义一些条件如where,subtype,or等等,每个state都应该有个名字,代表这一段处理完的数据如begin,next,followedBy
  2. Pattern 检测:通过一个 input DataStream 以及刚刚我们定义的 Pattern,我们可以创建一个PatternStream,一旦获得 PatternStream,我们就可以通过 select 或 flatSelect,从一个 Map 序列找到我们需要的信息。
  3. select:select 方法需要实现一个 PatternSelectFunction,通过 select 方法来输出需要的数据。它接受一个 Map 对,包含 string/event,其中 key 为 state 的名字,event 则为真实的 Event。
val loginPattern = Pattern.begin[LoginEvent]("begin").where(_.eventType=="fail")
      .next("next").where(_.eventType=="fail").within(Time.seconds(2))
val cepStream = CEP.pattern(input,loginPattern).select(new cepSelectFunction).print()

class cepSelectFunction extends PatternSelectFunction[LoginEvent,String] {
  override def select(map: util.Map[String, util.List[LoginEvent]]): String = {
    val firstLogin = map.get("begin").get(0)
    val nextLogin = map.get("next").iterator().next()
    firstLogin.userId+"在"+firstLogin.eventTime+"-"+nextLogin.eventTime+"可能恶意登录!"
  }
}

讲的比较乱,比较杂,适合之前学过但是很长时间没使用过的去复习一下,我也是防止自己忘了才写的。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值