Structured Streaming應用

一、Spark Streaming曲折發展史

Spark Streaming針對實時數據流,提供了一套可擴展、高吞吐、可容錯的流式計算模型。Spark Streaming接收實時數據源的數據,切分成很多小的batches,然後被Spark Engine執行,產出同樣由很多小的batchs組成的結果流。本質上,這是一種micro-batch(微批處理)的方式處理

不足在於處理延時較高(無法優化到秒以下的數量級), 無法支持基於event_time的時間窗口做聚合邏輯

介紹

官網

http://spark.apache.org/docs/latest/structured-streaming-programming-guide.html

spark在2.0版本中發佈了新的流計算的API,Structured Streaming/結構化流。

Structured Streaming是一個基於Spark SQL引擎的可擴展、容錯的流處理引擎。統一了流、批的編程模型,可以使用靜態數據批處理一樣的方式來編寫流式計算操作。並且支持基於event_time的時間窗口的處理邏輯

隨着數據不斷地到達,Spark 引擎會以一種增量的方式來執行這些操作,並且持續更新結算結果。可以使用Scala、Java、Python或R中的DataSet/DataFrame API來表示流聚合、事件時間窗口、流到批連接等。此外,Structured Streaming會通過checkpoint和預寫日誌等機制來實現Exactly-Once語義。

簡單來說,對於開發人員來說,根本不用去考慮是流式計算,還是批處理,只要使用同樣的方式來編寫計算操作即可Structured Streaming提供了快速、可擴展、容錯、端到端的一次性流處理,而用戶無需考慮更多細節

默認情況下,結構化流式查詢使用微批處理引擎進行處理,該引擎將數據流作爲一系列小批處理作業進行處理,從而實現端到端的延遲,最短可達100毫秒,並且完全可以保證一次容錯。自Spark 2.3以來,引入了一種新的低延遲處理模式,稱爲連續處理,它可以在至少一次保證的情況下實現低至1毫秒的端到端延遲。也就是類似於 Flink 那樣的實時流,而不是小批量處理。實際開發可以根據應用程序要求選擇處理模式,但是連續處理在使用的時候仍然有很多限制,目前大部分情況還是應該採用小批量模式。

API

1.Spark Streaming 時代 -DStream-RDD

Spark Streaming 採用的數據抽象是DStream,而本質上就是時間上連續的RDD,對數據流的操作就是針對RDD的操作

2.Structured Streaming 時代 - DataSet/DataFrame -RDD

Structured Streaming是Spark2.0新增的可擴展和高容錯性的實時計算框架,它構建於Spark SQL引擎,把流式計算也統一到DataFrame/Dataset裏去了。

Structured Streaming 相比於 Spark Streaming 的進步就類似於 Dataset 相比於 RDD 的進步

主要優勢

1.簡潔的模型。Structured Streaming 的模型很簡潔,易於理解。用戶可以直接把一個流想象成是無限增長的表格

2.一致的 API。由於 Spark SQL 共用大部分 API,對 Spaprk SQL 熟悉的用戶很容易上手,代碼也十分簡潔。同時批處理和流處理程序還可以共用代碼,不需要開發兩套不同的代碼,顯著提高了開發效率。

3.卓越的性能。Structured Streaming 在與 Spark SQL 共用 API 的同時,也直接使用了 Spark SQL 的 Catalyst 優化器和 Tungsten,數據處理性能十分出色。此外,Structured Streaming 還可以直接從未來 Spark SQL 的各種性能優化中受益。

4.多語言支持。Structured Streaming 直接支持目前 Spark SQL 支持的語言,包括 ScalaJavaPythonR SQL。用戶可以選擇自己喜歡的語言進行開發。

編程模型

編程模型概述

一個流的數據源從邏輯上來說就是一個不斷增長的動態表格,隨着時間的推移,新數據被持續不斷地添加到表格的末尾

對動態數據源進行實時查詢,就是對當前的表格內容執行一次 SQL 查詢。

數據查詢,用戶通過觸發器(Trigger)設定時間(毫秒級)。也可以設定執行週期。

一個流的輸出有多種模式,既可以是基於整個輸入執行查詢後的完整結果,也可以選擇只輸出與上次查詢相比的差異,或者就是簡單地追加最新的結果。

這個模型對於熟悉 SQL 的用戶來說很容易掌握,對流的查詢跟查詢一個表格幾乎完全一樣,十分簡潔,易於理解

核心思想

Structured Streaming最核心的思想就是將實時到達的數據不斷追加到unbound table無界表,到達流的每個數據項(RDD)就像是表中的一個新行被附加到無邊界的表中.這樣用戶就可以用靜態結構化數據的批處理查詢方式進行流計算,如可以使用SQL對到來的每一行數據進行實時查詢處理;(SparkSQL+SparkStreaming=StructuredStreaming)

●應用場景

Structured Streaming將數據源映射爲類似於關係數據庫中的表,然後將經過計算得到的結果映射爲另一張表,完全以結構化的方式去操作流式數據,這種編程模型非常有利於處理分析結構化的實時數據;

 ●WordCount圖解


第一行表示從socket不斷接收數據,
第二行可以看成是之前提到的“unbound table",
第三行爲最終的wordCounts是結果集。
當有新的數據到達時,Spark會執行“增量"查詢,並更新結果集;
該示例設置爲Complete Mode(輸出所有數據),因此每次都將所有數據輸出到控制檯;

  1. 在第1秒時,此時到達的數據爲"cat dog"和"dog dog",因此我們可以得到第1秒時的結果集cat=1 dog=3,並輸出到控制檯;
  2. 當第2秒時,到達的數據爲"owl cat",此時"unbound table"增加了一行數據"owl cat",執行word count查詢並更新結果集,可得第2秒時的結果集爲cat=2 dog=3 owl=1,並輸出到控制檯;
  3. 當第3秒時,到達的數據爲"dog"和"owl",此時"unbound table"增加兩行數據"dog"和"owl",執行word count查詢並更新結果集,可得第3秒時的結果集爲cat=2 dog=4 owl=2;

這種模型跟其他很多流式計算引擎都不同。大多數流式計算引擎都需要開發人員自己來維護新數據與歷史數據的整合並進行聚合操作。然後我們就需要自己去考慮和實現容錯機制、數據一致性的語義等。然而在structured streaming的這種模式下,spark會負責將新到達的數據與歷史數據進行整合,並完成正確的計算操作,同時更新result table,不需要我們去考慮這些事情。

二、Structured Streaming實戰

1、創建Source

spark 2.0中初步提供了一些內置的source支持。

Socket source (for testing): 從socket連接中讀取文本內容。

File source: 以數據流的方式讀取一個目錄中的文件。支持text、csv、json、parquet等文件類型。

Kafka source: 從Kafka中拉取數據,與0.10或以上的版本兼容,後面單獨整合Kafka

讀取Socket數據

●準備工作

nc -lk 9999

hadoop spark sqoop hadoop spark hive hadoop

●代碼演示

import org.apache.spark.SparkContext
import org.apache.spark.sql.streaming.Trigger
import org.apache.spark.sql.{DataFrame, Dataset, Row, SparkSession}

object StructStreaming_socket {

  def main(args: Array[String]): Unit = {

    val spark = SparkSession.builder().master("local[*]").appName("").getOrCreate()
    val sc: SparkContext = spark.sparkContext
    sc.setLogLevel("WARN")
    //讀取實時數據  數據類型是Row
    val socketDatasRow: DataFrame = spark.readStream.option("host","node01").option("port","9999").format("socket").load()
    //3.對數據進行處理和計算
    import spark.implicits._
    val socketDatasString: Dataset[String] = socketDatasRow.as[String]
    val word: Dataset[String] = socketDatasString.flatMap(a=>a.split(" "))
    val structWordCount: Dataset[Row] = word.groupBy("value").count().sort($"count")
    structWordCount.writeStream
      .trigger(Trigger.ProcessingTime(0))//儘快執行
      .format("console")    //數據輸出到控制檯
      .outputMode("complete")   //輸出所有數據
      .start()    //開始計算
      .awaitTermination() //等待關閉
  }

}

讀取目錄下文本數據

spark應用可以監聽某一個目錄,而web服務在這個目錄上實時產生日誌文件,這樣對於spark應用來說,日誌文件就是實時數據

Structured Streaming支持的文件類型有text,csv,json,parquet

●準備工作

在people.json文件輸入如下數據:

{"name":"json","age":23,"hobby":"running"}
{"name":"charles","age":32,"hobby":"basketball"}
{"name":"tom","age":28,"hobby":"football"}
{"name":"lili","age":24,"hobby":"running"}
{"name":"bob","age":20,"hobby":"swimming"}

注意:文件必須是被移動到目錄中的,且文件名不能有特殊字符

●需求

使用Structured Streaming統計年齡小於25歲的人羣的愛好排行榜

 ●代碼演示

import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.types.StructType

object StructStreaming_files {

  def main(args: Array[String]): Unit = {

    val spark = SparkSession.builder()
      .master("local[*]")
      .appName("StructStreaming_socket")
      .getOrCreate()
    val sc = spark.sparkContext
    sc.setLogLevel("WARN")
    //準備數據結構
    val structtype = new StructType().add("name","string").add("age","integer").add("hobby","string")
    //接收數據  指定目錄,不能指定文件
    val jsonDatas = spark.readStream.schema(structtype).json("E:\\input")
    import spark.implicits._
    val hobby = jsonDatas
//      .filter($"age"<25)
      .groupBy("hobby")
      .count()
      .sort($"count".desc)
    //輸出數據
    hobby.writeStream
      .format("console")
      .outputMode("complete")
      .start()
      .awaitTermination()
  }
}

 

2、計算操作

獲得到Source之後的基本數據處理方式和之前學習的DataFrame、DataSet一致,不再贅述

3、輸出

計算結果可以選擇輸出到多種設備並進行如下設定

1.output mode:以哪種方式將result table的數據寫入sink

2.format/output sink的一些細節:數據格式、位置等。

3.query name:指定查詢的標識。類似tempview的名字

4.trigger interval:觸發間隔,如果不指定,默認會儘可能快速地處理數據

5.checkpoint地址:一般是hdfs上的目錄。注意:Socket不支持數據恢復,如果設置了,第二次啓動會報錯 ,Kafka支持

output mode

每當結果表更新時,我們都希望將更改後的結果行寫入外部接收器。

這裏有三種輸出模型:

1.Append mode:輸出新增的行,默認模式。每次更新結果集時,只將新添加到結果集的結果行輸出到接收器。僅支持添加到結果表中的行永遠不會更改的查詢。因此,此模式保證每行僅輸出一次。例如,僅查詢select,where,map,flatMap,filter,join等會支持追加模式。不支持聚合

2.Complete mode: 所有內容都輸出,每次觸發後,整個結果表將輸出到接收器。聚合查詢支持此功能。僅適用於包含聚合操作的查詢

3.Update mode: 輸出更新的行,每次更新結果集時,僅將被更新的結果行輸出到接收器(自Spark 2.1.1起可用),不支持排序

output sink

使用說明

File sink 輸出到路徑

支持parquet文件,以及append模式

writeStream
    .format("parquet")        // can be "orc", "json", "csv", etc.
    .option("path", "path/to/destination/dir")
    .start()

Kafka sink 輸出到kafka內的一到多個topic

writeStream
    .format("kafka")
    .option("kafka.bootstrap.servers", "host1:port1,host2:port2")
    .option("topic", "updates")
    .start()

Foreach sink 對輸出中的記錄運行任意計算。

writeStream
    .foreach(...)
    .start()

Console sink (for debugging) 當有觸發器時,將輸出打印到控制檯。

writeStream
    .format("console")
    .start()

Memory sink (for debugging) - 輸出作爲內存表存儲在內存中.

writeStream
    .format("memory")
    .queryName("tableName")
    .start()

官網示例代碼

// ========== DF with no aggregations ==========
val noAggDF = deviceDataDf.select("device").where("signal > 10")   
// Print new data to console
noAggDF.writeStream.format("console").start()
// Write new data to Parquet files
noAggDF.writeStream.format("parquet").option("checkpointLocation", "path/to/checkpoint/dir").option("path", "path/to/destination/dir").start()
// ========== DF with aggregation ==========
val aggDF = df.groupBy("device").count()
// Print updated aggregations to console
aggDF.writeStream.outputMode("complete").format("console").start()
// Have all the aggregates in an in-memory table
aggDF.writeStream.queryName("aggregates").outputMode("complete").format("memory").start()
spark.sql("select * from aggregates").show()   // interactively query in-memory table

三、StructuredStreaming與其他技術整合

1、整合Kafka

官網介紹

http://spark.apache.org/docs/latest/structured-streaming-kafka-integration.html

Creating a Kafka Source for Streaming Queries

// Subscribe to 1 topic
val df = spark
  .readStream
  .format("kafka")
  .option("kafka.bootstrap.servers", "host1:port1,host2:port2")
  .option("subscribe", "topic1")
  .load()
df.selectExpr("CAST(key AS STRING)", "CAST(value AS STRING)")
  .as[(String, String)]
// Subscribe to multiple topics(多個topic)
val df = spark
  .readStream
  .format("kafka")
  .option("kafka.bootstrap.servers", "host1:port1,host2:port2")
  .option("subscribe", "topic1,topic2")
  .load()
df.selectExpr("CAST(key AS STRING)", "CAST(value AS STRING)")
  .as[(String, String)]
// Subscribe to a pattern(訂閱通配符topic)
val df = spark
  .readStream
  .format("kafka")
  .option("kafka.bootstrap.servers", "host1:port1,host2:port2")
  .option("subscribePattern", "topic.*")
  .load()
df.selectExpr("CAST(key AS STRING)", "CAST(value AS STRING)")
  .as[(String, String)]

Creating a Kafka Source for Batch Queries(kafka批處理查詢)

// Subscribe to 1 topic 
//defaults to the earliest and latest offsets(默認爲最早和最新偏移)
val df = spark
  .read
  .format("kafka")
  .option("kafka.bootstrap.servers", "host1:port1,host2:port2")
  .option("subscribe", "topic1")
  .load()df.selectExpr("CAST(key AS STRING)", "CAST(value AS STRING)")
  .as[(String, String)]
// Subscribe to multiple topics, (多個topic)
//specifying explicit Kafka offsets(指定明確的偏移量)
val df = spark
  .read
  .format("kafka")
  .option("kafka.bootstrap.servers", "host1:port1,host2:port2")
  .option("subscribe", "topic1,topic2")
  .option("startingOffsets", """{"topic1":{"0":23,"1":-2},"topic2":{"0":-2}}""")
  .option("endingOffsets", """{"topic1":{"0":50,"1":-1},"topic2":{"0":-1}}""")
  .load()df.selectExpr("CAST(key AS STRING)", "CAST(value AS STRING)")
  .as[(String, String)]
// Subscribe to a pattern, (訂閱通配符topic)at the earliest and latest offsets
val df = spark
  .read
  .format("kafka")
  .option("kafka.bootstrap.servers", "host1:port1,host2:port2")
  .option("subscribePattern", "topic.*")
  .option("startingOffsets", "earliest")
  .option("endingOffsets", "latest")
  .load()df.selectExpr("CAST(key AS STRING)", "CAST(value AS STRING)")
  .as[(String, String)]

注意:讀取後的數據的Schema是固定的,包含的列如下:

Column

Type

說明

key

binary

消息的key

value

binary

消息的value

topic

string

主題

partition

int

分區

offset

long

偏移量

timestamp

long

時間戳

timestampType

int

類型

●注意:下面的參數是不能被設置的,否則kafka會拋出異常:

  • group.id:kafka的source會在每次query的時候自定創建唯一的group id
  • auto.offset.reset :爲了避免每次手動設置startingoffsets的值,structured streaming在內部消費時會自動管理offset。這樣就能保證訂閱動態的topic時不會丟失數據。startingOffsets在流處理時,只會作用於第一次啓動時,之後的處理都會自動的讀取保存的offset。
  • key.deserializervalue.deserializerkey.serializervalue.serializer 序列化與反序列化,都是ByteArraySerializer

enable.auto.commit:Kafka源不支持提交任何偏移量

整合環境準備

啓動kafka

/export/servers/kafka/bin/kafka-server-start.sh -daemon /export/servers/kafka/config/server.properties

向topic中生產數據

/export/servers/kafka/bin/kafka-console-producer.sh --broker-list node01:9092 --topic  spark_kafka

代碼實現

import org.apache.spark.SparkContext
import org.apache.spark.sql.{DataFrame, Dataset, Row, SparkSession}
import org.apache.spark.sql.streaming.Trigger

object structStreaming_kafka {

  def main(args: Array[String]): Unit = {

    val spark = SparkSession.builder().master("local[*]").appName("").getOrCreate()
    val sc: SparkContext = spark.sparkContext
    sc.setLogLevel("WARN")
    import spark.implicits._
    //讀取實時數據,數據key value)不是字符串
    val kafkaDatas = spark.readStream
      .format("kafka")
      .option("bootstrap.servers", "node01:9092,node02:9092,node03:9092")
      .option("subscribe", "spark_kafka")
      .load()
    //第一個string是kafka中指定的key,第二個String是kafka接收的數據
    val kafkaDatasString: Dataset[(String, String)] = kafkaDatas.selectExpr("CAST(key AS STRING)","CAST(value AS STRING)").as[(String,String)]
    //讀取實時數據  數據類型是Row
    //3.對數據進行處理和計算
    //元組中第二個是接收的數據
    val word: Dataset[String] = kafkaDatasString.flatMap(a=>a._2.split(" "))
    val structWordCount: Dataset[Row] = word.groupBy("value").count().sort($"count")
    //輸出數據
    structWordCount.writeStream
      .trigger(Trigger.ProcessingTime(0))//儘快執行
      .format("console")    //數據輸出到控制檯
      .outputMode("complete")   //輸出所有數據
      .start()    //開始計算
      .awaitTermination() //等待關閉
  }
}

2、整合MySQL

我們開發中經常需要將流的運算結果輸出到外部數據庫,例如MySQL中,但是比較遺憾Structured Streaming API不支持外部數據庫作爲接收器

如果將來加入支持的話,它的API將會非常的簡單比如:

format("jdbc").option("url","jdbc:mysql://...").start()

但是目前我們只能自己自定義一個JdbcSink,繼承ForeachWriter並實現其方法

參考網站

https://databricks.com/blog/2017/04/04/real-time-end-to-end-integration-with-apache-kafka-in-apache-sparks-structured-streaming.html

 

代碼演示

開啓Hadoop集羣

開啓zookeeper

開啓hive --service metastore &

所有節點開啓kafka

/export/servers/kafka/bin/kafka-server-start.sh -daemon /export/servers/kafka/config/server.properties &

創建topic
/export/servers/kafka/bin/kafka-topics.sh --create --zookeeper node01:2181,node02:2181,node03:2181 --replication-factor 3 --partitions 3 --topic spark_kafka

啓動生產者
/export/servers/kafka/bin/kafka-console-producer.sh --broker-list node01:9092 --topic spark_kafka

進入mysql

創建表

 CREATE TABLE `t_word` (
        `id` int(11) NOT NULL AUTO_INCREMENT,
        `word` varchar(255) NOT NULL,
        `count` int(11) DEFAULT NULL,
        PRIMARY KEY (`id`),
        UNIQUE KEY `word` (`word`)
      ) ENGINE=InnoDB AUTO_INCREMENT=26 DEFAULT CHARSET=utf8;
import java.sql.{Connection, DriverManager, PreparedStatement}

import org.apache.spark.SparkContext
import org.apache.spark.sql._
import org.apache.spark.sql.streaming.Trigger


object structStreaming_kafka_mysql {

  def main(args: Array[String]): Unit = {

    val spark = SparkSession.builder()
      .master("local[*]")
      .appName("structStreaming_kafka_mysql")
      .getOrCreate()
    val sc: SparkContext = spark.sparkContext
    sc.setLogLevel("WARN")
    //讀取實時數據,數據key value)不是字符串
    val kafkaDatas = spark.readStream
      .format("kafka")
      .option("kafka.bootstrap.servers","node01:9092,node02:9092,node03:9092")
      .option("subscribe", "spark_kafka")
      .load()
    //第一個string是kafka中指定的key,第二個String是kafka接收的數據
    import spark.implicits._
    val kafkaDatasString: Dataset[(String, String)] =
      kafkaDatas.selectExpr("CAST(key AS STRING)","CAST(value AS STRING)")
        .as[(String,String)]
    //讀取實時數據  數據類型是Row
    //3.對數據進行處理和計算
    //元組中第二個是接收的數據
    val word: Dataset[String] = kafkaDatasString.flatMap(a=>a._2.split(" "))
    val structWordCount: Dataset[Row] = word.groupBy("value").count().sort($"count")

    var intoMysql = new intoMysql("jdbc:mysql://192.168.119.131:3306/bigdata?characterEncoding=UTF-8", "root", "123456")

    //輸出數據
    structWordCount.writeStream
      .trigger(Trigger.ProcessingTime(0))//儘快執行
      .foreach(intoMysql)
//      .format("console")    //數據輸出到控制檯
      .outputMode("complete")   //輸出所有數據
      .start()    //開始計算
      .awaitTermination() //等待關閉
    println("=======")

  }

  class intoMysql(url:String,username:String,passwd:String) extends ForeachWriter[Row] with Serializable{
    //數據庫連接
    var connection:Connection = _
    //設置sql
    var preparedStatement:PreparedStatement = _
    //打開數據庫連接
    override def open(partitionId: Long, version: Long): Boolean = {
      //通過DriverManager (驅動管理)獲得連接
      connection = DriverManager.getConnection(url,username,passwd)
      //連接成功返回true
      true
    }
    //數據吹方法(數據庫中沒有的數據連接插入,已有的數據更新)
    //這裏的value:Row就是前面計算的1 單詞,2 數量
    override def process(value: Row): Unit = {
      //獲取數據中的單詞
      val word = value.get(0).toString
      //獲取數據中的數量
      val count = value.get(1).toString.toInt
      println("word:"+word+"   count:"+count)
      //數據寫入MySQL
      //replace 沒有數據就插入,有數據就更新
//      var sql = "replace into t_word_count (id,word,count)values(Null,?,?)"
      var sql = "replace into t_word_count (id,word,count)values(Null,?,?)"
      preparedStatement = connection.prepareStatement(sql)
      preparedStatement.setString(1,word)
      preparedStatement.setInt(2,count)
      preparedStatement.executeUpdate()
    }

    //數據寫入完畢後關閉連接
    override def close(errorOrNull: Throwable): Unit = {
      if (connection!=null){
        connection.close()
      }
      if (preparedStatement!=null){
        preparedStatement.close()
      }
    }
  }

}

 

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