• Flink / Scala - 使用 CountWindow 实现按条数触发窗口


    一.引言

    CountWindow 数量窗口分为滑动窗口与滚动窗口,类似于之前 TimeWindow 的滚动时间与滑动时间,这里滚动窗口不存在元素重复而滑动窗口存在元素重复的情况,下面 demo 场景为非重复场景,所以将采用滚动窗口。

    二.CountWindow 简介

    这里最关键的一句话是: A Window that represents a count window. For each count window, we will assign a unique id. Thus this CountWindow can act as namespace part in state. We can attach data to each different CountWindow. 翻译的意思是:表示计数窗口的窗口。对于每个计数窗口,我们将分配一个唯一的 id。因此,此计数窗口可以作为状态中的命名空间部分。我们可以将数据附加到每个不同的 CountWindow。

    countWindow 共分为两种初始化方式,其中只添加 size 为滚动窗口,即不重复元素的 CountWindow,带 slide 滑动参数则生成滑动窗口,不在本文讨论范围之内。上面提到了对于每个计数的窗口,我们分配一个唯一的 id,这个 id 可以近似看做是以 keyBy 为依据进行数据分流,下面示例将给出解答。 

    三.任务场景与实现 👍

    通过自定义 Source 实现无限数据流,我们希望指定 count = N,使得每 N 个数据作为一个 batch 触发一次窗口并计算。

    1.自定义 Source

      case class info(num: Int, id: Int)
    

    自定义 Source 需要继承 RichSourceFunction,这里我们生成 info 类数据结构,其内部包含两个元素 num 与 id,num 代表其内部的数字信息,id 代表其 keyBy 的分组信息:

    1. class SourceFromCollection extends RichSourceFunction[info] {
    2. private var isRunning = true
    3. var start = 0
    4. override def run(ctx: SourceFunction.SourceContext[info]): Unit = {
    5. val random = new Random() // 生成随机 id
    6. while (isRunning) {
    7. (start until (start + 100)).foreach(num => {
    8. ctx.collect(info(num, random.nextInt(4))) // 随机id范围 0,1,2,3
    9. if (num % 20 == 0) { // 每生产20个数据 sleep 1s
    10. TimeUnit.SECONDS.sleep(1)
    11. }
    12. })
    13. start += 100 // 数值累加
    14. }
    15. }
    16. override def cancel(): Unit = {
    17. isRunning = false
    18. }
    19. }

    Source 函数每 s 生成 20 个数字和其对应的 randomId,最终输出 info(num, id) 类。

    2.执行流程

    主函数主要基于 Source 实现 ProcessWindowFunction,并制定 count=20,每20个元素触发一次窗口计算:

    1. val env = StreamExecutionEnvironment.getExecutionEnvironment
    2. val dataStream = env.addSource(new SourceFromCollection) // 添加 Source
    3. dataStream
    4. .keyBy(info => info.id) // 按 random 生成的 id 分配到不同 CountWindow
    5. .countWindow(20) // size = 20 的滚动窗口
    6. .process(new ProcessWindowFunction[info, String, Int, GlobalWindow] {
    7. override def process(key: Int, context: Context, elements: Iterable[info], out: Collector[String]): Unit = {
    8. // 输出 id + size + 元素
    9. val log = key + "\t" + elements.size + "\t" + elements.toArray.map(_.num).mkString("|")
    10. out.collect(log)
    11. }
    12. }).print()
    13. env.execute()

    其中 ProcessWindowFunction 共有四个参数:

    IN: 输入类型,本例数据流为 DataStream[info],所以 classOf[In] 为 info

    OUT: 输出类型,与 Collector 后类型一致,这里输出类型为 String

    KEY: Key 即为 keyBy 的 id,这里 key 为 random.nextInt,所以为 int 类型

    W: org.apache.flink.streaming.api.windowing.windows.window,这里主要分为两类,如果采用时间窗口即 TimeWindow,则对应类型为 TimeWindow,本例中采用 CountWindow,则对应类型为 GlobalWindow。

    3.运行结果

    批处理每一批 size = 20,分别输出 id + "\t" + size + "\t" + 批次数据,可以看到每一批触发20条数据,且 id 分别为 0,1,2,3,这里还是根据 process 的并行度来定,如果 process 的并行度设定为2,则很大概率 0,1,2,3 均分至两台 TaskManager 上,如果规定并行度为4,则分别分配到单台 TaskManager 上,也可以根据数据的吞吐,修改并行度与 keyBy 时 nextInt 的范围。

    四.CountTrigger

    1.自定义 CountTrigger

    之前提到过 CountTrigger 也可以实现按 count 数目进行窗口触发,但是有一点不同是 CountTrigger 不会清除窗口内元素,所以多次执行逻辑会重复处理一批数据,具体实现逻辑解析可以参考: Flink - CountTrigger && ProcessingTimeTriger 详解

    1. class SelfDefinedCountTrigger(maxCount: Long) extends Trigger[String, TimeWindow] {
    2. // 条数计数器
    3. val countStateDesc = new ReducingStateDescriptor[Long]("count", new ReduceSum(), classOf[Long])
    4. // 元素过来后执行的操作
    5. override def onElement(t: String, l: Long, w: TimeWindow, triggerContext: Trigger.TriggerContext): TriggerResult = {
    6. // 获取 count state 并累加数量
    7. val count = triggerContext.getPartitionedState(countStateDesc)
    8. count.add(1L)
    9. // 满足数量触发要求
    10. if (count.get() >= maxCount) {
    11. // 首先清空计数器
    12. count.clear()
    13. val dateFormat = new SimpleDateFormat("yyyy-MM-dd:HH-mm-ss")
    14. val cla = Calendar.getInstance()
    15. cla.setTimeInMillis(System.currentTimeMillis())
    16. val date = dateFormat.format(cla.getTime)
    17. println(s"[$date] Window Trigger By Count = ${maxCount}")
    18. TriggerResult.FIRE
    19. } else {
    20. TriggerResult.CONTINUE
    21. }
    22. }
    23. override def onProcessingTime(l: Long, w: TimeWindow, triggerContext: Trigger.TriggerContext): TriggerResult = {
    24. TriggerResult.CONTINUE
    25. }
    26. override def onEventTime(l: Long, w: TimeWindow, triggerContext: Trigger.TriggerContext): TriggerResult = {
    27. TriggerResult.CONTINUE
    28. }
    29. override def clear(w: TimeWindow, triggerContext: Trigger.TriggerContext): Unit = {
    30. val count = triggerContext.getPartitionedState(countStateDesc)
    31. count.clear()
    32. }
    33. }

    2.运行结果

    之前介绍 Window 触发 CountTrigger 时也实现了基于 Count 的窗口触发机制,但是存在一个问题,CountTrigger 每次达到 count 数量触发,但是不会清除窗口数据,即窗口数据累加同时多次触发窗口:

    如上,窗口逻辑为计算批次数据的最大最小值,同一个颜色框内为 count=30 多次触发的结果,可以看到 min 一直为同一个数字,max 持续增大,这就是上面提到的问题: 使用 countTrigger 时会造成窗口数据重复触发,所以想要实现无重复 CountWindow 就得最上面的 countWindow 实现。当然上面的执行逻辑也并不是没有使用场景,例如电商平台统计一段时间内商品销售的情况就可以使用 CountTrigger,实时滚动大屏数据展示。

    五.CountWindow 完整代码

    下面附上完整代码:

    1. package com.weibo.ug.push.flink.Main
    2. import org.apache.flink.api.scala.createTypeInformation
    3. import org.apache.flink.streaming.api.functions.source.{RichSourceFunction, SourceFunction}
    4. import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
    5. import org.apache.flink.streaming.api.scala.function.ProcessWindowFunction
    6. import org.apache.flink.streaming.api.windowing.windows.GlobalWindow
    7. import org.apache.flink.util.Collector
    8. import java.util.concurrent.TimeUnit
    9. import com.weibo.ug.push.flink.Main.TestCountWindow.info
    10. import scala.util.Random
    11. object TestCountWindow {
    12. case class info(num: Int, id: Int)
    13. def main(args: Array[String]): Unit = {
    14. val env = StreamExecutionEnvironment.getExecutionEnvironment
    15. val dataStream = env.addSource(new SourceFromCollection)
    16. dataStream
    17. .keyBy(x => x.id)
    18. .countWindow(20)
    19. .process(new ProcessWindowFunction[info, String, Int, GlobalWindow] {
    20. override def process(key: Int, context: Context, elements: Iterable[info], out: Collector[String]): Unit = {
    21. val log = key + "\t" + elements.size + "\t" + elements.toArray.map(_.num).mkString("|")
    22. out.collect(log)
    23. }
    24. }).print()
    25. env.execute()
    26. }
    27. }
    28. class SourceFromCollection extends RichSourceFunction[info] {
    29. private var isRunning = true
    30. var start = 0
    31. override def run(ctx: SourceFunction.SourceContext[info]): Unit = {
    32. val random = new Random()
    33. while (isRunning) {
    34. (start until (start + 100)).foreach(num => {
    35. ctx.collect(info(num, random.nextInt(4)))
    36. if (num % 20 == 0) {
    37. TimeUnit.SECONDS.sleep(1)
    38. }
    39. })
    40. start += 100
    41. }
    42. }
    43. override def cancel(): Unit = {
    44. isRunning = false
    45. }
    46. }
  • 相关阅读:
    什么是MQ
    封装class类一次性解决全屏问题
    指定牛导|肿瘤专业医生芝加哥大学博士后实现夙愿
    代理IP与Socks5代理在跨界电商、爬虫、游戏和网络安全中的应用
    Android MediaCodec message机制分析
    Spark入门
    java毕业生设计疫情防控医用品管理计算机源码+系统+mysql+调试部署+lw
    伦敦银行情中短线的支撑和阻力位
    如何阅读论文、文献的搜索与免费下载的一件套总结
    新移动时代内容制作呈现与开发利器-Zoomla!逐浪CMS v8.6.2发布
  • 原文地址:https://blog.csdn.net/BIT_666/article/details/126130054