Spark Streaming編程指南

  本文基於Spark Streaming Programming Guide原文翻譯, 加上一些自己的理解和小實驗的結果。
  

一、概述

  Spark Streaming是基於Core Spark API的可擴展,高吞吐量,並具有容錯能力的用於處理實時數據流的一個組件。Spark Streaming可以接收各種數據源傳遞來的數據,比如Kafka, Flume, Kinesis或者TCP等,對接收到的數據還可以使用一些用高階函數(比如map, reduce, joinwindow)進行封裝的複雜算法做進一步的處理。最後,處理好的數據可以寫入到文件系統,數據庫,或者直接用於實時展示。除此之外,還可以在數據流上應用一些機器學習或者圖計算等算法。
  這裏寫圖片描述

  上圖展示了Spark Streaming的整體數據流轉情況。在Spark Streaming中的處理過程可以參考下圖,Spark Streaming接收實時數據,然後把這些數據分割成一個個batch,然後通過Spark Engine分別處理每一個batch並輸出。
  這裏寫圖片描述

  Spark Streaming中一個最重要的概念是DStream,即離散化數據流(discretized stream),DStream由一系列連續的數據集組成。DStream的創建有兩種辦法,一種是從數據源接收數據生成初始DStream,另一種是由DStream A通過轉換生成DStream B。一個DStream實質上是由一系列的RDDs組成。
  本文介紹瞭如何基於DStream寫出Spark Streaming程序。Spark Streaming提供了Scala, Java以及Python接口,在官方文檔中對這三種語言都有示例程序的實現,在這裏只分析Scala寫的程序。

二、示例程序

  在深入分析Spark Streaming的特性和原理之前,以寫一個簡單的Spark Streaming程序並運行起來爲入口先了解一些相關的基礎知識。這個示例程序從TCP socket中接收數據,進行Word Count操作。

1、Streaming程序編寫

  首先需要導入Spark Streaming相關的類,其中StreamingContext是所有Streaming程序的主要入口。接下來的代碼中創建一個local StreamingContext,batch時間爲1秒,execution線程數爲2。

import org.apache.spark._
import org.apache.spark.streaming._
import org.apache.spark.streaming.StreamingContext._ // not necessary since Spark 1.3

// 創建一個local StreamingContext batch時間爲1秒,execution線程數爲2
// master的線程數數最少爲2,後面會詳細解釋

val conf = new SparkConf().setMaster("local[2]").setAppName("NetworkWordCount")
val ssc = new StreamingContext(conf, econds(1))

使用上面這個ssc對象,就可以創建一個lines變量用來表示從TCP接收的數據流了,指定機器名爲localhost端口號爲9999

// 創建一個連接到hostname:port的DStream, 下面代碼中使用的是localhost:9999
val lines = ssc.socketTextStream("localhost", 9999)

lines中的每一條記錄都是TCP中的一行文本信息。接下來,使用空格將每一行語句進行分割。

// 將每一行分割成單詞
val words = lines.flatMap(_.split(" "))

上面使用的flatMap操作是一個一對多的DStream操作,在這裏表示的是每輸入一行記錄,會根據空格生成多個單詞,這些單詞形成一個新的DStream words。接下來統計單詞個數。

import org.apache.spark.streaming.StreamingContext._ // not necessary since Spark 1.3
// 統計每個batch中的不同單詞個數
val pairs = words.map(word => (word, 1))
val wordCounts = pairs.reduceByKey(_ + _)

// 打印出其中前10個單詞出現的次數
wordCounts.print()

  上面代碼中,將每一個單詞使用map方法映射成(word, 1)的形式,即paris變量。然後調用reduceByKey方法,將相同單詞出現的次數進行疊加,最終打印出統計的結果。

  寫完上面的代碼,Spark Streaming程序還沒有運行起來,需要寫入以下兩行代碼使Spark Streaming程序能夠真正的開始執行。

ssc.start()            // 開始計算
ssc.awaitTermination()  // 等待計算結束

2、TCP發送數據並運行Spark Streaming程序

(1)運行Netcat
  使用以下命令啓動一個Netcat

nc -lk 9999

  接下來就可以在命令行中輸入任意語句了。

(2)運行Spark Streaming程序

./bin/run-example streaming.NetworkWordCount localhost 9999

  程序運行起來後Netcat中輸入的任何語句,都會被統計每個單詞出現的次數,例如
  這裏寫圖片描述

三、基本概念

  這一部分詳細介紹Spark Streaming中的基本概念。

1、依賴配置

  Spark Streaming相關jar包的依賴也可以使用Maven來管理,寫一個Spark Streaming程序的時候,需要將下面的內容寫入到Maven項目中

<dependency>
    <groupId>org.apache.spark</groupId>
    <artifactId>spark-streaming_2.11</artifactId>
    <version>2.0.0</version>
</dependency>

  對於從Kafka,Flume,Kinesis這些數據源接收數據的情況,Spark Streaming core API中不提供這些類和接口,需要添加下面這些依賴。
  

Source Artifact
Kafka spark-streaming-kafka-0-8_2.11
Flume spark-streaming-flume_2.11
Kinesis spark-streaming-kinesis-asl_2.11 [Amazon Software License]

2、初始化StreamingContext

  Spark Streaming程序的主要入口是一個StreamingContext對象,在程序的開始,需要初始化該對象,代碼如下

import org.apache.spark._
import org.apache.spark.streaming._

val conf = new SparkConf().setAppName(appName).setMaster(master)
val ssc = new StreamingContext(conf, Seconds(1))

  其中的參數appName是當前應用的名稱,可以在Cluster UI上進行顯示。master是Spark的運行模式,可以參考 Spark, Mesos or YARN cluster URL,或者設置成local[*]的形式在本地模式下運行。在生產環境中運行Streaming應用時,一般不會將master參數寫死在代碼中,而是在使用spark-submit命令提交時動態傳入--master參數,具體可以參考 launch the application with spark-submit
  至於batch時間間隔的設置,需要綜合考慮程序的性能要求以及集羣可提供的資源情況。

  也可以基於SparkContext對象,生成一個StreamingContext對象,使用如下代碼

import org.apache.spark.streaming._

val sc = ...                // 已有的SparkContext對象
val ssc = new StreamingContext(sc, Seconds(1))

  當context初始化後,還需要做的工作有:

  1. 根據數據源類型生成輸入DStreams
  2. 通過調用transformation以及輸出操作處理輸入的DStreams
  3. 使用代碼streamingContext.start()啓動程序,開始接收並處理數據
  4. 使用代碼streamingContext.awaitTermination()等待程序運行終止(包括手動停止,或者遇到Error後退出應用)
  5. 可以使用streamingContext.stop()手動停止應用

需要注意的點:

  • 當一個context開始運行後,不能再往其中添加新的計算邏輯
  • 當一個context被停止後,不能restart
  • 在一個JVM中只能同時有一個StreamingContext對象處於運行狀態
  • StreamingContext中的stop()方法同樣會終止SparkContext。如果只需要停止StreamingContext,將stop()方法的可選參數設置成false,避免SparkContext被終止
  • 一個SparkContext對象,可以用於構造多個StreamingContext對象,只要在新的StreamingContext對象被創建前,舊的StreamingContext對象被停止即可。

3、離散化數據流(Discretized Streams, DStreams)

  DStream是Spark Streaming中最基本最重要的一個抽象概念。DStream由一系列的數據組成,這些數據既可以是從數據源接收到的數據,也可以是從數據源接收到的數據經過transform操作轉換後的數據。從本質上來說一個DStream是由一系列連續的RDDs組成,DStream中的每一個RDD包含了一個batch的數據。
  這裏寫圖片描述

  DStream上的每一個操作,最終都反應到了底層的RDDs上。比如,在前面那個Word Count代碼中將lines轉化成words的邏輯,lines上的flatMap操作就以下圖中所示的形式,作用到了每一個底層的RDD上。
  這裏寫圖片描述

  這些底層RDDs上的轉換操作會有Spark Engine進行計算。對於開發者來說,DStream提供了一個更方便使用的高階API,從而開發者無需過多的關注每一個轉換操作的細節。
  DStream上可以執行的操作後續文章中會有進一步的介紹。

4、輸入和接收DStream

  
(1)基本數據源
  在前面Word Count的示例程序中,已經使用到了ssc.socketTextStream(...),這個會根據TCP socket中接收到的數據創建一個DStream。除了sockets之外,StreamingContext API還支持以文件爲數據源生成DStream

  • 文件數據源:如果需要從文件系統,比如HDFS,S3,NFS等中接收數據,可以使用以下代碼
streamingContext.fileStream[KeyClass, ValueClass, InputFormatClass](dataDirectory)

Spark Streaming程序會監控用戶輸入的dataDirectory路徑,接收並處理該路徑中的所有文件,不過不支持子文件夾中的文件。
需要注意的地方有:
a、所有的文件數據格式必須相同
b、該路徑下的文件應該是原子性的移動到該路徑,或者重命名到該路徑
c、文件進入該路徑後不可再發生變化,所以這種數據源不支持數據連續寫入的形式
  對於簡單的text文件,有一個簡單的StreamingContext.textFileStream(dataDirectory)方法來進行處理。並且文件數據源的形式不需要運行一個receiver進程,所以對Execution的核數沒有要求。

  • 基於自定義Receiver的數據源:DStream也支持從用戶自定義Receivers中讀取數據。
  • RDDs序列數據源:使用streamingContext.queueStream(queueOfRDDs),可以將一系列的RDDs轉化成一個DStream。該queue中的每一個RDD會被當做DStream中的一個batcn,然後以Streaming的形式處理這些數據。

(2)高階數據源
  
(3)自定義數據源
  除了上面兩類數據源之外,也可以支持自定義數據源。自定義數據源時,需要實現一個可以從自定義數據源接收數據併發送到Spark中的用戶自定義receiver。具體可以參考 Custom Receiver Guide

(4)數據接收的可靠性

5、DStreams上的Transformations

  類似於RDDs,transformations可以使輸入DStream中的數據內容根據特定邏輯發生轉換。DStreams上支持很多RDDs上相同的一些transformations
  具體含義和使用方法可參考另一篇博客:Spark Streaming中的操作函數分析

  在上面這些transformations中,有一些需要進行進一步的分析
(1)UpdateStateByKey操作

(2)Transform操作
  transform操作及其類似的一些transformwith操作,可以使DStream中的元素能夠調用任意的RDD-to-RDD的操作。可以使DStream調用一些只有RDD纔有而DStream API沒有提供的算子。例如,DStream API就不支持一個data DStream中的每一個batch數據可以直接和另外的一個數據集做join操作,但是使用transform就可以實現這一功能。這個操作可以說進一步豐富了DStream的操作功能。
  再列舉一個這個操作的使用場景,將某處計算到的重複信息與實時數據流中的記錄進行join,然後進行filter操作,可以當做一種數據清理的方法。

val spamInfoRDD = ssc.sparkContext.newAPIHadoopRDD(...) // 一個包含重複信息的RDD

val cleanedDStream = wordCounts.transform(rdd => {
  rdd.join(spamInfoRDD).filter(...) // 將重複信息與實時數據做join,然後根據指定規則filter,用於數據清洗
  ...})

  這裏需要注意的是,transform傳入的方法是被每一個batch調用的。這樣可以支持在RDD上做一些時變的操作,即RDD,分區數以及廣播變量可以在不同的batch之間發生變化。

(3)Window操作
  Spark Streaming提供一類基於窗口的操作,這類操作可以在一個滑動窗口中的數據集上進行一些transformations操作。下圖展示了窗口操作的示例
  這裏寫圖片描述

  上圖中,窗口在一個DStream源上滑動,DStream源中暴露在該窗口中的RDDs可以讓這個窗口進行相關的一些操作。在上圖中可以看到,該窗口中任一時刻都只能看到3個RDD,並且這個窗口每2秒中往前滑動一次。這裏提到的兩個參數,正好是任意一個窗口操作都必須指定的。

  • 窗口長度:例如上圖中,窗口長度爲3
  • 滑動間隔:指窗口多長時間往前滑動一次,上圖中爲2。

      需要注意的一點是,上面這兩個參數,必須是batch時間的整數倍,上圖中的batch時間爲1。

      接下來展示一個簡單的窗口操作示例。比如說,在前面那個word count示例程序的基礎上,我希望每隔10秒鐘就統計一下當前30秒時間內的每個單詞出現的次數。這一功能的簡單描述是,在paris DStream的當前30秒的數據集上,調用reduceByKey操作進行統計。爲了實現這一功能,可以使用窗口操作reduceByKeyAndWindow

val windowedWordCounts = pairs.reduceByKeyAndWindow((a:Int,b:Int) => (a + b), Seconds(30), Seconds(10))

  更多的窗口操作可以參考:Spark Streaming中的操作函數分析
  

6、DStreams上的輸出操作

  DStream上的輸出操作,可以使DStream中的數據發送到外部系統,比如數據庫或文件系統中。DStream只有經過輸出操作,其中的數據才能被外部系統使用。並且下面這些輸出操作才真正的觸發DStream對象上調用的transformations操作。這一點類似於RDDs上的Actions算子。
  輸出操作的使用和功能請參考:Spark Streaming中的操作函數分析

  下面主要進一步分析foreachRDD操作往外部數據庫寫入數據的一些注意事項。
  
  dstream.foreachRDD是DStream輸出操作中最常用也最重要的一個操作。關於這個操作如何正確高效的使用,下面會列舉出一些使用方法和案例,可以幫助讀者在使用過程中避免踩到一些坑。
  通常情況下,如果想把數據寫入到某個外部系統中時,需要爲之創建一個連接對象(比如提供一個TCP連接工具用於連接遠程服務器),使用這個連接工具才能將數據發送到遠程系統。在Spark Streaming中,開發者很可能會在Driver端創建這個對象,然後又去Worker端使用這個對象處理記錄。比如下面這個例子

dstream.foreachRDD { rdd =>
  val connection = createNewConnection()  // 在driver端執行
  rdd.foreach { record =>
    connection.send(record) // 在wroker端執行
  }}

  上面這個使用方法其實是錯誤的,當在driver端創建這個連接對象後,需要將這個連接對象序列化併發送到wroker端。通常情況下,連接對象都是不可傳輸的,即wroker端無法獲取該連接對象,當然也就無法將記錄通過這個連接對象發送出去了。這種情況下,應用系統的報錯提示可能是序列化錯誤(連接對象無法序列化),或者初始化錯誤(連接對象需要在wroker端完成初始化),等等。
  正確的做法是在worker端創建這個連接對象。
  但是,即使是在worker創建這個對象,又可能會犯以下錯誤。

dstream.foreachRDD { rdd =>
  rdd.foreach { record =>
    val connection = createNewConnection()
    connection.send(record)
    connection.close()
  }}

  上面代碼會爲每一條記錄創建一個連接對象,導致連接對象太多。 連接對象的創建個數會受到時間和系統資源情況的限制,因此爲每一條記錄都創建一個連接對象會導致系統出現不必要的高負載,進一步導致系統吞吐量降低。
  一個好的辦法是使用rdd.foreachPartition操作,然後爲RDD的每一個partition,使一個partition中的記錄使用同一個連接對象。如下面代碼所示

dstream.foreachRDD { rdd =>
  rdd.foreachPartition { partitionOfRecords =>
    val connection = createNewConnection()
    partitionOfRecords.foreach(record => connection.send(record))
    connection.close()
  }}

  
  最後,可以通過使用連接對象池進一步對上面的代碼進行優化。使用連接對象池可以進一步提高連接對象的使用效率,使得多個RDDs/batches之間可以重複使用連接對象。

dstream.foreachRDD { rdd =>
  rdd.foreachPartition { partitionOfRecords =>
    // 連接對象池是靜態的,並且建立對象只有在真正使用時才被創建
    val connection = ConnectionPool.getConnection()
    partitionOfRecords.foreach(record => connection.send(record))
    ConnectionPool.returnConnection(connection)  // 使用完之後,將連接對象歸還到池中以便下一次使用
  }}

  需要注意的是,連接對象池中的對象最好設置成懶生成模式,即在真正使用時纔去創建連接對象,並且給連接對象設置一個生命週期,一定時間不使用則註銷該連接對象。

總結一下關鍵點:

  • DStreamstransformations操作是由輸出操作觸發的,類似於RDDs中的actions操作。上面列舉出某些DStream的輸出操作中可以將其中的元素轉化成RDD,進而可以調用RDD提供的一些API操作,這時如果對RDD調用actions操作會立即強制對接收到的數據進行處理。因此,如果用戶應用程序中DStream不需要任何的輸出操作,或者僅僅對DStream使用一些類似於dstream.foreachRDD操作但是在這個操作中不調用任何的RDD action操作時,程序是不會進行任何實際運算的。系統只會簡單的接收數據,任何丟棄數據。
  • 默認情況下,輸出操作是順序執行的。

7、累加器和廣播變量

  Spark Streaming的累加器和廣播變量無法從checkpoint恢復。如果在應用中既使用到checkpoint又使用了累加器和廣播變量的話,最好對累加器和廣播變量做懶實例化操作,這樣纔可以使累加器和廣播變量在driver失敗重啓時能夠重新實例化。參考下面這段代碼

object WordBlacklist {

  @volatile private var instance: Broadcast[Seq[String]] = null

  def getInstance(sc: SparkContext): Broadcast[Seq[String]] = {
    if (instance == null) {
      synchronized {
        if (instance == null) {
          val wordBlacklist = Seq("a", "b", "c")
          instance = sc.broadcast(wordBlacklist)
        }
      }
    }
    instance
  }}

object DroppedWordsCounter {

  @volatile private var instance: Accumulator[Long] = null

  def getInstance(sc: SparkContext): Accumulator[Long] = {
    if (instance == null) {
      synchronized {
        if (instance == null) {
          instance = sc.accumulator(0L, "WordsInBlacklistCounter")
        }
      }
    }
    instance
  }}

wordCounts.foreachRDD((rdd: RDD[(String, Int)], time: Time) => {
  // Get or register the blacklist Broadcast
  val blacklist = WordBlacklist.getInstance(rdd.sparkContext)
  // Get or register the droppedWordsCounter Accumulator
  val droppedWordsCounter = DroppedWordsCounter.getInstance(rdd.sparkContext)
  // Use blacklist to drop words and use droppedWordsCounter to count them
  val counts = rdd.filter { case (word, count) =>
    if (blacklist.value.contains(word)) {
      droppedWordsCounter += count
      false
    } else {
      true
    }
  }.collect()
  val output = "Counts at time " + time + " " + counts})

  查看完整代碼請移步 source code

8、DataFrame和SQL操作

  在streaming數據上也可以很方便的使用到DataFrames和SQL操作。爲了支持這種操作,需要用StreamingContext對象使用的SparkContext對象初始化一個SQLContext對象出來,SQLContext對象設置成一個懶初始化的單例對象。下面代碼對前面的Word Count進行一些修改,通過使用DataFramesSQL來實現Word Count的功能。每一個RDD都被轉化成一個DataFrame對象,然後註冊成一個臨時表,最後就可以在這個臨時表上進行SQL查詢了。

val words: DStream[String] = ...

words.foreachRDD { rdd =>

  // 獲取單例SQLContext對象
  val sqlContext = SQLContext.getOrCreate(rdd.sparkContext)
  import sqlContext.implicits._

  // 將RDD[String]轉化成DataFrame
  val wordsDataFrame = rdd.toDF("word")

  // 註冊表
  wordsDataFrame.registerTempTable("words")

  // 在該臨時表上執行sql語句操作
  val wordCountsDataFrame =
    sqlContext.sql("select word, count(*) as total from words group by word")
  wordCountsDataFrame.show()}

  查看完整代碼請移步 source code.
  也可以在另一線程獲取到的Streaming數據上進行SQL操作(這裏涉及到異步運行StreamingContext)。StreamingContext對象無法感知到異步SQL查詢的存在,因此有StreamingContext對象有可能在SQL查詢完成之前把歷史數據刪除掉。爲了保證StreamingContext不刪除需要用到的歷史數據,需要告訴StreamingContext保留一定量的歷史數據。例如,如果你想在某一個batch的數據上執行SQL查詢操作,但是你這個SQL需要執行5分鐘的時間,那麼,需要執行streamingContext.remember(Minutes(5))語句告訴StreamingContext將歷史數據保留5分鐘。
  有關DataFrames的更多介紹,參考另一篇博客:Spark-SQL之DataFrame操作大全

9、MLlib操作

10、緩存和持久化

  類似於RDDsDStreams也允許開發者將stream中的數據持久化到內存中。在DStream對象上使用persist()方法會將DStream對象中的每一個RDD自動持久化到內存中。這個功能在某個DStream的數據需要進行多次計算時特別有用。對於窗口操作比如reduceByWindow,以及涉及到狀態的操作比如updateStateByKey,默認會對DStream對象執行持久化。因此,程序在運行時會自動將窗口操作和涉及到狀態的這些操作生成的DStream對象持久化到內存中,不需要開發者顯示的執行persist()操作。
  對那些通過網絡接收到的streams數據(比如Kafka, Flume, Socket等),默認的持久化等級是將數據持久化到兩個節點上,以保證其容錯能力。
  注意,不同於RDDs,默認情況下DStream的持久化等級是將數據序列化保存在內存中。這一特性會在後面的性能調優中進一步分析。有關持久化級別的介紹,可以參考rdd-persistence

11、檢查點

  當Streaming應用運行起來時,基本上需要7 * 24的處於運行狀態,所以需要有一定的容錯能力。檢查點的設置就是能夠支持Streaming應用程序快速的從失敗狀態進行恢復的。檢查點保存的數據主要有兩種:  

1 . 元數據(Metadata)檢查點:保存Streaming應用程序的定義信息。主要用於恢復運行Streaming應用程序的driver節點上的應用。元數據包括:
  a、配置信息:創建Streaming應用程序的配置信息
  b、DStream操作:在DStream上進行的一系列操作方法
  c、未處理的batch:記錄進入等待隊列但是還未處理完成的批次

2 . 數據(Data)檢查點:將計算得到的RDD保存起來。在一些跨批次計算並保存狀態的操作時,必須設置檢查點。因爲在這些操作中基於其他批次數據計算得到的RDDs,隨着時間的推移,計算鏈路會越來越長,如果發生錯誤重算的代價會特別高。

  元數據檢查點信息主要用於恢復driver端的失敗,數據檢查點主要用於計算的恢復。

(1)什麼時候需要使用檢查點

  當應用程序出現以下兩種情況時,需要配置檢查點。
  
- 使用到狀態相關的操作算子-比如updateStateByKey或者reduceByKeyAndWindow等,這種情況下必須爲應用程序設置檢查點,用於定期的對RDD進行檢查點設置。
- Driver端應用程序恢復-當應用程序從失敗狀態恢復時,需要從檢查點中讀取相關元數據信息。

(2)檢查點設置

  一般是在具有容錯能力,高可靠的文件系統上(比如HDFS, S3等)設置一個檢查點路徑,用於保存檢查點數據。設置檢查點可以在應用程序中使用streamingContext.checkpoint(checkpointDirectory)來指定路徑。
  如果想要應用程序在失敗重啓時使用到檢查點存儲的元數據信息,需要應用程序具有以下兩個特性,需要使用StreamingContext.getOrCreate代碼在失敗時重新創建StreamingContext對象:

  • 當應用程序是第一次運行時,創建一個新的StreamingContext對象,然後開始執行程序處理DStream。
  • 當應用程序失敗重啓時,可以從設置的檢查點路徑獲取元數據信息,創建一個StreamingContext對象,並恢復到失敗前的狀態。

      下面用Scala代碼實現上面的要求。

def functionToCreateContext(): StreamingContext = {
    val ssc = new StreamingContext(...)   // 創建一個新的StreamingContext對象
    val lines = ssc.socketTextStream(...) // 得到DStreams
    ...
    ssc.checkpoint(checkpointDirectory)   // 設置checkpoint路徑
    ssc
}

// 用checkpoint元數據創建StreamingContext對象或根據上面的函數創建新的對象
val context = StreamingContext.getOrCreate(checkpointDirectory, functionToCreateContext _)

// 設置context的其他參數
context. ...

// 啓動context
context.start()
context.awaitTermination()

  如果checkpointDirectory路徑存在,會使用檢查點元數據恢復一個StreamingContext對象。如果路徑不存在,或者程序是第一次運行,則會使用functionToCreateContext來創建一個新的StreamingContext對象。
  RecoverableNetWorkWordCount示例代碼演示了一個從檢查點恢復應用程序的示例。
  
  需要注意的是,想要用到上面的getOrCreate功能,需要在應用程序運行時使其支持失敗自動重跑的功能。這一功能,在接下來一節中有分析。

  另外,在往檢查點寫入數據這一過程,是會增加系統負荷的。因此,需要合理的設置寫入檢查點數據的時間間隔。對於小批量時間間隔(比如1秒)的應用,如果每一個batch都執行檢查點寫入操作,會顯著的降低系統的吞吐性能。相反的,如果寫入檢查點數據間隔太久,會導致lineage過長。對那些狀態相關的需要對RDD進行檢查點寫入的算子,檢查點寫入時間間隔最好設置成batch時間間隔的整數倍。比如對於1秒的batch間隔,設置成10秒。有關檢查點時間間隔,可以使用dstream.checkpoint(checkpointInterval)。一般來說,檢查點時間間隔設置成5~10倍滑動時間間隔是比較合理的。

12、部署應用程序

  這一節主要討論如何將一個Spark Streaming應用程序部署起來。
  
(1)需求
  運行一個Spark Streaming應用程序,需要滿足一下要求。

  • 需要有一個具有集羣管理器的集羣 - 可以參考Spark應用部署文檔
  • 應用程序打成JAR包 - 需要將應用程序打成JAR包。接下來使用spark-submit命令來運行應用程序的話,在該JAR包中無需打入Spark和Spark Streaming相關JAR包。然而,如果應用程序中使用到了比如Kafka或者Flume等高階數據源的話,需要將這些依賴的JAR包,以及這些依賴進一步的依賴都打入到應用JAR包中。比如,應用中使用到了KafkaUtils的話,需要將spark-streaming-kafka-0.8_2.11以及其依賴都打入到應用程序JAR包中。
  • 爲Executor設置足夠的內存 - 由於接收到的數據必須保存在內存中,必須爲Executor設置足夠的內存能容納這些接收到的數據。注意,如果在應用程序中做了10分鐘長度的窗口操作,系統會保存最少10分鐘的數據在內存中。所以應用程序需要的內存除了由接收的數據決定之外,還需要考慮應用程序中的操作類型。
  • 設置檢查點 - 如果應用程序需要用到檢查點,則需要在文件存儲系統上設置好檢查點路徑。
  • 爲應用程序的Driver設置自動重啓 - 爲了實現driver失敗後自動重啓的功能,應用程序運行的系統必須能夠監控driver進程,並且如果發現driver失敗時能夠自動重啓應用。不同的集羣使用不同的方式實現自動重啓功能。
    • Spark Standalone - 在這種模式下,driver程序運行在某個wroker節點上。並且,Standalone集羣管理器會監控driver程序,如果發現driver停止運行,並且其狀態碼爲非零值或者由於運行driver程序的節點失敗導致driver失敗,就會自動重啓該應用。具體的監控和失敗重啓可以進一步參考Spark Standalone guide
    • YARN - Yarn支持類似的自動重啓應用的機制。更多的細節可以進一步參考YARN的相關文檔
    • Mesos - Mesos使用Marathon實現了自動重啓功能
  • 設置write ahead logs - 從Spark-1.2版本開始,引入了一個write ahead log機制來實現容錯。如果設置了WAL功能,所有接收到的數據會寫入write ahead log中。這樣可以避免driver重啓時出現數據丟失,因此可以保證數據的零丟失,這一點可以參考前面有關介紹。通過將spark.streaming.receiver.writeAheadLog.enable=true來開啓這一功能。然而,這一功能的開啓會降低數據接收的吞吐量。這是可以通過同時並行運行多個接收進程(這一點在後面的性能調優部分會有介紹)進行來抵消該負面影響。另外,如果已經設置了輸入數據流的存儲級別爲Storagelevel.MEMORY_AND_DISK_SET,由於接收到的數據已經會在文件系統上保存一份,這樣就可以關閉WAL功能了。當使用S3以及其他任何不支持flushng功能的文件系統來write ahead logs時,要記得設置spark.streaming.driver.writeAheadLog.closeFileAfterWrite以及spark.streaming.receiver.writeAheadLog.closeFileAfterWrite兩個參數。
  • 設置Spark Streaming最大數據接收率 - 如果運行Streaming應用程序的資源不是很多,數據處理能力跟不上接收數據的速率,可以爲應用程序設置一個每秒最大接收記錄數進行限制。對於Receiver模式的應用,設置spark.streaming.receiver.maxRate,對於Direct Kafka模式,設置spark.streaming.kafka.maxRatePerPartition限制從每個Kafka的分區讀取數據的速率。假如某個Topic有8個分區,spark.streaming.kafka.maxRatePerpartition=100,那麼每個batch最大接收記錄爲800。從Spark-1.5版本開始,引入了一個backpressure的機制來避免設置這個限制閾值。Spark Streaming會自動算出當前的速率限制,並且動態調整這個閾值。通過將spark.streaming.backpressure.enabledtrue開啓backpressure功能。

(2)升級應用代碼
  如果運行中的應用程序有更新,需要運行更新後的代碼,有以下兩種機制。

  • 升級後的應用程序直接啓動,與現有的應用程序並行執行。在新舊應用並行運行的過程中,會接收和處理一部分相同的數據。
  • Gracefully停掉正在運行的應用,然後啓動升級後的應用程序,新的應用程序會從舊的應用程序停止處開始繼續處理數據。需要注意的是,使用這種方式,需要其數據源具有緩存數據的能力,否則在新舊應用程序銜接的間歇期內,數據無法被處理。比如Kafka和Flume都具有數據緩存的能力。並且,在這種情況下,再從舊應用程序的檢查點重新構造SparkStreamingContext對象不再合適,因爲檢查點中的信息可能不包含更新的代碼邏輯,這樣會導致程序出現錯誤。在這種情況下,要麼重新指定一個檢查點,要麼刪除之前的檢查點。

13、監控應用程序

  在Spark Streaming應用程序運行時,Spark Web UI頁面上會多出一個Streaming的選項卡,在這裏面可以顯示一些Streaming相關的參數,比如Receiver是否在運行,接收了多少記錄,處理了多少記錄等。以及Batch相關的信息,包括batch的執行時間,等待時間,完成的batch數,運行中的batch數等等。這裏面有兩個時間參數需要注意理解一些:

  • Processing Time - 每一個batch中數據的處理時間
  • Scheduling Delay - 當前batch從進入隊列到開始執行的延遲時間

      如果處理時間一直比batch時間跨度要長,或者延遲時間逐漸增長,表示系統已經無法處理當前的數據量了,這時候就需要考慮如何去降低每一個batch的處理時間。如何降低batch處理時間,可以參考第四節。

      除了監控頁面之外,Spark還提供了StreamingListener接口,通過這個接口可以獲取到receiver以及batch的處理時間等信息。

四、性能調優

  爲了使Spark Streaming應用能夠更好的運行,需要進行一些調優設置,這一節會分析一些性能調優中的參數和設置規則。在性能調優方面,主要需要考慮以下兩個問題:

  • 如何充分利用集羣資源降低每個Batch的處理時間
  • 如何設置合理的Batch大小,以便應用能夠及時處理接收到的這些數據

1、降低每個Batch的處理時間

  接下來的內容在Spark性能調優中已有介紹,這裏再次強調一下在Streaming中需要注意的一些地方。
  
(1)接收數據進程的並行度
  通過網絡(比如Kafka, Flume, socket等)接收到的數據,首先需要反序列化然後保存在Spark中。當數據接收成爲系統的瓶頸時,就需要考慮如何提高系統接收數據的能力了。每一個輸入的DStream會在一個Worker節點上運行一個接收數據流的進程。如果創建了多個接收數據流進程,就可以生成多個輸入DStream了。比如說,對於Kafka數據源,如果使用的是一個DStream接收來自兩個Topic中的數據的話,就可以將這兩個Topic拆開,由兩個數據接收進程分開接收。當用兩個receiver接收到DStream後,可以在應用中將這兩個DStream再進行合併。比如下面代碼中所示

val numStreams = 5
val kafkaStreams = (1 to numStreams).map { i => KafkaUtils.createStream(...) }
val unifiedStream = streamingContext.union(kafkaStreams)
unifiedStream.print()

  需要注意一個參數spark.streaming.blockInterval。對於Receiver來說,接收到的數據在保存到Spark內存中之前,會以block的形式匯聚到一起。每個Batch中block的個數決定了程序運行時處理這些數據的task的個數。每一個receiver的每一個batck對應的task個數大致爲(batch時間間隔 / block時間間隔)。比如說對於一個2m的batch,如果block時間間隔爲200ms那麼,將會有10個task。如果task的數量太少,對數據的處理就不會很高效。在batch時間固定的情況下,如果想增大task個數,那麼就需要降低blockInterval參數了,這個參數默認值爲200ms,官方建議的該參數下限爲50ms,如果低於50ms可能會引起其他問題。
  另一個提高數據併發處理能力的方法是顯式的對接收數據重新分區,inputStream.repartition(<number of partitions>)
  
(2)數據處理的並行度
  對於reduceByKeyreduceByKeyAndWindow操作來說,並行task個數由參數spark.default.parallelism來控制。如果想要提高數據處理的並行度,可以在調用這類方法時,指定並行參數,或者將spark.default.parallelism參數根據集羣實際情況進行調整。

(3)數據序列化
  可以通過調整序列化相關的參數,來提高數據序列化性能。在Streaming應用中,有兩類數據需要序列化操作。

  • 輸入數據:默認情況下,Receiver接收到的數據以StorageLevel.MEMORY_AND_DIS_SER_2的形式保存在Executor的內存中。也就是說,爲了降低GC開銷,這些數據會被序列化成bytes形式,並且還考慮到executor失敗的容錯。這些數據首先會保存在內存中,當內存不足時會spill到磁盤上。使用這種方式的一個明顯問題是,Spark接收到數據後,首先需要反序列化這些數據,然後再按照Spark的方式對這些數據重新序列化。
  • Streaming操作中持久化的RDD:Streaming計算產生的RDD可能也會持久化到內存中。比如窗口操作函數會將數據緩存起來以便後續多次使用。並且Streaming應用中,這些數據的存儲級別是StorageLevel.MEMORY_ONLY_SET(Spark Core的默認方式是StorageLevel.MEMORY_ONLY)。Streaming對這些數據多了一個序列化操作,這主要也是爲了降低GC開銷。

      在上面這兩種情況中,可以使用Kyro方式對數據進行序列化,同時降低CPU和內存的開銷。有關序列化可以進一步參考Spark調優。對於Kyro方式的參數設置,請參考Spark Kyro參數設置
      一般情況下,如果需要緩存的數據量不大,可以直接將數據以非序列化的形式進行存儲,這樣不會明顯的帶來GC的開銷。比如說,batch時間只有若干秒,並且沒有使用到窗口函數操作,那麼可以在持久化時顯示的指定存儲級別,避免持久化數據時對數據的序列化操作。
      
    (4)提高task啓動性能
      如果每秒啓動的task個數太多(一般指50個以上),那麼對task的頻繁啓動也是一個不容忽視的損耗。遇到這種情況時,需要考慮一下Execution模式了。一般來說,在Spark的Standalone模式以及coarse-grained Mesos模式下task的啓動時間會比fine-grained Mesos模式要低。

2、如何正確設置Batch時間間隔

  爲了使一個Spark Streaming應用在集羣上穩定運行,需要保證應用在接收到數據時能夠及時處理。如果處理速率不匹配,隨着時間的積累,等待處理的數據將會越來越多,最終導致應用無法正常運行。最好的情況是batch的處理時間小於batch的間隔時間。所以,正確合理的設置Batch時間間隔是很重要的。
  

3、內存調整

  有關Spark內存的使用以及Spark應用的GC性能調節的更多細節在Spark調優中已經有了更加詳細的描述。這裏簡單分析一些Spark Streaming應用程序會用到的參數。
  
  一個Spark Streaming應用程序需要使用集羣多少內存資源,很大程度上是由該應用中的具體邏輯來決定的,即需要看應用程序中的transformations的類型。比如代碼中使用到長達10分鐘的窗口操作時,就需要使用到能夠把10分鐘的數據都保存到內存中的內存量。如果使用updateStateByKey這種操作,而數據中不同key特別多,也會使用更多的內存。如果應用的邏輯比較簡單,僅僅是接收-過濾-存儲等一系列操作時,消耗的內存量會明顯減少。
  
  默認情況下,receivers接收到的數據會以StorageLevel.MEMORY_AND_DISK_SER_2級別進程存儲,當內存中容納不下時會spill到磁盤上,但是這樣會降低應用的處理性能,所以爲了應用能夠更高效的運行,最好還是多分配一些內存以供使用。一般可以通過在少量數據的情況下,評估一下數據使用的內存量,繼而計算出應用正式部署時需要分配的總內存量大小。
  
  內存調節的另一方面是垃圾回收的設置。對一個低延遲的應用系統來說,JVM在垃圾回收時導致應用長時間暫停運行是一個很討厭的場景。

  下面有一些可用於調節內存使用量和GC性能的方面:

  • DStreams的持久化級別:在前面已經提到,輸入數據在默認情況下會以序列化的字節形式進行持久化。與非序列化存儲相比,這樣會降低內存使用率和降低垃圾回收的負擔。使用Kryo方式進行序列化能夠進一步降低序列化後數據大小和內存的使用。想要進一步降低內存的使用量,可以在數據上再增加一個壓縮功能,通過參數spark.rdd.compress來設置。
  • 清除舊數據:默認情況下,所有輸入數據和DStream通過不同的transformations持久化的數據都會自動進行清理。Spark Streaming根據transformations的不同來決定哪些數據需要被清理掉。例如,當使用10分鐘的窗口函數時,Spark Streaming會保存最少10分鐘的數據。想要數據保存更長時間,可以設置streamingContext.remenber參數。
  • 使用CMS垃圾回收算法:特別建議使用CMS垃圾回收機制來降低GC壓力。driver上通過設置spark-submit命令的--driver-java-options參數來指定,executor上通過設置spark.executor.extraJavaOptions參數來指定。
  • 其他建議:進一步降低GC負擔,可以使用以下一些方法。
    • 使用Tachyon提供的OFF_HEAP存儲級別來持久化RDDs,可以參考RDD Persistence
    • 降低heap大小,使用更多executors。這樣可以降低每個JVM堆的GC壓力。

五、容錯性

  本節主要討論Spark Streaming應用程序失敗後的處理辦法。

1、背景

2、定義

3、基本概念

4、數據接收方式

(1)Files輸入

(2)基於Receiverd 數據源

(3)Kafka Direct輸入方式

5、輸出操作

六、Spark Streaming的升級

七、繼續

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