大数据技术AI
Flink/Spark/Hadoop/数仓,数据分析、面试,源码解读等干货学习资料
118篇原创内容
公众号
当使用细粒度的滑动窗口(窗口长度远远大于滑动步长)时,重叠的窗口过多,一个数据会属于多个窗口,性能会急剧下降。
以1分钟的频率实时计算App内各个子模块近24小时的PV和UV。
我们需要用粒度为1440 / 1 = 1440的滑动窗口来实现它,但是细粒度的滑动窗口会带来性能问题,有两点:
对于一个元素,会将其写入对应的(key, window)二元组所圈定的windowState状态中。如果粒度为1440,那么每个元素到来,更新windowState时都要遍历1440个窗口并写入,开销是非常大的。在采用RocksDB作为状态后端时,checkpoint的瓶颈也尤其明显。
每一个(key, window)二元组都需要注册两个定时器:一是触发器注册的定时器,用于决定窗口数据何时输出;二是registerCleanupTimer()方法注册的清理定时器,用于在窗口彻底过期(如allowedLateness过期)之后及时清理掉窗口的内部状态。
细粒度滑动窗口会造成维护的定时器增多,内存负担加重。
DataStreamAPI中,自己解决(https://issues.apache.org/jira/browse/FLINK-7001)
我们一般使用滚动窗口+在线存储+读时聚合的思路作为解决方案:
(1)从业务的视角来看,往往窗口的长度是可以被步长所整除的,可以找到窗口长度和窗口步长的最小公约数作为时间分片(一个滚动窗口的长度);
(2)每个滚动窗口将其周期内的数据做聚合,存到下游状态或打入外部在线存储(内存数据库如Redis,LSM-based NoSQL存储如HBase);
(3)扫描在线存储中对应时间区间(可以灵活指定)的所有行,并将计算结果返回给前端展示。
Flink 1.13对SQL模块的 Window TVF 进行了一系列的性能优化,可以自动对滑动窗口进行切片解决细粒度滑动问题。
https://nightlies.apache.org/flink/flink-docs-release-1.13/docs/dev/table/sql/queries/window-tvf/
// 2.0 过滤页面数据
.filter(x =>
StringUtils.isNullOrWhitespaceOnly(x.getString("start"))
)
.keyBy(_.getJSONObject("common").getString("mid"))
// 3.0 按照mid分组,过滤掉不是今天第一次访问的数据
.filter(new FilterFirstMidFunc)
package com.duo.app2.func
import com.alibaba.fastjson.JSONObject
import org.apache.flink.api.common.functions.RichFilterFunction
import org.apache.flink.api.common.state.{StateTtlConfig, ValueState, ValueStateDescriptor}
import org.apache.flink.configuration.Configuration
import java.text.SimpleDateFormat
class FilterFirstMidFunc extends RichFilterFunction[JSONObject]{
private var firstVisitDateState: ValueState[String] = null
private var simpleDateFormat: SimpleDateFormat = null
override def open(parameters: Configuration): Unit = {
simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd")
val firstVisitDesc = new ValueStateDescriptor[String](
"firstVisitDateState",
classOf[String]
)
// 设置状态的过期时间ttl
val ttlConfig = StateTtlConfig
.newBuilder(org.apache.flink.api.common.time.Time.days(1))
.setUpdateType(StateTtlConfig.UpdateType.OnCreateAndWrite)
.build()
firstVisitDesc.enableTimeToLive(ttlConfig)
firstVisitDateState = getRuntimeContext.getState(firstVisitDesc)
}
override def filter(in: JSONObject) = {
val lastPageId = in.getJSONObject("page").getString("last_page_id")
//判断是否存在上一个页面
if (lastPageId == null || lastPageId.length <= 0) {
val firstVisitDate = firstVisitDateState.value()
val ts = in.getLong("ts")
val curDate = simpleDateFormat.format(ts)
// 如果状态数据=null,或者 状态中时间!=当前时间,说明是第一次访问
if (firstVisitDate == null || !firstVisitDate.equals(curDate)) {
firstVisitDateState.update(curDate)
true
} else {
false
}
} else {
false
}
}
}
uvDS.map(x => 1L)
.windowAll(SlidingProcessingTimeWindows.of(Time.hours(1), Time.minutes(1)))
.reduce(
_ + _,
new ProcessAllWindowFunction[Long, (String, String, Long), TimeWindow] {
private val format = new SimpleDateFormat("yyyy-MM-dd HH:mm:SS")
override def process(context: Context, elements: Iterable[Long], out: Collector[(String, String, Long)]): Unit = {
val uvCount = elements.iterator.next()
val startDate = format.format(context.window.getStart)
val endDate = format.format(context.window.getEnd)
out.collect((startDate, endDate, uvCount))
}
})
错误代码:
type mismatch;
found : org.apache.flink.streaming.api.windowing.assigners.SlidingProcessingTimeWindows
required: org.apache.flink.streaming.api.windowing.assigners.WindowAssigner[_ >: Long, ?]
Note: Object <: Any (and org.apache.flink.streaming.api.windowing.assigners.SlidingProcessingTimeWindows <: org.apache.flink.streaming.api.windowing.assigners.WindowAssigner[Object,org.apache.flink.streaming.api.windowing.windows.TimeWindow]), but Java-defined class WindowAssigner is invariant in type T.
You may wish to investigate a wildcard type such as `_ <: Any`. (SLS 3.2.10)
.windowAll(SlidingProcessingTimeWindows.of(Time.hours(1), Time.minutes(1)))
猜一下窗口不能识别map(x => 1L)中1L的类型
uvDS.map(x => {
UV(1L)
})
.windowAll(SlidingProcessingTimeWindows.of(Time.hours(1), Time.minutes(1)))
.reduce(
(x: UV, y: UV) => UV(x.uvCount + y.uvCount),
new UVReduceFunc
)
.print()
case class UV(uvCount: Long)
打印结果:
(2022-05-24 19:00:00,2022-05-24 20:00:00,13784)
(2022-05-24 19:01:00,2022-05-24 20:01:00,145162)
val resultDS = uvDS.map(
x => {
UV(1L)
}
)
.windowAll(TumblingProcessingTimeWindows.of(Time.seconds(60)))
.reduce(
(x: UV, y: UV) => UV(x.uvCount + y.uvCount),
new UVReduceFunc
)
.keyBy(_._1)
.process(new SplitWindowAggFunc)
resultDS.print()
env.execute()
package com.duo.app2.func
import com.duo.app2.source.UV
import org.apache.flink.streaming.api.scala.function.ProcessAllWindowFunction
import org.apache.flink.streaming.api.windowing.windows.TimeWindow
import org.apache.flink.util.Collector
import java.text.SimpleDateFormat
class UVReduceFunc extends ProcessAllWindowFunction[UV, (String, String, Long), TimeWindow]{
private val format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
override def process(context: Context, elements: Iterable[UV], out: Collector[(String, String, Long)]): Unit = {
val uvCount = elements.iterator.next().uvCount
val startDate = format.format(context.window.getStart)
val endDate = format.format(context.window.getEnd)
out.collect((startDate, endDate, uvCount))
}
}
package com.duo.app2.func
import com.duo.app2.source.UVResult
import org.apache.flink.api.common.state.{ValueState, ValueStateDescriptor}
import org.apache.flink.configuration.Configuration
import org.apache.flink.streaming.api.functions.KeyedProcessFunction
import org.apache.flink.util.Collector
import scala.collection.mutable.ListBuffer
class SplitWindowAggFunc extends KeyedProcessFunction[String, (String, String, Long), UVResult]{
// 返回结果
val resultAgg: UVResult = UVResult(null, null, 0L)
// 存储分片数据的状态
var listState: ValueState[ListBuffer[(String, String, Long)]] = null
// 每次计算的临时变量
var list: ListBuffer[(String, String, Long)] = new ListBuffer[(String, String, Long)]()
// 分片数
var splitNum: Int = 0
override def open(parameters: Configuration): Unit = {
listState = getRuntimeContext
.getState(
new ValueStateDescriptor[ListBuffer[(String, String, Long)]](
"listState",
classOf[ListBuffer[(String, String, Long)]]
))
list = ListBuffer.empty
// 1小时的窗口,1分钟的滑动 ===》 分片数=60分钟/1分钟
splitNum = 60
}
override def processElement(value: (String, String, Long),
ctx: KeyedProcessFunction[String, (String, String, Long), UVResult]#Context,
out: Collector[UVResult]) = {
// 清空结果
resultAgg.uvCount = 0L
if (listState.value() != null) {
// 从状态中拿到分片数据
list ++= listState.value()
}
// 将新的时间分片数据添加到list
list += value
// 一小时之前,集合是不满60的
if (list.size >= splitNum) {
// 如果集合+当前数据=61,则删除第一个
if (list.size == (splitNum + 1)) {
list.remove(0)
}
}
// 遍历集合中分片数据
for (elem <- list) {
// 如果是第一个元素,拿到第一个元素的开始时间,即:窗口的开始时间
if (list.head == elem) {
resultAgg.start = elem._1
}
// 如果是最后一个元素,拿到最后一个元素的结束时间,即:窗口的结束时间
if (list.last == elem) {
resultAgg.end = elem._2
}
// 分片数据相加
resultAgg.uvCount += elem._3
}
listState.clear()
// 将分片数据存入状态中
listState.update(list)
out.collect(resultAgg)
}
}
case class UVResult(var start: String, var end: String, var uvCount: Long)
相关变量和状态
resultAgg:返回结果
listState:存储分片数据的状态
list:每次计算的临时变量
splitNum:分片数(1小时的窗口,1分钟的滑动,即:分片数=60分钟/1分钟)
初始化
初始化状态
初始化分片数
初始化list集合
计算逻辑
如果是第一个元素,拿到第一个元素的开始时间,即:窗口的开始时间
如果是最后一个元素,拿到最后一个元素的结束时间,即:窗口的结束时间
分片数据相加
清空结果
从状态中拿到分片数据
将新的时间分片数据添加到list
如果集合+当前数据=61,则删除第一个
遍历集合中分片数据
将分片数据存入状态中