第7章 Spark Streaming
7.1 流计算概述
7.1.1 静态数据和流数据
很多企业为了支持决策分析而构建的数据仓库系统,其中存放的大量历史数据就是静态数据。技术人员可以利用数据挖掘和OLAP(On-Line Analytical Processing)分析工具从静态数据中找到对企业有价值的信息
近年来,在Web应用、网络监控、传感监测等领域,兴起了一种新的数据密集型应用——流数据,即数据以大量、快速、时变的流形式持续到达
实例:PM2.5检测、电子商务网站用户点击流
流数据具有如下特征:
1数据快速持续到达,潜在大小也许是无穷无尽的
2数据来源众多,格式复杂
3数据量大,但是不十分关注存储,一旦经过处理,要么被丢弃,要么被归档存储
4注重数据的整体价值,不过分关注个别数据
5数据顺序颠倒,或者不完整,系统无法控制将要处理的新到达的数据元素的顺序
7.1.2 批量计算和实时计算
对静态数据和流数据的处理,对应着两种截然不同的计算模式:批量计算和实时计算
批量计算:充裕时间处理静态数据,如Hadoop
流数据不适合采用批量计算,因为流数据不适合用传统的关系模型建模
流数据必须采用实时计算,响应时间为秒级
数据量少时,不是问题,但是,在大数据时代,数据格式复杂、来源众多、数据量巨大,对实时计算提出了很大的挑战。因此,针对流数据的实时计算——流计算,应运而生
7.1.3 流计算概念
流计算:实时获取来自不同数据源的海量数据,经过实时分析处理,获得有价值的信息
流计算秉承一个基本理念,即数据的价值随着时间的流逝而降低,如用户点击流。因此,当事件出现时就应该立即进行处理,而不是缓存起来进行批量处理。为了及时处理流数据,就需要一个低延迟、可扩展、高可靠的处理引擎
对于一个流计算系统来说,它应达到如下需求:
高性能:处理大数据的基本要求,如每秒处理几十万条数据
海量式:支持TB级甚至是PB级的数据规模
实时性:保证较低的延迟时间,达到秒级别,甚至是毫秒级别
分布式:支持大数据的基本架构,必须能够平滑扩展
易用性:能够快速进行开发和部署
可靠性:能可靠地处理流数据
7.1.4 流计算框架
当前业界诞生了许多专门的流数据实时计算系统来满足各自需求
目前有三类常见的流计算框架和平台:商业级的流计算平台、开源流计算框架、公司为支持自身业务开发的流计算框架
商业级:IBM InfoSphere Streams和IBM StreamBase
较为常见的是开源流计算框架,代表如下:
1Twitter Storm:免费、开源的分布式实时计算系统,可简单、高效、可靠地处理大量的流数据
2Yahoo! S4(Simple Scalable Streaming System):开源流计算平台,是通用的、分布式的、可扩展的、分区容错的、可插拔的流式系统
公司为支持自身业务开发的流计算框架:
1Facebook Puma
2Dstream(百度)
3银河流数据处理平台(淘宝)
7.1.5 流计算处理流程
1.概述
传统的数据处理流程,需要先采集数据并存储在关系数据库等数据管理系统中,之后由用户通过查询操作和数据管理系统进行交互
传统的数据处理流程隐含了两个前提:
1存储的数据是旧的。存储的静态数据是过去某一时刻的快照,这些数据在查询时可能已不具备时效性了
2需要用户主动发出查询来获取结果流计算的处理流程一般包含三个阶段:数据实时采集、数据实时计算、实时查询服务
2.数据实时采集
数据实时采集阶段通常采集多个数据源的海量数据,需要保证实时性、低延迟与稳定可靠
以日志数据为例,由于分布式集群的广泛应用,数据分散存储在不同的机器上,因此需要实时汇总来自不同机器上的日志数据
目前有许多互联网公司发布的开源分布式日志采集系统均可满足每秒数百MB的数据采集和传输需求,如:
1Facebook的Scribe
2LinkedIn的Kafka
3淘宝的Time Tunnel
4基于Hadoop的Chukwa和Flume
3.数据实时计算
1数据实时计算阶段对采集的数据进行实时的分析和计算,并反馈实时结果
2经流处理系统处理后的数据,可视情况进行存储,以便之后再进行分析计算。在时效性要求较高的场景中,处理之后的数据也可以直接丢弃
4.实时查询服务
1实时查询服务:经由流计算框架得出的结果可供用户进行实时查询、展示或储存
2传统的数据处理流程,用户需要主动发出查询才能获得想要的结果。而在流处理流程中,实时查询服务可以不断更新结果,并将用户所需的结果实时推送给用户
3虽然通过对传统的数据处理系统进行定时查询,也可以实现不断地更新结果和结果推送,但通过这样的方式获取的结果,仍然是根据过去某一时刻的数据得到的结果,与实时结果有着本质的区别
可见,流处理系统与传统的数据处理系统有如下不同:
1流处理系统处理的是实时的数据,而传统的数据处理系统处理的是预先存储好的静态数据
2用户通过流处理系统获取的是实时结果,而通过传统的数据处理系统,获取的是过去某一时刻的结果
3流处理系统无需用户主动发出查询,实时查询服务可以主动将实时结果推送给用户
7.2 Spark Streaming
7.2.1 Spark Streaming设计
Spark Streaming可整合多种输入数据源,如Kafka、Flume、HDFS,甚至是普通的TCP套接字。经处理后的数据可存储至文件系统、数据库,或显示在仪表盘里
Spark Streaming的基本原理是将实时输入数据流以时间片(秒级)为单位进行拆分,然后经Spark引擎以类似批处理的方式处理每个时间片数据
Spark Streaming最主要的抽象是DStream(Discretized Stream,离散化数据流),表示连续不断的数据流。在内部实现上,Spark Streaming的输入数据按照时间片(如1秒)分成一段一段,每一段数据转换为Spark中的RDD,这些分段就是Dstream,并且对DStream的操作都最终转变为对相应的RDD的操作
7.2.2 Spark Streaming与Storm的对比
1Spark Streaming和Storm最大的区别在于,Spark Streaming无法实现毫秒级的流计算,而Storm可以实现毫秒级响应
2Spark Streaming构建在Spark上,一方面是因为Spark的低延迟执行引擎(100ms+)可以用于实时计算,另一方面,相比于Storm,RDD数据集更容易做高效的容错处理
3Spark Streaming采用的小批量处理的方式使得它可以同时兼容批量和实时数据处理的逻辑和算法,因此,方便了一些需要历史数据和实时数据联合分析的特定应用场合
7.2.3 从“Hadoop+Storm”架构转向Spark架构
图 用Spark架构满足批处理和流处理需求
采用Spark架构具有如下优点:
1实现一键式安装和配置、线程级别的任务监控和告警;
2降低硬件集群、软件维护、任务监控和应用开发的难度;
3便于做成统一的硬件、计算平台资源池。
7.3 DStream操作概述
7.3.1 Spark Streaming工作机制
1在Spark Streaming中,会有一个组件Receiver,作为一个长期运行的task跑在一个Executor上
2每个Receiver都会负责一个input DStream(比如从文件中读取数据的文件流,比如套接字流,或者从Kafka中读取的一个输入流等等)
3Spark Streaming通过input DStream与外部数据源进行连接,读取相关数据
7.3.2 Spark Streaming程序的基本步骤
1.通过创建输入DStream来定义输入源
2.通过对DStream应用转换操作和输出操作来定义流计算
3.用streamingContext.start()来开始接收数据和处理流程
4.通过streamingContext.awaitTermination()方法来等待处理结束(手动结束或因为错误而结束)
5.可以通过streamingContext.stop()来手动结束流计算进程
7.3.3 创建StreamingContext对象
1如果要运行一个Spark Streaming程序,就需要首先生成一个StreamingContext对象,它是Spark Streaming程序的主入口
2可以从一个SparkConf对象创建一个StreamingContext对象
3登录Linux系统后,启动spark-shell。进入spark-shell以后,就已经获得了一个默认的SparkConext,也就是sc。因此,可以采用如下方式来创建StreamingContext对象:
scala> import org.apache.spark.streaming._
scala> val ssc = new StreamingContext(sc, Seconds(1))
如果是编写一个独立的Spark Streaming程序,而不是在spark-shell中运行,则需要通过如下方式创建StreamingContext对象:
import org.apache.spark._
import org.apache.spark.streaming._
val conf = new SparkConf().setAppName("TestDStream").setMaster("local[2]")
val ssc = new StreamingContext(conf, Seconds(1))
7.4 基本输入源
7.4.1 文件流
1.在spark-shell中创建文件流
$ cd /usr/local/spark/mycode
$ mkdir streaming
$ cd streaming
$ mkdir logfile
$ cd logfile
进入spark-shell创建文件流。请另外打开一个终端窗口,启动进入spark-shell
scala> import org.apache.spark.streaming._
scala> val ssc = new StreamingContext(sc, Seconds(20))
scala> val lines = ssc.textFileStream("file:///usr/local/spark/mycode/streaming/logfile")
scala> val words = lines.flatMap(_.split(" "))
scala> val wordCounts = words.map(x => (x, 1)).reduceByKey(_ + _)
scala> wordCounts.print()
scala> ssc.start()
scala> ssc.awaitTermination()
上面在spark-shell中执行的程序,一旦你输入ssc.start()以后,程序就开始自动进入循环监听状态,屏幕上会显示一堆的信息,如下:
//这里省略若干屏幕信息
-------------------------------------------
Time: 1479431100000 ms
-------------------------------------------
//这里省略若干屏幕信息
-------------------------------------------
Time: 1479431120000 ms
-------------------------------------------
//这里省略若干屏幕信息
-------------------------------------------
Time: 1479431140000 ms
-------------------------------------------
在“/usr/local/spark/mycode/streaming/logfile”目录下新建一个log.txt文件,就可以在监听窗口中显示词频统计结果
2. 采用独立应用程序方式创建文件流
$ cd /usr/local/spark/mycode
$ mkdir streaming
$ cd streaming
$ mkdir -p src/main/scala
$ cd src/main/scala
$ vim TestStreaming.scala
用vim编辑器新建一个TestStreaming.scala代码文件,请在里面输入以下代码:
import org.apache.spark._
import org.apache.spark.streaming._
object WordCountStreaming {
def main(args: Array[String]) {
val sparkConf = new SparkConf().setAppName("WordCountStreaming").setMaster("local[2]")//设置为本地运行模式,2个线程,一个监听,另一个处理数据
val ssc = new StreamingContext(sparkConf, Seconds(2))// 时间间隔为2秒
val lines = ssc.textFileStream("file:///usr/local/spark/mycode/streaming/logfile") //这里采用本地文件,当然你也可以采用HDFS文件
val words = lines.flatMap(_.split(" "))
val wordCounts = words.map(x => (x, 1)).reduceByKey(_ + _)
wordCounts.print()
ssc.start()
ssc.awaitTermination()
}
}
$ cd /usr/local/spark/mycode/streaming
$ vim simple.sbt
在simple.sbt文件中输入以下代码:
name := "Simple Project"
version := "1.0"
scalaVersion := "2.11.8"
libraryDependencies += "org.apache.spark" % "spark-streaming_2.11" % "2.1.0"
执行sbt打包编译的命令如下:
$ cd /usr/local/spark/mycode/streaming
$ /usr/local/sbt/sbt package
打包成功以后,就可以输入以下命令启动这个程序:
$ cd /usr/local/spark/mycode/streaming
$ /usr/local/spark/bin/spark-submit --class "WordCountStreaming“ /usr/local/spark/mycode/streaming/target/scala-2.11/simple-project_2.11-1.0.jar
1执行上面命令后,就进入了监听状态(我们把运行这个监听程序的窗口称为监听窗口)
2切换到另外一个Shell窗口,在"/usr/local/spark/mycode/streaming/logfile"目录下再新建一个log2.txt文件,文件里面随便输入一些单词,保存好文件退出vim编辑器
3再次切换回“监听窗口”,等待20秒以后,按键盘Ctrl+C或者Ctrl+D停止监听程序,就可以看到监听窗口的屏幕上会打印出单词统计信息
7.4.2 套接字流
Spark Streaming可以通过Socket端口监听并接收数据,然后进行相应处理
1.Socket工作原理
2.使用套接字流作为数据源
$ cd /usr/local/spark/mycode
$ mkdir streaming #如果已经存在该目录,则不用创建
$ mkdir -p /src/main/scala #如果已经存在该目录,则不用创建
$ cd /usr/local/spark/mycode/streaming/src/main/scala
$ vim NetworkWordCount.scala
请在NetworkWordCount.scala文件中输入如下内容:
package org.apache.spark.examples.streaming
import org.apache.spark._
import org.apache.spark.streaming._
import org.apache.spark.storage.StorageLevel
object NetworkWordCount {
def main(args: Array[String]) {
if (args.length < 2) {
System.err.println("Usage: NetworkWordCount <hostname> <port>")
System.exit(1)
}
StreamingExamples.setStreamingLogLevels()
val sparkConf = new SparkConf().setAppName("NetworkWordCount").setMaster("local[2]")
val ssc = new StreamingContext(sparkConf, Seconds(1))
val lines = ssc.socketTextStream(args(0), args(1).toInt, StorageLevel.MEMORY_AND_DISK_SER)
val words = lines.flatMap(_.split(" "))
val wordCounts = words.map(x => (x, 1)).reduceByKey(_ + _)
wordCounts.print()
ssc.start()
ssc.awaitTermination()
}
}
在相同目录下再新建另外一个代码文件StreamingExamples.scala,文件内容如下:
package org.apache.spark.examples.streaming
import org.apache.spark.internal.Logging
import org.apache.log4j.{Level, Logger}
/** Utility functions for Spark Streaming examples. */
object StreamingExamples extends Logging {
/** Set reasonable logging levels for streaming if the user has not configured log4j. */
def setStreamingLogLevels() {
val log4jInitialized = Logger.getRootLogger.getAllAppenders.hasMoreElements
if (!log4jInitialized) {
// We first log something to initialize Spark's default logging, then we override the
// logging level.
logInfo("Setting log level to [WARN] for streaming example." +
" To override add a custom log4j.properties to the classpath.")
Logger.getRootLogger.setLevel(Level.WARN)
}
}
}
$ cd /usr/local/spark/mycode/streaming/
$ vim simple.sbt
name := "Simple Project"
version := "1.0"
scalaVersion := "2.11.8"
libraryDependencies += "org.apache.spark" % "spark-streaming_2.11" % "2.1.0"
$ cd /usr/local/spark/mycode/streaming
$ /usr/local/sbt/sbt package
$ cd /usr/local/spark/mycode/streaming
$ /usr/local/spark/bin/spark-submit --class " org.apache.spark.examples.streaming.NetworkWordCount" /usr/local/spark/mycode/streaming/target/scala-2.11/simple-project_2.11-1.0.jar localhost 9999
新打开一个窗口作为nc窗口,启动nc程序:
$ nc -lk 9999
可以在nc窗口中随意输入一些单词,监听窗口就会自动获得单词数据流信息,在监听窗口每隔1秒就会打印出词频统计信息,大概会在屏幕上出现类似如下的结果:
-------------------------------------------
Time: 1479431100000 ms
-------------------------------------------
(hello,1)
(world,1)
-------------------------------------------
Time: 1479431120000 ms
-------------------------------------------
(hadoop,1)
-------------------------------------------
Time: 1479431140000 ms
-------------------------------------------
(spark,1)
3.使用Socket编程实现自定义数据源
下面我们再前进一步,把数据源头的产生方式修改一下,不要使用nc程序,而是采用自己编写的程序产生Socket数据源
$ cd /usr/local/spark/mycode/streaming/src/main/scala
$ vim DataSourceSocket.scala
package org.apache.spark.examples.streaming
Import java.io.{PrintWriter}
Import java.net.ServerSocket
Import scala.io.Source
object DataSourceSocket {
def index(length: Int) = {
val rdm = new java.util.Random
rdm.nextInt(length)
}
def main(args: Array[String]) {
if (args.length != 3) {
System.err.println("Usage: <filename> <port> <millisecond>")
System.exit(1)
}
val fileName = args(0)
val lines = Source.fromFile(fileName).getLines.toList
val rowCount = lines.length
val listener = new ServerSocket(args(1).toInt)
while (true) {
val socket = listener.accept()
new Thread() {
override def run = {
println("Got client connected from: " + socket.getInetAddress)
val out = new PrintWriter(socket.getOutputStream(), true)
while (true) {
Thread.sleep(args(2).toLong)
val content = lines(index(rowCount))
println(content)
out.write(content + '\n')
out.flush()
}
socket.close()
}
}.start()
}
}
}
执行sbt打包编译:
$ cd /usr/local/spark/mycode/streaming
$ /usr/local/sbt/sbt package
DataSourceSocket程序需要把一个文本文件作为输入参数,所以,在启动这个程序之前,需要首先创建一个文本文件word.txt并随便输入几行内容:
/usr/local/spark/mycode/streaming/word.txt
启动DataSourceSocket程序:
$ /usr/local/spark/bin/spark-submit \
> --class "org.apache.spark.examples.streaming.DataSourceSocket" \
>/usr/local/spark/mycode/streaming/target/scala-2.11/simple-project_2.11-1.0.jar \
> /usr/local/spark/mycode/streaming/word.txt 9999 1000
这个窗口会不断打印出一些随机读取到的文本信息,这些信息也是Socket数据源,会被监听程序捕捉到
在另外一个窗口启动监听程序:
$ /usr/local/spark/bin/spark-submit --class "org.apache.spark.examples.streaming.NetworkWordCount" /usr/local/spark/mycode/streaming/target/scala-2.11/simple-project_2.11-1.0.jar localhost 9999
启动成功后,你就会看到,屏幕上不断打印出词频统计信息
7.4.3 RDD队列流
在调试Spark Streaming应用程序的时候,我们可以使用streamingContext.queueStream(queueOfRDD)创建基于RDD队列的DStream
新建一个TestRDDQueueStream.scala代码文件,功能是:每隔1秒创建一个RDD,Streaming每隔2秒就对数据进行处理
package org.apache.spark.examples.streaming
import org.apache.spark.SparkConf
import org.apache.spark.rdd.RDD
import org.apache.spark.streaming.StreamingContext._
import org.apache.spark.streaming.{Seconds, StreamingContext}
object QueueStream {
def main(args: Array[String]) {
val sparkConf = new SparkConf().setAppName("TestRDDQueue").setMaster("local[2]")
val ssc = new StreamingContext(sparkConf, Seconds(2))
val rddQueue =new scala.collection.mutable.SynchronizedQueue[RDD[Int]]()
val queueStream = ssc.queueStream(rddQueue)
val mappedStream = queueStream.map(r => (r % 10, 1))
val reducedStream = mappedStream.reduceByKey(_ + _)
reducedStream.print()
ssc.start()
for (i <- 1 to 10){
rddQueue += ssc.sparkContext.makeRDD(1 to 100,2)
Thread.sleep(1000)
}
ssc.stop()
}
}
sbt打包成功后,执行下面命令运行程序:
$ cd /usr/local/spark/mycode/streaming
$ /usr/local/spark/bin/spark-submit \
>--class "org.apache.spark.examples.streaming.QueueStream" \
>/usr/local/spark/mycode/streaming/target/scala-2.11/simple-project_2.11-1.0.jar
执行上面命令以后,程序就开始运行,就可以看到类似下面的结果:
-------------------------------------------
Time: 1479522100000 ms
-------------------------------------------
(4,10)
(0,10)
(6,10)
(8,10)
(2,10)
(1,10)
(3,10)
(7,10)
(9,10)
(5,10)
7.5 高级数据源
7.5.1 Kafka简介
Kafka是一种高吞吐量的分布式发布订阅消息系统,用户通过Kafka系统可以发布大量的消息,同时也能实时订阅消费消息
Kafka可以同时满足在线实时处理和批量离线处理
在公司的大数据生态系统中,可以把Kafka作为数据交换枢纽,不同类型的分布式系统(关系数据库、NoSQL数据库、流处理系统、批处理系统等),可以统一接入到Kafka,实现和Hadoop各个组件之间的不同类型数据的实时高效交换
7.5.2 Kafka准备工作
1.安装Kafka
关于Kafka的安装方法,请参考厦门大学数据库实验室建设的高校大数据课程公共服务平台的技术博客文章《Kafka的安装和简单实例测试》
http://dblab.xmu.edu.cn/blog/1096-2/ 这里假设已经成功安装Kafka到“/usr/local/kafka”目录下
2.启动Kafka
说明:本课程下载的安装文件为Kafka_2.11-0.10.2.0.tgz,前面的2.11就是该Kafka所支持的Scala版本号,后面的0.10.2.0是Kafka自身的版本号
打开一个终端,输入下面命令启动Zookeeper服务:
$ cd /usr/local/kafka
$ ./bin/zookeeper-server-start.sh config/zookeeper.properties
千万不要关闭这个终端窗口,一旦关闭,Zookeeper服务就停止了
打开第二个终端,然后输入下面命令启动Kafka服务:
$ cd /usr/local/kafka
$ bin/kafka-server-start.sh config/server.properties
千万不要关闭这个终端窗口,一旦关闭,Kafka服务就停止了
3.测试Kafka是否正常工作
再打开第三个终端,然后输入下面命令创建一个自定义名称为“wordsendertest”的Topic:
$ cd /usr/local/kafka
$ ./bin/kafka-topics.sh --create --zookeeper localhost:2181 \
>--replication-factor 1 --partitions 1 --topic wordsendertest
>$ ./bin/kafka-topics.sh --list --zookeeper localhost:2181
下面用生产者(Producer)来产生一些数据,请在当前终端内继续输入下面命令:
$ ./bin/kafka-console-producer.sh --broker-list localhost:9092 \
> --topic wordsendertest
上面命令执行后,就可以在当前终端内用键盘输入一些英文单词,比如可以输入:
hello hadoop
hello spark
现在可以启动一个消费者,来查看刚才生产者产生的数据。请另外打开第四个终端,输入下面命令:
$ cd /usr/local/kafka
$./bin/kafka-console-consumer.sh --zookeeper localhost:2181 \
> --topic wordsendertest --from-beginning
可以看到,屏幕上会显示出如下结果,也就是刚才在另外一个终端里面输入的内容:
hello hadoop
hello spark
7.5.3 Spark准备工作
1.添加相关jar包
Kafka和Flume等高级输入源,需要依赖独立的库(jar文件)
在spark-shell中执行下面import语句进行测试:
scala> import org.apache.spark.streaming.kafka._
<console>:25: error: object kafka is not a member of package org.apache.spark.streaming
import org.apache.spark.streaming.kafka._
^
对于Spark2.1.0版本,如果要使用Kafka,则需要下载spark-streaming-kafka-0-8_2.11相关jar包,下载地址:
http://mvnrepository.com/artifact/org.apache.spark/spark-streaming-kafka-0-8_2.11/2.1.0
把jar文件复制到Spark目录的jars目录下
2.启动spark-shell
7.5.4 编写Spark Streaming程序使用Kafka数据源
7.6 转换操作
7.7 输出操作
7.8 Structured Streaming
第8章 Spark MLlib