Flink 入門Demo詳解

一.引言:

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))
  }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章