Window
Flink中Window可以將無限流切分成有限流,是處理有限流的核心組件,現在Flink中 Window可以是時間驅動的Time Window,也可以是數據驅動的Count Window。
基於時間的窗口操作:在每個相同的時間間隔對Stream中的記錄進行處理,通常各個時間間隔內的窗口操作處理的記錄數不固定。
基於數據驅動的窗口操作:可以在Stream中選擇固定數量的記錄作爲一個窗口,對該窗口中的記錄進行處理。
窗口類型
Tumbling Window(滾動窗口):窗口間的元素無重複
一個翻滾窗口分配器的每個數據元分配給指定的窗口的窗口大小。翻滾窗具有固定的尺寸,不重疊。
Sliding Window(滑動窗口):窗口間的元素可能重複
該滑動窗口分配器分配元件以固定長度的窗口。與翻滾窗口分配器類似,窗口大小由窗口大小參數配置。附加的窗口滑動參數控制滑動窗口的啓動頻率。因此,如果幻燈片小於窗口大小,則滑動窗口可以重疊。在這種情況下,數據元被分配給多個窗口。
Session Window(會話窗口)
在會話窗口中按活動會話分配器組中的數據元。與翻滾窗口和滑動窗口相比,會話窗口不重疊並且沒有固定的開始和結束時間。相反,當會話窗口在一段時間內沒有接收到數據元時,即當發生不活動的間隙時,會關閉會話窗口。會話窗口分配器可以配置靜態會話間隙或會話間隙提取器函數,該函數定義不活動時間段的長度。當此期限到期時,當前會話將關閉,後續數據元將分配給新的會話窗口。
Global Window(全局窗口)
一個全局性的窗口分配器分配使用相同的Keys相同的單個的所有數據元全局窗口。此窗口方案僅在您還指定自定義觸發器時纔有用。否則,將不執行任何計算,因爲全局窗口沒有我們可以處理聚合數據元的自然結束。
Window Function在窗口觸發後,負責對窗口內的元素進行計算。
Window Function分爲兩類: 增量聚合和全量聚合。
增量聚合: 窗口不維護原始數據,只維護中間結果,每次基於中間結果和增量數據進行聚合。如: ReduceFunction、AggregateFunction等。
全量聚合: 窗口需要維護全部原始數據,窗口觸發進行全量聚合。如:ProcessWindowFunction。
Time的分類
Event-Time :事件時間是每個事件在其生產設備上發生的時間。
Ingestion-Time :攝取時間是事件進入Flink的時間。
Processing-Time : 處理時間是指執行相應算子操作的機器的系統時間。
不設置Time 類型,默認是processingTime。可以如下方式修改時間特性:
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
Watermark
主要解決延遲數據、亂序問題
Watermark是Apache Flink爲了處理EventTime窗口計算提出的一種機制,本質上也是一種時間戳,由Apache Flink Source或者自定義的Watermark生成器按照需求Punctuated或者Periodic兩種方式生成的一種系統Event,與普通數據流Event一樣流轉到對應的下游算子,接收到Watermark Event的算子以此不斷調整自己管理的EventTime Clock。
Watermark兩種生成方式:
Periodic - 週期性(一定時間間隔或者達到一定的記錄條數)產生一個Watermark,默認週期爲200毫秒。在實際的生產中Periodic的方式必須結合時間和積累條數兩個維度繼續週期性產生Watermark,否則在極端情況下會有很大的延時。
接口 AssignerWithPeriodicWatermarks
Punctuated:數據流中每一個遞增的EventTime都會產生一個Watermark。沒有時間週期規律,可打斷的生成Watermark。在實際的生產中Punctuated方式在TPS很高的場景下會產生大量的Watermark在一定程度上對下游算子造成壓力,所以只有在實時性要求非常高的場景纔會選擇Punctuated的方式進行Watermark的生成。
接口 AssignerWithPunctuatedWatermarks
在基於Event-Time的流處理應用中,每個數據有兩個必需的信息:
時間戳:事件發生的時間
Watermark:算子通過Watermark推斷當前的事件時間。Watermark用於通知算子沒有比水位更小的時間戳的事件會發生了。
基於時間的窗口會根據事件時間將一個數據分配給某個窗口。每個時間窗口都有一個開始時間戳和結束時間戳。所有內置的窗口分配器都會提供一個默認的觸發器,一旦時間超過某個窗口的結束時間,觸發器就會觸發對這個窗口的計算。
API簡單示例
import java.sql.Timestamp
import java.text.SimpleDateFormat
import java.util.Properties
import org.apache.flink.api.common.functions.AggregateFunction
import org.apache.flink.api.common.serialization.SimpleStringSchema
import org.apache.flink.api.common.state.{ListState, ListStateDescriptor}
import org.apache.flink.configuration.Configuration
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.functions.timestamps.BoundedOutOfOrdernessTimestampExtractor
import org.apache.flink.streaming.api.functions.{AssignerWithPeriodicWatermarks, AssignerWithPunctuatedWatermarks, KeyedProcessFunction}
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.scala.function.WindowFunction
import org.apache.flink.streaming.api.watermark.Watermark
import org.apache.flink.streaming.api.windowing.time.Time
import org.apache.flink.streaming.api.windowing.windows.TimeWindow
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer
import org.apache.flink.util.Collector
import scala.collection.mutable.ListBuffer
case class UserBehavior(userId:Long, itemId:Long, behavior:String, timestamp:Long)
case class ItemCount(itemId:Long, count:Long, timestamp:Long)
object TopNHotItemsGenerator {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
// 設置水位間隔,默認200毫秒
env.getConfig.setAutoWatermarkInterval(100)
// 設置時間特性
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
val dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss")
val properties = new Properties()
properties.setProperty("bootstrap.servers", "192.168.0.1:9092")
properties.setProperty("group.id", "tmp-1")
properties.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
properties.setProperty("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
properties.setProperty("auto.offset.reset", "latest")
val dataStream = env.addSource(new FlinkKafkaConsumer[String]("tmp1", new SimpleStringSchema(), properties))
.map(data => {
val dataArray = data.split(",")
UserBehavior(dataArray(0).trim().toLong, dataArray(1).trim().toLong,
dataArray(2).trim(), dateFormat.parse(dataArray(3).trim()).getTime)
})
.assignAscendingTimestamps(_.timestamp) // 數據中提取時間戳
/** 定義水位生成方式
.assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor[UserBehavior](Time.seconds(1)) {
override def extractTimestamp(t: UserBehavior): Long = {
t.timestamp
}
})
.assignAscendingTimestamps(new PeriodicWatermarksAssigner())
.assignAscendingTimestamps(new PunctuatedWatermarksAssigner())
*/
dataStream.print()
val aggregateStream = dataStream.filter(_.behavior == "click").keyBy(_.itemId)
.timeWindow(Time.hours(1), Time.minutes(5))
.aggregate(new CountAggregate(), new WindowAggregateResult()) // 窗口聚合
aggregateStream.print()
val processStream = aggregateStream.keyBy(_.timestamp)
.process(new TopNHotItemsProcessFunction(2))
processStream.print()
env.execute("topn hot items")
}
}
class CountAggregate() extends AggregateFunction[UserBehavior, Long, Long] {
override def createAccumulator(): Long = 0L
override def add(in: UserBehavior, acc: Long): Long = acc + 1
override def getResult(acc: Long): Long = acc
override def merge(acc: Long, acc1: Long): Long = acc + acc1
}
class AvgAggregate() extends AggregateFunction[UserBehavior, (Long, Long), Double] {
override def createAccumulator(): (Long, Long) = (0L, 0L)
override def add(in: UserBehavior, acc: (Long, Long)): (Long, Long) = (acc._1 + in.timestamp, acc._2 + 1)
override def getResult(acc: (Long, Long)): Double = acc._1 / acc._2
override def merge(acc: (Long, Long), acc1: (Long, Long)): (Long, Long) = (acc._1 + acc1._1, acc._2 + acc1._2)
}
class WindowAggregateResult() extends WindowFunction[Long, ItemCount, Long, TimeWindow] {
override def apply(key: Long, window: TimeWindow, input: Iterable[Long], out: Collector[ItemCount]): Unit = {
out.collect(ItemCount(key, input.iterator.next(), window.getEnd))
}
}
class TopNHotItemsProcessFunction(topN: Int) extends KeyedProcessFunction[Long, ItemCount, String] {
var itemsState:ListState[ItemCount] = _
override def open(parameters: Configuration): Unit = {
super.open(parameters)
itemsState = getRuntimeContext.getListState(new ListStateDescriptor[ItemCount]("hot-items-state", classOf[ItemCount]))
}
override def processElement(value: ItemCount, context: KeyedProcessFunction[Long, ItemCount, String]#Context, collector: Collector[String]): Unit = {
itemsState.add(value)
// 註冊事件時間定時器
context.timerService().registerEventTimeTimer(value.timestamp + 1)
}
override def onTimer(timestamp: Long, ctx: KeyedProcessFunction[Long, ItemCount, String]#OnTimerContext, out: Collector[String]): Unit = {
val items:ListBuffer[ItemCount] = new ListBuffer[ItemCount]()
import scala.collection.JavaConversions._
for (item <- itemsState.get()) {
items += item
}
val topNHotItems = items.sortBy(_.count)(Ordering.Long.reverse).take(topN)
val result: StringBuilder = new StringBuilder()
result.append("time: ").append(new Timestamp(timestamp - 1)).append("\n")
for (i <- topNHotItems.indices) {
val hotItem = topNHotItems(i)
result.append("top%d: itemId %d count %d".format(i + 1, hotItem.itemId, hotItem.count)).append("\n")
}
result.append("====================")
out.collect(result.toString())
itemsState.clear()
}
}
class PeriodicWatermarksAssigner extends AssignerWithPeriodicWatermarks[UserBehavior] {
val bound: Long = 60000; //延時一分鐘
var maxTs: Long = Long.MinValue //最大時間戳
override def getCurrentWatermark: Watermark = {
new Watermark(maxTs - bound)
}
override def extractTimestamp(element: UserBehavior, l: Long): Long = {
maxTs = maxTs.max(element.timestamp)
element.timestamp
}
}
class PunctuatedWatermarksAssigner extends AssignerWithPunctuatedWatermarks[UserBehavior] {
val bound: Long = 60000; //延時一分鐘
override def checkAndGetNextWatermark(element: UserBehavior, l: Long): Watermark = {
if (element.behavior == "") {
new Watermark(l - bound)
} else {
null
}
}
override def extractTimestamp(element: UserBehavior, l: Long): Long = element.timestamp
}