spark學習筆記-- Spark Streaming

zxrui發表於2018-08-03

許多應用需要即時處理收到的資料,Spark Streaming是Spark為這種應用而設計的模型,他允許使用者使用一套和批處理非常接近的API來編寫流失計算應用,這樣就可以大量重用批處理應用的技術。 和RDD的概念很相似,Spark Streaming使用離散化流作為抽象表示,叫做DStream。Dstream是隨著時間推移而收到的資料的序列。在內部,每個時間區間收到的資料都作為RDD存在,DStream是有這些RDD所組成的序列。DStream可以從各種輸入源建立,如Flume,Kafka或者HDFS。建立出來的DStream支援兩種操作,轉化和輸出

一個例子

Spark Streaming程式最好以使用Maven或者sbt編譯出來的獨立應用的形式執行。

scala:

//Spark Streaming 的 Maven索引
groupId = org.apache.spark
artifactId = spark-streaming_2.10
version = 1.2.0

//Scala流計算import宣告
import org.apache.spark.streaming.StreamingContext
import org.apache.spark.streaming.StreamingContext._
import org.apache.spark.streaming.dstream.DStream
import org.apache.spark.streaming.Duration
import org.apache.spark.streaming.Seconds

【舉例】

  • 從建立StreamingContext開始,它是計算功能的主要入口。StreamingContext會從底層建立出SparkContext ,用來處理資料。其建構函式還接收用來制定多長時間處理一次新資料的批次間隔(batch interval)作為輸入。
  • 呼叫socketTextStream()來建立出基於本地7777埠上收到的文字資料的DStream
  • 把DStream通過filter()進行轉化,只得到包含"error"的行
  • 使用輸出操作print把一些篩選出來的行列印出來

scala:

// 從SparkConf建立StreamingContext並指定1秒鐘的批處理大小
val ssc = new StreamingContext(conf, Seconds(1))
// 連線到本地機器7777埠上後,使用收到的資料建立DStream
val lines = ssc.socketTextStream("localhost", 7777)
// 從DStream中篩選出包含字串"error"的行
val errorLines = lines.filter(_.contains("error"))
// 列印出有"error"的行
errorLines.print()

系統收到資料時就會開始計算,要開始接受資料,必須顯式呼叫StreamingContext的start()方法 scala:

// 啟動流計算環境StreamingContext並等待它"完成"
ssc.start()
// 等待作業完成
ssc.awaitTermination()

架構與抽象

  • Spark Streaming的高層次架構

enter image description here

  • DStream是一個連續的RDD序列

enter image description here

  • DStream及其轉化關係

enter image description here

  • Spark Streaming在Spark各元件中的執行過程 enter image description here

轉化操作

DStream的轉化操作分位無狀態和有狀態兩種:

  • 無狀態轉化操作中,每個批次的處理不依賴於之前批次的資料,像RDD的map,filter,reduceBykey等操作都是無狀態轉化操作
  • 有狀態轉化操作需要使用之前批次的資料或者中間結果來計算當前批次的資料。有狀態轉化操作包括基於滑動視窗的轉化操作和追蹤狀態變化的轉化操作

無狀態轉化操作

無狀態轉化操作就是簡單把RDD轉化操作應用到每個批次上,部分無狀態轉化操作如下圖。注:針對鍵值對的DStream轉化操作(如reduceByKey())要新增import StreamingContext._才能在Scala中使用。

enter image description here

儘管這些函式看起來像所用在整個流上,其實是每個DStream在內部有許多RDD批次組成,這些操作是分別應用到每個RDD上的

Scala舉例:

// 假設ApacheAccessingLog是用來從Apache日誌中解析條目的工具類
val accessLogDStream = logData.map(line => ApacheAccessLog.parseFromLogLine(line))
val ipDStream = accessLogsDStream.map(entry => (entry.getIpAddress(), 1))
val ipCountsDStream = ipDStream.reduceByKey((x, y) => x + y)

有狀態轉化操作

DSteam的有狀態轉化操作是跨時間區間跟蹤資料的操作,即一些先前批次的資料也被用來在新的批次中計算結果。有狀態轉化操作需要在你的 StreamingContext 中開啟檢查點機制來確保容錯性,設定檢查點:

ssc.checkpoint("hdfs://...")

有狀態轉化操作主要有兩種型別:

  • 基於視窗的轉化操作

基於視窗的操作會在一個比StreamingContext的批次間隔更長的時間範圍內,通過整合多個批次的結果,計算出整個視窗的結果。所有基於視窗的引數都需要兩個引數:視窗時長和滑動步長,兩個必須是StreamContext的批次間隔的正數倍。

  • 視窗時長控制每次計算最近的多少個批次的資料,其實就是最近的windowDuration/batchInterval個批次。如果一個以10s為批次間隔的源DStream,要建立一個最近30s的時間視窗(即最近三個批次),就把windowDuratioin設為30s
  • 滑塊步長的預設值與批次間隔相等,用來控制對新的DStream進行計算的間隔。如果源DStream批次間隔為10s,且我們希望每兩個批次計算一次視窗結果,就應該把滑動步長設定為20s

enter image description here

在Scala中用window()對視窗進行計數:

val accessLogsWindow = accessLogsDStream.window(Seconds(30), Seconds(10))
val windowCounts = accessLogsWindow.count()

reduceByWindow()reduceByKeyAndWindow() 可以對每個視窗更高效地進行歸約操作。兩種不同是後者可以逆運算(-),可以記錄進入視窗和離開視窗的資料:

val ipDStream = accessLogsDStream.map{entry => entry.getIpAddress()}
val ipAddressRequestCount = ipDStream.countByValueAndWindow(Seconds(30), Seconds(10))
val requestCount = accessLogsDStream.countByWindow(Seconds(30), Seconds(10))

enter image description here

  • 跟蹤狀態的轉化操作

有時我們需要在DStream中跨批次維護狀態(例如跟蹤使用者訪問網站的會話),針對這種情況,updateStateByKey()提供了對一個狀態變數的訪問,用於鍵值對形式的DStream。updateStateByKey()提供了一個update(events, oldState) 函式,接收與某鍵相關的事件以及該鍵之前對應的狀態,返回這個鍵對應的新狀態。

  • event:是在當前批次中收到的事件的列表(可能為空)
  • oldState:是一個可選的狀態物件,存放在Option內;如果一個鍵沒有之前的狀態,這個值可以空缺
  • newState:由函式返回,也以Option形式存在;可以返回一個空的Option來表示要刪除該狀態

updateStateByKey() 的結果會是一個新的 DStream,其內部的 RDD 序列是由每個時間區間對應的(鍵,狀態)對組成的。

在Scala中使用updateStateByKey()執行響應程式碼的計數:

def updateRunningSum(values: Seq[Long], state: Option[Long]) = {
    Some(state.getOrElse(0L) + values.size)
}

val responseCodeDStream = accessLogsDStream.map(log => (log.getResponseCode(), 1L))
val responseCodeCountDStream = 
responseCodeDStream.updateStateByKey(updateRunningSum _)

輸出操作

輸出操作指定了對流資料經轉化操作得到的資料所要執行的操作(例如把結果推入外資料庫或者螢幕上)

與RDD中的惰性求值類似,如果一個DStream及其派生出的DStream都沒有被執行輸出操作,那麼這些DStream就都不會被求值。如果StreamingContext中沒有設定輸出操作,整個context就都不會啟動。

一旦除錯好了程式,就可以使用輸出操作來儲存結果了。在Scala中將DStream儲存為檔案:

ipAddressRequestCount.saveAsTextFiles("outputDir", "txt")

還可以用saveAsHadoopFiles()函式,接收Hadoop輸出格式儲存。儲存SequenceFile Spark Streaming比較特殊:

val writableIpAddressRequestCount = ipAddressRequestCount.map {
    (ip, count) => (new Text(ip), new LongWritable(count)) }
writableIpAddressRequestCount.saveAsHadoopFiles[
SequenceFileOutputFormat[Text, LongWritable]]("outputDir", "txt")

還有一個通用的輸出操作foreachRDD(),用來對DStream中的RDD執行任意計算:

ipAddressRequestCount.foreachRDD { rdd =>
    rdd.foreachPartition { partition =>
        // 開啟到儲存系統的連線(比如一個資料庫的連線)
       partition.foreach { item =>
       // 使用連線把item存到系統中
       }
       // 關閉連線
    }
}

輸入源

Spark Streaming原生支援一些冉的資料來源,一些核心資料來源已經打包到Maven工作中,而其他的一些則可以通過spark-streaming-kafka等附加工作獲取。

核心資料來源

  • 檔案流

因為Spark支援從任意Hadoop相容的檔案系統中讀取資料,所以Spark Streaming也就支援從任意Hadoop相容的檔案系統目錄中的檔案建立資料流:

val logData = ssc.textFileStream(logDirectory)

除了文字資料,也可以讀入任意 Hadoop 輸入格式:

ssc.fileStream[LongWritable, IntWritable,
SequenceFileInputFormat[LongWritable, IntWritable]](inputDirectory).map {
case (x, y) => (x.get(), y.get())

}

  • Akka actor流

另一個核心資料來源接收器是 actorStream,它可以把 Akka actor(http://akka.io/)作為資料流的源。要建立出一個 actor 流,需要建立一個 Akka actor,然後實現 org.apache.spark.streaming.receiver.ActorHelper 介面。要把輸入資料從 actor 複製到 Spark Streaming 中,需要在收到新資料時呼叫 actor 的 store() 函式。Akka actor 流不是很常見,所以我們不會對其進行深入探究。你可以閱讀流計算的文件(http://spark.apache.org/docs/latest/streaming-custom-receivers.html)以及 Spark 中的

附加資料來源

  • Apache Kafka

Apache Kafka(http://kafka.apache.org/)因其速度與彈性成為了一個流行的輸入源。使用 Kafka 原生的支援,可以輕鬆處理許多主題的訊息。在工程中需要引入 Maven 工件 spark-streaming-kafka_2.10 來使用它。包內提供的 KafkaUtils 物件可以在 StreamingContext 和 JavaStreamingContext 中以你的 Kafka 訊息建立出 DStream。由於 KafkaUtils 可以訂閱多個主題,因此它建立出的 DStream 由成對的主題和訊息組成。要建立出一個流資料,需要使用 StreamingContext 例項、一個由逗號隔開的 ZooKeeper 主機列表字串、消費者組的名字(唯一名字),以及一個從主題到針對這個主題的接收器執行緒數的對映表來呼叫 createStream() 方法:

import org.apache.spark.streaming.kafka._
...
// 建立一個從主題到接收器執行緒數的對映表
val topics = List(("pandas", 1), ("logs", 1)).toMap
val topicLines = KafkaUtils.createStream(ssc, zkQuorum, group, topics)
StreamingLogInput.processLines(topicLines.map(_._2))
  • Apache Flume Spark 提供兩個不同的接收器來使用 Apache Flume(http://flume.apache.org/,見圖 10-8)。兩個接收器簡介如下:

    • 推式接收器

      該接收器以 Avro 資料池的方式工作,由 Flume 向其中推資料。

    • 拉式接收器

      該接收器可以從自定義的中間資料池中拉資料,而其他程式可以使用 Flume 把資料推進該中間資料池。

enter image description here

  • 推式接收器

接收器以 Avro 資料池的方式工作,我們需要配置 Flume 來把資料發到 Avro 資料池 Flume對Avro池的配置:

a1.sinks = avroSink
a1.sinks.avroSink.type = avro
a1.sinks.avroSink.channel = memoryChannel
a1.sinks.avroSink.hostname = receiver-hostname
a1.sinks.avroSink.port = port-used-for-avro-sink-not-spark-port

Scala中的FlumeUtils代理:

val events = FlumeUtils.createStream(ssc, receiverHostname, receiverPort)

雖然這種方式很簡潔,但缺點是沒有事務支援。這會增加執行接收器的工作節點發生錯誤時丟失少量資料的機率。不僅如此,如果執行接收器的工作節點發生故障,系統會嘗試從另一個位置啟動接收器,這時需要重新配置 Flume 才能將資料發給新的工作節點。這樣配置會比較麻煩。

  • 拉式接收器

它設定了一個專用的 Flume 資料池供 Spark Streaming 讀取,並讓接收器主動從資料池中拉取資料。這種方式的優點在於彈性較好,Spark Streaming 通過事務從資料池中讀取並複製資料。在收到事務完成的通知前,這些資料還保留在資料池中。

我們需要先把自定義資料池配置為 Flume 的第三方外掛。安裝外掛的最新方法請參考 Flume 文件的相關部分(https://flume.apache.org/FlumeUserGuide.html#installing-third-party-plugins)。由於外掛是用 Scala 寫的,因此需要把外掛本身以及 Scala 庫都新增到 Flume 外掛中

Flume資料池的Maven索引:

groupId = org.apache.spark
artifactId = spark-streaming-flume-sink_2.10
version = 1.2.0

groupId = org.scala-lang
artifactId = scala-library
version = 2.10.4

Flume對自定義資料池的配置:

a1.sinks = spark
a1.sinks.spark.type = org.apache.spark.streaming.flume.sink.SparkSink
a1.sinks.spark.hostname = receiver-hostname
a1.sinks.spark.port = port-used-for-sync-not-spark-port
a1.sinks.spark.channel = memoryChannel

在Scala中使用FlumeUtils讀取自定義資料池:

val events = FlumeUtils.createPollingStream(ssc, receiverHostname, receiverPort)
  • 自定義輸入源 除了上述這些源,你也可以實現自己的接收器來支援別的輸入源。詳細資訊請參考 Spark 文件中的“自定義流計算接收器指南”(Streaming Custom Receivers guide,http://spark.apache.org/docs/latest/streaming-custom-receivers.html)

多資料來源與叢集規模

有時,使用多個接收器對於提高聚合操作中的資料獲取的吞吐量非常必要,有時,我們需要用不同的接收器來從不同的輸入源中接收各種資料,然後使用 join 或 cogroup 進行整合。

理解接收器是如何在 Spark 叢集中執行的,對於我們使用多個接收器至關重要。每個接收器都以 Spark 執行器程式中一個長期執行的任務的形式執行,因此會佔據分配給應用的 CPU 核心。此外,我們還需要有可用的 CPU 核心來處理資料。這意味著如果要執行多個接收器,就必須至少有和接收器數目相同的核心數,還要加上用來完成計算所需要的核心數。例如,如果我們想要在流計算應用中執行 10 個接收器,那麼至少需要為應用分配 11 個 CPU 核心。

24/7不間斷執行

要不間斷執行 Spark Streaming 應用,需要一些特別的配置。第一步是設定好諸如 HDFS 或 Amazon S3 等可靠儲存系統中的檢查點機制。我們還需要考慮驅動器程式的容錯性以及對不可靠輸入源的處理。

檢查點機制

檢查點機制是我們在 Spark Streaming 中用來保障容錯性的主要機制。它可以使 Spark Streaming 階段性地把應用資料儲存到諸如 HDFS 或 Amazon S3 這樣的可靠儲存系統中,以供恢復時使用。具體來說,檢查點機制主要為以下兩個目的服務:

  • 控制發生失敗時需要重算的狀態數。Spark Streaming 可以通過轉化圖的譜系圖來重算狀態,檢查點機制則可以控制需要在轉化圖中回溯多遠。
  • 提供驅動器程式容錯。如果流計算應用中的驅動器程式崩潰了,你可以重啟驅動器程式並讓驅動器程式從檢查點恢復,這樣 Spark Streaming 就可以讀取之前執行的程式處理資料的進度,並從那裡繼續。

驅動器程式容錯

驅動器程式的容錯要求我們以特殊的方式建立 StreamingContext。我們需要把檢查點目錄提供給 StreamingContext,應該使用StreamingContext.getOrCreate() 函式。

用 Scala 配置一個可以從錯誤中恢復的驅動器程式:

def createStreamingContext() = {
    ...
    val sc = new SparkContext(conf)
    // 以1秒作為批次大小建立StreamingContext
    val ssc = new StreamingContext(sc, Seconds(1))
    ssc.checkpoint(checkpointDir)
}
...
val ssc = StreamingContext.getOrCreate(checkpointDir, createStreamingContext _)

工作節點容錯

為了應對工作節點失敗的問題,Spark Streaming 使用與 Spark 的容錯機制相同的方法。所有從外部資料來源中收到的資料都在多個工作節點上備份。所有從備份資料轉化操作的過程中建立出來的 RDD 都能容忍一個工作節點的失敗,因為根據 RDD 譜系圖,系統可以把丟失的資料從倖存的輸入資料備份中重算出來。

接收器容錯

執行接收器的工作節點的容錯也是很重要的。接收器提供以下保證:

  • 所有從可靠檔案系統中讀取的資料,都是可靠的,因為底層的檔案系統是有備份的。Spark Streaming 會記住哪些資料存放到了檢查點中,並在應用崩潰後從檢查點處繼續執行。
  • 對於像 Kafka、推式 Flume、Twitter 這樣的不可靠資料來源,Spark 會把輸入資料複製到其他節點上,但是如果接收器任務崩潰,Spark 還是會丟失資料。

綜上,確保所有資料都被處理的最佳方式是使用可靠的資料來源(例如 HDFS、拉式 Flume 等)。如果你還要在批處理作業中處理這些資料,使用可靠資料來源是最佳方式,因為這種方式確保了你的批處理作業和流計算作業能讀取到相同的資料,因而可以得到相同的結果。

處理保證

由於 Spark Streaming 工作節點的容錯保障,Spark Streaming 可以為所有的轉化操作提供“精確一次”執行的語義,即使一個工作節點在處理部分資料時發生失敗,最終的轉化結果(即轉化操作得到的 RDD)仍然與資料只被處理一次得到的結果一樣。

然而,當把轉化操作得到的結果使用輸出操作推入外部系統中時,寫結果的任務可能因故障而執行多次,一些資料可能也就被寫了多次。由於這引入了外部系統,因此我們需要專門針對各系統的程式碼來處理這樣的情況。我們可以使用事務操作來寫入外部系統(即原子化地將一個 RDD 分割槽一次寫入),或者設計冪等的更新操作(即多次執行同一個更新操作仍生成相同的結果)。比如 Spark Streaming 的 saveAs...File 操作會在一個檔案寫完時自動將其原子化地移動到最終位置上,以此確保每個輸出檔案只存在一份。

效能考量

批次和視窗大小

最常見的問題是 Spark Streaming 可以使用的最小批次間隔是多少。如果 Streaming 使用者介面中顯示的處理時間保持不變,你就可以進一步減小批次大小。如果處理時間開始增加,你可能已經達到了應用的極限。 對於視窗操作,計算結果的間隔(也就是滑動步長)對於效能也有巨大的影響。當計算代價巨大併成為系統瓶頸時,就應該考慮提高滑動步長了。

並行度

減少批處理所消耗時間的常見方式還有提高並行度。有以下三種方式可以提高並行度:

  • 增加接收器數目

有時如果記錄太多導致單臺機器來不及讀入並分發的話,接收器會成為系統瓶頸。這時你就需要通過建立多個輸入 DStream(這樣會建立多個接收器)來增加接收器數目,然後使用 union 來把資料合併為一個資料來源。

  • 將接受器的資料顯式地重新分割槽

如果接收器數目無法再增加,你可以通過使用 DStream.repartition 來顯式重新分割槽輸入流(或者合併多個流得到的資料流)來重新分配收到的資料。

  • 提高聚合計算的並行度

對於像 reduceByKey() 這樣的操作,你可以在第二個引數中指定並行度,我們在介紹RDD 時提到過類似的手段。

垃圾回收和記憶體使用

Java 的垃圾回收機制(簡稱 GC)也可能會引起問題。你可以通過開啟 Java 的併發標誌—清除收集器(Concurrent Mark-Sweep garbage collector)來減少 GC 引起的不可預測的長暫停。併發標誌—清除收集器總體上會消耗更多的資源,但是會減少暫停的發生。

可以通過在配置引數 spark.executor.extraJavaOptions 中新增 -XX:+UseConcMarkSweepGC 來控制選擇併發標誌—清除收集器:

spark-submit --conf spark.executor.extraJavaOptions=-XX:+UseConcMarkSweepGC App.jar

除了使用較少引發暫停的垃圾回收器,你還可以通過減輕 GC 的壓力來大幅度改善效能。把 RDD 以序列化的格式快取(而不使用原生的物件)也可以減輕 GC 的壓力。

Spark 也允許我們控制快取下來的 RDD 以怎樣的策略從快取中移除。預設情況下,Spark 使用 LRU 快取。如果你設定了 spark.cleaner.ttl,Spark 也會顯式移除超出給定時間範圍的老 RDD。主動從快取中移除不大可能再用到的 RDD,可以減輕 GC 的壓力。

相關文章