flink自定義窗口分配器 周、月

關於分配器介紹內容來自官網

窗口分配的概念

窗口分配程序(Window Assigners)定義如何將元素分配給窗口。
通過window(...) (for keyed streams)windowAll()for non-keyed streams)指定需要的WindowAssigner。

WindowAssigner負責將每個傳入元素分配給一個或多個窗口。

Flink爲最常見的用例提供了預定義的窗口分配程序如:tumbling windows, sliding windows, session windows and global windows.

同時還可以通過擴展WindowAssigner類來實現自定義窗口assigner

所有內置的窗口分配程序(除了global windows)都根據時間將元素分配給窗口,時間可以是處理時間,也可以是事件時間。

基於時間的窗口有一個開始時間戳(包括)和一個結束時間戳(不包括),它們一起描述窗口的大小。[starTimestamp,endTimestamp):左閉右開

Flink預定義的窗口分配程序

Tumbling Windows (滾動窗口)

翻轉窗口分配程序將每個元素分配給指定窗口大小的窗口,滾動窗口有一個固定的大小並且元素之間不重疊。
Tumbling Windows.png

val input: DataStream[T] = ...

// tumbling event-time windows
input
    .keyBy(<key selector>)
    .window(TumblingEventTimeWindows.of(Time.seconds(5)))
    .<windowed transformation>(<window function>)

// tumbling processing-time windows
input
    .keyBy(<key selector>)
    .window(TumblingProcessingTimeWindows.of(Time.seconds(5)))
    .<windowed transformation>(<window function>)

//偏移量的一個重要用例是將窗口調整到UTC-0以外的時區。例如,在中國,您必須指定時間偏移量(-8)。
//事件時間窗口偏移-8小時
input
    .keyBy(<key selector>)
    .window(TumblingEventTimeWindows.of(Time.days(1), Time.hours(-8)))
    .<windowed transformation>(<window function>)

Sliding Windows (滑動窗口)

滑動窗口分配程序將元素分配給固定長度的窗口。類似於滾動窗口分配程序,窗口的大小由窗口大小參數配置。
同時還有一個附加的窗口滑動距離參數控制滑動窗口啓動的頻率。

滑動距離與窗口大小的的不同會導致數據元素是否重疊、丟失、恰好一次

具體情況如下:

  1. slideSize>windowSize 丟失數據
  2. slideSize<windowSize 數據重疊
  3. slideSize=windowSize 恰好一次(此時等同TumblingWindows)

Sliding Windows.png

val input: DataStream[T] = ...

// sliding event-time windows
input
    .keyBy(<key selector>)
    .window(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5)))
    .<windowed transformation>(<window function>)

// sliding processing-time windows
input
    .keyBy(<key selector>)
    .window(SlidingProcessingTimeWindows.of(Time.seconds(10), Time.seconds(5)))
    .<windowed transformation>(<window function>)

// sliding processing-time windows offset by -8 hours
input
    .keyBy(<key selector>)
    .window(SlidingProcessingTimeWindows.of(Time.hours(12), Time.hours(1), Time.hours(-8)))
    .<windowed transformation>(<window function>)

Session Windows(會話窗口)

會話窗口分配程序根據活動的會話對元素進行分組。
與滾動、滑動窗口不同的是,會話窗口沒有數據重疊,也沒有固定的開始和結束時間。
當某個會話窗口在一段時間內沒有接收到元素時,它將關閉窗口

Session Windows.png

val input: DataStream[T] = ...

// event-time session windows with static gap
input
    .keyBy(<key selector>)
    .window(EventTimeSessionWindows.withGap(Time.minutes(10)))
    .<windowed transformation>(<window function>)

// event-time session windows with dynamic gap
input
    .keyBy(<key selector>)
    .window(EventTimeSessionWindows.withDynamicGap(new SessionWindowTimeGapExtractor[String] {
      override def extract(element: String): Long = {
        // determine and return session gap
      }
    }))
    .<windowed transformation>(<window function>)

// processing-time session windows with static gap
input
    .keyBy(<key selector>)
    .window(ProcessingTimeSessionWindows.withGap(Time.minutes(10)))
    .<windowed transformation>(<window function>)


// processing-time session windows with dynamic gap
input
    .keyBy(<key selector>)
    .window(DynamicProcessingTimeSessionWindows.withDynamicGap(new SessionWindowTimeGapExtractor[String] {
      override def extract(element: String): Long = {
        // determine and return session gap
      }
    }))
    .<windowed transformation>(<window function>)

Global Windows(全局窗口)

全局窗口分配程序將具有相同鍵的所有元素分配給同一個全局窗口。

全局窗口模式僅在指定自定義觸發器時纔有用。否則,將不執行任何計算,因爲全局窗口沒有一個可以處理聚合元素的自然末端。(窗口沒有結束,沒有出發計算的條件,除非自定義觸發器)
Global Windows.png

val input: DataStream[T] = ...

input
    .keyBy(<key selector>)
    .window(GlobalWindows.create())
    .<windowed transformation>(<window function>)

上面就是flink提供一些窗口分配程序,基本能滿足大多數情況。

但是對於某些特殊情況flink提供的分配成程序沒法滿足要求,此時就需要根據需求自定義分配程序。

實現自定的分配程序需要實現org.apache.flink.streaming.api.windowing.assigners.WindowAssigner


自定義 WindowAssigner

如果我們定義按天、小時、分鐘的滾動窗口都很容易實現。

但是如果我們要定義一週(週日開始或週一),一個月(1號開始)的滾動窗口,那麼現有API基本沒法實現或很難實現。

對此就需要我們實現一個自定義的窗口分配器。

import java.text.SimpleDateFormat
import java.util
import java.util.{Calendar, Collections, Date}

import com.meda.utils.DateHelper
import org.apache.flink.api.common.ExecutionConfig
import org.apache.flink.api.common.typeutils.TypeSerializer
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment
import org.apache.flink.streaming.api.windowing.assigners.WindowAssigner
import org.apache.flink.streaming.api.windowing.triggers.{EventTimeTrigger, Trigger}
import org.apache.flink.streaming.api.windowing.windows.TimeWindow

/**
 * 實現根據周或月的窗口劃分窗口
 * 比如按照週日的00:00:00到下一個週六23:59:59
 * 或者每個月第一天的00:00:00到最後一天的23:59:59
 * 實現參考了 TumblingEventTimeWindows
 *
 * @param tag 標籤 month  or  week
 * @tparam T 需要劃分窗口的數據類型(輸入類型)
 */
class CustomWindowAssigner[T](tag: String) extends WindowAssigner[T, TimeWindow] {
  //窗口分配的主要方法,需要爲每一個元素指定所屬的分區
  override def assignWindows(element: T, timestamp: Long, context: WindowAssigner.WindowAssignerContext): util.Collection[TimeWindow] = {
    var offset: (Long, Long) = null
    tag match {
      case "month" => offset = getTimestampFromMon(timestamp)
      case "week" => offset = getTimestampFromWeek(timestamp)
    }
    //分配窗口
    Collections.singletonList(new TimeWindow(offset._1, offset._2))
  }

  //注意此處需要進行類型的轉換,否則或編譯出錯,java版本好像沒問題,但是java對於上面的offset處理有點難搞,所以放棄了
  override def getDefaultTrigger(env: StreamExecutionEnvironment): Trigger[T, TimeWindow] = EventTimeTrigger.create().asInstanceOf[Trigger[T, TimeWindow]]

  override def getWindowSerializer(executionConfig: ExecutionConfig): TypeSerializer[TimeWindow] = new TimeWindow.Serializer

  //是否使用事件時間
  override def isEventTime: Boolean = true


  /**
   * 獲取指定時間戳當月時間戳範圍
   * eg:2020-03-12 11:35:13 (timestamp=1583984113960l)
   * 結果爲:(1582992000000,1585670399999)=>(2020-03-01 00:00:00,2020-03-31 23:59:59)
   *
   * @param timestamp 時間戳
   * @return
   */
  def getTimestampFromMon(timestamp: Long): (Long, Long) = {
    val calendar = Calendar.getInstance()
    calendar.setTime(DateHelper.getInstance().getDateFromStr(new SimpleDateFormat("yyyyMM01000000").format(new Date(timestamp)), "yyyyMMddHHmmss"))
    val numsOfMon: Long = calendar.getActualMaximum(Calendar.DAY_OF_MONTH)
    calendar.set(Calendar.DAY_OF_MONTH, 1)
    val start: Long = calendar.getTimeInMillis
    val end: Long = start + numsOfMon * 24 * 60 * 60 * 1000 - 1
    (start, end)
  }

  /**
   * 獲取指定時間戳本週時間範圍(從週日開始)
   * eg:2020-03-14 23:59:59 (timestamp=1583895064000l)
   * 結果爲:(1583596800000,1584201599999)=>(2020-03-08 00:00:00,2020-03-14 23:59:59)
   *
   * @param timestamp 時間戳
   * @return
   */
  def getTimestampFromWeek(timestamp: Long): (Long, Long) = {
    val calendar = Calendar.getInstance()
    calendar.setTime(DateHelper.getInstance().getDateFromStr(new SimpleDateFormat("yyyyMMdd000000").format(new Date(timestamp)), "yyyyMMddHHmmss"))
    //    calendar.setFirstDayOfWeek(Calendar.SUNDAY)//設置週日爲首日  默認值,一般不用設置
    calendar.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY)
    val start: Long = calendar.getTimeInMillis
    (start, start + 7 * 24 * 60 * 60 * 1000l - 1)
  }
}

//輸入數據
case class Top100Input(event_id: String, date_d: String, timeStamp: Long, uid: Long, weekTag: String, monthTag: String)
//調用
val dStream: DataStream[Top100Input] = ...

dStream
      .keyBy(_.weekTag)
      .window(new CustomWindowAssigner[Top100Input]("week"))

dStream
      .keyBy(_.monthTag)
      .window(new CustomWindowAssigner[Top100Input]("month"))
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章