一.引言:
Apach Flink 是全新的流處理系統,在Spark Straming的基礎上添加了很多特性,主要在於其提供了基於時間和窗口計算的算子,並且支持有狀態的存儲和 Checkpoint 的重啓機制,下面假設有多個溫度傳感器持續傳輸當前溫度,Flink流處理需要每一段時間提供該時間段內的傳感器平均溫度。
二.依賴支持
項目是基於maven的scala項目,主要導入flink的scala依賴,如果是java需要另一套依賴:
1.scala
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<flink.version>1.7.1</flink.version>
<scala.binary.version>2.12</scala.binary.version>
<scala.version>2.12.8</scala.version>
<hadoop.version>2.6.0</hadoop.version>
</properties>
<dependencies>
<!-- Apache Flink dependencies -->
<!-- These dependencies are provided, because they should not be packaged into the JAR file. -->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-scala_${scala.binary.version}</artifactId>
<version>${flink.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-streaming-scala_${scala.binary.version}</artifactId>
<version>${flink.version}</version>
<scope>provided</scope>
</dependency>
<!-- Scala Library, provided by Flink as well. -->
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
<version>${scala.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
2.java
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-streaming-java_2.11</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-streaming-scala_2.11</artifactId>
<version>${flink.version}</version>
</dependency>
三.輔助類
1.基礎溫度類 SensorReading
採用case class簡化後續處理函數的代碼
case class SensorReading(id: String, timestamp: Long, temperature: Double)
2.時間戳提取類
這裏採用了Flink的特性: EventTime作爲數據的時間戳,通過提取生成SensorReading中的時間戳作爲溫度傳感器傳遞溫度的EventTime
import org.apache.flink.streaming.api.functions.timestamps.BoundedOutOfOrdernessTimestampExtractor
import org.apache.flink.streaming.api.windowing.time.Time
/**
* Assigns timestamps to SensorReadings based on their internal timestamp and
* emits watermarks with five seconds slack.
* 根據傳感器內部時間戳和傳感器讀取分配時間戳。
*/
class SensorTimeAssigner extends BoundedOutOfOrdernessTimestampExtractor[SensorReading](Time.seconds(5)) {
/** Extracts timestamp from SensorReading. */
override def extractTimestamp(r: SensorReading): Long = r.timestamp
}
如果是Java,寫法稍有不同
public static class SensorTimeAssigner extends BoundedOutOfOrdernessTimestampExtractor<SensorReading> {
public SensorTimeAssigner() {
super(Time.seconds(5));
}
@Override
public long extractTimestamp(SensorReading r) {
return r.timestamp;
}
}
3.自定義source類
SparkStreaming採用的是複寫Receiver函數實現自定義數據源,通過receiver的store生成數據;Storm通過覆蓋Spout的nextTruple方法,emit生成數據;這裏Flink通過集成SoureFunction實現run方法,通過collect方法生成數據,這幾種流式處理器在自定義數據流這方面其實大致比較類似,換湯不換藥。相關的註釋都寫在代碼裏了,這裏邏輯比較簡單,只是通過Random類,去隨機模擬溫度函數,如果自己有場景需求需要自定義數據源時,可以把Random看做是自己的Socket,在run方法初始化數據源生成數據即可,這裏生成數據可以通過Flink Env設置並行度,並行的接收數據,前提是你的數據源支持並行接收。
import java.util.Calendar
import org.apache.flink.streaming.api.functions.source.RichParallelSourceFunction
import org.apache.flink.streaming.api.functions.source.SourceFunction.SourceContext
import scala.util.Random
/**
* Flink 源功能,用於生成具有隨機溫度值的傳感器讀取。
*
* 自定義源方法需要實現run方法與cancel方法即可,需要初始化的連接放到run方法之內即可
*
* 源的每個並行實例模擬 10 個傳感器,這些傳感器發出一個傳感器
* 每 100 ms 閱讀一次。
*
* 注意:這是一個簡單的數據生成源函數,不檢查其狀態。
* 如果發生故障,源不會重播任何數據。
*/
// 繼承時需要定義生成的DStream的類型
class SensorSource extends RichParallelSourceFunction[SensorReading] {
// flag indicating whether source is still running.
// 指示源是否仍在運行。
var running: Boolean = true
/** run() continuously emits SensorReadings by emitting them through the SourceContext. */
override def run(srcCtx: SourceContext[SensorReading]): Unit = {
// SourceContext 通過 collect 方法不斷向flink發出數據
// initialize random number generator
val rand = new Random()
// 獲取當前parallel subtask的下標
val taskIdx = this.getRuntimeContext.getIndexOfThisSubtask
// initialize sensor ids and temperatures
// 初始化溫度轉換器 IndexSeq[String,Int] 序列長度爲10 華氏度65+
var curFTemp = (1 to 10).map {
i => ("sensor_" + (taskIdx * 10 + i), 65 + (rand.nextGaussian() * 20))
}
// emit data until being canceled
while (running) {
// update temperature
// 更新溫度
curFTemp = curFTemp.map( t => (t._1, t._2 + (rand.nextGaussian() * 0.5)) )
// get current time
val curTime = Calendar.getInstance.getTimeInMillis
// emit new SensorReading
// id 區分傳感器分區 curTime 標識eventTime temperature 標識溫度
curFTemp.foreach( t => srcCtx.collect(SensorReading(t._1, curTime, t._2)))
// wait for 100 ms
Thread.sleep(100)
}
}
/** Cancels this SourceFunction. */
override def cancel(): Unit = {
running = false
}
}
四.主類
主類主要提供三個邏輯:
=> 定義Flink Env 配置相關環境變量,這裏定義使用EventTime作爲處理時間,其他還有ProcessTime,IngestionTime
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
env.getConfig.setAutoWatermarkInterval(1000L)
=> 獲取數據源 Source 並設置 EventTime 的獲取方式,Source獲取數據源,assign設置事件時間
val sensorData: DataStream[SensorReading] = env
.addSource(new SensorSource)
.assignTimestampsAndWatermarks(new SensorTimeAssigner)
=> 定義數據處理方式並提交任務,這裏採用了時間窗口的處理方式,還有基於數據量的窗口以及基於處理函數的算子,這裏先介紹最基礎的
val avgTemp: DataStream[SensorReading] = sensorData
.map( r => SensorReading(r.id, r.timestamp, (r.temperature - 32) * (5.0 / 9.0)) )
.keyBy(_.id)
.timeWindow(Time.seconds(1))
.apply(new TemperatureAverage)
avgTemp.print()
env.execute("Compute average sensor temperature")
1.完整主類
通過KeyBy可以將原始DataStream轉換爲KeyedStream,這樣同一個key的數據都會發往一個窗口進行處理,這裏1s生成一個時間窗口用於監測平均溫度
import com.weibo.ug.push.flink.SensorData.{SensorReading, SensorSource, SensorTimeAssigner}
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.scala.function.WindowFunction
import org.apache.flink.streaming.api.windowing.time.Time
import org.apache.flink.streaming.api.windowing.windows.TimeWindow
import org.apache.flink.util.Collector
/** Object that defines the DataStream program in the main() method */
object AverageSensorReadings {
/** main() defines and executes the DataStream program */
def main(args: Array[String])
// set up the streaming execution environment
val env = StreamExecutionEnvironment.getExecutionEnvironment
// use event time for the application
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
// configure watermark interval
// 自動生成水位線
// 也可以通過 assignTimestampsAndWatermarks 函數內的 getCurrentWatermark 獲取水位線生成方式
env.getConfig.setAutoWatermarkInterval(1000L)
// ingest sensor stream
// 攝取流數據並綁定 eventTime
val sensorData: DataStream[SensorReading] = env
// SensorSource generates random temperature readings
// 自定義數據源
.addSource(new SensorSource)
// assign timestamps and watermarks which are required for event time
// 分配事件時間所需的時間戳和水印 主要從DStream的數據中獲取相關的時間戳 extractTimestamp
// 最好在事件生成時爲數據類型綁定 eventTime
.assignTimestampsAndWatermarks(new SensorTimeAssigner)
val avgTemp: DataStream[SensorReading] = sensorData
// convert Fahrenheit to Celsius using an inlined map function
// 通過map函數將華氏度轉換爲攝氏度 這裏也可以調用filter過濾認爲不需要的傳感器參數
.map( r =>
SensorReading(r.id, r.timestamp, (r.temperature - 32) * (5.0 / 9.0)) )
// organize stream by sensorId
// 將同一傳感器的溫度統一在一起 KeyedStream
.keyBy(_.id)
// group readings in 1 second windows
// 1s生成1個窗口 將一個窗口的數據傳輸並處理 類似 spark streaming 的 interval
.timeWindow(Time.seconds(1))
// compute average temperature using a user-defined function
.apply(new TemperatureAverage)
// print result stream to standard out
avgTemp.print()
// execute application
env.execute("Compute average sensor temperature")
}
}
2.窗口處理函數
窗口處理函數這裏需要覆蓋apply方法,有一個注意的點就是繼承WindowFunction函數的參數和apply方法的參數是不完全一致的,相關參數的註解都在代碼註釋裏,可以大致瀏覽;Collector作爲一個數據發射器,將處理好的類型進行下一步傳遞,這裏主類的處理邏輯比較簡單,只調用了print,也可以通過addSink方法繼續向下遊發送數據,常見的落盤HDFS,或者寫到Kafka,Flink都有相關的實現API。
/** User-defined WindowFunction to compute the average temperature of SensorReadings */
// WindowFunction 參數分別代表 In Out Key W ,前兩個比較好理解 輸入輸出類型 第三個爲key的類型 第四個用於獲取當前window參數
class TemperatureAverage extends WindowFunction[SensorReading, SensorReading, String, TimeWindow] {
/** apply() is invoked once for each window */
// apply方法的參數不完全和window保持相同順序 分別爲key 當前window 本次窗口輸入的迭代類型 與輸出的Collector
override def apply(
sensorId: String,
window: TimeWindow,
values: Iterable[SensorReading],
out: Collector[SensorReading]): Unit = {
// compute the average temperature
// Int Double 代表返回類型 (c,r)中c代表泛型 r代表values中的元素類型
val (cnt, sum) = values.foldLeft((0, 0.0))((c, r) => (c._1 + 1, c._2 + r.temperature))
val avgTemp = sum / cnt
// emit a SensorReading with the average temperature
// collector通過collect方法輸出每個窗口對應時間戳的平均溫度
out.collect(SensorReading(sensorId, window.getEnd, avgTemp))
}
}