Spark Streaming + Spark SQL 實現配置化ET

gamebus發表於2021-09-09

Spark Streaming 非常適合ETL。但是其開發模組化程度不高,所以這裡提供了一套方案,該方案提供了新的API用於開發Spark Streaming程式,同時也實現了模組化,配置化,並且支援SQL做資料處理。

前言

傳統的Spark Streaming程式需要:

  • 構建StreamingContext

  • 設定checkpoint

  • 連結資料來源

  • 各種transform

  • foreachRDD 輸出

通常而言,你可能會因為要走完上面的流程而構建了一個很大的程式,比如一個main方法裡上百行程式碼,雖然在開發小功能上足夠便利,但是複用度更方面是不夠的,而且不利於協作,所以需要一個更高層的開發包提供支援。

如何開發一個Spark Streaming程式

我只要在配置檔案新增如下一個job配置,就可以作為標準的的Spark Streaming 程式提交執行:

{  "test": {    "desc": "測試",    "strategy": "streaming.core.strategy.SparkStreamingStrategy",    "algorithm": [],    "ref": [],    "compositor": [
      {        "name": "streaming.core.compositor.kafka.MockKafkaStreamingCompositor",        "params": [
          {            "metadata.broker.list":"xxx",            "auto.offset.reset":"largest",            "topics":"xxx"
          }
        ]
      },
      {        "name": "streaming.core.compositor.spark.JSONTableCompositor",        "params": [{"tableName":"test"}
        ]
      },
      {        "name": "streaming.core.compositor.spark.SQLCompositor",        "params": [{"sql":"select a from test"}
        ]
      },
      {        "name": "streaming.core.compositor.RDDPrintOutputCompositor",        "params": [
          {
          }
        ]
      }
    ],    "configParams": {
    }
  }
}

上面的配置相當於完成了如下的一個流程:

  1. 從Kafka消費資料

  2. 將Kafka資料轉化為表

  3. 透過SQL進行處理

  4. 列印輸出

是不是很簡單,而且還可以支援熱載入,動態新增job等

特性

該實現的特性有:

  1. 配置化

  2. 支援多Job配置

  3. 支援各種資料來源模組

  4. 支援透過SQL完成資料處理

  5. 支援多種輸出模組

未來可擴充套件的支援包含:

  1. 動態新增或者刪除job更新,而不用重啟Spark Streaming

  2. 支援Storm等其他流式引擎

  3. 更好的多job互操作

配置格式說明

該實現完全基於 完成,核心功能大概只花了三個小時。

這裡我們先理出幾個概念:

  1. Spark Streaming 定義為一個App

  2. 每個Action定義為一個Job.一個App可以包含多個Job

配置檔案結構設計如下:

{  "job1": {    "desc": "測試",    "strategy": "streaming.core.strategy.SparkStreamingStrategy",    "algorithm": [],    "ref": [],    "compositor": [
      {        "name": "streaming.core.compositor.kafka.MockKafkaStreamingCompositor",        "params": [
          {            "metadata.broker.list":"xxx",            "auto.offset.reset":"largest",            "topics":"xxx"
          }
        ]
      } ,  
    ],    "configParams": {
    }
  },  "job2":{
   ........
 } 
}

一個完整的App 對應一個配置檔案。每個頂層配置選項,如job1,job2分別對應一個工作流。他們最終都會執行在一個App上(Spark Streaming例項上)。

  • strategy 用來定義如何組織 compositor,algorithm, ref 的呼叫關係

  • algorithm作為資料來源

  • compositor 資料處理鏈路模組。大部分情況我們都是針對該介面進行開發

  • ref 是對其他job的引用。透過配合合適的strategy,我們將多個job組織成一個新的job

  • 每個元件( compositor,algorithm, strategy) 都支援引數配置

上面主要是解析了配置檔案的形態,並且 已經給出了一套介面規範,只要照著實現就行。

模組實現

那對應的模組是如何實現的?本質是將上面的配置檔案,透過已經實現的模組,轉化為Spark Streaming程式。

以SQLCompositor 的具體實現為例:

class SQLCompositor[T] extends Compositor[T] {  private var _configParams: util.List[util.Map[Any, Any]] = _
  val logger = Logger.getLogger(classOf[SQLCompositor[T]].getName)//策略引擎ServiceFrameStrategy 會呼叫該方法將配置傳入進來
  override def initialize(typeFilters: util.List[String], configParams: util.List[util.Map[Any, Any]]): Unit = {
    this._configParams = configParams
  }// 獲取配置的sql語句
  def sql = {
    _configParams(0).get("sql").toString
  }

  def outputTable = {
    _configParams(0).get("outputTable").toString
  }//執行的主方法,大體是從上一個模組獲取SQLContext(已經註冊了對應的table),//然後根據該模組的配置,設定查詢語句,最後得到一個新的dataFrame.// middleResult裡的T其實是DStream,我們會傳遞到下一個模組,Output模組//params引數則是方便各個模組共享資訊,這裡我們將對應處理好的函式傳遞給下一個模組
  override def result(alg: util.List[Processor[T]], ref: util.List[Strategy[T]], middleResult: util.List[T], params: util.Map[Any, Any]): util.List[T] = {    var dataFrame: DataFrame = null
    val func = params.get("table").asInstanceOf[(RDD[String]) => SQLContext]
    params.put("sql",(rdd:RDD[String])=>{
      val sqlContext = func(rdd)
      dataFrame = sqlContext.sql(sql)
      dataFrame
    })
    middleResult
  }
}

上面的程式碼就完成了一個SQL模組。那如果我們要完成一個自定義的.map函式呢?可類似下面的實現:

abstract class MapCompositor[T,U] extends Compositor[T]{  private var _configParams: util.List[util.Map[Any, Any]] = _
  val logger = Logger.getLogger(classOf[SQLCompositor[T]].getName)

  override def initialize(typeFilters: util.List[String], configParams: util.List[util.Map[Any, Any]]): Unit = {
    this._configParams = configParams
  }

  
  override def result(alg: util.List[Processor[T]], ref: util.List[Strategy[T]], middleResult: util.List[T], params: util.Map[Any, Any]): util.List[T] = {
    val dstream = middleResult(0).asInstanceOf[DStream[String]]
    val newDstream = dstream.map(f=>parseLog(f))    List(newDstream.asInstanceOf[T])
  }
  def parseLog(line:String): U
}class YourCompositor[T,U] extends MapCompositor[T,U]{

 override def parseLog(line:String):U={
     ....your logical
  }
}

同理你可以實現filter,repartition等其他函式。

總結

該方式提供了一套更為高層的API抽象,使用者只要關注具體實現而無需關注Spark的使用。同時也提供了一套配置化系統,方便構建資料處理流程,並且複用原有的模組,支援使用SQL進行資料處理。



作者:祝威廉
連結:


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/4687/viewspace-2819304/,如需轉載,請註明出處,否則將追究法律責任。

相關文章